# 权限字段映射与 SQL 改造规范 > 适用范围:`leaudit-platform` 后端权限改造中的查询语句、资源详情、导出下载、统计聚合、管理列表 > 文档定位:为统一数据范围执行器的接入提供字段映射标准、SQL 拼接规范、资源回溯规范和反模式约束。 --- ## 1. 文档目标 这份文档解决的是一个非常具体的问题: 统一数据范围执行器设计出来以后,后端各模块到底怎么把它安全、稳定地接到现有 SQL 上。 当前项目最大风险不是“不会写 scope”,而是: 1. 同一个模块里不同查询使用了不同字段口径 2. 列表、详情、下载、导出没有复用同一主资源边界 3. 各 service 自己拼 `1 = 0`、`region`、`created_by`,可维护性很差 4. 多表 join 时,究竟该按 `u.area` 还是 `d.region`,目前没有统一规范 所以这份文档的目标是: 1. 定义统一字段映射标准 2. 定义统一 alias 规范 3. 定义统一 scope 子句拼接方式 4. 定义详情、下载、导出等派生资源的“主资源回溯”规则 5. 定义改造时哪些 SQL 能保留,哪些必须重构 --- ## 2. 当前代码里已经暴露出的典型问题 结合现有实现,已经确认有以下模式: - `documentServiceImpl` / `govdocServiceImpl` - 通过 `d.region` + `f.created_by` 控制范围 - `usageStatsServiceImpl` - 有时按 `u.area` - 有时按 `d.region` - 有时还要按 `e.area_snapshot` - `contractTemplateServiceImpl` - 既有 `t.region` - 又有 `t.created_by` - 还混入“省级模板可见” - `rbacAdminServiceImpl` - 用户列表、组织树本质上按 `u.area` 这说明项目不是“没有规则”,而是规则散了。 --- ## 3. 核心原则 ## 3.1 先确定主资源,再确定 scope 字段 SQL 改造第一步不是拼条件,而是先回答: - 这个接口的主资源是谁? 例如: - 文档列表:主资源是 `document` - 文档状态:主资源仍是 `document` - 公文报告下载:主资源不是 `report_artifact`,而是 `govdoc document` - RAG 文档分段列表:主资源不是 `segment`,而是 `dataset` - 交叉评查提案导出:主资源不是 `proposal`,而是“用户参与的任务文档” 只有先确定主资源,scope 才不会跑偏。 ## 3.2 范围条件必须挂在主资源口径上 同一个接口里如果出现多表: - `document d` - `document_file f` - `sso_users u` 必须先规定: - area 看哪个表 - self 看哪个表 不能在同一个模块里一会儿 `u.area`,一会儿 `d.region`,又没有解释。 ## 3.3 列表、详情、删除、下载、导出必须继承同一套 scope 这条必须强制执行。 不允许出现: - 列表按文档范围过滤 - 详情直接按 `id` - 下载直接按附件 `id` - 导出直接按 run `id` 凡是派生资源,都必须回溯主资源。 --- ## 4. 统一字段映射标准 ## 4.1 标准字段分类 建议统一按 5 类字段管理: 1. `area_field` 2. `creator_field` 3. `owner_field` 4. `user_field` 5. `public_field` 建议结构: ```python @dataclass class ScopeFieldMapping: area_field: str | None = None creator_field: str | None = None owner_field: str | None = None user_field: str | None = None public_field: str | None = None ``` ## 4.2 字段语义定义 ### `area_field` 表示资源的地区归属字段。 例如: - `d.region` - `t.region` - `dataset.area` - `app.area` - `u.area` ### `creator_field` 表示“创建该资源的用户”。 例如: - `f.created_by` - `t.created_by` - `proposal.created_by` ### `owner_field` 表示“资源所有者”,和创建者不一定相同。 如果当前项目没有成熟 owner 模型,可以先不启用。 ### `user_field` 用于用户本身或用户快照类数据。 例如: - `u.id` - `e.user_id` ### `public_field` 表示是否公共可见。 例如: - `dataset.is_public` - `app.is_public` --- ## 5. 各模块字段映射表 ## 5.1 文档模块 主资源:`leaudit_documents d` 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `d.region` | | `creator_field` | `f.created_by` | | `owner_field` | 暂无 | | `user_field` | `f.created_by` | 说明: - 当前代码事实口径就是 `d.region + f.created_by` - 文档的“自己数据”不建议改成 `d.created_by`,因为现有实现明显依赖文件记录 ## 5.2 公文模块 主资源:`govdoc document / leaudit_documents d` 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `d.region` | | `creator_field` | `f.created_by` | | `user_field` | `f.created_by` | 说明: - 公文 run、报告、原文都必须回溯到 `d.region` 和 `f.created_by` ## 5.3 使用统计模块 这块必须拆成两个口径。 ### 用户口径统计 主资源:`sso_users u` 或 `usage_login_events e` 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `u.area` 或 `e.area_snapshot` | | `user_field` | `u.id` 或 `e.user_id` | ### 文档口径统计 主资源:文档上传/评查事件 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `d.region` | | `creator_field` | `f.created_by` | | `user_field` | `f.created_by` | 说明: - `areaScope=user` 和 `areaScope=document` 本质上是两套字段映射切换 - 不能在一个 builder 里靠字符串替换硬凑 ## 5.4 合同模板模块 主资源:`contract_template t` 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `t.region` | | `creator_field` | `t.created_by` | | `public_field` | 暂无;如后续有省级公共模板逻辑,应单独建模 | 说明: - 当前模块有“省级模板 + 本地区模板”可见语义 - 这不是 `is_public` - 应在 `ContractTemplatePolicy` 中定义成“系统公共范围”,不要混同布尔公开字段 ## 5.5 RBAC 用户管理 主资源:`sso_users u` 推荐映射: | 类型 | 字段 | | --- | --- | | `area_field` | `u.area` | | `user_field` | `u.id` | 说明: - 角色对象本身不按 area - 但“查看哪个用户、给谁分配角色”按 `u.area` ## 5.6 RAG 模块 ### 数据集 主资源:`dataset` | 类型 | 字段 | | --- | --- | | `area_field` | `dataset.area` | | `creator_field` | `dataset.created_by` | | `public_field` | `dataset.is_public` | ### 聊天应用 主资源:`app` | 类型 | 字段 | | --- | --- | | `area_field` | `app.area` | | `public_field` | `dataset.is_public` 或显式 `app.is_public` | 说明: - 如果 app 的公开性来自关联 dataset,就要在 policy 层明确写清,不要分散在查询里隐式推断 ## 5.7 交叉评查 主资源:任务关系,而不是单表字段。 推荐映射: | 类型 | 字段 | | --- | --- | | `user_field` | `tm.user_id` | | `creator_field` | `proposal.created_by` | 说明: - 交叉评查以 `RELATION` 模型处理 - 这块不强制要求 `area_field` --- ## 6. Alias 统一规范 统一执行器要稳定接入,SQL alias 必须统一。 建议标准: | 资源 | 推荐 alias | | --- | --- | | 文档主表 | `d` | | 文档文件表 | `f` | | 用户表 | `u` | | 审查运行表 | `r` | | 统计登录事件表 | `e` | | 合同模板表 | `t` | | RAG 数据集 | `dataset` | | RAG 应用 | `app` | | 交叉评查任务 | `task` | | 交叉评查成员 | `tm` | | 交叉评查提案 | `proposal` | 目的不是强迫重命名所有 SQL,而是: - 新增或重构 SQL 时尽量统一 - `QueryScopeBuilder` 和 `ModulePolicy` 才能更容易复用 --- ## 7. 参数命名规范 当前代码里已经有: - `requested_region` - `scope_region` - `scope_user_id` - `requested_user_id` 建议固化为统一标准: | 参数 | 含义 | | --- | --- | | `requested_area` | 请求方传入的地区过滤 | | `scope_area` | 当前 scope 决策后的有效地区 | | `requested_user_id` | 请求方显式筛选的用户 | | `scope_user_id` | scope 决策后的当前用户 ID | | `resource_id` | 主资源 ID | | `visible_areas` | `PUBLIC_MIXED` 或公共范围列表 | 不建议再混用: - `query_area` - `user_area` - `region` - `scope_region` 除非当前模块确实已经固定使用 `region` 作为数据库字段名,而不是参数名。 建议规则: - 参数命名一律使用 `area` - 数据库字段保留 `region/area` 原名 即: ```sql COALESCE(d.region, '') = :scope_area ``` 而不是: ```sql COALESCE(d.region, '') = :scope_region ``` 这样更利于统一执行器复用。 --- ## 8. 标准 SQL 子句模板 ## 8.1 `ALL` 无地区限制时: ```sql 1 = 1 ``` 带请求地区过滤时: ```sql COALESCE({area_field}, '') = :requested_area ``` ## 8.2 `DEPT` ```sql COALESCE({area_field}, '') = :scope_area ``` 并且在进入 SQL 前先做规则: - 若 `requested_area` 非空且不等于 `scope_area`,直接拒绝或生成 `1 = 0` ## 8.3 `SELF` ```sql {creator_field} = :scope_user_id ``` 若请求还传了 `requested_user_id`: - 与 `scope_user_id` 不相等时直接拒绝 ## 8.4 `PUBLIC_MIXED` ```sql ( COALESCE({area_field}, '') IN :visible_areas OR {public_field} = TRUE ) ``` 推荐 `visible_areas` 至少包含: - 当前用户地区 - `省级` - `''` 具体由 `RagPolicy` 决定。 ## 8.5 `RELATION` 不建议硬编码为通用模板。 应由 `CrossReviewPolicy` 输出,例如: ```sql EXISTS ( SELECT 1 FROM leaudit_cross_review_task_member tm WHERE tm.task_id = task.id AND tm.user_id = :scope_user_id AND tm.deleted_at IS NULL ) ``` --- ## 9. 主资源回溯规范 这是本轮 SQL 改造里最重要的一条。 ## 9.1 什么叫主资源回溯 当接口操作的不是主资源表,而是主资源衍生物时,必须回溯到主资源做 scope 判断。 ## 9.2 必须回溯的场景 ### 文档状态 虽然接口直接拿文档 ID 列表查状态,但本质仍是文档资源。 必须回溯: - `document` - `file` ### 公文 run / report / original 不能只按: - `run_id` - `artifact_id` - `document_id` 直接查。 必须回溯到: - `govdoc document d` - `original file f` ### RAG dataset document / segment 不能只按: - `document_id` - `segment_id` 直接做可见性判断。 必须回溯到: - `dataset` ### 交叉评查 proposal export 不能只按 `DocumentId` 导出。 必须回溯到: - 当前用户是否属于该任务关系链 --- ## 10. 推荐接入方式 ## 10.1 列表接口 推荐模式: 1. 构造基础查询 2. 获取 `PermissionDecision` 3. 由 `QueryScopeBuilder` 生成 `scope_clause` 4. 将 `scope_clause.sql` 注入 `WHERE` 5. 业务筛选条件作为附加条件继续追加 示意: ```python decision = await permissionScopeFacade.require(...) scope_clause = queryScopeBuilder.build_by_mapping(...) where_clauses = [ "d.deleted_at IS NULL", scope_clause.sql, ] params = { **scope_clause.params, } ``` ## 10.2 详情接口 推荐模式: 不要先查,再判断。 而要直接: ```sql SELECT ... FROM ... WHERE id = :resource_id AND {scope_clause.sql} ``` 这样天然避免“查到了但后面忘记拒绝”的风险。 ## 10.3 删除/更新接口 推荐两种方式: ### 方式 A:先用带 scope 的 SELECT 锁定资源 ```sql SELECT id FROM ... WHERE id = :resource_id AND {scope_clause.sql} FOR UPDATE ``` ### 方式 B:直接带 scope 执行 UPDATE/DELETE ```sql UPDATE ... SET ... WHERE id = :resource_id AND {scope_clause.sql} ``` 建议优先用方式 A,更利于错误提示和审计。 ## 10.4 下载/导出接口 推荐模式: 1. 先回溯主资源并带 scope 查出合法记录 2. 再根据合法记录生成下载地址或导出文件 不允许: 1. 先按附件 ID 或 artifact ID 取到 OSS URL 2. 再事后补判断 --- ## 11. 统计聚合改造规范 统计类 SQL 最容易出错,因为不是简单查明细。 ## 11.1 先收 scope,再聚合 推荐: ```sql WITH scoped_docs AS ( SELECT d.id, d.region, f.created_by FROM ... WHERE ... AND {scope_clause.sql} ) SELECT ... FROM scoped_docs GROUP BY ... ``` 不推荐: 先全量聚合,再在外层做地区过滤。 原因: - 易错 - 性能差 - `SELF` 范围很容易被漏掉 ## 11.2 用户口径和文档口径必须拆分 不要写一个函数同时隐式支持: - `u.area` - `d.region` - `e.area_snapshot` 建议明确: - `build_user_area_scope_clause()` - `build_document_area_scope_clause()` - `build_login_snapshot_scope_clause()` 或者在 `UsageStatsPolicy` 内按口径分发。 --- ## 12. 反模式清单 以下写法在本轮改造中应视为禁用或逐步清理对象。 ## 12.1 反模式:角色名直接拼 SQL 范围 ```sql bool_or(r.role_key IN ('super_admin', 'provincial_admin')) ``` 问题: - 角色名直接耦合能力 - 新角色无法扩展 ## 12.2 反模式:详情先查后判 ```python row = await session.execute(...) if row and current_user["can_manage"]: ... ``` 问题: - 非法资源已经被读出来 - 派生接口最容易漏判 ## 12.3 反模式:用 `1 = 0` 到处散落表达拒绝 `1 = 0` 可以作为最终 SQL 结果,但不应成为业务逻辑表达方式。 更推荐: - 在决策层直接拒绝 - 或由 builder 明确返回 `denied clause` ## 12.4 反模式:同一模块 area 字段口径漂移 例如: - 文档列表按 `u.area` - 文档详情按 `d.region` 这种写法必须统一。 ## 12.5 反模式:下载 URL 先取后判 尤其是: - 原文下载 - 报告下载 - 导出文件 必须先判 scope,再产出 URL。 --- ## 13. 索引建议 统一执行器接入后,范围过滤会更集中,需要补齐索引。 建议优先确认以下索引: ### 文档/公文 - `documents(region)` - `document_files(created_by)` - `document_files(document_id, created_by)` ### 用户 - `sso_users(area)` - `sso_users(status, deleted_at, area)` ### 合同模板 - `contract_templates(region)` - `contract_templates(created_by)` ### RAG - `rag_datasets(area, is_public)` - `rag_apps(area)` ### 交叉评查 - `task_member(task_id, user_id)` - `proposal(document_id, created_by)` 说明: - 索引不是这轮文档的主体,但如果没有,会直接影响统一执行器上线后的查询性能 --- ## 14. 推荐代码组织 建议新增或统一下面这类方法: ```python build_scope_clause_for_document(...) build_scope_clause_for_user(...) build_scope_clause_for_dataset(...) resolve_primary_resource_scope(...) ``` 更推荐的最终形态是: ```python decision = await permissionDecisionService.decide(ctx) scope_clause = modulePolicy.build_clause(ctx, decision) ``` 这样业务 service 只负责: 1. 自身业务查询 2. 接入 scope 子句 3. 做结果组装 而不是自己解释权限模型。 --- ## 15. 分模块改造建议 ## 15.1 文档与公文 优先动作: 1. 抽出统一 `DocumentScopeMapping` 2. 统一 `d.region + f.created_by` 3. 所有详情/状态/run/report/download 统一回溯主文档 ## 15.2 使用统计 优先动作: 1. 拆分用户口径和文档口径 2. 不再通过字符串替换实现 `area_snapshot` 3. 将 `SELF` 范围明确化 ## 15.3 合同模板 优先动作: 1. 把“省级模板可见”收敛为 policy 2. 列表/搜索/详情共享同一范围规则 ## 15.4 RBAC 用户管理 优先动作: 1. 用户列表、组织树、角色分配统一按 `u.area` 2. 角色元数据接口不强加 area scope ## 15.5 RAG 优先动作: 1. `PUBLIC_MIXED` 统一化 2. dataset 子资源全部回溯 dataset --- ## 16. 最终要求 后续所有权限改造相关 SQL,应满足以下要求: 1. 能明确说清主资源是谁 2. 能明确说清 `area_field` 和 `creator_field` 是哪个 3. 能明确说清详情/下载/导出是否回溯主资源 4. 不再依赖角色名派生范围 5. 新增接口时可以直接挂到统一执行器,而不是复制旧判断 如果做不到这 5 条,即使逻辑暂时跑通,也不算完成“统一权限架构”的 SQL 层落地。