feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
@@ -0,0 +1,501 @@
# 租户主数据模型设计
> 适用范围:当前系统把 `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)