Merge branch 'codex/credit-info-maintenance-backend' into dev

This commit is contained in:
wkc
2026-03-24 09:25:34 +08:00
26 changed files with 1524 additions and 0 deletions

View File

@@ -29,6 +29,12 @@
<artifactId>ruoyi-system</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
<version>3.9.1</version>
</dependency>
<!-- easyexcel工具 -->
<dependency>
<groupId>com.alibaba</groupId>

View File

@@ -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<CreditInfoListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CreditInfoListVO> 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));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<CcdiDebtsInfo> debtList;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<CreditInfoUploadFailureVO> failures = new ArrayList<>();
}

View File

@@ -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<CreditInfoListVO> selectCreditInfoPage(@Param("page") Page<CreditInfoListVO> page,
@Param("query") CcdiCreditInfoQueryDTO queryDTO);
CreditInfoListVO selectCreditInfoSummaryByPersonId(@Param("personId") String personId);
CreditInfoDetailVO selectCreditInfoDetailByPersonId(@Param("personId") String personId);
List<CreditInfoListVO> selectCreditInfoList(@Param("query") CcdiCreditInfoQueryDTO queryDTO);
LocalDate selectLatestQueryDate(@Param("personId") String personId);
}

View File

@@ -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> {
CcdiCreditNegativeInfo selectByPersonId(@Param("personId") String personId);
int deleteByPersonId(@Param("personId") String personId);
}

View File

@@ -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<CcdiDebtsInfo> {
List<CcdiDebtsInfo> selectByPersonId(@Param("personId") String personId);
int deleteByPersonId(@Param("personId") String personId);
int insertBatch(@Param("list") List<CcdiDebtsInfo> list);
}

View File

@@ -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<MultipartFile> files);
Page<CreditInfoListVO> selectCreditInfoPage(Page<CreditInfoListVO> page, CcdiCreditInfoQueryDTO queryDTO);
CreditInfoDetailVO selectDetailByPersonId(String personId);
int deleteByPersonId(String personId);
void replaceEmployeeCredit(String personId, List<CcdiDebtsInfo> debts, CcdiCreditNegativeInfo negative, String userName);
}

View File

@@ -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<MultipartFile> files) {
CreditInfoUploadResultVO result = new CreditInfoUploadResultVO();
List<CreditInfoUploadFailureVO> 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<CreditInfoListVO> selectCreditInfoPage(Page<CreditInfoListVO> 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<CcdiDebtsInfo> 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<CcdiDebtsInfo> 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<String, Object> 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<CcdiDebtsInfo> 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<String, Object> requireHeader(CreditParsePayload payload) {
Map<String, Object> 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<CcdiBaseStaff>()
.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";
}
}
}

View File

@@ -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<DebtMapping> 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<CcdiDebtsInfo> buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
Map<String, Object> source = payload == null ? null : payload.getLxDebt();
if (source == null || source.isEmpty()) {
return List.of();
}
List<CcdiDebtsInfo> 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<String, Object> 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<String, Object> 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) {
}
}

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper">
<select id="selectCreditInfoPage" resultType="com.ruoyi.info.collection.domain.vo.CreditInfoListVO">
SELECT
s.staff_id,
s.name,
s.id_card,
d.dept_name,
debt_agg.query_date,
IFNULL(debt_agg.debt_count, 0) AS debt_count,
IFNULL(debt_agg.debt_total_amount, 0) AS debt_total_amount,
IFNULL(neg.civil_cnt, 0) AS civil_cnt,
IFNULL(neg.enforce_cnt, 0) AS enforce_cnt,
IFNULL(neg.adm_cnt, 0) AS adm_cnt
FROM ccdi_base_staff s
LEFT JOIN sys_dept d ON s.dept_id = d.dept_id
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
) debt_agg ON debt_agg.person_id = s.id_card
LEFT JOIN ccdi_credit_negative_info neg ON neg.person_id = s.id_card
<where>
<if test="query != null and query.name != null and query.name != ''">
AND s.name LIKE CONCAT('%', #{query.name}, '%')
</if>
<if test="query != null and query.staffId != null and query.staffId != ''">
AND CAST(s.staff_id AS CHAR) = #{query.staffId}
</if>
<if test="query != null and query.idCard != null and query.idCard != ''">
AND s.id_card LIKE CONCAT('%', #{query.idCard}, '%')
</if>
<if test="query != null and query.maintained == '1'">
AND (debt_agg.person_id IS NOT NULL OR neg.person_id IS NOT NULL)
</if>
<if test="query != null and query.maintained == '0'">
AND debt_agg.person_id IS NULL
AND neg.person_id IS NULL
</if>
</where>
ORDER BY debt_agg.query_date DESC, s.staff_id DESC
</select>
<select id="selectCreditInfoSummaryByPersonId" resultType="com.ruoyi.info.collection.domain.vo.CreditInfoListVO">
SELECT
s.staff_id,
s.name,
s.id_card,
d.dept_name,
debt_agg.query_date,
IFNULL(debt_agg.debt_count, 0) AS debt_count,
IFNULL(debt_agg.debt_total_amount, 0) AS debt_total_amount,
IFNULL(neg.civil_cnt, 0) AS civil_cnt,
IFNULL(neg.enforce_cnt, 0) AS enforce_cnt,
IFNULL(neg.adm_cnt, 0) AS adm_cnt
FROM ccdi_base_staff s
LEFT JOIN sys_dept d ON s.dept_id = d.dept_id
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
) debt_agg ON debt_agg.person_id = s.id_card
LEFT JOIN ccdi_credit_negative_info neg ON neg.person_id = s.id_card
WHERE s.id_card = #{personId}
LIMIT 1
</select>
<select id="selectLatestQueryDate" resultType="java.time.LocalDate">
SELECT MAX(query_date)
FROM ccdi_debts_info
WHERE person_id = #{personId}
</select>
</mapper>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper">
<resultMap id="CcdiCreditNegativeInfoResultMap" type="com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo">
<id property="negativeId" column="negative_id"/>
<result property="personId" column="person_id"/>
<result property="personName" column="person_name"/>
<result property="queryDate" column="query_date"/>
<result property="civilCnt" column="civil_cnt"/>
<result property="enforceCnt" column="enforce_cnt"/>
<result property="admCnt" column="adm_cnt"/>
<result property="civilLmt" column="civil_lmt"/>
<result property="enforceLmt" column="enforce_lmt"/>
<result property="admLmt" column="adm_lmt"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByPersonId" resultMap="CcdiCreditNegativeInfoResultMap">
SELECT
negative_id, person_id, person_name, query_date, civil_cnt, enforce_cnt, adm_cnt,
civil_lmt, enforce_lmt, adm_lmt, create_by, create_time, update_by, update_time
FROM ccdi_credit_negative_info
WHERE person_id = #{personId}
LIMIT 1
</select>
<delete id="deleteByPersonId">
DELETE FROM ccdi_credit_negative_info
WHERE person_id = #{personId}
</delete>
</mapper>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper">
<resultMap id="CcdiDebtsInfoResultMap" type="com.ruoyi.info.collection.domain.CcdiDebtsInfo">
<id property="debtId" column="debt_id"/>
<result property="personId" column="person_id"/>
<result property="personName" column="person_name"/>
<result property="queryDate" column="query_date"/>
<result property="debtMainType" column="debt_main_type"/>
<result property="debtSubType" column="debt_sub_type"/>
<result property="creditorType" column="creditor_type"/>
<result property="debtName" column="debt_name"/>
<result property="principalBalance" column="principal_balance"/>
<result property="debtTotalAmount" column="debt_total_amount"/>
<result property="debtStatus" column="debt_status"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByPersonId" resultMap="CcdiDebtsInfoResultMap">
SELECT
debt_id, 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
FROM ccdi_debts_info
WHERE person_id = #{personId}
ORDER BY debt_id ASC
</select>
<delete id="deleteByPersonId">
DELETE FROM ccdi_debts_info
WHERE person_id = #{personId}
</delete>
<insert id="insertBatch">
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
<foreach collection="list" item="item" separator=",">
(#{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())
</foreach>
</insert>
</mapper>

View File

@@ -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<CreditInfoListVO>(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"));
}
}

View File

@@ -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", "<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("征信解析失败"));
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", "<html>a</html>".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<String, Object> 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;
}
}

View File

@@ -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<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());
}
@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());
}
@Test
void shouldSkipDebtRowWhenAllMetricsAreEmpty() {
CreditParsePayload payload = new CreditParsePayload();
payload.setLxDebt(new HashMap<>());
assertTrue(assembler.buildDebts("3301", "张三", LocalDate.parse("2026-03-01"), payload).isEmpty());
}
}

View File

@@ -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 <token>" \
-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 <token>" \
'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 <token>" \
'http://127.0.0.1:62319/ccdi/creditInfo/558455197203132040'
```
结果摘要:
- 返回 `code = 200`
- `negativeInfo` 正常返回
- `debtList` 返回 7 条明细
删除验证:
```bash
curl -X DELETE -H "Authorization: Bearer <token>" \
'http://127.0.0.1:62319/ccdi/creditInfo/558455197203132040'
curl -H "Authorization: Bearer <token>" \
'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` 新增征信维护查询与接口

View File

@@ -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(), '');

View File

@@ -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='员工征信负面信息';

View File

@@ -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='员工征信负面信息';