Refactor credit parse to use remote HTML paths
This commit is contained in:
@@ -14,8 +14,10 @@ import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
|
|||||||
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
|
||||||
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
|
||||||
import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
|
import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
|
||||||
|
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
|
||||||
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
||||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||||
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -23,8 +25,6 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -39,6 +39,9 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
|||||||
@Resource
|
@Resource
|
||||||
private CreditParseClient creditParseClient;
|
private CreditParseClient creditParseClient;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CreditHtmlStorageService creditHtmlStorageService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CreditInfoPayloadAssembler assembler;
|
private CreditInfoPayloadAssembler assembler;
|
||||||
|
|
||||||
@@ -141,10 +144,9 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSingleFile(MultipartFile multipartFile, String userName) throws IOException {
|
private void handleSingleFile(MultipartFile multipartFile, String userName) throws Exception {
|
||||||
File tempFile = createTempFile(multipartFile);
|
CreditHtmlStorageService.StoredCreditHtml storedHtml = creditHtmlStorageService.save(multipartFile);
|
||||||
try {
|
CreditParseInvokeResponse response = creditParseClient.parse(storedHtml.remotePath());
|
||||||
CreditParseResponse response = creditParseClient.parse("LXCUSTALL", "PERSON", tempFile);
|
|
||||||
CreditParsePayload payload = requireResponse(response).getPayload();
|
CreditParsePayload payload = requireResponse(response).getPayload();
|
||||||
Map<String, Object> header = requireHeader(payload);
|
Map<String, Object> header = requireHeader(payload);
|
||||||
String personId = stringValue(header.get("query_cert_no"));
|
String personId = stringValue(header.get("query_cert_no"));
|
||||||
@@ -156,22 +158,6 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
|||||||
List<CcdiDebtsInfo> debts = assembler.buildDebts(personId, personName, queryDate, payload);
|
List<CcdiDebtsInfo> debts = assembler.buildDebts(personId, personName, queryDate, payload);
|
||||||
CcdiCreditNegativeInfo negative = assembler.buildNegative(personId, personName, queryDate, payload);
|
CcdiCreditNegativeInfo negative = assembler.buildNegative(personId, personName, queryDate, payload);
|
||||||
replaceEmployeeCredit(personId, debts, negative, userName);
|
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) {
|
private void validateHtmlFile(MultipartFile file) {
|
||||||
@@ -185,14 +171,21 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreditParseResponse requireResponse(CreditParseResponse response) {
|
private CreditParseResponse requireResponse(CreditParseInvokeResponse response) {
|
||||||
if (response == null || response.getPayload() == null) {
|
if (response == null || response.getData() == null || response.getData().getMappingOutputFields() == null) {
|
||||||
throw new RuntimeException("征信解析结果为空");
|
throw new RuntimeException("征信解析结果为空");
|
||||||
}
|
}
|
||||||
if (!"0".equals(response.getStatusCode())) {
|
CreditParseResponse mappingOutputFields = response.getData().getMappingOutputFields();
|
||||||
throw new RuntimeException(stringValue(response.getMessage(), "征信解析失败"));
|
if (!Boolean.TRUE.equals(response.getSuccess()) || response.getCode() == null || response.getCode() != 1000) {
|
||||||
|
throw new RuntimeException(stringValue(mappingOutputFields.getMessage(), "征信解析平台调用失败"));
|
||||||
}
|
}
|
||||||
return response;
|
if (!"0".equals(mappingOutputFields.getStatusCode())) {
|
||||||
|
throw new RuntimeException(stringValue(mappingOutputFields.getMessage(), "征信解析失败"));
|
||||||
|
}
|
||||||
|
if (mappingOutputFields.getPayload() == null) {
|
||||||
|
throw new RuntimeException("征信解析结果为空");
|
||||||
|
}
|
||||||
|
return mappingOutputFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> requireHeader(CreditParsePayload payload) {
|
private Map<String, Object> requireHeader(CreditParsePayload payload) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ruoyi.info.collection.service.support;
|
||||||
|
|
||||||
|
import com.ruoyi.common.config.RuoYiConfig;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 征信 HTML 服务器落盘与远程访问地址生成。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CreditHtmlStorageService {
|
||||||
|
|
||||||
|
private static final String CREDIT_HTML_DIR = "credit-html";
|
||||||
|
private static final String[] HTML_EXTENSIONS = {"html", "htm"};
|
||||||
|
|
||||||
|
@Value("${credit-parse.api.file-public-base-url}")
|
||||||
|
private String filePublicBaseUrl;
|
||||||
|
|
||||||
|
public StoredCreditHtml save(MultipartFile file) throws Exception {
|
||||||
|
String profilePath = FileUploadUtils.upload(getCreditHtmlBaseDir(), file, HTML_EXTENSIONS);
|
||||||
|
return new StoredCreditHtml(profilePath, buildRemotePath(profilePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCreditHtmlBaseDir() {
|
||||||
|
return RuoYiConfig.getProfile() + File.separator + CREDIT_HTML_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRemotePath(String profilePath) {
|
||||||
|
if (StringUtils.isBlank(filePublicBaseUrl)) {
|
||||||
|
throw new IllegalStateException("征信HTML公开访问地址未配置");
|
||||||
|
}
|
||||||
|
String normalizedBaseUrl = StringUtils.stripEnd(filePublicBaseUrl.trim(), "/");
|
||||||
|
String normalizedProfilePath = profilePath.startsWith("/") ? profilePath : "/" + profilePath;
|
||||||
|
return normalizedBaseUrl + normalizedProfilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StoredCreditHtml(String profilePath, String remotePath) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ruoyi.info.collection.service;
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import com.ruoyi.common.config.RuoYiConfig;
|
||||||
import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
|
import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
|
||||||
import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
|
import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
|
||||||
import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
|
import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
|
||||||
@@ -7,26 +8,33 @@ import com.ruoyi.info.collection.mapper.CcdiCreditInfoQueryMapper;
|
|||||||
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiCreditNegativeInfoMapper;
|
||||||
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
|
import com.ruoyi.info.collection.mapper.CcdiDebtsInfoMapper;
|
||||||
import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
|
import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
|
||||||
|
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
|
||||||
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
||||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||||
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeData;
|
||||||
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -41,6 +49,9 @@ class CcdiCreditInfoServiceImplTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private CreditParseClient creditParseClient;
|
private CreditParseClient creditParseClient;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CreditHtmlStorageService creditHtmlStorageService;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private CreditInfoPayloadAssembler assembler;
|
private CreditInfoPayloadAssembler assembler;
|
||||||
|
|
||||||
@@ -54,11 +65,15 @@ class CcdiCreditInfoServiceImplTest {
|
|||||||
private CcdiCreditInfoQueryMapper queryMapper;
|
private CcdiCreditInfoQueryMapper queryMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void uploadHtmlFiles_shouldStoreCreditObjectWithoutStaffBinding() {
|
void uploadHtmlFiles_shouldStoreCreditObjectWithoutStaffBinding() throws Exception {
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
"files", "family.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
|
"files", "family.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
|
when(creditHtmlStorageService.save(any()))
|
||||||
|
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
|
||||||
|
"/profile/credit-html/2026/05/12/family_1.html",
|
||||||
|
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html"));
|
||||||
|
when(creditParseClient.parse(anyString()))
|
||||||
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
|
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
|
||||||
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
|
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
|
||||||
.thenReturn(List.of(buildDebt("330101199202020022")));
|
.thenReturn(List.of(buildDebt("330101199202020022")));
|
||||||
@@ -69,15 +84,20 @@ class CcdiCreditInfoServiceImplTest {
|
|||||||
|
|
||||||
assertEquals(1, result.getSuccessCount());
|
assertEquals(1, result.getSuccessCount());
|
||||||
assertEquals(0, result.getFailureCount());
|
assertEquals(0, result.getFailureCount());
|
||||||
|
verify(creditParseClient).parse("http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
|
||||||
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
|
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
|
||||||
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
|
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void uploadHtmlFiles_shouldRejectOlderReportDate() {
|
void uploadHtmlFiles_shouldRejectOlderReportDate() throws Exception {
|
||||||
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
|
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
when(creditParseClient.parse(anyString(), anyString(), any(File.class)))
|
when(creditHtmlStorageService.save(any()))
|
||||||
|
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
|
||||||
|
"/profile/credit-html/2026/05/12/a_1.html",
|
||||||
|
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
|
||||||
|
when(creditParseClient.parse(anyString()))
|
||||||
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
|
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
|
||||||
when(queryMapper.selectLatestQueryDate("330101199001010011"))
|
when(queryMapper.selectLatestQueryDate("330101199001010011"))
|
||||||
.thenReturn(LocalDate.parse("2026-03-05"));
|
.thenReturn(LocalDate.parse("2026-03-05"));
|
||||||
@@ -88,7 +108,29 @@ class CcdiCreditInfoServiceImplTest {
|
|||||||
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
|
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
private CreditParseResponse successResponse(String personId, String personName, String reportTime) {
|
@Test
|
||||||
|
void creditHtmlStorage_shouldStoreHtmlUnderProfileAndBuildRemotePath(@TempDir Path profileDir) throws Exception {
|
||||||
|
String oldProfile = RuoYiConfig.getProfile();
|
||||||
|
new RuoYiConfig().setProfile(profileDir.toString());
|
||||||
|
try {
|
||||||
|
CreditHtmlStorageService storageService = new CreditHtmlStorageService();
|
||||||
|
ReflectionTestUtils.setField(storageService, "filePublicBaseUrl", "http://127.0.0.1:62318/");
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"files", "credit.html", "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
CreditHtmlStorageService.StoredCreditHtml storedHtml = storageService.save(file);
|
||||||
|
|
||||||
|
assertTrue(storedHtml.profilePath().startsWith("/profile/credit-html/"));
|
||||||
|
assertTrue(storedHtml.profilePath().endsWith(".html"));
|
||||||
|
assertEquals("http://127.0.0.1:62318" + storedHtml.profilePath(), storedHtml.remotePath());
|
||||||
|
Path savedFile = profileDir.resolve(storedHtml.profilePath().substring("/profile/".length()));
|
||||||
|
assertTrue(Files.exists(savedFile));
|
||||||
|
} finally {
|
||||||
|
new RuoYiConfig().setProfile(oldProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreditParseInvokeResponse successResponse(String personId, String personName, String reportTime) {
|
||||||
CreditParsePayload payload = new CreditParsePayload();
|
CreditParsePayload payload = new CreditParsePayload();
|
||||||
Map<String, Object> header = new HashMap<>();
|
Map<String, Object> header = new HashMap<>();
|
||||||
header.put("query_cert_no", personId);
|
header.put("query_cert_no", personId);
|
||||||
@@ -99,9 +141,18 @@ class CcdiCreditInfoServiceImplTest {
|
|||||||
payload.setLxPublictype(Map.of("civil_cnt", 1));
|
payload.setLxPublictype(Map.of("civil_cnt", 1));
|
||||||
|
|
||||||
CreditParseResponse response = new CreditParseResponse();
|
CreditParseResponse response = new CreditParseResponse();
|
||||||
|
response.setMessage("成功");
|
||||||
response.setStatusCode("0");
|
response.setStatusCode("0");
|
||||||
response.setPayload(payload);
|
response.setPayload(payload);
|
||||||
return response;
|
|
||||||
|
CreditParseInvokeData data = new CreditParseInvokeData();
|
||||||
|
data.setMappingOutputFields(response);
|
||||||
|
|
||||||
|
CreditParseInvokeResponse invokeResponse = new CreditParseInvokeResponse();
|
||||||
|
invokeResponse.setSuccess(true);
|
||||||
|
invokeResponse.setCode(1000);
|
||||||
|
invokeResponse.setData(data);
|
||||||
|
return invokeResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CcdiDebtsInfo buildDebt(String personId) {
|
private CcdiDebtsInfo buildDebt(String personId) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.ruoyi.lsfx.client;
|
package com.ruoyi.lsfx.client;
|
||||||
|
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||||
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||||
import com.ruoyi.lsfx.util.HttpUtil;
|
import com.ruoyi.lsfx.util.HttpUtil;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -8,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -22,26 +23,51 @@ public class CreditParseClient {
|
|||||||
@Value("${credit-parse.api.url}")
|
@Value("${credit-parse.api.url}")
|
||||||
private String creditParseUrl;
|
private String creditParseUrl;
|
||||||
|
|
||||||
public CreditParseResponse parse(String model, String hType, File file) {
|
@Value("${credit-parse.api.org-code:902000}")
|
||||||
|
private String orgCode;
|
||||||
|
|
||||||
|
@Value("${credit-parse.api.run-type:1}")
|
||||||
|
private String runType;
|
||||||
|
|
||||||
|
@Value("${credit-parse.api.model:LXCUSTALL}")
|
||||||
|
private String defaultModel;
|
||||||
|
|
||||||
|
public CreditParseInvokeResponse parse(String remotePath) {
|
||||||
|
return parse(defaultModel, remotePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreditParseInvokeResponse parse(String model, String remotePath) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
log.info("【征信解析】开始调用: fileName={}, model={}, hType={}", file.getName(), model, hType);
|
String actualModel = StringUtils.isBlank(model) ? defaultModel : model;
|
||||||
|
log.info("【征信解析】开始调用: model={}, remotePath={}", actualModel, remotePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
params.put("model", model);
|
params.put("serialNum", buildSerialNum());
|
||||||
params.put("hType", hType);
|
params.put("orgCode", orgCode);
|
||||||
params.put("file", file);
|
params.put("runType", runType);
|
||||||
|
params.put("remotePath", remotePath);
|
||||||
|
params.put("model", actualModel);
|
||||||
|
|
||||||
CreditParseResponse response = httpUtil.uploadFile(creditParseUrl, params, null, CreditParseResponse.class);
|
CreditParseInvokeResponse response = httpUtil.postUrlEncodedForm(
|
||||||
|
creditParseUrl, params, null, CreditParseInvokeResponse.class);
|
||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - startTime;
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
log.info("【征信解析】调用完成: statusCode={}, cost={}ms",
|
log.info("【征信解析】调用完成: success={}, code={}, businessStatusCode={}, cost={}ms",
|
||||||
response != null ? response.getStatusCode() : null, elapsed);
|
response != null ? response.getSuccess() : null,
|
||||||
|
response != null ? response.getCode() : null,
|
||||||
|
response != null && response.getData() != null && response.getData().getMappingOutputFields() != null
|
||||||
|
? response.getData().getMappingOutputFields().getStatusCode() : null,
|
||||||
|
elapsed);
|
||||||
return response;
|
return response;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("【征信解析】调用失败: fileName={}, model={}, hType={}, error={}",
|
log.error("【征信解析】调用失败: model={}, remotePath={}, error={}",
|
||||||
file.getName(), model, hType, e.getMessage(), e);
|
actualModel, remotePath, e.getMessage(), e);
|
||||||
throw new LsfxApiException("征信解析调用失败: " + e.getMessage(), e);
|
throw new LsfxApiException("征信解析调用失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildSerialNum() {
|
||||||
|
return "CCDI_CREDIT_" + System.currentTimeMillis() + "_" + IdUtils.fastSimpleUUID();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.ruoyi.common.annotation.Anonymous;
|
|||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -14,13 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
|
|
||||||
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
|
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
|
||||||
@Anonymous
|
@Anonymous
|
||||||
@@ -29,56 +22,27 @@ import java.nio.file.StandardCopyOption;
|
|||||||
public class CreditParseController {
|
public class CreditParseController {
|
||||||
|
|
||||||
private static final String DEFAULT_MODEL = "LXCUSTALL";
|
private static final String DEFAULT_MODEL = "LXCUSTALL";
|
||||||
private static final String DEFAULT_HTYPE = "PERSON";
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CreditParseClient creditParseClient;
|
private CreditParseClient creditParseClient;
|
||||||
|
|
||||||
@Operation(summary = "解析征信HTML", description = "上传征信HTML文件并调用外部解析服务")
|
@Operation(summary = "解析征信HTML", description = "传入征信HTML远程地址并调用外部解析服务")
|
||||||
@PostMapping("/parse")
|
@PostMapping("/parse")
|
||||||
public AjaxResult parse(@Parameter(description = "征信HTML文件") @RequestParam("file") MultipartFile file,
|
public AjaxResult parse(@Parameter(description = "征信HTML远程访问地址") @RequestParam("remotePath") String remotePath,
|
||||||
@Parameter(description = "解析模型,默认LXCUSTALL") @RequestParam(required = false) String model,
|
@Parameter(description = "模型编码,默认LXCUSTALL") @RequestParam(required = false) String model) {
|
||||||
@Parameter(description = "主体类型,默认PERSON") @RequestParam(required = false) String hType) {
|
if (StringUtils.isBlank(remotePath)) {
|
||||||
if (file == null || file.isEmpty()) {
|
return AjaxResult.error("征信HTML远程地址不能为空");
|
||||||
return AjaxResult.error("征信HTML文件不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
if (StringUtils.isBlank(originalFilename)) {
|
|
||||||
return AjaxResult.error("文件名不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
String lowerCaseName = originalFilename.toLowerCase();
|
|
||||||
if (!lowerCaseName.endsWith(".html") && !lowerCaseName.endsWith(".htm")) {
|
|
||||||
return AjaxResult.error("仅支持 HTML 格式文件");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String actualModel = StringUtils.isBlank(model) ? DEFAULT_MODEL : model;
|
String actualModel = StringUtils.isBlank(model) ? DEFAULT_MODEL : model;
|
||||||
String actualHType = StringUtils.isBlank(hType) ? DEFAULT_HTYPE : hType;
|
|
||||||
|
|
||||||
Path tempFile = null;
|
|
||||||
try {
|
try {
|
||||||
String suffix = lowerCaseName.endsWith(".htm") ? ".htm" : ".html";
|
CreditParseInvokeResponse response = creditParseClient.parse(actualModel, remotePath);
|
||||||
tempFile = Files.createTempFile("credit_parse_", suffix);
|
|
||||||
Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
|
|
||||||
File convertedFile = tempFile.toFile();
|
|
||||||
CreditParseResponse response = creditParseClient.parse(actualModel, actualHType, convertedFile);
|
|
||||||
return AjaxResult.success(response);
|
return AjaxResult.success(response);
|
||||||
} catch (LsfxApiException e) {
|
} catch (LsfxApiException e) {
|
||||||
return AjaxResult.error(e.getMessage());
|
return AjaxResult.error(e.getMessage());
|
||||||
} catch (IOException e) {
|
|
||||||
return AjaxResult.error("文件转换失败:" + e.getMessage());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return AjaxResult.error("征信解析失败:" + e.getMessage());
|
return AjaxResult.error("征信解析失败:" + e.getMessage());
|
||||||
} finally {
|
|
||||||
if (tempFile != null) {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
// 忽略临时文件删除失败,避免影响主流程返回
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ruoyi.lsfx.domain.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreditParseInvokeData {
|
||||||
|
|
||||||
|
private CreditParseResponse mappingOutputFields;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ruoyi.lsfx.domain.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class CreditParseInvokeResponse {
|
||||||
|
|
||||||
|
private Boolean success;
|
||||||
|
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private CreditParseInvokeData data;
|
||||||
|
}
|
||||||
@@ -206,6 +206,47 @@ public class HttpUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送POST请求(application/x-www-form-urlencoded格式,带请求头)
|
||||||
|
* @param url 请求URL
|
||||||
|
* @param params 表单参数
|
||||||
|
* @param headers 请求头
|
||||||
|
* @param responseType 响应类型
|
||||||
|
* @return 响应对象
|
||||||
|
*/
|
||||||
|
public <T> T postUrlEncodedForm(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
|
||||||
|
try {
|
||||||
|
HttpHeaders httpHeaders = createHeaders(headers);
|
||||||
|
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
|
||||||
|
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||||
|
if (params != null) {
|
||||||
|
params.forEach((key, value) -> {
|
||||||
|
if (value != null) {
|
||||||
|
body.add(key, value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
|
||||||
|
|
||||||
|
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||||
|
|
||||||
|
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||||
|
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
T responseBody = response.getBody();
|
||||||
|
if (responseBody == null) {
|
||||||
|
throw new LsfxApiException("API返回数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseBody;
|
||||||
|
} catch (RestClientException e) {
|
||||||
|
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件(Multipart格式)
|
* 上传文件(Multipart格式)
|
||||||
* @param url 请求URL
|
* @param url 请求URL
|
||||||
|
|||||||
@@ -2,23 +2,28 @@ package com.ruoyi.lsfx.controller;
|
|||||||
|
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||||
|
import com.ruoyi.lsfx.util.HttpUtil;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.util.Map;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -31,31 +36,21 @@ class CreditParseControllerTest {
|
|||||||
private CreditParseController controller;
|
private CreditParseController controller;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parse_shouldRejectEmptyFile() {
|
void parse_shouldRejectBlankRemotePath() {
|
||||||
AjaxResult result = controller.parse(null, null, null);
|
AjaxResult result = controller.parse(null, null);
|
||||||
assertEquals(500, result.get("code"));
|
assertEquals(500, result.get("code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void parse_shouldRejectNonHtmlFile() {
|
void shouldUseDefaultModelWhenMissing() {
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
|
||||||
"file", "credit.pdf", "application/pdf", "x".getBytes(StandardCharsets.UTF_8)
|
response.setSuccess(true);
|
||||||
);
|
response.setCode(1000);
|
||||||
AjaxResult result = controller.parse(file, null, null);
|
|
||||||
assertEquals(500, result.get("code"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||||
void shouldUseDefaultModelAndTypeWhenMissing() {
|
when(client.parse(eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
|
||||||
"file", "credit.html", "text/html", "<html/>".getBytes(StandardCharsets.UTF_8)
|
|
||||||
);
|
|
||||||
CreditParseResponse response = new CreditParseResponse();
|
|
||||||
response.setStatusCode("0");
|
|
||||||
|
|
||||||
when(client.parse(eq("LXCUSTALL"), eq("PERSON"), any(File.class))).thenReturn(response);
|
AjaxResult result = controller.parse(remotePath, null);
|
||||||
|
|
||||||
AjaxResult result = controller.parse(file, null, null);
|
|
||||||
|
|
||||||
assertEquals(200, result.get("code"));
|
assertEquals(200, result.get("code"));
|
||||||
assertSame(response, result.get("data"));
|
assertSame(response, result.get("data"));
|
||||||
@@ -63,14 +58,53 @@ class CreditParseControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnAjaxErrorWhenClientThrows() {
|
void shouldReturnAjaxErrorWhenClientThrows() {
|
||||||
MockMultipartFile file = new MockMultipartFile(
|
when(client.parse(anyString(), anyString()))
|
||||||
"file", "credit.html", "text/html", "<html/>".getBytes(StandardCharsets.UTF_8)
|
|
||||||
);
|
|
||||||
when(client.parse(anyString(), anyString(), any(File.class)))
|
|
||||||
.thenThrow(new LsfxApiException("超时"));
|
.thenThrow(new LsfxApiException("超时"));
|
||||||
|
|
||||||
AjaxResult result = controller.parse(file, null, null);
|
AjaxResult result = controller.parse("http://127.0.0.1:62318/profile/credit-html/a.html", null);
|
||||||
|
|
||||||
assertEquals(500, result.get("code"));
|
assertEquals(500, result.get("code"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
void creditParseClient_shouldPostUrlEncodedRemotePathParameters() {
|
||||||
|
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||||
|
CreditParseClient parseClient = new CreditParseClient();
|
||||||
|
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||||
|
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||||
|
ReflectionTestUtils.setField(parseClient, "orgCode", "902000");
|
||||||
|
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||||
|
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||||
|
|
||||||
|
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
|
||||||
|
response.setSuccess(true);
|
||||||
|
response.setCode(1000);
|
||||||
|
when(httpUtil.postUrlEncodedForm(
|
||||||
|
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||||
|
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||||
|
isNull(),
|
||||||
|
eq(CreditParseInvokeResponse.class)
|
||||||
|
)).thenReturn(response);
|
||||||
|
|
||||||
|
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||||
|
CreditParseInvokeResponse actual = parseClient.parse(remotePath);
|
||||||
|
|
||||||
|
assertSame(response, actual);
|
||||||
|
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||||
|
verify(httpUtil).postUrlEncodedForm(
|
||||||
|
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||||
|
paramsCaptor.capture(),
|
||||||
|
isNull(),
|
||||||
|
eq(CreditParseInvokeResponse.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> params = paramsCaptor.getValue();
|
||||||
|
assertNotNull(params.get("serialNum"));
|
||||||
|
assertTrue(params.get("serialNum").toString().startsWith("CCDI_CREDIT_"));
|
||||||
|
assertEquals("902000", params.get("orgCode"));
|
||||||
|
assertEquals("1", params.get("runType"));
|
||||||
|
assertEquals(remotePath, params.get("remotePath"));
|
||||||
|
assertEquals("LXCUSTALL", params.get("model"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 征信解析远程路径调用改造实施记录
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
天座征信解析接口调用方式变更:不再直接 multipart 上传 HTML 文件,改为先将 HTML 保存到业务服务器可访问目录,再将完整远程访问地址作为 `remotePath` 以 `application/x-www-form-urlencoded` 表单参数提交到新接口。
|
||||||
|
|
||||||
|
本次按新接口文档确认输出外层结构为 `success/code/data/mappingOutputFields`,其中 `mappingOutputFields.payload` 内部仍沿用原有 `lx_header/lx_debt/lx_publictype` 结构。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
1. 后端上传处理
|
||||||
|
- 上传入口 `/ccdi/creditInfo/upload` 保持不变。
|
||||||
|
- 新增 `CreditHtmlStorageService`,校验通过后的 HTML 保存到 `ruoyi.profile/credit-html/...`。
|
||||||
|
- 根据 `credit-parse.api.file-public-base-url` 与 `/profile/credit-html/...` 拼接生成 `remotePath`。
|
||||||
|
- 征信维护落库逻辑改为调用 `CreditParseClient.parse(remotePath)`,后续日期校验、落库、失败记录逻辑保持原样。
|
||||||
|
|
||||||
|
2. 天座接口客户端
|
||||||
|
- `CreditParseClient` 改为提交 `serialNum/orgCode/runType/remotePath/model` 表单参数。
|
||||||
|
- `HttpUtil` 增加 `postUrlEncodedForm` 方法,统一提交 `application/x-www-form-urlencoded`。
|
||||||
|
- 新增 `CreditParseInvokeResponse`、`CreditParseInvokeData`,承载新外层响应结构。
|
||||||
|
- 业务解析只读取 `data.mappingOutputFields` 下的 `message/status_code/payload`。
|
||||||
|
|
||||||
|
3. 配置
|
||||||
|
- `application-dev.yml`、`application-pro.yml`、`application-nas.yml`、`application-uat.yml` 同步调整 `credit-parse.api.url` 到 `/api/service/interface/invokeService/xfeature`。
|
||||||
|
- 新增 `credit-parse.api.file-public-base-url`、`org-code=902000`、`run-type=1`、`model=LXCUSTALL`。
|
||||||
|
|
||||||
|
4. 本地 mock
|
||||||
|
- `lsfx-mock-server` 新增支持 `/api/service/interface/invokeService/xfeature`。
|
||||||
|
- mock 接收新表单参数并通过 `remotePath` 读取 HTML。
|
||||||
|
- mock 返回新外层结构,payload 仍按旧结构生成。
|
||||||
|
|
||||||
|
5. 测试覆盖
|
||||||
|
- `CreditParseControllerTest` 覆盖新调试入口、默认模型、客户端异常、表单参数完整性。
|
||||||
|
- `CcdiCreditInfoServiceImplTest` 覆盖 HTML 保存路径与 `remotePath` 拼接、旧日期拦截、成功落库、从新外层结构读取旧 payload。
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
1. 接口文档核对
|
||||||
|
- 已读取 `天座征信解析接口文档.xlsx`,确认调用方式为 `POST application/x-www-form-urlencoded`。
|
||||||
|
- 已确认输入参数为 `serialNum/orgCode/runType/remotePath/model`。
|
||||||
|
- 已确认输出结构为 `success/code/data/mappingOutputFields`。
|
||||||
|
|
||||||
|
2. 单元测试与编译
|
||||||
|
- `mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test`:通过,4 个用例成功。
|
||||||
|
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`:通过,3 个用例成功。
|
||||||
|
- `mvn -pl ccdi-lsfx,ccdi-info-collection -am compile`:通过。
|
||||||
|
|
||||||
|
3. 联调验证
|
||||||
|
- 通过 `bin/restart_java_backend.sh restart` 启动后端。
|
||||||
|
- 启动本地 mock 后,通过 `/ccdi/creditInfo/upload` 上传临时 HTML。
|
||||||
|
- 后端日志确认调用参数包含:
|
||||||
|
- `model=LXCUSTALL`
|
||||||
|
- `remotePath=http://127.0.0.1:62318/profile/credit-html/2026/05/12/credit_remote_path_test_20260512190228A001.html`
|
||||||
|
- mock 日志确认收到 `POST /api/service/interface/invokeService/xfeature` 并返回 200。
|
||||||
|
- 上传结果:`successCount=1`、`failureCount=0`。
|
||||||
|
- 列表与详情均可查询到解析后的征信负面信息和债务信息。
|
||||||
|
- 联调测试身份证号 `330781199001019999` 对应数据已通过删除接口清理,复查列表 `total=0`。
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 影响征信维护上传入口、征信解析客户端、本地 mock、相关环境配置。
|
||||||
|
- 前端上传入口和页面接口路径不变。
|
||||||
|
- 不保留旧 multipart 外部接口兼容逻辑。
|
||||||
|
|
||||||
@@ -110,16 +110,23 @@ response = requests.post(
|
|||||||
### 征信解析 Mock
|
### 征信解析 Mock
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST http://localhost:8000/xfeature-mngs/conversation/htmlEval \
|
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
|
||||||
-F model=LXCUSTALL \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
-F hType=PERSON \
|
-d serialNum=CCDI_CREDIT_1 \
|
||||||
-F file=@./sample-credit.html
|
-d orgCode=902000 \
|
||||||
|
-d runType=1 \
|
||||||
|
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
|
||||||
|
-d model=LXCUSTALL
|
||||||
```
|
```
|
||||||
|
|
||||||
成功时返回:
|
成功时返回:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"success": true,
|
||||||
|
"code": 1000,
|
||||||
|
"data": {
|
||||||
|
"mappingOutputFields": {
|
||||||
"message": "成功",
|
"message": "成功",
|
||||||
"status_code": "0",
|
"status_code": "0",
|
||||||
"payload": {
|
"payload": {
|
||||||
@@ -127,16 +134,21 @@ curl -s -X POST http://localhost:8000/xfeature-mngs/conversation/htmlEval \
|
|||||||
"lx_debt": {},
|
"lx_debt": {},
|
||||||
"lx_publictype": {}
|
"lx_publictype": {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
调试错误码时,可在 `model` 中追加错误标记:
|
调试错误码时,可在 `model` 中追加错误标记:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s -X POST http://localhost:8000/xfeature-mngs/conversation/htmlEval \
|
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
|
||||||
-F model=error_ERR_10001 \
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
-F hType=PERSON \
|
-d serialNum=CCDI_CREDIT_1 \
|
||||||
-F file=@./sample-credit.html
|
-d orgCode=902000 \
|
||||||
|
-d runType=1 \
|
||||||
|
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
|
||||||
|
-d model=error_ERR_10001
|
||||||
```
|
```
|
||||||
|
|
||||||
健康检查:
|
健康检查:
|
||||||
@@ -258,7 +270,7 @@ pytest tests/ -v --cov=. --cov-report=html
|
|||||||
| 4 | POST | `/watson/api/project/upload/getpendings` | 检查解析状态 |
|
| 4 | POST | `/watson/api/project/upload/getpendings` | 检查解析状态 |
|
||||||
| 5 | POST | `/watson/api/project/batchDeleteUploadFile` | 删除文件 |
|
| 5 | POST | `/watson/api/project/batchDeleteUploadFile` | 删除文件 |
|
||||||
| 6 | POST | `/watson/api/project/getBSByLogId` | 获取银行流水 |
|
| 6 | POST | `/watson/api/project/getBSByLogId` | 获取银行流水 |
|
||||||
| 7 | POST | `/xfeature-mngs/conversation/htmlEval` | 征信解析 Mock |
|
| 7 | POST | `/api/service/interface/invokeService/xfeature` | 征信解析 Mock |
|
||||||
| 8 | GET | `/credit/health` | 征信解析健康检查 |
|
| 8 | GET | `/credit/health` | 征信解析健康检查 |
|
||||||
|
|
||||||
## ⚠️ 错误码列表
|
## ⚠️ 错误码列表
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ app = FastAPI(
|
|||||||
- **解析状态** - 轮询检查文件解析状态
|
- **解析状态** - 轮询检查文件解析状态
|
||||||
- **文件删除** - 批量删除上传的文件
|
- **文件删除** - 批量删除上传的文件
|
||||||
- **流水查询** - 分页获取银行流水数据
|
- **流水查询** - 分页获取银行流水数据
|
||||||
- **征信解析** - 上传 HTML 并返回结构化征信 payload
|
- **征信解析** - 读取 HTML 远程地址并返回结构化征信 payload
|
||||||
|
|
||||||
### 错误模拟
|
### 错误模拟
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ app = FastAPI(
|
|||||||
2. 上传文件: POST /watson/api/project/remoteUploadSplitFile
|
2. 上传文件: POST /watson/api/project/remoteUploadSplitFile
|
||||||
3. 轮询解析状态: POST /watson/api/project/upload/getpendings
|
3. 轮询解析状态: POST /watson/api/project/upload/getpendings
|
||||||
4. 获取流水: POST /watson/api/project/getBSByLogId
|
4. 获取流水: POST /watson/api/project/getBSByLogId
|
||||||
5. 征信解析: POST /xfeature-mngs/conversation/htmlEval
|
5. 征信解析: POST /api/service/interface/invokeService/xfeature
|
||||||
""",
|
""",
|
||||||
version=settings.APP_VERSION,
|
version=settings.APP_VERSION,
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from fastapi import APIRouter, File, Form, UploadFile
|
from fastapi import APIRouter, Form
|
||||||
|
|
||||||
from services.credit_debug_service import CreditDebugService
|
from services.credit_debug_service import CreditDebugService
|
||||||
from services.credit_html_identity_service import CreditHtmlIdentityService
|
from services.credit_html_identity_service import CreditHtmlIdentityService
|
||||||
@@ -12,26 +14,30 @@ debug_service = CreditDebugService("config/credit_response_examples.json")
|
|||||||
identity_service = CreditHtmlIdentityService()
|
identity_service = CreditHtmlIdentityService()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/xfeature-mngs/conversation/htmlEval")
|
@router.post("/api/service/interface/invokeService/xfeature")
|
||||||
async def html_eval(
|
async def html_eval(
|
||||||
|
serialNum: Optional[str] = Form(None),
|
||||||
|
orgCode: Optional[str] = Form(None),
|
||||||
|
runType: Optional[str] = Form(None),
|
||||||
|
remotePath: Optional[str] = Form(None),
|
||||||
model: Optional[str] = Form(None),
|
model: Optional[str] = Form(None),
|
||||||
hType: Optional[str] = Form(None),
|
|
||||||
file: Optional[UploadFile] = File(None),
|
|
||||||
):
|
):
|
||||||
error_response = debug_service.validate_request(
|
error_response = debug_service.validate_request(
|
||||||
|
serial_num=serialNum,
|
||||||
|
org_code=orgCode,
|
||||||
|
run_type=runType,
|
||||||
|
remote_path=remotePath,
|
||||||
model=model,
|
model=model,
|
||||||
h_type=hType,
|
|
||||||
file_present=file is not None,
|
|
||||||
)
|
)
|
||||||
if error_response:
|
if error_response:
|
||||||
return error_response
|
return error_response
|
||||||
|
|
||||||
html_content = await file.read()
|
html_content = fetch_remote_html(remotePath)
|
||||||
subject_identity = identity_service.extract_identity(html_content)
|
subject_identity = identity_service.extract_identity(html_content)
|
||||||
payload = payload_service.generate_payload(
|
payload = payload_service.generate_payload(
|
||||||
model=model,
|
model=model,
|
||||||
h_type=hType,
|
h_type="PERSON",
|
||||||
filename=file.filename or "credit.html",
|
filename=remote_filename(remotePath),
|
||||||
subject_identity=subject_identity,
|
subject_identity=subject_identity,
|
||||||
)
|
)
|
||||||
return debug_service.build_success_response(payload)
|
return debug_service.build_success_response(payload)
|
||||||
@@ -40,3 +46,14 @@ async def html_eval(
|
|||||||
@router.get("/credit/health")
|
@router.get("/credit/health")
|
||||||
async def credit_health():
|
async def credit_health():
|
||||||
return {"status": "healthy", "service": "credit-mock"}
|
return {"status": "healthy", "service": "credit-mock"}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_remote_html(remote_path: str) -> bytes:
|
||||||
|
with urlopen(remote_path, timeout=5) as response:
|
||||||
|
return response.read()
|
||||||
|
|
||||||
|
|
||||||
|
def remote_filename(remote_path: str) -> str:
|
||||||
|
path = urlparse(remote_path).path
|
||||||
|
filename = path.rsplit("/", 1)[-1]
|
||||||
|
return filename or "credit.html"
|
||||||
|
|||||||
@@ -9,19 +9,29 @@ class CreditDebugService:
|
|||||||
"""处理征信解析接口的调试标记、参数校验与响应封装。"""
|
"""处理征信解析接口的调试标记、参数校验与响应封装。"""
|
||||||
|
|
||||||
VALID_MODEL = "LXCUSTALL"
|
VALID_MODEL = "LXCUSTALL"
|
||||||
VALID_HTYPES = {"PERSON", "ENTERPRISE"}
|
|
||||||
|
|
||||||
def __init__(self, template_path: str):
|
def __init__(self, template_path: str):
|
||||||
self.template_path = template_path
|
self.template_path = template_path
|
||||||
self.templates = self._load_templates()
|
self.templates = self._load_templates()
|
||||||
|
|
||||||
def validate_request(self, model: Optional[str], h_type: Optional[str], file_present: bool):
|
def validate_request(
|
||||||
|
self,
|
||||||
|
serial_num: Optional[str],
|
||||||
|
org_code: Optional[str],
|
||||||
|
run_type: Optional[str],
|
||||||
|
remote_path: Optional[str],
|
||||||
|
model: Optional[str],
|
||||||
|
):
|
||||||
|
if not serial_num:
|
||||||
|
return self.build_missing_param_response("serialNum")
|
||||||
|
if not org_code:
|
||||||
|
return self.build_missing_param_response("orgCode")
|
||||||
|
if not run_type:
|
||||||
|
return self.build_missing_param_response("runType")
|
||||||
|
if not remote_path:
|
||||||
|
return self.build_missing_param_response("remotePath")
|
||||||
if not model:
|
if not model:
|
||||||
return self.build_missing_param_response("model")
|
return self.build_missing_param_response("model")
|
||||||
if not file_present:
|
|
||||||
return self.build_missing_param_response("file")
|
|
||||||
if not h_type:
|
|
||||||
return self.build_missing_param_response("hType")
|
|
||||||
|
|
||||||
error_code = self.detect_error_marker(model)
|
error_code = self.detect_error_marker(model)
|
||||||
if error_code:
|
if error_code:
|
||||||
@@ -29,22 +39,21 @@ class CreditDebugService:
|
|||||||
|
|
||||||
if model != self.VALID_MODEL:
|
if model != self.VALID_MODEL:
|
||||||
return self.build_error_response("ERR_10002")
|
return self.build_error_response("ERR_10002")
|
||||||
if h_type not in self.VALID_HTYPES:
|
|
||||||
return self.build_error_response("ERR_10003")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def build_success_response(self, payload: dict) -> dict:
|
def build_success_response(self, payload: dict) -> dict:
|
||||||
response = copy.deepcopy(self.templates["success"])
|
response = copy.deepcopy(self.templates["success"])
|
||||||
response["payload"] = payload
|
response["payload"] = payload
|
||||||
return response
|
return self.wrap_mapping_response(response)
|
||||||
|
|
||||||
def build_missing_param_response(self, param_name: str) -> dict:
|
def build_missing_param_response(self, param_name: str) -> dict:
|
||||||
response = self.build_error_response("ERR_99999")
|
response = self.build_error_response("ERR_99999")
|
||||||
response["message"] = response["message"].replace("XX", param_name)
|
mapping_output_fields = response["data"]["mappingOutputFields"]
|
||||||
|
mapping_output_fields["message"] = mapping_output_fields["message"].replace("XX", param_name)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def build_error_response(self, error_code: str) -> dict:
|
def build_error_response(self, error_code: str) -> dict:
|
||||||
return copy.deepcopy(self.templates["errors"][error_code])
|
return self.wrap_mapping_response(copy.deepcopy(self.templates["errors"][error_code]))
|
||||||
|
|
||||||
def detect_error_marker(self, model: str) -> Optional[str]:
|
def detect_error_marker(self, model: str) -> Optional[str]:
|
||||||
matched = re.search(r"error_(ERR_\d+)", model)
|
matched = re.search(r"error_(ERR_\d+)", model)
|
||||||
@@ -55,6 +64,15 @@ class CreditDebugService:
|
|||||||
return error_code
|
return error_code
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def wrap_mapping_response(self, mapping_output_fields: dict) -> dict:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"code": 1000,
|
||||||
|
"data": {
|
||||||
|
"mappingOutputFields": mapping_output_fields,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
def _load_templates(self) -> dict:
|
def _load_templates(self) -> dict:
|
||||||
template_file = Path(self.template_path)
|
template_file = Path(self.template_path)
|
||||||
if not template_file.is_absolute():
|
if not template_file.is_absolute():
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
ruoyi:
|
ruoyi:
|
||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: D:/ruoyi/uploadPath
|
profile: tmp/uploadPath
|
||||||
|
|
||||||
ccdi:
|
ccdi:
|
||||||
report:
|
report:
|
||||||
@@ -149,4 +149,8 @@ lsfx:
|
|||||||
|
|
||||||
credit-parse:
|
credit-parse:
|
||||||
api:
|
api:
|
||||||
url: http://localhost:8000/xfeature-mngs/conversation/htmlEval
|
url: http://localhost:8000/api/service/interface/invokeService/xfeature
|
||||||
|
file-public-base-url: http://127.0.0.1:62318
|
||||||
|
org-code: 902000
|
||||||
|
run-type: 1
|
||||||
|
model: LXCUSTALL
|
||||||
|
|||||||
@@ -149,4 +149,8 @@ lsfx:
|
|||||||
|
|
||||||
credit-parse:
|
credit-parse:
|
||||||
api:
|
api:
|
||||||
url: http://192.168.0.111:62320/xfeature-mngs/conversation/htmlEval
|
url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeature
|
||||||
|
file-public-base-url: http://192.168.0.111:62318
|
||||||
|
org-code: 902000
|
||||||
|
run-type: 1
|
||||||
|
model: LXCUSTALL
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
ruoyi:
|
ruoyi:
|
||||||
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
|
||||||
profile: backend/uploadPath
|
profile: /webapps/ccdi/uploadPath
|
||||||
|
|
||||||
ccdi:
|
ccdi:
|
||||||
report:
|
report:
|
||||||
@@ -144,4 +144,8 @@ lsfx:
|
|||||||
|
|
||||||
credit-parse:
|
credit-parse:
|
||||||
api:
|
api:
|
||||||
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval
|
url: http://64.202.32.40:8083/api/service/interface/invokeService/xfeature
|
||||||
|
file-public-base-url: http://64.116.19.153
|
||||||
|
org-code: 999000
|
||||||
|
run-type: 1
|
||||||
|
model: LXCUSTALL
|
||||||
|
|||||||
@@ -147,4 +147,8 @@ lsfx:
|
|||||||
|
|
||||||
credit-parse:
|
credit-parse:
|
||||||
api:
|
api:
|
||||||
url: http://192.168.0.111:62320/xfeature-mngs/conversation/htmlEval
|
url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeature
|
||||||
|
file-public-base-url: http://158.234.199.250:62318
|
||||||
|
org-code: 902000
|
||||||
|
run-type: 1
|
||||||
|
model: LXCUSTALL
|
||||||
|
|||||||
Reference in New Issue
Block a user