# Staff Recruitment Dual-Sheet Import Backend Implementation Plan > **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. **Goal:** 将招聘信息管理后端导入链路收口为“招聘信息 + 历史工作经历”双 Sheet 单任务模式,并补齐失败 Sheet、失败行号、失败原因。 **Architecture:** 保留现有 `ccdi:staffRecruitment:*` 权限、`/ccdi/staffRecruitment/*` 路径和现有主从表结构,只收口控制器、服务接口和异步导入编排。导入任务先处理主信息 Sheet,再按 `recruitId` 分组处理工作经历 Sheet,工作经历匹配“本次主 Sheet 成功数据 + 数据库已有主信息”,若数据库中已存在旧工作经历则直接整组失败,不做覆盖。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis-Plus, EasyExcel, Redis, JUnit 5, Mockito --- ## File Map - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java` - 收口双 Sheet 模板下载与统一导入入口,移除独立工作经历导入接口 - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java` - 暴露统一的双 Sheet 提交入口 - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java` - 暴露统一的双 Sheet 异步导入入口 - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java` - 初始化统一 Redis 任务状态并提交统一异步任务 - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` - 实现主 Sheet 与工作经历 Sheet 两阶段编排、行号上下文、已有工作经历报错规则 - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java` - 新增 `sheetName`、`sheetRowNum` - Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java` - 锁定控制器与接口的双 Sheet 契约 - Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java` - 锁定异步导入编排、已有工作经历报错和失败定位行为 - Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java` - 补模板双 Sheet 约束回归 - Create: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md` - 记录最终实施内容、验证结果与浏览器实测结论 ### Task 1: 锁定双 Sheet 导入接口契约 **Files:** - Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java` - [ ] **Step 1: 写控制器与接口契约失败测试** ```java @Test void shouldExposeSingleDualSheetImportEntry() throws Exception { String controller = Files.readString( Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java") ); assertTrue(controller.contains("\"招聘信息\"")); assertTrue(controller.contains("\"历史工作经历\"")); assertFalse(controller.contains("workImportTemplate")); assertFalse(controller.contains("importWorkData")); } ``` - [ ] **Step 2: 运行测试确认失败** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test` Expected: FAIL,提示控制器仍存在 `workImportTemplate` / `importWorkData` 或接口签名仍为双入口 - [ ] **Step 3: 最小化修改控制器与服务接口** ```java String importRecruitment( List recruitmentList, List workList ); void importRecruitmentAsync( List recruitmentList, List workList, String taskId, String userName ); ``` - [ ] **Step 4: 重跑契约测试** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test` Expected: PASS,控制器仅保留双 Sheet 模板与单导入入口 - [ ] **Step 5: 提交这一小步** ```bash git add \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java git commit -m "收口招聘双Sheet导入接口" ``` ### Task 2: 接入双 Sheet 模板与失败 VO 字段 **Files:** - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java` - Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java` - Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java` - [ ] **Step 1: 先补失败 VO 与模板契约测试** ```java assertHasField( "com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO", "sheetName" ); assertHasField( "com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO", "sheetRowNum" ); ``` - [ ] **Step 2: 运行测试确认失败** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,EasyExcelUtilTemplateTest test` Expected: FAIL,提示 `RecruitmentImportFailureVO` 缺字段或模板断言未通过 - [ ] **Step 3: 最小化实现字段与模板导出** ```java EasyExcelUtil.importTemplateWithDictDropdown( response, CcdiStaffRecruitmentExcel.class, "招聘信息", CcdiStaffRecruitmentWorkExcel.class, "历史工作经历", "招聘信息管理导入模板" ); ``` - [ ] **Step 4: 重跑测试** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,EasyExcelUtilTemplateTest test` Expected: PASS,模板输出双 Sheet,失败 VO 具备新字段 - [ ] **Step 5: 提交这一小步** ```bash git add \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java git commit -m "补齐招聘双Sheet模板与失败字段" ``` ### Task 3: 收口服务层任务初始化与统一状态统计 **Files:** - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java` - Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java` - [ ] **Step 1: 先写服务层统一任务初始化失败测试** ```java @Test void shouldInitializeSingleRedisTaskForTwoSheets() throws Exception { String service = Files.readString( Path.of("src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java") ); assertTrue(service.contains("recruitmentList.size() + workList.size()")); assertFalse(service.contains("importRecruitmentWork(")); } ``` - [ ] **Step 2: 运行测试确认失败** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test` Expected: FAIL,提示服务层仍保留独立工作经历任务初始化 - [ ] **Step 3: 实现统一提交入口** ```java public String importRecruitment( List recruitmentList, List workList ) { int totalCount = recruitmentList.size() + workList.size(); recruitmentImportService.importRecruitmentAsync(recruitmentList, workList, taskId, userName); return taskId; } ``` - [ ] **Step 4: 重跑契约测试** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest test` Expected: PASS,任务总数按双 Sheet 合并统计,接口只剩统一入口 - [ ] **Step 5: 提交这一小步** ```bash git add \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java git commit -m "统一招聘双Sheet任务初始化" ``` ### Task 4: 实现异步导入两阶段编排与失败定位 **Files:** - Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` - [ ] **Step 1: 先写异步导入失败测试** ```java @Test void shouldFailWholeWorkGroupWhenExistingHistoryExists() { // arrange: recruitmentWorkMapper.countByRecruitId("RC001") -> 1 // act: importRecruitmentAsync(mainRows, workRows, taskId, "admin") // assert: recruitmentWorkMapper.insert(...) not called // assert: failure.sheetName == "历史工作经历" // assert: failure.sheetRowNum == "2" } ``` - [ ] **Step 2: 运行测试确认失败** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentImportServiceImplTest test` Expected: FAIL,提示当前实现仍会删除旧工作经历或未记录 `sheetName` / `sheetRowNum` - [ ] **Step 3: 以最小改动实现两阶段编排** ```java List indexedMainRows = buildMainImportRows(recruitmentList); List indexedWorkRows = buildWorkImportRows(workList); Map importedRecruitmentMap = importMainSheet(indexedMainRows, failures, userName); importWorkSheet(indexedWorkRows, importedRecruitmentMap, failures, userName); ``` - [ ] **Step 4: 补充工作经历“已有旧记录即失败”与行号上下文** ```java if (hasExistingWorkHistory(recruitId)) { throw buildValidationException( "历史工作经历", extractWorkRowNums(workRows), String.format("招聘记录编号[%s]已存在历史工作经历,不允许重复导入", recruitId) ); } ``` - [ ] **Step 5: 重跑测试** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentImportServiceImplTest test` Expected: PASS,已有工作经历不覆盖,失败记录带 Sheet 与行号 - [ ] **Step 6: 提交这一小步** ```bash git add \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java git commit -m "实现招聘双Sheet异步导入编排" ``` ### Task 5: 做后端回归、补实施记录并交付联调入口 **Files:** - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` - Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java` - Create: `docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md` - [ ] **Step 1: 运行后端定向测试** Run: `mvn -pl ccdi-info-collection -Dtest=CcdiStaffRecruitmentDualImportContractTest,CcdiStaffRecruitmentImportServiceImplTest,EasyExcelUtilTemplateTest test` Expected: PASS,双 Sheet 契约、异步编排、模板约束全部通过 - [ ] **Step 2: 运行模块编译** Run: `mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile` Expected: BUILD SUCCESS - [ ] **Step 3: 启动后端供前端联调** Run: `sh bin/restart_java_backend.sh` Expected: 后端正常重启,`/ccdi/staffRecruitment/importTemplate` 与 `/importData` 可访问 - [ ] **Step 4: 补实施记录** ```md - 导入入口收口为双 Sheet 单任务 - 工作经历导入改为“已有旧记录时报错” - 失败记录补齐失败 Sheet、失败行号、失败原因 - 已完成后端编译与定向测试 ``` - [ ] **Step 5: 提交后端收尾** ```bash git add \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java \ ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentDualImportContractTest.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java \ ccdi-info-collection/src/test/java/com/ruoyi/info/collection/utils/EasyExcelUtilTemplateTest.java \ docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md git commit -m "完成招聘双Sheet导入后端改造" ```