# 权限字段映射与 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 层落地。
