# 项目详情风险明细异常账户人员信息后端 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. > > 仓库约束:当前仓库明确禁止开启 subagent,执行时统一使用 `superpowers:executing-plans`。 **Goal:** 为项目详情风险明细补齐“异常账户人员信息”的真实后端查询与统一导出能力,让页面展示和第 3 个 Excel sheet 共用同一套异常账户明细口径。 **Architecture:** 在结果总览域内新增异常账户分页查询接口和非分页导出查询,数据源直接使用 `ccdi_bank_statement_tag_result + ccdi_account_info`,按“一条命中结果一行”返回。统一导出继续复用 `POST /ccdi/project/overview/risk-details/export`,仅将第 3 个 sheet 从空表头改为真实数据写出,不新增平行导出接口。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus Page, MyBatis XML, Apache POI, JUnit 5, Mockito --- ## File Map **Create:** - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java` - 结果总览异常账户分页查询入参,仅承载 `projectId/pageNum/pageSize` - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java` - 异常账户单行展示对象,字段与页面列一致 - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java` - 异常账户分页返回对象,统一承载 `rows/total` - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java` - `异常账户人员信息` sheet 行对象 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java` - 单独覆盖异常账户分页与导出映射 **Modify:** - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java` - 新增异常账户分页查询接口 - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java` - 新增异常账户分页与导出方法定义 - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` - 实现异常账户分页查询、导出映射,并接入统一导出流程 - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java` - 新增异常账户分页与导出查询声明 - `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` - 新增异常账户基础 SQL、分页 SQL、导出 SQL - `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java` - 第 3 个 sheet 改为写出真实异常账户数据 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java` - 覆盖新分页接口委托行为 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java` - 覆盖新接口路径、注解与参数签名 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java` - 更新统一导出测试,断言第 3 个 sheet 已传入真实数据 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java` - 更新工作簿导出断言,校验异常账户真实数据行 - `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java` - 覆盖异常账户分页与导出 SQL 口径 - `docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md` - 记录后端实施细节与验证结果 ## Task 1: 锁定结果总览异常账户分页接口契约 **Files:** - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java` - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java` - [ ] **Step 1: 先写控制器契约测试** 在 `CcdiProjectOverviewControllerContractTest` 中新增对新方法的反射断言: ```java Method method = controllerClass.getMethod( "getAbnormalAccountPeople", CcdiProjectAbnormalAccountQueryDTO.class ); GetMapping getMapping = method.getAnnotation(GetMapping.class); assertEquals("/abnormal-account-people", getMapping.value()[0]); ``` 同时断言: - 存在 `@Operation(summary = "查询异常账户人员信息")` - 存在 `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")` - [ ] **Step 2: 再写控制器委托单测,先让它失败** 在 `CcdiProjectOverviewControllerTest` 中新增测试: ```java CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); queryDTO.setProjectId(40L); when(overviewService.getAbnormalAccountPeople(queryDTO)).thenReturn(pageVO); AjaxResult result = controller.getAbnormalAccountPeople(queryDTO); verify(overviewService).getAbnormalAccountPeople(same(queryDTO)); assertSame(pageVO, result.get("data")); ``` - [ ] **Step 3: 运行控制器定向测试确认失败点正确** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest \ test ``` Expected: - FAIL,提示缺少 `getAbnormalAccountPeople` 方法、DTO 或服务接口方法 - [ ] **Step 4: 最小化补齐控制器与接口骨架** 1. 创建 `CcdiProjectAbnormalAccountQueryDTO` 2. 创建 `CcdiProjectAbnormalAccountPageVO` 3. 在 `ICcdiProjectOverviewService` 增加: ```java default CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople( CcdiProjectAbnormalAccountQueryDTO queryDTO ) { return new CcdiProjectAbnormalAccountPageVO(); } ``` 4. 在控制器中增加: ```java @GetMapping("/abnormal-account-people") @Operation(summary = "查询异常账户人员信息") @PreAuthorize("@ss.hasPermi('ccdi:project:query')") public AjaxResult getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) { CcdiProjectAbnormalAccountPageVO pageVO = overviewService.getAbnormalAccountPeople(queryDTO); return AjaxResult.success(pageVO); } ``` - [ ] **Step 5: 重新运行控制器测试** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest \ test ``` Expected: - PASS - [ ] **Step 6: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java git commit -m "补充异常账户人员查询接口契约" ``` ## Task 2: 补齐异常账户分页与导出 SQL 口径 **Files:** - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java` - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java` - [ ] **Step 1: 先写 Mapper SQL 测试** 在 `CcdiProjectOverviewMapperSqlTest` 中新增两个 select 断言: ```java String abnormalPageSql = extractSelect(xml, "selectAbnormalAccountPage"); String abnormalExportSql = extractSelect(xml, "selectAbnormalAccountList"); assertTrue(abnormalPageSql.contains("tr.model_code = 'ABNORMAL_ACCOUNT'"), abnormalPageSql); assertTrue(abnormalPageSql.contains("tr.bank_statement_id is null"), abnormalPageSql); assertTrue(abnormalPageSql.contains("account.owner_type = 'EMPLOYEE'"), abnormalPageSql); assertTrue(abnormalPageSql.contains("tr.reason_detail"), abnormalPageSql); assertTrue(abnormalExportSql.contains("order by abnormal_time desc"), abnormalExportSql); ``` 同时补充对状态映射和规则时间字段的静态断言: - `when account.status = 1 then '正常'` - `when account.status = 2 then '已销户'` - `when tr.rule_code = 'SUDDEN_ACCOUNT_CLOSURE'` - `when tr.rule_code = 'DORMANT_ACCOUNT_LARGE_ACTIVATION'` - [ ] **Step 2: 运行 SQL 测试确认失败** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewMapperSqlTest \ test ``` Expected: - FAIL,提示缺少 `selectAbnormalAccountPage` 或 `selectAbnormalAccountList` - [ ] **Step 3: 设计基础 SQL 片段,再补 Mapper 方法签名** 在 `CcdiProjectOverviewMapper.java` 中新增: ```java Page selectAbnormalAccountPage( Page page, @Param("query") CcdiProjectAbnormalAccountQueryDTO query ); List selectAbnormalAccountList(@Param("projectId") Long projectId); ``` 在 XML 中先抽出基础 SQL 片段: - `abnormalAccountBaseSql` - 统一负责: - 项目过滤 - `ABNORMAL_ACCOUNT` 模型过滤 - 对象型结果过滤 - `owner_type = 'EMPLOYEE'` - 账号唯一关联 - `账号 / 开户人 / 银行 / 异常类型 / 异常发生时间 / 状态` 映射 - [ ] **Step 4: 实现分页 SQL 与导出 SQL** 分页 SQL: - 使用 `#{query.projectId}` - 按 `abnormal_time desc, account.account_no asc, tr.rule_code asc` 导出 SQL: - 使用 `#{projectId}` - 与分页 SQL 保持同一列集合与同一排序规则 账号唯一关联要求: - 优先通过 `tr.reason_detail` 中包含的账号匹配 `account.account_no` - 没有账号匹配条件时不要把员工名下全部账户笛卡尔展开 - [ ] **Step 5: 重新运行 SQL 测试** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewMapperSqlTest \ test ``` Expected: - PASS - [ ] **Step 6: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java \ ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java git commit -m "补充异常账户人员查询SQL" ``` ## Task 3: 完成服务层分页映射与项目校验 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` - Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java` - [ ] **Step 1: 先写服务层失败测试** 在 `CcdiProjectOverviewServiceAbnormalAccountTest` 中新增 4 个测试: 1. 分页查询返回 `rows/total` 2. 分页查询默认页码为 `1`、分页大小为 `5` 3. 导出查询返回 `List` 4. 项目不存在时,分页与导出都抛 `ServiceException` 核心断言示例: ```java CcdiProjectAbnormalAccountPageVO result = service.getAbnormalAccountPeople(queryDTO); assertEquals(1, result.getRows().size()); assertEquals("突然销户", result.getRows().getFirst().getAbnormalType()); verify(overviewMapper).selectAbnormalAccountPage(any(Page.class), any(CcdiProjectAbnormalAccountQueryDTO.class)); ``` - [ ] **Step 2: 跑服务层测试确认失败** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest \ test ``` Expected: - FAIL,提示缺少服务方法、Mapper 调用或 Excel 映射 - [ ] **Step 3: 实现最小服务层逻辑** 在 `ICcdiProjectOverviewService` 中新增: ```java default List exportAbnormalAccountPeople(Long projectId) { return List.of(); } ``` 在 `CcdiProjectOverviewServiceImpl` 中实现: 1. `getAbnormalAccountPeople(queryDTO)` 2. `exportAbnormalAccountPeople(projectId)` 3. `buildAbnormalAccountExcelRow(...)` 实现要求: - 先 `ensureProjectExists(...)` - 分页默认值沿用现有结果总览风格 - 页面 VO 和 Excel 行对象字段完全同构 - [ ] **Step 4: 重新运行服务层测试** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest \ test ``` Expected: - PASS - [ ] **Step 5: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceAbnormalAccountTest.java git commit -m "补充异常账户人员服务映射" ``` ## Task 4: 将异常账户真实数据接入统一导出工作簿 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java` - [ ] **Step 1: 先改统一导出测试,让它要求第 3 个 sheet 有真实数据** 在 `CcdiProjectRiskDetailWorkbookExporterTest` 中把原先“只有表头”改成: ```java CcdiProjectAbnormalAccountExcel abnormalRow = new CcdiProjectAbnormalAccountExcel(); abnormalRow.setAccountNo("6222000000000001"); abnormalRow.setAccountName("李四"); abnormalRow.setBankName("中国农业银行"); abnormalRow.setAbnormalType("突然销户"); abnormalRow.setAbnormalTime("2026-03-20"); abnormalRow.setStatus("已销户"); ``` 断言: - sheet 名仍为 `异常账户人员信息` - 第 1 行写出真实数据 - 列顺序依次为: - `账号` - `开户人` - `银行` - `异常类型` - `异常发生时间` - `状态` - [ ] **Step 2: 再改服务层统一导出测试** 在 `CcdiProjectOverviewServiceImplTest.shouldExportRiskDetailsWorkbook` 中增加异常账户 stub: ```java when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem)); ``` 并把 `verify(workbookExporter).export(...)` 扩展为包含第 3 个参数列表断言。 - [ ] **Step 3: 运行导出相关测试确认失败** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest \ test ``` Expected: - FAIL,提示导出器方法签名或第 3 个 sheet 断言不匹配 - [ ] **Step 4: 最小化修改导出器与服务** 1. 将 `CcdiProjectRiskDetailWorkbookExporter.export(...)` 方法签名扩为接收异常账户列表 2. `writeAbnormalAccountSheet(...)` 从“只写表头”改成“表头 + 数据行” 3. `CcdiProjectOverviewServiceImpl.exportRiskDetails(...)` 中查询 `exportAbnormalAccountPeople(projectId)` 4. 调用导出器时一并传入异常账户列表 - [ ] **Step 5: 重新运行导出相关测试** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest \ test ``` Expected: - PASS - [ ] **Step 6: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java git commit -m "补充风险明细异常账户统一导出" ``` ## Task 5: 记录实施结果并做最终回归 **Files:** - Modify: `docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md` - [ ] **Step 1: 运行后端最终回归测试** Run: ```bash mvn -pl ccdi-project -am \ -Dsurefire.failIfNoSpecifiedTests=false \ -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceAbnormalAccountTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest \ test ``` Expected: - PASS - [ ] **Step 2: 如需手工联调,启动后端并验证后立即关闭** Run: ```bash mvn -pl ruoyi-admin -am package -DskipTests cd ruoyi-admin/target && java -jar ruoyi-admin.jar ``` 至少验证: 1. `GET /ccdi/project/overview/abnormal-account-people` 可返回 `rows/total` 2. `POST /ccdi/project/overview/risk-details/export` 第 3 个 sheet 含真实异常账户数据 验证结束后必须关闭 `java -jar ruoyi-admin.jar` 进程。 - [ ] **Step 3: 编写后端实施记录** 在实施记录中写清: - 新增接口路径 - 新增 DTO/VO/Excel 对象 - Mapper SQL 口径 - 统一导出第 3 个 sheet 的真实化改动 - 自动化测试命令与结果 - 如有手工联调,记录启动与关闭进程情况 - [ ] **Step 4: 提交本任务** ```bash git add docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md git commit -m "记录异常账户人员信息后端实施" ``` ## Final Verification - [ ] `GET /ccdi/project/overview/abnormal-account-people` 返回字段完整:`accountNo/accountName/bankName/abnormalType/abnormalTime/status` - [ ] 页面查询与导出查询都只取 `ABNORMAL_ACCOUNT` 对象型结果 - [ ] 第 3 个 sheet 不再是空白模板 - [ ] 同一账号命中多条规则时保留多行 - [ ] 如启动了后端进程,验证结束后已手动关闭