Files
ccdi/docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement.md

11 KiB
Raw Blame History

流水文件解析成功状态延后到流水入库完成 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

示例骨架:

@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:

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

@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:

mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest

Expected:

  • FAIL
  • 至少一条断言失败,证明当前实现不符合新设计

Step 5: Commit

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 内新增私有静态结果类:

@Data
private static class FetchBankStatementResult {
    private boolean success;
    private int totalCount;
    private int savedCount;
    private String errorMessage;
}

Step 2: Change the fetch method signature

把:

private void fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)

改为:

private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)

Step 3: Return explicit failure instead of swallowing exceptions

把当前“记录错误后继续下一页”的逻辑改成显式失败返回。例如:

catch (Exception e) {
    result.setSuccess(false);
    result.setErrorMessage("获取或保存流水数据失败: " + e.getMessage());
    return result;
}

Step 4: Delay parsed_success update until the fetch result succeeds

processFileAsync(...) 中当前这段提前成功逻辑:

record.setFileStatus("parsed_success");
record.setEnterpriseNames(enterpriseNamesStr);
record.setAccountNos(accountNosStr);
recordMapper.updateById(record);

fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);

改成:

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,返回成功结果:

result.setSuccess(true);
result.setTotalCount(0);
result.setSavedCount(0);
return result;

Step 6: Run targeted tests

Run:

mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest

Expected:

  • PASS
  • 新增的状态延后测试全部通过

Step 7: Commit

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

在流水转换循环内补齐:

statement.setProjectId(projectId);
statement.setBatchId(logId);

Step 2: Add the cleanup mapper method

CcdiBankStatementMapper.java 中新增:

int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
                                @Param("batchId") Integer batchId);

并在 XML 中实现:

<delete id="deleteByProjectIdAndBatchId">
    delete from ccdi_bank_statement
    where project_id = #{projectId}
      and batch_id = #{batchId}
</delete>

Step 3: Call cleanup before returning failure

fetchAndSaveBankStatements(...) 的失败分支中调用:

bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId);

只允许使用 projectId + logId(batchId) 双条件,避免误删其他批次数据。

Step 4: Write a failing cleanup test

在测试类中新增:

@Test
void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() {
    // mock 某页或某批插入失败
    // assert deleteByProjectIdAndBatchId(projectId, logId) 被调用
}

Step 5: Run the new test

Run:

mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails

Expected:

  • 先 FAIL再在实现后 PASS

Step 6: Run the module tests

Run:

mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest

Expected:

  • PASS
  • 旧的 CcdiBankStatementTest 不回归

Step 7: Commit

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:

mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest

Expected:

  • PASS
  • 无编译错误

Step 2: Inspect git diff

Run:

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

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 相关测试通过