diff --git a/ccdi-info-collection/pom.xml b/ccdi-info-collection/pom.xml
index 34473cc6..8158c290 100644
--- a/ccdi-info-collection/pom.xml
+++ b/ccdi-info-collection/pom.xml
@@ -29,6 +29,12 @@
ruoyi-system
+
+ com.ruoyi
+ ccdi-lsfx
+ 3.9.1
+
+
com.alibaba
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java
new file mode 100644
index 00000000..4d447ffd
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiCreditInfoController.java
@@ -0,0 +1,72 @@
+package com.ruoyi.info.collection.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.PageDomain;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.core.page.TableSupport;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
+import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Arrays;
+
+/**
+ * 征信维护 Controller
+ */
+@Tag(name = "征信维护")
+@RestController
+@RequestMapping("/ccdi/creditInfo")
+public class CcdiCreditInfoController extends BaseController {
+
+ @Resource
+ private ICcdiCreditInfoService creditInfoService;
+
+ @Operation(summary = "上传征信 HTML")
+ @PreAuthorize("@ss.hasPermi('ccdi:creditInfo:upload')")
+ @Log(title = "征信维护", businessType = BusinessType.IMPORT)
+ @PostMapping("/upload")
+ public AjaxResult upload(@RequestParam("files") MultipartFile[] files) {
+ return AjaxResult.success("上传成功", creditInfoService.upload(Arrays.asList(files)));
+ }
+
+ @Operation(summary = "查询征信维护列表")
+ @PreAuthorize("@ss.hasPermi('ccdi:creditInfo:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(CcdiCreditInfoQueryDTO queryDTO) {
+ PageDomain pageDomain = TableSupport.buildPageRequest();
+ Page page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
+ Page result = creditInfoService.selectCreditInfoPage(page, queryDTO);
+ return getDataTable(result.getRecords(), result.getTotal());
+ }
+
+ @Operation(summary = "查询征信维护详情")
+ @PreAuthorize("@ss.hasPermi('ccdi:creditInfo:query')")
+ @GetMapping("/{personId}")
+ public AjaxResult detail(@PathVariable String personId) {
+ return success(creditInfoService.selectDetailByPersonId(personId));
+ }
+
+ @Operation(summary = "删除征信维护数据")
+ @PreAuthorize("@ss.hasPermi('ccdi:creditInfo:remove')")
+ @Log(title = "征信维护", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{personId}")
+ public AjaxResult remove(@PathVariable String personId) {
+ return toAjax(creditInfoService.deleteByPersonId(personId));
+ }
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiCreditNegativeInfo.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiCreditNegativeInfo.java
new file mode 100644
index 00000000..c490fd28
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiCreditNegativeInfo.java
@@ -0,0 +1,39 @@
+package com.ruoyi.info.collection.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 员工征信负面信息对象 ccdi_credit_negative_info
+ */
+@Data
+@TableName("ccdi_credit_negative_info")
+public class CcdiCreditNegativeInfo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long negativeId;
+
+ private String personId;
+ private String personName;
+ private Date queryDate;
+ private Integer civilCnt;
+ private Integer enforceCnt;
+ private Integer admCnt;
+ private BigDecimal civilLmt;
+ private BigDecimal enforceLmt;
+ private BigDecimal admLmt;
+ private String createBy;
+ private Date createTime;
+ private String updateBy;
+ private Date updateTime;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiDebtsInfo.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiDebtsInfo.java
new file mode 100644
index 00000000..eff8bd56
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiDebtsInfo.java
@@ -0,0 +1,40 @@
+package com.ruoyi.info.collection.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 员工征信负债明细对象 ccdi_debts_info
+ */
+@Data
+@TableName("ccdi_debts_info")
+public class CcdiDebtsInfo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @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;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java
new file mode 100644
index 00000000..3554cb93
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiCreditInfoQueryDTO.java
@@ -0,0 +1,21 @@
+package com.ruoyi.info.collection.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 征信维护查询 DTO
+ */
+@Data
+public class CcdiCreditInfoQueryDTO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String name;
+ private String staffId;
+ private String idCard;
+ private String maintained;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java
new file mode 100644
index 00000000..6983eb09
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java
@@ -0,0 +1,24 @@
+package com.ruoyi.info.collection.domain.vo;
+
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 征信详情聚合 VO
+ */
+@Data
+public class CreditInfoDetailVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String personId;
+ private String personName;
+ private String idCard;
+ private CreditInfoNegativeVO negativeInfo;
+ private List debtList;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java
new file mode 100644
index 00000000..8c7017e1
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java
@@ -0,0 +1,29 @@
+package com.ruoyi.info.collection.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 征信维护列表 VO
+ */
+@Data
+public class CreditInfoListVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Long staffId;
+ private String name;
+ private String idCard;
+ private String deptName;
+ private Date queryDate;
+ private Long debtCount;
+ private BigDecimal debtTotalAmount;
+ private Integer civilCnt;
+ private Integer enforceCnt;
+ private Integer admCnt;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java
new file mode 100644
index 00000000..aca1233b
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java
@@ -0,0 +1,28 @@
+package com.ruoyi.info.collection.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 征信负面信息展示 VO
+ */
+@Data
+public class CreditInfoNegativeVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String personId;
+ private String personName;
+ private Date queryDate;
+ private Integer civilCnt;
+ private Integer enforceCnt;
+ private Integer admCnt;
+ private BigDecimal civilLmt;
+ private BigDecimal enforceLmt;
+ private BigDecimal admLmt;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java
new file mode 100644
index 00000000..7085fd11
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java
@@ -0,0 +1,21 @@
+package com.ruoyi.info.collection.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 征信上传失败结果 VO
+ */
+@Data
+public class CreditInfoUploadFailureVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private String fileName;
+ private String personId;
+ private String personName;
+ private String reason;
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java
new file mode 100644
index 00000000..8920214f
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java
@@ -0,0 +1,23 @@
+package com.ruoyi.info.collection.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 征信上传结果 VO
+ */
+@Data
+public class CreditInfoUploadResultVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private Integer totalCount;
+ private Integer successCount;
+ private Integer failureCount;
+ private List failures = new ArrayList<>();
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java
new file mode 100644
index 00000000..aa610a98
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java
@@ -0,0 +1,27 @@
+package com.ruoyi.info.collection.mapper;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoDetailVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 征信维护聚合查询 Mapper
+ */
+public interface CcdiCreditInfoQueryMapper {
+
+ Page selectCreditInfoPage(@Param("page") Page page,
+ @Param("query") CcdiCreditInfoQueryDTO queryDTO);
+
+ CreditInfoListVO selectCreditInfoSummaryByPersonId(@Param("personId") String personId);
+
+ CreditInfoDetailVO selectCreditInfoDetailByPersonId(@Param("personId") String personId);
+
+ List selectCreditInfoList(@Param("query") CcdiCreditInfoQueryDTO queryDTO);
+
+ LocalDate selectLatestQueryDate(@Param("personId") String personId);
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java
new file mode 100644
index 00000000..ce399847
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java
@@ -0,0 +1,15 @@
+package com.ruoyi.info.collection.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 员工征信负面信息 Mapper
+ */
+public interface CcdiCreditNegativeInfoMapper extends BaseMapper {
+
+ CcdiCreditNegativeInfo selectByPersonId(@Param("personId") String personId);
+
+ int deleteByPersonId(@Param("personId") String personId);
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java
new file mode 100644
index 00000000..9ceaf8b0
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java
@@ -0,0 +1,19 @@
+package com.ruoyi.info.collection.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 员工征信负债明细 Mapper
+ */
+public interface CcdiDebtsInfoMapper extends BaseMapper {
+
+ List selectByPersonId(@Param("personId") String personId);
+
+ int deleteByPersonId(@Param("personId") String personId);
+
+ int insertBatch(@Param("list") List list);
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java
new file mode 100644
index 00000000..3a0f6d83
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiCreditInfoService.java
@@ -0,0 +1,28 @@
+package com.ruoyi.info.collection.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoDetailVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 征信维护服务接口
+ */
+public interface ICcdiCreditInfoService {
+
+ CreditInfoUploadResultVO upload(List files);
+
+ Page selectCreditInfoPage(Page page, CcdiCreditInfoQueryDTO queryDTO);
+
+ CreditInfoDetailVO selectDetailByPersonId(String personId);
+
+ int deleteByPersonId(String personId);
+
+ void replaceEmployeeCredit(String personId, List debts, CcdiCreditNegativeInfo negative, String userName);
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java
new file mode 100644
index 00000000..3aaaacba
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java
@@ -0,0 +1,291 @@
+package com.ruoyi.info.collection.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.info.collection.domain.CcdiBaseStaff;
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoDetailVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoNegativeVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoUploadFailureVO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
+import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
+import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
+import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
+import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
+import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
+import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
+import com.ruoyi.lsfx.client.CreditParseClient;
+import com.ruoyi.lsfx.domain.response.CreditParsePayload;
+import com.ruoyi.lsfx.domain.response.CreditParseResponse;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 征信维护服务实现
+ */
+@Service
+public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
+
+ @Resource
+ private CreditParseClient creditParseClient;
+
+ @Resource
+ private CreditInfoPayloadAssembler assembler;
+
+ @Resource
+ private CcdiBaseStaffMapper baseStaffMapper;
+
+ @Resource
+ private CcdiDebtsInfoMapper debtsInfoMapper;
+
+ @Resource
+ private CcdiCreditNegativeInfoMapper negativeInfoMapper;
+
+ @Resource
+ private CcdiCreditInfoQueryMapper queryMapper;
+
+ @Override
+ public CreditInfoUploadResultVO upload(List files) {
+ CreditInfoUploadResultVO result = new CreditInfoUploadResultVO();
+ List failures = new ArrayList<>();
+ int totalCount = files == null ? 0 : files.size();
+ int successCount = 0;
+ String userName = currentUserName();
+
+ if (files == null || files.isEmpty()) {
+ result.setTotalCount(0);
+ result.setSuccessCount(0);
+ result.setFailureCount(0);
+ result.setFailures(failures);
+ return result;
+ }
+
+ for (MultipartFile file : files) {
+ try {
+ validateHtmlFile(file);
+ handleSingleFile(file, userName);
+ successCount++;
+ } catch (Exception e) {
+ failures.add(buildFailure(file, null, null, e.getMessage()));
+ }
+ }
+
+ result.setTotalCount(totalCount);
+ result.setSuccessCount(successCount);
+ result.setFailureCount(failures.size());
+ result.setFailures(failures);
+ return result;
+ }
+
+ @Override
+ public Page selectCreditInfoPage(Page page, CcdiCreditInfoQueryDTO queryDTO) {
+ return queryMapper.selectCreditInfoPage(page, queryDTO);
+ }
+
+ @Override
+ public CreditInfoDetailVO selectDetailByPersonId(String personId) {
+ CreditInfoListVO summary = queryMapper.selectCreditInfoSummaryByPersonId(personId);
+ CcdiCreditNegativeInfo negative = negativeInfoMapper.selectByPersonId(personId);
+ List debts = debtsInfoMapper.selectByPersonId(personId);
+ if (summary == null && negative == null && (debts == null || debts.isEmpty())) {
+ return null;
+ }
+
+ CreditInfoDetailVO detail = new CreditInfoDetailVO();
+ detail.setPersonId(personId);
+ detail.setIdCard(summary != null && !isBlank(summary.getIdCard()) ? summary.getIdCard() : personId);
+ if (summary != null) {
+ detail.setPersonName(summary.getName());
+ }
+ if (isBlank(detail.getPersonName()) && negative != null) {
+ detail.setPersonName(negative.getPersonName());
+ }
+ if (isBlank(detail.getPersonName()) && debts != null && !debts.isEmpty()) {
+ detail.setPersonName(debts.get(0).getPersonName());
+ }
+ detail.setNegativeInfo(toNegativeVO(negative));
+ detail.setDebtList(debts == null ? List.of() : debts);
+ return detail;
+ }
+
+ @Override
+ public int deleteByPersonId(String personId) {
+ int debtCount = debtsInfoMapper.deleteByPersonId(personId);
+ int negativeCount = negativeInfoMapper.deleteByPersonId(personId);
+ return debtCount + negativeCount;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void replaceEmployeeCredit(String personId, List debts, CcdiCreditNegativeInfo negative, String userName) {
+ debtsInfoMapper.deleteByPersonId(personId);
+ negativeInfoMapper.deleteByPersonId(personId);
+ if (debts != null && !debts.isEmpty()) {
+ debts.forEach(item -> {
+ item.setCreateBy(userName);
+ item.setUpdateBy(userName);
+ });
+ debtsInfoMapper.insertBatch(debts);
+ }
+ if (negative != null) {
+ negative.setCreateBy(userName);
+ negative.setUpdateBy(userName);
+ negativeInfoMapper.insert(negative);
+ }
+ }
+
+ private void handleSingleFile(MultipartFile multipartFile, String userName) throws IOException {
+ File tempFile = createTempFile(multipartFile);
+ try {
+ CreditParseResponse response = creditParseClient.parse("LXCUSTALL", "PERSON", tempFile);
+ CreditParsePayload payload = requireResponse(response).getPayload();
+ Map header = requireHeader(payload);
+ String personId = stringValue(header.get("query_cert_no"));
+ String personName = stringValue(header.get("query_cust_name"));
+ LocalDate queryDate = parseQueryDate(stringValue(header.get("report_time")));
+ ensureStaffExists(personId);
+ ensureLatestQueryDate(personId, queryDate);
+
+ List debts = assembler.buildDebts(personId, personName, queryDate, payload);
+ CcdiCreditNegativeInfo negative = assembler.buildNegative(personId, personName, queryDate, payload);
+ replaceEmployeeCredit(personId, debts, negative, userName);
+ } finally {
+ if (tempFile.exists()) {
+ tempFile.delete();
+ }
+ }
+ }
+
+ private File createTempFile(MultipartFile multipartFile) throws IOException {
+ String originalFilename = multipartFile.getOriginalFilename();
+ String suffix = ".html";
+ if (originalFilename != null && originalFilename.contains(".")) {
+ suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));
+ }
+ File tempFile = File.createTempFile("credit-info-", suffix);
+ multipartFile.transferTo(tempFile);
+ return tempFile;
+ }
+
+ private void validateHtmlFile(MultipartFile file) {
+ String originalFilename = file == null ? null : file.getOriginalFilename();
+ if (originalFilename == null) {
+ throw new RuntimeException("上传文件不能为空");
+ }
+ String lowerName = originalFilename.toLowerCase();
+ if (!lowerName.endsWith(".html") && !lowerName.endsWith(".htm")) {
+ throw new RuntimeException("仅支持上传.html或.htm征信文件");
+ }
+ }
+
+ private CreditParseResponse requireResponse(CreditParseResponse response) {
+ if (response == null || response.getPayload() == null) {
+ throw new RuntimeException("征信解析结果为空");
+ }
+ if (!"0".equals(response.getStatusCode())) {
+ throw new RuntimeException(stringValue(response.getMessage(), "征信解析失败"));
+ }
+ return response;
+ }
+
+ private Map requireHeader(CreditParsePayload payload) {
+ Map header = payload.getLxHeader();
+ if (header == null || header.isEmpty()) {
+ throw new RuntimeException("征信解析结果缺少头信息");
+ }
+ return header;
+ }
+
+ private void ensureStaffExists(String personId) {
+ if (isBlank(personId)) {
+ throw new RuntimeException("征信解析结果缺少员工身份证号");
+ }
+ CcdiBaseStaff staff = baseStaffMapper.selectOne(new LambdaQueryWrapper()
+ .eq(CcdiBaseStaff::getIdCard, personId)
+ .last("LIMIT 1"));
+ if (staff == null) {
+ throw new RuntimeException("未找到对应员工信息");
+ }
+ }
+
+ private void ensureLatestQueryDate(String personId, LocalDate queryDate) {
+ LocalDate latestQueryDate = queryMapper.selectLatestQueryDate(personId);
+ if (latestQueryDate != null && queryDate.isBefore(latestQueryDate)) {
+ throw new RuntimeException("上传征信日期早于当前已维护最新记录");
+ }
+ }
+
+ private LocalDate parseQueryDate(String reportTime) {
+ if (isBlank(reportTime)) {
+ throw new RuntimeException("征信解析结果缺少征信查询日期");
+ }
+ String normalized = reportTime.trim();
+ if (normalized.length() > 10) {
+ normalized = normalized.substring(0, 10);
+ }
+ return LocalDate.parse(normalized);
+ }
+
+ private CreditInfoUploadFailureVO buildFailure(MultipartFile file, String personId, String personName, String reason) {
+ CreditInfoUploadFailureVO failure = new CreditInfoUploadFailureVO();
+ failure.setFileName(file == null ? null : file.getOriginalFilename());
+ failure.setPersonId(personId);
+ failure.setPersonName(personName);
+ failure.setReason(reason);
+ return failure;
+ }
+
+ private String stringValue(Object value) {
+ return stringValue(value, null);
+ }
+
+ private String stringValue(Object value, String defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ String text = value.toString().trim();
+ return text.isEmpty() ? defaultValue : text;
+ }
+
+ private boolean isBlank(String text) {
+ return text == null || text.trim().isEmpty();
+ }
+
+ private CreditInfoNegativeVO toNegativeVO(CcdiCreditNegativeInfo negative) {
+ if (negative == null) {
+ return null;
+ }
+ CreditInfoNegativeVO negativeVO = new CreditInfoNegativeVO();
+ negativeVO.setPersonId(negative.getPersonId());
+ negativeVO.setPersonName(negative.getPersonName());
+ negativeVO.setQueryDate(negative.getQueryDate());
+ negativeVO.setCivilCnt(negative.getCivilCnt());
+ negativeVO.setEnforceCnt(negative.getEnforceCnt());
+ negativeVO.setAdmCnt(negative.getAdmCnt());
+ negativeVO.setCivilLmt(negative.getCivilLmt());
+ negativeVO.setEnforceLmt(negative.getEnforceLmt());
+ negativeVO.setAdmLmt(negative.getAdmLmt());
+ return negativeVO;
+ }
+
+ private String currentUserName() {
+ try {
+ return SecurityUtils.getUsername();
+ } catch (Exception e) {
+ return "system";
+ }
+ }
+}
diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java
new file mode 100644
index 00000000..607c932a
--- /dev/null
+++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java
@@ -0,0 +1,124 @@
+package com.ruoyi.info.collection.service.support;
+
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import com.ruoyi.lsfx.domain.response.CreditParsePayload;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 征信解析结果装配器
+ */
+@Component
+public class CreditInfoPayloadAssembler {
+
+ private static final List DEBT_MAPPINGS = List.of(
+ new DebtMapping("uncle_bank_house", "银行", "住房贷款", "银行", "未结清银行住房贷款"),
+ new DebtMapping("uncle_bank_car", "银行", "汽车贷款", "银行", "未结清银行汽车贷款"),
+ new DebtMapping("uncle_bank_manage", "银行", "经营贷款", "银行", "未结清银行经营贷款"),
+ new DebtMapping("uncle_bank_consume", "银行", "消费贷款", "银行", "未结清银行消费贷款"),
+ new DebtMapping("uncle_bank_other", "银行", "其他贷款", "银行", "未结清银行其他贷款"),
+ new DebtMapping("uncle_not_bank", "非银", "非银行贷款", "非银", "未结清非银行贷款"),
+ new DebtMapping("uncle_credit_cart", "银行", "信用卡", "银行", "未结清信用卡")
+ );
+
+ public List buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
+ Map source = payload == null ? null : payload.getLxDebt();
+ if (source == null || source.isEmpty()) {
+ return List.of();
+ }
+ List rows = new ArrayList<>();
+ for (DebtMapping mapping : DEBT_MAPPINGS) {
+ CcdiDebtsInfo row = buildDebtRow(personId, personName, queryDate, source, mapping);
+ if (row != null) {
+ rows.add(row);
+ }
+ }
+ return rows;
+ }
+
+ public CcdiCreditNegativeInfo buildNegative(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
+ Map source = payload == null ? null : payload.getLxPublictype();
+ CcdiCreditNegativeInfo info = new CcdiCreditNegativeInfo();
+ info.setPersonId(personId);
+ info.setPersonName(personName);
+ info.setQueryDate(queryDate == null ? null : Date.valueOf(queryDate));
+ info.setCivilCnt(toInteger(source == null ? null : source.get("civil_cnt")));
+ info.setEnforceCnt(toInteger(source == null ? null : source.get("enforce_cnt")));
+ info.setAdmCnt(toInteger(source == null ? null : source.get("adm_cnt")));
+ info.setCivilLmt(toBigDecimal(source == null ? null : source.get("civil_lmt")));
+ info.setEnforceLmt(toBigDecimal(source == null ? null : source.get("enforce_lmt")));
+ info.setAdmLmt(toBigDecimal(source == null ? null : source.get("adm_lmt")));
+ return info;
+ }
+
+ private CcdiDebtsInfo buildDebtRow(String personId, String personName, LocalDate queryDate,
+ Map source, DebtMapping mapping) {
+ BigDecimal principalBalance = toBigDecimal(source.get(mapping.prefix() + "_bal"));
+ BigDecimal debtTotalAmount = toBigDecimal(source.get(mapping.prefix() + "_lmt"));
+ String debtStatus = toStringValue(source.get(mapping.prefix() + "_state"));
+ if (isEmptyMetrics(principalBalance, debtTotalAmount, debtStatus)) {
+ return null;
+ }
+
+ CcdiDebtsInfo row = new CcdiDebtsInfo();
+ row.setPersonId(personId);
+ row.setPersonName(personName);
+ row.setQueryDate(queryDate == null ? null : Date.valueOf(queryDate));
+ row.setDebtMainType(mapping.debtMainType());
+ row.setDebtSubType(mapping.debtSubType());
+ row.setCreditorType(mapping.creditorType());
+ row.setDebtName(mapping.debtName());
+ row.setPrincipalBalance(principalBalance);
+ row.setDebtTotalAmount(debtTotalAmount);
+ row.setDebtStatus(debtStatus);
+ return row;
+ }
+
+ private boolean isEmptyMetrics(BigDecimal principalBalance, BigDecimal debtTotalAmount, String debtStatus) {
+ boolean principalEmpty = principalBalance == null || BigDecimal.ZERO.compareTo(principalBalance) == 0;
+ boolean totalEmpty = debtTotalAmount == null || BigDecimal.ZERO.compareTo(debtTotalAmount) == 0;
+ return principalEmpty && totalEmpty && isBlank(debtStatus);
+ }
+
+ private Integer toInteger(Object value) {
+ BigDecimal decimal = toBigDecimal(value);
+ return decimal == null ? 0 : decimal.intValue();
+ }
+
+ private BigDecimal toBigDecimal(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof BigDecimal decimal) {
+ return decimal;
+ }
+ String text = Objects.toString(value, "").trim();
+ if (text.isEmpty()) {
+ return null;
+ }
+ return new BigDecimal(text);
+ }
+
+ private String toStringValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+ String text = Objects.toString(value, "").trim();
+ return text.isEmpty() ? null : text;
+ }
+
+ private boolean isBlank(String value) {
+ return value == null || value.trim().isEmpty();
+ }
+
+ private record DebtMapping(String prefix, String debtMainType, String debtSubType, String creditorType, String debtName) {
+ }
+}
diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml
new file mode 100644
index 00000000..a8eeb4e9
--- /dev/null
+++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditNegativeInfoMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditNegativeInfoMapper.xml
new file mode 100644
index 00000000..41d67334
--- /dev/null
+++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditNegativeInfoMapper.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DELETE FROM ccdi_credit_negative_info
+ WHERE person_id = #{personId}
+
+
+
diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiDebtsInfoMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiDebtsInfoMapper.xml
new file mode 100644
index 00000000..2d6ce569
--- /dev/null
+++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiDebtsInfoMapper.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DELETE FROM ccdi_debts_info
+ WHERE person_id = #{personId}
+
+
+
+ INSERT INTO ccdi_debts_info
+ (person_id, person_name, query_date, debt_main_type, debt_sub_type, creditor_type,
+ debt_name, principal_balance, debt_total_amount, debt_status, create_by, create_time, update_by, update_time)
+ VALUES
+
+ (#{item.personId}, #{item.personName}, #{item.queryDate}, #{item.debtMainType}, #{item.debtSubType}, #{item.creditorType},
+ #{item.debtName}, #{item.principalBalance}, #{item.debtTotalAmount}, #{item.debtStatus}, #{item.createBy}, NOW(), #{item.updateBy}, NOW())
+
+
+
+
diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java
new file mode 100644
index 00000000..89a11801
--- /dev/null
+++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiCreditInfoControllerTest.java
@@ -0,0 +1,53 @@
+package com.ruoyi.info.collection.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
+import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
+import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class CcdiCreditInfoControllerTest {
+
+ @InjectMocks
+ private CcdiCreditInfoController controller;
+
+ @Mock
+ private ICcdiCreditInfoService service;
+
+ @Test
+ void list_shouldDelegateWithPageRequest() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addParameter("pageNum", "1");
+ request.addParameter("pageSize", "10");
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
+ when(service.selectCreditInfoPage(any(), any())).thenReturn(new Page(1, 10, 0));
+
+ TableDataInfo result = controller.list(new CcdiCreditInfoQueryDTO());
+
+ assertEquals(0L, result.getTotal());
+ RequestContextHolder.resetRequestAttributes();
+ }
+
+ @Test
+ void remove_shouldCallDeleteByPersonId() {
+ when(service.deleteByPersonId("330101199001010011")).thenReturn(1);
+
+ AjaxResult result = controller.remove("330101199001010011");
+
+ assertEquals(200, result.get("code"));
+ }
+}
diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java
new file mode 100644
index 00000000..59cdb105
--- /dev/null
+++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiCreditInfoServiceImplTest.java
@@ -0,0 +1,138 @@
+package com.ruoyi.info.collection.service;
+
+import com.ruoyi.info.collection.domain.CcdiBaseStaff;
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
+import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
+import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
+import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
+import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
+import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
+import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
+import com.ruoyi.lsfx.client.CreditParseClient;
+import com.ruoyi.lsfx.domain.response.CreditParsePayload;
+import com.ruoyi.lsfx.domain.response.CreditParseResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockMultipartFile;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class CcdiCreditInfoServiceImplTest {
+
+ @InjectMocks
+ private CcdiCreditInfoServiceImpl service;
+
+ @Mock
+ private CreditParseClient creditParseClient;
+
+ @Mock
+ private CreditInfoPayloadAssembler assembler;
+
+ @Mock
+ private CcdiBaseStaffMapper baseStaffMapper;
+
+ @Mock
+ private CcdiDebtsInfoMapper debtsInfoMapper;
+
+ @Mock
+ private CcdiCreditNegativeInfoMapper negativeInfoMapper;
+
+ @Mock
+ private CcdiCreditInfoQueryMapper queryMapper;
+
+ @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("征信解析失败"));
+ when(baseStaffMapper.selectOne(any())).thenReturn(baseStaff("330101199001010011", "张三"));
+ when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
+ .thenReturn(List.of(buildDebt()));
+ when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
+ .thenReturn(buildNegative());
+
+ CreditInfoUploadResultVO result = service.upload(Arrays.asList(successFile, failFile));
+
+ assertEquals(1, result.getSuccessCount());
+ assertEquals(1, result.getFailureCount());
+ assertEquals("征信解析失败", result.getFailures().get(0).getReason());
+ verify(debtsInfoMapper).deleteByPersonId("330101199001010011");
+ verify(negativeInfoMapper).deleteByPersonId("330101199001010011");
+ verify(debtsInfoMapper).insertBatch(any());
+ verify(negativeInfoMapper).insert(any(CcdiCreditNegativeInfo.class));
+ }
+
+ @Test
+ void uploadHtmlFiles_shouldRejectOlderReportDate() {
+ MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "a".getBytes(StandardCharsets.UTF_8));
+
+ when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
+ .thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
+ when(baseStaffMapper.selectOne(any())).thenReturn(baseStaff("330101199001010011", "张三"));
+ 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());
+ }
+
+ private CreditParseResponse successResponse(String personId, String personName, String reportTime) {
+ CreditParsePayload payload = new CreditParsePayload();
+ Map header = new HashMap<>();
+ header.put("query_cert_no", personId);
+ header.put("query_cust_name", personName);
+ header.put("report_time", reportTime);
+ payload.setLxHeader(header);
+ payload.setLxDebt(Map.of("uncle_bank_house_bal", "1"));
+ payload.setLxPublictype(Map.of("civil_cnt", 1));
+
+ CreditParseResponse response = new CreditParseResponse();
+ response.setStatusCode("0");
+ response.setPayload(payload);
+ return response;
+ }
+
+ private CcdiBaseStaff baseStaff(String idCard, String name) {
+ CcdiBaseStaff staff = new CcdiBaseStaff();
+ staff.setIdCard(idCard);
+ staff.setName(name);
+ return staff;
+ }
+
+ private CcdiDebtsInfo buildDebt() {
+ CcdiDebtsInfo debt = new CcdiDebtsInfo();
+ debt.setPersonId("330101199001010011");
+ debt.setDebtTotalAmount(new BigDecimal("100"));
+ return debt;
+ }
+
+ private CcdiCreditNegativeInfo buildNegative() {
+ CcdiCreditNegativeInfo info = new CcdiCreditNegativeInfo();
+ info.setPersonId("330101199001010011");
+ return info;
+ }
+}
diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java
new file mode 100644
index 00000000..e0316190
--- /dev/null
+++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java
@@ -0,0 +1,61 @@
+package com.ruoyi.info.collection.service.support;
+
+import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
+import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
+import com.ruoyi.lsfx.domain.response.CreditParsePayload;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CreditInfoPayloadAssemblerTest {
+
+ private final CreditInfoPayloadAssembler assembler = new CreditInfoPayloadAssembler();
+
+ @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());
+ }
+
+ @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());
+ }
+
+ @Test
+ void shouldSkipDebtRowWhenAllMetricsAreEmpty() {
+ CreditParsePayload payload = new CreditParsePayload();
+ payload.setLxDebt(new HashMap<>());
+
+ assertTrue(assembler.buildDebts("3301", "张三", LocalDate.parse("2026-03-01"), payload).isEmpty());
+ }
+}
diff --git a/docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md b/docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md
new file mode 100644
index 00000000..7d5d8730
--- /dev/null
+++ b/docs/reports/implementation/2026-03-23-credit-info-maintenance-backend-implementation.md
@@ -0,0 +1,214 @@
+# 征信维护后端实施记录
+
+## 1. 实施概述
+
+- 实施日期:2026-03-24
+- 实施范围:`ccdi-info-collection` 征信维护后端能力、SQL 脚本、联调验证、环境修正记录
+- 实施结果:代码实现完成,目标测试通过,隔离端口联调通过
+
+本次实现按设计文档完成以下能力:
+
+- 新增 `ccdi_debts_info`、`ccdi_credit_negative_info` 两张业务表脚本
+- 新增“征信维护”菜单与按钮权限脚本
+- 新增征信维护实体、DTO、VO、Mapper、装配器、服务、控制器
+- 支持批量上传征信 HTML、按员工身份证号归户、按最新征信覆盖写入
+- 支持员工维度列表、详情、删除接口
+
+## 2. 实际修改文件
+
+### 2.1 SQL
+
+- `sql/migration/2026-03-23-create-credit-info-tables.sql`
+- `sql/ccdi_credit_info_menu.sql`
+- `sql/migration/2026-03-24-recreate-credit-negative-info-table.sql`
+
+### 2.2 后端代码
+
+- `ccdi-info-collection/pom.xml`
+- `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`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoListVO.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoDetailVO.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoNegativeVO.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadResultVO.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CreditInfoUploadFailureVO.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiDebtsInfoMapper.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditNegativeInfoMapper.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiCreditInfoQueryMapper.java`
+- `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssembler.java`
+- `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/controller/CcdiCreditInfoController.java`
+- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditInfoQueryMapper.xml`
+- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiDebtsInfoMapper.xml`
+- `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiCreditNegativeInfoMapper.xml`
+
+### 2.3 测试代码
+
+- `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/CreditInfoPayloadAssemblerTest.java`
+- `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`
+
+## 3. 关键实施说明
+
+### 3.1 解析与落库
+
+- 上传接口调用 `CreditParseClient.parse("LXCUSTALL", "PERSON", tempFile)`
+- 从 `lx_header.query_cert_no` 归户到 `ccdi_base_staff.id_card`
+- 从 `lx_header.report_time` 提取 `query_date`
+- `lx_debt` 按 7 组固定前缀映射为负债明细
+- `lx_publictype` 直接映射为负面信息汇总
+- 同一员工在事务内执行“先删后插”,仅保留最新征信
+
+### 3.2 实际环境偏差与处理
+
+联调时发现开发库已存在同名旧表 `ccdi_credit_negative_info`,结构与本次设计不一致:
+
+- 旧表字段:`id`、`credit_info_id` 等
+- 目标字段:`negative_id`、`person_id`、`person_name`、`query_date` 等
+
+处理步骤:
+
+1. 先核对旧表行数:`SELECT COUNT(*) FROM ccdi_credit_negative_info;`
+2. 确认旧表为空表后,执行重建脚本
+3. 重建后重新执行上传、列表、详情、删除联调
+
+## 4. SQL 执行记录
+
+按仓库约定,涉及中文与建表脚本的执行均使用 `bin/mysql_utf8_exec.sh`:
+
+```bash
+bin/mysql_utf8_exec.sh sql/migration/2026-03-23-create-credit-info-tables.sql
+bin/mysql_utf8_exec.sh sql/migration/2026-03-24-recreate-credit-negative-info-table.sql
+```
+
+执行结果:
+
+- 首次执行建表脚本时,`ccdi_debts_info` 创建成功
+- 由于开发库已存在旧版 `ccdi_credit_negative_info`,脚本在第二张表处提示已存在
+- 通过重建脚本完成 `ccdi_credit_negative_info` 修正
+
+## 5. 测试与验证
+
+### 5.1 单元测试与编译
+
+执行命令:
+
+```bash
+mvn -pl ccdi-info-collection -Dtest=CreditInfoPayloadAssemblerTest test
+mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoServiceImplTest test
+mvn -pl ccdi-info-collection -Dtest=CcdiCreditInfoControllerTest test
+mvn -pl ccdi-info-collection -Dtest=CreditInfoPayloadAssemblerTest,CcdiCreditInfoServiceImplTest,CcdiCreditInfoControllerTest test
+mvn -pl ccdi-info-collection -am compile
+mvn -pl ruoyi-admin -am package -DskipTests
+```
+
+验证结果:
+
+- `CreditInfoPayloadAssemblerTest` 通过
+- `CcdiCreditInfoServiceImplTest` 通过
+- `CcdiCreditInfoControllerTest` 通过
+- 目标测试集合 7 条全部通过
+- `ccdi-info-collection` 及上游依赖编译通过
+- `ruoyi-admin.jar` 打包成功
+
+### 5.2 隔离端口联调
+
+为避免影响本机已有开发进程,本次联调使用隔离端口:
+
+- Mock 服务:`http://127.0.0.1:8001`
+- 后端服务:`http://127.0.0.1:62319`
+
+登录获取 token:
+
+```bash
+curl -X POST 'http://127.0.0.1:62319/login/test' \
+ -H 'Content-Type: application/json' \
+ -d '{"username":"admin","password":"admin123"}'
+```
+
+上传验证:
+
+```bash
+curl -H "Authorization: Bearer " \
+ -F 'files=@assets/征信解析员工样本/0001_徐伟_2040.html;type=text/html' \
+ 'http://127.0.0.1:62319/ccdi/creditInfo/upload'
+```
+
+结果:
+
+- `code = 200`
+- `data.totalCount = 1`
+- `data.successCount = 1`
+- `data.failureCount = 0`
+
+列表验证:
+
+```bash
+curl -H "Authorization: Bearer " \
+ 'http://127.0.0.1:62319/ccdi/creditInfo/list?pageNum=1&pageSize=10'
+```
+
+结果摘要:
+
+- 返回 `code = 200`
+- 员工 `徐伟 / 558455197203132040` 可见
+- `queryDate = 2024-12-09`
+- `debtCount = 7`
+- `civilCnt = 2`
+- `enforceCnt = 13`
+- `admCnt = 3`
+
+详情验证:
+
+```bash
+curl -H "Authorization: Bearer " \
+ 'http://127.0.0.1:62319/ccdi/creditInfo/558455197203132040'
+```
+
+结果摘要:
+
+- 返回 `code = 200`
+- `negativeInfo` 正常返回
+- `debtList` 返回 7 条明细
+
+删除验证:
+
+```bash
+curl -X DELETE -H "Authorization: Bearer " \
+ 'http://127.0.0.1:62319/ccdi/creditInfo/558455197203132040'
+
+curl -H "Authorization: Bearer " \
+ 'http://127.0.0.1:62319/ccdi/creditInfo/558455197203132040'
+```
+
+结果摘要:
+
+- 删除接口返回 `code = 200`
+- 删除后详情仍返回员工壳信息,但 `negativeInfo = null`、`debtList = []`
+- 满足“删除后详情为空或提示未维护征信”的预期
+
+## 6. 进程启停记录
+
+本次联调启动的进程:
+
+- Mock 服务:
+ `python3 -m uvicorn main:app --host 0.0.0.0 --port 8001`
+- 后端服务:
+ `java -jar ruoyi-admin/target/ruoyi-admin.jar --server.port=62319 --credit-parse.api.url=http://127.0.0.1:8001/xfeature-mngs/conversation/htmlEval`
+
+处理要求:
+
+- 联调结束后已按仓库约定停止本次启动的后端与 Mock 进程
+- 未触碰本机原有 `8000`、`62318` 端口上的既有开发进程
+
+## 7. 提交记录
+
+本次实施包含以下提交:
+
+- `fc78c2f` 新增征信维护建表与菜单脚本
+- `6959c7a` 新增征信维护对象与依赖骨架
+- `d2e3388` 新增征信解析结果装配器
+- `c22e379` 新增征信维护上传与覆盖服务
+- `155adbe` 新增征信维护查询与接口
diff --git a/sql/ccdi_credit_info_menu.sql b/sql/ccdi_credit_info_menu.sql
new file mode 100644
index 00000000..b19c33c1
--- /dev/null
+++ b/sql/ccdi_credit_info_menu.sql
@@ -0,0 +1,17 @@
+-- 添加征信维护菜单
+-- 注意: 执行前请确认已存在"信息维护"父菜单
+-- 如果不存在,请先执行以下语句创建父菜单:
+-- INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+-- VALUES (2000, '信息维护', 0, 4, 'dpc', NULL, '', '', 1, 0, 'M', '0', '0', '', 'example', 'admin', NOW(), '信息维护目录');
+
+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(), '');
diff --git a/sql/migration/2026-03-23-create-credit-info-tables.sql b/sql/migration/2026-03-23-create-credit-info-tables.sql
new file mode 100644
index 00000000..974ac206
--- /dev/null
+++ b/sql/migration/2026-03-23-create-credit-info-tables.sql
@@ -0,0 +1,40 @@
+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='员工征信负面信息';
diff --git a/sql/migration/2026-03-24-recreate-credit-negative-info-table.sql b/sql/migration/2026-03-24-recreate-credit-negative-info-table.sql
new file mode 100644
index 00000000..49885f28
--- /dev/null
+++ b/sql/migration/2026-03-24-recreate-credit-negative-info-table.sql
@@ -0,0 +1,20 @@
+DROP TABLE IF EXISTS `ccdi_credit_negative_info`;
+
+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='员工征信负面信息';