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,635 @@
# 地区租户化与自定义租户扩展改造方案
> 适用范围:当前系统把“地区”同时当作数据隔离键、入口模块分配维度、部分公共资源维度使用的场景
> 文档定位:针对“地区其实已经在承担租户语义,但现在是固定死的、没法新增”的问题,给出完整扩展改造方案。
---
## 1. 结论先行
你指出的问题是准确的,而且不是局部问题。
当前系统表面上叫“地区隔离”,但从实际代码和数据模型看,`area` 已经在多个地方承担了“租户”语义:
1. 用户归属
2. 文档归属
3. 合同模板归属
4. RAG 数据集和应用归属
5. 首页入口模块可见范围
6. 交叉评查参与地区展示
7. RBAC 用户管理范围
8. 部分统计口径和筛选口径
但是当前系统没有真正的“租户主数据模型”,导致出现 4 个结构性问题:
1. 允许在数据表里写任意 `area` 字符串,但没有统一租户字典
2. 前端多处把可选地区硬编码成 `梅州 / 云浮 / 揭阳 / 潮州 / 省局`
3. 入口模块 `areas` 只是自由 JSON,缺乏租户主数据约束,新增租户后无法统一分配
4. “省局 / 省级 / 空字符串 / default” 混杂使用,公共范围语义不统一
所以这次问题不应该只修“入口模块新增租户没地方选”,而是应该把“地区”正式收敛为“租户维度主数据 + 兼容地区展示名”的模型。
---
## 2. 当前现状深度分析
## 2.1 数据层已经把 area 当租户键用了
已确认:
- `sso_users.area`
- `leaudit_documents.region`
- `contract_templates.region`
- `rag datasets/apps area`
- `entry_modules.areas`
这些字段虽然命名不同,但本质都在表达:
- 这条数据属于哪个业务边界域
这个“业务边界域”就是租户。
## 2.2 入口模块是当前最明显的断点
入口模块表:
- `leaudit_entry_modules.areas JSONB`
当前后端:
- 允许写任意 JSON `[{ area, enabled, sort_order }]`
- 不校验这些 `area` 是否属于合法主数据
当前前端:
- 新建页把地区选项硬编码成:
- `梅州`
- `云浮`
- `揭阳`
- `潮州`
- `省局`
这意味着:
- 后端理论上能存新租户字符串
- 但前端没有入口选择它
- 首页也没有统一租户主数据做可见性计算
## 2.3 首页入口可见性已经严重依赖 area
`homeServiceImpl.py` 当前做法:
- 直接按 `user_area` 匹配 `em.areas[].area`
- `super_admin` 特判 `bypass_area`
- 还把 `default` 当成兜底 area
这说明首页可见性已经是租户配置功能,而不是简单地区展示。
## 2.4 交叉评查入口又叠了一层前端租户别名
`cross-checking-access.ts` 当前做法:
- `AREA_ALIASES = ["梅州", "云浮", "揭阳", "潮州", "省局"]`
- 角色是 `provincial_admin` 就自动补 `省局`
这说明前端为了适配没有租户主数据,只能自己发明一套:
- 别名识别
- 角色补值
- 入口匹配
这类逻辑会随着新租户持续失控。
## 2.5 RAG 和合同模板已经有“公共租户”语义,但不统一
RAG 当前:
- `area in (user_area, '省级', '') or is_public`
合同模板当前:
- 多处用 `省级` 作为公共范围
入口模块当前:
-`省局`
首页当前:
- 还支持 `default`
这说明“公共租户”语义现在至少有 4 种写法:
1. `省局`
2. `省级`
3. `''`
4. `default`
这是后续扩展新租户时最大的数据一致性风险。
---
## 3. 真实问题不止入口模块
你已经发现入口模块新增租户无法分配,但如果继续深挖,至少还有下面这些隐含问题。
## 3.1 新租户无法稳定出现在前端筛选中
已确认存在固定地区名单或地区别名逻辑的地方:
- 入口模块新建页
- 入口模块列表页
- 交叉评查入口访问逻辑
- RAG 地区配置页
- 合同模板地区展示
- 使用统计地区筛选
结果是:
- 后端有新租户数据
- 前端很多页面也未必看得见或选得到
## 3.2 新租户无法作为“入口可见范围主数据”统一管理
目前入口模块的地区可见性完全靠:
- 每个模块自己存一份 `areas JSON`
但没有:
- 可复用的租户字典
- 可禁用租户
- 可排序租户
- 可标识是否公共租户/总部租户
后果:
- 每个入口模块都可能写出不同拼法
- 例如:`省局` / `省级` / `广东省局`
## 3.3 用户归属和入口可见性没有共享主数据
当前用户主归属在:
- `sso_users.area`
入口模块配置在:
- `leaudit_entry_modules.areas[].area`
二者之间没有外键,没有主数据表,没有统一编码。
所以只要字符串不完全相同,用户就可能:
- 有租户
- 但首页看不到对应入口
## 3.4 统计、RAG、模板的“公共租户”语义无法扩展
如果以后新增:
- 总部租户
- 试点租户
- 集团公共租户
当前模型无法优雅表达。
因为现在公共语义散在:
- `省局`
- `省级`
- 空串
- `is_public`
- `default`
没有统一的“租户类型”字段。
## 3.5 文档和公文本身的 `region` 注释仍带旧代码语义
`leauditDocument.py` 里还写着:
- `所属地区: mz/yf/jy/cz/default`
这已经明显说明模型和代码注释都还把可选租户想成固定集合。
这类地方如果不统一改,后面团队会默认:
- 新租户不在支持范围内
---
## 4. 为什么不能只在前端把地区下拉改成可配置
因为问题不是 UI,而是模型。
只改前端下拉,只能解决:
- 用户能选一个新租户字符串
但解决不了:
1. 这个租户是否合法
2. 这个租户的展示名、编码、排序、启用状态是什么
3. 这个租户是不是公共租户
4. 这个租户能不能被入口模块使用
5. 这个租户和 `sso_users.area` 是否一致
6. 旧数据里的 `省局/省级/default` 怎么兼容
所以必须建租户主数据。
---
## 5. 推荐目标模型
## 5.1 核心结论
建议把当前“地区字段”升级为:
- `tenant_code`
- `tenant_name`
其中:
- `tenant_code` 用于内部引用和匹配
- `tenant_name` 用于展示
当前 `area/region` 先作为兼容展示字段保留,不立即强拆。
## 5.2 推荐新增主数据表
建议新增:
```sql
CREATE TABLE IF NOT EXISTS sys_tenants (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
tenant_code VARCHAR(64) NOT NULL UNIQUE,
tenant_name VARCHAR(128) NOT NULL,
tenant_type VARCHAR(32) NOT NULL DEFAULT 'REGIONAL',
parent_tenant_code VARCHAR(64),
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
```
建议 `tenant_type`
- `HEADQUARTERS`
- `REGIONAL`
- `PUBLIC`
- `TEST`
## 5.3 推荐字段语义
| 字段 | 作用 |
| --- | --- |
| `tenant_code` | 稳定编码,例如 `mz``yf``jy``cz``hq` |
| `tenant_name` | 展示名,例如 `梅州``云浮``揭阳``潮州``省局` |
| `tenant_type` | 租户类别 |
| `is_public` | 是否属于公共资源可见租户 |
| `parent_tenant_code` | 可选,用于总部-地区树 |
---
## 6. 推荐兼容演进路径
## 6.1 第一阶段:不强拆 area/region
第一阶段不建议立刻把所有业务表从 `area/region` 改成 `tenant_id`
原因:
- 当前系统大量 SQL 直接用字符串比较
- 一次性切换成本过高
第一阶段建议:
1. 新增 `sys_tenants`
2. 在所有“配置与入口层”先改成认租户主数据
3. 业务数据层先继续使用原 `area/region`
4. 通过 `tenant_code/tenant_name` 映射兼容旧值
## 6.2 第二阶段:入口和配置域先租户化
优先改:
1. 入口模块
2. 首页入口可见性
3. 前端地区下拉
4. 交叉评查入口地区判断
5. RAG 地区配置页
## 6.3 第三阶段:业务资源逐步 tenant_code 化
后续再逐步考虑:
- `sso_users.area -> tenant_code`
- `leaudit_documents.region -> tenant_code`
- `contract_templates.region -> tenant_code`
但这一步可以晚于权限执行器改造。
---
## 7. 入口模块改造方案
入口模块是本次租户化最优先的模块。
## 7.1 当前问题
当前:
- 表里 `areas` 是自由 JSON
- 前端是固定地区多选框
- 首页用 `user_area` 直接匹配 JSON 里的 `area`
## 7.2 推荐改法
新增入口模块租户配置表:
```sql
CREATE TABLE IF NOT EXISTS leaudit_entry_module_tenants (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
entry_module_id BIGINT NOT NULL REFERENCES leaudit_entry_modules(id) ON DELETE CASCADE,
tenant_code VARCHAR(64) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(entry_module_id, tenant_code)
);
```
说明:
- 不建议继续把入口租户配置长期保留在 JSON
- JSON 适合作为兼容字段,不适合作为主配置结构
## 7.3 兼容策略
短期可以:
1. 保留 `leaudit_entry_modules.areas`
2. 新增 `leaudit_entry_module_tenants`
3. 后端优先读新表
4. 若新表为空,再回退读旧 JSON
## 7.4 前端改法
入口模块新建/编辑页不再使用固定 `AREA_OPTIONS`,改为:
1. 先调用租户列表接口
2. 渲染启用租户多选
3. 允许选择新增租户
---
## 8. 首页入口改造方案
## 8.1 当前问题
`homeServiceImpl.py` 当前按:
- `user_area`
- `em.areas[].area`
- `default`
- `super_admin bypass`
混合判定。
## 8.2 推荐改法
引入:
- `current_user.tenant_code`
- `entry_module_tenants.tenant_code`
首页判断改为:
1. 用户可见租户集合
2. 模块绑定租户集合
3. 交集判断
公共模块单独处理:
- 通过 `sys_tenants.is_public`
- 或入口模块配置的公共租户绑定
## 8.3 不要再用 `default`
建议:
- `default` 语义废弃
- 若是“公共可见”,显式建公共租户
---
## 9. 前端固定地区名单改造方案
## 9.1 当前固定名单热点
已确认典型位置:
- `EntryModuleNewClient.tsx`
- `EntryModulesClient.tsx`
- `cross-checking-access.ts`
- `RAG` 地区配置相关页面
- 其他地区筛选组件
## 9.2 推荐统一方案
新增前端租户 API
- `GET /api/v3/tenants`
返回:
```json
[
{
"tenant_code": "mz",
"tenant_name": "梅州",
"tenant_type": "REGIONAL",
"is_public": false,
"is_enabled": true,
"sort_order": 10
}
]
```
前端所有租户下拉统一改成:
- 用这个接口拉
- 不再本地硬编码
## 9.3 别名逻辑收缩
`cross-checking-access.ts` 这类 `AREA_ALIASES` 逻辑建议删除,改成:
- 使用 `tenant_code`
- `tenant_name` 只展示,不参与判断
---
## 10. 用户模型改造建议
## 10.1 当前情况
用户当前有:
- `area`
- `tenant_name`
但这两个字段并不等价:
- `area` 更像当前业务租户边界
- `tenant_name` 更像组织树分组信息
## 10.2 推荐方向
建议新增:
- `sso_users.tenant_code`
并定义:
- `tenant_code`:业务租户边界
- `tenant_name`:组织展示名称,可保留
- `area`:兼容旧字段,逐步弱化
## 10.3 为什么不能直接复用 tenant_name
因为现有 `tenant_name` 明显被当作组织分组字段在用:
- RBAC 组织树
- 用户管理分组
- 交叉评查成员展示
它不适合作为稳定权限租户编码。
---
## 11. RAG、合同模板、统计的联动影响
## 11.1 RAG
当前:
- `省级`
- `''`
- `is_public`
混合表达公共范围。
建议改造:
1. 引入公共租户
2. 统一 `PUBLIC_MIXED` 的租户集合来源
3. 不再靠字符串字面值 `省级`
## 11.2 合同模板
当前:
- `省级` 被当作公共模板归属
建议:
-`public tenant``headquarters tenant`
- 不再把展示名硬编码进判断逻辑
## 11.3 使用统计
统计筛选当前虽然不是入口配置,但未来如果租户扩展:
- 地区筛选必须改成租户筛选
- area 文本不能再假定固定集合
---
## 12. 交叉评查的潜在影响
交叉评查主权限模型是成员关系,不是地区模型。
但它仍会受租户化影响:
1. 入口模块可见性
2. 任务参与地区展示
3. 成员树的租户展示
所以它不是第一优先改造对象,但必须纳入兼容分析。
---
## 13. 数据治理问题
在进入租户化之前,必须先做一次脏数据巡检。
建议巡检:
1. `sso_users.area` 去重值
2. `leaudit_documents.region` 去重值
3. `contract_templates.region` 去重值
4. `rag datasets/apps area` 去重值
5. `entry_modules.areas[].area` 去重值
目的:
- 找出 `省局 / 省级 / default / 空串 / 其他历史拼法`
只有先做这一步,后续租户映射表才能准确建立。
---
## 14. 推荐新增文档与接口
建议后续再补:
1. `租户主数据模型设计.md`
2. `地区到租户编码映射清洗清单.md`
3. `入口模块租户配置表迁移方案.md`
建议新增接口:
1. `GET /v3/tenants`
2. `POST /v3/tenants`
3. `PUT /v3/tenants/{TenantCode}`
4. `GET /v3/tenants/options`
其中 `options` 可专门给前端下拉用。
---
## 15. 推荐实施顺序
建议按下面顺序做,而不是一次性全改:
1.`sys_tenants`
2. 做租户脏数据巡检
3. 入口模块改成租户主数据驱动
4. 首页入口可见性改成租户集合判断
5. 前端所有固定地区下拉改成租户接口
6. 交叉评查/RAG/合同模板里的字符串公共语义收口
7. 再评估业务主表是否逐步从 `area/region` 迁到 `tenant_code`
---
## 16. 最终建议
当前系统不应再把“地区”视为一个永远固定的枚举集合。
从代码和业务实际上看,它已经是“租户边界”了,只是还没有主数据模型支撑。
如果现在不做这层抽象,后面每新增一个租户,都会反复踩同一类问题:
1. 前端没地方选
2. 入口模块分配不了
3. 首页看不到
4. 公共范围语义冲突
5. 老字符串拼法继续扩散
因此,建议把“地区租户化”作为权限架构之后的下一优先级平台改造项,且入口模块应作为第一落点。