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

363 lines
11 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.
# 流水文件解析成功状态延后到流水入库完成 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 id="deleteByProjectIdAndBatchId">
delete from ccdi_bank_statement
where project_id = #{projectId}
and batch_id = #{batchId}
</delete>
```
**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` 相关测试通过