# Project Detail Risk People Export 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. > > **Repo note:** 本仓库 `AGENTS.md` 明确禁止开启 subagent,执行本计划时请在当前会话使用 `superpowers:executing-plans`。 **Goal:** 为项目详情“结果总览 -> 风险总览”人员列表补齐后端 Excel 导出能力,导出当前项目全部风险人员,并保证导出口径与页面表格字段一致。 **Architecture:** 后端继续沿用 `CcdiProjectOverviewController -> ICcdiProjectOverviewService -> CcdiProjectOverviewServiceImpl -> CcdiProjectOverviewMapper.xml` 这条结果总览链路,不新增平行模块或补丁接口。分页查询与导出查询共用同一组风险人员列映射与排序口径,服务层继续复用 `buildRiskPeopleItem(...)` 统一组装页面字段,再映射为导出 Excel 对象,避免页面与导出出现两套业务口径。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, ExcelUtil, JUnit 5, Mockito, Maven, Swagger/OpenAPI --- ### Task 1: 建立风险人员导出契约 **Files:** - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectRiskPeopleOverviewExcel.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java` - [ ] **Step 1: 先写失败测试,锁定导出接口路径、Service 新签名和 Excel 对象列** ```java Method method = CcdiProjectOverviewController.class.getMethod( "exportRiskPeople", HttpServletResponse.class, Long.class ); assertEquals("/risk-people/export", method.getAnnotation(PostMapping.class).value()[0]); ``` ```java assertNotNull(ICcdiProjectOverviewService.class.getMethod("exportRiskPeopleOverview", Long.class)); ``` ```java assertNotNull(CcdiProjectRiskPeopleOverviewExcel.class.getDeclaredField("name")); assertNotNull(CcdiProjectRiskPeopleOverviewExcel.class.getDeclaredField("riskPoint")); ``` - [ ] **Step 2: 运行控制器结构测试,确认当前仓库还没有风险人员导出入口** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest test ``` Expected: - FAIL,提示 `exportRiskPeople` 或 `exportRiskPeopleOverview` 尚不存在 - [ ] **Step 3: 写最小导出契约** ```java @PostMapping("/risk-people/export") @Operation(summary = "导出风险人员总览") @PreAuthorize("@ss.hasPermi('ccdi:project:query')") public void exportRiskPeople(HttpServletResponse response, Long projectId) { List rows = overviewService.exportRiskPeopleOverview(projectId); ExcelUtil util = new ExcelUtil<>(CcdiProjectRiskPeopleOverviewExcel.class); util.exportExcel(response, rows, "风险人员总览"); } ``` ```java default List exportRiskPeopleOverview(Long projectId) { return List.of(); } ``` ```java @Excel(name = "姓名") private String name; ``` - [ ] **Step 4: 回跑控制器结构测试,确认导出外壳建立完成** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest test ``` Expected: - PASS - 控制器测试能断言 `/risk-people/export` - 权限仍为 `ccdi:project:query` - [ ] **Step 5: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectRiskPeopleOverviewExcel.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java \ ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java git commit -m "补充风险总览人员导出接口契约" ``` ### Task 2: 让分页查询与导出查询共用同一套风险人员口径 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java` - [ ] **Step 1: 先写失败测试,锁定导出查询、排序和共用 SQL 片段** ```java String xml = readMapperXml(); assertTrue(xml.contains(""), xml); assertTrue(xml.contains(""), xml); assertTrue(xml.contains("selectRiskPeopleOverviewPage"), xml); assertTrue(xml.contains("selectRiskPeopleOverviewList"), xml); assertTrue(xml.contains("order by risk_level_sort asc, result.model_count desc, result.rule_count desc, result.staff_id_card asc"), xml); ``` - [ ] **Step 2: 运行 SQL 测试,确认当前 Mapper 还没有导出列表查询** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest test ``` Expected: - FAIL,提示缺少 `selectRiskPeopleOverviewList` - FAIL,提示未抽出共用 SQL 片段 - [ ] **Step 3: 写最小共用查询结构** ```java List selectRiskPeopleOverviewList(@Param("projectId") Long projectId); ``` ```xml result.staff_id_card, result.staff_name, result.dept_id, result.dept_name, result.rule_count, result.model_count, result.hit_count, null as top_rule_code, null as top_rule_name, result.risk_point, result.risk_level_code, case when result.risk_level_code = 'HIGH' then '高风险' when result.risk_level_code = 'MEDIUM' then '中风险' else '低风险' end as risk_level_name, case when result.risk_level_code = 'HIGH' then 1 when result.risk_level_code = 'MEDIUM' then 2 else 3 end as risk_level_sort ``` ```xml order by risk_level_sort asc, result.model_count desc, result.rule_count desc, result.staff_id_card asc ``` - [ ] **Step 4: 回跑 SQL 测试,确认分页与导出将使用同一套排序和列映射** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest test ``` Expected: - PASS - `selectRiskPeopleOverviewPage` 与 `selectRiskPeopleOverviewList` 共用列与排序片段 - [ ] **Step 5: 提交本任务** ```bash git add 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 "统一风险总览人员查询与导出口径" ``` ### Task 3: 实现服务层导出映射并复用现有字段组装逻辑 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java` - [ ] **Step 1: 先写失败测试,锁定“导出全部人员”和字段映射** ```java when(projectMapper.selectById(40L)).thenReturn(project); when(overviewMapper.selectRiskPeopleOverviewList(40L)).thenReturn(List.of(aggregate)); when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of( buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"), buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM") )); List rows = service.exportRiskPeopleOverview(40L); assertEquals(1, rows.size()); assertEquals("李四", rows.getFirst().getName()); assertEquals("高风险", rows.getFirst().getRiskLevel()); assertEquals("大额单笔收入、疑似兼职", rows.getFirst().getRiskPoint()); verify(overviewMapper).selectRiskPeopleOverviewList(40L); ``` - [ ] **Step 2: 运行 Service 测试,确认当前实现还没有导出映射** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest test ``` Expected: - FAIL,提示 `exportRiskPeopleOverview` 尚未实现 - [ ] **Step 3: 写最小服务实现** ```java @Override public List exportRiskPeopleOverview(Long projectId) { ensureProjectExists(projectId); return defaultList(overviewMapper.selectRiskPeopleOverviewList(projectId)).stream() .map(aggregate -> buildRiskPeopleItem(projectId, aggregate)) .map(this::buildRiskPeopleExcelRow) .toList(); } ``` ```java private CcdiProjectRiskPeopleOverviewExcel buildRiskPeopleExcelRow(CcdiProjectRiskPeopleOverviewItemVO item) { CcdiProjectRiskPeopleOverviewExcel row = new CcdiProjectRiskPeopleOverviewExcel(); row.setName(item.getName()); row.setIdNo(item.getIdNo()); row.setDepartment(item.getDepartment()); row.setRiskCount(item.getRiskCount()); row.setRiskLevel(item.getRiskLevel()); row.setModelCount(item.getModelCount()); row.setRiskPoint(item.getRiskPoint()); return row; } ``` - [ ] **Step 4: 回跑 Service 测试,确认导出复用了页面口径** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest test ``` Expected: - PASS - 测试明确断言导出走 `selectRiskPeopleOverviewList` - 导出字段与页面表格字段一致 - 项目不存在时继续抛 `ServiceException` - [ ] **Step 5: 提交本任务** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java \ ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java git commit -m "实现风险总览人员列表后端导出" ``` ### Task 4: 补齐导出控制器回归与文档留痕 **Files:** - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java` - Create: `docs/reports/implementation/2026-03-30-project-detail-risk-people-export-backend-record.md` - Create: `docs/tests/records/2026-03-30-project-detail-risk-people-export-backend-verification.md` - Verify: `docs/design/2026-03-30-project-detail-risk-people-export-design.md` - [ ] **Step 1: 补一条控制器导出回归测试** ```java MockHttpServletResponse response = new MockHttpServletResponse(); when(overviewService.exportRiskPeopleOverview(40L)).thenReturn(List.of(new CcdiProjectRiskPeopleOverviewExcel())); controller.exportRiskPeople(response, 40L); verify(overviewService).exportRiskPeopleOverview(40L); ``` - [ ] **Step 2: 跑本次后端最小回归集** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest test ``` Expected: - PASS - [ ] **Step 3: 记录测试结果** 在 `docs/tests/records/2026-03-30-project-detail-risk-people-export-backend-verification.md` 记录: - 执行命令 - 通过情况 - 导出接口路径 - 字段口径与页面一致的核对结论 - [ ] **Step 4: 记录实施内容** 在 `docs/reports/implementation/2026-03-30-project-detail-risk-people-export-backend-record.md` 记录: - 新增导出接口与 Excel 对象 - 复用 `buildRiskPeopleItem(...)` 的实现方式 - 分页查询与导出查询共用排序/字段 SQL 片段 - 未引入新的筛选、菜单或权限 - [ ] **Step 5: 提交本任务** ```bash git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java \ docs/reports/implementation/2026-03-30-project-detail-risk-people-export-backend-record.md \ docs/tests/records/2026-03-30-project-detail-risk-people-export-backend-verification.md git commit -m "补充风险总览人员导出后端记录" ``` ### 最终验证 - [ ] 运行后端回归: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest test ``` - [ ] 确认以下结果: - `POST /ccdi/project/overview/risk-people/export` 已接通 - 导出范围为当前项目全部风险人员 - 导出列仅包含页面展示列,不包含操作列 - 导出字段顺序与设计文档一致 - [ ] 准备执行时,只暂存本次任务相关后端文件后再提交