# 租户主数据模型设计

> 适用范围：当前系统把 `area / region / 入口地区 / 省级公共范围` 混合当成业务隔离边界使用的场景  
> 文档定位：定义“地区正式升级为租户主数据”后的核心表结构、字段规则、接口边界和兼容策略，作为后续入口模块、RAG、合同模板、文档、公文、统计、RBAC 改造的底层依据。

---

## 1. 结论先行

当前系统真正缺的不是“再补几个地区下拉”，而是：

1. 缺少租户主数据表
2. 缺少统一租户编码
3. 缺少“公共租户 / 总部租户 / 普通租户”的类型语义
4. 缺少租户启停、排序、展示名、别名、扩展字段
5. 缺少业务表和租户主数据之间的稳定关联

因此建议正式引入：

1. `sys_tenants`
2. `sys_tenant_aliases`
3. `sys_tenant_feature_flags`
4. 业务表中的 `tenant_code`

同时保留现有 `area / region / tenant_name` 一段时间作为兼容字段，但不再把它们当主键语义使用。

---

## 2. 当前模型为什么不够

当前系统涉及租户边界的核心字段分散如下：

1. `sso_users.area`
2. `sso_users.tenant_name`
3. `leaudit_documents.region`
4. `contract_templates.region`
5. `rag_dataset.area`
6. `rag_chat_app.area`
7. `leaudit_entry_modules.areas[].area`
8. 前端若干页面里的固定地区常量

这些字段的问题不是“名字不同”这么简单，而是：

1. 没有统一编码
2. 没有主数据约束
3. 没有类型定义
4. 没有别名映射
5. 没有生命周期管理
6. 没有租户能力开关

只要继续沿用字符串直写模式，新增一个租户就必须人工排查：

1. 登录态信息
2. 首页入口
3. RAG 数据集
4. 合同模板
5. 文档上传
6. 交叉评查入口
7. 统计维度
8. RBAC 管理树

这已经不是可维护架构。

---

## 3. 目标模型

## 3.1 核心原则

后续统一按下面规则建模：

1. `tenant_code` 是唯一稳定编码，只用于程序匹配和外键引用
2. `tenant_name` 是展示名称，可修改
3. `area / region` 退化为兼容字段，不再承担主键语义
4. “公共资源”不再用 `省级 / 省局 / default / 空串` 表示，而是用明确租户类型或独立作用域字段表达
5. 所有租户候选值都必须来自主数据，不允许前后端各自发明字符串

## 3.2 推荐主表 `sys_tenants`

建议表结构：

```sql
CREATE TABLE IF NOT EXISTS sys_tenants (
    id              BIGSERIAL PRIMARY KEY,
    tenant_code     VARCHAR(64) NOT NULL UNIQUE,
    tenant_name     VARCHAR(128) NOT NULL,
    tenant_short_name VARCHAR(64) NULL,
    tenant_type     VARCHAR(32) NOT NULL,
    parent_tenant_code VARCHAR(64) NULL,
    display_order   INT NOT NULL DEFAULT 0,
    is_enabled      BOOLEAN NOT NULL DEFAULT TRUE,
    is_builtin      BOOLEAN NOT NULL DEFAULT FALSE,
    is_public       BOOLEAN NOT NULL DEFAULT FALSE,
    can_host_entry_module BOOLEAN NOT NULL DEFAULT TRUE,
    can_host_documents BOOLEAN NOT NULL DEFAULT TRUE,
    can_host_rag    BOOLEAN NOT NULL DEFAULT TRUE,
    can_host_templates BOOLEAN NOT NULL DEFAULT TRUE,
    ext             JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    deleted_at      TIMESTAMP NULL
);
```

## 3.3 字段语义

重点字段建议如下：

1. `tenant_code`
   用于所有内部匹配，例如 `MZ`、`YF`、`JY`、`CZ`、`PROVINCIAL`、`PUBLIC`

2. `tenant_name`
   展示名，例如 `梅州`、`云浮`、`揭阳`、`潮州`、`省局公共`

3. `tenant_type`
   枚举建议：
   - `LOCAL`
   - `HEADQUARTER`
   - `PUBLIC`
   - `PILOT`
   - `INTERNAL`

4. `parent_tenant_code`
   用于表达层级，例如某试点租户挂在省级租户下

5. `is_public`
   表示该租户是否代表跨租户公共资源域，不再让业务侧自行解释 `省级 / default / 空串`

6. `is_builtin`
   标识是否系统内置。当前固定地区和公共租户可以先作为内置租户落地

7. `can_host_entry_module / can_host_documents / can_host_rag / can_host_templates`
   表示这个租户是否允许承载某类业务配置，避免把“公共租户”误用于不该投放的模块

8. `ext`
   用于保留显示标签、颜色、旧编码映射、第三方系统标识等扩展信息

---

## 4. 别名表设计

## 4.1 为什么必须有别名表

当前系统已存在以下同义值混用：

1. `省局`
2. `省级`
3. `default`
4. `''`
5. 未来可能出现的 `广东省局`

如果不显式建别名表，只靠 if/else 兼容，后续每个模块都会各写一遍归一逻辑。

## 4.2 推荐表 `sys_tenant_aliases`

```sql
CREATE TABLE IF NOT EXISTS sys_tenant_aliases (
    id              BIGSERIAL PRIMARY KEY,
    tenant_code     VARCHAR(64) NOT NULL,
    alias_value     VARCHAR(128) NOT NULL,
    alias_type      VARCHAR(32) NOT NULL,
    is_enabled      BOOLEAN NOT NULL DEFAULT TRUE,
    created_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    deleted_at      TIMESTAMP NULL,
    UNIQUE (tenant_code, alias_value)
);
```

## 4.3 `alias_type` 建议

建议最少支持：

1. `LEGACY_AREA`
2. `LEGACY_REGION`
3. `DISPLAY`
4. `IMPORT`
5. `EXTERNAL_SYSTEM`

这样可以区分：

1. 历史库里的旧值
2. 前端展示名
3. 导入脚本映射值
4. 第三方接口传入值

---

## 5. 租户能力开关表设计

## 5.1 为什么需要功能级能力开关

用户这次点出来的“入口模块无法给新租户分配”只是第一层问题。更深一层是：

新增租户后，不一定所有模块都同时开放。

例如某新租户可能只开：

1. 首页入口
2. 文档上传

但暂时不开：

1. 合同模板
2. RAG 知识库管理
3. 交叉评查

所以需要功能级能力模型，而不是只维护一个名字列表。

## 5.2 推荐表 `sys_tenant_feature_flags`

```sql
CREATE TABLE IF NOT EXISTS sys_tenant_feature_flags (
    id              BIGSERIAL PRIMARY KEY,
    tenant_code     VARCHAR(64) NOT NULL,
    feature_key     VARCHAR(64) NOT NULL,
    is_enabled      BOOLEAN NOT NULL DEFAULT TRUE,
    ext             JSONB NOT NULL DEFAULT '{}'::jsonb,
    created_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMP NOT NULL DEFAULT NOW(),
    deleted_at      TIMESTAMP NULL,
    UNIQUE (tenant_code, feature_key)
);
```

## 5.3 `feature_key` 建议

建议按当前系统模块先固化：

1. `home.entry_module`
2. `documents.upload`
3. `documents.list`
4. `govdoc.audit`
5. `rag.dataset`
6. `rag.chat`
7. `contract.template`
8. `cross.review`
9. `usage.stats`

---

## 6. 业务表接入规范

## 6.1 用户表

当前：

1. `sso_users.area`
2. `sso_users.tenant_name`

建议新增：

1. `sso_users.tenant_code`

规则：

1. `tenant_code` 为主归属租户
2. `area` 先保留，作为历史兼容展示字段
3. `tenant_name` 保留，但展示应优先来自 `sys_tenants.tenant_name`

## 6.2 文档表

当前：

1. `leaudit_documents.region`

建议新增：

1. `leaudit_documents.tenant_code`

兼容策略：

1. 老代码读取 `region` 时，通过别名表映射为 `tenant_code`
2. 新代码写入时同时写 `tenant_code` 和兼容 `region`

## 6.3 合同模板

当前：

1. `contract_templates.region`

建议新增：

1. `contract_templates.tenant_code`
2. 可选新增 `scope_type`

原因：

当前 `省级` 既像租户，又像公共范围，语义混乱。建议拆成：

1. `tenant_code`
2. `scope_type = TENANT / PUBLIC`

## 6.4 RAG

当前：

1. `rag_dataset.area`
2. `rag_chat_app.area`
3. `is_public`
4. `is_default`

建议新增：

1. `tenant_code`
2. `scope_type`

说明：

`is_public` 可以保留，但应由 `scope_type` 和租户类型统一解释，避免 `省级 + is_public` 双重表达。

## 6.5 入口模块

当前：

1. `leaudit_entry_modules.areas JSONB`

建议：

1. 保留 `leaudit_entry_modules`
2. 新增 `leaudit_entry_module_tenants`
3. 由关系表表达模块与租户的分配关系

详见文档：

- [入口模块租户配置表迁移方案.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/入口模块租户配置表迁移方案.md)

---

## 7. 租户类型与公共范围语义

## 7.1 当前问题

现在公共范围至少存在 4 种写法：

1. `省局`
2. `省级`
3. `default`
4. `''`

这 4 种值不能继续并存。

## 7.2 推荐统一语义

建议统一为两层：

1. 租户归属层
2. 数据作用域层

推荐枚举：

1. `scope_type = TENANT`
2. `scope_type = PUBLIC`
3. `scope_type = CROSS_TENANT`

说明：

1. `tenant_code` 决定“归属”
2. `scope_type` 决定“谁可以看”

例如：

1. 某模板属于 `PROVINCIAL_PUBLIC`，`scope_type=PUBLIC`
2. 某知识库属于 `MZ`，`scope_type=TENANT`

这样比单纯写 `region='省级'` 清晰得多。

---

## 8. 后端接口建议

## 8.1 新增租户主数据接口

建议最少提供：

1. `GET /api/v3/tenants`
   返回启用中的租户列表

2. `GET /api/v3/tenants/{tenantCode}`
   返回租户详情

3. `POST /api/v3/tenants`
   创建租户

4. `PUT /api/v3/tenants/{tenantCode}`
   更新租户

5. `GET /api/v3/tenants/options`
   返回前端下拉所需精简结构

6. `GET /api/v3/tenants/features`
   查询租户能力矩阵

## 8.2 返回结构建议

建议前端统一使用：

```json
{
  "tenant_code": "MZ",
  "tenant_name": "梅州",
  "tenant_type": "LOCAL",
  "is_public": false,
  "display_order": 10,
  "is_enabled": true,
  "features": [
    "home.entry_module",
    "documents.upload",
    "rag.chat"
  ]
}
```

---

## 9. 前端接入规范

所有原来写死地区列表的页面，后续都不应该再维护常量数组，而应读取租户主数据接口。

重点包括：

1. 入口模块新建页
2. 入口模块列表页筛选
3. 交叉评查入口可见性计算
4. RAG 地区配置页
5. 合同模板地区筛选
6. 使用统计地区维度筛选

前端要遵守两条规则：

1. 存储和提交用 `tenant_code`
2. 展示才用 `tenant_name`

---

## 10. 不建议复用 `tenant_name` 当主键

虽然系统里已经有 `sso_users.tenant_name`，但不建议直接把它升级为租户主键，原因有 5 个：

1. 它本质更像组织展示字段
2. 历史数据可能存在空值和重复值
3. 它不一定稳定
4. 它可能包含业务展示修饰词
5. 它不能稳定承载外键关系

因此：

1. `tenant_name` 可保留
2. 但必须引入新的 `tenant_code`

---

## 11. 与权限系统的关系

租户主数据模型不是替代 RBAC，而是给 RBAC 提供更稳定的数据边界基座。

关系应为：

1. RBAC 决定“你能做什么”
2. 租户主数据决定“你的租户边界是什么”
3. 数据范围执行器把两者合并成最终访问决策

也就是：

1. `permission_key`
2. `data_scope`
3. `tenant_code`

这三者必须同时成立。

---

## 12. 推荐实施顺序

建议顺序如下：

1. 建 `sys_tenants`、`sys_tenant_aliases`、`sys_tenant_feature_flags`
2. 先把当前固定地区和公共范围落成主数据
3. 给 `sso_users` 增加 `tenant_code`
4. 先改入口模块为租户接口驱动
5. 再改 RAG、合同模板、文档上传
6. 最后将统一数据范围执行器接到 `tenant_code`

---

## 13. 本文档解决什么问题

本文档主要解决：

1. “地区”到底该怎么升级成“租户”
2. 新租户为什么不能只改前端下拉
3. 为什么必须引入 `tenant_code`
4. 为什么必须有别名表和能力开关
5. 业务表后续应该如何统一接租户主数据

下一步建议继续阅读：

1. [地区租户化与自定义租户扩展改造方案.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/地区租户化与自定义租户扩展改造方案.md)
2. [地区到租户编码映射清洗清单.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/地区到租户编码映射清洗清单.md)
3. [入口模块租户配置表迁移方案.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/入口模块租户配置表迁移方案.md)
4. [自定义租户功能连带影响深度补充.md](/home/wren-dev/Porject/leaudit-platform/docs/权限与地区隔离/自定义租户功能连带影响深度补充.md)
