# 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` 进程，避免残留端口。
