Merge branch 'codex/credit-parse-client-backend' into dev
This commit is contained in:
@@ -44,5 +44,11 @@
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CreditParseClient {
|
||||
|
||||
@Resource
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@Value("${credit-parse.api.url}")
|
||||
private String creditParseUrl;
|
||||
|
||||
public CreditParseResponse parse(String model, String hType, File file) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
log.info("【征信解析】开始调用: fileName={}, model={}, hType={}", file.getName(), model, hType);
|
||||
|
||||
try {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("model", model);
|
||||
params.put("hType", hType);
|
||||
params.put("file", file);
|
||||
|
||||
CreditParseResponse response = httpUtil.uploadFile(creditParseUrl, params, null, CreditParseResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("【征信解析】调用完成: statusCode={}, cost={}ms",
|
||||
response != null ? response.getStatusCode() : null, elapsed);
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("【征信解析】调用失败: fileName={}, model={}, hType={}, error={}",
|
||||
file.getName(), model, hType, e.getMessage(), e);
|
||||
throw new LsfxApiException("征信解析调用失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
|
||||
@Anonymous
|
||||
@RestController
|
||||
@RequestMapping("/lsfx/credit")
|
||||
public class CreditParseController {
|
||||
|
||||
private static final String DEFAULT_MODEL = "LXCUSTALL";
|
||||
private static final String DEFAULT_HTYPE = "PERSON";
|
||||
|
||||
@Resource
|
||||
private CreditParseClient creditParseClient;
|
||||
|
||||
@Operation(summary = "解析征信HTML", description = "上传征信HTML文件并调用外部解析服务")
|
||||
@PostMapping("/parse")
|
||||
public AjaxResult parse(@Parameter(description = "征信HTML文件") @RequestParam("file") MultipartFile file,
|
||||
@Parameter(description = "解析模型,默认LXCUSTALL") @RequestParam(required = false) String model,
|
||||
@Parameter(description = "主体类型,默认PERSON") @RequestParam(required = false) String hType) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
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 actualHType = StringUtils.isBlank(hType) ? DEFAULT_HTYPE : hType;
|
||||
|
||||
Path tempFile = null;
|
||||
try {
|
||||
String suffix = lowerCaseName.endsWith(".htm") ? ".htm" : ".html";
|
||||
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);
|
||||
} catch (LsfxApiException e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
} catch (IOException e) {
|
||||
return AjaxResult.error("文件转换失败:" + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("征信解析失败:" + e.getMessage());
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException ignored) {
|
||||
// 忽略临时文件删除失败,避免影响主流程返回
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class CreditParsePayload {
|
||||
|
||||
@JsonProperty("lx_header")
|
||||
private Map<String, Object> lxHeader;
|
||||
|
||||
@JsonProperty("lx_debt")
|
||||
private Map<String, Object> lxDebt;
|
||||
|
||||
@JsonProperty("lx_publictype")
|
||||
private Map<String, Object> lxPublictype;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreditParseResponse {
|
||||
|
||||
private String message;
|
||||
|
||||
@JsonProperty("status_code")
|
||||
private String statusCode;
|
||||
|
||||
private CreditParsePayload payload;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CreditParseClientTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Mock
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@InjectMocks
|
||||
private CreditParseClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(client, "creditParseUrl", "http://credit-host/xfeature-mngs/conversation/htmlEval");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeserializeCreditParseResponse() throws Exception {
|
||||
String json = """
|
||||
{
|
||||
"message": "成功",
|
||||
"status_code": "0",
|
||||
"payload": {
|
||||
"lx_header": {"query_cert_no": "3301"},
|
||||
"lx_debt": {"uncle_bank_house_bal": "12.00"},
|
||||
"lx_publictype": {"civil_cnt": 1}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
CreditParseResponse response = objectMapper.readValue(json, CreditParseResponse.class);
|
||||
|
||||
assertEquals("0", response.getStatusCode());
|
||||
assertEquals("3301", response.getPayload().getLxHeader().get("query_cert_no"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallConfiguredUrlWithMultipartParams() {
|
||||
File file = new File("sample.html");
|
||||
CreditParseResponse response = new CreditParseResponse();
|
||||
response.setStatusCode("0");
|
||||
|
||||
when(httpUtil.uploadFile(eq("http://credit-host/xfeature-mngs/conversation/htmlEval"), anyMap(), isNull(), eq(CreditParseResponse.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
CreditParseResponse actual = client.parse("LXCUSTALL", "PERSON", file);
|
||||
|
||||
assertEquals("0", actual.getStatusCode());
|
||||
verify(httpUtil).uploadFile(eq("http://credit-host/xfeature-mngs/conversation/htmlEval"), argThat(params ->
|
||||
"LXCUSTALL".equals(params.get("model"))
|
||||
&& "PERSON".equals(params.get("hType"))
|
||||
&& file.equals(params.get("file"))
|
||||
), isNull(), eq(CreditParseResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWrapHttpErrorsAsLsfxApiException() {
|
||||
when(httpUtil.uploadFile(anyString(), anyMap(), isNull(), eq(CreditParseResponse.class)))
|
||||
.thenThrow(new LsfxApiException("网络失败"));
|
||||
|
||||
assertThrows(LsfxApiException.class,
|
||||
() -> client.parse("LXCUSTALL", "PERSON", new File("sample.html")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CreditParseControllerTest {
|
||||
|
||||
@Mock
|
||||
private CreditParseClient client;
|
||||
|
||||
@InjectMocks
|
||||
private CreditParseController controller;
|
||||
|
||||
@Test
|
||||
void parse_shouldRejectEmptyFile() {
|
||||
AjaxResult result = controller.parse(null, null, null);
|
||||
assertEquals(500, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_shouldRejectNonHtmlFile() {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file", "credit.pdf", "application/pdf", "x".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
AjaxResult result = controller.parse(file, null, null);
|
||||
assertEquals(500, result.get("code"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultModelAndTypeWhenMissing() {
|
||||
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(file, null, null);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertSame(response, result.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAjaxErrorWhenClientThrows() {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"file", "credit.html", "text/html", "<html/>".getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
when(client.parse(anyString(), anyString(), any(File.class)))
|
||||
.thenThrow(new LsfxApiException("超时"));
|
||||
|
||||
AjaxResult result = controller.parse(file, null, null);
|
||||
|
||||
assertEquals(500, result.get("code"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# 征信解析客户端实施记录
|
||||
|
||||
## 1. 改动概述
|
||||
|
||||
本次在 `ccdi-lsfx` 模块新增了独立征信解析能力,包含以下内容:
|
||||
|
||||
- 新增 `CreditParseClient`,通过独立配置 `credit-parse.api.url` 调用外部征信解析服务。
|
||||
- 新增 `POST /lsfx/credit/parse` 联调接口,支持上传 `.html/.htm` 文件、默认补齐 `model=LXCUSTALL` 与 `hType=PERSON`,并将结果以 `AjaxResult` 形式返回。
|
||||
- 新增征信解析响应对象 `CreditParseResponse`、`CreditParsePayload`。
|
||||
- 为 `ccdi-lsfx` 补充 `spring-boot-starter-test` 依赖,并新增客户端、控制器单元测试。
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
- `ccdi-lsfx/pom.xml`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/CreditParseClient.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/CreditParseController.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParseResponse.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParsePayload.java`
|
||||
- `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java`
|
||||
- `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java`
|
||||
- `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- `ruoyi-admin/src/main/resources/application-nas.yml`
|
||||
|
||||
## 3. 测试记录
|
||||
|
||||
### 3.1 TDD 过程命令
|
||||
|
||||
执行过的关键测试命令如下:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test
|
||||
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest#shouldDeserializeCreditParseResponse test
|
||||
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest test
|
||||
mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- `CreditParseControllerTest` 首次运行按预期失败,定位为 `CreditParseController` 不存在。
|
||||
- `CreditParseClientTest#shouldDeserializeCreditParseResponse` 首次运行按预期失败,定位为响应对象不存在。
|
||||
- `CreditParseClientTest` 与 `CreditParseControllerTest` 最终均通过。
|
||||
|
||||
### 3.2 模块回归命令
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-lsfx test
|
||||
mvn -pl ccdi-lsfx -am compile
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- `mvn -pl ccdi-lsfx test`:通过,`CreditParseClientTest` 和 `CreditParseControllerTest` 共 7 个测试全部通过。
|
||||
- `mvn -pl ccdi-lsfx -am compile`:通过,`ruoyi-common`、`ccdi-lsfx` 编译成功。
|
||||
|
||||
### 3.3 Swagger / 接口联调
|
||||
|
||||
执行过程:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am install -DskipTests
|
||||
cd ruoyi-admin && mvn spring-boot:run
|
||||
curl -I http://127.0.0.1:62318/swagger-ui.html
|
||||
curl http://127.0.0.1:62318/v3/api-docs
|
||||
curl -F 'file=@/tmp/credit-parse-sample.html;type=text/html' http://127.0.0.1:62318/lsfx/credit/parse
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- `swagger-ui.html` 可正常跳转到 `/swagger-ui/index.html`。
|
||||
- OpenAPI 文档中已存在 `征信解析接口测试` 标签以及 `POST /lsfx/credit/parse` 路径。
|
||||
- 上传 `.html` 文件时,请求已进入 `CreditParseController` 与 `CreditParseClient`,但外部地址 `http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval` 在联调时返回 `The target server failed to respond`,因此未拿到成功解析结果。
|
||||
- 联调结束后已停止 `ruoyi-admin` 启动进程,未保留端口占用。
|
||||
|
||||
## 4. 结果说明
|
||||
|
||||
- 设计范围内的后端代码已完成,未接入 `ccdi-project`,未新增落库逻辑。
|
||||
- 独立配置 `credit-parse.api.url` 已在 `application-dev.yml` 与 `application-nas.yml` 中补齐。
|
||||
- 控制器已实现文件校验、默认值补齐、临时文件转换、调用客户端和临时文件清理。
|
||||
- 客户端已实现调用日志、耗时记录和统一异常包装。
|
||||
- 本次联调结论为:系统侧接口注册、Swagger 暴露和请求链路已打通;外部征信解析服务当前未响应,需待对方服务可用后继续做成功结果验证。
|
||||
@@ -141,4 +141,8 @@ lsfx:
|
||||
# 连接池配置
|
||||
pool:
|
||||
max-total: 100 # 最大连接数
|
||||
default-max-per-route: 20 # 每个路由最大连接数
|
||||
default-max-per-route: 20 # 每个路由最大连接数
|
||||
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval
|
||||
|
||||
@@ -141,4 +141,8 @@ lsfx:
|
||||
# 连接池配置
|
||||
pool:
|
||||
max-total: 100 # 最大连接数
|
||||
default-max-per-route: 20 # 每个路由最大连接数
|
||||
default-max-per-route: 20 # 每个路由最大连接数
|
||||
|
||||
credit-parse:
|
||||
api:
|
||||
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval
|
||||
|
||||
Reference in New Issue
Block a user