Files
ccdi/docs/plans/backend/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation-plan.md

524 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目详情风险明细异常账户人员信息后端 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<CcdiProjectAbnormalAccountItemVO> selectAbnormalAccountPage(
Page<CcdiProjectAbnormalAccountItemVO> page,
@Param("query") CcdiProjectAbnormalAccountQueryDTO query
);
List<CcdiProjectAbnormalAccountItemVO> 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<CcdiProjectAbnormalAccountExcel>`
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<CcdiProjectAbnormalAccountExcel> 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 不再是空白模板
- [ ] 同一账号命中多条规则时保留多行
- [ ] 如启动了后端进程,验证结束后已手动关闭