# 入口模块租户配置表迁移方案

> 适用范围：首页入口模块、入口模块管理页、首页可见性判定、交叉评查入口等依赖 `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)
