14 KiB
地区租户化与自定义租户扩展改造方案
适用范围:当前系统把“地区”同时当作数据隔离键、入口模块分配维度、部分公共资源维度使用的场景
文档定位:针对“地区其实已经在承担租户语义,但现在是固定死的、没法新增”的问题,给出完整扩展改造方案。
1. 结论先行
你指出的问题是准确的,而且不是局部问题。
当前系统表面上叫“地区隔离”,但从实际代码和数据模型看,area 已经在多个地方承担了“租户”语义:
- 用户归属
- 文档归属
- 合同模板归属
- RAG 数据集和应用归属
- 首页入口模块可见范围
- 交叉评查参与地区展示
- RBAC 用户管理范围
- 部分统计口径和筛选口径
但是当前系统没有真正的“租户主数据模型”,导致出现 4 个结构性问题:
- 允许在数据表里写任意
area字符串,但没有统一租户字典 - 前端多处把可选地区硬编码成
梅州 / 云浮 / 揭阳 / 潮州 / 省局 - 入口模块
areas只是自由 JSON,缺乏租户主数据约束,新增租户后无法统一分配 - “省局 / 省级 / 空字符串 / default” 混杂使用,公共范围语义不统一
所以这次问题不应该只修“入口模块新增租户没地方选”,而是应该把“地区”正式收敛为“租户维度主数据 + 兼容地区展示名”的模型。
2. 当前现状深度分析
2.1 数据层已经把 area 当租户键用了
已确认:
sso_users.arealeaudit_documents.regioncontract_templates.regionrag datasets/apps areaentry_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 种写法:
省局省级''default
这是后续扩展新租户时最大的数据一致性风险。
3. 真实问题不止入口模块
你已经发现入口模块新增租户无法分配,但如果继续深挖,至少还有下面这些隐含问题。
3.1 新租户无法稳定出现在前端筛选中
已确认存在固定地区名单或地区别名逻辑的地方:
- 入口模块新建页
- 入口模块列表页
- 交叉评查入口访问逻辑
- RAG 地区配置页
- 合同模板地区展示
- 使用统计地区筛选
结果是:
- 后端有新租户数据
- 前端很多页面也未必看得见或选得到
3.2 新租户无法作为“入口可见范围主数据”统一管理
目前入口模块的地区可见性完全靠:
- 每个模块自己存一份
areas JSON
但没有:
- 可复用的租户字典
- 可禁用租户
- 可排序租户
- 可标识是否公共租户/总部租户
后果:
- 每个入口模块都可能写出不同拼法
- 例如:
省局/省级/广东省局
3.3 用户归属和入口可见性没有共享主数据
当前用户主归属在:
sso_users.area
入口模块配置在:
leaudit_entry_modules.areas[].area
二者之间没有外键,没有主数据表,没有统一编码。
所以只要字符串不完全相同,用户就可能:
- 有租户
- 但首页看不到对应入口
3.4 统计、RAG、模板的“公共租户”语义无法扩展
如果以后新增:
- 总部租户
- 试点租户
- 集团公共租户
当前模型无法优雅表达。
因为现在公共语义散在:
省局省级- 空串
is_publicdefault
没有统一的“租户类型”字段。
3.5 文档和公文本身的 region 注释仍带旧代码语义
leauditDocument.py 里还写着:
所属地区: mz/yf/jy/cz/default
这已经明显说明模型和代码注释都还把可选租户想成固定集合。
这类地方如果不统一改,后面团队会默认:
- 新租户不在支持范围内
4. 为什么不能只在前端把地区下拉改成可配置
因为问题不是 UI,而是模型。
只改前端下拉,只能解决:
- 用户能选一个新租户字符串
但解决不了:
- 这个租户是否合法
- 这个租户的展示名、编码、排序、启用状态是什么
- 这个租户是不是公共租户
- 这个租户能不能被入口模块使用
- 这个租户和
sso_users.area是否一致 - 旧数据里的
省局/省级/default怎么兼容
所以必须建租户主数据。
5. 推荐目标模型
5.1 核心结论
建议把当前“地区字段”升级为:
tenant_codetenant_name
其中:
tenant_code用于内部引用和匹配tenant_name用于展示
当前 area/region 先作为兼容展示字段保留,不立即强拆。
5.2 推荐新增主数据表
建议新增:
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:
HEADQUARTERSREGIONALPUBLICTEST
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 直接用字符串比较
- 一次性切换成本过高
第一阶段建议:
- 新增
sys_tenants - 在所有“配置与入口层”先改成认租户主数据
- 业务数据层先继续使用原
area/region - 通过
tenant_code/tenant_name映射兼容旧值
6.2 第二阶段:入口和配置域先租户化
优先改:
- 入口模块
- 首页入口可见性
- 前端地区下拉
- 交叉评查入口地区判断
- RAG 地区配置页
6.3 第三阶段:业务资源逐步 tenant_code 化
后续再逐步考虑:
sso_users.area -> tenant_codeleaudit_documents.region -> tenant_codecontract_templates.region -> tenant_code
但这一步可以晚于权限执行器改造。
7. 入口模块改造方案
入口模块是本次租户化最优先的模块。
7.1 当前问题
当前:
- 表里
areas是自由 JSON - 前端是固定地区多选框
- 首页用
user_area直接匹配 JSON 里的area
7.2 推荐改法
新增入口模块租户配置表:
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 兼容策略
短期可以:
- 保留
leaudit_entry_modules.areas - 新增
leaudit_entry_module_tenants - 后端优先读新表
- 若新表为空,再回退读旧 JSON
7.4 前端改法
入口模块新建/编辑页不再使用固定 AREA_OPTIONS,改为:
- 先调用租户列表接口
- 渲染启用租户多选
- 允许选择新增租户
8. 首页入口改造方案
8.1 当前问题
homeServiceImpl.py 当前按:
user_areaem.areas[].areadefaultsuper_admin bypass
混合判定。
8.2 推荐改法
引入:
current_user.tenant_codeentry_module_tenants.tenant_code
首页判断改为:
- 用户可见租户集合
- 模块绑定租户集合
- 交集判断
公共模块单独处理:
- 通过
sys_tenants.is_public - 或入口模块配置的公共租户绑定
8.3 不要再用 default
建议:
default语义废弃- 若是“公共可见”,显式建公共租户
9. 前端固定地区名单改造方案
9.1 当前固定名单热点
已确认典型位置:
EntryModuleNewClient.tsxEntryModulesClient.tsxcross-checking-access.tsRAG地区配置相关页面- 其他地区筛选组件
9.2 推荐统一方案
新增前端租户 API:
GET /api/v3/tenants
返回:
[
{
"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 当前情况
用户当前有:
areatenant_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
混合表达公共范围。
建议改造:
- 引入公共租户
- 统一
PUBLIC_MIXED的租户集合来源 - 不再靠字符串字面值
省级
11.2 合同模板
当前:
省级被当作公共模板归属
建议:
- 用
public tenant或headquarters tenant - 不再把展示名硬编码进判断逻辑
11.3 使用统计
统计筛选当前虽然不是入口配置,但未来如果租户扩展:
- 地区筛选必须改成租户筛选
- area 文本不能再假定固定集合
12. 交叉评查的潜在影响
交叉评查主权限模型是成员关系,不是地区模型。
但它仍会受租户化影响:
- 入口模块可见性
- 任务参与地区展示
- 成员树的租户展示
所以它不是第一优先改造对象,但必须纳入兼容分析。
13. 数据治理问题
在进入租户化之前,必须先做一次脏数据巡检。
建议巡检:
sso_users.area去重值leaudit_documents.region去重值contract_templates.region去重值rag datasets/apps area去重值entry_modules.areas[].area去重值
目的:
- 找出
省局 / 省级 / default / 空串 / 其他历史拼法
只有先做这一步,后续租户映射表才能准确建立。
14. 推荐新增文档与接口
建议后续再补:
租户主数据模型设计.md地区到租户编码映射清洗清单.md入口模块租户配置表迁移方案.md
建议新增接口:
GET /v3/tenantsPOST /v3/tenantsPUT /v3/tenants/{TenantCode}GET /v3/tenants/options
其中 options 可专门给前端下拉用。
15. 推荐实施顺序
建议按下面顺序做,而不是一次性全改:
- 建
sys_tenants - 做租户脏数据巡检
- 入口模块改成租户主数据驱动
- 首页入口可见性改成租户集合判断
- 前端所有固定地区下拉改成租户接口
- 交叉评查/RAG/合同模板里的字符串公共语义收口
- 再评估业务主表是否逐步从
area/region迁到tenant_code
16. 最终建议
当前系统不应再把“地区”视为一个永远固定的枚举集合。
从代码和业务实际上看,它已经是“租户边界”了,只是还没有主数据模型支撑。
如果现在不做这层抽象,后面每新增一个租户,都会反复踩同一类问题:
- 前端没地方选
- 入口模块分配不了
- 首页看不到
- 公共范围语义冲突
- 老字符串拼法继续扩散
因此,建议把“地区租户化”作为权限架构之后的下一优先级平台改造项,且入口模块应作为第一落点。