Implement credit parse result polling and sentinel handling
This commit is contained in:
@@ -36,6 +36,10 @@ import java.util.Map;
|
||||
@Service
|
||||
public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
|
||||
private static final int CREDIT_PARSE_SUCCESS_CODE = 10000;
|
||||
private static final int CREDIT_PARSE_SUCCESS_STATUS = 1;
|
||||
private static final int CREDIT_PARSE_SUCCESS_REASON_CODE = 200;
|
||||
|
||||
@Resource
|
||||
private CreditParseClient creditParseClient;
|
||||
|
||||
@@ -179,8 +183,14 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
if (!Boolean.TRUE.equals(response.getSuccess())) {
|
||||
throw new RuntimeException(stringValue(mappingOutputFields.getMessage(), "征信解析平台调用失败"));
|
||||
}
|
||||
if (!"0".equals(mappingOutputFields.getStatusCode())) {
|
||||
throw new RuntimeException(stringValue(mappingOutputFields.getMessage(), "征信解析失败"));
|
||||
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_CODE).equals(response.getCode())) {
|
||||
throw new RuntimeException("征信解析平台状态码异常: " + response.getCode());
|
||||
}
|
||||
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_STATUS).equals(response.getData().getStatus())) {
|
||||
throw new RuntimeException(parseErrorMessage(response, "征信解析状态异常: " + response.getData().getStatus()));
|
||||
}
|
||||
if (!Integer.valueOf(CREDIT_PARSE_SUCCESS_REASON_CODE).equals(response.getData().getReasonCode())) {
|
||||
throw new RuntimeException(parseErrorMessage(response, "征信解析原因码异常: " + response.getData().getReasonCode()));
|
||||
}
|
||||
if (mappingOutputFields.getPayload() == null) {
|
||||
throw new RuntimeException("征信解析结果为空");
|
||||
@@ -188,6 +198,20 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
return mappingOutputFields;
|
||||
}
|
||||
|
||||
private String parseErrorMessage(CreditParseInvokeResponse response, String defaultValue) {
|
||||
if (response == null || response.getData() == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String reasonMessage = stringValue(response.getData().getReasonMessage());
|
||||
if (!isBlank(reasonMessage)) {
|
||||
return reasonMessage;
|
||||
}
|
||||
if (response.getData().getMappingOutputFields() != null) {
|
||||
return stringValue(response.getData().getMappingOutputFields().getMessage(), defaultValue);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Map<String, Object> requireHeader(CreditParsePayload payload) {
|
||||
Map<String, Object> header = payload.getLxHeader();
|
||||
if (header == null || header.isEmpty()) {
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.util.Objects;
|
||||
@Component
|
||||
public class CreditInfoPayloadAssembler {
|
||||
|
||||
private static final BigDecimal MISSING_SENTINEL = new BigDecimal("-9999");
|
||||
|
||||
private static final List<DebtMapping> DEBT_MAPPINGS = List.of(
|
||||
new DebtMapping("uncle_bank_house", "银行", "住房贷款", "银行", "未结清银行住房贷款"),
|
||||
new DebtMapping("uncle_bank_car", "银行", "汽车贷款", "银行", "未结清银行汽车贷款"),
|
||||
@@ -26,7 +28,7 @@ public class CreditInfoPayloadAssembler {
|
||||
new DebtMapping("uncle_bank_consume", "银行", "消费贷款", "银行", "未结清银行消费贷款"),
|
||||
new DebtMapping("uncle_bank_other", "银行", "其他贷款", "银行", "未结清银行其他贷款"),
|
||||
new DebtMapping("uncle_not_bank", "非银", "非银行贷款", "非银", "未结清非银行贷款"),
|
||||
new DebtMapping("uncle_credit_cart", "银行", "信用卡", "银行", "未结清信用卡")
|
||||
new DebtMapping("uncle_credit_card", "银行", "信用卡", "银行", "未结清信用卡")
|
||||
);
|
||||
|
||||
public List<CcdiDebtsInfo> buildDebts(String personId, String personName, LocalDate queryDate, CreditParsePayload payload) {
|
||||
@@ -61,9 +63,13 @@ public class CreditInfoPayloadAssembler {
|
||||
|
||||
private CcdiDebtsInfo buildDebtRow(String personId, String personName, LocalDate queryDate,
|
||||
Map<String, Object> source, DebtMapping mapping) {
|
||||
Object stateValue = source.get(mapping.prefix() + "_state");
|
||||
if (isMissingSentinel(stateValue)) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal principalBalance = toBigDecimal(source.get(mapping.prefix() + "_bal"));
|
||||
BigDecimal debtTotalAmount = toBigDecimal(source.get(mapping.prefix() + "_lmt"));
|
||||
String debtStatus = toStringValue(source.get(mapping.prefix() + "_state"));
|
||||
String debtStatus = toStringValue(stateValue);
|
||||
if (isEmptyMetrics(principalBalance, debtTotalAmount, debtStatus)) {
|
||||
return null;
|
||||
}
|
||||
@@ -97,6 +103,9 @@ public class CreditInfoPayloadAssembler {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (isMissingSentinel(value)) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal) {
|
||||
return decimal;
|
||||
}
|
||||
@@ -111,10 +120,28 @@ public class CreditInfoPayloadAssembler {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (isMissingSentinel(value)) {
|
||||
return null;
|
||||
}
|
||||
String text = Objects.toString(value, "").trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private boolean isMissingSentinel(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
String text = Objects.toString(value, "").trim();
|
||||
if (text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(text).compareTo(MISSING_SENTINEL) == 0;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
@@ -108,6 +108,45 @@ class CcdiCreditInfoServiceImplTest {
|
||||
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadHtmlFiles_shouldRejectInvalidPlatformCode() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
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"));
|
||||
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
|
||||
response.setCode(99999);
|
||||
when(creditParseClient.parse(anyString())).thenReturn(response);
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
|
||||
assertEquals(0, result.getSuccessCount());
|
||||
assertEquals("征信解析平台状态码异常: 99999", result.getFailures().get(0).getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadHtmlFiles_shouldRejectInvalidResultStatus() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("files", "a.html", "text/html", "<html>a</html>".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
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"));
|
||||
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
|
||||
response.getData().setStatus(0);
|
||||
response.getData().setReasonCode(500);
|
||||
response.getData().setReasonMessage("结果解析失败");
|
||||
when(creditParseClient.parse(anyString())).thenReturn(response);
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
|
||||
assertEquals(0, result.getSuccessCount());
|
||||
assertEquals("结果解析失败", result.getFailures().get(0).getReason());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void creditHtmlStorage_shouldStoreHtmlUnderProfileAndBuildRemotePath(@TempDir Path profileDir) throws Exception {
|
||||
String oldProfile = RuoYiConfig.getProfile();
|
||||
@@ -142,15 +181,17 @@ class CcdiCreditInfoServiceImplTest {
|
||||
|
||||
CreditParseResponse response = new CreditParseResponse();
|
||||
response.setMessage("成功");
|
||||
response.setStatusCode("0");
|
||||
response.setStatusCode("ERR_SHOULD_IGNORE");
|
||||
response.setPayload(payload);
|
||||
|
||||
CreditParseInvokeData data = new CreditParseInvokeData();
|
||||
data.setMappingOutputFields(response);
|
||||
data.setStatus(1);
|
||||
data.setReasonCode(200);
|
||||
|
||||
CreditParseInvokeResponse invokeResponse = new CreditParseInvokeResponse();
|
||||
invokeResponse.setSuccess(true);
|
||||
invokeResponse.setCode(99999);
|
||||
invokeResponse.setCode(10000);
|
||||
invokeResponse.setData(data);
|
||||
return invokeResponse;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CreditInfoPayloadAssemblerTest {
|
||||
@@ -28,13 +29,18 @@ class CreditInfoPayloadAssemblerTest {
|
||||
debt.put("uncle_not_bank_bal", "2000");
|
||||
debt.put("uncle_not_bank_lmt", "3000");
|
||||
debt.put("uncle_not_bank_state", "逾期");
|
||||
debt.put("uncle_credit_card_bal", "100");
|
||||
debt.put("uncle_credit_card_lmt", "500");
|
||||
debt.put("uncle_credit_card_state", "正常");
|
||||
payload.setLxDebt(debt);
|
||||
|
||||
List<CcdiDebtsInfo> rows = assembler.buildDebts("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
|
||||
|
||||
assertEquals(2, rows.size());
|
||||
assertEquals(3, rows.size());
|
||||
assertEquals("住房贷款", rows.get(0).getDebtSubType());
|
||||
assertEquals("非银", rows.get(1).getCreditorType());
|
||||
assertEquals("信用卡", rows.get(2).getDebtSubType());
|
||||
assertEquals(new BigDecimal("500"), rows.get(2).getDebtTotalAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -51,6 +57,42 @@ class CreditInfoPayloadAssemblerTest {
|
||||
assertEquals(new BigDecimal("9800"), info.getCivilLmt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipDebtTypeWhenStateIsMissingSentinel() {
|
||||
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", "-9999");
|
||||
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(1, rows.size());
|
||||
assertEquals("非银行贷款", rows.get(0).getDebtSubType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTreatNegativeRiskMissingSentinelAsEmptyValue() {
|
||||
CreditParsePayload payload = new CreditParsePayload();
|
||||
Map<String, Object> publictype = new HashMap<>();
|
||||
publictype.put("civil_cnt", "-9999");
|
||||
publictype.put("civil_lmt", "-9999.0");
|
||||
publictype.put("enforce_cnt", 1);
|
||||
publictype.put("enforce_lmt", "1200");
|
||||
payload.setLxPublictype(publictype);
|
||||
|
||||
CcdiCreditNegativeInfo info = assembler.buildNegative("330101199001010011", "张三", LocalDate.parse("2026-03-01"), payload);
|
||||
|
||||
assertEquals(0, info.getCivilCnt());
|
||||
assertNull(info.getCivilLmt());
|
||||
assertEquals(1, info.getEnforceCnt());
|
||||
assertEquals(new BigDecimal("1200"), info.getEnforceLmt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipDebtRowWhenAllMetricsAreEmpty() {
|
||||
CreditParsePayload payload = new CreditParsePayload();
|
||||
|
||||
@@ -18,6 +18,12 @@ import java.util.Map;
|
||||
@Component
|
||||
public class CreditParseClient {
|
||||
|
||||
private static final int PLATFORM_SUCCESS_CODE = 10000;
|
||||
private static final int INITIATE_SUCCESS_STATUS = 1;
|
||||
private static final int INITIATE_SUCCESS_REASON_CODE = 200;
|
||||
private static final int RESULT_QUERY_MAX_ATTEMPTS = 5;
|
||||
private static final long RESULT_QUERY_INTERVAL_MILLIS = 2000L;
|
||||
|
||||
@Resource
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@@ -27,7 +33,10 @@ public class CreditParseClient {
|
||||
@Value("${credit-parse.api.url}")
|
||||
private String creditParseUrl;
|
||||
|
||||
@Value("${credit-parse.api.org-code:902000}")
|
||||
@Value("${credit-parse.api.result-url}")
|
||||
private String creditParseResultUrl;
|
||||
|
||||
@Value("${credit-parse.api.org-code:999000}")
|
||||
private String orgCode;
|
||||
|
||||
@Value("${credit-parse.api.run-type:1}")
|
||||
@@ -43,19 +52,13 @@ public class CreditParseClient {
|
||||
public CreditParseInvokeResponse parse(String model, String remotePath) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String actualModel = StringUtils.isBlank(model) ? defaultModel : model;
|
||||
String serialNum = buildSerialNum();
|
||||
try {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("serialNum", buildSerialNum());
|
||||
params.put("orgCode", orgCode);
|
||||
params.put("runType", runType);
|
||||
params.put("remotePath", remotePath);
|
||||
params.put("model", actualModel);
|
||||
Map<String, Object> initiateParams = buildInitiateParams(serialNum, actualModel, remotePath);
|
||||
CreditParseInvokeResponse initiateResponse = request(creditParseUrl, initiateParams, "发起接口");
|
||||
requireSuccessfulInitiateResponse(initiateResponse, "征信解析发起接口");
|
||||
|
||||
log.info("【征信解析】调用请求: url={}, params={}", creditParseUrl, toJson(params));
|
||||
String responseJson = httpUtil.postUrlEncodedFormForString(creditParseUrl, params, null);
|
||||
log.info("【征信解析】调用返回JSON: {}", responseJson);
|
||||
|
||||
CreditParseInvokeResponse response = objectMapper.readValue(responseJson, CreditParseInvokeResponse.class);
|
||||
CreditParseInvokeResponse response = queryResult(serialNum);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("【征信解析】调用完成: success={}, code={}, businessStatusCode={}, cost={}ms",
|
||||
@@ -65,17 +68,136 @@ public class CreditParseClient {
|
||||
? response.getData().getMappingOutputFields().getStatusCode() : null,
|
||||
elapsed);
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【征信解析】调用失败: serialNum={}, model={}, remotePath={}, error={}",
|
||||
serialNum, actualModel, remotePath, e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【征信解析】调用失败: model={}, remotePath={}, error={}",
|
||||
actualModel, remotePath, e.getMessage(), e);
|
||||
log.error("【征信解析】调用失败: serialNum={}, model={}, remotePath={}, error={}",
|
||||
serialNum, actualModel, remotePath, e.getMessage(), e);
|
||||
throw new LsfxApiException("征信解析调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildInitiateParams(String serialNum, String model, String remotePath) {
|
||||
Map<String, Object> params = buildBaseParams(serialNum);
|
||||
params.put("remotePath", remotePath);
|
||||
params.put("model", model);
|
||||
return params;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBaseParams(String serialNum) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("serialNum", serialNum);
|
||||
params.put("orgCode", orgCode);
|
||||
params.put("runType", runType);
|
||||
return params;
|
||||
}
|
||||
|
||||
private CreditParseInvokeResponse queryResult(String serialNum) {
|
||||
Map<String, Object> params = buildBaseParams(serialNum);
|
||||
for (int attempt = 1; attempt <= RESULT_QUERY_MAX_ATTEMPTS; attempt++) {
|
||||
CreditParseInvokeResponse response = request(creditParseResultUrl, params,
|
||||
"结果接口第" + attempt + "次查询");
|
||||
requireSuccessfulServiceResponse(response, "征信解析结果接口");
|
||||
if (response.getData() == null || response.getData().getMappingOutputFields() == null) {
|
||||
waitForNextResult(serialNum, attempt);
|
||||
continue;
|
||||
}
|
||||
if (response.getData().getMappingOutputFields().getPayload() != null) {
|
||||
return response;
|
||||
}
|
||||
waitForNextResult(serialNum, attempt);
|
||||
}
|
||||
throw new LsfxApiException("征信解析结果未返回");
|
||||
}
|
||||
|
||||
private void waitForNextResult(String serialNum, int attempt) {
|
||||
if (attempt >= RESULT_QUERY_MAX_ATTEMPTS) {
|
||||
return;
|
||||
}
|
||||
log.info("【征信解析】结果未返回: serialNum={}, attempt={}/{}, {}ms后重试",
|
||||
serialNum, attempt, RESULT_QUERY_MAX_ATTEMPTS, RESULT_QUERY_INTERVAL_MILLIS);
|
||||
try {
|
||||
sleepBeforeNextResultQuery(RESULT_QUERY_INTERVAL_MILLIS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LsfxApiException("征信解析结果查询被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void sleepBeforeNextResultQuery(long intervalMillis) throws InterruptedException {
|
||||
Thread.sleep(intervalMillis);
|
||||
}
|
||||
|
||||
private CreditParseInvokeResponse request(String url, Map<String, Object> params, String stage) {
|
||||
try {
|
||||
log.info("【征信解析】{}请求: url={}, params={}", stage, url, toJson(params));
|
||||
String responseJson = httpUtil.postUrlEncodedFormForString(url, params, null);
|
||||
log.info("【征信解析】{}返回JSON: {}", stage, responseJson);
|
||||
return objectMapper.readValue(responseJson, CreditParseInvokeResponse.class);
|
||||
} catch (LsfxApiException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new LsfxApiException("征信解析" + stage + "调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireSuccessfulInitiateResponse(CreditParseInvokeResponse response, String stage) {
|
||||
requireSuccessfulServiceResponse(response, stage);
|
||||
}
|
||||
|
||||
private void requireSuccessfulServiceResponse(CreditParseInvokeResponse response, String stage) {
|
||||
requireSuccessfulPlatformResponse(response, stage);
|
||||
if (response.getData() == null) {
|
||||
throw new LsfxApiException(stage + "返回结果为空");
|
||||
}
|
||||
if (!Integer.valueOf(INITIATE_SUCCESS_STATUS).equals(response.getData().getStatus())) {
|
||||
throw new LsfxApiException(serviceErrorMessage(response, stage + "状态异常: " + response.getData().getStatus()));
|
||||
}
|
||||
if (!Integer.valueOf(INITIATE_SUCCESS_REASON_CODE).equals(response.getData().getReasonCode())) {
|
||||
throw new LsfxApiException(serviceErrorMessage(response, stage + "原因码异常: " + response.getData().getReasonCode()));
|
||||
}
|
||||
}
|
||||
|
||||
private void requireSuccessfulPlatformResponse(CreditParseInvokeResponse response, String stage) {
|
||||
if (response == null) {
|
||||
throw new LsfxApiException(stage + "返回结果为空");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(response.getSuccess())) {
|
||||
throw new LsfxApiException(stage + "平台调用失败");
|
||||
}
|
||||
if (!Integer.valueOf(PLATFORM_SUCCESS_CODE).equals(response.getCode())) {
|
||||
throw new LsfxApiException(stage + "平台状态码异常: " + response.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSerialNum() {
|
||||
return "CCDI_CREDIT_" + System.currentTimeMillis() + "_" + IdUtils.fastSimpleUUID();
|
||||
}
|
||||
|
||||
private String stringValue(Object value, String defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String text = value.toString().trim();
|
||||
return text.isEmpty() ? defaultValue : text;
|
||||
}
|
||||
|
||||
private String serviceErrorMessage(CreditParseInvokeResponse response, String defaultValue) {
|
||||
if (response == null || response.getData() == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String reasonMessage = stringValue(response.getData().getReasonMessage(), null);
|
||||
if (reasonMessage != null) {
|
||||
return reasonMessage;
|
||||
}
|
||||
if (response.getData().getMappingOutputFields() != null) {
|
||||
return stringValue(response.getData().getMappingOutputFields().getMessage(), defaultValue);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private String toJson(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CreditParseInvokeData {
|
||||
|
||||
private CreditParseResponse mappingOutputFields;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private Integer reasonCode;
|
||||
|
||||
private String reasonMessage;
|
||||
}
|
||||
|
||||
@@ -14,16 +14,20 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -46,7 +50,7 @@ class CreditParseControllerTest {
|
||||
void shouldUseDefaultModelWhenMissing() {
|
||||
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
|
||||
response.setSuccess(true);
|
||||
response.setCode(1000);
|
||||
response.setCode(10000);
|
||||
|
||||
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||
when(client.parse(eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
|
||||
@@ -69,13 +73,14 @@ class CreditParseControllerTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
void creditParseClient_shouldParseSuccessResponseWithStringPayload() throws Exception {
|
||||
void creditParseClient_shouldInitiateAndQueryResultWithSameSerialNum() throws Exception {
|
||||
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||
CreditParseClient parseClient = new CreditParseClient();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "902000");
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
|
||||
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
@@ -86,8 +91,12 @@ class CreditParseControllerTest {
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"0\",\"payload\":"
|
||||
+ objectMapper.writeValueAsString(payload) + "}}}");
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE"));
|
||||
|
||||
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||
CreditParseInvokeResponse actual = parseClient.parse(remotePath);
|
||||
@@ -96,19 +105,188 @@ class CreditParseControllerTest {
|
||||
assertEquals(10000, actual.getCode());
|
||||
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
|
||||
.getPayload().getLxHeader().get("query_cert_no"));
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||
ArgumentCaptor<Map<String, Object>> initiateParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||
verify(httpUtil).postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
paramsCaptor.capture(),
|
||||
initiateParamsCaptor.capture(),
|
||||
isNull()
|
||||
);
|
||||
ArgumentCaptor<Map<String, Object>> resultParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||
verify(httpUtil).postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
resultParamsCaptor.capture(),
|
||||
isNull()
|
||||
);
|
||||
|
||||
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"));
|
||||
Map<String, Object> initiateParams = initiateParamsCaptor.getValue();
|
||||
Map<String, Object> resultParams = resultParamsCaptor.getValue();
|
||||
assertNotNull(initiateParams.get("serialNum"));
|
||||
assertTrue(initiateParams.get("serialNum").toString().startsWith("CCDI_CREDIT_"));
|
||||
assertEquals(initiateParams.get("serialNum"), resultParams.get("serialNum"));
|
||||
assertEquals("999000", initiateParams.get("orgCode"));
|
||||
assertEquals("999000", resultParams.get("orgCode"));
|
||||
assertEquals("1", initiateParams.get("runType"));
|
||||
assertEquals("1", resultParams.get("runType"));
|
||||
assertEquals(remotePath, initiateParams.get("remotePath"));
|
||||
assertEquals("LXCUSTALL", initiateParams.get("model"));
|
||||
assertEquals(false, resultParams.containsKey("remotePath"));
|
||||
assertEquals(false, resultParams.containsKey("model"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
void creditParseClient_shouldRetryEmptyPayloadFiveTimesWithTwoSecondInterval() throws Exception {
|
||||
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||
TestableCreditParseClient parseClient = new TestableCreditParseClient();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
|
||||
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
String emptyPayloadResponse = "{\"success\":true,\"code\":10000,\"data\":{\"status\":1,\"reasonCode\":200,\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"ERR_SHOULD_IGNORE\"}}}";
|
||||
String payload = "{\"lx_header\":{\"query_cert_no\":\"330101199001010011\",\"query_cust_name\":\"张三\",\"report_time\":\"2026-03-24\"}}";
|
||||
String resultResponse = resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE");
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, resultResponse);
|
||||
|
||||
CreditParseInvokeResponse actual = parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html");
|
||||
|
||||
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
|
||||
.getPayload().getLxHeader().get("query_cert_no"));
|
||||
verify(httpUtil, times(5)).postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
);
|
||||
assertEquals(List.of(2000L, 2000L, 2000L, 2000L), parseClient.getSleepIntervals());
|
||||
}
|
||||
|
||||
@Test
|
||||
void creditParseClient_shouldRejectInvalidOuterCode() throws Exception {
|
||||
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||
TestableCreditParseClient parseClient = new TestableCreditParseClient();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
|
||||
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":99999,\"data\":{\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"0\"}}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("平台状态码异常"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void creditParseClient_shouldRejectFailedResultStatus() throws Exception {
|
||||
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||
TestableCreditParseClient parseClient = new TestableCreditParseClient();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
|
||||
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"reasonMessage\":\"解析失败\",\"reasonCode\":500,\"status\":0,\"mappingOutputFields\":{\"message\":\"结果异常\"}}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("解析失败"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void creditParseClient_shouldRejectFailedInitiateStatus() throws Exception {
|
||||
HttpUtil httpUtil = mock(HttpUtil.class);
|
||||
TestableCreditParseClient parseClient = new TestableCreditParseClient();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(parseClient, "httpUtil", httpUtil);
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseUrl", "http://tz/api/service/interface/invokeService/xfeature");
|
||||
ReflectionTestUtils.setField(parseClient, "creditParseResultUrl", "http://tz/api/service/interface/invokeService/xfeatureResult");
|
||||
ReflectionTestUtils.setField(parseClient, "orgCode", "999000");
|
||||
ReflectionTestUtils.setField(parseClient, "runType", "1");
|
||||
ReflectionTestUtils.setField(parseClient, "defaultModel", "LXCUSTALL");
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"mappingOutputFields\":{\"message\":\"文件写入失败\"},\"reasonMessage\":\"文件写入失败\",\"reasonCode\":500,\"status\":0}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("文件写入失败"));
|
||||
verify(httpUtil, times(0)).postUrlEncodedFormForString(
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
);
|
||||
}
|
||||
|
||||
private static String initiateSuccessResponse() {
|
||||
return "{\"success\":true,\"code\":10000,\"data\":{\"traceId\":\"TRACE-001\","
|
||||
+ "\"mappingOutputFields\":{\"message\":\"文件写入成功,流水号为: CCDI_CREDIT_TEST\"},"
|
||||
+ "\"reasonMessage\":\"Running successfully\",\"procCode\":\"999000\","
|
||||
+ "\"reasonCode\":200,\"status\":1}}";
|
||||
}
|
||||
|
||||
private static String resultSuccessResponse(ObjectMapper objectMapper, String payload, String statusCode) throws Exception {
|
||||
return "{\"success\":true,\"code\":10000,\"data\":{\"status\":1,\"reasonCode\":200,"
|
||||
+ "\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"" + statusCode + "\",\"payload\":"
|
||||
+ objectMapper.writeValueAsString(payload) + "}}}";
|
||||
}
|
||||
|
||||
private static class TestableCreditParseClient extends CreditParseClient {
|
||||
private final List<Long> sleepIntervals = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void sleepBeforeNextResultQuery(long intervalMillis) {
|
||||
sleepIntervals.add(intervalMillis);
|
||||
}
|
||||
|
||||
private List<Long> getSleepIntervals() {
|
||||
return sleepIntervals;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
| `uncle_bank_consume` | 银行 | 消费贷款 | 银行 | 未结清银行消费贷款 |
|
||||
| `uncle_bank_other` | 银行 | 其他贷款 | 银行 | 未结清银行其他贷款 |
|
||||
| `uncle_not_bank` | 非银 | 非银行贷款 | 非银 | 未结清非银行贷款 |
|
||||
| `uncle_credit_cart` | 银行 | 信用卡 | 银行 | 未结清信用卡 |
|
||||
| `uncle_credit_card` | 银行 | 信用卡 | 银行 | 未结清信用卡 |
|
||||
|
||||
字段映射规则:
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
|
||||
落库过滤规则:
|
||||
|
||||
- 当某组 `*_state` 为 `-9999` 时,表示不存在该类型负债,不生成明细记录
|
||||
- 当某组 `principal_balance`、`debt_total_amount` 都为空或 `0`,且 `debt_status` 为空时,不生成明细记录
|
||||
- 其余情况生成一条明细
|
||||
|
||||
@@ -220,6 +221,8 @@
|
||||
- `enforce_lmt` -> `enforce_lmt`
|
||||
- `adm_lmt` -> `adm_lmt`
|
||||
|
||||
负面风险字段值为 `-9999` 或 `-9999.0` 时,表示不存在对应风险类型;次数按 `0` 处理,金额按空值处理。
|
||||
|
||||
## 10. 最新征信判定与覆盖策略
|
||||
|
||||
系统只保留员工最新征信,规则如下:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# 征信解析双接口与结果轮询后端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
根据 `天座征信解析接口文档.xlsx`,征信解析需要拆分为发起接口和结果接口:先通过 `/api/service/interface/invokeService/xfeature` 提交 HTML 远程地址,再通过 `/api/service/interface/invokeService/xfeatureResult` 按同一业务流水号查询解析结果。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 后端客户端
|
||||
- 保持 `CreditParseClient.parse(remotePath)` 和 `parse(model, remotePath)` 对外签名不变。
|
||||
- 内部生成同一个 `serialNum`,发起接口提交 `serialNum/orgCode/runType/remotePath/model`。
|
||||
- 结果接口提交 `serialNum/orgCode/runType`,最多查询 5 次,每次间隔 2 秒。
|
||||
- 外层严格校验 `success=true`、`code=10000`,业务层严格校验 `status_code=0`。
|
||||
- 仅当结果未就绪或 `payload` 为空时继续轮询,明确业务失败立即返回失败原因。
|
||||
|
||||
2. 配置
|
||||
- 保留 `credit-parse.api.url` 作为发起接口地址。
|
||||
- 新增 `credit-parse.api.result-url` 作为结果接口地址。
|
||||
- `dev/uat/nas/pro` 环境 `credit-parse.api.org-code` 统一调整为 `999000`。
|
||||
|
||||
3. Mock 服务
|
||||
- `/xfeature` 读取 `remotePath` 生成 payload,按 `serialNum` 暂存结果,只返回发起成功结构。
|
||||
- `/xfeatureResult` 按 `serialNum` 返回暂存 payload,未知流水号返回业务失败。
|
||||
|
||||
4. 征信维护落库
|
||||
- 上传入口、HTML 落盘、`remotePath` 拼接、页面接口均保持不变。
|
||||
- 原有落库逻辑继续读取最终结果中的 `lx_header/lx_debt/lx_publictype`。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- `mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test`
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ccdi-lsfx,ccdi-info-collection -am compile`
|
||||
- `cd lsfx-mock-server && pytest tests/test_credit_api.py tests/test_startup.py -q`
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 征信解析双接口与结果轮询实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
本次按 `天座征信解析接口文档.xlsx` 调整征信解析调用方式:由单次发起接口直接获取 `payload`,改为发起接口 `/api/service/interface/invokeService/xfeature` 加结果接口 `/api/service/interface/invokeService/xfeatureResult` 的两步调用。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. `CreditParseClient`
|
||||
- 保持原有 `parse` 方法签名不变。
|
||||
- 发起接口提交 `serialNum/orgCode/runType/remotePath/model`。
|
||||
- 结果接口使用同一 `serialNum` 提交 `serialNum/orgCode/runType`。
|
||||
- 结果接口最多轮询 5 次,每次间隔 2 秒。
|
||||
- 严格校验外层 `success=true`、`code=10000` 和业务层 `status_code=0`。
|
||||
|
||||
2. 配置
|
||||
- `application-dev.yml`、`application-uat.yml`、`application-nas.yml`、`application-pro.yml` 新增 `credit-parse.api.result-url`。
|
||||
- 上述环境的 `credit-parse.api.org-code` 统一调整为 `999000`。
|
||||
|
||||
3. `CcdiCreditInfoServiceImpl`
|
||||
- 征信维护落库前补充外层 `code=10000` 校验。
|
||||
- 上传入口、HTML 保存、`remotePath` 生成和落库字段映射保持不变。
|
||||
|
||||
4. `lsfx-mock-server`
|
||||
- `/xfeature` 改为发起接口,只暂存结果并返回发起成功结构。
|
||||
- 新增 `/xfeatureResult`,按 `serialNum` 返回暂存 payload。
|
||||
- Mock README 和启动路由说明同步更新。
|
||||
|
||||
5. 测试
|
||||
- Java 测试补充同一 `serialNum` 双接口调用、5 次轮询、2 秒间隔和外层状态码校验。
|
||||
- Mock 测试补充发起接口、结果接口、缺参、未知流水号和 payload 返回。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 影响征信解析客户端、征信维护上传后的解析调用、征信解析 Mock 服务和相关环境配置。
|
||||
- 不涉及前端页面、前端 API、数据库表结构和业务 payload 字段变更。
|
||||
|
||||
## 验证
|
||||
|
||||
- `mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test`:通过,6 个用例成功。
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`:通过,4 个用例成功。
|
||||
- `mvn -pl ccdi-lsfx,ccdi-info-collection -am compile`:通过。
|
||||
- `cd lsfx-mock-server && python3 -m pytest tests/test_credit_api.py tests/test_startup.py -q`:通过,9 个用例成功。
|
||||
- `git diff --check`:通过。
|
||||
- 后端联调:
|
||||
- 启动 `lsfx-mock-server`,并通过 `bin/restart_java_backend.sh restart` 重启后端。
|
||||
- 调用 `/login/test` 获取测试令牌后,通过 `/ccdi/creditInfo/upload` 上传本轮 HTML 测试文件。
|
||||
- 上传结果:`totalCount=1`、`successCount=1`、`failureCount=0`。
|
||||
- 列表按身份证号 `330781199001019914` 查询到 1 条征信记录。
|
||||
- 后端日志确认同一个 `serialNum` 先调用 `/api/service/interface/invokeService/xfeature`,再调用 `/api/service/interface/invokeService/xfeatureResult`。
|
||||
- 调用删除接口清理本轮测试数据,回查列表 `total=0`。
|
||||
- 本轮启动的 mock 和后端进程已关闭。
|
||||
- 浏览器验证说明:
|
||||
- 已使用 browser-use 打开本地前端并完成登录。
|
||||
- 后端重启后,浏览器旧登录态触发接口 500,且 browser-use 安全策略阻止通过 `javascript:` URL 清理本地状态,因此未继续执行页面上传动作。
|
||||
- 已改用真实后端接口完成上传、落库、日志和清理闭环验证。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 征信解析信用卡字段前缀调整实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
征信解析负债字段中,信用卡字段前缀需要由 `uncle_credit_cart_*` 调整为 `uncle_credit_card_*`,保持字段命名与信用卡含义一致。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. 后端字段装配
|
||||
- `CreditInfoPayloadAssembler` 中信用卡负债映射前缀由 `uncle_credit_cart` 调整为 `uncle_credit_card`。
|
||||
|
||||
2. Mock 字段生成
|
||||
- `lsfx-mock-server/config/credit_feature_schema.json` 中信用卡字段同步调整为:
|
||||
- `uncle_credit_card_bal`
|
||||
- `uncle_credit_card_lmt`
|
||||
- `uncle_credit_card_state`
|
||||
|
||||
3. 文档
|
||||
- `docs/design/2026-03-23-credit-info-maintenance-design.md` 中信用卡字段前缀同步调整。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅影响征信解析 payload 中信用卡负债字段的读取与本地 Mock 生成字段。
|
||||
- 不涉及接口成功判断、数据库结构、前端页面和其他负债类型。
|
||||
|
||||
## 验证
|
||||
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CreditInfoPayloadAssemblerTest,CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`:通过,10 个用例成功。
|
||||
- `cd lsfx-mock-server && python3 -m pytest tests/test_credit_api.py tests/test_startup.py -q`:通过,9 个用例成功。
|
||||
- `git diff --check`:通过。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 征信解析缺失标记过滤实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
征信解析返回中,负债字段的 `*_state` 若为 `-9999`,表示不存在该类型负债;负面风险字段中 `-9999` 或 `-9999.0` 也表示不存在对应风险类型,不能按真实负债或负面风险指标落库。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. 负债明细装配
|
||||
- `CreditInfoPayloadAssembler` 中新增 `-9999` 缺失标记识别。
|
||||
- 当某组负债的 `*_state` 为 `-9999` 时,直接跳过该负债类型,不生成 `ccdi_debts_info` 明细。
|
||||
- 数值和状态转换过程中同步将 `-9999` 视为空值,避免缺失标记落库。
|
||||
|
||||
2. 负面风险装配
|
||||
- `lx_publictype` 中次数字段为 `-9999` 时按 `0` 处理。
|
||||
- `lx_publictype` 中金额字段为 `-9999` 或 `-9999.0` 时按空值处理。
|
||||
|
||||
3. 测试
|
||||
- 补充 `*_state=-9999` 时跳过负债类型的单测。
|
||||
- 补充负面风险 `-9999` 转换为 `0/null` 的单测。
|
||||
|
||||
4. 文档
|
||||
- 更新 `docs/design/2026-03-23-credit-info-maintenance-design.md` 中负债过滤和负面风险缺失值规则。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅影响征信解析 payload 到负债明细、负面风险表的装配逻辑。
|
||||
- 不涉及接口调用、成功判断、数据库结构和前端页面。
|
||||
|
||||
## 验证
|
||||
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CreditInfoPayloadAssemblerTest,CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`:通过,10 个用例成功。
|
||||
- `git diff --check`:通过。
|
||||
- 使用 `/Users/wkc/Downloads/zxjx.txt` 按当前规则复核:
|
||||
- 负债明细仅保留 `uncle_bank_manage` 与 `uncle_not_bank` 两类。
|
||||
- 负面风险 `civil/enforce/adm` 的 `-9999` 均按无对应风险处理。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 征信解析发起接口成功判断调整实施记录
|
||||
|
||||
## 背景
|
||||
|
||||
联调日志显示,征信解析发起接口 `/api/service/interface/invokeService/xfeature` 已返回外层 `success=true`、`code=10000`,并在 `data.mappingOutputFields.message` 中返回“文件写入成功,流水号为...”。但系统仍按结果接口的 `mappingOutputFields.status_code=0` 判断发起接口成功,导致“文件写入成功”被误判为失败,后续结果接口未继续查询。
|
||||
|
||||
## 修改内容
|
||||
|
||||
1. `CreditParseClient`
|
||||
- 发起接口改为校验外层 `success=true`、`code=10000`,并校验发起层 `data.status=1`、`data.reasonCode=200`。
|
||||
- 结果接口改为与发起接口一致,校验外层 `success=true`、`code=10000`,并校验 `data.status=1`、`data.reasonCode=200`。
|
||||
- `mappingOutputFields.status_code` 不再作为 `/xfeatureResult` 是否成功的判断条件;结果接口返回成功后,仅通过 `payload` 是否为空判断是否继续轮询。
|
||||
- 接口异常时优先返回 `reasonMessage`,其次返回 `mappingOutputFields.message`。
|
||||
|
||||
2. 响应对象
|
||||
- `CreditParseInvokeData` 新增 `status`、`reasonCode`、`reasonMessage` 字段。
|
||||
- 对 `data` 层开启未知字段忽略,适配发起接口返回的 `traceId`、`procCode`、`bizId` 等额外字段。
|
||||
|
||||
3. Mock 与文档
|
||||
- `lsfx-mock-server` 发起接口和结果接口成功返回均补充 `status=1`、`reasonCode=200`。
|
||||
- Mock README 示例同步更新。
|
||||
|
||||
4. 测试
|
||||
- Java 单测改为使用发起接口真实成功结构。
|
||||
- 新增发起接口 `status/reasonCode` 失败时不继续查询结果接口的断言。
|
||||
- Java 单测增加 `/xfeatureResult` 返回 `status_code` 非 0 但 `status/reasonCode` 成功时仍继续落库的覆盖。
|
||||
- Mock 测试同步校验发起接口和结果接口的 `data.status=1`、`data.reasonCode=200`。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅影响征信解析双接口调用链的成功判断与本地 Mock 返回结构。
|
||||
- 不涉及前端页面、数据库结构、征信 payload 字段映射和落库逻辑。
|
||||
|
||||
## 验证
|
||||
|
||||
- `mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test`:通过,8 个用例成功。
|
||||
- `mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`:通过,5 个用例成功。
|
||||
- `cd lsfx-mock-server && python3 -m pytest tests/test_credit_api.py tests/test_startup.py -q`:通过,9 个用例成功。
|
||||
- `git diff --check`:通过。
|
||||
@@ -113,18 +113,50 @@ response = requests.post(
|
||||
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d serialNum=CCDI_CREDIT_1 \
|
||||
-d orgCode=902000 \
|
||||
-d orgCode=999000 \
|
||||
-d runType=1 \
|
||||
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
|
||||
-d model=LXCUSTALL
|
||||
```
|
||||
|
||||
成功时返回:
|
||||
发起成功时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 1000,
|
||||
"code": 10000,
|
||||
"data": {
|
||||
"mappingOutputFields": {
|
||||
"message": "文件写入成功,流水号为: CCDI_CREDIT_1"
|
||||
},
|
||||
"reasonMessage": "Running successfully",
|
||||
"outputFields": {
|
||||
"C_S_ZXJXMESSAGE": "文件写入成功,流水号为: CCDI_CREDIT_1"
|
||||
},
|
||||
"procCode": "999000",
|
||||
"bizId": "CCDI_CREDIT_1",
|
||||
"reasonCode": 200,
|
||||
"status": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
再用相同 `serialNum` 查询解析结果:
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatureResult \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d serialNum=CCDI_CREDIT_1 \
|
||||
-d orgCode=999000 \
|
||||
-d runType=1
|
||||
```
|
||||
|
||||
结果成功时返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 10000,
|
||||
"data": {
|
||||
"mappingOutputFields": {
|
||||
"message": "成功",
|
||||
@@ -134,7 +166,10 @@ curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatu
|
||||
"lx_debt": {},
|
||||
"lx_publictype": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"reasonMessage": "成功",
|
||||
"reasonCode": 200,
|
||||
"status": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -145,7 +180,7 @@ curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatu
|
||||
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d serialNum=CCDI_CREDIT_1 \
|
||||
-d orgCode=902000 \
|
||||
-d orgCode=999000 \
|
||||
-d runType=1 \
|
||||
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
|
||||
-d model=error_ERR_10001
|
||||
@@ -270,8 +305,9 @@ pytest tests/ -v --cov=. --cov-report=html
|
||||
| 4 | POST | `/watson/api/project/upload/getpendings` | 检查解析状态 |
|
||||
| 5 | POST | `/watson/api/project/batchDeleteUploadFile` | 删除文件 |
|
||||
| 6 | POST | `/watson/api/project/getBSByLogId` | 获取银行流水 |
|
||||
| 7 | POST | `/api/service/interface/invokeService/xfeature` | 征信解析 Mock |
|
||||
| 8 | GET | `/credit/health` | 征信解析健康检查 |
|
||||
| 7 | POST | `/api/service/interface/invokeService/xfeature` | 征信解析发起 Mock |
|
||||
| 8 | POST | `/api/service/interface/invokeService/xfeatureResult` | 征信解析结果 Mock |
|
||||
| 9 | GET | `/credit/health` | 征信解析健康检查 |
|
||||
|
||||
## ⚠️ 错误码列表
|
||||
|
||||
|
||||
@@ -112,17 +112,17 @@
|
||||
},
|
||||
{
|
||||
"domain": "lx_debt",
|
||||
"field": "uncle_credit_cart_bal",
|
||||
"field": "uncle_credit_card_bal",
|
||||
"type": "amount"
|
||||
},
|
||||
{
|
||||
"domain": "lx_debt",
|
||||
"field": "uncle_credit_cart_lmt",
|
||||
"field": "uncle_credit_card_lmt",
|
||||
"type": "amount"
|
||||
},
|
||||
{
|
||||
"domain": "lx_debt",
|
||||
"field": "uncle_credit_cart_state",
|
||||
"field": "uncle_credit_card_state",
|
||||
"type": "status",
|
||||
"options": ["正常", "逾期", "不良"]
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ app = FastAPI(
|
||||
- **解析状态** - 轮询检查文件解析状态
|
||||
- **文件删除** - 批量删除上传的文件
|
||||
- **流水查询** - 分页获取银行流水数据
|
||||
- **征信解析** - 读取 HTML 远程地址并返回结构化征信 payload
|
||||
- **征信解析** - 发起 HTML 远程解析并通过结果接口返回结构化征信 payload
|
||||
|
||||
### 错误模拟
|
||||
|
||||
@@ -40,7 +40,8 @@ app = FastAPI(
|
||||
2. 上传文件: POST /watson/api/project/remoteUploadSplitFile
|
||||
3. 轮询解析状态: POST /watson/api/project/upload/getpendings
|
||||
4. 获取流水: POST /watson/api/project/getBSByLogId
|
||||
5. 征信解析: POST /api/service/interface/invokeService/xfeature
|
||||
5. 征信解析发起: POST /api/service/interface/invokeService/xfeature
|
||||
6. 征信解析结果: POST /api/service/interface/invokeService/xfeatureResult
|
||||
""",
|
||||
version=settings.APP_VERSION,
|
||||
docs_url="/docs",
|
||||
|
||||
@@ -12,6 +12,7 @@ router = APIRouter()
|
||||
payload_service = CreditPayloadService("config/credit_feature_schema.json")
|
||||
debug_service = CreditDebugService("config/credit_response_examples.json")
|
||||
identity_service = CreditHtmlIdentityService()
|
||||
result_cache = {}
|
||||
|
||||
|
||||
@router.post("/api/service/interface/invokeService/xfeature")
|
||||
@@ -40,6 +41,27 @@ async def html_eval(
|
||||
filename=remote_filename(remotePath),
|
||||
subject_identity=subject_identity,
|
||||
)
|
||||
result_cache[serialNum] = payload
|
||||
return debug_service.build_initiate_success_response(serialNum)
|
||||
|
||||
|
||||
@router.post("/api/service/interface/invokeService/xfeatureResult")
|
||||
async def html_eval_result(
|
||||
serialNum: Optional[str] = Form(None),
|
||||
orgCode: Optional[str] = Form(None),
|
||||
runType: Optional[str] = Form(None),
|
||||
):
|
||||
error_response = debug_service.validate_result_request(
|
||||
serial_num=serialNum,
|
||||
org_code=orgCode,
|
||||
run_type=runType,
|
||||
)
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
payload = result_cache.get(serialNum)
|
||||
if payload is None:
|
||||
return debug_service.build_result_not_found_response(serialNum)
|
||||
return debug_service.build_success_response(payload)
|
||||
|
||||
|
||||
|
||||
@@ -41,10 +41,51 @@ class CreditDebugService:
|
||||
return self.build_error_response("ERR_10002")
|
||||
return None
|
||||
|
||||
def validate_result_request(
|
||||
self,
|
||||
serial_num: Optional[str],
|
||||
org_code: Optional[str],
|
||||
run_type: 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")
|
||||
return None
|
||||
|
||||
def build_initiate_success_response(self, serial_num: str) -> dict:
|
||||
message = f"文件写入成功,流水号为: {serial_num}"
|
||||
return {
|
||||
"success": True,
|
||||
"code": 10000,
|
||||
"data": {
|
||||
"mappingOutputFields": {
|
||||
"message": message,
|
||||
},
|
||||
"reasonMessage": "Running successfully",
|
||||
"outputFields": {
|
||||
"C_S_ZXJXMESSAGE": message,
|
||||
},
|
||||
"procCode": "999000",
|
||||
"bizId": serial_num,
|
||||
"reasonCode": 200,
|
||||
"status": 1,
|
||||
},
|
||||
}
|
||||
|
||||
def build_success_response(self, payload: dict) -> dict:
|
||||
response = copy.deepcopy(self.templates["success"])
|
||||
response["payload"] = payload
|
||||
return self.wrap_mapping_response(response)
|
||||
return self.wrap_mapping_response(response, status=1, reason_code=200)
|
||||
|
||||
def build_result_not_found_response(self, serial_num: str) -> dict:
|
||||
return self.wrap_mapping_response({
|
||||
"message": f"征信解析结果未返回: {serial_num}",
|
||||
"payload": None,
|
||||
"status_code": "ERR_99999",
|
||||
}, status=0, reason_code=500, reason_message=f"征信解析结果未返回: {serial_num}")
|
||||
|
||||
def build_missing_param_response(self, param_name: str) -> dict:
|
||||
response = self.build_error_response("ERR_99999")
|
||||
@@ -53,7 +94,8 @@ class CreditDebugService:
|
||||
return response
|
||||
|
||||
def build_error_response(self, error_code: str) -> dict:
|
||||
return self.wrap_mapping_response(copy.deepcopy(self.templates["errors"][error_code]))
|
||||
return self.wrap_mapping_response(copy.deepcopy(self.templates["errors"][error_code]),
|
||||
status=0, reason_code=500)
|
||||
|
||||
def detect_error_marker(self, model: str) -> Optional[str]:
|
||||
matched = re.search(r"error_(ERR_\d+)", model)
|
||||
@@ -64,12 +106,21 @@ class CreditDebugService:
|
||||
return error_code
|
||||
return None
|
||||
|
||||
def wrap_mapping_response(self, mapping_output_fields: dict) -> dict:
|
||||
def wrap_mapping_response(
|
||||
self,
|
||||
mapping_output_fields: dict,
|
||||
status: int = 0,
|
||||
reason_code: int = 500,
|
||||
reason_message: Optional[str] = None,
|
||||
) -> dict:
|
||||
return {
|
||||
"success": True,
|
||||
"code": 10000,
|
||||
"data": {
|
||||
"mappingOutputFields": mapping_output_fields,
|
||||
"reasonMessage": reason_message or mapping_output_fields.get("message"),
|
||||
"reasonCode": reason_code,
|
||||
"status": status,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,16 @@ class FakeStaffIdentityRepository:
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_file_service_state():
|
||||
"""避免 file_service 单例状态影响测试顺序。"""
|
||||
from routers import credit_api
|
||||
|
||||
file_service.file_records.clear()
|
||||
file_service.log_counter = settings.INITIAL_LOG_ID
|
||||
file_service.staff_identity_repository = FakeStaffIdentityRepository()
|
||||
credit_api.result_cache.clear()
|
||||
yield
|
||||
file_service.file_records.clear()
|
||||
file_service.log_counter = settings.INITIAL_LOG_ID
|
||||
credit_api.result_cache.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -41,7 +45,7 @@ def client():
|
||||
try:
|
||||
from routers import credit_api
|
||||
|
||||
if not any(route.path == "/xfeature-mngs/conversation/htmlEval" for route in app.routes):
|
||||
if not any(route.path == "/api/service/interface/invokeService/xfeature" for route in app.routes):
|
||||
app.include_router(credit_api.router, tags=["征信解析接口"])
|
||||
app.openapi_schema = None
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -1,47 +1,124 @@
|
||||
def test_html_eval_should_return_credit_payload(client, sample_credit_html_file):
|
||||
from routers import credit_api
|
||||
|
||||
|
||||
def mapping_fields(response):
|
||||
return response.json()["data"]["mappingOutputFields"]
|
||||
|
||||
|
||||
def mock_remote_html(monkeypatch, sample_credit_html_file):
|
||||
monkeypatch.setattr(credit_api, "fetch_remote_html", lambda remote_path: sample_credit_html_file[1])
|
||||
|
||||
|
||||
def test_credit_parse_should_initiate_and_return_result(client, monkeypatch, sample_credit_html_file):
|
||||
mock_remote_html(monkeypatch, sample_credit_html_file)
|
||||
serial_num = "CCDI_CREDIT_TEST_001"
|
||||
|
||||
initiate_response = client.post(
|
||||
"/api/service/interface/invokeService/xfeature",
|
||||
data={
|
||||
"serialNum": serial_num,
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
|
||||
"model": "LXCUSTALL",
|
||||
},
|
||||
)
|
||||
|
||||
assert initiate_response.status_code == 200
|
||||
initiate_data = initiate_response.json()
|
||||
assert initiate_data["success"] is True
|
||||
assert initiate_data["code"] == 10000
|
||||
assert initiate_data["data"]["status"] == 1
|
||||
assert initiate_data["data"]["reasonCode"] == 200
|
||||
assert "文件写入成功" in mapping_fields(initiate_response)["message"]
|
||||
assert "payload" not in mapping_fields(initiate_response)
|
||||
|
||||
result_response = client.post(
|
||||
"/api/service/interface/invokeService/xfeatureResult",
|
||||
data={
|
||||
"serialNum": serial_num,
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
},
|
||||
)
|
||||
|
||||
assert result_response.status_code == 200
|
||||
result_data = result_response.json()
|
||||
assert result_data["data"]["status"] == 1
|
||||
assert result_data["data"]["reasonCode"] == 200
|
||||
result_fields = mapping_fields(result_response)
|
||||
assert result_fields["status_code"] == "0"
|
||||
assert result_fields["message"] == "成功"
|
||||
assert result_fields["payload"]["lx_header"]["query_cust_name"] == "测试员工"
|
||||
assert result_fields["payload"]["lx_header"]["query_cert_no"] == "320101199001010030"
|
||||
assert "uncle_credit_card_bal" in result_fields["payload"]["lx_debt"]
|
||||
assert "uncle_credit_cart_bal" not in result_fields["payload"]["lx_debt"]
|
||||
|
||||
|
||||
def test_credit_parse_should_return_err_99999_for_missing_model(client, monkeypatch, sample_credit_html_file):
|
||||
mock_remote_html(monkeypatch, sample_credit_html_file)
|
||||
|
||||
response = client.post(
|
||||
"/xfeature-mngs/conversation/htmlEval",
|
||||
data={"model": "LXCUSTALL", "hType": "PERSON"},
|
||||
files={"file": sample_credit_html_file},
|
||||
"/api/service/interface/invokeService/xfeature",
|
||||
data={
|
||||
"serialNum": "CCDI_CREDIT_TEST_002",
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status_code"] == "0"
|
||||
assert data["message"] == "成功"
|
||||
assert "lx_header" in data["payload"]
|
||||
assert data["payload"]["lx_header"]["query_cust_name"] == "测试员工"
|
||||
assert data["payload"]["lx_header"]["query_cert_no"] == "320101199001010030"
|
||||
assert response.json()["data"]["status"] == 0
|
||||
assert response.json()["data"]["reasonCode"] == 500
|
||||
assert mapping_fields(response)["status_code"] == "ERR_99999"
|
||||
assert "model" in mapping_fields(response)["message"]
|
||||
|
||||
|
||||
def test_html_eval_should_return_err_99999_for_missing_model(client, sample_credit_html_file):
|
||||
def test_credit_parse_should_support_debug_error_marker(client, monkeypatch, sample_credit_html_file):
|
||||
mock_remote_html(monkeypatch, sample_credit_html_file)
|
||||
|
||||
response = client.post(
|
||||
"/xfeature-mngs/conversation/htmlEval",
|
||||
data={"hType": "PERSON"},
|
||||
files={"file": sample_credit_html_file},
|
||||
"/api/service/interface/invokeService/xfeature",
|
||||
data={
|
||||
"serialNum": "CCDI_CREDIT_TEST_003",
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
|
||||
"model": "error_ERR_10001",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status_code"] == "ERR_99999"
|
||||
assert mapping_fields(response)["status_code"] == "ERR_10001"
|
||||
|
||||
|
||||
def test_html_eval_should_return_err_10003_for_invalid_h_type(client, sample_credit_html_file):
|
||||
def test_credit_result_should_return_err_99999_for_unknown_serial_num(client):
|
||||
response = client.post(
|
||||
"/xfeature-mngs/conversation/htmlEval",
|
||||
data={"model": "LXCUSTALL", "hType": "JSON"},
|
||||
files={"file": sample_credit_html_file},
|
||||
"/api/service/interface/invokeService/xfeatureResult",
|
||||
data={
|
||||
"serialNum": "CCDI_CREDIT_NOT_EXISTS",
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status_code"] == "ERR_10003"
|
||||
result_fields = mapping_fields(response)
|
||||
assert result_fields["status_code"] == "ERR_99999"
|
||||
assert "征信解析结果未返回" in result_fields["message"]
|
||||
|
||||
|
||||
def test_html_eval_should_support_debug_error_marker(client, sample_credit_html_file):
|
||||
def test_credit_result_should_return_err_99999_for_missing_serial_num(client):
|
||||
response = client.post(
|
||||
"/xfeature-mngs/conversation/htmlEval",
|
||||
data={"model": "error_ERR_10001", "hType": "PERSON"},
|
||||
files={"file": sample_credit_html_file},
|
||||
"/api/service/interface/invokeService/xfeatureResult",
|
||||
data={
|
||||
"orgCode": "999000",
|
||||
"runType": "1",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status_code"] == "ERR_10001"
|
||||
result_fields = mapping_fields(response)
|
||||
assert result_fields["status_code"] == "ERR_99999"
|
||||
assert "serialNum" in result_fields["message"]
|
||||
|
||||
@@ -23,5 +23,6 @@ def test_dev_parse_args_should_reject_invalid_mode():
|
||||
def test_app_should_register_credit_mock_routes():
|
||||
paths = {route.path for route in app.routes}
|
||||
|
||||
assert "/xfeature-mngs/conversation/htmlEval" in paths
|
||||
assert "/api/service/interface/invokeService/xfeature" in paths
|
||||
assert "/api/service/interface/invokeService/xfeatureResult" in paths
|
||||
assert "/credit/health" in paths
|
||||
|
||||
@@ -150,7 +150,8 @@ lsfx:
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://localhost:8000/api/service/interface/invokeService/xfeature
|
||||
result-url: http://localhost:8000/api/service/interface/invokeService/xfeatureResult
|
||||
file-public-base-url: http://127.0.0.1:62318
|
||||
org-code: 902000
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
@@ -150,7 +150,8 @@ lsfx:
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeature
|
||||
result-url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeatureResult
|
||||
file-public-base-url: http://192.168.0.111:62318
|
||||
org-code: 902000
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
@@ -145,6 +145,7 @@ lsfx:
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/xfeature
|
||||
result-url: http://64.202.32.40:8083/api/service/interface/invokeService/xfeatureResult
|
||||
file-public-base-url: http://64.116.19.153
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
|
||||
@@ -148,7 +148,8 @@ lsfx:
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeature
|
||||
result-url: http://192.168.0.111:62320/api/service/interface/invokeService/xfeatureResult
|
||||
file-public-base-url: http://158.234.199.250:62318
|
||||
org-code: 902000
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
Reference in New Issue
Block a user