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

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

---

## 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. 老字符串拼法继续扩散

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