502 lines
12 KiB
Markdown
502 lines
12 KiB
Markdown
# 租户主数据模型设计
|
||
|
||
> 适用范围:当前系统把 `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)
|