# 入口模块租户配置表迁移方案 > 适用范围:首页入口模块、入口模块管理页、首页可见性判定、交叉评查入口等依赖 `leaudit_entry_modules.areas` 的场景 > 文档定位:把当前入口模块的 `areas JSONB` 从自由字符串配置升级为“租户关系表 + 主数据校验”的落地方案。 --- ## 1. 结论先行 当前入口模块是整个“地区租户化”里最明显、最优先、最适合先落地的切入点。 原因有 4 个: 1. 问题最直观,用户已经明确感知到“新租户没法分配入口” 2. 当前设计最脆弱,`areas JSONB` 没有主数据约束 3. 前后端都存在固定地区假设 4. 首页和交叉评查都依赖它做可见性判断 因此建议先把入口模块从: 1. `leaudit_entry_modules + areas JSONB` 升级为: 1. `leaudit_entry_modules` 2. `leaudit_entry_module_tenants` 3. `sys_tenants` --- ## 2. 当前问题深挖 ## 2.1 现有表结构问题 当前表: ```sql leaudit_entry_modules( id, name, description, path, icon_path, areas JSONB, sort_order, is_enabled ) ``` 问题: 1. `areas` 是自由 JSON 2. `area` 值没有外键约束 3. `enabled/sort_order` 与租户主数据重复 4. 无法防止写入不存在的租户 5. 无法直接做关系查询和索引优化 ## 2.2 后端当前风险 当前 `EntryModuleAdminServiceImpl.py`: 1. 创建和更新时直接把前端传入 `areas` 序列化成 JSON 2. 没有合法租户校验 3. 没有和用户租户主数据对齐 当前 `homeServiceImpl.py`: 1. 直接按 `user_area = area_item->>'area'` 2. 支持 `default` 兜底 3. `super_admin` 特判 bypass 这意味着: 1. 可见性规则绑定在脏字符串上 2. 新租户即使进库,首页也可能因为字符串不一致看不到 ## 2.3 前端当前风险 当前 `EntryModuleNewClient.tsx`: 1. 地区候选列表硬编码为固定 5 项 当前 `cross-checking-access.ts`: 1. 用固定地区别名数组判断入口可见性 2. `provincial_admin` 自动补 `省局` 这意味着: 1. 后端就算支持新租户,前端也无法选择 2. 首页入口和交叉评查入口会出现不一致 --- ## 3. 目标数据模型 ## 3.1 保留主表 保留: 1. `leaudit_entry_modules` 用于表达模块本身: 1. 名称 2. 描述 3. 跳转路径 4. 图标 5. 排序 6. 启用状态 ## 3.2 新增关系表 建议新增: ```sql CREATE TABLE IF NOT EXISTS leaudit_entry_module_tenants ( id BIGSERIAL PRIMARY KEY, entry_module_id BIGINT NOT NULL REFERENCES leaudit_entry_modules(id), tenant_code VARCHAR(64) NOT NULL, is_enabled BOOLEAN NOT NULL DEFAULT TRUE, sort_order INT NOT NULL DEFAULT 0, visibility_scope VARCHAR(32) NOT NULL DEFAULT 'TENANT', 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 (entry_module_id, tenant_code) ); ``` ## 3.3 为什么关系表足够 关系表解决了当前 JSON 方案的关键缺陷: 1. 可按 `tenant_code` 精确筛选 2. 可直接做唯一约束 3. 可扩展更多可见性字段 4. 可做索引和分页查询 5. 能和租户主数据自然联表 --- ## 4. 字段设计建议 ## 4.1 `tenant_code` 必须来自 `sys_tenants.tenant_code`,不允许任意字符串。 ## 4.2 `is_enabled` 表示该入口是否对该租户生效。 说明: 1. 模块总开关在 `leaudit_entry_modules.is_enabled` 2. 租户粒度开关在 `leaudit_entry_module_tenants.is_enabled` ## 4.3 `sort_order` 表示该模块在该租户视角下的局部排序。 如果当前不需要租户级排序,也建议先保留,因为后续: 1. 不同租户首页布局可能不同 2. 试点租户可能需要不同优先级 ## 4.4 `visibility_scope` 建议预留: 1. `TENANT` 2. `PUBLIC` 3. `HEADQUARTER_ONLY` 当前先以 `TENANT` 为主,但保留扩展空间,避免以后再次改表。 --- ## 5. 接口改造建议 ## 5.1 管理端列表接口 当前: 1. `GET /api/v3/entry-modules?area=...` 建议后续改为: 1. `GET /api/v3/entry-modules?tenant_code=...` 兼容期: 1. `area` 仍可保留,但后端先归一成 `tenant_code` ## 5.2 管理端创建/更新接口 当前请求体: ```json { "name": "交叉评查", "route_path": "/cross-checking", "areas": [ {"area": "梅州", "enabled": true, "sort_order": 1} ] } ``` 建议升级为: ```json { "name": "交叉评查", "route_path": "/cross-checking", "tenants": [ {"tenant_code": "MZ", "enabled": true, "sort_order": 1} ] } ``` 兼容策略: 1. 后端 DTO 同时接收 `areas` 和 `tenants` 2. 兼容期内优先使用 `tenants` 3. `areas` 走别名归一后再写关系表 ## 5.3 首页获取入口接口 当前: 1. `HomeServiceImpl.GetEntryModules(UserId)` 建议内部改造为: 1. 先解析用户 `tenant_code` 2. 再根据 `leaudit_entry_module_tenants` 判断可见性 SQL 方向应改成: 1. 联表 `leaudit_entry_module_tenants` 2. 不再扫描 `jsonb_array_elements` --- ## 6. 前端改造建议 ## 6.1 入口模块新建页 当前问题: 1. `AREA_OPTIONS` 写死 建议: 1. 页面加载时调用租户主数据接口 2. 用 `tenant_code` 作为 value 3. 用 `tenant_name` 作为 label 4. 表单字段从 `selectedAreas` 改为 `selectedTenantCodes` ## 6.2 入口模块列表页筛选 建议: 1. 筛选条件改为租户下拉 2. 后端接收 `tenant_code` 3. 列表展示使用租户名称而非裸字符串 ## 6.3 交叉评查入口判定 当前 `cross-checking-access.ts` 不应再: 1. 猜测地区别名 2. `includes("省")` 3. 针对 `provincial_admin` 自动补 `省局` 建议: 1. 登录态透出 `tenant_code` 2. 首页接口直接返回“当前用户可见模块” 3. 前端不再做租户匹配推断 --- ## 7. 迁移策略 ## 7.1 第一步:建新表,不改老表 先新增: 1. `leaudit_entry_module_tenants` 此时: 1. 旧 `areas JSONB` 继续存在 2. 新服务层支持双写 ## 7.2 第二步:把历史 JSON 展开入新表 迁移逻辑: 1. 读取 `leaudit_entry_modules.areas` 2. 对每个 `area` 执行租户归一 3. 写入 `leaudit_entry_module_tenants` 4. 对映射失败的值输出复核清单 ## 7.3 第三步:服务层读新表 顺序建议: 1. 管理端详情/列表先从新表组装返回 2. 首页可见性改为读新表 3. 交叉评查入口依赖首页接口结果,不再本地推断 ## 7.4 第四步:前端提交新结构 前端提交统一改成 `tenant_code` 后: 1. `areas` 字段只保留兼容 2. 新表成为唯一写入目标 ## 7.5 第五步:下线旧 JSON 待所有读写都切到新表后: 1. `areas` 字段可以保留为快照 2. 或在后续版本彻底废弃 --- ## 8. 兼容期设计 ## 8.1 DTO 兼容 建议 DTO 增加: 1. `tenants: list[EntryModuleTenantDTO] | None` 保留: 1. `areas: list[EntryModuleAreaDTO] | None` 优先级: 1. `tenants` 优先 2. `areas` 仅用于历史前端兼容 ## 8.2 VO 兼容 接口返回建议同时包含: 1. `tenants` 2. `areas` 其中: 1. `tenants` 是标准结构 2. `areas` 是兼容展示结构 避免一次性打崩旧页面。 ## 8.3 SQL seed 兼容 当前: 1. `seed_home_entry_modules.sql` 2. `seed_govdoc_entry_module.sql` 都直接写死 `areas JSONB`。 建议: 1. 先保留旧插入 2. 新增对应关系表 seed 3. 后续把内置入口的租户分配从 JSON 迁到关系表 --- ## 9. 需要同步改的代码点 后端重点: 1. `entryModuleDto.py` 2. `entryModuleAdminServiceImpl.py` 3. `homeServiceImpl.py` 4. `entryModuleController.py` 5. 相关 VO 定义 前端重点: 1. `EntryModuleNewClient.tsx` 2. `entry-modules` API 封装 3. 首页入口 hooks 4. `cross-checking-access.ts` 数据重点: 1. `schema_v2_add_evaluation_tables.sql` 2. `seed_home_entry_modules.sql` 3. `seed_govdoc_entry_module.sql` --- ## 10. 风险清单 ## 10.1 首页入口可能瞬时丢失 如果首页先切新表,但历史数据尚未迁入新关系表,会导致所有入口不可见。 所以顺序必须是: 1. 先迁数据 2. 再切读逻辑 ## 10.2 新旧字段并存期可能双写不一致 必须规定: 1. 标准源为新关系表 2. 旧 `areas` 只做兼容输出或快照 ## 10.3 交叉评查入口可能与首页结果不一致 因为当前它自己做了一套前端地区推断。 必须同步下线这套本地逻辑。 ## 10.4 自定义新租户仍可能被旧 seed 覆盖 如果后续还执行写死地区的 seed,会把新租户体系重新拉回旧模型。 --- ## 11. 验收标准 入口模块迁移完成后,至少应满足: 1. 新增租户后,无需改前端常量即可在管理页被选中 2. 管理端不能给不存在的租户分配入口 3. 首页入口仅按 `tenant_code` 判断,不再比对中文地区字符串 4. 交叉评查入口不再依赖固定地区别名数组 5. 历史入口模块在旧租户下可见性与迁移前一致 6. 新租户创建后,可以直接分配现有入口模块 --- ## 12. 本文档解决什么问题 本文档主要解决: 1. 入口模块为什么是租户化第一改造点 2. 当前 `areas JSONB` 设计具体哪里不够 3. 关系表应该怎么建 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)