Files
ccdi/docs/plans/backend/2026-03-24-credit-info-direct-storage-backend-implementation.md

393 lines
13 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.
# Credit Info Direct Storage 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.
**Goal:** 将征信维护后端从“员工征信口径”调整为“征信对象口径”,上传时不再校验员工或亲属归属,解析成功后按 `person_id` 直接覆盖入库,并让列表查询直接基于征信表聚合。
**Architecture:** 保留现有 `CcdiCreditInfoController -> ICcdiCreditInfoService -> CcdiCreditInfoServiceImpl -> CcdiCreditInfoQueryMapper.xml` 链路,不新增表、不改接口路径。核心改造点是删除上传链路中的员工存在性校验,收敛查询 DTO/VO 到征信对象字段,并将列表 SQL 从 `ccdi_base_staff` 驱动改成“征信业务表驱动”的按证件号聚合查询。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, JUnit 5, Mockito, Maven, MySQL
---
## 文件结构与职责
**修改文件**
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java`
删除员工口径查询字段,仅保留征信对象筛选条件。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java`
删除 `staffId``deptName` 等员工字段,只保留征信对象摘要字段。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java`
删除 `CcdiBaseStaffMapper` 依赖与 `ensureStaffExists` 校验,保留最新日期与覆盖写入逻辑。
- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml`
列表与摘要查询改为直接从征信业务表按 `person_id` 聚合,不再 `JOIN ccdi_base_staff`
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java`
锁定“非员工征信对象也可上传成功”和“旧日期仍失败”行为。
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapperXmlTest.java`
锁定列表 SQL 已去掉 `ccdi_base_staff``staffId``maintained` 依赖。
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java`
保持控制层分页委派契约,确认 DTO 精简后接口仍可调用。
- `docs/reports/implementation/2026-03-24-credit-info-direct-storage-backend-record.md`
后端实施记录。
**参考文件**
- `docs/design/2026-03-24-credit-info-direct-storage-design.md`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java`
## Task 1: 锁定“直接入库”后端契约
**Files:**
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapperXmlTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java`
- [ ] **Step 1: 先把服务测试改成征信对象口径**
`CcdiCreditInfoServiceImplTest` 新增或改写用例,明确非员工证件号也能成功上传:
```java
@Test
void uploadHtmlFiles_shouldStoreCreditObjectWithoutStaffBinding() {
MockMultipartFile file = new MockMultipartFile(
"files", "family.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(List.of(buildDebt("330101199202020022")));
when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(buildNegative("330101199202020022"));
CreditInfoUploadResultVO result = service.upload(List.of(file));
assertEquals(1, result.getSuccessCount());
assertEquals(0, result.getFailureCount());
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
}
```
- [ ] **Step 2: 删除测试里对员工表 Mock 的依赖**
清理以下旧逻辑:
```java
@Mock
private CcdiBaseStaffMapper baseStaffMapper;
when(baseStaffMapper.selectOne(any())).thenReturn(baseStaff(...));
```
并同步移除不再使用的 `CcdiBaseStaff``CcdiBaseStaffMapper``any()` 导入。
- [ ] **Step 3: 改写 SQL 契约测试**
`CcdiCreditInfoQueryMapperXmlTest` 改成下面这组断言:
```java
assertFalse(source.contains("FROM ccdi_base_staff"),
"征信维护列表不应再从员工表驱动查询");
assertFalse(source.contains("query.staffId"),
"征信维护列表不应再接收柜员号筛选");
assertFalse(source.contains("query.maintained"),
"征信维护列表不应再接收是否已维护筛选");
assertTrue(source.contains("person_id"),
"征信维护列表必须按征信对象证件号聚合");
```
- [ ] **Step 4: 运行失败测试确认改造点被覆盖**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest,CcdiCreditInfoQueryMapperXmlTest,CcdiCreditInfoControllerTest test
```
Expected:
- FAIL原因应集中在服务实现仍校验员工存在、查询 XML 仍依赖员工表、DTO/VO 仍残留旧字段。
- [ ] **Step 5: 提交测试契约**
```bash
git add ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapperXmlTest.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java
git commit -m "新增征信直接入库后端测试契约"
```
## Task 2: 收敛 DTO / VO 到征信对象字段
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java`
- [ ] **Step 1: 精简查询 DTO**
`CcdiCreditInfoQueryDTO` 改为只保留:
```java
@Data
public class CcdiCreditInfoQueryDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String name;
private String idCard;
}
```
- [ ] **Step 2: 精简列表 VO**
`CreditInfoListVO` 改为只保留:
```java
private String name;
private String idCard;
private Date queryDate;
private Long debtCount;
private BigDecimal debtTotalAmount;
private Integer civilCnt;
private Integer enforceCnt;
private Integer admCnt;
```
删除:
```java
private Long staffId;
private String deptName;
```
- [ ] **Step 3: 运行单测确认编译通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoControllerTest test
```
Expected:
- PASS控制层在 DTO 精简后仍能正常分页委派。
- [ ] **Step 4: 提交 DTO / VO 调整**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java \
ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java
git commit -m "精简征信维护后端查询字段"
```
## Task 3: 改造上传服务为“直接入库”
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java`
- Review: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java`
- [ ] **Step 1: 移除员工主数据依赖**
删除成员与导入:
```java
@Resource
private CcdiBaseStaffMapper baseStaffMapper;
```
以及:
```java
ensureStaffExists(personId);
```
- [ ] **Step 2: 删除 `ensureStaffExists` 方法**
从实现中彻底移除:
```java
private void ensureStaffExists(String personId) { ... }
```
保留对以下字段的校验:
```java
String personId = stringValue(header.get("query_cert_no"));
String personName = stringValue(header.get("query_cust_name"));
LocalDate queryDate = parseQueryDate(stringValue(header.get("report_time")));
```
- [ ] **Step 3: 保持最新日期覆盖逻辑**
保留并验证以下路径仍然成立:
```java
ensureLatestQueryDate(personId, queryDate);
replaceEmployeeCredit(personId, debts, negative, userName);
```
这里不用改方法名,优先走最短路径;只要行为变成“按征信对象本人证件号覆盖写入”即可。
- [ ] **Step 4: 运行服务测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
```
Expected:
- PASS非员工征信对象可以上传成功
- PASS旧日期上传仍然失败
- [ ] **Step 5: 提交服务实现调整**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java
git commit -m "调整征信维护后端直接入库逻辑"
```
## Task 4: 重写列表 SQL 为征信表聚合查询
**Files:**
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapperXmlTest.java`
- [ ] **Step 1: 设计征信对象主集合**
在 XML 中构造统一主集合,覆盖“只有负债”“只有负面信息”“两者都有”三种场景:
```sql
SELECT person_id
FROM ccdi_debts_info
GROUP BY person_id
UNION
SELECT person_id
FROM ccdi_credit_negative_info
GROUP BY person_id
```
- [ ] **Step 2: 改写分页查询 SQL**
参考结构:
```sql
SELECT
keys.person_id AS id_card,
COALESCE(debt_agg.person_name, neg.person_name) AS name,
COALESCE(debt_agg.query_date, neg.query_date) AS query_date,
IFNULL(debt_agg.debt_count, 0) AS debt_count,
IFNULL(debt_agg.debt_total_amount, 0) AS debt_total_amount,
IFNULL(neg.civil_cnt, 0) AS civil_cnt,
IFNULL(neg.enforce_cnt, 0) AS enforce_cnt,
IFNULL(neg.adm_cnt, 0) AS adm_cnt
FROM (
SELECT person_id FROM ccdi_debts_info GROUP BY person_id
UNION
SELECT person_id FROM ccdi_credit_negative_info GROUP BY person_id
) keys
LEFT JOIN (
SELECT
person_id,
MAX(person_name) AS person_name,
MAX(query_date) AS query_date,
COUNT(*) AS debt_count,
SUM(debt_total_amount) AS debt_total_amount
FROM ccdi_debts_info
GROUP BY person_id
) debt_agg ON debt_agg.person_id = keys.person_id
LEFT JOIN ccdi_credit_negative_info neg ON neg.person_id = keys.person_id
<where>
<if test="query != null and query.name != null and query.name != ''">
AND COALESCE(debt_agg.person_name, neg.person_name) LIKE CONCAT('%', #{query.name}, '%')
</if>
<if test="query != null and query.idCard != null and query.idCard != ''">
AND keys.person_id LIKE CONCAT('%', #{query.idCard}, '%')
</if>
</where>
ORDER BY COALESCE(debt_agg.query_date, neg.query_date) DESC, keys.person_id DESC
```
- [ ] **Step 3: 改写详情摘要查询**
`selectCreditInfoSummaryByPersonId` 同样改为直接从征信表聚合,不再依赖员工表。至少保证:
```sql
WHERE keys.person_id = #{personId}
LIMIT 1
```
- [ ] **Step 4: 运行 XML 契约测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoQueryMapperXmlTest test
```
Expected:
- PASSXML 不再出现 `ccdi_base_staff``query.staffId``query.maintained`
- [ ] **Step 5: 提交查询 SQL 改造**
```bash
git add ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml \
ccdi-info-collection/src/test/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapperXmlTest.java
git commit -m "调整征信维护后端聚合查询口径"
```
## Task 5: 全量回归与实施记录
**Files:**
- Create: `docs/reports/implementation/2026-03-24-credit-info-direct-storage-backend-record.md`
- [ ] **Step 1: 运行后端回归测试**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest,CcdiCreditInfoQueryMapperXmlTest,CcdiCreditInfoControllerTest test
```
Expected:
- PASS直接入库、聚合查询、控制层分页委派相关测试全部通过。
- [ ] **Step 2: 编写后端实施记录**
`docs/reports/implementation/2026-03-24-credit-info-direct-storage-backend-record.md` 中记录:
- 实施目标
- 实际修改文件
- 直接入库口径
- SQL 聚合调整
- 验证命令与结果
- 若测试期间启动本地服务,结束后已关闭进程
- [ ] **Step 3: 检查工作区仅包含本次改动**
Run:
```bash
git status --short
git diff --staged --name-only
```
Expected:
- 仅出现本任务相关的后端源码、测试和实施记录文件。
- [ ] **Step 4: 提交后端收尾**
```bash
git add docs/reports/implementation/2026-03-24-credit-info-direct-storage-backend-record.md
git commit -m "补充征信维护后端直接入库实施记录"
```