fix: 完善工作流公共接口上传与错误契约

- 区分 HTTP 请求标识与内部上传标识,补齐 Redis 旧记录兼容和关联日志

- 保持归一化 MIME 一致,并隔离工作流鉴权错误契约对其他公共接口的影响

- 收口 Multipart 操作日志与对象存储故障分类,归档范围:S05
This commit is contained in:
2026-08-10 11:41:09 +08:00
parent bae7b18977
commit 9d2fa39a2d
16 changed files with 573 additions and 115 deletions

View File

@@ -122,21 +122,23 @@ public class XFIleStorageServiceImpl implements FileStorageService {
}
/**
* 获取上传文件的 Content-Type并为文本文件补充 UTF-8 编码。
* 获取上传文件的 Content-Type在客户端未声明时为文本文件补充 UTF-8 编码。
*
* @param file 上传文件
* @return 文件媒体类型
*/
public static String getFileContentType(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String contentType = null;
if (originalFilename != null && originalFilename.toLowerCase().endsWith(".txt")) {
contentType = "text/plain; charset=utf-8";
} else {
// 其他类型文件可以按需设置
contentType = file.getContentType();
String contentType = file.getContentType();
if (StringUtils.hasText(contentType)) {
return contentType;
}
return contentType;
if (StringUtils.endsWithIgnoreCase(
originalFilename,
".txt")) {
return "text/plain; charset=utf-8";
}
return null;
}
/**

View File

@@ -109,6 +109,53 @@ public class XFIleStorageServiceImplTest {
assertTrue(platform.exists);
}
/**
* 验证可恢复上传保留上游已经归一化的 MIME不再按 txt 扩展名二次覆盖。
*
* @throws Exception 注入测试替身失败
*/
@Test
public void recoverableSavePreservesNormalizedTextContentType()
throws Exception {
RecoverablePlatform platform = new RecoverablePlatform(
"minio-main",
"attachment",
"https://files/");
RecoverableStorageService delegate =
new RecoverableStorageService(platform);
XFIleStorageServiceImpl service = createService(delegate);
FileStorageWriteHandle handle = service.prepareRecoverableWrite(
"workflow-api-upload/upload-1",
"content.txt");
service.saveRecoverable(
new BytesMultipartFile(
"content".getBytes(
java.nio.charset.StandardCharsets.UTF_8),
"content.txt",
"application/octet-stream"),
handle);
assertEquals(
"application/octet-stream",
delegate.uploadContentType);
}
/**
* 验证客户端未声明 MIME 时继续为 txt 文件补充 UTF-8 文本类型。
*/
@Test
public void textFileWithoutContentTypeUsesUtf8Fallback() {
BytesMultipartFile file = new BytesMultipartFile(
new byte[]{1},
"content.TXT",
null);
assertEquals(
"text/plain; charset=utf-8",
XFIleStorageServiceImpl.getFileContentType(file));
}
/**
* 验证 MinIO 可恢复读取使用已配置客户端和句柄中的精确对象键,不请求公开 URL。
*
@@ -398,6 +445,8 @@ public class XFIleStorageServiceImplTest {
private String uploadPath;
/** 上传文件名。 */
private String uploadFilename;
/** 上传媒体类型。 */
private String uploadContentType;
/** recorder 删除调用次数。 */
private int recorderDeleteCalls;
/** recorder 删除是否抛出异常。 */
@@ -536,6 +585,7 @@ public class XFIleStorageServiceImplTest {
*/
@Override
public org.dromara.x.file.storage.core.upload.UploadPretreatment setContentType(String contentType) {
delegate.uploadContentType = contentType;
return this;
}
@@ -562,20 +612,42 @@ public class XFIleStorageServiceImplTest {
private static final class BytesMultipartFile implements MultipartFile {
/** 文件内容。 */
private final byte[] bytes;
/** 文件名。 */
private final String filename;
/** 文件媒体类型。 */
private final String contentType;
/**
* 创建上传文件替身。
*
* @param bytes 文件内容
*/
private BytesMultipartFile(byte[] bytes) { this.bytes = bytes.clone(); }
private BytesMultipartFile(byte[] bytes) {
this(bytes, "content.bin", "application/octet-stream");
}
/**
* 创建指定文件名和媒体类型的上传文件替身。
*
* @param bytes 文件内容
* @param filename 文件名
* @param contentType 文件媒体类型
*/
private BytesMultipartFile(
byte[] bytes,
String filename,
String contentType) {
this.bytes = bytes.clone();
this.filename = filename;
this.contentType = contentType;
}
/** {@inheritDoc} */
@Override public String getName() { return "file"; }
/** {@inheritDoc} */
@Override public String getOriginalFilename() { return "content.bin"; }
@Override public String getOriginalFilename() { return filename; }
/** {@inheritDoc} */
@Override public String getContentType() { return "application/octet-stream"; }
@Override public String getContentType() { return contentType; }
/** {@inheritDoc} */
@Override public boolean isEmpty() { return bytes.length == 0; }
/** {@inheritDoc} */