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

14 KiB
Raw Permalink Blame History

地区租户化与自定义租户扩展改造方案

适用范围:当前系统把“地区”同时当作数据隔离键、入口模块分配维度、部分公共资源维度使用的场景
文档定位:针对“地区其实已经在承担租户语义,但现在是固定死的、没法新增”的问题,给出完整扩展改造方案。


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 推荐新增主数据表

建议新增:

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 稳定编码,例如 mzyfjyczhq
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 推荐改法

新增入口模块租户配置表:

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

返回:

[
  {
    "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 tenantheadquarters 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. 老字符串拼法继续扩散

因此,建议把“地区租户化”作为权限架构之后的下一优先级平台改造项,且入口模块应作为第一落点。