diff --git a/docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md b/docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md new file mode 100644 index 00000000..12820ed6 --- /dev/null +++ b/docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md @@ -0,0 +1,554 @@ +# 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` 技能中的子代理审阅环节。 +- 实施时如果发现外部接口真实返回结构与说明书不一致,应先修正响应映射,再更新实施记录,不要额外扩展业务逻辑。 diff --git a/docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md b/docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md new file mode 100644 index 00000000..7caccf21 --- /dev/null +++ b/docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md @@ -0,0 +1,152 @@ +# Credit Parse Client Frontend 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:** 确认本次征信解析客户端建设不涉及前端代码改动,并输出可执行的前端协作与验收计划,避免后续联调时误改前端现有上传链路。 + +**Architecture:** 本次需求边界限定在 `ccdi-lsfx` 后端模块:新增独立征信解析 `Client`、独立配置和联调接口,不接入 `ccdi-project` 现有上传流程,也不新增前端页面或 API 封装。因此前端计划以“确认无改动、保留联调信息、避免误接入”为主。 + +**Tech Stack:** Vue 2, Axios request 封装, 若依前端工程结构, Markdown 文档 + +--- + +## 文件结构与职责 + +**本次不修改的前端文件** + +- `ruoyi-ui/src/api/ccdiProjectUpload.js` +- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` + +**新增文件** + +- `docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md` + 在后端实施完成后,由同一次改动统一补充实施记录,其中需明确本次前端无代码改动。 + +**参考文件** + +- `docs/design/2026-03-23-credit-parse-client-design.md` +- `docs/plans/backend/2026-03-23-credit-parse-client-backend-implementation.md` +- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` + +## Task 1: 确认前端边界 + +**Files:** +- Review: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` +- Review: `ruoyi-ui/src/api/ccdiProjectUpload.js` + +- [ ] **Step 1: 核对设计边界** + +确认以下结论成立: + +- 新接口落在 `ccdi-lsfx` +- 调用入口为 `POST /lsfx/credit/parse` +- 不接入项目上传主链路 +- 不新增前端上传卡片行为 +- 不新增前端 API 文件 + +- [ ] **Step 2: 检查现有前端征信入口不做误修改** + +查看 `UploadData.vue` 中现有征信卡片定义,确认本次不改: + +```js +{ + key: "credit", + title: "征信导入", + desc: "支持 HTML 格式征信数据解析", + icon: "el-icon-s-data", + btnText: "上传征信", + uploaded: false, + disabled: true, +} +``` + +- [ ] **Step 3: 记录前端不改动结论** + +在实施记录中预留一段文字: + +```md +## 前端影响说明 + +本次需求仅在 `ccdi-lsfx` 后端模块新增征信解析 Client 与联调接口,前端未做代码改动。 +``` + +- [ ] **Step 4: 提交边界确认** + +```bash +git add docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md +git commit -m "新增征信解析客户端前端实施计划" +``` + +## Task 2: 后端联调验收配合 + +**Files:** +- Review: `docs/reports/implementation/2026-03-23-credit-parse-client-implementation.md` + +- [ ] **Step 1: 后端完成后校验接口契约** + +核对后端实际输出是否满足以下约定: + +- `AjaxResult.code = 200` +- `AjaxResult.data.message` +- `AjaxResult.data.status_code` +- `AjaxResult.data.payload.lx_header` +- `AjaxResult.data.payload.lx_debt` +- `AjaxResult.data.payload.lx_publictype` + +- [ ] **Step 2: 确认无前端阻塞项** + +若没有以下情况,则维持前端零改动: + +- 需要在项目页面挂接新按钮 +- 需要新增前端 API 封装 +- 需要前端解析 `payload` 并渲染页面 + +若出现上述新需求,应单独发起新的设计和计划,不在本次实施中顺带处理。 + +- [ ] **Step 3: 在实施记录中写明前端验收结论** + +补充: + +```md +## 前端验收结论 + +- 本次无前端代码改动 +- 后端接口契约已形成,可供后续独立前端联调使用 +``` + +## Task 3: 最终检查 + +**Files:** +- Modify: `docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md` + +- [ ] **Step 1: 检查工作区无前端源码改动** + +Run: + +```bash +git status --short ruoyi-ui +``` + +Expected: + +- 无 `ruoyi-ui` 目录下源码改动,或只有用户明确要求的相关改动 + +- [ ] **Step 2: 检查计划与设计一致** + +确认没有出现以下偏移: + +- 把征信解析顺手接入前端上传卡片 +- 顺手新增前端 API 封装 +- 顺手调整项目上传状态逻辑 + +- [ ] **Step 3: 最终提交** + +```bash +git add docs/plans/frontend/2026-03-23-credit-parse-client-frontend-implementation.md +git commit -m "补充征信解析客户端前端实施说明" +``` + +## Review Notes + +- 本计划明确为“前端零代码改动计划”,目的是固化边界,避免后续联调时无意改动现有项目上传前端链路。 +- 由于当前协作约定禁止开启 subagent,本计划不执行子代理审阅环节。