# 开发风险明细涉疑交易明细后端 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:** 为结果总览页新增“涉疑交易明细”后端能力，支持按“全部可疑人员类型 / 名单库命中 / 模型规则命中”查询和导出，同时保证同一流水只展示一条。

**Architecture:** 以后端结果总览专用接口承载本功能，不污染现有通用流水明细查询接口。查询以 `ccdi_bank_statement` 为基表，分别聚合“规则名称包含可疑”的命中结果和“交易对手方命中中介库”的命中结果，再按 `bank_statement_id` 去重输出展示所需业务字段。由于数据库当前缺少交易对手方证件号与统一社会信用代码字段，先补齐表结构、LSFX 响应映射和本地入库链路，再落查询聚合。

**Tech Stack:** Java 21, Spring Boot 3, MyBatis XML, MyBatis Plus `Page`, Maven, JUnit 5, Mockito, MySQL 8, ExcelUtil

---

### Task 1: 补齐交易对手方身份字段入库链路

**Files:**
- Create: `sql/migration/2026-03-27-ccdi-bank-statement-counterparty-identity-columns.sql`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java`
- Modify: `lsfx-mock-server/models/response.py`
- Modify: `lsfx-mock-server/services/statement_service.py`
- Modify: `lsfx-mock-server/services/statement_rule_samples.py`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java`
- Test: `lsfx-mock-server/tests/test_statement_service.py`

- [ ] **Step 1: 先写失败测试，锁定新增字段名和映射行为**

```java
@Test
void testFromResponse_ShouldMapCounterpartyIdentityFields() {
    BankStatementItem item = new BankStatementItem();
    item.setCustomerCertNo("330101199001011234");
    item.setCustomerSocialCreditCode("91330100123456789X");

    CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);

    assertEquals("330101199001011234", entity.getCustomerCertNo());
    assertEquals("91330100123456789X", entity.getCustomerSocialCreditCode());
}
```

- [ ] **Step 2: 运行测试，确认当前实现确实缺字段**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiBankStatementMapperXmlTest test
```

Expected:

- `CcdiBankStatementTest` 编译失败或断言失败，提示缺少 `customerCertNo` / `customerSocialCreditCode`
- `CcdiBankStatementMapperXmlTest` 尚未覆盖新列

- [ ] **Step 3: 实现最小字段补齐**

```sql
ALTER TABLE `ccdi_bank_statement`
    ADD COLUMN `customer_cert_no` varchar(50) NULL COMMENT '交易对手方证件号' AFTER `customer_reference`,
    ADD COLUMN `customer_social_credit_code` varchar(50) NULL COMMENT '交易对手方统一社会信用代码' AFTER `customer_cert_no`;
```

```java
private String customerCertNo;
private String customerSocialCreditCode;

entity.setCustomerCertNo(item.getCustomerCertNo());
entity.setCustomerSocialCreditCode(item.getCustomerSocialCreditCode());
```

```xml
<result property="customerCertNo" column="customer_cert_no" />
<result property="customerSocialCreditCode" column="customer_social_credit_code" />
...
customer_bank, customer_reference, customer_cert_no, customer_social_credit_code,
...
#{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode},
```

- [ ] **Step 4: 同步本地 LSFX mock 契约，保证联调可生成这两个字段**

Run:

```bash
pytest lsfx-mock-server/tests/test_statement_service.py -v
```

Expected:

- mock 返回结构包含 `customerCertNo`
- mock 返回结构包含 `customerSocialCreditCode`

- [ ] **Step 5: 提交本任务**

```bash
git add sql/migration/2026-03-27-ccdi-bank-statement-counterparty-identity-columns.sql \
  ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java \
  ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml \
  ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java \
  lsfx-mock-server/models/response.py \
  lsfx-mock-server/services/statement_service.py \
  lsfx-mock-server/services/statement_rule_samples.py \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java \
  lsfx-mock-server/tests/test_statement_service.py
git commit -m "补齐涉疑交易明细对手方身份字段"
```

### Task 2: 建立结果总览涉疑交易接口契约

**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionPageVO.java`
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectSuspiciousTransactionExcel.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/service/CcdiProjectOverviewServiceStructureTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java`

- [ ] **Step 1: 先加结构测试，锁定新 DTO、接口方法和控制器路由**

```java
assertNotNull(clazz.getMethod(
    "getSuspiciousTransactions",
    Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
));
```

```java
assertEquals("/suspicious-transactions", getMapping.value()[0]);
assertEquals(List.of("projectId", "suspiciousType", "pageNum", "pageSize"), fieldNames);
```

- [ ] **Step 2: 运行控制器与结构测试，确认当前缺少接口**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest test
```

Expected:

- 结构测试报 `NoSuchMethodException`
- 控制器契约测试提示缺少 `/suspicious-transactions`

- [ ] **Step 3: 写最小 DTO / VO / Controller / Service 签名**

```java
public class CcdiProjectSuspiciousTransactionQueryDTO {
    private Long projectId;
    private String suspiciousType;
    private Integer pageNum;
    private Integer pageSize;
}
```

```java
@GetMapping("/suspicious-transactions")
public AjaxResult getSuspiciousTransactions(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
    return AjaxResult.success(overviewService.getSuspiciousTransactions(queryDTO));
}
```

- [ ] **Step 4: 增加导出接口契约**

```java
@PostMapping("/suspicious-transactions/export")
public void exportSuspiciousTransactions(HttpServletResponse response,
                                         CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
    ...
}
```

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest test
```

Expected: PASS

- [ ] **Step 5: 提交本任务**

```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java \
  ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java \
  ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionPageVO.java \
  ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectSuspiciousTransactionExcel.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/service/CcdiProjectOverviewServiceStructureTest.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 3: 落地涉疑交易聚合 SQL 与服务逻辑

**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`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java`
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java`

- [ ] **Step 1: 先写 SQL 结构测试和服务测试，锁定去重与筛选口径**

```java
assertTrue(suspiciousSql.contains("rule_name like '%可疑%'"));
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"));
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"));
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"));
```

```java
assertEquals(1, result.getRows().size());
assertTrue(result.getRows().getFirst().getHasModelRuleHit());
assertTrue(result.getRows().getFirst().getHasNameListHit());
```

- [ ] **Step 2: 运行测试，确认当前尚无聚合查询实现**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceSuspiciousTransactionTest test
```

Expected:

- SQL 测试找不到 `selectSuspiciousTransactionPage`
- 服务测试编译失败或返回空实现

- [ ] **Step 3: 在 Mapper XML 中实现“三层聚合 + 按流水去重”**

```xml
<select id="selectSuspiciousTransactionPage" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
  SELECT merged.bankStatementId,
         MAX(merged.trxDate) AS trxDate,
         MAX(merged.suspiciousPersonName) AS suspiciousPersonName,
         MAX(merged.relatedPersonName) AS relatedPersonName,
         MAX(merged.relatedStaffName) AS relatedStaffName,
         MAX(merged.relatedStaffCode) AS relatedStaffCode,
         MAX(merged.relationType) AS relationType,
         MAX(merged.userMemo) AS userMemo,
         MAX(merged.cashType) AS cashType,
         MAX(merged.displayAmount) AS displayAmount,
         MAX(merged.hasModelRuleHit) AS hasModelRuleHit,
         MAX(merged.hasNameListHit) AS hasNameListHit
  FROM (...)
  GROUP BY merged.bankStatementId
</select>
```

- [ ] **Step 4: 在 Service 中补查询类型标准化、项目存在性校验和分页返回**

```java
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
    queryDTO.setSuspiciousType("ALL");
}
queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
```

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceSuspiciousTransactionTest,CcdiProjectOverviewServiceImplTest test
```

Expected: PASS

- [ ] **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/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java
git commit -m "实现结果总览涉疑交易聚合查询"
```

### Task 4: 接通导出与回归验证

**Files:**
- 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/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java`

- [ ] **Step 1: 先补导出控制器测试**

```java
Method method = CcdiProjectOverviewController.class.getMethod(
    "exportSuspiciousTransactions",
    HttpServletResponse.class,
    CcdiProjectSuspiciousTransactionQueryDTO.class
);
assertEquals("/suspicious-transactions/export", postMapping.value()[0]);
```

- [ ] **Step 2: 运行导出相关测试，确认导出接口尚未闭环**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest,CcdiProjectOverviewControllerContractTest test
```

Expected: FAIL，提示导出接口缺失或未调用服务

- [ ] **Step 3: 实现导出列表查询与 Excel 输出**

```java
List<CcdiProjectSuspiciousTransactionExcel> rows = overviewService.exportSuspiciousTransactions(queryDTO);
ExcelUtil<CcdiProjectSuspiciousTransactionExcel> util = new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class);
util.exportExcel(response, rows, "涉疑交易明细");
```

- [ ] **Step 4: 运行后端回归测试**

Run:

```bash
mvn -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiBankStatementMapperXmlTest,CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewServiceSuspiciousTransactionTest test
```

Expected: PASS

- [ ] **Step 5: 提交本任务**

```bash
git add 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/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java \
  ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java
git commit -m "接通结果总览涉疑交易导出能力"
```
