# 流水文件解析成功状态延后到流水入库完成 Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 让流水文件上传记录只有在步骤 7 获取并保存流水数据成功后才更新为 `parsed_success`,在此之前继续显示 `parsing`。 **Architecture:** 重构 `CcdiFileUploadServiceImpl` 的步骤 7,使其返回结构化执行结果而不是吞异常;主流程基于该结果决定最终状态。使用 `ccdi_bank_statement.batch_id` 绑定本次上传 `logId`,在步骤 7 失败时通过 Mapper 补偿删除本次已写入流水,避免半成品数据残留。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito(来自 `spring-boot-starter-test`) --- ### Task 1: 为状态延后规则编写服务层失败测试 **Files:** - Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:340-619` **Step 1: Write the failing test** 在新测试类中先写“平台解析成功但步骤 7 失败时,记录最终为 `parsed_failed`”的测试。使用 Mockito mock 以下依赖: - `LsfxAnalysisClient` - `CcdiFileUploadRecordMapper` - `CcdiBankStatementMapper` 示例骨架: ```java @ExtendWith(MockitoExtension.class) class CcdiFileUploadServiceImplTest { @InjectMocks private CcdiFileUploadServiceImpl service; @Mock private CcdiFileUploadRecordMapper recordMapper; @Mock private CcdiProjectMapper projectMapper; @Mock private LsfxAnalysisClient lsfxClient; @Mock private CcdiBankStatementMapper bankStatementMapper; @Test void processFileAsync_shouldKeepParsingUntilBankStatementsSaved() { // arrange // mock 上传成功、轮询完成、状态接口解析成功 // mock getBankStatement 首次调用抛异常 // act // assert verify(recordMapper, never()).updateById(argThat(record -> "parsed_success".equals(record.getFileStatus()))); verify(recordMapper).updateById(argThat(record -> "parsed_failed".equals(record.getFileStatus()))); } } ``` **Step 2: Run test to verify it fails** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldKeepParsingUntilBankStatementsSaved ``` Expected: - FAIL - 失败原因应体现当前实现会先更新 `parsed_success`,或测试类尚未编译通过 **Step 3: Write a second failing test for the success path** 补一条成功路径测试,验证步骤 7 成功后才更新为 `parsed_success`: ```java @Test void processFileAsync_shouldMarkSuccessAfterBankStatementsSaved() { // mock 上传成功、解析成功、getBankStatement 返回 totalCount=0 // 执行后应只在步骤7完成后出现 parsed_success verify(recordMapper).updateById(argThat(record -> "parsed_success".equals(record.getFileStatus()))); } ``` **Step 4: Run both tests to verify they fail** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest ``` Expected: - FAIL - 至少一条断言失败,证明当前实现不符合新设计 **Step 5: Commit** ```bash git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "test(ccdi-project): add file upload status transition tests" ``` ### Task 2: 重构步骤 7 返回结果对象并延后成功状态更新 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:340-619` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Add a result object inside the service** 在 `CcdiFileUploadServiceImpl` 内新增私有静态结果类: ```java @Data private static class FetchBankStatementResult { private boolean success; private int totalCount; private int savedCount; private String errorMessage; } ``` **Step 2: Change the fetch method signature** 把: ```java private void fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId) ``` 改为: ```java private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId) ``` **Step 3: Return explicit failure instead of swallowing exceptions** 把当前“记录错误后继续下一页”的逻辑改成显式失败返回。例如: ```java catch (Exception e) { result.setSuccess(false); result.setErrorMessage("获取或保存流水数据失败: " + e.getMessage()); return result; } ``` **Step 4: Delay `parsed_success` update until the fetch result succeeds** 把 `processFileAsync(...)` 中当前这段提前成功逻辑: ```java record.setFileStatus("parsed_success"); record.setEnterpriseNames(enterpriseNamesStr); record.setAccountNos(accountNosStr); recordMapper.updateById(record); fetchAndSaveBankStatements(projectId, lsfxProjectId, logId); ``` 改成: ```java FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId); if (!fetchResult.isSuccess()) { record.setFileStatus("parsed_failed"); record.setErrorMessage(fetchResult.getErrorMessage()); recordMapper.updateById(record); return; } record.setFileStatus("parsed_success"); record.setEnterpriseNames(enterpriseNamesStr); record.setAccountNos(accountNosStr); record.setErrorMessage(null); recordMapper.updateById(record); ``` **Step 5: Handle the zero-data path explicitly** 在首次总数查询后,若 `totalCount == null || totalCount <= 0`,返回成功结果: ```java result.setSuccess(true); result.setTotalCount(0); result.setSavedCount(0); return result; ``` **Step 6: Run targeted tests** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest ``` Expected: - PASS - 新增的状态延后测试全部通过 **Step 7: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "refactor(ccdi-project): delay parsed success until bank statements saved" ``` ### Task 3: 为本次上传绑定 batchId 并补偿清理半成品流水 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:527-619` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java:15-23` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml:62-87` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Attach the upload logId to each statement** 在流水转换循环内补齐: ```java statement.setProjectId(projectId); statement.setBatchId(logId); ``` **Step 2: Add the cleanup mapper method** 在 `CcdiBankStatementMapper.java` 中新增: ```java int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId, @Param("batchId") Integer batchId); ``` 并在 XML 中实现: ```xml delete from ccdi_bank_statement where project_id = #{projectId} and batch_id = #{batchId} ``` **Step 3: Call cleanup before returning failure** 在 `fetchAndSaveBankStatements(...)` 的失败分支中调用: ```java bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId); ``` 只允许使用 `projectId + logId(batchId)` 双条件,避免误删其他批次数据。 **Step 4: Write a failing cleanup test** 在测试类中新增: ```java @Test void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() { // mock 某页或某批插入失败 // assert deleteByProjectIdAndBatchId(projectId, logId) 被调用 } ``` **Step 5: Run the new test** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails ``` Expected: - 先 FAIL,再在实现后 PASS **Step 6: Run the module tests** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest ``` Expected: - PASS - 旧的 `CcdiBankStatementTest` 不回归 **Step 7: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "fix(ccdi-project): cleanup partial bank statements on upload failure" ``` ### Task 4: 回归验证并整理交付 **Files:** - Modify: `docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement-design.md` - Modify: `docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement.md` **Step 1: Run final verification** Run: ```bash mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest ``` Expected: - PASS - 无编译错误 **Step 2: Inspect git diff** Run: ```bash git diff -- ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ``` Expected: - 只包含状态时机调整、结果对象、清理接口和测试 **Step 3: Update docs if implementation deviates** 若实现中出现与设计或计划不一致的细节,及时回写到这两份文档,避免文档失真。 **Step 4: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement-design.md docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement.md git commit -m "docs: finalize file upload parse success timing plan" ``` --- ## Implementation Checklist - [ ] 服务测试已覆盖“成功延后”和“步骤 7 失败”场景 - [ ] `fetchAndSaveBankStatements(...)` 改为返回结构化结果 - [ ] 步骤 7 完成前记录状态保持 `parsing` - [ ] 步骤 7 成功后才更新 `parsed_success` - [ ] 步骤 7 失败后更新 `parsed_failed` - [ ] 本次 `logId` 对应流水写入 `batch_id` - [ ] 步骤 7 失败时清理本次半成品流水 - [ ] `totalCount = 0` 场景按成功处理 - [ ] `ccdi-project` 相关测试通过