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