Files
leaudit-platform-backend/docs/superpowers/plans/2026-05-23-entry-module-menu-profile-multitenant-refactor.md
T
2026-05-25 09:50:01 +08:00

2225 lines
81 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 入口模块菜单配置与多租户重构实施计划
> **给后续执行开发的人:** 按本文任务逐步实施,不要跳着改。本文只做入口模块、菜单、文档归属、多租户范围的重构计划,不直接改动运行时规则评查逻辑。
**目标:** 让“入口模块”真正成为用户能理解的业务工作台,菜单显示、文档过滤、上传范围、规则分组、租户可见性都从入口模块配置出发,不再依赖“名称里是否包含合同/公文”这种硬编码判断。
**总体架构:** 保留 `leaudit_entry_modules` 作为首页入口模块主表,保留现有 `leaudit_entry_module_tenants` 作为租户可见性关系表。在入口模块上增加“菜单模板字段”`menu_profile` 和“功能清单字段”`features`,用它们决定该入口模块显示哪些功能菜单。角色权限系统继续负责页面和接口权限,入口模块只负责“业务工作台编排”和“业务范围”。
**涉及技术:** 后端接口、数据库原生查询、数据库迁移脚本、前端页面、前端侧边栏、现有角色权限系统、现有租户识别逻辑。
---
## 一、当前根因
现在系统的问题不是用户没配置好,而是代码在“猜业务类型”。
当前错误判断包括:
- 合同菜单靠“入口模块名称里有没有合同两个字”判断。
- 公文菜单靠“入口模块名称里有没有公文两个字”或固定路径判断。
- 公文列表页面强行显示成“内部公文”。
- 公文列表页面虽然读取了浏览器地址里的 `entryModuleId`,但没有传给后端列表接口。
- 文档列表和规则页面部分支持 `entryModuleId`,但侧边栏菜单不认它。
- 租户可见性已经有 `leaudit_entry_module_tenants`,但菜单功能没有和入口模块、租户上下文形成闭环。
目标链路应该变成:
```text
当前用户租户 -> 可见入口模块 -> 用户选择入口模块
入口模块 -> 菜单模板字段/功能清单字段 -> 生成左侧菜单
入口模块 -> 文档类型 -> 上传/列表/规则分组范围
文档 -> entry_module_id + type_id + group_id + tenant_code -> 后续评查/统计/列表过滤
```
## 二、用户应该怎么理解
管理员配置入口模块时,只需要按业务语言操作:
1. 新建入口模块,例如 `入口模块-测试`
2. 选择菜单模板:合同工作台、公文工作台、交叉评查工作台、通用文档评查、自定义。
3. 勾选功能:文档列表、文件上传、规则配置、规则分组、模板搜索、模板列表、公文列表、公文上传等。
4. 分配租户:云浮、揭阳、梅州、公共资源域等。
5. 绑定一级文档类型。
6. 在规则分组里配置二级运行子类型和规则绑定。
用户不应该被迫把入口模块命名为“合同xxx”或“公文xxx”才能显示对应功能。
## 三、核心数据模型
### 3.1 保留现有表
继续使用:
- `leaudit_entry_modules`:入口模块主表,表示首页业务入口。
- `leaudit_entry_module_tenants`:入口模块和租户的可见关系。
- `leaudit_document_types`:文档类型,通过 `entry_module_id` 归属入口模块。
- `leaudit_evaluation_point_groups`:规则分组树。
- `leaudit_rule_group_bindings`:运行时规则绑定唯一事实源。
- `leaudit_documents`:文档记录,后续需要补 `entry_module_id`
### 3.2 入口模块新增字段
`leaudit_entry_modules` 增加:
```sql
ALTER TABLE leaudit_entry_modules
ADD COLUMN IF NOT EXISTS menu_profile VARCHAR(64) NOT NULL DEFAULT 'document_review',
ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '[]'::jsonb;
CREATE INDEX IF NOT EXISTS idx_leaudit_entry_modules_menu_profile
ON leaudit_entry_modules(menu_profile)
WHERE deleted_at IS NULL;
```
字段解释:
- `menu_profile`:菜单模板字段,比如合同工作台、公文工作台、通用文档评查。
- `features`:功能清单字段,表示该入口模块启用哪些功能菜单。
### 3.3 文档表新增字段
`leaudit_documents` 增加:
```sql
ALTER TABLE leaudit_documents
ADD COLUMN IF NOT EXISTS entry_module_id BIGINT NULL REFERENCES leaudit_entry_modules(id);
CREATE INDEX IF NOT EXISTS idx_leaudit_documents_entry_module_id
ON leaudit_documents(entry_module_id);
```
作用:
- 文档创建后明确知道自己来自哪个入口模块。
- 文档列表、公文列表、统计、后续质量校验都可以按入口模块过滤。
## 四、菜单模板与功能编码
### 4.1 菜单模板
固定使用这些编码:
```text
document_review 通用文档评查
contract 合同工作台
govdoc 内部公文工作台
cross_checking 交叉评查工作台
custom 自定义工作台
```
### 4.2 功能编码
固定使用这些编码:
```text
home 首页/概览
documents 文档列表
upload 文件上传
rules 规则配置
rule_groups 规则分组
contract_template_search 合同模板搜索
contract_template_list 合同模板列表
govdoc_audits 公文列表
govdoc_upload 公文上传
cross_checking 交叉评查
cross_checking_upload 创建交叉评查任务
cross_checking_list 交叉评查任务列表
usage_stats 使用统计
```
### 4.3 默认功能
如果老数据没有配置功能清单字段,就按菜单模板字段自动补默认值:
```json
{
"document_review": ["home", "documents", "upload", "rules", "rule_groups"],
"contract": ["home", "documents", "upload", "rules", "contract_template_search", "contract_template_list"],
"govdoc": ["home", "govdoc_audits", "govdoc_upload", "rule_groups"],
"cross_checking": ["cross_checking", "cross_checking_upload", "cross_checking_list"],
"custom": ["home", "documents"]
}
```
## 五、多租户规则
这里必须沿用你当前项目已有模型,不新造租户体系。
### 5.1 入口模块可见性
入口模块是否对某个租户可见,继续由 `leaudit_entry_module_tenants` 控制。
判断规则:
```text
用户 tenant_code 命中 leaudit_entry_module_tenants.tenant_code
并且 leaudit_entry_module_tenants.is_enabled = true
并且 leaudit_entry_modules.is_enabled = true
并且入口模块没有 deleted_at
```
特殊情况:
- `PUBLIC` 可以作为公共入口模块。
- 超级管理员或全局管理员可以跨租户查看。
- 旧的地区字段暂时保留兼容,但不再作为新逻辑主模型。
### 5.2 功能菜单是否需要租户级差异
第一阶段不做“同一个入口模块不同租户显示不同功能”。
第一阶段规则:
```text
入口模块功能清单对所有可见租户一致
租户只控制入口模块是否可见
```
如果后续真有需求,例如同一个入口模块云浮显示模板搜索、梅州不显示模板搜索,再扩展:
```sql
ALTER TABLE leaudit_entry_module_tenants
ADD COLUMN IF NOT EXISTS features_override JSONB NULL,
ADD COLUMN IF NOT EXISTS menu_profile_override VARCHAR(64) NULL;
```
最终生效逻辑:
```text
租户关系表 features_override 有值 -> 用租户覆盖配置
否则入口模块 features 有值 -> 用入口模块配置
否则按 menu_profile 默认功能生成
```
第一阶段先不要上这个复杂度。
### 5.3 入口模块配置权限
入口模块是系统级业务工作台配置,但不要在代码里硬编码“只有某个角色名能配置”。正确口径是:谁被分配了入口模块管理权限,谁才能配置入口模块。
也就是说:
```text
不要写死 role_key = super_admin
不要写死 role_key = provincial_admin
不要写死市区管理员一定不能操作
```
后端只认权限点:
```text
entry_module:list:read 查看入口模块列表
entry_module:detail:read 查看入口模块详情
entry_module:create:write 创建入口模块
entry_module:update:write 编辑入口模块
entry_module:delete:delete 删除入口模块
entry_module:image:write 上传或替换入口模块图标
```
推荐默认分配策略:
```text
系统超级管理员角色:默认分配入口模块全部管理权限
省级管理员角色:默认不分配创建/编辑/删除入口模块权限,是否开放由实际授权决定
市区管理员角色:默认不分配创建/编辑/删除入口模块权限,是否开放由实际授权决定
```
所以最终判断不是“这个人是不是超级管理员”,而是:
```text
能不能创建入口模块 -> 看有没有 entry_module:create:write
能不能编辑入口模块 -> 看有没有 entry_module:update:write
能不能删除入口模块 -> 看有没有 entry_module:delete:delete
能不能上传图标 -> 看有没有 entry_module:image:write
```
三层边界必须分清:
```text
RBAC 权限点:控制谁能配置入口模块
入口模块租户关系:控制哪个租户能看到/使用入口模块
入口模块 features + 页面/API 权限:控制进入入口后能看到和使用哪些功能
```
举例:
```text
某个市区管理员没有 entry_module:create:write
即使他属于云浮租户,也不能新建入口模块
某个市区管理员有 entry_module:create:write
他就可以进入入口模块管理页执行创建动作
但创建出来的入口能被哪些租户看到,仍然必须写入 leaudit_entry_module_tenants
```
## 六、一级分组、二级分组、规则绑定设计
这一块是本次重构最关键的业务边界。入口模块只解决“用户从哪个工作台进来”,规则分组解决“这个工作台下有哪些业务类型”,规则绑定解决“这个业务类型运行哪套规则”。
### 6.1 最终业务口径
固定使用两层分组,不继续发散:
```text
入口模块
-> 一级分组:业务大类
-> 二级分组:具体运行类型
-> 规则绑定:绑定规则集
```
举例:
```text
入口模块:合同评查
一级分组:合同
二级分组:建设工程合同、买卖合同、租赁合同
规则绑定:建设工程合同 -> 建设工程合同规则集
```
再举例:
```text
入口模块:内部公文
一级分组:内部公文
二级分组:请示、通知、会议纪要
规则绑定:请示 -> 请示规则集
```
### 6.2 一级分组职责
一级分组只表达业务大类,不直接参与运行时规则命中。
数据库仍然复用:
```text
leaudit_evaluation_point_groups
```
一级分组判断:
```text
pid = 0
```
一级分组字段规则:
```text
entry_module_id 必填,表示这个一级分组属于哪个入口模块
document_type_id 为空,不直接绑定具体文档类型
name 使用业务大类名称,例如 合同、内部公文、行政卷宗
```
也就是说,入口模块页面里的“规则分组”应该先看到这个入口模块下的一级分组,而不是全系统所有分组。
### 6.3 二级分组职责
二级分组才是实际运行类型,也就是用户上传、列表过滤、评查执行真正要落的 `groupId`
二级分组判断:
```text
pid != 0
```
二级分组字段规则:
```text
pid 指向一级分组 id
document_type_id 必填,表示这个二级分组对应哪个具体文档类型
entry_module_id 可以冗余保存,也可以从一级分组继承,但查询时必须能按入口模块过滤
```
前端显示时,用户理解成:
```text
一级分组 = 大类
二级分组 = 具体要评查的文档类型/运行类型
```
后端理解成:
```text
文档最终必须落到二级分组 group_id
```
### 6.4 规则绑定职责
规则绑定只允许挂二级分组,不允许挂一级分组。
继续使用:
```text
leaudit_rule_group_bindings
```
绑定关系:
```text
leaudit_rule_group_bindings.group_id = 二级分组 id
leaudit_rule_group_bindings.rule_set_id = 规则集 id
```
禁止口径:
```text
不要把规则集挂在一级分组
不要再按入口模块名称猜规则集
不要再绕回旧的文档类型规则绑定表作为主链路
```
运行时唯一主链路:
```text
document.group_id
-> leaudit_rule_group_bindings.group_id
-> rule_set_id
-> 当前可执行规则版本
```
### 6.5 多租户下规则怎么生效
分组树本身不建议第一阶段按租户复制一份。否则云浮、梅州、揭阳每个租户一套树,后面维护会爆炸。
第一阶段建议:
```text
入口模块租户关系:控制哪个租户看得到哪个入口模块
一级/二级分组:表达入口模块下的业务结构
规则绑定 tenant_code/scope_type:控制不同租户实际运行哪套规则集
```
也就是说:
```text
同一个二级分组,可以存在多条规则绑定
```
例如:
```text
二级分组:建设工程合同
云浮租户 -> 云浮建设工程合同规则集
梅州租户 -> 梅州建设工程合同规则集
省级兜底 -> 通用建设工程合同规则集
```
运行时选择顺序:
```text
优先当前租户绑定
没有当前租户绑定,再走省级/公共兜底绑定
仍然没有,就提示该二级分组未配置有效规则
```
### 6.6 上传、列表、评查的字段落点
上传时必须落这些字段:
```text
entry_module_id:从哪个入口进来
type_id:具体文档类型
group_id:二级分组 id
tenant_code:当前租户
```
如果上传时只传了文档类型,没有传二级分组,后端只能在“该文档类型只有一个可用二级分组”时兜底推断。只要有多个二级分组,就必须让用户明确选择,否则规则命中会不稳定。
列表过滤顺序:
```text
先按 tenant_code 做租户边界
再按 entry_module_id 做入口模块范围
再按 group_id/type_id 做具体业务过滤
```
评查执行顺序:
```text
读取文档 group_id
确认 group_id 是二级分组
按 group_id + tenant_code 找有效规则绑定
取 rule_set 当前可执行版本
执行评查
```
### 6.7 后台页面怎么改
入口模块管理页:
```text
只负责入口模块本身、菜单功能、租户可见性
不在这里直接绑规则集
```
文档类型管理页:
```text
负责维护具体文档类型属于哪个入口模块
可以辅助展示它被哪个二级分组使用
```
规则分组页:
```text
进入某入口模块后,只显示该入口模块下的一级分组
一级分组下面维护二级分组
二级分组上配置规则绑定
```
规则绑定页或弹窗:
```text
只能从二级分组进入
可以配置当前租户绑定、省级兜底绑定、公共绑定
保存到 leaudit_rule_group_bindings
```
### 6.8 必须加的校验
后端保存规则分组时必须校验:
```text
一级分组 pid = 0 时,必须有 entry_module_id
一级分组不允许绑定 rule_set
二级分组 pid != 0 时,必须有 document_type_id
二级分组所属一级分组必须和当前入口模块一致
二级分组才允许写 leaudit_rule_group_bindings
上传文档时传入的 group_id 必须是二级分组
上传文档时 group_id 对应的入口模块必须等于 entry_module_id
```
建议补充数据库约束或唯一索引:
```sql
CREATE UNIQUE INDEX IF NOT EXISTS uq_leaudit_ep_groups_parent_doc_type_active
ON leaudit_evaluation_point_groups(pid, document_type_id)
WHERE deleted_at IS NULL
AND COALESCE(pid, 0) <> 0
AND document_type_id IS NOT NULL;
```
作用是避免同一个一级分组下面重复挂同一个文档类型,导致上传时无法稳定推断二级分组。
### 6.9 旧数据兼容策略
当前系统里可能还有旧结构:
```text
一级分组 = 具体文档类型
二级分组 = 通用
```
这个是过渡结构,不要继续加深。
兼容策略:
```text
短期:后端查询同时兼容旧结构,避免线上功能直接断
中期:迁移成 一级=业务大类、二级=具体文档类型
长期:规则绑定只认二级分组,旧结构下线
```
迁移示例:
```text
旧:
一级:建设工程合同
二级:通用
规则绑定:通用 -> 规则集
新:
一级:合同
二级:建设工程合同
规则绑定:建设工程合同 -> 原规则集
```
这一段迁移可以参考已有脚本:
```text
scripts/创建sql/migrate_rule_groups_to_business_roots.sql
scripts/创建sql/precheck_rule_group_migration.sql
```
## 七、后端改造范围
### 7.1 后端请求对象
文件:
```text
fastapi_modules/fastapi_leaudit/domian/Dto/entryModuleDto.py
```
`EntryModuleCreateDTO``EntryModuleUpdateDTO` 增加:
```python
menu_profile: str | None = Field(None, description="菜单模板:document_review/contract/govdoc/cross_checking/custom")
features: list[str] | None = Field(None, description="启用功能编码列表")
```
### 7.2 后端返回对象
文件:
```text
fastapi_modules/fastapi_leaudit/domian/vo/entryModuleAdminVo.py
fastapi_modules/fastapi_leaudit/domian/vo/homeVo.py
```
入口模块管理返回对象增加:
```python
menu_profile: str = Field("document_review", description="菜单模板")
features: list[str] = Field(default_factory=list, description="启用功能编码列表")
business_scope: EntryModuleBusinessScopeVO = Field(default_factory=EntryModuleBusinessScopeVO, description="业务范围摘要")
```
首页入口返回对象增加:
```python
menuProfile: str = Field("document_review", description="菜单模板")
features: list[str] = Field(default_factory=list, description="启用功能编码列表")
tenantCode: str | None = Field(None, description="当前命中的租户编码")
```
### 7.3 入口模块管理服务
文件:
```text
fastapi_modules/fastapi_leaudit/services/impl/entryModuleAdminServiceImpl.py
```
要做:
1. 确保菜单模板字段和功能清单字段存在。
2. 创建入口模块时保存菜单模板和功能清单。
3. 更新入口模块时保存菜单模板和功能清单。
4. 查询列表和详情时返回这两个字段。
5. 查询列表和详情时返回业务范围摘要 `business_scope`,用于前端展示已绑定业务大类和业务类型数量。
6. 校验菜单模板和功能清单只能使用允许的编码。
7. 继续沿用现有入口模块权限点做接口控制,不要额外硬编码角色名。
8. 如果当前用户没有对应权限点,后端直接返回 403,不能只靠前端隐藏按钮。
允许值:
```python
_ALLOWED_MENU_PROFILES = {"document_review", "contract", "govdoc", "cross_checking", "custom"}
_ALLOWED_FEATURES = {
"home",
"documents",
"upload",
"rules",
"rule_groups",
"contract_template_search",
"contract_template_list",
"govdoc_audits",
"govdoc_upload",
"cross_checking",
"cross_checking_upload",
"cross_checking_list",
"usage_stats",
}
```
### 7.4 首页入口服务
文件:
```text
fastapi_modules/fastapi_leaudit/services/impl/homeServiceImpl.py
```
要做:
1. 保持现有 `leaudit_entry_module_tenants` 过滤逻辑不变。
2. 查询入口模块时带出菜单模板和功能清单。
3. 返回给首页,前端点击入口时存起来。
4. 返回当前命中的租户编码,方便前端继续传递上下文。
### 7.5 文档服务
文件:
```text
fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py
fastapi_modules/fastapi_leaudit/controllers/documentController.py
```
要做:
1. 上传接口接收 `entryModuleId`
2. 创建文档时写入 `entry_module_id`
3. 如果前端没传,则按:
```text
二级分组 entry_module_id -> 一级分组 entry_module_id -> 文档类型 entry_module_id
```
兜底解析。
4. 文档列表过滤时使用:
```sql
COALESCE(d.entry_module_id, eg.entry_module_id, eg_parent.entry_module_id, dt.entry_module_id) = :entry_module_id
```
### 7.6 公文服务
文件:
```text
fastapi_modules/fastapi_leaudit/controllers/govdocController.py
fastapi_modules/fastapi_leaudit/services/impl/govdocServiceImpl.py
```
要做:
1. `/api/govdoc/documents` 增加 `entry_module_id` 查询参数。
2. `GovdocServiceImpl.ListDocuments` 增加 `EntryModuleId` 参数。
3. 查询时按:
```sql
COALESCE(d.entry_module_id, dt.entry_module_id) = :entry_module_id
```
过滤。
4. 继续保留现有:
```sql
COALESCE(d.engine_type, 'leaudit') = 'govdoc'
```
也就是说:
```text
公文列表 = 文档引擎类型是公文 + 当前入口模块范围
```
### 7.7 规则分组服务
文件:
```text
fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py
fastapi_modules/fastapi_leaudit/services/impl/ruleConfigServiceImpl.py
```
要做:
1. 规则组列表按 `entry_module_id` 过滤。
2. 过滤表达式统一用:
```sql
COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
```
3. 入口模块只决定“用户看到哪些规则组”,不参与运行时选规则。
4. 运行时选规则继续保持:
```text
document.group_id -> leaudit_rule_group_bindings -> rule_set/rule_version
```
## 八、前端改造范围
### 8.0 前端 UI 风格约束
本次前端改造必须和当前系统 UI 设计、样式、配色保持统一,不允许为了新功能另起一套视觉风格。
统一要求:
```text
继续使用当前系统已有的 Card、Button、Table、FilterPanel、FilterSelect、SearchFilter、Pagination、Modal 等组件
继续使用当前页面已有的 CSS 变量和主题色
继续沿用现有后台管理页的布局、间距、圆角、阴影、表格操作列样式
继续沿用现有 toast、loading、empty state、403 提示样式
新增表单项要和当前入口模块编辑页的表单布局一致
新增功能勾选区要像现有权限/配置类页面一样清晰,不做花哨卡片风格
```
禁止:
```text
不要新增一套独立主题色
不要引入新的 UI 框架
不要写和当前后台风格不一致的大面积渐变、特殊字体、复杂动效
不要把入口模块管理页改成和系统其他管理页完全不同的视觉
不要为了功能勾选单独设计一套复杂组件
```
具体页面要求:
```text
入口模块列表页:沿用当前表格、筛选、操作按钮风格
入口模块编辑页:沿用当前表单分组风格,只增加菜单模板和功能勾选
侧边栏:沿用当前菜单高亮、图标、折叠、权限过滤表现
规则分组页:沿用当前树/列表/弹窗样式,不重做视觉
上传页:只补入口模块上下文和二级分组选择,不重做上传区视觉
公文列表页:只改标题和过滤参数,不重做列表视觉
```
如果必须新增样式,优先放在现有对应页面样式文件里,并复用当前 CSS 变量,例如:
```text
var(--color-primary-text)
var(--color-primary-text-muted)
var(--color-border)
var(--color-surface)
```
### 8.1 入口模块前端接口类型
文件:
```text
legal-platform-frontend/lib/api/legacy/entry-modules/entry-modules.ts
legal-platform-frontend/lib/api/legacy/entry-modules/request-body.ts
```
增加类型:
```ts
export type EntryModuleMenuProfile = "document_review" | "contract" | "govdoc" | "cross_checking" | "custom";
export type EntryModuleFeature =
| "home"
| "documents"
| "upload"
| "rules"
| "rule_groups"
| "contract_template_search"
| "contract_template_list"
| "govdoc_audits"
| "govdoc_upload"
| "cross_checking"
| "cross_checking_upload"
| "cross_checking_list"
| "usage_stats";
```
入口模块类型增加:
```ts
menu_profile?: EntryModuleMenuProfile | string;
features?: EntryModuleFeature[] | string[];
business_scope?: {
category_count: number;
business_type_count: number;
categories: string[];
} | null;
```
请求体增加:
```ts
menu_profile?: string | null;
features?: string[];
```
### 8.2 入口模块编辑页
文件:
```text
legal-platform-frontend/app/(audit)/entry-modules/new/EntryModuleNewClient.tsx
```
页面增加两个配置区:
1. 菜单模板选择:
```text
通用文档评查
合同工作台
内部公文工作台
自定义工作台
```
2. 功能勾选:
```text
文档列表
文件上传
规则配置
规则分组
模板搜索
模板列表
公文列表
公文上传
交叉评查
创建交叉评查任务
交叉评查任务列表
使用统计
```
用户选择菜单模板后,自动带出默认功能,用户可以再调整。
权限显示规则:
```text
没有 entry_module:create:write:隐藏新建入口模块按钮
没有 entry_module:update:write:隐藏或禁用保存修改按钮
没有 entry_module:delete:delete:隐藏删除按钮
没有 entry_module:image:write:隐藏或禁用上传图标能力
```
注意:前端隐藏只是体验优化,最终以后端权限点校验为准。
### 8.3 首页点击入口
文件:
```text
legal-platform-frontend/app/page.tsx
```
点击入口模块时,除了原来的 `selectedModuleId/selectedModuleName`,还要保存:
```ts
{
id,
name,
targetPath,
menuProfile,
features,
tenantCode,
documentTypeIds,
iconPath
}
```
建议统一封装到新文件:
```text
legal-platform-frontend/lib/auth/entry-module-context.ts
```
同时 URL 必须带:
```text
entryModuleId=xxx
documentTypeIds=1,2,3
```
不能只依赖 sessionStorage,否则刷新和复制链接会丢上下文。
### 8.4 侧边栏菜单
文件:
```text
legal-platform-frontend/components/layout/Sidebar.tsx
legal-platform-frontend/lib/auth/entry-module-menu.ts
```
新增 `entry-module-menu.ts`,把功能编码转成菜单:
```ts
documents -> /documents
upload -> /files/upload
rules -> /rules-test/list
rule_groups -> /rule-groups
contract_template_search -> /contract-template/search
contract_template_list -> /contract-template/list
govdoc_audits -> /govdoc/audits
govdoc_upload -> /govdoc/upload
cross_checking -> /cross-checking
cross_checking_upload -> /cross-checking/upload
cross_checking_list -> /cross-checking/list
```
侧边栏必须删除这些判断:
```ts
selectedModuleName.includes("合同")
selectedModuleName.includes("公文")
shouldUseGovdocAuditMenu(effectiveSelectedModuleName)
```
改成:
```text
读取当前入口模块功能清单
功能清单决定显示哪些菜单
角色权限系统再过滤用户没权限的菜单
```
重点:
```text
功能清单不能绕过角色权限系统
```
如果入口模块启用了“模板列表”,但用户角色没有模板列表页面权限,仍然不能显示或访问。
入口模块管理菜单本身也要按权限点显示:
```text
有 entry_module:list:read 才显示入口模块管理菜单
没有 entry_module:list:read 即使知道路径也不能正常加载数据
```
### 8.5 公文列表页面
文件:
```text
legal-platform-frontend/components/govdoc-audit/audits.tsx
legal-platform-frontend/lib/api/govdoc-audit/api.ts
```
要做:
1. 从 URL 或入口模块上下文读取 `entryModuleId`
2. 调用 `api.listAudits()` 时传入 `entryModuleId`
3. `api.listAudits()` 转成后端参数:
```text
entry_module_id=xxx
```
4. 页面标题/侧边栏显示当前入口模块名,不再写死“内部公文”。
### 8.6 上传页面
文件:
```text
legal-platform-frontend/app/(audit)/files/upload/FilesUploadClient.tsx
```
要做:
1. 上传时读取当前入口模块上下文。
2. 上传表单数据增加:
```text
entryModuleId
```
3. 继续保留:
```text
typeId
groupId
tenantCode
```
上传后的文档必须具备完整归属:
```text
entry_module_id + type_id + group_id + tenant_code
```
## 九、SQL 迁移脚本
建议新增:
```text
scripts/创建sql/entry_module_menu_profile_migration.sql
```
内容:
```sql
ALTER TABLE leaudit_entry_modules
ADD COLUMN IF NOT EXISTS menu_profile VARCHAR(64) NOT NULL DEFAULT 'document_review',
ADD COLUMN IF NOT EXISTS features JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE leaudit_documents
ADD COLUMN IF NOT EXISTS entry_module_id BIGINT NULL REFERENCES leaudit_entry_modules(id);
CREATE INDEX IF NOT EXISTS idx_leaudit_entry_modules_menu_profile
ON leaudit_entry_modules(menu_profile)
WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_leaudit_documents_entry_module_id
ON leaudit_documents(entry_module_id);
CREATE UNIQUE INDEX IF NOT EXISTS uq_leaudit_ep_groups_parent_doc_type_active
ON leaudit_evaluation_point_groups(pid, document_type_id)
WHERE deleted_at IS NULL
AND COALESCE(pid, 0) <> 0
AND document_type_id IS NOT NULL;
```
旧数据回填:
```sql
UPDATE leaudit_entry_modules
SET
menu_profile = CASE
WHEN path IN ('/govdoc/audits', '/govdoc', '/govdoc-audit') THEN 'govdoc'
WHEN path IN ('/contract-template', '/contract-template/list', '/contract-template/search') THEN 'contract'
ELSE COALESCE(NULLIF(menu_profile, ''), 'document_review')
END,
features = CASE
WHEN path IN ('/govdoc/audits', '/govdoc', '/govdoc-audit')
THEN '["home","govdoc_audits","govdoc_upload","rule_groups"]'::jsonb
WHEN path IN ('/contract-template', '/contract-template/list', '/contract-template/search')
THEN '["home","documents","upload","rules","contract_template_search","contract_template_list"]'::jsonb
WHEN features = '[]'::jsonb
THEN '["home","documents","upload","rules","rule_groups"]'::jsonb
ELSE features
END
WHERE deleted_at IS NULL;
```
文档归属回填:
```sql
UPDATE leaudit_documents d
SET entry_module_id = COALESCE(g.entry_module_id, parent.entry_module_id, dt.entry_module_id)
FROM leaudit_document_types dt
LEFT JOIN leaudit_evaluation_point_groups g ON g.id = d.group_id
LEFT JOIN leaudit_evaluation_point_groups parent ON parent.id = g.pid
WHERE d.type_id = dt.id
AND d.entry_module_id IS NULL;
```
验证脚本:
```sql
SELECT id, name, path, menu_profile, features
FROM leaudit_entry_modules
WHERE deleted_at IS NULL
ORDER BY sort_order, id;
SELECT COUNT(*) AS documents_without_entry_module
FROM leaudit_documents
WHERE deleted_at IS NULL
AND entry_module_id IS NULL;
```
## 十、实施顺序
### 第一阶段:先修用户能看到的问题
目标:
```text
入口模块名字不含合同/公文,也能按配置显示正确菜单
```
任务:
1. 后端入口模块请求对象和返回对象增加菜单模板、功能清单。
2. 入口模块管理页支持菜单模板和功能勾选。
3. 首页返回入口模块时带菜单模板、功能清单。
4. 首页点击入口时保存完整上下文。
5. 侧边栏改成按功能清单生成菜单。
6. 删除侧边栏按名称包含“合同/公文”的判断。
### 第二阶段:列表和上传真正按入口过滤
目标:
```text
进入哪个入口模块,就只看这个入口模块下的文档和公文
```
任务:
1. 文档上传保存 `entry_module_id`
2. 普通文档列表按 `entry_module_id` 过滤。
3. 公文列表按 `entry_module_id` 过滤。
4. 公文列表前端传 `entryModuleId`
5. 页面刷新后仍能从 URL 恢复入口上下文。
### 第三阶段:规则分组范围统一
目标:
```text
规则分组页面只显示当前入口模块相关规则组
```
任务:
1. 规则分组列表支持 `entry_module_id`
2. 前端规则分组页面从 URL/session 读取当前入口。
3. 规则组创建时继承当前入口模块。
4. 二级分组绑定具体文档类型。
5. 规则绑定仍然只挂二级分组。
6. 上传和评查接口校验 `group_id` 必须是二级分组。
7. 规则绑定保存接口拒绝一级分组。
### 第四阶段:旧逻辑下线
目标:
```text
不再靠名字和路径猜业务类型
```
任务:
1. 删除 `includes("合同")` 决定合同菜单的逻辑。
2. 删除 `includes("公文")` 决定公文菜单的逻辑。
3. 删除 `/govdoc` 强制显示“内部公文”的逻辑。
4. 保留路径字段只作为跳转地址。
5. 旧地区字段保留兼容,但新写入只走租户关系。
## 十一、验收标准
### 11.1 合同入口验收
配置:
```text
入口模块名称:入口模块-测试
menu_profilecontract
featurescontract_template_search, contract_template_list
租户:云浮
```
预期:
```text
云浮用户首页能看到该入口
点击后左侧显示模板搜索、模板列表
入口名字不包含“合同”也正常显示
其他未绑定租户看不到该入口
没有角色权限的用户仍然不能访问模板页面
```
### 11.2 公文入口验收
配置:
```text
入口模块名称:入口模块-测试
route_path/govdoc/audits
menu_profilegovdoc
featuresgovdoc_audits
租户:梅州
```
预期:
```text
梅州用户首页能看到该入口
点击进入 /govdoc/audits
页面显示入口模块-测试,而不是固定显示内部公文
公文列表请求带 entry_module_id
列表只显示该入口模块范围内的公文
```
### 11.3 文档列表验收
配置:
```text
入口 A 绑定文档类型 A
入口 B 绑定文档类型 B
同一个租户下分别上传文档
```
预期:
```text
从入口 A 进入只看到 A 的文档
从入口 B 进入只看到 B 的文档
刷新页面后过滤条件不丢
```
### 11.3.1 交叉评查入口验收
配置:
```text
入口模块名称:入口模块-测试交叉评查
route_path/cross-checking
menu_profilecross_checking
featurescross_checking, cross_checking_upload, cross_checking_list
租户:目标租户或 PUBLIC
```
预期:
```text
目标租户首页能看到该入口
点击进入 /cross-checking
侧边栏显示交叉评查、创建任务、评查任务列表
没有交叉评查路由权限的用户即使入口模块启用交叉评查功能,也不能看到或访问交叉评查菜单
```
### 11.4 多租户验收
配置:
```text
入口模块 X 只绑定云浮
入口模块 Y 只绑定梅州
入口模块 Z 绑定 PUBLIC
```
预期:
```text
云浮用户看到 X 和 Z
梅州用户看到 Y 和 Z
普通用户看不到其他租户入口
超级管理员可以跨租户管理
角色权限仍然控制页面和接口能不能访问
```
### 11.5 规则分组和规则绑定验收
配置:
```text
入口模块:合同评查
一级分组:合同
二级分组:建设工程合同
规则绑定:建设工程合同 -> 建设工程合同规则集
租户:云浮
```
预期:
```text
从合同评查入口进入,只看到合同一级分组
建设工程合同显示在合同一级分组下面
规则绑定只能在建设工程合同这个二级分组上配置
一级分组合同不能直接绑定规则集
上传建设工程合同后,文档 group_id 等于建设工程合同二级分组 id
评查时按 group_id + tenant_code 命中云浮对应规则集
```
### 11.6 入口模块配置权限验收
配置:
```text
角色 A 分配 entry_module:list:read
角色 A 不分配 entry_module:create:write / entry_module:update:write / entry_module:delete:delete
角色 B 分配 entry_module:list:read / entry_module:create:write / entry_module:update:write
角色 B 不分配 entry_module:delete:delete
```
预期:
```text
角色 A 可以查看入口模块列表
角色 A 看不到新建、编辑保存、删除入口模块能力
角色 A 直接调用创建/编辑/删除接口返回 403
角色 B 可以查看入口模块列表
角色 B 可以创建和编辑入口模块
角色 B 不能删除入口模块
角色 B 直接调用删除接口返回 403
```
关键验收点:
```text
系统不通过 role_key 硬编码判断谁能配置入口模块
入口模块配置权限完全由 RBAC 权限点分配决定
入口模块租户关系只控制可见和使用,不控制谁能配置
```
## 十二、非目标
这次重构不做:
- 不删除角色权限系统。
- 不删除 `leaudit_rule_type_bindings`
- 不改变“文档二级分组 -> 规则绑定 -> 规则集版本”的运行时主链路。
- 不把入口模块变成权限系统。
- 不第一阶段支持“同一入口不同租户不同菜单”。
- 不硬编码某个角色名才能配置入口模块,入口模块管理能力由权限点分配决定。
运行时规则仍然保持:
```text
document.group_id -> leaudit_rule_group_bindings -> rule_set/rule_version
```
入口模块只负责:
```text
用户从哪里进来
左侧显示哪些菜单
文档/规则/上传属于哪个业务范围
```
## 十三、风险控制
1. `menu_profile/features` 设置默认值,保证老入口模块不崩。
2. 旧地区字段暂时保留兼容,避免历史数据立即失效。
3. 功能清单生成菜单后还要经过角色权限过滤,不能绕过权限。
4. 浏览器地址参数必须带 `entryModuleId`,不能只靠浏览器临时缓存。
5. 文档上传必须保存 `entry_module_id/type_id/group_id/tenant_code`,后续列表和统计才稳定。
6. 第一阶段先解决菜单和入口体验,规则运行链路不乱动。
7. 一级分组只做业务大类,二级分组才做运行类型,规则绑定只能挂二级分组。
8. 入口模块管理不能靠前端隐藏按钮保证安全,后端必须按权限点返回 403。
## 十四、Superpowers 执行任务清单
本节是给后续开发直接执行的任务清单。执行时必须从上到下推进,每完成一个小任务就验证一次,不要一次性大改完再排错。
### 14.1 执行原则
- [x] 先做数据库和后端返回字段,再做前端展示。
- [x] 先解决入口模块菜单显示错误,再处理列表和上传过滤。
- [x] 不改运行时评查主链路,只补入口模块、分组、文档归属。
- [x] 不硬编码角色名判断入口模块管理权限,只使用 RBAC 权限点。
- [x] 每一步都保留旧数据兼容,避免老入口模块、老文档、旧规则分组直接失效。
### 14.2 数据库任务
- [x] 新增迁移脚本 `scripts/创建sql/entry_module_menu_profile_migration.sql`
- [x]`leaudit_entry_modules` 增加 `menu_profile` 字段。
- [x]`leaudit_entry_modules` 增加 `features` 字段。
- [x]`leaudit_documents` 增加 `entry_module_id` 字段。
- [x]`leaudit_documents.entry_module_id` 增加索引。
- [x]`leaudit_entry_modules.menu_profile` 增加索引。
- [x]`leaudit_evaluation_point_groups(pid, document_type_id)` 增加二级分组唯一索引。
- [x] 回填老入口模块的菜单模板和功能清单。
- [x] 尝试按二级分组、一级分组、文档类型回填历史文档的 `entry_module_id`
- [x] 写验证 SQL,检查入口模块字段、文档归属、重复二级分组。
### 14.3 后端入口模块任务
- [x] 修改 `fastapi_modules/fastapi_leaudit/domian/Dto/entryModuleDto.py`,创建和更新请求增加 `menu_profile/features`
- [x] 修改 `fastapi_modules/fastapi_leaudit/domian/vo/entryModuleAdminVo.py`,入口模块管理返回增加 `menu_profile/features`
- [x] 修改 `entryModuleAdminVo.py``entryModuleAdminServiceImpl.py`,入口模块列表和详情返回 `business_scope` 业务范围摘要。
- [x] 修改 `fastapi_modules/fastapi_leaudit/domian/vo/homeVo.py`,首页入口返回增加 `menuProfile/features/tenantCode`
- [x] 修改 `fastapi_modules/fastapi_leaudit/services/impl/entryModuleAdminServiceImpl.py`,创建入口模块时保存菜单模板和功能清单。
- [x] 修改 `entryModuleAdminServiceImpl.py`,更新入口模块时保存菜单模板和功能清单。
- [x] 修改 `entryModuleAdminServiceImpl.py`,列表和详情查询返回菜单模板和功能清单。
- [x] 修改 `entryModuleAdminServiceImpl.py`,列表和详情查询返回已绑定业务大类数量、业务类型数量和大类名称列表。
- [x]`entryModuleAdminServiceImpl.py` 增加菜单模板、功能编码白名单校验。
- [x] 保留 `entryModuleController.py` 现有权限点校验,不新增角色名硬编码。
- [x] 确认没有权限点时,创建、编辑、删除、上传图标接口都返回 403。
### 14.4 后端首页和入口上下文任务
- [x] 修改 `fastapi_modules/fastapi_leaudit/services/impl/homeServiceImpl.py`,首页入口查询带出 `menu_profile/features`
- [x] 首页入口仍按 `leaudit_entry_module_tenants` 过滤当前租户可见入口。
- [x] 首页入口返回当前命中的租户编码。
- [x] 首页入口返回文档类型范围,方便前端拼接上下文。
- [x] 保持 `PUBLIC` 入口模块兼容。
- [x] 保持老地区字段兼容,但新链路以租户关系为准。
### 14.5 后端文档和公文任务
- [x] 修改文档上传接口,接收 `entryModuleId`
- [x] 修改 `fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py`,创建文档时写入 `entry_module_id`
- [x] 上传时校验 `group_id` 是二级分组。
- [x] 上传时校验 `group_id` 所属入口模块与 `entryModuleId` 一致。
- [x] 当前端没传 `group_id` 时,只允许在唯一可用二级分组场景下兜底推断。
- [x] 普通文档列表支持按 `entry_module_id` 过滤。
- [x] 修改 `fastapi_modules/fastapi_leaudit/controllers/govdocController.py`,公文列表接收 `entry_module_id`
- [x] 修改 `fastapi_modules/fastapi_leaudit/services/impl/govdocServiceImpl.py`,公文列表按 `entry_module_id` 过滤。
- [x] 公文列表继续保留公文引擎类型过滤。
### 14.6 后端规则分组和规则绑定任务
- [x] 修改 `fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py`,规则分组列表支持按入口模块过滤。
- [ ] 一级分组创建时必须有 `entry_module_id`
- [x] 一级分组不允许绑定规则集。
- [x] 二级分组创建时必须有 `document_type_id`
- [x] 二级分组必须挂在当前入口模块下的一级分组下面。
- [x] 规则绑定保存接口只允许传二级分组 `group_id`
- [x] 规则绑定保存接口拒绝一级分组。
- [x] 规则绑定仍保存到 `leaudit_rule_group_bindings`
- [x] 评查运行时继续按 `document.group_id -> leaudit_rule_group_bindings -> rule_set` 命中规则。
- [x] 不把 `leaudit_rule_type_bindings` 重新作为新主链路。
### 14.7 前端入口模块管理任务
- [x] 所有新增 UI 必须沿用当前系统组件、CSS 变量、表格、表单、弹窗、按钮风格,不另起视觉体系。
- [x] 修改 `legal-platform-frontend/lib/api/legacy/entry-modules/entry-modules.ts`,增加菜单模板和功能清单类型。
- [x] 修改 `legal-platform-frontend/lib/api/legacy/entry-modules/entry-modules.ts`,入口模块类型增加 `business_scope` 摘要字段。
- [x] 修改 `legal-platform-frontend/lib/api/legacy/entry-modules/request-body.ts`,请求体支持菜单模板和功能清单。
- [x] 修改 `legal-platform-frontend/app/(audit)/entry-modules/EntryModulesClient.tsx`,列表展示菜单模板和功能摘要。
- [x] 修改 `EntryModulesClient.tsx`,业务范围列按 `business_scope` 展示真实大类和业务类型数量,不再写死“待配置”。
- [x] 修改 `legal-platform-frontend/app/(audit)/entry-modules/new/EntryModuleNewClient.tsx`,增加菜单模板选择。
- [x] 修改 `EntryModuleNewClient.tsx`,增加功能勾选区域。
- [x] 选择菜单模板时自动带出默认功能。
- [x] 新建按钮按 `entry_module:create:write` 显示。
- [x] 保存按钮按 `entry_module:update:write` 显示或禁用。
- [x] 删除按钮按 `entry_module:delete:delete` 显示。
- [x] 图标上传按 `entry_module:image:write` 显示或禁用。
- [x] 前端只做体验控制,后端权限点校验必须保留。
### 14.8 前端首页和侧边栏任务
- [x] 修改 `legal-platform-frontend/app/page.tsx`,点击入口模块时保存完整入口上下文。
- [x] 新增 `legal-platform-frontend/lib/auth/entry-module-context.ts`,统一读写入口模块上下文。
- [x] 入口跳转 URL 带 `entryModuleId`
- [x] 入口跳转 URL 带 `documentTypeIds`
- [x] 修改 `legal-platform-frontend/lib/auth/entry-module-menu.ts`,把功能编码转换为菜单项。
- [x] 入口模块支持 `cross_checking` 菜单模板和交叉评查功能编码。
- [x] 修改 `legal-platform-frontend/components/layout/Sidebar.tsx`,侧边栏按入口模块功能清单生成菜单。
- [x] 删除 `selectedModuleName.includes("合同")`
- [x] 删除 `selectedModuleName.includes("公文")`
- [x] 删除按 `/govdoc` 路径强行判断公文菜单的逻辑。
- [x] 侧边栏生成菜单后继续经过用户页面权限过滤。
- [x]`entry_module:list:read` 才显示入口模块管理菜单。
### 14.9 前端公文、上传、规则分组任务
- [x] 公文、上传、规则分组页面只补业务上下文和过滤逻辑,不重做页面视觉。
- [x] 修改 `legal-platform-frontend/components/govdoc-audit/audits.tsx`,从 URL 或入口上下文读取 `entryModuleId`
- [x] 修改 `legal-platform-frontend/lib/api/govdoc-audit/api.ts`,请求参数带 `entry_module_id`
- [x] 公文页面标题使用当前入口模块名称,不再写死“内部公文”。
- [x] 修改 `legal-platform-frontend/app/(audit)/files/upload/FilesUploadClient.tsx`,上传表单带 `entryModuleId`
- [x] 上传页选择二级分组后传 `groupId`
- [x] 上传页在存在多个二级分组时必须让用户明确选择。
- [x] 修改 `legal-platform-frontend/app/(audit)/rule-groups/RuleGroupsClient.tsx`,规则分组页读取当前入口模块。
- [x] 规则分组页只展示当前入口模块下的一级分组。
- [x] 规则绑定入口只出现在二级分组上。
### 14.10 测试和验收任务
- [x] 验证入口模块名称不包含“合同”,但配置合同功能后侧边栏仍显示模板搜索和模板列表。
- [x] 验证入口模块名称不包含“公文”,但配置公文功能后仍进入公文列表。
- [x] 验证没有模板页面权限时,即使入口模块启用模板功能,侧边栏也不显示模板菜单。
- [x] 验证云浮租户只能看到分配给云浮或 `PUBLIC` 的入口模块。
- [x] 验证梅州租户只能看到分配给梅州或 `PUBLIC` 的入口模块。
- [x] 验证没有 `entry_module:create:write` 的用户不能创建入口模块。
- [x] 验证没有 `entry_module:update:write` 的用户不能编辑入口模块。
- [x] 验证没有 `entry_module:delete:delete` 的用户不能删除入口模块。
- [x] 验证普通文档列表按入口模块隔离。
- [x] 验证公文列表按入口模块隔离。
- [x] 验证上传文档写入 `entry_module_id/type_id/group_id/tenant_code`
- [x] 验证规则绑定不能挂一级分组。
- [x] 验证规则绑定能挂二级分组。
- [x] 验证入口模块列表业务范围列展示真实绑定摘要,不再全部显示“待配置”。
- [x] 验证评查按文档二级分组命中正确规则集。
### 14.10.1 执行记录 2026-05-23
- [x] 使用 Playwright 真实账号 `000/admin06111` 登录验证首页入口和侧边栏。
- [x] 验证点击 `合同评查` 后进入 `/documents/list?entryModuleId=1&documentTypeIds=...`
- [x] 验证侧边栏顺序为:首页、文件上传、文档列表、规则管理。
- [x] 验证文档列表菜单不重复。
- [x] 验证上传文档按钮可见。
- [x] 使用 Playwright 跑优先项 `2/3/4/5``node /tmp/leaudit-playwright-priority-2-5-v3.js`,结果 `25 pass / 0 fail`
- [x] 修复二级分组绑定规则集 500`evaluationPointGroupServiceImpl._get_binding_row` SQL 中 `{access_filter}` 未注入。
- [x] 回归验证:二级分组绑定规则集返回 200。
- [x] 回归验证:`pytest -q tests/test_rule_group_binding_scope.py` 结果 `4 passed`
- [x] 回归验证:`legal-platform-frontend``npx tsc --noEmit --pretty false` 通过。
- [x] 回归验证:目标 eslint 结果 `0 error / 6 warnings`warning 为既有 `<img>`、未使用变量类提示。
- [x] 使用 `./leaudit.sh status` 确认后端、前端、Worker、Beat 均运行。
- [x] 修复公文入口侧边栏仍使用旧“系统概览/文件管理”菜单的问题,统一为入口模块菜单:首页、公文列表、公文上传、规则分组。
- [x] 修复公文入口侧边栏混入普通“文件上传”的问题,避免和“公文上传”重复或语义冲突。
- [x] 使用 Playwright 真实账号 `000/admin06111` 跑页面入口 smoke`node /tmp/leaudit-playwright-page-entry-suite.js`,结果 `31 pass / 0 fail`
- [x] 页面入口 smoke 覆盖:合同入口名称不含“合同”仍显示模板搜索/模板列表;公文入口名称不含“公文”仍进入 `/govdoc/audits`;自定义入口只显示首页/文件上传/文档列表;三类入口刷新后不丢上下文。
- [x] 使用 Playwright 跑优先项 `2/3/4/5``node /tmp/leaudit-playwright-priority-2-5-v3.js`,结果 `25 pass / 0 fail`
- [x] 优先项覆盖:上传写入 `entryModuleId/typeId/groupId/tenantCode`、入口 A/B 文档列表隔离、规则分组按入口过滤、一级分组拒绝绑定、二级分组可绑定、无入口管理权限接口返回 403。
- [x] 补充交叉评查入口模块配置能力:入口模块路由下拉支持 `/cross-checking`,菜单模板支持 `cross_checking`,功能清单支持 `cross_checking/cross_checking_upload/cross_checking_list`
- [x] 修复首页交叉评查入口仍走旧“写死交叉评查卡片”的问题,改为按真实入口模块渲染,入口名称、入口 ID、功能清单全部来自 `leaudit_entry_modules`
- [x] 修复 `/cross-checking` 入口点击不带入口上下文的问题,跳转统一进入 `/cross-checking/list?entryModuleId=...`
- [x] 修复交叉评查入口侧边栏混入普通“文件上传”的问题,只显示 `创建任务/评查任务列表` 等交叉评查功能菜单。
- [x] 使用 Playwright 真实账号 `000/admin06111` 验证交叉评查入口模块:`node /tmp/leaudit-playwright-cross-entry.js`,结果 `8 pass / 0 fail`
- [x] 新增数据库只读巡检脚本:`scripts/创建sql/verify_entry_module_menu_profile.sql`,覆盖字段、索引、非法菜单配置、文档入口归属、重复二级分组、未绑定入口模块一级分组。
- [x] 补充公文列表标题逻辑:有入口模块上下文时显示当前入口模块名称,例如 `入口模块-测试文档列表`;无上下文时才兜底 `内部公文文档列表`
- [x] 补充入口模块管理菜单权限过滤:`/entry-modules` 设置子菜单必须带 `entry_module:list:read` 才显示。
- [x] 回归验证:`node --test --experimental-strip-types tests/govdoc-audit/govdoc-entry-title.test.mts` 结果 `2 pass / 0 fail`
- [x] 回归验证:`node --test --experimental-strip-types tests/govdoc-audit/settings-menu-permission.test.mts` 结果 `1 pass / 0 fail`
- [x] 修复 `Sidebar` 中无入口上下文时按 `/govdoc` 路径强行套公文菜单的问题;公文菜单只由入口模块 `menuProfile/features` 决定。
- [x] 使用 Playwright 真实账号 `000/admin06111` 跑剩余项验收:`node test-results/leaudit-playwright-entry-module-remaining-acceptance.js`,结果 `11 pass / 0 fail`
- [x] 剩余项验收覆盖:名字不含“公文”的 govdoc 入口可进入;公文列表标题使用当前入口名称;侧栏只显示 `公文列表/公文上传/规则分组`;无 `entry_module:image:write` 的用户上传入口模块图标返回 403。
### 14.10.2 下一步验收和开发安排
- [x] Playwright 页面级验收入口模块管理页:新建入口、编辑入口、功能勾选保存、功能回显、租户回显。
- [x] Playwright 页面级验收上传页:文件上传菜单排第二,真实表单选择文档类型和二级分组后提交。
- [x] Playwright 页面级验收规则分组页:入口 A/B 切换后页面树隔离,一级分组不出现绑定入口,二级分组绑定弹窗可用。
- [x] Playwright 页面级验收公文入口:入口名称不含“公文”但配置 `govdoc_audits/govdoc_upload` 后,公文列表和上传可进入。
- [x] Playwright 页面级验收公文详情页:确认不出现平台 Sidebar + 公文工作区 Sidebar 双侧栏。
- [x] 多租户真实账号验收:云浮、揭阳、梅州分别只看到已分配或 `PUBLIC` 入口模块。
- [x] 多租户权限验收:没有入口模块管理权限的租户用户不能在 UI 和接口层创建、编辑、删除入口模块。
- [x] 评查命中验收:上传带二级分组的文档后,启动评查,确认命中该二级分组绑定规则集。
### 14.10.3 当前最新状态 2026-05-23
当前入口模块主链路已经完成:
- [x] 入口模块不再靠名称包含“合同/公文”决定菜单。
- [x] 入口模块支持 `document_review/contract/govdoc/cross_checking/custom` 菜单模板。
- [x] 入口模块支持 `features` 控制首页、上传、文档列表、规则、规则分组、模板、公文、交叉评查菜单。
- [x] 首页点击入口后会保存完整入口上下文,并在 URL 带 `entryModuleId/documentTypeIds`
- [x] 侧边栏菜单由入口模块 `menuProfile/features` 生成,再经过用户页面权限过滤。
- [x] 普通文档上传和列表已经接入 `entry_module_id/type_id/group_id/tenant_code`
- [x] 公文列表接口已经支持 `entry_module_id`,页面标题已经改为当前入口模块名称。
- [x] 规则分组列表已经支持入口模块过滤,规则绑定只允许挂二级分组。
- [x] 入口模块管理权限不硬编码角色名,创建、编辑、删除、上传图标走 RBAC 权限点。
- [x] 数据库迁移脚本和只读验证 SQL 已补齐。
当前仍然需要验收或收口:
- [x] 模板页面权限过滤:入口模块启用模板功能,但用户没有模板页面权限时,侧边栏不能显示模板菜单。
- [x] 多租户真实账号验收:云浮、揭阳、梅州只能看到自己租户或 `PUBLIC` 入口。
- [x] 公文列表数据隔离:同一租户下两个公文入口分别进入时,列表数据不能串入口。
- [x] 公文详情页侧栏:确认不出现平台 Sidebar 和公文工作区 Sidebar 双侧栏。
- [x] 入口模块管理页真实表单验收:新建、编辑、功能勾选、租户回显必须跑 Playwright。
- [x] 上传页真实表单验收:从入口进入后,选择文档类型和二级分组提交,后端落库字段正确。
- [x] 规则分组页真实页面验收:入口 A/B 切换后树隔离,一级分组无绑定入口,二级分组绑定弹窗可用。
- [x] 评查命中验收:上传带二级分组的文档后启动评查,确认按该二级分组命中规则集。
当前暂缓硬改:
- [ ] `一级分组创建时必须有 entry_module_id` 仍保留兼容口径,避免旧全局分组或历史数据立即断链。下一轮只先补告警和数据巡检,不直接硬拦。
### 14.10.4 执行记录 2026-05-24
- [x] 清理 Playwright 和手工验收产生的测试入口模块、测试业务大类、测试规则分组。
- [x] 测试数据清理采用软删除,未硬删历史文档,避免破坏审计记录和外键引用。
- [x] 已软删除测试入口模块 31 个:`37,39-68`
- [x] 已软删除测试业务大类 24 个:`24-47`
- [x] 已软删除测试规则分组 71 个:`52-122`
- [x] 已软删除入口租户关联 34 条、规则分组绑定 3 条。
- [x] 反查验证:未删除测试入口模块 0 个、未删除测试业务大类 0 个、未删除测试规则分组 0 个、测试入口有效租户关联残留 0 条、测试分组有效规则绑定残留 0 条。
- [x] 修复入口模块列表“业务范围”列全部显示“待配置”的问题。
- [x] 根因确认:数据库中合同评查、案卷智能评查、内部公文已有业务范围绑定,但 `EntryModulesClient.tsx` 把业务范围列写死为“待配置”,后端入口模块列表也没有返回业务范围摘要。
- [x] 后端 `EntryModuleVO` 增加 `business_scope`,入口模块列表和详情返回业务大类数量、业务类型数量、大类名称列表。
- [x] `entryModuleAdminServiceImpl.py``business_scope` 查询兼容两种历史结构:通过 `leaudit_document_types.entry_module_id` 绑定业务大类,以及通过一级规则分组 `entry_module_id` 统计二级业务类型。
- [x] 前端 `EntryModule` 类型增加 `business_scope`
- [x] 前端新增 `summarizeBusinessEntryScope()`,入口模块列表按真实业务范围展示,例如“建设工程合同、买卖合同、借款合同 等 10 类 / 覆盖 10 个业务类型”。
- [x] 真实数据库只读验证:合同评查 `10` 个业务大类、`10` 个业务类型;案卷智能评查 `10` 个业务大类、`10` 个业务类型;内部公文 `1` 个业务大类、`1` 个业务类型;智慧法务助手和交叉评查当前未配置业务范围,显示“待配置”符合数据事实。
- [x] 回归验证:`node --test --experimental-strip-types tests/govdoc-audit/business-entry-ui.test.mts` 结果 `7 pass / 0 fail`
- [x] 回归验证:`legal-platform-frontend``npx tsc --noEmit --pretty false` 通过。
- [x] 回归验证:`.venv/bin/python -m py_compile fastapi_modules/fastapi_leaudit/domian/vo/entryModuleAdminVo.py fastapi_modules/fastapi_leaudit/services/impl/entryModuleAdminServiceImpl.py` 通过。
- [x] 回归验证:目标 eslint 通过,`0 error / 0 warning`
- [x] RBAC 数据调整:删除 `省级管理员 / provincial_admin` 角色。
- [x] RBAC 删除级联清理:`user_role` 1 条、`role_permissions` 94 条、`role_route` 29 条。
- [x]`000` 对应用户调整为系统超级管理员:当前库中该用户为 `sso_users.id=5, sub=000, username=admin`
- [x] RBAC 反查验证:`provincial_admin` 剩余 0 个;`000` 用户当前只绑定 `super_admin / 系统超级管理员 / data_scope=ALL`
- [x] 使用 Playwright 真实账号 `000/admin06111` 验收入口模块编辑页和上传页:`LEAUDIT_FRONTEND_URL=http://127.0.0.1:5193 node /tmp/leaudit-playwright-entry-upload-management-v1.js`,结果 `16 pass / 0 fail`
- [x] 入口模块编辑页验收覆盖:4 步表单、菜单模板回显、功能勾选回显、PUBLIC 租户回显、业务范围回显。
- [x] 上传页验收覆盖:从入口上下文进入后按入口加载业务大类和业务类型,显示所属业务入口,上传接口和文档详情接口返回正确 `entryModuleId/typeId/groupId/tenantCode`
- [x] 修复上传页单一业务类型时查不到二级分组的问题:`FilesUploadClient.tsx` 不再只在 `childDocumentTypeIds.length > 1` 时批量查询二级分组。
- [x] 后端文档上传、列表、详情 VO 返回 `entryModuleId`,方便前端和测试确认文档入口归属。
- [x] 修复公文详情页双侧栏风险:`/govdoc/detail/...``/govdoc-audit/detail/...` 不再渲染平台 Sidebar,只保留公文工作区布局。
- [x] 回归验证:`.venv/bin/python -m py_compile fastapi_modules/fastapi_leaudit/domian/vo/documentVo.py fastapi_modules/fastapi_leaudit/services/impl/documentServiceImpl.py` 通过。
- [x] 回归验证:`legal-platform-frontend``npx tsc --noEmit --pretty false` 通过。
- [x] 回归验证:目标 eslint 通过,`0 error / 1 warning`warning 为 `files-upload.ts` 被 eslint ignore,非本次阻塞。
- [x] 本轮 Playwright 临时数据已清理:`PW入口编辑上传-pwui%``pw.root.pwui%``pw.child.pwui%``pw.type.pwui%``pw-ui-upload-pwui%` 有效残留均为 0。
- [x] 环境记录:`./leaudit.sh` 本轮出现 pidfile/后台进程状态不稳定,真实验收改用 TTY 手动启动后端 `run.py` 和前端 `npm run dev:dev`Playwright 统一访问 `http://127.0.0.1:5193`
- [x] 使用 Playwright 真实账号 `000/admin06111` 验收规则分组页:`LEAUDIT_FRONTEND_URL=http://127.0.0.1:5193 node /tmp/leaudit-playwright-rule-groups-scope-v1.js`,结果 `21 pass / 0 fail`
- [x] 规则分组页验收覆盖:入口 A/B 树隔离、请求带 `entry_module_id`、一级分组不显示“配置规则”、后端拒绝一级分组绑定规则集、二级分组绑定弹窗可打开并保存。
- [x] 规则分组页验收发现一个非产品 BUG:临时脚本第一次按旧弹窗类名 `.modal-content/[role=dialog]` 断言失败;当前页面实际使用 `.rg-modal`,修正脚本后通过。
- [x] 规则分组 Playwright 临时数据已清理:`PW规则分组入口%``pw.rg.type.%``pw.rg.%``note like '%pwrg%'` 有效残留均为 0。
- [x] 使用 Playwright 真实账号 `000/admin06111` 验收公文入口深度链路:`LEAUDIT_FRONTEND_URL=http://127.0.0.1:5193 node /tmp/leaudit-playwright-govdoc-entry-scope-v1.js`,结果 `17 pass / 0 fail`
- [x] 公文入口深度验收覆盖:创建 A/B 公文入口、创建 A/B 公文业务类型、A/B 公文列表请求带 `entry_module_id`、A/B 列表数据不串、标题使用当前入口模块名称、公文上传链接携带 `entryModuleId/documentTypeIds`、公文上传落库携带 `entry_module_id/type_id`、公文详情页不渲染平台 Sidebar。
- [x] 修正公文 Playwright 验收脚本:不再用 `request.postData()` 判断 multipart 表单,因为该方式对文件上传体不稳定;改为上传后解析新 `documentId` 并查询数据库验证 `leaudit_documents.entry_module_id/type_id`
- [x] 公文 Playwright 临时数据已清理:`PW公文入口%``pw.govdoc.type.%``pw-govdoc-%` 有效文档残留均为 0。
- [x] 发现并记录一个非阻塞清理差异:公文删除接口会软删 `leaudit_documents`,但 `leaudit_document_files` 中历史测试文件记录仍保持 `deleted_at is null`;当前列表以文档主表删除态过滤,不影响入口隔离验收,后续如要严格清理可单独优化删除服务。
- [x] 使用 Playwright 真实账号验收多租户入口可见性和普通权限:`LEAUDIT_FRONTEND_URL=http://127.0.0.1:5193 node /tmp/leaudit-playwright-multitenant-entry-scope-v1.js`,结果 `37 pass / 0 fail`
- [x] 多租户验收账号:`yf001/yfyc06111``jy001/jyyc06111``mz001/mzyc06111``001/gdyc06111`
- [x] 多租户验收覆盖:云浮只看 `合同评查/案卷智能评查/内部公文`;揭阳只看 `合同评查/案卷智能评查/内部公文/交叉评查`;梅州地区管理员看 `合同评查/案卷智能评查/内部公文/智慧法务助手/交叉评查`;普通用户 `001` 看不到 `智慧法务助手`
- [x] 权限验收覆盖:普通用户 `001` 首页不显示系统设置按钮,直调 `/api/v3/entry-modules` 返回 403。
- [x] 使用 Playwright APIRequestContext + 数据库只读查询验收评查命中链路:真实账号 `mz001/mzyc06111` 上传 `legal-platform-frontend/public/testWork/(最终版)智慧法务平台建设采购项目合同(1).docx`,传入 `typeId=1/groupId=2/entryModuleId=1/tenant_code=MZ/autoRun=true`
- [x] 评查命中验收结果:上传生成 `documentId=232/runId=110``leaudit_documents` 写入 `type_id=1/group_id=2/entry_module_id=1/tenant_code=MZ/current_run_id=110`
- [x] 规则集命中数据库证据:`leaudit_audit_runs.id=110` 写入 `rule_set_id=126/rule_version_id=500/group_id_snapshot=2/rule_binding_id_snapshot=101/tenant_code=MZ/scope_type_snapshot=TENANT`,符合 `leaudit_rule_group_bindings.group_id=2` 下梅州租户绑定。
- [x] 使用 Playwright 真实账号 `001/gdyc06111` 验收模板页面权限过滤:进入 `合同评查` 后 URL 为 `/documents/list?entryModuleId=1&documentTypeIds=...`,侧栏显示 `文件上传/文档列表`,不显示 `合同管理/模板搜索/模板列表`
- [x] 模板权限验收补充证据:`001``common` 角色,无 `contract_template:*``entry_module:*` 权限;`/api/auth/session-data` 返回的合同入口 `features` 已被裁剪为 `home/documents/upload/rules/rule_groups``permissionMap` 不含 `/contract-template` 路由。
- [x] RBAC 权限数据收口:已按“入口模块只能系统超级管理员维护”口径移除 `admin / 地区管理员``entry_module:create/update/delete/image/list/detail` 权限和 `/entry-modules` 菜单。
- [x] 新增可重复执行 SQL`scripts/创建sql/rbac_entry_module_super_admin_only.sql`,用于保证 `super_admin` 保留入口模块权限,同时移除 `admin/provincial_admin` 的入口模块权限和菜单。
- [x] 修复首页系统设置按钮跳转:不再固定跳 `/entry-modules`,而是跳当前用户有权限的第一个设置子菜单;没有任何设置子菜单时不显示系统设置按钮。
- [x] RBAC 收口真实验收:`mz001/mzyc06111` 登录后无 `entry_module:*` 权限,直调 `/api/v3/entry-modules` 返回 403`settingsChildren` 不含 `/entry-modules`
- [x] RBAC 收口真实验收:`000/admin06111` 登录后仍有入口模块 6 个权限,直调 `/api/v3/entry-modules` 返回 200`settingsChildren` 包含 `/entry-modules`
### 14.10.5 当前最新状态 2026-05-24
当前已完成并验证:
- [x] 入口模块列表业务范围列已经接真实后端摘要,不再统一显示“待配置”。
- [x] 合同评查、案卷智能评查、内部公文的业务范围摘要和当前数据库绑定一致。
- [x] 智慧法务助手、交叉评查当前没有绑定业务大类,所以继续显示“待配置”是正确状态。
- [x] 测试入口模块和测试业务大类已经清理,不会继续污染入口模块管理页和业务大类管理页。
- [x] `000/admin06111` 对应账号已变成系统超级管理员,后续 Playwright 验收使用该账号时具备系统级管理权限。
- [x] `provincial_admin` 已删除,后续计划和验收不要再依赖省级管理员这个角色。
- [x] 入口模块管理页和上传页已经完成真实 Playwright 验收,上传链路能落 `entry_module_id/type_id/group_id/tenant_code`
- [x] 规则分组页已经完成真实 Playwright 验收,入口 A/B 不串树,一级不绑定规则,二级绑定弹窗可用。
- [x] 公文详情页双侧栏代码风险已修复,并已用真实公文详情页面 Playwright 确认。
- [x] 多租户真实账号验收已完成,云浮、揭阳、梅州入口可见性符合当前入口模块租户分配和 RBAC 菜单权限。
- [x] 模板页面权限过滤已完成真实 Playwright 验收,普通用户没有模板页面权限时,即使进入合同入口也不显示模板菜单。
- [x] 评查命中验收已完成,普通文档上传后按 `document.group_id -> leaudit_rule_group_bindings -> rule_set/current_version_id` 命中租户级规则集。
后续仍需继续验收:
- [x] RBAC 权限数据收口:已从 `admin / 地区管理员` 移除入口模块管理权限,当前只有系统超级管理员可以维护入口模块。
### 14.10.6 下一轮执行顺序
前置调整:
```text
真实前端 Playwright 全量验收前,先执行入口模块和文档类型的 UI/交互优化计划:
docs/superpowers/plans/2026-05-23-business-entry-ux-optimization.md
```
原因:
```text
当前“入口模块管理 + 文档类型管理 + 规则分组 + 上传”的技术语义学习成本过高。
如果先写真实前端测试,会把当前复杂交互固化下来。
应先把页面统一成“业务入口、业务大类、业务类型、规则配置”的用户语义,再做真实 Playwright 验收。
```
按下面顺序继续,不要先碰评查引擎:
1. P0:入口模块管理页 Playwright 验收。
验收内容:
```text
新建入口 -> 选择菜单模板 -> 勾选功能 -> 分配租户 -> 保存 -> 列表回显 -> 编辑回显
```
输出要求:
```text
已完成。继续维护时如果页面字段或回显错,先修前端表单和接口字段映射,不改运行时评查逻辑。
```
2. P1:上传页真实表单 Playwright 验收。
验收内容:
```text
从入口模块进入上传页 -> 文件上传菜单排第二 -> 选择文档类型 -> 选择二级分组 -> 提交
```
输出要求:
```text
已完成。验收确认数据库或接口返回里存在 entry_module_id/type_id/group_id/tenant_code。
```
3. P2:规则分组页 Playwright 验收。
验收内容:
```text
入口 A 只看到 A 的一级分组
入口 B 只看到 B 的一级分组
一级分组不出现绑定规则入口
二级分组可以打开绑定弹窗并保存
```
输出要求:
```text
已完成。验收确认前端透传 `entryModuleId`,后端列表过滤生效,一级/二级规则绑定边界正确。
```
4. P3:公文入口深度验收。
验收内容:
```text
公文列表按 entry_module_id 隔离
公文上传能从入口模块进入
公文详情页不出现双侧栏
```
输出要求:
```text
如果列表串数据,优先查 govdoc API 请求参数和 GovdocServiceImpl 查询过滤。
```
5. P4:多租户真实账号验收。
验收内容:
```text
云浮用户只看到云浮或 PUBLIC 入口
揭阳用户只看到揭阳或 PUBLIC 入口
梅州用户只看到梅州或 PUBLIC 入口
没有入口模块管理权限的租户用户 UI 不显示管理能力,接口返回 403
```
输出要求:
```text
如果缺少真实账号,先用现有 RBAC/租户数据造最小测试账号,并记录账号和租户。
```
6. P5:评查命中验收。
验收内容:
```text
上传带二级分组的文档 -> 绑定该二级分组规则集 -> 启动评查 -> 确认命中该规则集
```
输出要求:
```text
这里只验证 group_id -> leaudit_rule_group_bindings -> rule_set 主链路,不重构评查引擎。
```
### 14.10.7 下一轮可并行安排
可以拆成 3 个并行工作流。当前 3 个工作流均已完成,本节保留为后续复盘参考:
- [x] 工作流 A:入口模块管理页 + 上传页 Playwright 验收,主要看表单和字段落库。
- [x] 工作流 B:规则分组页 + 公文入口深度验收,主要看入口上下文过滤和侧栏。
- [x] 工作流 C:多租户真实账号 + 模板权限过滤 + 评查命中验收,主要看权限、租户和运行时规则命中。
并行约束:
- [x] 三个工作流都必须先执行 `./leaudit.sh status`,确认后端、前端、Worker、Beat 运行。
- [x] UI 验收必须使用 Playwright,账号优先使用 `000/admin06111`,多租户验收再补具体租户账号。
- [x] 每个工作流完成后都要回写本计划文档的执行记录。
- [x] 发现 BUG 先补最小复现脚本,再修代码,最后跑对应 Playwright 或 pytest 回归。
### 14.11 建议提交顺序
1. 数据库迁移脚本。
2. 后端 DTO/VO 和入口模块服务。
3. 首页入口返回和前端入口上下文。
4. 侧边栏菜单生成逻辑。
5. 入口模块管理页菜单模板和功能勾选。
6. 文档上传和列表入口模块归属。
7. 公文列表入口模块过滤。
8. 规则分组和规则绑定校验。
9. 删除名称包含“合同/公文”的旧判断。
10. 跑完整验收清单。
### 14.12 暂不执行的任务
- [ ] 暂不做同一个入口模块不同租户不同功能清单。
- [ ] 暂不删除旧地区字段。
- [ ] 暂不删除旧规则类型绑定表。
- [ ] 暂不重写评查运行引擎。
- [ ] 暂不把入口模块变成权限系统。
### 14.13 下一阶段收口计划:entry_module_id 强一致性
#### 14.13.1 先把概念说清楚
`entry_module_id` 不是给用户看的新概念,它在前端应该叫“业务入口”或“工作台”。
它的作用只有一个:让系统知道一条数据属于哪个入口模块。
如果没有 `entry_module_id`,系统只能靠下面这些不稳定方式猜:
```text
入口名字里有没有“合同”
当前 URL 是不是 /govdoc
文档类型刚好属于哪个入口
规则分组刚好挂在哪个父节点下面
```
这些猜法会导致同一个租户下多个入口互相串数据,例如:
```text
合同入口能看到案卷文档
公文入口侧边栏混入普通文件上传
规则分组 A/B 入口互相出现
上传后评查命中错规则集
```
所以新链路必须明确落字段:
```text
入口模块 leaudit_entry_modules.id
-> 业务大类/一级分组 leaudit_evaluation_point_groups.entry_module_id
-> 业务类型/二级分组 group_id
-> 文档 leaudit_documents.entry_module_id + group_id
-> 评查按 group_id 命中规则绑定
```
用户理解口径:
```text
我从哪个业务入口进来,上传、列表、规则配置就只看这个入口下的数据。
```
开发理解口径:
```text
entry_module_id 是入口隔离字段。
tenant_code 管租户边界。
group_id 管规则命中。
type_id 管文档分类。
features/menu_profile 管菜单显示。
```
#### 14.13.2 当前不直接强拦的原因
当前代码里“一级业务大类必须有 `entry_module_id`”还没有完全强拦,是刻意保留兼容。
原因:
```text
老数据里可能还有全局一级分组
老文档可能只能从 type_id 或 group_id 推导入口
如果现在直接 400,可能会把旧规则配置、旧上传链路或历史列表打断
```
因此下一阶段不能一步到位硬改,必须按下面顺序推进:
```text
先巡检旧数据
再修复或回填孤儿数据
再加后端软提示/日志
最后才把新建和编辑一级业务大类改成必须有入口模块
```
#### 14.13.3 实现顺序
1. 数据巡检。
执行只读巡检脚本:
```bash
psql "$DATABASE_URL" -f scripts/创建sql/verify_entry_module_menu_profile.sql
```
重点看这几类结果:
```text
文档 entry_module_id 为空
文档 entry_module_id 和 group/type 推导不一致
重复二级分组
一级分组 entry_module_id 为空
非法 menu_profile/features
```
2. 旧数据处理。
如果一级分组没有 `entry_module_id`,先按可推导关系回填:
```text
一级分组有 document_type_id -> 用 leaudit_document_types.entry_module_id 回填
一级分组下二级分组有 document_type_id -> 用二级分组对应文档类型的 entry_module_id 回填
确实无法推导 -> 先列清单人工判断,不自动乱填
```
注意:
```text
不允许为了清零数据随便塞 entry_module_id=1
不允许把跨入口共用的旧分组直接强绑到某一个入口
```
3. 后端创建校验收紧。
文件:
```text
fastapi_modules/fastapi_leaudit/services/impl/evaluationPointGroupServiceImpl.py
```
目标:
```text
新建一级业务大类 pid=0 时,必须传 entry_module_id
新建一级业务大类不允许只靠 document_type_id 反推入口
新建二级业务类型 pid!=0 时,继承父级 entry_module_id
新建二级业务类型必须绑定 document_type_id
二级业务类型的 document_type_id 所属入口必须和父级入口一致
```
兼容策略:
```text
只强拦新建/编辑请求
不因为历史数据缺 entry_module_id 导致列表查询直接失败
```
4. 前端规则配置页补齐入口上下文。
文件:
```text
legal-platform-frontend/app/(audit)/rule-groups/RuleGroupsClient.tsx
```
目标:
```text
从业务入口进入规则配置页时,新增业务大类自动带当前 entryModuleId
页面顶部显示“当前业务入口:xxx”
如果没有入口上下文,不允许新增业务大类,只提示从首页业务入口进入
```
用户看到的是:
```text
当前业务入口:合同评查
新增业务大类时自动归属当前入口
```
用户不需要看到:
```text
entry_module_id
pid = 0
group_id
```
5. 上传页保持现有两级选择。
文件:
```text
legal-platform-frontend/app/(audit)/files/upload/FilesUploadClient.tsx
```
目标不变:
```text
先选业务大类
再选业务类型
提交 entryModuleId/typeId/groupId/tenantCode
```
这里不要做自动创建业务类型,也不要跳过业务类型选择。
6. 验收。
必须使用 Playwright 真实账号验收:
```text
000/admin06111:新增业务大类 -> 新增业务类型 -> 配置规则 -> 上传 -> 列表只看当前入口
mz001/mzyc06111:不能进入业务入口管理,能使用已分配入口
yf001/yfyc06111:只能看到云浮已分配入口
jy001/jyyc06111:只能看到揭阳已分配入口
```
数据库验收:
```text
leaudit_evaluation_point_groups 一级分组有 entry_module_id
leaudit_evaluation_point_groups 二级分组能通过父级拿到 entry_module_id
leaudit_documents 上传后有 entry_module_id/type_id/group_id/tenant_code
leaudit_audit_runs 按 group_id_snapshot 命中 rule_binding_id_snapshot
```
#### 14.13.4 推荐下一步直接做什么
下一步不先改代码,先做一次真实数据库巡检并记录结果。
执行顺序:
```text
1. 运行 verify_entry_module_menu_profile.sql
2. 把异常结果整理到本计划文档
3. 如果一级分组 entry_module_id 为空数量为 0,再开始后端强校验
4. 如果不为 0,先补一个只读清单和人工回填 SQL 草案
5. 回填完成后再改后端校验和前端规则配置页
```
这样做的原因:
```text
entry_module_id 是隔离字段,一旦强校验加错,会直接影响上传、规则配置、列表过滤和评查命中。
先把数据状态摸清,再收紧代码,风险最低。
```
#### 14.13.5 巡检记录 2026-05-24
已执行:
```bash
PGPASSWORD='zhfw*123*' psql -h nas.7bm.co -p 54302 -U docauditai_admin -d leaudit_platform -f scripts/创建sql/verify_entry_module_menu_profile.sql
```
同时修正巡检脚本字段错误:
```text
leaudit_documents 没有 filename 字段,实际字段为 normalized_name。
已把脚本中的 d.filename 改为 d.normalized_name AS document_name。
```
巡检结论:
```text
必要字段存在:
leaudit_entry_modules.menu_profile
leaudit_entry_modules.features
leaudit_documents.entry_module_id
必要索引存在:
idx_leaudit_entry_modules_menu_profile
idx_leaudit_documents_entry_module_id
非法 menu_profile0 条
非法 feature 编码:0 条
重复二级分组:0 条
文档 entry_module_id 与 group/type 推导不一致:0 条
```
发现的问题:
```text
案卷智能评查 features 为空
智慧法务助手 features 为空
历史文档 entry_module_id 为空:68 条
其中 65 条可以通过 group/type 推导入口
其中 3 条公文示例没有 type_id/group_id,暂时无法自动推导入口
一级业务大类 entry_module_id 为空:14 条
```
68 条历史文档缺入口归属的推导结果:
```text
可推导为 合同评查 entry_module_id=147 条
可推导为 案卷智能评查 entry_module_id=214 条
可推导为 内部公文 entry_module_id=34 条
无法推导:3 条,均为旧公文示例,type_id/group_id 为空
```
14 条一级业务大类缺入口归属的来源:
```text
行政卷宗 root.casefile:可通过子业务类型推导为 案卷智能评查 entry_module_id=2
testmzceshi:测试残留,无子节点、无 document_type_id,需要人工确认是否删除
12 条 PW*/pw.* 测试业务大类:来自已软删除的 Playwright 临时入口,关联文档类型仍未软删
```
关键判断:
```text
当前正式 5 个入口本身没有结构性问题。
真正阻塞强校验的是历史数据和 Playwright 测试残留。
现在不能直接加“一级业务大类必须有 entry_module_id”的硬拦截,否则旧残留会继续污染巡检,也可能影响旧数据展示。
```
建议下一步:
```text
1. 先出只读清单 SQL,列出可自动回填的 65 条历史文档和 root.casefile。
2. 出数据修复 SQL 草案,但先不执行。
3. Playwright 测试残留建议统一软删:pw.* 文档类型、PW* 规则分组、对应临时入口关系。
4. testmzceshi 需要人工确认是否删除。
5. 3 条无 type_id/group_id 的旧公文示例不要自动回填,保留人工处理。
6. 数据修复完成后,再改后端强校验和前端规则配置页入口上下文。
```
#### 14.13.6 数据修复执行记录 2026-05-24
已新增并执行数据修复脚本:
```text
scripts/创建sql/repair_entry_module_scope_data_20260524.sql
```
执行前做了事务回滚演练:
```text
可回填历史文档:65 条
可回填一级业务大类:1 条,root.casefile 行政卷宗
可继承父级入口的二级业务类型:19 条
可安全软删测试规则分组:25 条
可补默认功能的入口模块:1 条,案卷智能评查
```
正式执行结果:
```text
UPDATE 65
UPDATE 1
UPDATE 19
UPDATE 25
UPDATE 1
COMMIT
```
修复内容:
```text
65 条历史文档已按 group/type 推导回填 entry_module_id。
行政卷宗 root.casefile 已回填 entry_module_id=2,归属案卷智能评查。
行政卷宗及合同正式二级业务类型已继承父级 entry_module_id。
testmzceshi 和 PW*/pw.* 无引用测试规则分组已软删除。
案卷智能评查已补齐默认功能:home/documents/upload/rules/rule_groups。
```
修复后巡检结果:
```text
非法 menu_profile0 条
非法 feature 编码:0 条
文档 entry_module_id 与 group/type 推导不一致:0 条
重复二级分组:0 条
一级业务大类 entry_module_id 为空:0 条
活跃 PW 测试文档类型:0 条
活跃测试规则分组残留:0 条
活跃文档类型指向已删除入口:0 条
```
剩余问题:
```text
仍有 3 条旧公文示例文档 entry_module_id 为空。
这 3 条同时没有 type_id 和 group_id,无法确定属于哪个入口模块。
本次没有自动回填,避免把历史公文示例错误塞进内部公文或其他入口。
```
剩余 3 条:
```text
id=46 公文示例-瑕疵
id=47 公文示例-瑕疵
id=48 公文示例-合规-OOXML修正版
```
后续处理建议:
```text
如果确认它们是内部公文旧测试数据,可以单独回填 entry_module_id=3。
如果只是历史测试样例,也可以软删除。
在未确认前,不建议自动处理。
```
#### 14.13.7 旧公文示例回填记录 2026-05-24
用户已确认 3 条旧公文示例可以回填。
已新增并执行:
```text
scripts/创建sql/repair_govdoc_sample_entry_module_20260524.sql
```
执行条件:
```text
只更新 id IN (46, 47, 48)
只更新 deleted_at IS NULL
只更新 entry_module_id IS NULL
只更新 engine_type='govdoc' 且 review_scope='govdoc'
只回填 entry_module_id=3,不伪造 type_id/group_id
```
执行结果:
```text
UPDATE 3
COMMIT
```
回填后结果:
```text
id=46 公文示例-瑕疵 -> entry_module_id=3
id=47 公文示例-瑕疵 -> entry_module_id=3
id=48 公文示例-合规-OOXML修正版 -> entry_module_id=3
```
最终巡检结果:
```text
documents_without_entry_module = 0
文档 entry_module_id 与 group/type 推导不一致 = 0
一级业务大类 entry_module_id 为空 = 0
重复二级分组 = 0
非法 menu_profile = 0
非法 feature 编码 = 0
```