diff --git a/docs/design/2026-04-20-intermediary-import-refactor-design.md b/docs/design/2026-04-20-intermediary-import-refactor-design.md new file mode 100644 index 00000000..5c59486a --- /dev/null +++ b/docs/design/2026-04-20-intermediary-import-refactor-design.md @@ -0,0 +1,522 @@ +# 中介库导入改造设计文档 + +**模块**: 中介库管理 +**日期**: 2026-04-20 +**作者**: Codex +**状态**: 待评审 + +## 一、背景 + +当前中介库模块已经完成“中介本人 / 中介亲属 / 中介关联机构”三类记录的统一展示与维护,但导入能力仍停留在旧语义: + +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. 对历史数据执行一次性迁移,不保留双语义兼容逻辑 + +## 三、范围 + +### 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.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 = 关联中介本人证件号码` + +### 7.2 `related_num_id` 语义切换 + +本次统一调整为: + +- **旧语义**:关联中介本人 `biz_id` +- **新语义**:关联中介本人 `person_id` + +影响原则: + +1. 手工新增亲属时写入本人证件号码 +2. 查询亲属列表时按本人证件号码关联 +3. 首页联合查询亲属记录时按本人证件号码关联 +4. 删除本人时按本人证件号码删除其亲属 +5. 导入亲属时按本人证件号码回填 `related_num_id` + +### 7.3 `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. 关系类型 +15. 备注 + +说明: + +1. `personSubType = 本人` 时,`relatedNumId` 必须为空 +2. `personSubType != 本人` 时,`relatedNumId` 必须填写“关联中介本人证件号码” + +#### 8.1.4 导入处理规则 + +导入分两阶段处理: + +第一阶段:处理中介本人 + +1. 提取所有 `personSubType = 本人` 的行 +2. 校验必填、证件号格式、文件内重复、库内重复 +3. 导入成功后建立“本人证件号集合” + +第二阶段:处理中介亲属 + +1. 提取所有 `personSubType != 本人` 的行 +2. 校验 `relatedNumId` 必填 +3. 校验 `relatedNumId` 对应的中介本人是否存在于: + - 数据库已存在记录 + - 本次第一阶段已导入成功的本人记录 +4. 校验亲属个人档案与关系唯一性 +5. 落库时 `related_num_id = 本人证件号码` + +#### 8.1.5 去重逻辑 + +双层去重: + +1. 文件内按 `personId` 去重 +2. 数据库内按 `personId` 去重 +3. 对亲属记录额外按 `relatedNumId + personId` 去重 + +失败判定口径: + +1. 同一文件内证件号重复,记失败 +2. 数据库中证件号已存在,记失败 +3. 同一文件内相同“本人证件号 + 亲属证件号”重复,记失败 +4. 数据库中相同“本人证件号 + 亲属证件号”已存在,记失败 + +### 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. `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. 导入模板说明文案 + +### 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. 同一历史关系迁移后是否会与现有新语义数据冲突 + +### 11.3 迁移原则 + +1. 先清洗异常数据,再执行正式迁移 +2. 迁移完成后,全部代码统一按新语义运行 +3. 不保留同时兼容 `biz_id` 与证件号码的查询逻辑 + +## 十二、异常处理口径 + +### 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. 中介实体关联关系导入成功与失败路径正确 + +### 13.2 前端测试 + +至少覆盖: + +1. 顶部两个导入按钮显示正确 +2. 模板下载地址正确 +3. 上传后异步轮询正常 +4. 完成后结果提示正确 +5. 失败记录弹窗、分页、清除历史记录可用 +6. 页面刷新后可恢复最近一次导入任务状态 + +## 十四、实施建议 + +建议实施顺序如下: + +1. 先完成 `related_num_id` 语义切换设计落地与历史数据迁移脚本 +2. 再改造中介模块内既有亲属查询与级联删除逻辑 +3. 再接入两类导入后端接口与异步处理 +4. 最后改造前端入口、弹窗、失败记录与状态恢复 + +## 十五、设计结论 + +本次中介库导入改造最终确认如下: + +1. 顶部仅保留 2 个导入按钮 +2. 中介本人和中介亲属合并为“导入中介信息” +3. 中介实体关联关系单独导入,落关系表 +4. `related_num_id` 整个中介模块统一改为“关联中介本人证件号码” +5. 所有导入采用纯新增模式 +6. 去重采用“文件内去重 + 库内去重” +7. 通过一次性历史数据迁移完成语义统一 + +该方案满足当前需求边界,且符合最短路径实现原则,不引入兼容性补丁方案。