# 上传流水文件原始文件名保持设计

## 背景

项目详情的“上传数据”页支持批量上传流水文件。当前前端提交文件时保留了浏览器选择文件的原始文件名，但后端为了异步处理，会先把文件保存为带有批次号、序号和时间戳的临时文件名。异步任务再把这个临时文件对象转传给流水分析平台，导致流水分析平台上传接口收到的 multipart 文件名不是用户初始上传的文件名。

同时，后端在查询流水分析平台文件状态后，会用平台返回的 `uploadFileName` 或 `downloadFileName` 覆盖本系统上传记录 `ccdi_file_upload_record.file_name`，页面列表读取该字段展示文件名，因此页面展示名也可能与初始上传文件名不一致。

## 目标

- 新上传流水文件时，页面展示的文件名必须保持用户初始上传文件名。
- 调用流水分析平台上传文件接口时，multipart 文件 part 的 filename 必须保持用户初始上传文件名。
- 临时文件仍需保持唯一命名，避免批量上传、同名文件或并发上传互相覆盖。
- 本次只处理“上传本地流水文件”链路，不改变“拉取本行信息”链路的现有文件名展示与状态处理行为。
- 历史已上传记录不做批量回改。

## 非目标

- 不修改历史上传记录的文件名。
- 不调整上传入口、文件格式限制、文件大小限制、异步任务调度和解析轮询规则。
- 不新增数据库字段。
- 不改变流水分析平台接口地址、请求参数名和响应解析口径。
- 不改变“拉取本行信息”生成的上传记录文件名规则。

## 现状链路

1. 前端 `UploadData.vue` 将 `selectedFiles.map((f) => f.raw)` 传给 `batchUploadFiles`。
2. `ccdiProjectUpload.js` 使用 `FormData.append("files", file)` 上传，浏览器侧仍保留原始文件名。
3. `CcdiFileUploadController.batchUpload` 通过 `MultipartFile.getOriginalFilename()` 校验格式。
4. `CcdiFileUploadServiceImpl.batchUploadFiles` 保存记录时写入原始文件名，但临时文件路径使用 `batchId_index_timestamp_originalFilename`。
5. `processFileAsync` 读取临时文件并调用 `lsfxClient.uploadFile(lsfxProjectId, file)`。
6. `LsfxAnalysisClient.uploadFile` 和 `HttpUtil.uploadFile` 只接收 `File`，最终用 `FileSystemResource` 发送文件，multipart filename 来自临时文件名。
7. 查询文件上传状态后，`processRecordAfterLogIdReady` 使用平台返回文件名覆盖 `record.fileName`。

## 设计方案

### 后端上传转发

保留当前临时文件唯一命名方式，避免同名文件覆盖。异步处理时以 `CcdiFileUploadRecord.fileName` 作为原始文件名来源，将“临时文件内容”和“原始文件名”一起传给流水分析客户端。

`LsfxAnalysisClient.uploadFile` 增加可指定上传文件名的能力。对项目上传流水链路，调用新签名并传入原始文件名；测试 Controller 或其他调用方如果不传原始文件名，可继续使用现有文件名语义。

`HttpUtil.uploadFile` 增加对“文件内容 + 指定 filename”的 multipart 资源包装。发送时仍使用参数名 `files`，但文件 part 的 filename 使用原始文件名，而不是临时文件名。

### 上传记录文件名

“上传本地流水文件”链路中的 `ccdi_file_upload_record.file_name` 只记录本系统初始上传文件名。查询流水分析平台状态后，该链路不再用 `uploadFileName` 或 `downloadFileName` 覆盖该字段。

平台返回的文件大小仍可继续更新到 `file_size`，解析状态、主体名称、账号、错误信息等字段保持现有处理方式。

当前状态后处理逻辑会被本地上传和“拉取本行信息”复用。实现时需要在调用或方法参数上区分来源：本地上传链路保留初始文件名；拉取本行信息链路保持现有行为，继续按当前规则处理平台返回文件名。

### 前端展示

前端无需改造。上传记录列表继续展示后端返回的 `fileName`。由于本地上传链路后端不再覆盖该字段，新上传记录从创建、处理中、成功或失败状态都会展示初始上传文件名。

## 数据流

```text
用户选择文件: 银行流水A.xlsx
  -> 前端 FormData files: filename=银行流水A.xlsx
  -> 后端 MultipartFile originalFilename=银行流水A.xlsx
  -> 本地临时文件: batchId_0_timestamp_银行流水A.xlsx
  -> 上传记录 file_name=银行流水A.xlsx
  -> 流水分析平台 multipart files: filename=银行流水A.xlsx
  -> 页面上传记录 fileName=银行流水A.xlsx
```

## 错误处理

- 原始文件名为空时，沿用 Controller 现有“文件名不能为空”校验。
- 临时文件不存在、上传失败、解析失败时，沿用现有失败状态和错误信息记录方式。
- 指定原始文件名只影响 multipart filename，不影响临时文件读取和异常处理。

## 测试计划

1. 后端单测或轻量验证覆盖 multipart 资源 filename：传入临时文件和原始文件名后，上传请求中的文件名应为原始文件名。
2. 后端链路测试覆盖 `CcdiFileUploadServiceImpl`：创建上传记录后，异步上传调用使用 `record.fileName` 作为原始文件名。
3. 状态处理测试覆盖平台返回 `uploadFileName/downloadFileName` 与初始文件名不一致时，本系统记录仍保持初始文件名。
4. 拉取本行信息链路回归：本次改动不改变其现有文件名展示与状态处理行为。
5. 真实页面测试：在项目详情“上传数据”页上传一个文件名带中文的流水文件，核对页面上传记录展示初始文件名，并通过日志或 mock 接收端确认流水分析平台上传接口收到的 filename 为初始文件名。

## 影响范围

- `ccdi-project`：上传流水异步处理链路。
- `ccdi-lsfx`：流水分析平台上传客户端和 multipart 工具。
- `ruoyi-ui`：无需源码改动，仅做真实页面验证。
- 数据库：无需结构变更，历史数据不回改。
