前面几篇一直在讲一个很早期的问题:Spring JPA / Hibernate 项目里,复杂列表、报表、导出这些查询,经常需要写动态 SQL。
一开始我们关心的是 SQL 怎么别写成一堆 Java 字符串拼接。后来发现,AI 时代下,LLM 很适合生成这种 SQL 模板;再往前走一步,就会遇到另一个更实际的问题:权限条件到底写在哪里。
在企业系统里,很多查询不是简单的"按页面参数筛选"。
它通常还要带上这些东西:
- 当前用户是谁;
- 当前租户或公司是什么;
- 用户能看哪些团队、部门、仓库;
- 是否只能看自己创建的数据;
- 是否需要叠加某些业务状态或组织边界。
如果每个查询都自己写一遍,这些条件很快就会散掉。
比如一个订单列表里写:
java
if (!currentUser.isAdmin()) {
sql.append(" and o.team_id in (");
sql.append(teamIds.stream().map(x -> "?").collect(joining(",")));
sql.append(")");
args.addAll(teamIds);
}
另一个导出接口里写:
java
if (userContext.hasTenant()) {
sql.append(" and tenant_id = ?");
args.add(userContext.getTenantId());
}
这些代码单看都能理解,但久了以后会出现几个问题。
第一,权限条件和业务筛选混在一起。开发者很难一眼看出哪些是页面条件,哪些是系统必须带上的数据范围。
第二,不同查询的写法会不一致。有的地方用了租户,有的地方忘了租户;有的地方按团队,有的地方按创建人;有的地方空团队列表返回全部,有的地方返回空结果。
第三,AI 帮忙改查询时,也很容易漏。LLM 看到的是"订单列表增加一个状态筛选",它未必知道旁边那段团队范围其实不能动。
所以到了 SQL 模板阶段,我们很自然会想:权限上下文能不能也从 Spring 里接进来,让模板里只保留清晰的调用点?
比如:
javascript
import {getCurrentUser, getDataScope} from '@authService';
const user = getCurrentUser();
const scope = getDataScope('order');
export const sql = `
SELECT
o.order_id,
o.order_no,
o.amount,
o.status,
o.create_time
FROM orders o
WHERE o.deleted = 0
${sqlExp(user.tenantId, 'AND o.tenant_id = ?', true)}
${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}
${sqlExp(form.param.status, 'AND o.status = ?')}
${sqlExp(toLikeStr(form.param.keyword), 'AND o.order_no LIKE ?')}
ORDER BY o.create_time DESC
`;
这里重点不是 authService 这个名字,而是思路:权限上下文由 Spring Bean 提供,模板只负责把它放到查询里。
这样做有几个好处。
第一个好处是,权限条件有了明确入口。
代码评审时,一眼能看到:
javascript
const user = getCurrentUser();
const scope = getDataScope('order');
以及:
javascript
${sqlExp(user.tenantId, 'AND o.tenant_id = ?', true)}
${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}
这比在 Java 业务代码里翻一堆 if、append、args.add 更直接。
第二个好处是,权限逻辑可以集中。
比如"订单数据范围"到底是团队、部门、仓库,还是"自己 + 下属",这个判断不应该分散在每个 SQL 模板里。模板应该拿到一个已经整理好的 scope,再用固定 helper 注入查询。
真正复杂的权限判断仍然留在 Java 服务里:
java
public DataScope getDataScope(String resource) {
// 根据当前用户、角色、组织、业务资源计算可访问范围
}
模板只处理 SQL 表达:
javascript
${sqlInExp(scope.teamIds, 'AND o.team_id IN ', true)}
这就把"权限怎么计算"和"SQL 怎么表达"分开了。
第三个好处是,AI 更不容易误改。
如果提示词里明确说:
text
权限条件来自 getCurrentUser() 和 getDataScope(resource)。
这些条件必须保留,不要删除或改成页面参数。
新增页面筛选时,只能追加 form.param 相关条件。
LLM 生成或修改模板时,就有比较清晰的边界。它可以帮忙加状态、关键字、时间范围,但不应该随手删掉租户和数据范围。
这对人工审查也有帮助。审查者不用猜"这段 team_id 是不是权限条件",因为权限相关调用已经有稳定形态。
当然,这一步仍然不是完整权限治理。
它不能保证所有模型都有正确权限,也不能自动解决复杂的多角色、多组织、多公司规则。更不能说"有了模板注入,就可以让 AI 随便查库"。
它解决的是一个更早、更朴素的问题:在 Spring 企业项目里,权限条件不要散落在每个 Repository、Service、JdbcTemplate 拼接分支里。
先让当前用户、租户、团队范围这些上下文有统一入口,再让 SQL 模板以固定方式引用它们。
这样一来,动态 SQL 模板不只是"写起来更舒服",也开始有了一点工程边界:
- 页面筛选来自
form.param; - 权限上下文来自 Spring Bean;
- SQL 参数通过 helper 收集;
- 必带条件用
force明确表达; - 调试时能看到最终 SQL 和参数。
这一步仍然很小,但它已经开始把复杂查询从"谁写谁负责"往"团队有规则可遵守"推进。
对 AI 辅助开发来说,这个变化也很关键。LLM 不需要理解你们公司完整的权限系统,但它需要知道:有些条件不是它自由发挥的业务筛选,而是必须保留的访问边界。
相关代码:foggy-dataset Java 引擎模块
相关文档:FSScript SQL helper functions