# 地区租户化与自定义租户扩展改造方案 > 适用范围:当前系统把“地区”同时当作数据隔离键、入口模块分配维度、部分公共资源维度使用的场景 > 文档定位:针对“地区其实已经在承担租户语义,但现在是固定死的、没法新增”的问题,给出完整扩展改造方案。 --- ## 1. 结论先行 你指出的问题是准确的,而且不是局部问题。 当前系统表面上叫“地区隔离”,但从实际代码和数据模型看,`area` 已经在多个地方承担了“租户”语义: 1. 用户归属 2. 文档归属 3. 合同模板归属 4. RAG 数据集和应用归属 5. 首页入口模块可见范围 6. 交叉评查参与地区展示 7. RBAC 用户管理范围 8. 部分统计口径和筛选口径 但是当前系统没有真正的“租户主数据模型”,导致出现 4 个结构性问题: 1. 允许在数据表里写任意 `area` 字符串,但没有统一租户字典 2. 前端多处把可选地区硬编码成 `梅州 / 云浮 / 揭阳 / 潮州 / 省局` 3. 入口模块 `areas` 只是自由 JSON,缺乏租户主数据约束,新增租户后无法统一分配 4. “省局 / 省级 / 空字符串 / default” 混杂使用,公共范围语义不统一 所以这次问题不应该只修“入口模块新增租户没地方选”,而是应该把“地区”正式收敛为“租户维度主数据 + 兼容地区展示名”的模型。 --- ## 2. 当前现状深度分析 ## 2.1 数据层已经把 area 当租户键用了 已确认: - `sso_users.area` - `leaudit_documents.region` - `contract_templates.region` - `rag datasets/apps area` - `entry_modules.areas` 这些字段虽然命名不同,但本质都在表达: - 这条数据属于哪个业务边界域 这个“业务边界域”就是租户。 ## 2.2 入口模块是当前最明显的断点 入口模块表: - `leaudit_entry_modules.areas JSONB` 当前后端: - 允许写任意 JSON `[{ area, enabled, sort_order }]` - 不校验这些 `area` 是否属于合法主数据 当前前端: - 新建页把地区选项硬编码成: - `梅州` - `云浮` - `揭阳` - `潮州` - `省局` 这意味着: - 后端理论上能存新租户字符串 - 但前端没有入口选择它 - 首页也没有统一租户主数据做可见性计算 ## 2.3 首页入口可见性已经严重依赖 area `homeServiceImpl.py` 当前做法: - 直接按 `user_area` 匹配 `em.areas[].area` - `super_admin` 特判 `bypass_area` - 还把 `default` 当成兜底 area 这说明首页可见性已经是租户配置功能,而不是简单地区展示。 ## 2.4 交叉评查入口又叠了一层前端租户别名 `cross-checking-access.ts` 当前做法: - `AREA_ALIASES = ["梅州", "云浮", "揭阳", "潮州", "省局"]` - 角色是 `provincial_admin` 就自动补 `省局` 这说明前端为了适配没有租户主数据,只能自己发明一套: - 别名识别 - 角色补值 - 入口匹配 这类逻辑会随着新租户持续失控。 ## 2.5 RAG 和合同模板已经有“公共租户”语义,但不统一 RAG 当前: - `area in (user_area, '省级', '') or is_public` 合同模板当前: - 多处用 `省级` 作为公共范围 入口模块当前: - 用 `省局` 首页当前: - 还支持 `default` 这说明“公共租户”语义现在至少有 4 种写法: 1. `省局` 2. `省级` 3. `''` 4. `default` 这是后续扩展新租户时最大的数据一致性风险。 --- ## 3. 真实问题不止入口模块 你已经发现入口模块新增租户无法分配,但如果继续深挖,至少还有下面这些隐含问题。 ## 3.1 新租户无法稳定出现在前端筛选中 已确认存在固定地区名单或地区别名逻辑的地方: - 入口模块新建页 - 入口模块列表页 - 交叉评查入口访问逻辑 - RAG 地区配置页 - 合同模板地区展示 - 使用统计地区筛选 结果是: - 后端有新租户数据 - 前端很多页面也未必看得见或选得到 ## 3.2 新租户无法作为“入口可见范围主数据”统一管理 目前入口模块的地区可见性完全靠: - 每个模块自己存一份 `areas JSON` 但没有: - 可复用的租户字典 - 可禁用租户 - 可排序租户 - 可标识是否公共租户/总部租户 后果: - 每个入口模块都可能写出不同拼法 - 例如:`省局` / `省级` / `广东省局` ## 3.3 用户归属和入口可见性没有共享主数据 当前用户主归属在: - `sso_users.area` 入口模块配置在: - `leaudit_entry_modules.areas[].area` 二者之间没有外键,没有主数据表,没有统一编码。 所以只要字符串不完全相同,用户就可能: - 有租户 - 但首页看不到对应入口 ## 3.4 统计、RAG、模板的“公共租户”语义无法扩展 如果以后新增: - 总部租户 - 试点租户 - 集团公共租户 当前模型无法优雅表达。 因为现在公共语义散在: - `省局` - `省级` - 空串 - `is_public` - `default` 没有统一的“租户类型”字段。 ## 3.5 文档和公文本身的 `region` 注释仍带旧代码语义 `leauditDocument.py` 里还写着: - `所属地区: mz/yf/jy/cz/default` 这已经明显说明模型和代码注释都还把可选租户想成固定集合。 这类地方如果不统一改,后面团队会默认: - 新租户不在支持范围内 --- ## 4. 为什么不能只在前端把地区下拉改成可配置 因为问题不是 UI,而是模型。 只改前端下拉,只能解决: - 用户能选一个新租户字符串 但解决不了: 1. 这个租户是否合法 2. 这个租户的展示名、编码、排序、启用状态是什么 3. 这个租户是不是公共租户 4. 这个租户能不能被入口模块使用 5. 这个租户和 `sso_users.area` 是否一致 6. 旧数据里的 `省局/省级/default` 怎么兼容 所以必须建租户主数据。 --- ## 5. 推荐目标模型 ## 5.1 核心结论 建议把当前“地区字段”升级为: - `tenant_code` - `tenant_name` 其中: - `tenant_code` 用于内部引用和匹配 - `tenant_name` 用于展示 当前 `area/region` 先作为兼容展示字段保留,不立即强拆。 ## 5.2 推荐新增主数据表 建议新增: ```sql CREATE TABLE IF NOT EXISTS sys_tenants ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, tenant_code VARCHAR(64) NOT NULL UNIQUE, tenant_name VARCHAR(128) NOT NULL, tenant_type VARCHAR(32) NOT NULL DEFAULT 'REGIONAL', parent_tenant_code VARCHAR(64), is_enabled BOOLEAN NOT NULL DEFAULT TRUE, is_public BOOLEAN NOT NULL DEFAULT FALSE, sort_order INTEGER NOT NULL DEFAULT 0, metadata JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ ); ``` 建议 `tenant_type`: - `HEADQUARTERS` - `REGIONAL` - `PUBLIC` - `TEST` ## 5.3 推荐字段语义 | 字段 | 作用 | | --- | --- | | `tenant_code` | 稳定编码,例如 `mz`、`yf`、`jy`、`cz`、`hq` | | `tenant_name` | 展示名,例如 `梅州`、`云浮`、`揭阳`、`潮州`、`省局` | | `tenant_type` | 租户类别 | | `is_public` | 是否属于公共资源可见租户 | | `parent_tenant_code` | 可选,用于总部-地区树 | --- ## 6. 推荐兼容演进路径 ## 6.1 第一阶段:不强拆 area/region 第一阶段不建议立刻把所有业务表从 `area/region` 改成 `tenant_id`。 原因: - 当前系统大量 SQL 直接用字符串比较 - 一次性切换成本过高 第一阶段建议: 1. 新增 `sys_tenants` 2. 在所有“配置与入口层”先改成认租户主数据 3. 业务数据层先继续使用原 `area/region` 4. 通过 `tenant_code/tenant_name` 映射兼容旧值 ## 6.2 第二阶段:入口和配置域先租户化 优先改: 1. 入口模块 2. 首页入口可见性 3. 前端地区下拉 4. 交叉评查入口地区判断 5. RAG 地区配置页 ## 6.3 第三阶段:业务资源逐步 tenant_code 化 后续再逐步考虑: - `sso_users.area -> tenant_code` - `leaudit_documents.region -> tenant_code` - `contract_templates.region -> tenant_code` 但这一步可以晚于权限执行器改造。 --- ## 7. 入口模块改造方案 入口模块是本次租户化最优先的模块。 ## 7.1 当前问题 当前: - 表里 `areas` 是自由 JSON - 前端是固定地区多选框 - 首页用 `user_area` 直接匹配 JSON 里的 `area` ## 7.2 推荐改法 新增入口模块租户配置表: ```sql CREATE TABLE IF NOT EXISTS leaudit_entry_module_tenants ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, entry_module_id BIGINT NOT NULL REFERENCES leaudit_entry_modules(id) ON DELETE CASCADE, tenant_code VARCHAR(64) NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, sort_order INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(entry_module_id, tenant_code) ); ``` 说明: - 不建议继续把入口租户配置长期保留在 JSON - JSON 适合作为兼容字段,不适合作为主配置结构 ## 7.3 兼容策略 短期可以: 1. 保留 `leaudit_entry_modules.areas` 2. 新增 `leaudit_entry_module_tenants` 3. 后端优先读新表 4. 若新表为空,再回退读旧 JSON ## 7.4 前端改法 入口模块新建/编辑页不再使用固定 `AREA_OPTIONS`,改为: 1. 先调用租户列表接口 2. 渲染启用租户多选 3. 允许选择新增租户 --- ## 8. 首页入口改造方案 ## 8.1 当前问题 `homeServiceImpl.py` 当前按: - `user_area` - `em.areas[].area` - `default` - `super_admin bypass` 混合判定。 ## 8.2 推荐改法 引入: - `current_user.tenant_code` - `entry_module_tenants.tenant_code` 首页判断改为: 1. 用户可见租户集合 2. 模块绑定租户集合 3. 交集判断 公共模块单独处理: - 通过 `sys_tenants.is_public` - 或入口模块配置的公共租户绑定 ## 8.3 不要再用 `default` 建议: - `default` 语义废弃 - 若是“公共可见”,显式建公共租户 --- ## 9. 前端固定地区名单改造方案 ## 9.1 当前固定名单热点 已确认典型位置: - `EntryModuleNewClient.tsx` - `EntryModulesClient.tsx` - `cross-checking-access.ts` - `RAG` 地区配置相关页面 - 其他地区筛选组件 ## 9.2 推荐统一方案 新增前端租户 API: - `GET /api/v3/tenants` 返回: ```json [ { "tenant_code": "mz", "tenant_name": "梅州", "tenant_type": "REGIONAL", "is_public": false, "is_enabled": true, "sort_order": 10 } ] ``` 前端所有租户下拉统一改成: - 用这个接口拉 - 不再本地硬编码 ## 9.3 别名逻辑收缩 `cross-checking-access.ts` 这类 `AREA_ALIASES` 逻辑建议删除,改成: - 使用 `tenant_code` - `tenant_name` 只展示,不参与判断 --- ## 10. 用户模型改造建议 ## 10.1 当前情况 用户当前有: - `area` - `tenant_name` 但这两个字段并不等价: - `area` 更像当前业务租户边界 - `tenant_name` 更像组织树分组信息 ## 10.2 推荐方向 建议新增: - `sso_users.tenant_code` 并定义: - `tenant_code`:业务租户边界 - `tenant_name`:组织展示名称,可保留 - `area`:兼容旧字段,逐步弱化 ## 10.3 为什么不能直接复用 tenant_name 因为现有 `tenant_name` 明显被当作组织分组字段在用: - RBAC 组织树 - 用户管理分组 - 交叉评查成员展示 它不适合作为稳定权限租户编码。 --- ## 11. RAG、合同模板、统计的联动影响 ## 11.1 RAG 当前: - `省级` - `''` - `is_public` 混合表达公共范围。 建议改造: 1. 引入公共租户 2. 统一 `PUBLIC_MIXED` 的租户集合来源 3. 不再靠字符串字面值 `省级` ## 11.2 合同模板 当前: - `省级` 被当作公共模板归属 建议: - 用 `public tenant` 或 `headquarters tenant` - 不再把展示名硬编码进判断逻辑 ## 11.3 使用统计 统计筛选当前虽然不是入口配置,但未来如果租户扩展: - 地区筛选必须改成租户筛选 - area 文本不能再假定固定集合 --- ## 12. 交叉评查的潜在影响 交叉评查主权限模型是成员关系,不是地区模型。 但它仍会受租户化影响: 1. 入口模块可见性 2. 任务参与地区展示 3. 成员树的租户展示 所以它不是第一优先改造对象,但必须纳入兼容分析。 --- ## 13. 数据治理问题 在进入租户化之前,必须先做一次脏数据巡检。 建议巡检: 1. `sso_users.area` 去重值 2. `leaudit_documents.region` 去重值 3. `contract_templates.region` 去重值 4. `rag datasets/apps area` 去重值 5. `entry_modules.areas[].area` 去重值 目的: - 找出 `省局 / 省级 / default / 空串 / 其他历史拼法` 只有先做这一步,后续租户映射表才能准确建立。 --- ## 14. 推荐新增文档与接口 建议后续再补: 1. `租户主数据模型设计.md` 2. `地区到租户编码映射清洗清单.md` 3. `入口模块租户配置表迁移方案.md` 建议新增接口: 1. `GET /v3/tenants` 2. `POST /v3/tenants` 3. `PUT /v3/tenants/{TenantCode}` 4. `GET /v3/tenants/options` 其中 `options` 可专门给前端下拉用。 --- ## 15. 推荐实施顺序 建议按下面顺序做,而不是一次性全改: 1. 建 `sys_tenants` 2. 做租户脏数据巡检 3. 入口模块改成租户主数据驱动 4. 首页入口可见性改成租户集合判断 5. 前端所有固定地区下拉改成租户接口 6. 交叉评查/RAG/合同模板里的字符串公共语义收口 7. 再评估业务主表是否逐步从 `area/region` 迁到 `tenant_code` --- ## 16. 最终建议 当前系统不应再把“地区”视为一个永远固定的枚举集合。 从代码和业务实际上看,它已经是“租户边界”了,只是还没有主数据模型支撑。 如果现在不做这层抽象,后面每新增一个租户,都会反复踩同一类问题: 1. 前端没地方选 2. 入口模块分配不了 3. 首页看不到 4. 公共范围语义冲突 5. 老字符串拼法继续扩散 因此,建议把“地区租户化”作为权限架构之后的下一优先级平台改造项,且入口模块应作为第一落点。