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

264 lines
8.3 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.
# 流水文件解析成功状态延后到流水入库完成设计
## 概述
调整流水文件上传异步处理链路中“解析成功”的业务含义。
当前实现里,只要流水分析平台返回“解析成功且可确认账户”,系统就会立即把上传记录状态更新为 `parsed_success`,随后才执行步骤 7 获取流水数据并写入本地数据库。
本次设计将 `parsed_success` 的含义收紧为:
- 流水分析平台解析成功
- 步骤 7 获取流水数据成功
- 流水数据成功写入本地数据库
在步骤 7 完成前,页面继续显示“解析中”。
## 背景
### 当前现状
当前核心逻辑位于 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
1. 上传文件到流水分析平台
2. 轮询解析状态
3. 调用上传状态接口判断是否解析成功
4. 立即更新 `ccdi_file_upload_record.file_status = parsed_success`
5. 再调用 `fetchAndSaveBankStatements(...)` 获取流水并入库
这会产生两个问题:
1. 前端会看到“解析成功”,但数据库里的流水可能还没有写完
2. 步骤 7 失败时,记录可能先显示成功,随后又因为异常被改成失败,状态语义不稳定
### 前端约束
当前前端只识别以下四种状态:
- `uploading`
- `parsing`
- `parsed_success`
- `parsed_failed`
本次需求明确要求:
- 不新增前端状态
- 当步骤 6 已经确认平台解析成功,但步骤 7 尚未完成时,页面继续显示“解析中”
## 方案对比
### 方案一:仅后移 `parsed_success` 更新时机
做法:
- 步骤 6 解析成功后,不更新状态
- 执行步骤 7
- 步骤 7 执行结束后,再更新为 `parsed_success`
优点:
- 改动最小
- 前端和数据库状态枚举都不需要调整
缺点:
- 当前 `fetchAndSaveBankStatements(...)` 没有显式返回成功或失败结果
- 方法内部存在“记录异常后继续处理”的行为,容易把部分失败误判为成功
### 方案二:后移成功状态,并让步骤 7 返回明确执行结果
做法:
- 步骤 6 只确认“平台解析成功且可以获取流水”
- 记录状态继续保持 `parsing`
- 步骤 7 返回结构化结果,例如 `success``savedCount``errorMessage`
- 只有步骤 7 明确成功后,才更新 `parsed_success`
- 步骤 7 任一关键失败,则更新为 `parsed_failed`
优点:
- 状态语义完整且稳定
- 能避免“伪成功”
- 与当前前端状态模型兼容
缺点:
- 需要对步骤 7 做一定重构
### 方案三:拆分为解析状态和入库状态两个维度
做法:
- 新增“解析状态”和“入库状态”两个字段
- 前端组合展示
优点:
- 状态表达最完整
缺点:
- 涉及数据库、后端查询统计、前端状态映射等多处改动
- 超出本次需求范围
## 最终方案
采用方案二。
### 核心决策
1. `parsed_success` 只表示“流水数据已经成功入库”
2. 步骤 6 解析成功后,记录状态继续保持 `parsing`
3. 步骤 7 必须显式返回成功或失败结果
4. 步骤 7 失败时,将上传记录更新为 `parsed_failed`
5. 步骤 7 失败时,清理本次 `logId` 对应的已落库流水,避免半成品数据残留
## 详细设计
### 1. 主流程状态流转
调整 `processFileAsync(...)` 的状态流转如下:
1. 初始创建记录时为 `uploading`
2. 文件上传到流水分析平台成功后,更新为 `parsing`
3. 轮询解析完成
4. 调用文件上传状态接口判断平台是否解析成功
5. 若平台解析失败,更新为 `parsed_failed`
6. 若平台解析成功,不更新为 `parsed_success`,继续保持 `parsing`
7. 执行步骤 7 获取流水并入库
8. 步骤 7 成功后,一次性更新:
- `file_status = parsed_success`
- `enterprise_names`
- `account_nos`
- 清空可能残留的 `error_message`
9. 步骤 7 失败后,更新:
- `file_status = parsed_failed`
- `error_message = 失败原因`
### 2. 步骤 7 返回结构化结果
`fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)``void` 改为返回结构化结果对象。
建议新增内部结果对象,例如:
```java
@Data
private static class FetchBankStatementResult {
private boolean success;
private int totalCount;
private int savedCount;
private String errorMessage;
}
```
返回语义建议如下:
- `success = true`
- 已成功完成全部分页拉取和数据库落库
- `savedCount` 为实际保存条数
- `success = false`
- 任一关键步骤失败
- `errorMessage` 写明失败原因
### 3. 步骤 7 的成功判定
步骤 7 需同时满足以下条件才算成功:
1. 首次 `getBankStatement` 请求成功返回
2. 分页总数计算正常
3. 所有分页请求成功完成
4. 所有批量插入操作成功完成
5. 最终保存条数与已拉取条数一致
其中 `totalCount = 0` 的场景按成功处理,原因如下:
- 平台已经解析成功
- 业务上允许“解析成功但无流水”
- 否则记录会长期停留在 `parsing` 或被错误标记为失败
### 4. 步骤 7 的失败处理
当前实现中,分页循环内部发生异常后会记录日志并继续下一页。该行为不适用于本次状态语义。
调整后规则:
1. 首次查询总数失败,直接返回失败
2. 任一分页请求失败,直接返回失败
3. 任一批量插入失败,直接返回失败
4. 返回失败前,清理当前 `logId` 已写入的流水数据
### 5. 半成品流水清理
`ccdi_bank_statement` 已存在 `batch_id` 字段,且当前实体 `CcdiBankStatement.batchId` 已映射该字段。
因此步骤 7 中应确保每条流水都带上本次上传的 `logId`
```java
statement.setProjectId(projectId);
statement.setBatchId(logId);
```
同时在 `CcdiBankStatementMapper` 中新增清理接口,例如:
```java
int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
@Param("batchId") Integer batchId);
```
用于在步骤 7 失败时删除本次已插入的流水,避免出现“部分落库但上传记录失败”的脏数据。
### 6. 为什么不使用长事务
不建议把步骤 7 做成覆盖远程接口调用和全部分页落库的单个数据库事务,原因如下:
1. 远程接口调用时间不可控
2. 全量分页获取可能持续较久
3. 长事务会占用数据库连接并增加锁持有时间
因此本次采用“显式成功判定 + 失败补偿清理”的方式,而不是“长事务回滚”。
## 影响范围
### 后端
- `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`
### 前端
无须改动。
当前前端的 `parsing` 状态即可承载“平台解析成功但流水尚未入库完成”的阶段。
## 测试设计
至少覆盖以下场景:
1. 平台解析成功,步骤 7 全量拉取并入库成功,最终状态应为 `parsed_success`
2. 平台解析成功,但首次获取流水总数失败,最终状态应为 `parsed_failed`
3. 分页处理中途失败,最终状态应为 `parsed_failed`,且已写入流水被清理
4. 批量插入失败,最终状态应为 `parsed_failed`,且已写入流水被清理
5. `totalCount = 0`,最终状态应为 `parsed_success`
6. 平台解析失败,保持现有失败路径
## 风险与缓解
| 风险 | 影响 | 缓解方案 |
|------|------|----------|
| 步骤 7 重构后改变现有异常处理行为 | 中 | 使用单元测试锁定成功、失败、零数据三类分支 |
| 清理逻辑误删其他流水 | 高 | 删除条件必须同时绑定 `projectId``batchId(logId)` |
| 失败原因不清晰 | 中 | 统一由步骤 7 返回明确 `errorMessage`,最终写入 `ccdi_file_upload_record.error_message` |
## 验收标准
1. 当流水平台解析成功但本地仍在入库时,上传记录保持 `parsing`
2. 只有本地流水入库完成后,上传记录才变为 `parsed_success`
3. 任一步骤 7 失败,上传记录为 `parsed_failed`
4. 步骤 7 失败后,不残留本次 `logId` 的半成品流水
5. 前端无需新增状态,现有页面展示符合预期