Files
ccdi/docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md

555 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Credit Parse Client Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**`ccdi-lsfx` 模块中新增独立征信解析 `Client` 和联调接口 `POST /lsfx/credit/parse`,通过独立配置调用外部征信解析服务。
**Architecture:** 复用 `ccdi-lsfx` 现有 `HttpUtil` 与异常体系,但不复用 `LsfxAnalysisClient``lsfx.api.*` 配置。控制器负责最小参数校验和临时文件转换,独立 `CreditParseClient` 负责 `multipart/form-data` 调用与响应映射,结果以 `AjaxResult.success(response)` 形式透传。
**Tech Stack:** Java 21, Spring Boot 3, Spring MVC, Jackson, RestTemplate, JUnit 5, Mockito, Maven
---
## 文件结构与职责
**新增文件**
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/CreditParseClient.java`
负责读取 `credit-parse.api.url`、组装 `multipart/form-data`、调用征信解析服务、记录日志。
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/CreditParseController.java`
负责接收 HTML 文件和可选参数、做最小校验、转换临时文件、调用 `CreditParseClient`、返回 `AjaxResult`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParseResponse.java`
映射外部返回的 `message``status_code``payload`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParsePayload.java`
按三个主题域承载 `payload`,使用 `Map<String, Object>` 保存字段。
- `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java`
校验 `CreditParseClient` 的参数组装、URL 读取和异常传播。
- `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java`
校验控制器参数默认值、文件校验、成功透传和异常兜底。
- `docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md`
记录本次后端实施实际改动、测试命令和结果。
**修改文件**
- `ccdi-lsfx/pom.xml`
补充测试依赖 `spring-boot-starter-test`
- `ruoyi-admin/src/main/resources/application-dev.yml`
新增 `credit-parse.api.url` 配置。
- `ruoyi-admin/src/main/resources/application-nas.yml`
新增 `credit-parse.api.url` 配置。
**参考文件**
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java`
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
- `docs/design/2026-03-23-credit-parse-client-design.md`
## Task 1: 补齐 `ccdi-lsfx` 测试基础
**Files:**
- Modify: `ccdi-lsfx/pom.xml`
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java`
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java`
- [ ] **Step 1: 为 `ccdi-lsfx` 添加测试依赖**
`ccdi-lsfx/pom.xml` 追加测试依赖:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
- [ ] **Step 2: 先写控制器失败用例**
`ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java` 写出最小失败用例,覆盖空文件与非法后缀:
```java
@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"));
}
```
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test
```
Expected:
- 编译失败,提示 `CreditParseController``parse(...)` 不存在。
- [ ] **Step 4: 提交前置依赖与测试骨架**
```bash
git add ccdi-lsfx/pom.xml ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java
git commit -m "新增征信解析测试基础"
```
## Task 2: 实现响应对象
**Files:**
- Create: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParseResponse.java`
- Create: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CreditParsePayload.java`
- Test: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java`
- [ ] **Step 1: 先写响应映射测试**
`CreditParseClientTest` 中先写 Jackson 反序列化测试:
```java
@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"));
}
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest#shouldDeserializeCreditParseResponse test
```
Expected:
- FAIL提示 `CreditParseResponse``CreditParsePayload` 不存在。
- [ ] **Step 3: 编写最小响应对象**
`CreditParseResponse.java` 中实现:
```java
@Data
public class CreditParseResponse {
private String message;
@JsonProperty("status_code")
private String statusCode;
private CreditParsePayload payload;
}
```
`CreditParsePayload.java` 中实现:
```java
@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;
}
```
- [ ] **Step 4: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest#shouldDeserializeCreditParseResponse test
```
Expected:
- PASS
- [ ] **Step 5: 提交响应对象**
```bash
git add 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
git commit -m "新增征信解析响应对象"
```
## Task 3: 实现 `CreditParseClient`
**Files:**
- Create: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/CreditParseClient.java`
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
- Modify: `ruoyi-admin/src/main/resources/application-nas.yml`
- Test: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java`
- [ ] **Step 1: 先写 `Client` 成功用例**
`CreditParseClientTest` 中用 Mockito 模拟 `HttpUtil`
```java
@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));
}
```
- [ ] **Step 2: 再写 `Client` 异常传播用例**
```java
@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")));
}
```
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest test
```
Expected:
- FAIL提示 `CreditParseClient` 不存在。
- [ ] **Step 4: 实现 `CreditParseClient`**
`CreditParseClient.java` 中实现最小调用逻辑:
```java
@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) {
Map<String, Object> params = new HashMap<>();
params.put("model", model);
params.put("hType", hType);
params.put("file", file);
return httpUtil.uploadFile(creditParseUrl, params, null, CreditParseResponse.class);
}
}
```
补充日志和异常包装,保持 `LsfxAnalysisClient` 风格:
- 请求开始记录文件名、`model``hType`
- 请求结束记录耗时和 `statusCode`
- 异常时抛 `LsfxApiException("征信解析调用失败: ...", e)`
- [ ] **Step 5: 增加独立配置**
`ruoyi-admin/src/main/resources/application-dev.yml``ruoyi-admin/src/main/resources/application-nas.yml` 增加:
```yml
credit-parse:
api:
url: http://64.202.94.120:8081/xfeature-mngs/conversation/htmlEval
```
- [ ] **Step 6: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseClientTest test
```
Expected:
- PASS
- [ ] **Step 7: 提交 `Client` 与配置**
```bash
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/CreditParseClient.java ruoyi-admin/src/main/resources/application-dev.yml ruoyi-admin/src/main/resources/application-nas.yml ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/CreditParseClientTest.java
git commit -m "新增征信解析客户端"
```
## Task 4: 实现 `CreditParseController`
**Files:**
- Create: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/CreditParseController.java`
- Test: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java`
- [ ] **Step 1: 先写控制器成功透传用例**
```java
@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"));
}
```
- [ ] **Step 2: 再写异常兜底用例**
```java
@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"));
}
```
- [ ] **Step 3: 运行测试确认失败**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test
```
Expected:
- FAIL提示 `CreditParseController` 不存在。
- [ ] **Step 4: 编写控制器最小实现**
控制器结构按 `LsfxTestController` 风格实现:
```java
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
@Anonymous
@RestController
@RequestMapping("/lsfx/credit")
public class CreditParseController {
@Resource
private CreditParseClient creditParseClient;
@PostMapping("/parse")
public AjaxResult parse(@RequestParam("file") MultipartFile file,
@RequestParam(required = false) String model,
@RequestParam(required = false) String hType) {
// 参数校验
// 默认值补齐
// 临时文件转换
// 调用 client
// finally 删除临时文件
}
}
```
校验规则固定为:
- `file == null``file.isEmpty()` -> `AjaxResult.error("征信HTML文件不能为空")`
- `originalFilename` 为空 -> `AjaxResult.error("文件名不能为空")`
- 不是 `.html`/`.htm` -> `AjaxResult.error("仅支持 HTML 格式文件")`
- `model` 为空 -> `LXCUSTALL`
- `hType` 为空 -> `PERSON`
- [ ] **Step 5: 运行测试确认通过**
Run:
```bash
mvn -pl ccdi-lsfx -Dtest=CreditParseControllerTest test
```
Expected:
- PASS
- [ ] **Step 6: 提交控制器**
```bash
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/CreditParseController.java ccdi-lsfx/src/test/java/com/ruoyi/lsfx/controller/CreditParseControllerTest.java
git commit -m "新增征信解析联调接口"
```
## Task 5: 做模块级回归验证
**Files:**
- Modify: `docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md`
- [ ] **Step 1: 运行 `ccdi-lsfx` 测试**
Run:
```bash
mvn -pl ccdi-lsfx test
```
Expected:
- `CreditParseClientTest` PASS
- `CreditParseControllerTest` PASS
- [ ] **Step 2: 运行模块编译验证**
Run:
```bash
mvn -pl ccdi-lsfx -am compile
```
Expected:
- BUILD SUCCESS
- [ ] **Step 3: 手工联调检查 Swagger 与接口**
Run:
```bash
mvn -pl ruoyi-admin -am spring-boot:run
```
打开:
- `http://localhost:62318/swagger-ui.html`
检查:
- 存在 `POST /lsfx/credit/parse`
- 上传 `.html` 文件时能返回 `AjaxResult`
测试完成后停止进程。
- [ ] **Step 4: 记录实施结果**
`docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md` 写入:
- 实际修改文件列表
- 执行的测试命令
- 测试结果
- 是否完成 Swagger 联调
建议结构:
```md
# 征信解析客户端实施记录
## 1. 改动概述
## 2. 修改文件
## 3. 测试记录
## 4. 结果说明
```
- [ ] **Step 5: 提交验证与实施记录**
```bash
git add docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md
git commit -m "补充征信解析客户端实施记录"
```
## Task 6: 最终整理
**Files:**
- Modify: `docs/design/2026-03-23-credit-parse-client-design.md`
- Modify: `docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md`
- [ ] **Step 1: 回看设计与实现是否一致**
逐项核对:
- 独立 `CreditParseClient`
- 独立配置 `credit-parse.api.url`
- 接口路径 `POST /lsfx/credit/parse`
- 不接入 `ccdi-project`
- 不落库
- [ ] **Step 2: 如果实现偏差,更新设计或实施记录**
只允许修正文档,不允许在这个阶段临时扩需求。
- [ ] **Step 3: 检查暂存区只包含本次任务相关文件**
Run:
```bash
git status --short
git diff --cached --name-only
```
Expected:
- 暂存区仅包含本次征信解析相关代码与文档
- [ ] **Step 4: 最终提交**
```bash
git add 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 docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md
git commit -m "完成征信解析客户端后端实现"
```
## Review Notes
- 由于当前仓库协作约定要求“不开启 subagent”本计划不执行 `writing-plans` 技能中的子代理审阅环节。
- 实施时如果发现外部接口真实返回结构与说明书不一致,应先修正响应映射,再更新实施记录,不要额外扩展业务逻辑。