# 新平台主链路租户边界扫描报告

> 扫描日期：2026-05-21  
> 范围：新平台主链路  
> 不含：旧 `evaluation_points` 兼容链路、旧 `rules/list`、`rules/new`

---

## 1. 总结论

当前新平台主链路已经不是“完全没租户化”，而是进入了一个更危险的阶段：

- 文档主表已经开始写 `tenant_code`
- 首页入口模块已经开始按租户过滤
- 但评查组/规则集/前端上传作用域仍没有完整切到 `tenant_code`

这意味着系统现在处于一种“半租户化”状态：

1. 一部分链路按租户工作
2. 一部分链路仍按 `region/area/document_type/entry_module_id`
3. 还有一部分链路默认假设主数据是全局共享

真正的风险不是“功能不能用”，而是：

- 会把数据落错边界
- 会把旧脏数据和新租户数据串到同一条版本链
- 会在多租户复用同入口模块/同文档类型时，把用户带到错误的业务树或规则树

---

## 2. 三个最危险的核心问题

## 2.1 评查组/规则集主数据仍然是全局共享模型

当前：

- `leaudit_evaluation_point_groups` 没有 `tenant_code`
- `leaudit_rule_group_bindings` 没有 `tenant_code`
- `leaudit_rule_sets` / `rule_type` 仍按全局唯一思路工作

结果：

- 不同租户可能共享同一套分组树
- 不同租户可能共享同一个 `rule_type -> rule_set`
- 一个租户新建规则版本，可能覆盖另一个租户当前使用的规则资产

这不是“显示错了”，而是规则资产归属模型本身还没分租户。

## 2.2 文档链路仍存在 `tenant_code + region` 混查

当前：

- 文档上传虽然开始写 `tenant_code`
- 但历史版本命中、列表过滤、详情过滤仍允许 `tenant_code` 不命中时回退 `region`

结果：

- 同地区旧脏数据可能被续成新租户文档的新版本
- 列表和详情在历史数据上可能出现租户边界漂移
- 只要旧数据 `tenant_code` 为空，系统就会继续依赖中文展示名做范围匹配

这是当前最容易造成“查对了表面、查错了真实归属”的问题。

## 2.3 前端上传和页面作用域仍未显式传递 `tenant_code`

当前：

- 上传页提交时主要传 `region`
- 首页跳转主要传 `entryModuleId + documentTypeIds`
- 子类型分组查询只按 `document_type_id`
- 文档列表读范围也主要依赖 URL/sessionStorage 中的模块/文档类型

结果：

- 当前租户上下文在前端不是一等主语义
- 很多页面只能“假设后端已经帮你隔离好了”
- 一旦后端返回混合数据，前端没有第二道确认防线

---

## 3. 高风险问题清单

## 3.1 评查组/规则集后端高风险

### 3.1.1 `RuleController` 基本是裸接口

文件：

- [ruleController.py](/home/wren-dev/Porject/leaudit-platform/fastapi_modules/fastapi_leaudit/controllers/ruleController.py)

问题：

- 缺少统一 `verify_access_token`
- 缺少功能权限校验
- 也没有数据范围/租户上下文透传

影响：

- 规则集列表、版本内容、发布、回滚、绑定增删改都可能绕过租户边界

### 3.1.2 `CreateVersion` 仍按全局 `rule_type` 工作

文件：

- [ruleServiceImpl.py](/home/wren-dev/Porject/leaudit-platform/fastapi_modules/fastapi_leaudit/services/impl/ruleServiceImpl.py)

问题：

- 规则版本创建仍按全局 `rule_type` 命中 `rule_set`
- 没有 `tenant_code`

影响：

- 不同租户只要规则类型编码一致，就可能共用同一个规则集和版本链

### 3.1.3 评查组树所有读写都没有租户谓词

文件：

- [evaluationPointGroupServiceImpl.py](/home/wren-dev/Porject/leaudit-platform/fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py)

问题：

- `ListGroups`
- `ListAllGroups`
- `GetGroup`
- `GetChildren`
- `ListGroupsByDocumentTypes`
- `CreateGroup`
- `UpdateGroup`
- `DeleteGroup`
- `BatchDelete`
- `RebindGroup`
- `CreateBinding`
- `UpdateBinding`
- `DeleteBinding`

以上全部没有显式 `tenant_code` 约束。

影响：

- 管理页看到的可能是跨租户混合分组
- 修改和删除可能直接作用到其他租户的业务树

### 3.1.4 `RebindGroup` 是最高危错挂点

问题：

- 直接按 `pid` 批量迁移子分组
- 只校验层级，不校验租户一致性

影响：

- 一次操作可能把多个二级子类型整体迁入错误根分组

### 3.1.5 分组/绑定表结构本身没有 `tenant_code`

文件：

- [ruleGroupSupport.py](/home/wren-dev/Porject/leaudit-platform/fastapi_modules/fastapi_leaudit/services/impl/ruleGroupSupport.py)

问题：

- `ensure_rule_group_schema` 建表时就没有租户字段

影响：

- 即使服务层开始传租户，也没有稳定持久化载体

## 3.2 文档后端主链路高风险

### 3.2.1 历史版本命中仍会 `tenant_code` 回退 `region`

文件：

- [documentServiceImpl.py](/home/wren-dev/Porject/leaudit-platform/fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py)

问题：

- `_find_latest_version_candidate`
- `_document_tenant_filter_sql`
- `_buildDocumentScopeFilters`

都还允许 `tenant_code` 不足时回退 `region`

影响：

- 新租户文档可能接到旧地区文档的历史版本链

### 3.2.2 `group_id / entry_module_id` 仍在查询时反推

问题：

- 列表和详情展示并不完全依赖文档上传时的稳定快照
- 而是会根据当前分组树状态推断

影响：

- 配置一变，历史文档显示的业务树就变了
- 容易造成“文档看起来属于当前子类型，但上传时并不属于”

### 3.2.3 上传仅校验 `group_id` 属于 `document_type_id`

问题：

- 没有进一步校验：
  - 是否属于当前入口模块
  - 是否属于当前租户允许范围
  - 是否属于当前业务树

影响：

- 只要知道一个合法分组 ID，就可能落错业务边界

### 3.2.4 交叉评查链路会绕过原租户范围

问题：

- `GetReviewPoints`
- `AuditReviewPoint`
- `ConfirmReviewResults`

在交叉评查任务成员命中时会 bypass scope

影响：

- 如果业务上不允许跨租户交叉评查，这就是直接越界读取/更新

## 3.3 前端主链路高风险

### 3.3.1 上传页没有显式提交 `tenant_code`

文件：

- [FilesUploadClient.tsx](/home/wren-dev/Porject/leaudit-platform/legal-platform-frontend/app/(audit)/files/upload/FilesUploadClient.tsx)
- [files-upload.ts](/home/wren-dev/Porject/leaudit-platform/legal-platform-frontend/lib/api/legacy/files/files-upload.ts)

问题：

- 当前前端主要把 `tenant_name/area` 当 `region` 传
- 没有直接传 `tenant_code`

影响：

- 后端仍要猜
- 租户显示名不唯一时最容易落错数据

### 3.3.2 子类型分组下拉没有按 `tenant_code` 拉取

问题：

- `/api/v3/evaluation-point-groups/by-document-types`
- 只按 `document_type_ids`
- 最多本地再按 `entryModuleId` 过滤

影响：

- 不同租户复用同文档类型时，前端会把别的租户二级组混到下拉框

### 3.3.3 首页跳转/文档列表作用域没有显式携带租户

问题：

- 首页只传 `selectedModuleId/documentTypeIds`
- 文档列表和上传页继续从 sessionStorage/URL 恢复范围

影响：

- 模块是对的，不代表租户也是对的
- 最容易在跨页、刷新、缓存残留时串范围

---

## 4. 中风险问题清单

## 4.1 后端中风险

1. `_ensure_entry_module_valid` 只校验入口模块存在，不校验租户可用性
2. `_ensure_document_type_valid` 仍是全局文种唯一/存在校验
3. `_ensure_code_unique` 仍是全局唯一
4. `bootstrap_rule_groups`、`ensure_group_for_doc_type` 仍是假设全局分组树
5. 文档当前用户上下文仍依赖 `area` 兼容与硬编码管理角色
6. `review_point_audits` 没有租户/入口/分组快照

## 4.2 前端中风险

1. `rule-groups` 页面没有当前租户确认层
2. `document-types` 页面只围绕入口模块，不围绕租户
3. 首页系统概览仍用硬编码 `documentTypeIds` 推断业务

---

## 5. 低风险问题清单

1. 首页仍把 `user_area` 写入本地缓存
2. `region` 仍是多个接口 VO 的正式字段
3. 上传接口同时暴露 `typeId/typeCode/groupId/region/tenant_code` 多套口径
4. 多个管理页缺少“当前租户/当前作用域”显式展示

---

## 6. 正确的改造顺序

## 第一阶段：堵住最容易落错/查错数据的链路

1. 上传前端显式提交 `tenant_code`
2. 后端上传与列表停止 `tenant_code -> region` 混查
3. 上传时强校验 `group_id + document_type_id + entry_module_id + tenant_code`
4. 文档持久化真实 `group_id/root_group_id/entry_module_id/tenant_code`

## 第二阶段：补评查组/规则集的入口防线与读写边界

1. `RuleController` 全面加鉴权与权限
2. 评查组 controller/service 全链路透传租户
3. 分组树和绑定查询全部加租户过滤
4. `RebindGroup`、`CreateBinding`、`CreateRuleDraft` 改成租户内操作

## 第三阶段：补主数据模型

1. 给 `leaudit_evaluation_point_groups` 增加租户归属
2. 给 `leaudit_rule_group_bindings` 增加租户归属
3. 重新定义 `rule_set` 是否按租户隔离
4. 重做 `bootstrap_rule_groups / ensure_group_for_doc_type`

## 第四阶段：补界面确认层

1. 首页跳转带上租户上下文
2. 上传页/文档列表/评查组页显示当前租户
3. 子类型、规则集、文档列表全部按租户查询

---

## 7. 建议接下来直接执行的第一批任务

1. 后端：收紧文档主链路，去掉危险的 `tenant_code + region` 混查
2. 前端：上传显式传 `tenant_code`，文档列表显式带 `tenant_code`
3. 后端：给 `RuleController` 全量补 token 和权限
4. 后端：评查组接口开始透传 `tenant_code`
5. 文档：补“新平台主链路租户改造实施任务单”

