Files
ccdi/docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md
2026-03-23 22:39:15 +08:00

740 lines
26 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 Maintenance 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:**`ccdi-info-collection` 模块中新增征信维护后端能力,支持批量上传征信 HTML、调用现有 `ccdi-lsfx` 征信解析客户端、按员工维度覆盖写入 `ccdi_debts_info``ccdi_credit_negative_info`,并提供列表、详情、删除接口。
**Architecture:** 业务入口放在 `ccdi-info-collection`,由独立 `CcdiCreditInfoController` 暴露上传、列表、详情、删除接口。上传链路直接复用 `ccdi-lsfx``CreditParseClient`,通过 `query_cert_no` 归户到员工,并由独立 payload 装配器把 `lx_debt` 转成负债明细、把 `lx_publictype` 转成负面信息;最终按员工身份证号在单事务内先删后插,保证“只保留最新征信”。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, Jackson, JUnit 5, Mockito, Maven, MySQL
---
## 文件结构与职责
**新增文件**
- `sql/migration/2026-03-23-create-credit-info-tables.sql`
创建 `ccdi_debts_info``ccdi_credit_negative_info` 两张业务表。
- `sql/ccdi_credit_info_menu.sql`
在“信息维护”下新增“征信维护”菜单与按钮权限。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiDebtsInfo.java`
负债明细实体。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiCreditNegativeInfo.java`
负面信息实体。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java`
列表查询条件 DTO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java`
员工维度摘要列表 VO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java`
征信详情聚合 VO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java`
负面信息展示 VO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java`
批量上传结果 VO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java`
单文件失败结果 VO。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java`
负债表增删查 Mapper。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java`
负面信息表增删查 Mapper。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java`
员工维度摘要列表与详情聚合查询 Mapper。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java`
征信维护业务服务接口。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java`
征信维护业务服务实现。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java`
负责把 `CreditParseResponse` 装配成负债明细和负面信息对象。
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java`
征信维护上传、列表、详情、删除接口。
- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml`
聚合查询 SQL。
- `docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md`
后端实施记录。
**修改文件**
- `ccdi-info-collection/pom.xml`
显式引入 `ccdi-lsfx` 依赖,允许业务服务直接复用 `CreditParseClient`
**测试文件**
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java`
锁定 `lx_debt``lx_publictype` 的映射规则。
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java`
锁定批量上传、最新征信覆盖、单员工失败隔离与删除行为。
- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java`
锁定接口入参、分页、删除与返回结构。
**参考文件**
- `docs/design/2026-03-23-credit-info-maintenance-design.md`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/CreditParseClient.java`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParseResponse.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java`
- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java`
- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiBaseStaffMapper.xml`
## Task 1: 建表脚本与菜单脚本
**Files:**
- Create: `sql/migration/2026-03-23-create-credit-info-tables.sql`
- Create: `sql/ccdi_credit_info_menu.sql`
- Review: `assets/征信解析/ccdi_debts_info.xlsx`
- Review: `docs/design/2026-03-23-credit-info-maintenance-design.md`
- [ ] **Step 1: 先写脚本契约检查命令**
Run:
```bash
test -f sql/migration/2026-03-23-create-credit-info-tables.sql
```
Expected:
- FAIL因为建表脚本尚不存在。
- [ ] **Step 2: 编写两张业务表脚本**
`sql/migration/2026-03-23-create-credit-info-tables.sql` 中创建:
```sql
CREATE TABLE `ccdi_debts_info` (
`debt_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`person_id` VARCHAR(18) NOT NULL COMMENT '员工身份证号',
`person_name` VARCHAR(100) DEFAULT NULL COMMENT '员工姓名',
`query_date` DATE DEFAULT NULL COMMENT '征信查询日期',
`debt_main_type` VARCHAR(50) DEFAULT NULL COMMENT '负债大类',
`debt_sub_type` VARCHAR(50) DEFAULT NULL COMMENT '负债小类',
`creditor_type` VARCHAR(50) DEFAULT NULL COMMENT '债权人类型',
`debt_name` VARCHAR(100) DEFAULT NULL COMMENT '负债名称',
`principal_balance` DECIMAL(18,2) DEFAULT NULL COMMENT '负债本金余额',
`debt_total_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '负债总额',
`debt_status` VARCHAR(20) DEFAULT NULL COMMENT '负债状态',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`debt_id`),
KEY `idx_person_id` (`person_id`),
KEY `idx_query_date` (`query_date`),
KEY `idx_person_query_date` (`person_id`, `query_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工征信负债明细';
CREATE TABLE `ccdi_credit_negative_info` (
`negative_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`person_id` VARCHAR(18) NOT NULL COMMENT '员工身份证号',
`person_name` VARCHAR(100) DEFAULT NULL COMMENT '员工姓名',
`query_date` DATE DEFAULT NULL COMMENT '征信查询日期',
`civil_cnt` INT DEFAULT 0 COMMENT '民事案件笔数',
`enforce_cnt` INT DEFAULT 0 COMMENT '强制执行笔数',
`adm_cnt` INT DEFAULT 0 COMMENT '行政处罚笔数',
`civil_lmt` DECIMAL(18,2) DEFAULT 0 COMMENT '民事案件金额',
`enforce_lmt` DECIMAL(18,2) DEFAULT 0 COMMENT '强制执行金额',
`adm_lmt` DECIMAL(18,2) DEFAULT 0 COMMENT '行政处罚金额',
`create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`negative_id`),
UNIQUE KEY `uk_person_id` (`person_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工征信负面信息';
```
- [ ] **Step 3: 编写菜单与权限脚本**
`sql/ccdi_credit_info_menu.sql` 中新增:
```sql
SET @parent_menu_id = (SELECT menu_id FROM sys_menu WHERE menu_name='信息维护' AND parent_id=0 LIMIT 1);
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES ('征信维护', @parent_menu_id, 4, 'creditInfo', 'ccdiCreditInfo/index', 1, 0, 'C', '0', '0', 'ccdi:creditInfo:list', 'document', 'admin', NOW(), '', NULL, '员工征信维护菜单');
SET @menu_id = LAST_INSERT_ID();
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) VALUES
('征信查询', @menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'ccdi:creditInfo:query', '#', 'admin', NOW(), ''),
('征信上传', @menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'ccdi:creditInfo:upload', '#', 'admin', NOW(), ''),
('征信删除', @menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'ccdi:creditInfo:remove', '#', 'admin', NOW(), '');
```
- [ ] **Step 4: 检查脚本关键字**
Run:
```bash
rg -n "ccdi_debts_info|ccdi_credit_negative_info|征信维护|ccdi:creditInfo" sql/migration/2026-03-23-create-credit-info-tables.sql sql/ccdi_credit_info_menu.sql
```
Expected:
- PASS能看到两张表名、菜单名和权限前缀。
- [ ] **Step 5: 提交 SQL 脚本**
```bash
git add sql/migration/2026-03-23-create-credit-info-tables.sql sql/ccdi_credit_info_menu.sql
git commit -m "新增征信维护建表与菜单脚本"
```
## Task 2: 接入 `ccdi-lsfx` 依赖并建立对象骨架
**Files:**
- Modify: `ccdi-info-collection/pom.xml`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiDebtsInfo.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiCreditNegativeInfo.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java`
- [ ] **Step 1: 先写编译失败用例**
`ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java` 先写最小测试骨架,显式引用待创建的对象:
```java
class CcdiCreditInfoServiceImplTest {
@Test
void shouldCompileCreditInfoContracts() {
CcdiCreditInfoQueryDTO queryDTO = new CcdiCreditInfoQueryDTO();
CreditInfoUploadResultVO resultVO = new CreditInfoUploadResultVO();
assertNotNull(queryDTO);
assertNotNull(resultVO);
}
}
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
```
Expected:
- FAIL提示相关 DTO / VO 不存在,且 `ccdi-info-collection` 还不能解析 `CreditParseClient` 依赖。
- [ ] **Step 3: 增加模块依赖**
`ccdi-info-collection/pom.xml` 中追加:
```xml
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
<version>3.9.1</version>
</dependency>
```
- [ ] **Step 4: 建立实体与 VO 骨架**
实体最小结构示例:
```java
@Data
@TableName("ccdi_debts_info")
public class CcdiDebtsInfo {
@TableId(type = IdType.AUTO)
private Long debtId;
private String personId;
private String personName;
private Date queryDate;
private String debtMainType;
private String debtSubType;
private String creditorType;
private String debtName;
private BigDecimal principalBalance;
private BigDecimal debtTotalAmount;
private String debtStatus;
private String createBy;
private Date createTime;
private String updateBy;
private Date updateTime;
}
```
查询 DTO 最小结构:
```java
@Data
public class CcdiCreditInfoQueryDTO {
private String name;
private String staffId;
private String idCard;
private String maintained;
}
```
- [ ] **Step 5: 建立 Mapper 接口骨架**
至少补齐如下方法:
```java
List<CcdiDebtsInfo> selectByPersonId(String personId);
int deleteByPersonId(String personId);
int insertBatch(@Param("list") List<CcdiDebtsInfo> list);
CreditInfoListVO selectCreditInfoSummaryByPersonId(String personId);
```
- [ ] **Step 6: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
```
Expected:
- PASS合同对象已补齐模块可成功编译测试。
- [ ] **Step 7: 提交对象骨架**
```bash
git add ccdi-info-collection/pom.xml ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java
git commit -m "新增征信维护对象与依赖骨架"
```
## Task 3: 实现 payload 装配器
**Files:**
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java`
- [ ] **Step 1: 先写负债映射失败用例**
`CreditInfoPayloadAssemblerTest.java` 中先锁定 `lx_debt` 的 7 组映射:
```java
@Test
void shouldConvertDebtPayloadToSevenTypedRows() {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> debt = new HashMap<>();
debt.put("uncle_bank_house_bal", "50000");
debt.put("uncle_bank_house_lmt", "100000");
debt.put("uncle_bank_house_state", "正常");
debt.put("uncle_not_bank_bal", "2000");
debt.put("uncle_not_bank_lmt", "3000");
debt.put("uncle_not_bank_state", "逾期");
payload.setLxDebt(debt);
List<CcdiDebtsInfo> rows = assembler.buildDebts("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
assertEquals(2, rows.size());
assertEquals("住房贷款", rows.get(0).getDebtSubType());
assertEquals("非银", rows.get(1).getCreditorType());
}
```
- [ ] **Step 2: 再写负面信息失败用例**
```java
@Test
void shouldBuildNegativeInfoFromPublicTypePayload() {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> publictype = new HashMap<>();
publictype.put("civil_cnt", 2);
publictype.put("civil_lmt", "9800");
payload.setLxPublictype(publictype);
CcdiCreditNegativeInfo info = assembler.buildNegative("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
assertEquals(2, info.getCivilCnt());
assertEquals(new BigDecimal("9800"), info.getCivilLmt());
}
```
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CreditInfoPayloadAssemblerTest test
```
Expected:
- FAIL提示 `CreditInfoPayloadAssembler` 不存在。
- [ ] **Step 4: 编写最小装配实现**
示例结构:
```java
@Component
public class CreditInfoPayloadAssembler {
public List<CcdiDebtsInfo> buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
List<CcdiDebtsInfo> rows = new ArrayList<>();
rows.add(buildDebtRow("uncle_bank_house", "银行", "住房贷款", "银行", "未结清银行住房贷款", personId, personName, queryDate, payload.getLxDebt()));
// 其余 6 组同理
return rows.stream().filter(Objects::nonNull).toList();
}
public CcdiCreditNegativeInfo buildNegative(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
Map<String, Object> source = payload.getLxPublictype();
CcdiCreditNegativeInfo info = new CcdiCreditNegativeInfo();
info.setPersonId(personId);
info.setPersonName(personName);
info.setQueryDate(java.sql.Date.valueOf(queryDate));
info.setCivilCnt(toInteger(source.get("civil_cnt")));
return info;
}
}
```
- [ ] **Step 5: 补上 0 值过滤断言**
再追加测试:
```java
@Test
void shouldSkipDebtRowWhenAllMetricsAreEmpty() {
CreditParsePayload payload = new CreditParsePayload();
payload.setLxDebt(new HashMap<>());
assertTrue(assembler.buildDebts("3301", "张三", LocalDate.parse("2026-03-01"), payload).isEmpty());
}
```
- [ ] **Step 6: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CreditInfoPayloadAssemblerTest test
```
Expected:
- PASS
- [ ] **Step 7: 提交装配器**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java
git commit -m "新增征信解析结果装配器"
```
## Task 4: 实现批量上传与最新征信覆盖服务
**Files:**
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java`
- [ ] **Step 1: 先写批量上传失败用例**
`CcdiCreditInfoServiceImplTest.java` 中覆盖“一个员工失败不影响其他员工”:
```java
@Test
void uploadHtmlFiles_shouldIsolateFailuresPerEmployee() {
MockMultipartFile successFile = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
MockMultipartFile failFile = new MockMultipartFile("files", "b.html", "text/html", "<html>b</html>".getBytes(StandardCharsets.UTF_8));
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"))
.thenThrow(new RuntimeException("征信解析失败"));
CreditInfoUploadResultVO result = service.upload(Arrays.asList(successFile, failFile));
assertEquals(1, result.getSuccessCount());
assertEquals(1, result.getFailureCount());
assertEquals("征信解析失败", result.getFailures().get(0).getReason());
}
```
- [ ] **Step 2: 再写“旧日期拒绝覆盖”失败用例**
```java
@Test
void uploadHtmlFiles_shouldRejectOlderReportDate() {
when(queryMapper.selectLatestQueryDate("330101199001010011"))
.thenReturn(LocalDate.parse("2026-03-05"));
CreditInfoUploadResultVO result = service.upload(List.of(file));
assertEquals(0, result.getSuccessCount());
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
}
```
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
```
Expected:
- FAIL提示 `ICcdiCreditInfoService` / `CcdiCreditInfoServiceImpl` 不存在或上传方法未实现。
- [ ] **Step 4: 实现最小上传服务**
服务核心流程:
```java
@Transactional(rollbackFor = Exception.class)
public void replaceEmployeeCredit(String personId, List<CcdiDebtsInfo> debts, CcdiCreditNegativeInfo negative, String userName) {
debtsInfoMapper.deleteByPersonId(personId);
negativeInfoMapper.deleteByPersonId(personId);
if (!debts.isEmpty()) {
debts.forEach(item -> {
item.setCreateBy(userName);
item.setUpdateBy(userName);
});
debtsInfoMapper.insertBatch(debts);
}
negative.setCreateBy(userName);
negative.setUpdateBy(userName);
negativeInfoMapper.insert(negative);
}
```
批量上传方法要求:
- 遍历 `MultipartFile[]`
-`.html/.htm` 文件直接记失败
- 通过 `CreditParseClient.parse("LXCUSTALL", "PERSON", tempFile)` 解析
-`query_cert_no` 查询员工是否存在
-`report_time` 判断是否为最新
- 同一员工事务内执行覆盖写入
- 聚合成功数、失败数、失败清单
- [ ] **Step 5: 补充“删除旧数据再插新数据”的验证**
追加断言:
```java
verify(debtsInfoMapper).deleteByPersonId("330101199001010011");
verify(negativeInfoMapper).deleteByPersonId("330101199001010011");
verify(debtsInfoMapper).insertBatch(anyList());
verify(negativeInfoMapper).insert(any(CcdiCreditNegativeInfo.class));
```
- [ ] **Step 6: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
```
Expected:
- PASS
- [ ] **Step 7: 提交上传服务**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java 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 5: 实现列表聚合、详情与删除接口
**Files:**
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java`
- Create: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java`
- [ ] **Step 1: 先写控制器失败用例**
```java
@Test
void list_shouldDelegateWithPageRequest() {
when(service.selectCreditInfoPage(any(), any())).thenReturn(new Page<>(1, 10, 0));
TableDataInfo result = controller.list(new CcdiCreditInfoQueryDTO());
assertEquals(0L, result.getTotal());
}
@Test
void remove_shouldCallDeleteByPersonId() {
when(service.deleteByPersonId("330101199001010011")).thenReturn(1);
AjaxResult result = controller.remove("330101199001010011");
assertEquals(200, result.get("code"));
}
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoControllerTest test
```
Expected:
- FAIL提示 `CcdiCreditInfoController` 不存在。
- [ ] **Step 3: 编写最小聚合 SQL**
`CcdiCreditInfoQueryMapper.xml` 中实现员工维度列表聚合:
```xml
<select id="selectCreditInfoPage" resultType="com.ruoyi.info.collection.domain.vo.CreditInfoListVO">
SELECT
s.staff_id,
s.name,
s.id_card,
s.dept_name,
debtAgg.query_date,
IFNULL(debtAgg.debt_count, 0) AS debtCount,
IFNULL(debtAgg.debt_total_amount, 0) AS debtTotalAmount,
IFNULL(neg.civil_cnt, 0) AS civilCnt,
IFNULL(neg.enforce_cnt, 0) AS enforceCnt,
IFNULL(neg.adm_cnt, 0) AS admCnt
FROM ccdi_base_staff s
LEFT JOIN (
SELECT person_id, 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
) debtAgg ON debtAgg.person_id = s.id_card
LEFT JOIN ccdi_credit_negative_info neg ON neg.person_id = s.id_card
</select>
```
- [ ] **Step 4: 编写控制器**
接口定义:
```java
@PostMapping("/upload")
public AjaxResult upload(@RequestParam("files") MultipartFile[] files) { ... }
@GetMapping("/list")
public TableDataInfo list(CcdiCreditInfoQueryDTO queryDTO) { ... }
@GetMapping("/{personId}")
public AjaxResult detail(@PathVariable String personId) { ... }
@DeleteMapping("/{personId}")
public AjaxResult remove(@PathVariable String personId) { ... }
```
权限要求:
- `ccdi:creditInfo:list`
- `ccdi:creditInfo:query`
- `ccdi:creditInfo:upload`
- `ccdi:creditInfo:remove`
- [ ] **Step 5: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoControllerTest test
```
Expected:
- PASS
- [ ] **Step 6: 做一次模块回归**
Run:
```bash
mvn -pl ccdi-info-collection -am test -Dtest=CreditInfoPayloadAssemblerTest,CcdiCreditInfoServiceImplTest,CcdiCreditInfoControllerTest
mvn -pl ccdi-info-collection -am compile
```
Expected:
- PASS测试与编译均通过。
- [ ] **Step 7: 提交接口与聚合查询**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java
git commit -m "新增征信维护查询与接口"
```
## Task 6: 联调验证与实施记录
**Files:**
- Create: `docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md`
- [ ] **Step 1: 启动 Mock 服务**
Run:
```bash
cd lsfx-mock-server
python3 main.py
```
Expected:
- 本地监听 `http://localhost:8000`
- [ ] **Step 2: 启动后端并执行上传联调**
Run:
```bash
mvn -pl ruoyi-admin -am spring-boot:run
curl -F 'files=@assets/征信解析员工样本/0001_徐伟_2040.html;type=text/html' http://127.0.0.1:62318/ccdi/creditInfo/upload
```
Expected:
- 返回 `AjaxResult.code = 200`
- `data.successCount = 1`
- `data.failureCount = 0`
- [ ] **Step 3: 验证列表、详情、删除接口**
Run:
```bash
curl 'http://127.0.0.1:62318/ccdi/creditInfo/list?pageNum=1&pageSize=10'
curl 'http://127.0.0.1:62318/ccdi/creditInfo/320101199001010030'
curl -X DELETE 'http://127.0.0.1:62318/ccdi/creditInfo/320101199001010030'
```
Expected:
- 列表能返回员工维度摘要
- 详情能返回负债列表和负面信息
- 删除后再次查询详情应为空或提示未维护征信
- [ ] **Step 4: 记录实施结果**
在实施记录中至少写明:
- 实际修改文件
- 关键测试命令与结果
- 使用 `bin/mysql_utf8_exec.sh` 执行 SQL 的命令
- 启停的后端与 mock 进程及已停止说明
- [ ] **Step 5: 提交实施记录**
```bash
git add docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md
git commit -m "补充征信维护后端实施记录"
```
## Review Notes
- 仓库协作约定禁止开启 subagent因此本计划执行时使用 `superpowers:executing-plans`,不走子代理审阅。
- SQL 执行必须使用 `bin/mysql_utf8_exec.sh <sql-file>`,不要直接手写 `mysql -e`
- 联调完成后要主动关闭 `ruoyi-admin``lsfx-mock-server` 进程,避免残留端口。