12 KiB
租户主数据模型设计
适用范围:当前系统把
area / region / 入口地区 / 省级公共范围混合当成业务隔离边界使用的场景
文档定位:定义“地区正式升级为租户主数据”后的核心表结构、字段规则、接口边界和兼容策略,作为后续入口模块、RAG、合同模板、文档、公文、统计、RBAC 改造的底层依据。
1. 结论先行
当前系统真正缺的不是“再补几个地区下拉”,而是:
- 缺少租户主数据表
- 缺少统一租户编码
- 缺少“公共租户 / 总部租户 / 普通租户”的类型语义
- 缺少租户启停、排序、展示名、别名、扩展字段
- 缺少业务表和租户主数据之间的稳定关联
因此建议正式引入:
sys_tenantssys_tenant_aliasessys_tenant_feature_flags- 业务表中的
tenant_code
同时保留现有 area / region / tenant_name 一段时间作为兼容字段,但不再把它们当主键语义使用。
2. 当前模型为什么不够
当前系统涉及租户边界的核心字段分散如下:
sso_users.areasso_users.tenant_nameleaudit_documents.regioncontract_templates.regionrag_dataset.arearag_chat_app.arealeaudit_entry_modules.areas[].area- 前端若干页面里的固定地区常量
这些字段的问题不是“名字不同”这么简单,而是:
- 没有统一编码
- 没有主数据约束
- 没有类型定义
- 没有别名映射
- 没有生命周期管理
- 没有租户能力开关
只要继续沿用字符串直写模式,新增一个租户就必须人工排查:
- 登录态信息
- 首页入口
- RAG 数据集
- 合同模板
- 文档上传
- 交叉评查入口
- 统计维度
- RBAC 管理树
这已经不是可维护架构。
3. 目标模型
3.1 核心原则
后续统一按下面规则建模:
tenant_code是唯一稳定编码,只用于程序匹配和外键引用tenant_name是展示名称,可修改area / region退化为兼容字段,不再承担主键语义- “公共资源”不再用
省级 / 省局 / default / 空串表示,而是用明确租户类型或独立作用域字段表达 - 所有租户候选值都必须来自主数据,不允许前后端各自发明字符串
3.2 推荐主表 sys_tenants
建议表结构:
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 字段语义
重点字段建议如下:
-
tenant_code用于所有内部匹配,例如MZ、YF、JY、CZ、PROVINCIAL、PUBLIC -
tenant_name展示名,例如梅州、云浮、揭阳、潮州、省局公共 -
tenant_type枚举建议:LOCALHEADQUARTERPUBLICPILOTINTERNAL
-
parent_tenant_code用于表达层级,例如某试点租户挂在省级租户下 -
is_public表示该租户是否代表跨租户公共资源域,不再让业务侧自行解释省级 / default / 空串 -
is_builtin标识是否系统内置。当前固定地区和公共租户可以先作为内置租户落地 -
can_host_entry_module / can_host_documents / can_host_rag / can_host_templates表示这个租户是否允许承载某类业务配置,避免把“公共租户”误用于不该投放的模块 -
ext用于保留显示标签、颜色、旧编码映射、第三方系统标识等扩展信息
4. 别名表设计
4.1 为什么必须有别名表
当前系统已存在以下同义值混用:
省局省级default''- 未来可能出现的
广东省局
如果不显式建别名表,只靠 if/else 兼容,后续每个模块都会各写一遍归一逻辑。
4.2 推荐表 sys_tenant_aliases
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 建议
建议最少支持:
LEGACY_AREALEGACY_REGIONDISPLAYIMPORTEXTERNAL_SYSTEM
这样可以区分:
- 历史库里的旧值
- 前端展示名
- 导入脚本映射值
- 第三方接口传入值
5. 租户能力开关表设计
5.1 为什么需要功能级能力开关
用户这次点出来的“入口模块无法给新租户分配”只是第一层问题。更深一层是:
新增租户后,不一定所有模块都同时开放。
例如某新租户可能只开:
- 首页入口
- 文档上传
但暂时不开:
- 合同模板
- RAG 知识库管理
- 交叉评查
所以需要功能级能力模型,而不是只维护一个名字列表。
5.2 推荐表 sys_tenant_feature_flags
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 建议
建议按当前系统模块先固化:
home.entry_moduledocuments.uploaddocuments.listgovdoc.auditrag.datasetrag.chatcontract.templatecross.reviewusage.stats
6. 业务表接入规范
6.1 用户表
当前:
sso_users.areasso_users.tenant_name
建议新增:
sso_users.tenant_code
规则:
tenant_code为主归属租户area先保留,作为历史兼容展示字段tenant_name保留,但展示应优先来自sys_tenants.tenant_name
6.2 文档表
当前:
leaudit_documents.region
建议新增:
leaudit_documents.tenant_code
兼容策略:
- 老代码读取
region时,通过别名表映射为tenant_code - 新代码写入时同时写
tenant_code和兼容region
6.3 合同模板
当前:
contract_templates.region
建议新增:
contract_templates.tenant_code- 可选新增
scope_type
原因:
当前 省级 既像租户,又像公共范围,语义混乱。建议拆成:
tenant_codescope_type = TENANT / PUBLIC
6.4 RAG
当前:
rag_dataset.arearag_chat_app.areais_publicis_default
建议新增:
tenant_codescope_type
说明:
is_public 可以保留,但应由 scope_type 和租户类型统一解释,避免 省级 + is_public 双重表达。
6.5 入口模块
当前:
leaudit_entry_modules.areas JSONB
建议:
- 保留
leaudit_entry_modules - 新增
leaudit_entry_module_tenants - 由关系表表达模块与租户的分配关系
详见文档:
7. 租户类型与公共范围语义
7.1 当前问题
现在公共范围至少存在 4 种写法:
省局省级default''
这 4 种值不能继续并存。
7.2 推荐统一语义
建议统一为两层:
- 租户归属层
- 数据作用域层
推荐枚举:
scope_type = TENANTscope_type = PUBLICscope_type = CROSS_TENANT
说明:
tenant_code决定“归属”scope_type决定“谁可以看”
例如:
- 某模板属于
PROVINCIAL_PUBLIC,scope_type=PUBLIC - 某知识库属于
MZ,scope_type=TENANT
这样比单纯写 region='省级' 清晰得多。
8. 后端接口建议
8.1 新增租户主数据接口
建议最少提供:
-
GET /api/v3/tenants返回启用中的租户列表 -
GET /api/v3/tenants/{tenantCode}返回租户详情 -
POST /api/v3/tenants创建租户 -
PUT /api/v3/tenants/{tenantCode}更新租户 -
GET /api/v3/tenants/options返回前端下拉所需精简结构 -
GET /api/v3/tenants/features查询租户能力矩阵
8.2 返回结构建议
建议前端统一使用:
{
"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. 前端接入规范
所有原来写死地区列表的页面,后续都不应该再维护常量数组,而应读取租户主数据接口。
重点包括:
- 入口模块新建页
- 入口模块列表页筛选
- 交叉评查入口可见性计算
- RAG 地区配置页
- 合同模板地区筛选
- 使用统计地区维度筛选
前端要遵守两条规则:
- 存储和提交用
tenant_code - 展示才用
tenant_name
10. 不建议复用 tenant_name 当主键
虽然系统里已经有 sso_users.tenant_name,但不建议直接把它升级为租户主键,原因有 5 个:
- 它本质更像组织展示字段
- 历史数据可能存在空值和重复值
- 它不一定稳定
- 它可能包含业务展示修饰词
- 它不能稳定承载外键关系
因此:
tenant_name可保留- 但必须引入新的
tenant_code
11. 与权限系统的关系
租户主数据模型不是替代 RBAC,而是给 RBAC 提供更稳定的数据边界基座。
关系应为:
- RBAC 决定“你能做什么”
- 租户主数据决定“你的租户边界是什么”
- 数据范围执行器把两者合并成最终访问决策
也就是:
permission_keydata_scopetenant_code
这三者必须同时成立。
12. 推荐实施顺序
建议顺序如下:
- 建
sys_tenants、sys_tenant_aliases、sys_tenant_feature_flags - 先把当前固定地区和公共范围落成主数据
- 给
sso_users增加tenant_code - 先改入口模块为租户接口驱动
- 再改 RAG、合同模板、文档上传
- 最后将统一数据范围执行器接到
tenant_code
13. 本文档解决什么问题
本文档主要解决:
- “地区”到底该怎么升级成“租户”
- 新租户为什么不能只改前端下拉
- 为什么必须引入
tenant_code - 为什么必须有别名表和能力开关
- 业务表后续应该如何统一接租户主数据
下一步建议继续阅读: