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

19 KiB
Raw Blame History

项目详情风险明细异常账户人员信息后端 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 中新增对新方法的反射断言:

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 中新增测试:

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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest \
  test

Expected:

  • FAIL提示缺少 getAbnormalAccountPeople 方法、DTO 或服务接口方法

  • Step 4: 最小化补齐控制器与接口骨架

  1. 创建 CcdiProjectAbnormalAccountQueryDTO
  2. 创建 CcdiProjectAbnormalAccountPageVO
  3. ICcdiProjectOverviewService 增加:
default CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople(
    CcdiProjectAbnormalAccountQueryDTO queryDTO
) {
    return new CcdiProjectAbnormalAccountPageVO();
}
  1. 在控制器中增加:
@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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest \
  test

Expected:

  • PASS

  • Step 6: 提交本任务

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 断言:

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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewMapperSqlTest \
  test

Expected:

  • FAIL提示缺少 selectAbnormalAccountPageselectAbnormalAccountList

  • Step 3: 设计基础 SQL 片段,再补 Mapper 方法签名

CcdiProjectOverviewMapper.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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewMapperSqlTest \
  test

Expected:

  • PASS

  • Step 6: 提交本任务

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

核心断言示例:

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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest \
  test

Expected:

  • FAIL提示缺少服务方法、Mapper 调用或 Excel 映射

  • Step 3: 实现最小服务层逻辑

ICcdiProjectOverviewService 中新增:

default List<CcdiProjectAbnormalAccountExcel> exportAbnormalAccountPeople(Long projectId) {
    return List.of();
}

CcdiProjectOverviewServiceImpl 中实现:

  1. getAbnormalAccountPeople(queryDTO)
  2. exportAbnormalAccountPeople(projectId)
  3. buildAbnormalAccountExcelRow(...)

实现要求:

  • ensureProjectExists(...)

  • 分页默认值沿用现有结果总览风格

  • 页面 VO 和 Excel 行对象字段完全同构

  • Step 4: 重新运行服务层测试

Run:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest \
  test

Expected:

  • PASS

  • Step 5: 提交本任务

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 中把原先“只有表头”改成:

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

when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem));

并把 verify(workbookExporter).export(...) 扩展为包含第 3 个参数列表断言。

  • Step 3: 运行导出相关测试确认失败

Run:

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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest \
  test

Expected:

  • PASS

  • Step 6: 提交本任务

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:

mvn -pl ccdi-project -am \
  -Dsurefire.failIfNoSpecifiedTests=false \
  -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceAbnormalAccountTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest \
  test

Expected:

  • PASS

  • Step 2: 如需手工联调,启动后端并验证后立即关闭

Run:

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: 提交本任务

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 不再是空白模板
  • 同一账号命中多条规则时保留多行
  • 如启动了后端进程,验证结束后已手动关闭