# 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` 保存字段。 - `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 org.springframework.boot spring-boot-starter-test test ``` - [ ] **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 lxHeader; @JsonProperty("lx_debt") private Map lxDebt; @JsonProperty("lx_publictype") private Map 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 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", "".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", "".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` 技能中的子代理审阅环节。 - 实施时如果发现外部接口真实返回结构与说明书不一致,应先修正响应映射,再更新实施记录,不要额外扩展业务逻辑。