# 流水文件解析成功状态延后到流水入库完成设计

## 概述

调整流水文件上传异步处理链路中“解析成功”的业务含义。

当前实现里，只要流水分析平台返回“解析成功且可确认账户”，系统就会立即把上传记录状态更新为 `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. 前端无需新增状态，现有页面展示符合预期
