Files
leaudit-platform-backend/docs/权限与地区隔离/地区租户化与自定义租户扩展改造方案.md

636 lines
14 KiB
Markdown
Raw Permalink 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.
# 地区租户化与自定义租户扩展改造方案
> 适用范围:当前系统把“地区”同时当作数据隔离键、入口模块分配维度、部分公共资源维度使用的场景
> 文档定位:针对“地区其实已经在承担租户语义,但现在是固定死的、没法新增”的问题,给出完整扩展改造方案。
---
## 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. 老字符串拼法继续扩散
因此,建议把“地区租户化”作为权限架构之后的下一优先级平台改造项,且入口模块应作为第一落点。