363 lines
11 KiB
Markdown
363 lines
11 KiB
Markdown
|
|
# 流水文件解析成功状态延后到流水入库完成 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` 相关测试通过
|