Files
leaudit-platform-backend/docs/权限与地区隔离/租户主数据模型设计.md
T

12 KiB
Raw Blame History

租户主数据模型设计

适用范围:当前系统把 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

建议表结构:

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 用于所有内部匹配,例如 MZYFJYCZPROVINCIALPUBLIC

  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

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

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. 由关系表表达模块与租户的分配关系

详见文档:


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_PUBLICscope_type=PUBLIC
  2. 某知识库属于 MZscope_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 返回结构建议

建议前端统一使用:

{
  "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_tenantssys_tenant_aliasessys_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
  2. 地区到租户编码映射清洗清单.md
  3. 入口模块租户配置表迁移方案.md
  4. 自定义租户功能连带影响深度补充.md