# 中介库导入改造后端实施计划

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
> **执行约束：** 仓库约定不开 subagent，执行时使用 `superpowers:executing-plans`。

**Goal:** 完成中介库后端导入改造，统一 `related_num_id` 为“关联中介本人证件号码”，新增“导入中介信息”和“导入中介实体关联关系”两条异步导入链路，并保证手工维护、统一查询和级联删除与新语义一致。

**Architecture:** 后端保持 `CcdiIntermediaryController + CcdiIntermediaryServiceImpl + ccdi_biz_intermediary/ccdi_intermediary_enterprise_relation` 的现有主线，不新增平行模块。外部 API 继续保持 `bizId` 契约，服务层内部把亲属链路统一转换为“本人证件号码”处理；导入链路对齐员工数据导入，拆分为“中介信息导入”和“中介实体关联关系导入”两套独立任务与失败记录。

**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MySQL, Redis, EasyExcel, JUnit 5, Maven, Markdown

---

## 文件结构与职责

**设计与计划基线**

- `docs/design/2026-04-20-intermediary-import-refactor-design.md`
  当前需求和边界的唯一设计基线，实施不得偏离其中已确认口径。

**SQL 与迁移**

- Create: `sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
  负责把历史 `related_num_id = 本人 biz_id` 迁移为 `related_num_id = 本人 person_id`。
- Create: `sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
  负责补齐或校正 `ccdi_person_sub_type` 字典项，供导入模板下拉使用。

**中介主链路**

- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java`
  调整 `relatedNumId` 字段注释语义。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java`
  暴露新的校验与导入能力方法。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
  完成亲属主从语义切换、手工维护逻辑修正、关系唯一性判断和级联删除。
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml`
  把统一列表中的亲属关联条件从 `parent.biz_id` 改为 `parent.person_id`。

**导入链路**

- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
  重组导入接口，保留中介信息导入，新增实体关联关系导入。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java`
  让 `personSubType` 使用字典下拉，移除废弃的 `relationType` 列，重命名 `relatedNumId` 模板文案。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java`
  对齐新模板字段，移除废弃字段。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java`
  调整中介信息导入接口定义。
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
  重写为“本人阶段 + 亲属阶段”的混合导入实现。
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java`
  中介实体关联关系导入模板实体。
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java`
  实体关联关系失败记录 VO。
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java`
  实体关联关系导入服务接口。
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
  实体关联关系异步导入实现。

**既有 DTO / VO / Mapper 补充**

- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryPersonAddDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryPersonEditDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryPersonDetailVO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiIntermediaryRelativeVO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryEnterpriseRelationMapper.java`
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml`

**测试**

- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java`

## 实施任务

### Task 1: 编写迁移脚本并锁定实施窗口

**Files:**

- Create: `sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
- Create: `sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
- Reference: `sql/migration/2026-04-17-fix-intermediary-person-sub-type-dict.sql`
- Reference: `sql/dpc_intermediary_dict_data_20260129.sql`

- [ ] **Step 1: 写迁移前校验 SQL**

在迁移脚本头部先写校验查询，明确输出：

```sql
-- 1. 找不到对应本人 biz_id 的亲属
-- 2. 本人 person_id 为空的记录
-- 3. 迁移后同一中介本人下 related_num_id + person_id 冲突的记录
```

- [ ] **Step 2: 写正式迁移 SQL**

将旧语义统一改为新语义：

```sql
UPDATE ccdi_biz_intermediary child
JOIN ccdi_biz_intermediary parent
  ON child.related_num_id = parent.biz_id
SET child.related_num_id = parent.person_id
WHERE child.person_sub_type <> '本人';
```

- [ ] **Step 3: 写 `ccdi_person_sub_type` 字典修正脚本**

至少补齐：

```sql
本人 / 配偶 / 子女 / 父母 / 兄弟姐妹 / 其他
```

Expected: 模板下拉和后端校验使用同一字典源。

- [ ] **Step 4: 用 UTF-8 脚本执行 SQL 验证**

Run:

```bash
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
```

Expected: SQL 正常执行，无乱码，无唯一性冲突。

- [ ] **Step 5: 提交迁移脚本**

```bash
git add sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql
git commit -m "新增中介导入迁移脚本"
```

### Task 2: 切换中介模块内部主从语义

**Files:**

- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java`

- [ ] **Step 1: 先写失败测试，覆盖亲属新语义**

补测试场景：

```java
// 1. 手工新增亲属时 related_num_id 写本人 person_id
// 2. 查询亲属列表按 parent.person_id 关联
// 3. 删除本人时按本人 person_id 级联删除亲属
// 4. 相同亲属 person_id 挂到不同本人时允许成功
```

- [ ] **Step 2: 调整服务层内部转换逻辑**

关键改造点：

```java
CcdiBizIntermediary owner = requireIntermediaryPerson(bizId);
relative.setRelatedNumId(owner.getPersonId());
```

以及：

```java
wrapper.eq(CcdiBizIntermediary::getRelatedNumId, owner.getPersonId())
```

- [ ] **Step 3: 收敛唯一性校验**

新增两个明确规则：

```java
// 本人：personId 全表唯一
// 亲属：同一 relatedNumId + personId 唯一
```

Expected: 不再把亲属 `personId` 误当成全表唯一。

- [ ] **Step 4: 修改联合查询 SQL**

把亲属联表条件改成：

```xml
ON child.related_num_id = parent.person_id
```

Expected: 统一列表在迁移后仍能查出亲属记录。

- [ ] **Step 5: 运行服务层与 Mapper 测试**

Run:

```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest test
```

Expected: 目标测试通过，新的主从语义断言成立。

- [ ] **Step 6: 提交语义切换代码**

```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiBizIntermediary.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryMapper.xml ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeAddDTO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryRelativeEditDTO.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiIntermediaryMapperTest.java
git commit -m "调整中介亲属关联语义"
```

### Task 3: 重构中介信息导入为“本人 + 亲属”混合导入

**Files:**

- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`

- [ ] **Step 1: 先写导入失败测试**

覆盖：

```java
// 1. personSubType 使用字典下拉字段
// 2. personSubType=本人 且 relatedNumId 非空 -> 失败
// 3. personSubType!=本人 且 relatedNumId 为空 -> 失败
// 4. 亲属引用本次导入前面成功的本人 -> 成功
// 5. 同一亲属挂不同本人 -> 成功
// 6. 同一本人名下重复亲属 -> 失败
```

- [ ] **Step 2: 调整 Excel 模板实体**

最小改动：

```java
@DictDropdown(dictType = "ccdi_person_sub_type")
private String personSubType;

@ExcelProperty("关联中介本人证件号码")
private String relatedNumId;
```

同时删除：

```java
private String relationType;
```

- [ ] **Step 3: 按“两阶段”重写导入实现**

核心流程：

```java
List<CcdiIntermediaryPersonExcel> owners = ...
List<CcdiIntermediaryPersonExcel> relatives = ...

// owners: personId 全表唯一
// relatives: relatedNumId + personId 唯一
```

Expected: 中介本人和亲属在一个文件中可混合导入。

- [ ] **Step 4: 统一失败记录字段**

`IntermediaryPersonImportFailureVO` 只保留当前模板字段，不再暴露废弃的 `relationType`。

- [ ] **Step 5: 调整 Controller 返回文案与模板下载**

Expected: 下载模板时 `personSubType` 下拉来源于 `ccdi_person_sub_type`，返回的失败记录字段与模板一致。

- [ ] **Step 6: 运行导入链路测试**

Run:

```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryControllerTest test
```

Expected: 混合导入与失败口径通过。

- [ ] **Step 7: 提交中介信息导入改造**

```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryPersonExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryPersonImportFailureVO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryPersonImportService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryPersonImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryPersonImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java
git commit -m "改造中介信息导入链路"
```

### Task 4: 新增中介实体关联关系导入链路

**Files:**

- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiIntermediaryController.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java`
- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java`

- [ ] **Step 1: 先写关系导入测试**

覆盖：

```java
// 1. 中介本人证件号码不存在 -> 失败
// 2. socialCreditCode 不存在于机构表 -> 失败
// 3. 同一文件内相同 personId + socialCreditCode -> 失败
// 4. 数据库内已存在相同 intermediary_biz_id + socialCredit_code -> 失败
// 5. 正常导入成功并写入关系表
```

- [ ] **Step 2: 新建 Excel 与失败记录对象**

字段固定为：

```java
ownerPersonId
socialCreditCode
relationPersonPost
remark
```

- [ ] **Step 3: 新建异步导入服务**

服务逻辑：

```java
// 证件号码 -> 查本人 bizId
// 社会信用代码 -> 校验机构存在
// intermediaryBizId + socialCreditCode 唯一
```

- [ ] **Step 4: 补 Controller 接口**

新增：

```java
/importEnterpriseRelationTemplate
/importEnterpriseRelationData
/importEnterpriseRelationStatus/{taskId}
/importEnterpriseRelationFailures/{taskId}
```

- [ ] **Step 5: 运行关系导入测试**

Run:

```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiIntermediaryControllerTest test
```

Expected: 实体关联关系导入链路完整通过。

- [ ] **Step 6: 提交关系导入代码**

```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiIntermediaryEnterpriseRelationImportService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java 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 ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiIntermediaryEnterpriseRelationMapper.xml ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiIntermediaryControllerTest.java
git commit -m "新增中介实体关联关系导入"
```

### Task 5: 做完整回归验证并更新文档

**Files:**

- Modify: `docs/design/2026-04-20-intermediary-import-refactor-design.md`
- Modify: `docs/plans/backend/2026-04-20-intermediary-import-backend-implementation.md`
- Reference: `bin/mysql_utf8_exec.sh`

- [ ] **Step 1: 运行后端完整相关测试集合**

Run:

```bash
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test
```

Expected: 全部 PASS。

- [ ] **Step 2: 编译信息采集模块，确认没有遗漏编译错误**

Run:

```bash
mvn -pl ccdi-info-collection -am clean compile
```

Expected: `BUILD SUCCESS`。

- [ ] **Step 3: 记录 SQL 执行与测试结果**

在计划文档底部补：

```markdown
- 实际执行的 SQL 脚本
- 实际执行的 Maven 命令
- PASS / FAIL 结果
```

- [ ] **Step 4: 提交文档回写**

```bash
git add docs/design/2026-04-20-intermediary-import-refactor-design.md docs/plans/backend/2026-04-20-intermediary-import-backend-implementation.md
git commit -m "补充中介导入后端实施记录"
```

## 验证命令

```bash
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
mvn -pl ccdi-info-collection -am -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test
mvn -pl ccdi-info-collection -am clean compile
```

## 完成标准

- `related_num_id` 已按“关联中介本人证件号码”完成迁移和代码切换
- 手工新增亲属、查询亲属、删除本人级联删除全部与新语义一致
- 中介信息导入支持本人和亲属混合导入
- 同一亲属证件号允许关联多个不同中介本人
- 中介实体关联关系导入独立可用，且只写关系表
- `relationType` 已从导入链路中移除
- `personSubType` 模板下拉已切换为 `ccdi_person_sub_type`
- 后端测试与编译验证通过

## 执行结果

- SQL 脚本：
  `sql/migration/2026-04-20-fix-ccdi-person-sub-type-dict.sql`
  `sql/migration/2026-04-20-migrate-intermediary-related-num-id-to-person-id.sql`
  结果：已执行 `bin/mysql_utf8_exec.sh`。
- SQL 核查：
  `post_migration_missing_parent = 1025`
  `legacy_biz_id_reference = 0`
  `owner_person_id_empty_after_migration = 754`
  结果：可迁移数据已切换完成，旧 `biz_id` 语义残留清零，但历史脏数据仍需后续清洗。
- Maven 命令：
  `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest test`
  结果：PASS
- Maven 命令：
  `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiIntermediaryControllerTest test`
  结果：PASS
- Maven 命令：
  `mvn -pl ccdi-info-collection -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryMapperTest,CcdiIntermediaryControllerTest,CcdiIntermediaryPersonImportServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test`
  结果：PASS
- Maven 命令：
  `mvn -pl ccdi-info-collection -am clean compile`
  结果：PASS（BUILD SUCCESS）
