740 lines
26 KiB
Markdown
740 lines
26 KiB
Markdown
# 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` 进程,避免残留端口。
|