# 中介库导入改造设计文档

**模块**: 中介库管理  
**日期**: 2026-04-20  
**作者**: Codex  
**状态**: 已实现（SQL 已执行，历史脏数据待清洗）

## 一、背景

当前中介库模块已经完成“中介本人 / 中介亲属 / 中介关联机构”三类记录的统一展示与维护，但导入能力仍停留在旧语义：

1. 页面导入入口与当前主从维护模式不一致
2. `related_num_id` 仍按“关联中介本人 `biz_id`”理解
3. 中介人员导入与中介实体关系导入边界不清晰
4. 导入交互未完全对齐系统内“员工数据导入”的标准模式

本次需求要求在中介库管理中补齐导入能力，并统一中介模块内 `relatedNumId` 的业务语义。

## 二、目标

本次设计目标如下：

1. 中介库顶部保留 2 个导入按钮：
   - 导入中介信息
   - 导入中介实体关联关系
2. “导入中介信息”统一处理中介本人和中介亲属，依据 `personSubType` 判断身份
3. “导入中介实体关联关系”单独写入 `ccdi_intermediary_enterprise_relation`
4. `ccdi_biz_intermediary.related_num_id` 在整个中介模块内统一改为保存“关联中介本人证件号码”
5. 全部导入采用纯新增模式，不支持覆盖更新
6. 导入交互、异步任务状态、失败记录展示与“员工数据导入”保持一致
7. 对历史数据执行一次性迁移，不保留双语义兼容逻辑
8. 中介模块现有外部接口继续保持 `bizId` 作为主资源标识，不改成证件号码路由
9. `relationType` 在中介模块中废弃，不再参与导入模板、校验与维护流程

## 三、范围

### 3.1 本次范围

- 中介库导入入口改造
- 中介信息导入与中介实体关联关系导入设计
- `related_num_id` 语义切换
- 中介模块内受影响代码调整
- 历史数据迁移设计
- 失败记录、去重、异步任务状态设计
- 设计文档、后端实施计划、前端实施计划沉淀

### 3.2 不在本次范围

- 不新增独立的“中介亲属关系导入”按钮
- 不新增机构主档导入能力
- 不支持导入覆盖更新
- 不做“同时兼容旧 `biz_id` 与新证件号码语义”的补丁方案
- 不抽象通用关系平台

## 四、现状分析

### 4.1 中介模块现状

当前中介库页面位于：

- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`

后端核心位于：

- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`

现有中介模块特点：

1. `ccdi_biz_intermediary` 同时承载中介本人和中介亲属
2. `ccdi_intermediary_enterprise_relation` 维护中介与机构关系
3. 中介统一列表通过联合查询展示本人、亲属、机构关系三类记录
4. 中介亲属当前通过 `related_num_id = 本人 biz_id` 建立归属关系

### 4.2 导入能力现状

当前仓库内已存在：

1. 个人中介导入异步链路
2. 实体中介导入异步链路
3. 员工数据导入标准交互

其中存在的偏差：

1. 现有中介导入仍沿用“个人 / 机构”旧语义
2. 顶部入口与当前业务结构不一致
3. 个人导入没有支持“本人 + 亲属”混合导入
4. 中介实体关联关系没有对应独立导入能力
5. `related_num_id` 新旧语义未统一

### 4.3 参考实现

本次导入交互和异步链路主要参考：

- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java`

本次关系导入与失败记录模式主要参考：

- 员工实体关系导入
- 员工亲属关系导入

## 五、方案对比

### 5.1 方案 A：两个导入按钮，人员与机构关系分开导入

做法：

1. 顶部仅保留“导入中介信息”和“导入中介实体关联关系”
2. 中介本人和中介亲属共用一个人员导入模板
3. 中介实体关联关系单独导入关系表
4. `related_num_id` 整模块统一改为“关联中介本人证件号码”
5. 执行一次性历史数据迁移

优点：

1. 与当前业务边界一致
2. 符合最短路径原则
3. 导入入口数量最少，业务理解成本低
4. 不引入补丁兼容逻辑

缺点：

1. 需要同步改造中介模块内既有主从关系代码
2. 需要补历史数据迁移脚本

### 5.2 方案 B：三个导入按钮，亲属关系独立导入

问题：

1. 与用户最终确认的交互不一致
2. 与“本人 / 亲属字段完全一致”的业务口径不一致
3. 增加额外入口和模板维护成本

### 5.3 方案 C：保留 `related_num_id` 旧语义，仅导入层转换

问题：

1. 形成代码与导入口径分裂
2. 后续维护成本高
3. 属于兼容型补丁方案，不符合本次约束

### 5.4 结论

采用 **方案 A：两个导入按钮，人员与机构关系分开导入，并整体切换 `related_num_id` 语义**。

## 六、总体设计

### 6.1 设计原则

1. 保持中介模块现有主从结构，不新增平行模块
2. 所有“亲属归属中介本人”的关系统一按证件号码表达
3. 导入全部为纯新增，失败逐条返回
4. 文件内去重与库内去重同时存在
5. 不引入补丁兼容逻辑
6. 现有外部接口对前端继续保持 `bizId` 契约，内部再转换为本人证件号码处理

### 6.2 顶部入口设计

中介库列表页顶部工具栏最终包含：

1. 新增
2. 导入中介信息
3. 导入中介实体关联关系
4. 查看中介信息导入失败记录
5. 查看中介实体关联关系导入失败记录

说明：

1. 不再保留独立“中介亲属关系导入”按钮
2. 两类导入任务各自维护状态和失败记录

## 七、数据模型与语义调整

### 7.1 `ccdi_biz_intermediary` 使用方式

`ccdi_biz_intermediary` 继续承载中介本人与中介亲属：

1. 中介本人
   - `person_sub_type = 本人`
   - `related_num_id = null`
2. 中介亲属
   - `person_sub_type != 本人`
   - `related_num_id = 关联中介本人证件号码`

唯一性口径调整为：

1. 中介本人 `person_id` 全表唯一
2. 中介亲属允许相同 `person_id` 关联多个不同中介本人
3. 同一中介本人名下的亲属以 `related_num_id + person_id` 唯一

### 7.2 `related_num_id` 语义切换

本次统一调整为：

- **旧语义**：关联中介本人 `biz_id`
- **新语义**：关联中介本人 `person_id`

影响原则：

1. 手工新增亲属时写入本人证件号码
2. 查询亲属列表时按本人证件号码关联
3. 首页联合查询亲属记录时按本人证件号码关联
4. 删除本人时按本人证件号码删除其亲属
5. 导入亲属时按本人证件号码回填 `related_num_id`
6. 外部接口继续传本人 `bizId`，服务层内部先查本人证件号码后再执行业务逻辑

### 7.3 外部接口契约

本次仅调整中介模块内部关联语义，不调整现有外部接口的路径参数契约：

1. 查询中介详情继续使用 `bizId`
2. 查询亲属列表继续使用 `bizId`
3. 新增中介亲属继续使用 `bizId`
4. 查询中介关联机构列表继续使用 `bizId`

服务层处理方式统一为：

1. 先根据外部传入的 `bizId` 查询中介本人
2. 再使用本人 `personId` 处理亲属链路
3. 机构关系链路继续使用本人 `bizId`

### 7.4 `ccdi_intermediary_enterprise_relation`

中介实体关联关系继续存于：

- `ccdi_intermediary_enterprise_relation`

该表语义保持不变：

1. `intermediary_biz_id` 仍保存中介本人 `biz_id`
2. 导入时先通过“中介本人证件号码”定位本人，再回填 `intermediary_biz_id`
3. 不在该链路中修改关系表结构

## 八、导入能力设计

### 8.1 导入中介信息

#### 8.1.1 目标

统一导入中介本人与中介亲属，落表：

- `ccdi_biz_intermediary`

#### 8.1.2 接口

建议保留或调整为：

1. `POST /ccdi/intermediary/importPersonTemplate`
2. `POST /ccdi/intermediary/importPersonData`
3. `GET /ccdi/intermediary/importPersonStatus/{taskId}`
4. `GET /ccdi/intermediary/importPersonFailures/{taskId}`

#### 8.1.3 模板字段

模板字段如下：

1. 姓名
2. 人员类型
3. 人员子类型
4. 性别
5. 证件类型
6. 证件号码
7. 手机号码
8. 微信号
9. 联系地址
10. 所在公司
11. 企业统一信用码
12. 职位
13. 关联中介本人证件号码
14. 备注

说明：

1. `personSubType` 在导入模板中必须使用下拉框，不允许自由文本输入
2. `personSubType` 下拉来源使用系统字典 `ccdi_person_sub_type`
3. 模板生成方式与系统现有 Excel 字典下拉保持一致
4. `personSubType = 本人` 时，`relatedNumId` 必须为空
5. `personSubType != 本人` 时，`relatedNumId` 必须填写“关联中介本人证件号码”
6. `relationType` 字段在本次导入中废弃，不出现在模板中，也不参与导入校验与落库

#### 8.1.4 导入处理规则

导入分两阶段处理：

第一阶段：处理中介本人

1. `personSubType = 本人` 时，`relatedNumId` 必须为空
2. 提取所有 `personSubType = 本人` 的行
3. 校验必填、证件号格式、文件内重复、库内重复
4. 导入成功后建立“本人证件号集合”

第二阶段：处理中介亲属

1. 提取所有 `personSubType != 本人` 的行
2. 校验 `relatedNumId` 必填
3. 校验 `relatedNumId` 对应的中介本人是否存在于：
   - 数据库已存在记录
   - 本次第一阶段已导入成功的本人记录
4. 允许相同亲属证件号码关联多个不同中介本人
5. 校验同一中介本人名下的亲属关系唯一性
6. 落库时 `related_num_id = 本人证件号码`

#### 8.1.5 去重逻辑

双层去重：

1. 对中介本人记录，文件内按 `personId` 去重
2. 对中介本人记录，数据库内按 `personId` 去重
3. 对中介亲属记录，文件内按 `relatedNumId + personId` 去重
4. 对中介亲属记录，数据库内按 `relatedNumId + personId` 去重

失败判定口径：

1. 中介本人证件号在同一文件内重复，记失败
2. 中介本人证件号在数据库中已存在，记失败
3. 同一文件内相同“本人证件号 + 亲属证件号”重复，记失败
4. 数据库中相同“本人证件号 + 亲属证件号”已存在，记失败
5. 相同亲属证件号若关联的是不同中介本人，允许导入成功

### 8.2 导入中介实体关联关系

#### 8.2.1 目标

导入中介与机构的关系，落表：

- `ccdi_intermediary_enterprise_relation`

#### 8.2.2 接口

新增独立接口：

1. `POST /ccdi/intermediary/importEnterpriseRelationTemplate`
2. `POST /ccdi/intermediary/importEnterpriseRelationData`
3. `GET /ccdi/intermediary/importEnterpriseRelationStatus/{taskId}`
4. `GET /ccdi/intermediary/importEnterpriseRelationFailures/{taskId}`

#### 8.2.3 模板字段

模板字段如下：

1. 中介本人证件号码
2. 统一社会信用代码
3. 关联角色/职务
4. 备注

#### 8.2.4 导入处理规则

1. 根据“中介本人证件号码”查找 `person_sub_type = 本人` 的中介本人
2. 根据统一社会信用代码校验机构已存在于 `ccdi_enterprise_base_info`
3. 将本人 `biz_id` 回填为 `intermediary_biz_id`
4. 写入 `ccdi_intermediary_enterprise_relation`

#### 8.2.5 去重逻辑

双层去重：

1. 文件内按 `中介本人证件号码 + 统一社会信用代码` 去重
2. 数据库内按 `intermediary_biz_id + social_credit_code` 去重

失败判定口径：

1. 中介本人证件号码不存在，记失败
2. 统一社会信用代码不存在于系统机构表，记失败
3. 文件内重复关系，记失败
4. 数据库内已存在关系，记失败

## 九、异步任务与失败记录设计

### 9.1 异步链路

两类导入都采用与员工数据导入一致的异步模式：

1. Controller 解析 Excel
2. 主 Service 生成 `taskId`
3. Redis 初始化状态为 `PROCESSING`
4. 异步 ImportService 后台处理
5. 前端轮询任务状态
6. 完成后展示结果并支持查看失败记录

### 9.2 Redis Key 设计

中介信息导入：

1. `import:intermediary-person:{taskId}`
2. `import:intermediary-person:{taskId}:failures`

中介实体关联关系导入：

1. `import:intermediary-enterprise-relation:{taskId}`
2. `import:intermediary-enterprise-relation:{taskId}:failures`

### 9.3 状态字段

状态字段统一为：

1. `taskId`
2. `status`
3. `totalCount`
4. `successCount`
5. `failureCount`
6. `progress`
7. `startTime`
8. `endTime`
9. `message`

### 9.4 失败记录

失败记录按模板字段原样返回，并增加：

- `errorMessage`

前端可直接复用员工数据导入的失败记录弹窗模式。

## 十、`relatedNumId` 语义切换影响清单

### 10.1 直接受影响代码

1. `CcdiIntermediaryServiceImpl`
   - 亲属新增、修改、查询、删除、级联删除
2. `CcdiIntermediaryMapper.xml`
   - 统一列表中亲属联表条件
3. `CcdiIntermediaryPersonImportServiceImpl`
   - 本人/亲属混合导入逻辑
4. `CcdiIntermediaryController`
   - 导入接口重组
5. `CcdiBizIntermediary`
   - 字段语义注释
6. `CcdiIntermediaryPersonDetailVO`
7. `CcdiIntermediaryRelativeVO`
8. `CcdiIntermediaryPersonAddDTO`
9. `CcdiIntermediaryPersonEditDTO`
10. `CcdiIntermediaryRelativeAddDTO`
11. `CcdiIntermediaryRelativeEditDTO`
12. `CcdiIntermediaryPersonExcel`

### 10.2 前端受影响代码

1. `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
2. `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
3. `ruoyi-ui/src/api/ccdiIntermediary.js`
4. 失败记录弹窗相关状态管理
5. 导入模板说明文案
6. 现有依赖 `bizId` 的亲属维护入口保持不变

### 10.3 基本不受影响代码

1. `CcdiIntermediaryEnterpriseRelationMapper.xml`
   - 仍通过 `intermediary_biz_id -> biz_id` 关联
2. 账户模块中 `INTERMEDIARY` 所属人校验逻辑
   - 当前仅以中介证件号作为 ownerId，不依赖 `relatedNumId`

### 10.4 文档与资料

以下文档需要同步修正语义：

1. 旧中介库设计文档
2. 中介 API 文档
3. 数据库字段说明
4. 相关实施计划与测试文档

## 十一、历史数据迁移设计

### 11.1 迁移目标

将历史数据中 `related_num_id = 中介本人 biz_id` 的记录一次性迁移为：

- `related_num_id = 中介本人 person_id`

### 11.2 迁移前校验

迁移前必须检查：

1. `related_num_id` 有值，但找不到对应本人 `biz_id`
2. 找到本人后，本人 `person_id` 为空
3. 同一历史关系迁移后是否会与现有新语义数据冲突
4. 现有亲属记录在迁移后是否出现同一中介本人名下 `related_num_id + person_id` 冲突

### 11.3 迁移原则

1. 按用户要求，迁移先于功能改造执行
2. 先清洗异常数据，再执行正式迁移
3. 迁移完成后，全部代码统一按新语义运行
4. 不保留同时兼容 `biz_id` 与证件号码的查询逻辑
5. 为避免旧逻辑读取失败，迁移、代码发布、回归验证必须放在同一实施窗口内连续完成

## 十二、异常处理口径

### 12.1 导入中介信息

常见失败原因包括：

1. 证件号码不能为空
2. 人员子类型不能为空
3. `personSubType = 本人` 但 `relatedNumId` 非空
4. `personSubType != 本人` 但 `relatedNumId` 为空
5. 关联中介本人证件号码不存在
6. 中介本人证件号在导入文件内重复
7. 中介本人证件号在数据库中已存在
8. 导入文件内亲属关系重复
9. 数据库中亲属关系已存在

### 12.2 导入中介实体关联关系

常见失败原因包括：

1. 中介本人证件号码不能为空
2. 统一社会信用代码不能为空
3. 中介本人不存在
4. 统一社会信用代码不存在于系统机构表
5. 导入文件内关系重复
6. 数据库中关系已存在

## 十三、测试边界

### 13.1 后端测试

至少覆盖：

1. 历史数据迁移后亲属列表查询正确
2. 手工新增亲属后 `related_num_id` 保存本人证件号码
3. 删除中介本人时级联删除其亲属和关联机构关系
4. 本人和亲属混合导入成功
5. 亲属引用“本次文件内先导入成功的本人”成功
6. 相同亲属证件号关联不同中介本人成功
7. 文件内重复、库内重复、无效关联均能正确记失败
8. 中介实体关联关系导入成功与失败路径正确

### 13.2 前端测试

至少覆盖：

1. 顶部两个导入按钮显示正确
2. 模板下载地址正确
3. 上传后异步轮询正常
4. 完成后结果提示正确
5. 失败记录弹窗、分页、清除历史记录可用
6. 页面刷新后可恢复最近一次导入任务状态

## 十四、实施建议

建议实施顺序如下：

1. 先完成 `related_num_id` 历史数据迁移脚本与迁移校验 SQL
2. 在同一实施窗口内先执行迁移
3. 紧接着发布中介模块语义切换代码，完成亲属查询、手工维护、删除级联改造
4. 再接入两类导入后端接口与异步处理
5. 最后改造前端入口、弹窗、失败记录与状态恢复

## 十五、设计结论

本次中介库导入改造最终确认如下：

1. 顶部仅保留 2 个导入按钮
2. 中介本人和中介亲属合并为“导入中介信息”
3. 中介实体关联关系单独导入，落关系表
4. `related_num_id` 整个中介模块统一改为“关联中介本人证件号码”
5. 所有导入采用纯新增模式
6. 中介本人按证件号全局唯一，亲属允许关联多个不同中介本人
7. 去重采用“文件内去重 + 库内去重”
8. 外部接口继续保持 `bizId` 契约
9. `relationType` 废弃，`personSubType` 使用字典下拉
10. 通过一次性历史数据迁移完成语义统一

该方案满足当前需求边界，且符合最短路径实现原则，不引入兼容性补丁方案。

## 十六、实施回写

- 2026-04-20 已完成中介模块 `related_num_id` 语义切换，手工新增亲属、统一列表查询、本人证件号变更同步和级联删除逻辑均按“关联中介本人证件号码”改造。
- 2026-04-20 已完成“导入中介信息”和“导入中介实体关联关系”两条异步导入链路，并同步完成前端双按钮、双任务状态、双失败记录模式改造。
- 2026-04-20 已完成后端目标测试回归、信息采集模块编译、前端静态单测和 `build:prod` 构建验证。
- 2026-04-20 已执行 `bin/mysql_utf8_exec.sh sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql` 与 `bin/mysql_utf8_exec.sh sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`。
- 迁移后核查结果：`legacy_biz_id_reference = 0`，说明旧 `biz_id` 语义残留已清零；`post_migration_missing_parent = 1025`、`owner_person_id_empty_after_migration = 754`，说明历史数据中仍存在无法关联到本人证件号的脏数据，需后续专项清洗。
