feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
@@ -0,0 +1,461 @@
# 入口模块租户配置表迁移方案
> 适用范围:首页入口模块、入口模块管理页、首页可见性判定、交叉评查入口等依赖 `leaudit_entry_modules.areas` 的场景
> 文档定位:把当前入口模块的 `areas JSONB` 从自由字符串配置升级为“租户关系表 + 主数据校验”的落地方案。
---
## 1. 结论先行
当前入口模块是整个“地区租户化”里最明显、最优先、最适合先落地的切入点。
原因有 4 个:
1. 问题最直观,用户已经明确感知到“新租户没法分配入口”
2. 当前设计最脆弱,`areas JSONB` 没有主数据约束
3. 前后端都存在固定地区假设
4. 首页和交叉评查都依赖它做可见性判断
因此建议先把入口模块从:
1. `leaudit_entry_modules + areas JSONB`
升级为:
1. `leaudit_entry_modules`
2. `leaudit_entry_module_tenants`
3. `sys_tenants`
---
## 2. 当前问题深挖
## 2.1 现有表结构问题
当前表:
```sql
leaudit_entry_modules(
id,
name,
description,
path,
icon_path,
areas JSONB,
sort_order,
is_enabled
)
```
问题:
1. `areas` 是自由 JSON
2. `area` 值没有外键约束
3. `enabled/sort_order` 与租户主数据重复
4. 无法防止写入不存在的租户
5. 无法直接做关系查询和索引优化
## 2.2 后端当前风险
当前 `EntryModuleAdminServiceImpl.py`
1. 创建和更新时直接把前端传入 `areas` 序列化成 JSON
2. 没有合法租户校验
3. 没有和用户租户主数据对齐
当前 `homeServiceImpl.py`
1. 直接按 `user_area = area_item->>'area'`
2. 支持 `default` 兜底
3. `super_admin` 特判 bypass
这意味着:
1. 可见性规则绑定在脏字符串上
2. 新租户即使进库,首页也可能因为字符串不一致看不到
## 2.3 前端当前风险
当前 `EntryModuleNewClient.tsx`
1. 地区候选列表硬编码为固定 5 项
当前 `cross-checking-access.ts`
1. 用固定地区别名数组判断入口可见性
2. `provincial_admin` 自动补 `省局`
这意味着:
1. 后端就算支持新租户,前端也无法选择
2. 首页入口和交叉评查入口会出现不一致
---
## 3. 目标数据模型
## 3.1 保留主表
保留:
1. `leaudit_entry_modules`
用于表达模块本身:
1. 名称
2. 描述
3. 跳转路径
4. 图标
5. 排序
6. 启用状态
## 3.2 新增关系表
建议新增:
```sql
CREATE TABLE IF NOT EXISTS leaudit_entry_module_tenants (
id BIGSERIAL PRIMARY KEY,
entry_module_id BIGINT NOT NULL REFERENCES leaudit_entry_modules(id),
tenant_code VARCHAR(64) NOT NULL,
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INT NOT NULL DEFAULT 0,
visibility_scope VARCHAR(32) NOT NULL DEFAULT 'TENANT',
ext JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP NULL,
UNIQUE (entry_module_id, tenant_code)
);
```
## 3.3 为什么关系表足够
关系表解决了当前 JSON 方案的关键缺陷:
1. 可按 `tenant_code` 精确筛选
2. 可直接做唯一约束
3. 可扩展更多可见性字段
4. 可做索引和分页查询
5. 能和租户主数据自然联表
---
## 4. 字段设计建议
## 4.1 `tenant_code`
必须来自 `sys_tenants.tenant_code`,不允许任意字符串。
## 4.2 `is_enabled`
表示该入口是否对该租户生效。
说明:
1. 模块总开关在 `leaudit_entry_modules.is_enabled`
2. 租户粒度开关在 `leaudit_entry_module_tenants.is_enabled`
## 4.3 `sort_order`
表示该模块在该租户视角下的局部排序。
如果当前不需要租户级排序,也建议先保留,因为后续:
1. 不同租户首页布局可能不同
2. 试点租户可能需要不同优先级
## 4.4 `visibility_scope`
建议预留:
1. `TENANT`
2. `PUBLIC`
3. `HEADQUARTER_ONLY`
当前先以 `TENANT` 为主,但保留扩展空间,避免以后再次改表。
---
## 5. 接口改造建议
## 5.1 管理端列表接口
当前:
1. `GET /api/v3/entry-modules?area=...`
建议后续改为:
1. `GET /api/v3/entry-modules?tenant_code=...`
兼容期:
1. `area` 仍可保留,但后端先归一成 `tenant_code`
## 5.2 管理端创建/更新接口
当前请求体:
```json
{
"name": "交叉评查",
"route_path": "/cross-checking",
"areas": [
{"area": "梅州", "enabled": true, "sort_order": 1}
]
}
```
建议升级为:
```json
{
"name": "交叉评查",
"route_path": "/cross-checking",
"tenants": [
{"tenant_code": "MZ", "enabled": true, "sort_order": 1}
]
}
```
兼容策略:
1. 后端 DTO 同时接收 `areas``tenants`
2. 兼容期内优先使用 `tenants`
3. `areas` 走别名归一后再写关系表
## 5.3 首页获取入口接口
当前:
1. `HomeServiceImpl.GetEntryModules(UserId)`
建议内部改造为:
1. 先解析用户 `tenant_code`
2. 再根据 `leaudit_entry_module_tenants` 判断可见性
SQL 方向应改成:
1. 联表 `leaudit_entry_module_tenants`
2. 不再扫描 `jsonb_array_elements`
---
## 6. 前端改造建议
## 6.1 入口模块新建页
当前问题:
1. `AREA_OPTIONS` 写死
建议:
1. 页面加载时调用租户主数据接口
2.`tenant_code` 作为 value
3.`tenant_name` 作为 label
4. 表单字段从 `selectedAreas` 改为 `selectedTenantCodes`
## 6.2 入口模块列表页筛选
建议:
1. 筛选条件改为租户下拉
2. 后端接收 `tenant_code`
3. 列表展示使用租户名称而非裸字符串
## 6.3 交叉评查入口判定
当前 `cross-checking-access.ts` 不应再:
1. 猜测地区别名
2. `includes("省")`
3. 针对 `provincial_admin` 自动补 `省局`
建议:
1. 登录态透出 `tenant_code`
2. 首页接口直接返回“当前用户可见模块”
3. 前端不再做租户匹配推断
---
## 7. 迁移策略
## 7.1 第一步:建新表,不改老表
先新增:
1. `leaudit_entry_module_tenants`
此时:
1.`areas JSONB` 继续存在
2. 新服务层支持双写
## 7.2 第二步:把历史 JSON 展开入新表
迁移逻辑:
1. 读取 `leaudit_entry_modules.areas`
2. 对每个 `area` 执行租户归一
3. 写入 `leaudit_entry_module_tenants`
4. 对映射失败的值输出复核清单
## 7.3 第三步:服务层读新表
顺序建议:
1. 管理端详情/列表先从新表组装返回
2. 首页可见性改为读新表
3. 交叉评查入口依赖首页接口结果,不再本地推断
## 7.4 第四步:前端提交新结构
前端提交统一改成 `tenant_code` 后:
1. `areas` 字段只保留兼容
2. 新表成为唯一写入目标
## 7.5 第五步:下线旧 JSON
待所有读写都切到新表后:
1. `areas` 字段可以保留为快照
2. 或在后续版本彻底废弃
---
## 8. 兼容期设计
## 8.1 DTO 兼容
建议 DTO 增加:
1. `tenants: list[EntryModuleTenantDTO] | None`
保留:
1. `areas: list[EntryModuleAreaDTO] | None`
优先级:
1. `tenants` 优先
2. `areas` 仅用于历史前端兼容
## 8.2 VO 兼容
接口返回建议同时包含:
1. `tenants`
2. `areas`
其中:
1. `tenants` 是标准结构
2. `areas` 是兼容展示结构
避免一次性打崩旧页面。
## 8.3 SQL seed 兼容
当前:
1. `seed_home_entry_modules.sql`
2. `seed_govdoc_entry_module.sql`
都直接写死 `areas JSONB`
建议:
1. 先保留旧插入
2. 新增对应关系表 seed
3. 后续把内置入口的租户分配从 JSON 迁到关系表
---
## 9. 需要同步改的代码点
后端重点:
1. `entryModuleDto.py`
2. `entryModuleAdminServiceImpl.py`
3. `homeServiceImpl.py`
4. `entryModuleController.py`
5. 相关 VO 定义
前端重点:
1. `EntryModuleNewClient.tsx`
2. `entry-modules` API 封装
3. 首页入口 hooks
4. `cross-checking-access.ts`
数据重点:
1. `schema_v2_add_evaluation_tables.sql`
2. `seed_home_entry_modules.sql`
3. `seed_govdoc_entry_module.sql`
---
## 10. 风险清单
## 10.1 首页入口可能瞬时丢失
如果首页先切新表,但历史数据尚未迁入新关系表,会导致所有入口不可见。
所以顺序必须是:
1. 先迁数据
2. 再切读逻辑
## 10.2 新旧字段并存期可能双写不一致
必须规定:
1. 标准源为新关系表
2.`areas` 只做兼容输出或快照
## 10.3 交叉评查入口可能与首页结果不一致
因为当前它自己做了一套前端地区推断。
必须同步下线这套本地逻辑。
## 10.4 自定义新租户仍可能被旧 seed 覆盖
如果后续还执行写死地区的 seed,会把新租户体系重新拉回旧模型。
---
## 11. 验收标准
入口模块迁移完成后,至少应满足:
1. 新增租户后,无需改前端常量即可在管理页被选中
2. 管理端不能给不存在的租户分配入口
3. 首页入口仅按 `tenant_code` 判断,不再比对中文地区字符串
4. 交叉评查入口不再依赖固定地区别名数组
5. 历史入口模块在旧租户下可见性与迁移前一致
6. 新租户创建后,可以直接分配现有入口模块
---
## 12. 本文档解决什么问题
本文档主要解决:
1. 入口模块为什么是租户化第一改造点
2. 当前 `areas JSONB` 设计具体哪里不够
3. 关系表应该怎么建
4. 首页和交叉评查为什么会被连带影响
5. 迁移顺序应该怎么安排
建议联动阅读:
1. [地区租户化与自定义租户扩展改造方案.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/地区租户化与自定义租户扩展改造方案.md)
2. [租户主数据模型设计.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/租户主数据模型设计.md)
3. [自定义租户功能连带影响深度补充.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/自定义租户功能连带影响深度补充.md)