25 KiB
Bank Upload Original Filename Implementation Plan
For agentic workers: Steps use checkbox (
- [ ]) syntax for tracking. Follow the CCDI project rule: unless the user explicitly declaresusing-superpowersfor 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
- 项目路径规则覆盖
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。
- 新增可指定 multipart filename 的
- 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可被显式指定。
- 验证 multipart body 中
- 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.javaprocessFileAsync调用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
HttpUtilTestwith a failing filename assertion
Add:
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
LsfxAnalysisClientTestwith a failing client assertion
Add:
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:
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:
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:
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.uploadFileaccept existingResourcevalues
Replace the file branch in uploadFile(...) with:
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:
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-lsfxtests
Run:
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:
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.
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
processFileAsyncstubs to the new signature
In CcdiFileUploadServiceImplTest.java, replace local file upload stubs:
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any())).thenReturn(buildUploadResponse());
with:
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:
@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:
@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:
"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:
@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:
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:
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
with:
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
- Step 2: Add source control to status post-processing
Add an overload:
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
CcdiFileUploadRecord record, Integer logId) {
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
}
Change the current method signature to:
private void processRecordAfterLogIdReady(Long projectId, Integer lsfxProjectId,
CcdiFileUploadRecord record, Integer logId,
boolean preserveRecordFileName) {
Then replace the filename update block with:
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:
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
with:
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:
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:
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.
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:
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:
mvn -pl ccdi-lsfx,ccdi-project -am -DskipTests compile
Expected: BUILD SUCCESS.
- Step 3: Start verification services
Use the project backend restart script:
sh bin/restart_java_backend.sh
For the mock service, use local dev defaults:
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:
cd ruoyi-ui
nvm use
npm run dev
- Step 4: Perform real page upload check with @browser-use:browser
In the actual business page:
- Login through the real application.
- Open project detail -> 上传数据.
- Upload a test file named with a distinctive original name, for example
原始文件名验证-20260506.xlsx. - Confirm the upload record table displays
原始文件名验证-20260506.xlsx. - Confirm the mock LSFX upload response or service log records the same filename. The mock service receives the filename through FastAPI
UploadFile.filename, andFileService.upload_filewrites it intofile_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:
# 上传流水文件原始文件名保持实施记录
## 修改内容
- `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:
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.
git commit -m "文档: 记录上传流水文件名修复验证"
Final Review Checklist
- 本地上传流水文件转传 LSFX 时 multipart
filespart 的 filename 是初始上传文件名。 - 本地上传记录
file_name在解析成功或失败后仍是初始上传文件名。 - 拉取本行信息链路仍按平台返回文件名更新记录,不被本次改动影响。
- 无数据库结构变更。
- 无前端源码变更。
- 实施记录已包含测试、真实页面验证和进程清理结果。
- 提交前
git status --short无.DS_Store或无关文件。