新增征信维护实施计划
This commit is contained in:
@@ -0,0 +1,739 @@
|
|||||||
|
# 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` 进程,避免残留端口。
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
# Credit Info Maintenance Frontend 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:** 在 `ruoyi-ui` 中新增“征信维护”页面,支持批量上传征信 HTML、展示员工维度征信摘要列表、查看详情并删除员工当前征信数据。
|
||||||
|
|
||||||
|
**Architecture:** 复用现有若依列表页模式和 Element UI 弹窗交互,新页面独立放在 `ccdiCreditInfo/index.vue`,不改造现有员工维护页。页面通过单独 API 文件调用后端业务接口,顶部负责文件上传和过滤查询,中部列表展示员工聚合摘要,详情弹窗展示负债明细与负面信息,删除操作按员工维度执行。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 2, Element UI, Axios request 封装, Node.js 源码契约测试, npm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构与职责
|
||||||
|
|
||||||
|
**新增文件**
|
||||||
|
|
||||||
|
- `ruoyi-ui/src/api/ccdiCreditInfo.js`
|
||||||
|
封装上传、列表、详情、删除接口。
|
||||||
|
- `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||||
|
征信维护页面。
|
||||||
|
- `ruoyi-ui/tests/unit/credit-info-api-contract.test.js`
|
||||||
|
锁定 API 文件与路径契约。
|
||||||
|
- `ruoyi-ui/tests/unit/credit-info-page-layout.test.js`
|
||||||
|
锁定页面主结构、查询区、列表列和操作列。
|
||||||
|
- `ruoyi-ui/tests/unit/credit-info-upload-ui.test.js`
|
||||||
|
锁定批量上传弹窗、上传结果汇总和失败清单展示。
|
||||||
|
- `ruoyi-ui/tests/unit/credit-info-detail-ui.test.js`
|
||||||
|
锁定详情弹窗和删除交互结构。
|
||||||
|
- `docs/reports/implementation/2026-03-23-credit-info-maintenance-frontend-implementation.md`
|
||||||
|
前端实施记录。
|
||||||
|
|
||||||
|
**修改文件**
|
||||||
|
|
||||||
|
- 无强制修改路由文件;菜单由后端 `sys_menu` 动态下发。
|
||||||
|
|
||||||
|
**参考文件**
|
||||||
|
|
||||||
|
- `docs/design/2026-03-23-credit-info-maintenance-design.md`
|
||||||
|
- `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
|
||||||
|
- `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
|
||||||
|
- `ruoyi-ui/src/api/ccdiBaseStaff.js`
|
||||||
|
- `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js`
|
||||||
|
|
||||||
|
## Task 1: 新增前端 API 封装
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ruoyi-ui/src/api/ccdiCreditInfo.js`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/credit-info-api-contract.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写失败校验脚本**
|
||||||
|
|
||||||
|
在 `ruoyi-ui/tests/unit/credit-info-api-contract.test.js` 中先断言 API 文件与关键函数不存在前应失败:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const apiPath = path.resolve(__dirname, "../../src/api/ccdiCreditInfo.js");
|
||||||
|
|
||||||
|
assert(fs.existsSync(apiPath), "未找到征信维护 API 文件 ccdiCreditInfo.js");
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行校验确认失败**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-api-contract.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- FAIL,因为 `ccdiCreditInfo.js` 尚不存在。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编写最小 API 文件**
|
||||||
|
|
||||||
|
在 `ruoyi-ui/src/api/ccdiCreditInfo.js` 中实现:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function uploadCreditHtml(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/creditInfo/upload',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listCreditInfo(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/creditInfo/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreditInfoDetail(personId) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/creditInfo/' + personId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCreditInfo(personId) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/creditInfo/' + personId,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 补全契约断言**
|
||||||
|
|
||||||
|
脚本里至少断言以下 token:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
[
|
||||||
|
"export function uploadCreditHtml(data)",
|
||||||
|
"export function listCreditInfo(query)",
|
||||||
|
"export function getCreditInfoDetail(personId)",
|
||||||
|
"export function deleteCreditInfo(personId)",
|
||||||
|
"/ccdi/creditInfo/upload",
|
||||||
|
"/ccdi/creditInfo/list",
|
||||||
|
"/ccdi/creditInfo/",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `征信维护 API 缺少关键契约: ${token}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行校验确认通过**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-api-contract.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交 API 封装**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/api/ccdiCreditInfo.js ruoyi-ui/tests/unit/credit-info-api-contract.test.js
|
||||||
|
git commit -m "新增征信维护前端接口封装"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 2: 搭建征信维护页面骨架
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/credit-info-page-layout.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写页面结构失败校验**
|
||||||
|
|
||||||
|
在 `credit-info-page-layout.test.js` 中断言页面必须出现:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
[
|
||||||
|
"征信维护",
|
||||||
|
"批量上传征信HTML",
|
||||||
|
"最近征信查询日期",
|
||||||
|
"负债笔数",
|
||||||
|
"负债总额",
|
||||||
|
"民事案件笔数",
|
||||||
|
"强制执行笔数",
|
||||||
|
"行政处罚笔数",
|
||||||
|
"详情",
|
||||||
|
"删除",
|
||||||
|
"@/api/ccdiCreditInfo",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `征信维护页面缺少关键结构: ${token}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行校验确认失败**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-page-layout.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- FAIL,因为页面文件尚不存在。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编写最小页面骨架**
|
||||||
|
|
||||||
|
页面至少包含:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||||
|
<el-form-item label="姓名">
|
||||||
|
<el-input v-model="queryParams.name" placeholder="请输入姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="柜员号">
|
||||||
|
<el-input v-model="queryParams.staffId" placeholder="请输入柜员号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="身份证号">
|
||||||
|
<el-input v-model="queryParams.idCard" placeholder="请输入身份证号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否已维护">
|
||||||
|
<el-select v-model="queryParams.maintained" clearable>
|
||||||
|
<el-option label="已维护" value="1" />
|
||||||
|
<el-option label="未维护" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-table :data="creditInfoList" v-loading="loading">
|
||||||
|
<el-table-column label="姓名" prop="name" />
|
||||||
|
<el-table-column label="柜员号" prop="staffId" />
|
||||||
|
<el-table-column label="身份证号" prop="idCard" />
|
||||||
|
<el-table-column label="最近征信查询日期" prop="queryDate" />
|
||||||
|
</el-table>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 补齐查询与列表基础方法**
|
||||||
|
|
||||||
|
至少实现:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
creditInfoList: [],
|
||||||
|
queryParams: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
staffId: undefined,
|
||||||
|
idCard: undefined,
|
||||||
|
maintained: undefined,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {},
|
||||||
|
handleQuery() {},
|
||||||
|
resetQuery() {},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行校验确认通过**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-page-layout.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交页面骨架**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiCreditInfo/index.vue ruoyi-ui/tests/unit/credit-info-page-layout.test.js
|
||||||
|
git commit -m "新增征信维护页面骨架"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 3: 实现批量上传弹窗与结果展示
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/credit-info-upload-ui.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写上传交互失败校验**
|
||||||
|
|
||||||
|
在 `credit-info-upload-ui.test.js` 中断言页面必须包含:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
[
|
||||||
|
"uploadDialogVisible",
|
||||||
|
"uploadFileList",
|
||||||
|
"批量上传征信HTML",
|
||||||
|
"仅支持 .html / .htm 文件",
|
||||||
|
"上传结果",
|
||||||
|
"failureList",
|
||||||
|
"successCount",
|
||||||
|
"failureCount",
|
||||||
|
"handleUploadSubmit",
|
||||||
|
"beforeUpload",
|
||||||
|
"uploadCreditHtml",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `征信上传交互缺少关键结构: ${token}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行校验确认失败**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-upload-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- FAIL,因为上传弹窗和状态尚未实现。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 加入上传弹窗与文件校验**
|
||||||
|
|
||||||
|
最小结构:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-dialog title="批量上传征信HTML" :visible.sync="uploadDialogVisible" width="600px">
|
||||||
|
<el-upload
|
||||||
|
ref="upload"
|
||||||
|
action=""
|
||||||
|
:auto-upload="false"
|
||||||
|
:file-list="uploadFileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
multiple>
|
||||||
|
<el-button type="primary" size="small">选择文件</el-button>
|
||||||
|
<div slot="tip" class="el-upload__tip">仅支持 .html / .htm 文件,可一次选择多个文件</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
文件校验:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
beforeUpload(file) {
|
||||||
|
const fileName = file.name.toLowerCase()
|
||||||
|
const passed = fileName.endsWith('.html') || fileName.endsWith('.htm')
|
||||||
|
if (!passed) {
|
||||||
|
this.$modal.msgError('仅支持上传 .html 或 .htm 文件')
|
||||||
|
}
|
||||||
|
return passed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 展示上传结果汇总**
|
||||||
|
|
||||||
|
上传完成后,将接口返回赋值到:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
this.uploadResult = {
|
||||||
|
totalCount: data.totalCount,
|
||||||
|
successCount: data.successCount,
|
||||||
|
failureCount: data.failureCount,
|
||||||
|
failures: data.failures || [],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
并在弹窗或结果卡片中展示:
|
||||||
|
|
||||||
|
- 总文件数
|
||||||
|
- 成功数
|
||||||
|
- 失败数
|
||||||
|
- 失败文件名 / 身份证号 / 原因
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行校验确认通过**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-upload-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交上传交互**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiCreditInfo/index.vue ruoyi-ui/tests/unit/credit-info-upload-ui.test.js
|
||||||
|
git commit -m "新增征信维护上传交互"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 4: 实现详情弹窗与删除交互
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiCreditInfo/index.vue`
|
||||||
|
- Create: `ruoyi-ui/tests/unit/credit-info-detail-ui.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 先写详情与删除失败校验**
|
||||||
|
|
||||||
|
在 `credit-info-detail-ui.test.js` 中断言页面包含:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
[
|
||||||
|
"detailDialogVisible",
|
||||||
|
"detailForm",
|
||||||
|
"负债信息",
|
||||||
|
"负面信息",
|
||||||
|
"civilCnt",
|
||||||
|
"enforceCnt",
|
||||||
|
"admCnt",
|
||||||
|
"handleDetail",
|
||||||
|
"handleDelete",
|
||||||
|
"deleteCreditInfo",
|
||||||
|
"确认删除该员工当前已维护的征信信息吗?",
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `详情或删除交互缺少关键结构: ${token}`);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行校验确认失败**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-detail-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- FAIL,因为详情弹窗和删除逻辑尚未实现。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 编写详情弹窗**
|
||||||
|
|
||||||
|
详情区至少展示:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-dialog title="征信详情" :visible.sync="detailDialogVisible" width="900px">
|
||||||
|
<div class="section-header">征信摘要</div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="8">征信查询日期:{{ detailForm.queryDate }}</el-col>
|
||||||
|
<el-col :span="8">负债笔数:{{ detailForm.debtCount }}</el-col>
|
||||||
|
<el-col :span="8">负债总额:{{ detailForm.debtTotalAmount }}</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div class="section-header">负债信息</div>
|
||||||
|
<el-table :data="detailForm.debts || []">
|
||||||
|
<el-table-column label="负债大类" prop="debtMainType" />
|
||||||
|
<el-table-column label="负债小类" prop="debtSubType" />
|
||||||
|
<el-table-column label="债权人类型" prop="creditorType" />
|
||||||
|
<el-table-column label="负债名称" prop="debtName" />
|
||||||
|
<el-table-column label="负债本金余额" prop="principalBalance" />
|
||||||
|
<el-table-column label="负债总额" prop="debtTotalAmount" />
|
||||||
|
<el-table-column label="负债状态" prop="debtStatus" />
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 编写删除交互**
|
||||||
|
|
||||||
|
删除方法最小实现:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleDelete(row) {
|
||||||
|
const personId = row.idCard
|
||||||
|
this.$modal.confirm('确认删除该员工当前已维护的征信信息吗?').then(() => {
|
||||||
|
return deleteCreditInfo(personId)
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: 运行校验确认通过**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-detail-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- PASS
|
||||||
|
|
||||||
|
- [ ] **Step 6: 提交详情与删除**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiCreditInfo/index.vue ruoyi-ui/tests/unit/credit-info-detail-ui.test.js
|
||||||
|
git commit -m "新增征信维护详情与删除交互"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 5: 前端回归验证与实施记录
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/reports/implementation/2026-03-23-credit-info-maintenance-frontend-implementation.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 跑源码契约测试**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-api-contract.test.js
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-page-layout.test.js
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-upload-ui.test.js
|
||||||
|
node ruoyi-ui/tests/unit/credit-info-detail-ui.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- 四个脚本全部 PASS。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 执行前端构建**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ruoyi-ui
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- PASS,构建产物生成成功。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 页面联调检查**
|
||||||
|
|
||||||
|
人工检查以下行为:
|
||||||
|
|
||||||
|
- 菜单进入 `征信维护` 页面正常
|
||||||
|
- 上传多个 HTML 后结果汇总正确
|
||||||
|
- 列表摘要字段与后端返回一致
|
||||||
|
- 详情弹窗能显示负债表与负面信息
|
||||||
|
- 删除后列表数据即时刷新
|
||||||
|
|
||||||
|
- [ ] **Step 4: 记录实施结果**
|
||||||
|
|
||||||
|
在实施记录中至少写明:
|
||||||
|
|
||||||
|
- 实际修改文件
|
||||||
|
- 前端构建命令与结果
|
||||||
|
- 页面联调截图或关键行为说明
|
||||||
|
- 若本地启动了前端 dev server,记录已关闭
|
||||||
|
|
||||||
|
- [ ] **Step 5: 提交实施记录**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/reports/implementation/2026-03-23-credit-info-maintenance-frontend-implementation.md
|
||||||
|
git commit -m "补充征信维护前端实施记录"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Notes
|
||||||
|
|
||||||
|
- 菜单由后端 `sys_menu` 下发,前端不需要手改 `router/index.js`。
|
||||||
|
- 页面只负责征信维护,不要顺手把入口加回员工维护页。
|
||||||
|
- 仓库协作约定禁止开启 subagent,执行本计划时直接使用 `superpowers:executing-plans`。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-03-23 征信维护实施计划产出记录
|
||||||
|
|
||||||
|
## 本次产出
|
||||||
|
|
||||||
|
- 后端实施计划:`docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md`
|
||||||
|
- 前端实施计划:`docs/plans/frontend/2026-03-23-credit-info-maintenance-frontend-implementation.md`
|
||||||
|
|
||||||
|
## 计划范围
|
||||||
|
|
||||||
|
- 后端:建表、菜单、模块依赖、解析结果装配、上传落库、员工维度聚合列表、详情与删除接口
|
||||||
|
- 前端:独立征信维护页面、批量上传弹窗、员工摘要列表、详情弹窗、删除交互
|
||||||
|
|
||||||
|
## 关键实现约束
|
||||||
|
|
||||||
|
- 不新增征信主表
|
||||||
|
- 负债表固定为 `ccdi_debts_info`
|
||||||
|
- 每个员工只保留最新征信
|
||||||
|
- 上传按员工维度原子化处理
|
||||||
|
- 菜单挂在“信息维护”下
|
||||||
|
|
||||||
|
## 执行说明
|
||||||
|
|
||||||
|
- 仓库约定不启用 subagent,后续执行计划时应直接使用 `executing-plans`
|
||||||
|
- SQL 执行需使用 `bin/mysql_utf8_exec.sh`
|
||||||
|
- 测试或联调结束后要关闭启动的前后端与 mock 进程
|
||||||
Reference in New Issue
Block a user