# Bank Upload Original Filename Implementation Plan

> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow the CCDI project rule: unless the user explicitly declares `using-superpowers` for implementation, execute this plan through the ordinary workflow and do not enable subagents by default.

**Goal:** 上传本地流水文件后，页面记录名和转传流水分析平台 multipart 文件名都保持用户初始上传文件名。

**Architecture:** 后端继续使用唯一临时文件名保存文件，避免同名和并发冲突；上传流水分析平台时额外传入原始文件名，并用可覆盖 `Resource#getFilename()` 的资源对象构造 multipart 文件 part。上传记录状态后处理增加“是否保留当前记录文件名”的来源参数，本地上传链路保留初始文件名，拉取本行信息链路保持现状。

**Tech Stack:** Java 21, Spring Boot 3, RestTemplate, MyBatis Plus, JUnit 5, Mockito, Vue 2 页面真实验证。

---

## Project Notes

- 设计文档：[docs/superpowers/specs/2026-05-06-bank-upload-original-filename-design.md](/Users/wkc/Desktop/ccdi/ccdi/docs/superpowers/specs/2026-05-06-bank-upload-original-filename-design.md)
- 项目路径规则覆盖 `writing-plans` 默认目录：本次只涉及后端源码，因此实施计划放在 `docs/plans/backend/`。
- 本次不新增前端源码，不新增数据库字段，不回改历史上传记录。
- `.DS_Store` 忽略，不纳入暂存或提交。
- 完成实现后必须新增实施记录：`docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`。
- 真实页面验证使用 @browser-use:browser 打开实际业务页面，不打开 prototype 页面。
- 如果启动了后端、前端或 `lsfx-mock-server`，测试结束后必须关闭本轮启动的进程。

## File Map

- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
  - 新增可指定 multipart filename 的 `NamedFileSystemResource`。
  - `uploadFile` 继续支持 `File` 参数；当参数已经是 `Resource` 时直接加入 multipart body。
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
  - 新增 `uploadFile(Integer groupId, File file, String uploadFileName)`。
  - 现有 `uploadFile(Integer groupId, File file)` 委托到新方法并使用 `file.getName()`。
  - 项目上传链路使用新方法传入初始上传文件名。
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java`
  - 验证 multipart body 中 `files` 资源的 `filename` 可被显式指定。
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java`
  - 验证流水分析客户端上传时把指定原始文件名写入 `files` 参数。
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
  - `processFileAsync` 调用 `lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName())`。
  - `processRecordAfterLogIdReady` 增加来源控制参数，本地上传不覆盖 `record.fileName`，拉取本行信息保持现有覆盖行为。
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
  - 更新现有 `processFileAsync` 上传 stubbing 到三参数签名。
  - 新增本地上传使用原始文件名转传、平台返回名不覆盖上传记录、拉取本行信息保持现状的回归测试。
- Create: `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`
  - 记录修改内容、影响范围、测试命令、真实页面验证和进程清理情况。

## Task 1: Add Failing Tests For Multipart Filename

**Files:**
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java`
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java`

- [ ] **Step 1: Create `HttpUtilTest` with a failing filename assertion**

Add:

```java
package com.ruoyi.lsfx.util;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class HttpUtilTest {

    @Mock
    private RestTemplate restTemplate;

    @TempDir
    Path tempDir;

    @Test
    void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
        HttpUtil httpUtil = new HttpUtil();
        ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);

        Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
        Files.writeString(tempFile, "content");

        ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
        when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
            .thenReturn(ResponseEntity.ok("ok"));

        Map<String, Object> params = new HashMap<>();
        params.put("groupId", 200);
        params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx"));

        String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);

        assertEquals("ok", result);
        MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
        Object filePart = body.getFirst("files");
        Resource resource = assertInstanceOf(Resource.class, filePart);
        assertEquals("银行流水A.xlsx", resource.getFilename());
    }
}
```

- [ ] **Step 2: Create `LsfxAnalysisClientTest` with a failing client assertion**

Add:

```java
package com.ruoyi.lsfx.client;

import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.response.UploadFileResponse;
import com.ruoyi.lsfx.util.HttpUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.test.util.ReflectionTestUtils;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class LsfxAnalysisClientTest {

    @Mock
    private HttpUtil httpUtil;

    @InjectMocks
    private LsfxAnalysisClient client;

    @TempDir
    Path tempDir;

    @Test
    void uploadFile_shouldPassOriginalFilenameToMultipartResource() throws Exception {
        ReflectionTestUtils.setField(client, "baseUrl", "http://lsfx");
        ReflectionTestUtils.setField(client, "uploadFileEndpoint", "/upload");
        ReflectionTestUtils.setField(client, "clientId", "client-1");

        Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
        Files.writeString(tempFile, "content");

        UploadFileResponse response = new UploadFileResponse();
        response.setData(new UploadFileResponse.UploadData());

        ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
        ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
        when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
            .thenReturn(response);

        client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx");

        assertEquals(200, paramsCaptor.getValue().get("groupId"));
        Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
        assertEquals("银行流水A.xlsx", filePart.getFilename());
        assertEquals("client-1", headersCaptor.getValue().get(LsfxConstants.HEADER_CLIENT_ID));
    }
}
```

- [ ] **Step 3: Run tests and confirm they fail**

Run:

```bash
mvn -pl ccdi-lsfx -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest -Dsurefire.failIfNoSpecifiedTests=false test
```

Expected: FAIL because `HttpUtil.namedFileResource(...)` and `LsfxAnalysisClient.uploadFile(Integer, File, String)` do not exist yet.

## Task 2: Implement Multipart Filename Support

**Files:**
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java`
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`

- [ ] **Step 1: Add named file resource to `HttpUtil`**

In `HttpUtil.java`, add only this import:

```java
import org.springframework.util.StringUtils;
```

Do not import `org.springframework.core.io.Resource` in this file because it conflicts with the existing `jakarta.annotation.Resource` annotation import. Use the Spring Resource type by fully qualified name in implementation snippets.

Add this nested class and factory method inside `HttpUtil`:

```java
public static org.springframework.core.io.Resource namedFileResource(File file, String filename) {
    return new NamedFileSystemResource(file, filename);
}

private static class NamedFileSystemResource extends FileSystemResource {
    private final String filename;

    NamedFileSystemResource(File file, String filename) {
        super(file);
        this.filename = StringUtils.hasText(filename) ? filename : file.getName();
    }

    @Override
    public String getFilename() {
        return filename;
    }
}
```

- [ ] **Step 2: Let `HttpUtil.uploadFile` accept existing `Resource` values**

Replace the file branch in `uploadFile(...)` with:

```java
if (value instanceof File) {
    File file = (File) value;
    body.add(key, new FileSystemResource(file));
} else if (value instanceof org.springframework.core.io.Resource) {
    body.add(key, value);
} else {
    body.add(key, value);
}
```

- [ ] **Step 3: Add explicit filename upload overload to `LsfxAnalysisClient`**

Replace the current upload method body with a delegating overload:

```java
public UploadFileResponse uploadFile(Integer groupId, File file) {
    return uploadFile(groupId, file, file.getName());
}

public UploadFileResponse uploadFile(Integer groupId, File file, String uploadFileName) {
    String multipartFileName = org.springframework.util.StringUtils.hasText(uploadFileName)
        ? uploadFileName
        : file.getName();
    log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, multipartFileName);
    long startTime = System.currentTimeMillis();

    try {
        String url = baseUrl + uploadFileEndpoint;

        Map<String, Object> params = new HashMap<>();
        params.put("groupId", groupId);
        params.put("files", HttpUtil.namedFileResource(file, multipartFileName));

        Map<String, String> headers = new HashMap<>();
        headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);

        UploadFileResponse response = httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);

        long elapsed = System.currentTimeMillis() - startTime;
        if (response != null && response.getData() != null) {
            log.info("【流水分析】上传文件成功: uploadStatus={}, 耗时={}ms",
                response.getData().getUploadStatus(), elapsed);
        } else {
            log.warn("【流水分析】上传文件响应异常: 耗时={}ms", elapsed);
        }

        return response;
    } catch (LsfxApiException e) {
        log.error("【流水分析】上传文件失败: groupId={}, error={}", groupId, e.getMessage(), e);
        throw e;
    } catch (Exception e) {
        log.error("【流水分析】上传文件未知异常: groupId={}", groupId, e);
        throw new LsfxApiException("上传文件失败: " + e.getMessage(), e);
    }
}
```

- [ ] **Step 4: Run `ccdi-lsfx` tests**

Run:

```bash
mvn -pl ccdi-lsfx -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CreditParseControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
```

Expected: PASS.

- [ ] **Step 5: Commit lsfx client changes**

Check the worktree, stage only task files, then verify staged scope:

```bash
git status --short
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java \
  ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java \
  ccdi-lsfx/src/test/java/com/ruoyi/lsfx/util/HttpUtilTest.java \
  ccdi-lsfx/src/test/java/com/ruoyi/lsfx/client/LsfxAnalysisClientTest.java
git diff --cached --name-status
```

Expected staged files: only the four `ccdi-lsfx` files in this task.

```bash
git commit -m "修复流水分析上传文件名传递"
```

## Task 3: Add Failing Project Upload Service Tests

**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`

- [ ] **Step 1: Update existing `processFileAsync` stubs to the new signature**

In `CcdiFileUploadServiceImplTest.java`, replace local file upload stubs:

```java
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
```

with:

```java
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
    .thenReturn(buildUploadResponse());
```

Only update tests that exercise `processFileAsync`. Do not change `processPullBankInfoAsync` tests to upload files; that chain uses `fetchInnerFlow`.

- [ ] **Step 2: Add test that local upload calls LSFX with original record filename**

Add:

```java
@Test
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
    when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
        .thenReturn(buildUploadResponse());
    when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
        .thenReturn(buildCheckParseStatusResponse(false));
    when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
    when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
        .thenReturn(buildEmptyBankStatementResponse());

    CcdiFileUploadRecord record = buildRecord();
    record.setFileName("原始流水.xlsx");
    Path tempFile = createTempFile();

    service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);

    verify(lsfxClient).uploadFile(eq(LSFX_PROJECT_ID), argThat(file ->
        file.getName().startsWith("upload-") && file.getName().endsWith(".xlsx")
    ), eq("原始流水.xlsx"));
}
```

- [ ] **Step 3: Add test that platform filename does not overwrite local upload record filename**

Add:

```java
@Test
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
    when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
        .thenReturn(buildUploadResponse());
    when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
        .thenReturn(buildCheckParseStatusResponse(false));
    when(lsfxClient.getFileUploadStatus(any()))
        .thenReturn(buildParsedSuccessStatusResponse("平台返回文件名.xlsx"));
    when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
        .thenReturn(buildEmptyBankStatementResponse());

    CcdiFileUploadRecord record = buildRecord();
    record.setFileName("原始流水.xlsx");
    Path tempFile = createTempFile();

    service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);

    verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
        org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
            "parsed_success".equals(item.getFileStatus())
                && "原始流水.xlsx".equals(item.getFileName()))
    );
}
```

- [ ] **Step 4: Keep pull-bank-info regression explicit**

Keep the existing test `processPullBankInfoAsync_shouldUpdateFileSizeFromStatusResponse` asserting:

```java
"XX身份证.xlsx".equals(item.getFileName())
```

This verifies the shared status method still allows platform filename overwrite for the “拉取本行信息” chain.

- [ ] **Step 5: Add failure-state filename regression**

Add:

```java
@Test
void processFileAsync_shouldKeepOriginalFileNameWhenParseStatusFails() throws IOException {
    GetFileUploadStatusResponse statusResponse = buildParsedSuccessStatusResponse("平台失败文件名.xlsx");
    GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
    logItem.setStatus(-1);
    logItem.setUploadStatusDesc("parse.failed");

    when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
        .thenReturn(buildUploadResponse());
    when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
        .thenReturn(buildCheckParseStatusResponse(false));
    when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);

    CcdiFileUploadRecord record = buildRecord();
    record.setFileName("原始流水.xlsx");
    Path tempFile = createTempFile();

    service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);

    verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
        org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
            "parsed_failed".equals(item.getFileStatus())
                && "原始流水.xlsx".equals(item.getFileName()))
    );
}
```

- [ ] **Step 6: Run service tests and confirm expected failure**

Run:

```bash
mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```

Expected: FAIL because `processFileAsync` still calls the old two-argument upload method and status handling still overwrites `record.fileName`.

## Task 4: Implement Project Upload Filename Isolation

**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`

- [ ] **Step 1: Use original record filename when forwarding local uploaded files**

In `processFileAsync`, replace:

```java
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
```

with:

```java
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
```

- [ ] **Step 2: Add source control to status post-processing**

Add an overload:

```java
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
                                          CcdiFileUploadRecord record, Integer logId) {
    processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
}
```

Change the current method signature to:

```java
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
                                          CcdiFileUploadRecord record, Integer logId,
                                          boolean preserveRecordFileName) {
```

Then replace the filename update block with:

```java
if (!preserveRecordFileName) {
    String fileName = StringUtils.hasText(logItem.getUploadFileName())
        ? logItem.getUploadFileName()
        : logItem.getDownloadFileName();
    if (StringUtils.hasText(fileName)) {
        record.setFileName(fileName);
    }
}
```

- [ ] **Step 3: Call status post-processing with preservation from local upload**

In `processFileAsync`, replace:

```java
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
```

with:

```java
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
```

Do not change `processPullBankInfoAsync`; it should continue to call the four-argument overload and preserve the current pull-bank-info behavior.

- [ ] **Step 4: Run service tests**

Run:

```bash
mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```

Expected: PASS.

- [ ] **Step 5: Commit project upload service changes**

Check the worktree, stage only task files, then verify staged scope:

```bash
git status --short
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 diff --cached --name-status
```

Expected staged files: only `CcdiFileUploadServiceImpl.java` and `CcdiFileUploadServiceImplTest.java`.

```bash
git commit -m "修复本地上传流水记录文件名覆盖"
```

## Task 5: Verification, Real Page Check, And Implementation Record

**Files:**
- Create: `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md`
- Reference: `ruoyi-admin/src/main/resources/application-dev.yml`
- Reference: `lsfx-mock-server/routers/api.py`
- Reference: `lsfx-mock-server/services/file_service.py`

- [ ] **Step 1: Run focused backend regression tests**

Run:

```bash
mvn -pl ccdi-lsfx,ccdi-project -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest -Dsurefire.failIfNoSpecifiedTests=false test
```

Expected: PASS.

- [ ] **Step 2: Run compile check for affected modules**

Run:

```bash
mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile
```

Expected: BUILD SUCCESS.

- [ ] **Step 3: Start verification services**

Use the project backend restart script:

```bash
sh bin/restart_java_backend.sh
```

For the mock service, use local dev defaults:

```bash
cd lsfx-mock-server
python3 main.py --rule-hit-mode subset
```

If the frontend is not already running, start it with the project Node version:

```bash
cd ruoyi-ui
nvm use
npm run dev
```

- [ ] **Step 4: Perform real page upload check with @browser-use:browser**

In the actual business page:

1. Login through the real application.
2. Open project detail -> 上传数据.
3. Upload a test file named with a distinctive original name, for example `原始文件名验证-20260506.xlsx`.
4. Confirm the upload record table displays `原始文件名验证-20260506.xlsx`.
5. Confirm the mock LSFX upload response or service log records the same filename. The mock service receives the filename through FastAPI `UploadFile.filename`, and `FileService.upload_file` writes it into `file_record.file_name`.

Expected: page table filename and mock LSFX received filename both match the original uploaded filename.

- [ ] **Step 5: Clean test data and stop started processes**

Remove the uploaded test record through the existing page delete action if it reached parsed success. If test data was created but cannot be removed from the page, clean only the test row by its unique filename or upload record id after confirming the scope.

Stop only the backend, frontend, or mock-service processes started in Step 3.

- [ ] **Step 6: Write implementation record**

Create `docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md` with:

```markdown
# 上传流水文件原始文件名保持实施记录

## 修改内容

- `ccdi-lsfx` 支持 multipart 文件 part 显式指定 filename。
- `ccdi-project` 本地上传流水文件链路转传流水分析平台时使用初始上传文件名。
- 本地上传链路查询状态后不再使用平台返回文件名覆盖上传记录文件名。
- 拉取本行信息链路保持原有文件名处理行为。

## 影响范围

- 影响项目详情“上传数据”中的本地流水文件上传。
- 不影响历史上传记录。
- 不影响拉取本行信息。
- 不涉及前端源码和数据库结构变更。

## 验证情况

- `mvn -pl ccdi-lsfx,ccdi-project -am -Dtest=HttpUtilTest,LsfxAnalysisClientTest,CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`
- `mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile`
- 真实页面验证：记录页面文件名、mock LSFX 接收 filename、测试数据清理和进程关闭结果。
```

- [ ] **Step 7: Commit final verification record**

Check the worktree, stage only the implementation record, then verify staged scope:

```bash
git status --short
git add docs/reports/implementation/2026-05-06-bank-upload-original-filename-implementation.md
git diff --cached --name-status
```

Expected staged file: only the implementation record.

```bash
git commit -m "文档: 记录上传流水文件名修复验证"
```

## Final Review Checklist

- [ ] 本地上传流水文件转传 LSFX 时 multipart `files` part 的 filename 是初始上传文件名。
- [ ] 本地上传记录 `file_name` 在解析成功或失败后仍是初始上传文件名。
- [ ] 拉取本行信息链路仍按平台返回文件名更新记录，不被本次改动影响。
- [ ] 无数据库结构变更。
- [ ] 无前端源码变更。
- [ ] 实施记录已包含测试、真实页面验证和进程清理结果。
- [ ] 提交前 `git status --short` 无 `.DS_Store` 或无关文件。
