From 2a9ddf33732c71292c31dde99d3cdc37f88be7ef Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Mon, 23 Mar 2026 22:39:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BE=81=E4=BF=A1=E7=BB=B4?= =?UTF-8?q?=E6=8A=A4=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...info-maintenance-backend-implementation.md | 739 ++++++++++++++++++ ...nfo-maintenance-frontend-implementation.md | 546 +++++++++++++ ...-23-credit-info-maintenance-plan-record.md | 25 + 3 files changed, 1310 insertions(+) create mode 100644 docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md create mode 100644 docs/plans/frontend/2026-03-23-credit-info-maintenance-frontend-implementation.md create mode 100644 docs/reports/implementation/2026-03-23-credit-info-maintenance-plan-record.md diff --git a/docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md b/docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md new file mode 100644 index 00000000..25542fbd --- /dev/null +++ b/docs/plans/backend/2026-03-23-credit-info-maintenance-backend-implementation.md @@ -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 + + com.ruoyi + ccdi-lsfx + 3.9.1 + +``` + +- [ ] **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 selectByPersonId(String personId); +int deleteByPersonId(String personId); +int insertBatch(@Param("list") List 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 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 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 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 buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) { + List 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 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", "a".getBytes(StandardCharsets.UTF_8)); + MockMultipartFile failFile = new MockMultipartFile("files", "b.html", "text/html", "b".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 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 + +``` + +- [ ] **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 `,不要直接手写 `mysql -e`。 +- 联调完成后要主动关闭 `ruoyi-admin` 与 `lsfx-mock-server` 进程,避免残留端口。 diff --git a/docs/plans/frontend/2026-03-23-credit-info-maintenance-frontend-implementation.md b/docs/plans/frontend/2026-03-23-credit-info-maintenance-frontend-implementation.md new file mode 100644 index 00000000..7c45e176 --- /dev/null +++ b/docs/plans/frontend/2026-03-23-credit-info-maintenance-frontend-implementation.md @@ -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 + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- [ ] **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 + + + 选择文件 +
仅支持 .html / .htm 文件,可一次选择多个文件
+
+
+``` + +文件校验: + +```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 + +
征信摘要
+ + 征信查询日期:{{ detailForm.queryDate }} + 负债笔数:{{ detailForm.debtCount }} + 负债总额:{{ detailForm.debtTotalAmount }} + + +
负债信息
+ + + + + + + + + +
+``` + +- [ ] **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`。 diff --git a/docs/reports/implementation/2026-03-23-credit-info-maintenance-plan-record.md b/docs/reports/implementation/2026-03-23-credit-info-maintenance-plan-record.md new file mode 100644 index 00000000..cad6cbde --- /dev/null +++ b/docs/reports/implementation/2026-03-23-credit-info-maintenance-plan-record.md @@ -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 进程