fix: 优化到文件上传逻辑
This commit is contained in:
parent
85434d4d33
commit
006b544b04
@ -34,6 +34,7 @@ import com.yfd.platform.data.service.IFishStatisticsService;
|
|||||||
import com.yfd.platform.data.service.IImportTaskService;
|
import com.yfd.platform.data.service.IImportTaskService;
|
||||||
import com.yfd.platform.data.utils.ZipFileUtil;
|
import com.yfd.platform.data.utils.ZipFileUtil;
|
||||||
import com.yfd.platform.utils.KendoUtil;
|
import com.yfd.platform.utils.KendoUtil;
|
||||||
|
import com.yfd.platform.utils.MultipartStreamParser;
|
||||||
import com.yfd.platform.utils.SecurityUtils;
|
import com.yfd.platform.utils.SecurityUtils;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -48,7 +49,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -450,16 +450,11 @@ public class FishDraftDataController {
|
|||||||
|
|
||||||
@PostMapping("/importZip")
|
@PostMapping("/importZip")
|
||||||
@Operation(summary = "导入ZIP过鱼数据(每个用户同时只能进行一次导入)")
|
@Operation(summary = "导入ZIP过鱼数据(每个用户同时只能进行一次导入)")
|
||||||
public ResponseResult importZip(@RequestParam("file") MultipartFile file) {
|
public ResponseResult importZip(HttpServletRequest request) {
|
||||||
log.info("开始导入ZIP文件");
|
log.info("开始导入ZIP文件");
|
||||||
if (file == null || file.isEmpty()) {
|
|
||||||
return ResponseResult.error("请上传文件");
|
|
||||||
}
|
|
||||||
String fileName = file.getOriginalFilename();
|
|
||||||
if (fileName == null || (!fileName.endsWith(".zip"))) {
|
|
||||||
return ResponseResult.error("请上传ZIP文件(.zip)");
|
|
||||||
}
|
|
||||||
String uploadUserId = SecurityUtils.getUserId();
|
String uploadUserId = SecurityUtils.getUserId();
|
||||||
|
|
||||||
if (importTaskService.hasImportingTask(uploadUserId)) {
|
if (importTaskService.hasImportingTask(uploadUserId)) {
|
||||||
return ResponseResult.error("您有正在进行的导入任务,请等待完成后重试");
|
return ResponseResult.error("您有正在进行的导入任务,请等待完成后重试");
|
||||||
}
|
}
|
||||||
@ -467,52 +462,72 @@ public class FishDraftDataController {
|
|||||||
String importNo = "IMP" + System.currentTimeMillis();
|
String importNo = "IMP" + System.currentTimeMillis();
|
||||||
String taskId = UUID.randomUUID().toString();
|
String taskId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
ImportTask task = null;
|
||||||
|
Path tempDirPath = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String baseTempDir = ZipFileUtil.getDefaultTempDir();
|
String baseTempDir = ZipFileUtil.getDefaultTempDir();
|
||||||
String taskDirName = "zip_" + UUID.randomUUID().toString().substring(0, 8);
|
String taskDirName = "zip_" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
Path tempDirPath = Paths.get(baseTempDir, taskDirName);
|
tempDirPath = Paths.get(baseTempDir, taskDirName);
|
||||||
Files.createDirectories(tempDirPath);
|
|
||||||
File savedZipFile = new File(tempDirPath.toFile(), "upload.zip");
|
|
||||||
file.transferTo(savedZipFile);
|
|
||||||
log.info("ZIP文件已保存到: {}", savedZipFile.getAbsolutePath());
|
|
||||||
|
|
||||||
ImportTask task = new ImportTask();
|
task = new ImportTask();
|
||||||
task.setId(taskId);
|
task.setId(taskId);
|
||||||
task.setImportNo(importNo);
|
task.setImportNo(importNo);
|
||||||
task.setBizType("FISH");
|
task.setBizType("FISH");
|
||||||
task.setFileName(fileName);
|
task.setStatus("UPLOADING");
|
||||||
task.setFileSize(file.getSize());
|
|
||||||
task.setStatus("UPLOADED");
|
|
||||||
task.setUploadUserId(uploadUserId);
|
task.setUploadUserId(uploadUserId);
|
||||||
task.setUploadTime(new Date());
|
task.setUploadTime(new Date());
|
||||||
task.setTempDir(tempDirPath.toString());
|
task.setTempDir(tempDirPath.toString());
|
||||||
importTaskService.save(task);
|
importTaskService.save(task);
|
||||||
log.info("导入任务已创建: {}", taskId);
|
log.info("导入任务已预创建: {}, 状态: UPLOADING", taskId);
|
||||||
|
|
||||||
|
MultipartStreamParser.StreamedFile streamedFile =
|
||||||
|
MultipartStreamParser.streamToFile(request, tempDirPath);
|
||||||
|
|
||||||
|
String fileName = streamedFile.getOriginalFileName();
|
||||||
|
if (fileName == null || !fileName.toLowerCase().endsWith(".zip")) {
|
||||||
|
importTaskService.markFailed(taskId, "文件格式错误: 请上传ZIP文件");
|
||||||
|
return ResponseResult.error("请上传ZIP文件(.zip)");
|
||||||
|
}
|
||||||
|
|
||||||
|
File savedZipFile = streamedFile.getTargetFile();
|
||||||
|
long fileSize = streamedFile.getFileSize();
|
||||||
|
log.info("ZIP文件已保存到: {} ({} bytes)", savedZipFile.getAbsolutePath(), fileSize);
|
||||||
|
|
||||||
|
task.setFileName(fileName);
|
||||||
|
task.setFileSize(fileSize);
|
||||||
|
task.setStatus("UPLOADED");
|
||||||
|
importTaskService.updateById(task);
|
||||||
|
log.info("导入任务已更新: {}, 状态: UPLOADED, 文件名: {}", taskId, fileName);
|
||||||
|
|
||||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||||
|
Path finalTempDirPath = tempDirPath;
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
SecurityContextHolder.setContext(securityContext);
|
SecurityContextHolder.setContext(securityContext);
|
||||||
log.info("异步开始解析ZIP文件, taskId: {}", taskId);
|
log.info("异步开始解析ZIP文件, taskId: {}", taskId);
|
||||||
|
|
||||||
FishImportResult result = fishImportService.parseAndMapZipFromFile(
|
FishImportResult result = fishImportService.parseAndMapZipFromFile(
|
||||||
savedZipFile, tempDirPath.toString(), uploadUserId);
|
savedZipFile, finalTempDirPath.toString(), uploadUserId);
|
||||||
result.setTaskId(taskId);
|
result.setTaskId(taskId);
|
||||||
String status = "VALIDATED";
|
|
||||||
if ("1".equals(result.getCode())) {
|
String status = "1".equals(result.getCode()) ? "FAILED" : "VALIDATED";
|
||||||
status = "FAILED";
|
|
||||||
}
|
|
||||||
importTaskService.updateStatus(taskId, status, result.getTempDir(), null);
|
importTaskService.updateStatus(taskId, status, result.getTempDir(), null);
|
||||||
importTaskService.updateProgress(taskId, result.getTotalCount(),
|
importTaskService.updateProgress(taskId, result.getTotalCount(),
|
||||||
result.getSuccessCount(), result.getFailedCount());
|
result.getSuccessCount(), result.getFailedCount());
|
||||||
// String resultJson = objectMapper.writeValueAsString(result);
|
|
||||||
importTaskService.saveResultJson(taskId, result);
|
importTaskService.saveResultJson(taskId, result);
|
||||||
log.info("异步解析完成, taskId: {}, 状态: {}", taskId, status);
|
|
||||||
|
log.info("异步解析完成, taskId: {}, 状态: {}, 成功: {}, 失败: {}",
|
||||||
|
taskId, status, result.getSuccessCount(), result.getFailedCount());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("异步解析ZIP失败, taskId: {}", taskId, e);
|
log.error("异步解析ZIP失败, taskId: {}", taskId, e);
|
||||||
importTaskService.markFailed(taskId, "导入失败: " + e.getMessage());
|
importTaskService.markFailed(taskId, "导入失败: " + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
SecurityContextHolder.clearContext();
|
SecurityContextHolder.clearContext();
|
||||||
if (savedZipFile.exists()) {
|
if (savedZipFile.exists()) {
|
||||||
savedZipFile.delete();
|
boolean deleted = savedZipFile.delete();
|
||||||
|
log.debug("临时ZIP文件删除: {}, 结果: {}", savedZipFile.getAbsolutePath(), deleted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -522,9 +537,23 @@ public class FishDraftDataController {
|
|||||||
response.put("importNo", importNo);
|
response.put("importNo", importNo);
|
||||||
response.put("status", "UPLOADED");
|
response.put("status", "UPLOADED");
|
||||||
return ResponseResult.successData(response);
|
return ResponseResult.successData(response);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建导入任务失败", e);
|
log.error("导入ZIP文件失败, taskId: {}", taskId, e);
|
||||||
|
|
||||||
|
if (task != null) {
|
||||||
importTaskService.markFailed(taskId, "导入失败: " + e.getMessage());
|
importTaskService.markFailed(taskId, "导入失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempDirPath != null) {
|
||||||
|
try {
|
||||||
|
cn.hutool.core.io.FileUtil.del(tempDirPath.toFile());
|
||||||
|
log.debug("清理临时目录: {}", tempDirPath);
|
||||||
|
} catch (Exception cleanupEx) {
|
||||||
|
log.warn("清理临时目录失败: {}", tempDirPath, cleanupEx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseResult.error("导入失败: " + e.getMessage());
|
return ResponseResult.error("导入失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,334 @@
|
|||||||
|
package com.yfd.platform.utils;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式 multipart 文件上传解析器。
|
||||||
|
* <p>
|
||||||
|
* 绕过 Spring 的 {@code MultipartResolver}(它会先把整个请求体解析完才调用 Controller),
|
||||||
|
* 直接从 {@link HttpServletRequest#getInputStream()} 边读边写目标文件,
|
||||||
|
* 80MB+ 的大文件也能在 Controller 方法内立即开始接收。
|
||||||
|
* <p>
|
||||||
|
* 前提:{@code spring.servlet.multipart.resolve-lazily=true}
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public final class MultipartStreamParser {
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
|
private MultipartStreamParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 multipart/form-data 请求中流式提取第一个文件并写入目标目录。
|
||||||
|
*
|
||||||
|
* @param request HTTP 请求(Content-Type: multipart/form-data)
|
||||||
|
* @param targetDir 目标目录(文件将保存为 upload.zip)
|
||||||
|
* @return 解析结果(原始文件名、目标文件、文件大小)
|
||||||
|
*/
|
||||||
|
public static StreamedFile streamToFile(HttpServletRequest request, Path targetDir) throws IOException {
|
||||||
|
long t0 = System.currentTimeMillis();
|
||||||
|
|
||||||
|
String contentType = request.getContentType();
|
||||||
|
if (contentType == null || !contentType.startsWith("multipart/form-data")) {
|
||||||
|
throw new IOException("请求不是 multipart/form-data 类型");
|
||||||
|
}
|
||||||
|
|
||||||
|
String boundary = extractBoundary(contentType);
|
||||||
|
byte[] closingDelim = ("\r\n--" + boundary + "--").getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
|
||||||
|
InputStream rawInput = new BufferedInputStream(request.getInputStream(), BUFFER_SIZE);
|
||||||
|
|
||||||
|
long t1 = System.currentTimeMillis();
|
||||||
|
String originalFileName = readPartHeaders(rawInput, boundary);
|
||||||
|
long t2 = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Files.createDirectories(targetDir);
|
||||||
|
File targetFile = new File(targetDir.toFile(), "upload.zip");
|
||||||
|
long fileSize = streamBinaryToFile(rawInput, targetFile, closingDelim);
|
||||||
|
long t3 = System.currentTimeMillis();
|
||||||
|
|
||||||
|
log.info("流式接收完成: {} -> {} ({} bytes), 头解析{}ms, 体写入{}ms, 总耗时{}ms",
|
||||||
|
originalFileName, targetFile.getAbsolutePath(), fileSize,
|
||||||
|
t2 - t1, t3 - t2, t3 - t0);
|
||||||
|
return new StreamedFile(originalFileName, targetFile, fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 阶段1:读 part 头 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳过第一个 boundary 行,读取该 part 的 HTTP 头,从中提取 filename。
|
||||||
|
*/
|
||||||
|
private static String readPartHeaders(InputStream in, String boundary) throws IOException {
|
||||||
|
String firstBoundary = "--" + boundary;
|
||||||
|
String fileName = null;
|
||||||
|
|
||||||
|
// 跳过第一个 boundary 行
|
||||||
|
String line = readLine(in);
|
||||||
|
if (line == null || !line.equals(firstBoundary)) {
|
||||||
|
// 可能前面有空行或 boundary 带了 \r
|
||||||
|
if (line != null && line.startsWith(firstBoundary)) {
|
||||||
|
// ok
|
||||||
|
} else {
|
||||||
|
throw new IOException("未找到 multipart boundary,收到: " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 part 头,直到空行
|
||||||
|
while ((line = readLine(in)) != null) {
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
break; // 空行 = 头结束,接下来是二进制数据
|
||||||
|
}
|
||||||
|
if (line.startsWith("Content-Disposition") || line.startsWith("content-disposition")) {
|
||||||
|
fileName = extractFileName(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName == null) {
|
||||||
|
fileName = "unknown.zip";
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Content-Disposition 头中提取 filename 值。
|
||||||
|
* 格式:form-data; name="file"; filename="xxx.zip"
|
||||||
|
*/
|
||||||
|
static String extractFileName(String headerLine) {
|
||||||
|
for (String part : headerLine.split(";")) {
|
||||||
|
part = part.trim();
|
||||||
|
if (part.startsWith("filename")) {
|
||||||
|
int eq = part.indexOf('=');
|
||||||
|
if (eq >= 0) {
|
||||||
|
String val = part.substring(eq + 1).trim();
|
||||||
|
if (val.startsWith("\"") && val.endsWith("\"")) {
|
||||||
|
val = val.substring(1, val.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先进行 URL 解码(处理 %E4%B8%AD%E6%96%87 这种编码)
|
||||||
|
try {
|
||||||
|
val = java.net.URLDecoder.decode(val, StandardCharsets.UTF_8.name());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("URL解码失败,使用原始值: {}", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再进行字符编码转换(处理 GBK/UTF-8 乱码)
|
||||||
|
String decodedFileName = decodeFileName(val);
|
||||||
|
return decodedFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试修复乱码的文件名,支持 GBK 和 UTF-8 编码
|
||||||
|
*/
|
||||||
|
private static String decodeFileName(String rawFileName) {
|
||||||
|
if (rawFileName == null || rawFileName.isEmpty()) {
|
||||||
|
return rawFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] rawBytes = rawFileName.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
|
||||||
|
// 优先尝试 UTF-8 解码(现代浏览器常用)
|
||||||
|
String utf8Decoded = new String(rawBytes, StandardCharsets.UTF_8);
|
||||||
|
if (isValidChinese(utf8Decoded)) {
|
||||||
|
return utf8Decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再尝试 GBK 解码(Windows/旧版浏览器常用)
|
||||||
|
String gbkDecoded = new String(rawBytes, "GBK");
|
||||||
|
if (isValidChinese(gbkDecoded)) {
|
||||||
|
return gbkDecoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 都不包含中文,返回 UTF-8 解码结果
|
||||||
|
return utf8Decoded;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("文件名解码失败,使用原始值: {}", rawFileName, e);
|
||||||
|
return rawFileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单判断字符串是否包含有效的中文字符
|
||||||
|
*/
|
||||||
|
private static boolean isValidChinese(String str) {
|
||||||
|
if (str == null || str.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (char c : str.toCharArray()) {
|
||||||
|
// 检查是否在中文 Unicode 范围内
|
||||||
|
if (c >= '\u4e00' && c <= '\u9fa5') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 检查中文标点符号范围
|
||||||
|
if (c >= '\u3000' && c <= '\u303f') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 阶段2:流式写二进制 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从输入流读取二进制数据写入目标文件,遇到 closingDelim 时停止。
|
||||||
|
* <p>
|
||||||
|
* 使用重叠窗口策略:每次读取一个 chunk,保留末尾 (delimLen-1) 字节
|
||||||
|
* 与下一个 chunk 拼接,防止 boundary 跨 chunk 被截断。
|
||||||
|
*/
|
||||||
|
private static long streamBinaryToFile(InputStream in, File targetFile, byte[] closingDelim) throws IOException {
|
||||||
|
int delimLen = closingDelim.length;
|
||||||
|
int overlap = delimLen - 1;
|
||||||
|
|
||||||
|
byte[] buf = new byte[BUFFER_SIZE];
|
||||||
|
byte[] carry = new byte[overlap]; // 上一轮末尾的重叠字节
|
||||||
|
int carryLen = 0;
|
||||||
|
long totalWritten = 0;
|
||||||
|
|
||||||
|
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile), BUFFER_SIZE)) {
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
int n = in.read(buf);
|
||||||
|
if (n == -1) {
|
||||||
|
// 流结束,写出剩余的重叠字节
|
||||||
|
if (carryLen > 0) {
|
||||||
|
out.write(carry, 0, carryLen);
|
||||||
|
totalWritten += carryLen;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将上一轮的重叠字节 + 本轮数据拼接,搜索 closing boundary
|
||||||
|
byte[] searchBuf = new byte[carryLen + n];
|
||||||
|
if (carryLen > 0) {
|
||||||
|
System.arraycopy(carry, 0, searchBuf, 0, carryLen);
|
||||||
|
}
|
||||||
|
System.arraycopy(buf, 0, searchBuf, carryLen, n);
|
||||||
|
|
||||||
|
int boundaryPos = indexOf(searchBuf, closingDelim);
|
||||||
|
if (boundaryPos >= 0) {
|
||||||
|
// 找到 closing boundary,写出之前的数据
|
||||||
|
if (boundaryPos > 0) {
|
||||||
|
out.write(searchBuf, 0, boundaryPos);
|
||||||
|
totalWritten += boundaryPos;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未找到 boundary:写出安全部分(保留末尾 overlap 字节)
|
||||||
|
int safeLen = searchBuf.length - overlap;
|
||||||
|
if (safeLen > 0) {
|
||||||
|
out.write(searchBuf, 0, safeLen);
|
||||||
|
totalWritten += safeLen;
|
||||||
|
// 保留末尾 overlap 字节到 carry
|
||||||
|
System.arraycopy(searchBuf, safeLen, carry, 0, overlap);
|
||||||
|
carryLen = overlap;
|
||||||
|
} else {
|
||||||
|
// 数据量不足 overlap,全部保留
|
||||||
|
System.arraycopy(searchBuf, 0, carry, 0, searchBuf.length);
|
||||||
|
carryLen = searchBuf.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalWritten;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Content-Type 头中提取 boundary 值。
|
||||||
|
* 格式:multipart/form-data; boundary=----WebKitFormBoundary...
|
||||||
|
*/
|
||||||
|
static String extractBoundary(String contentType) {
|
||||||
|
for (String part : contentType.split(";")) {
|
||||||
|
part = part.trim();
|
||||||
|
if (part.startsWith("boundary=")) {
|
||||||
|
return part.substring("boundary=".length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("无法从 Content-Type 提取 boundary: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按行读取(以 \r\n 或 \n 结尾),返回行内容(不含换行符)。
|
||||||
|
*/
|
||||||
|
private static String readLine(InputStream in) throws IOException {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(256);
|
||||||
|
int prev = -1;
|
||||||
|
int b;
|
||||||
|
while ((b = in.read()) != -1) {
|
||||||
|
if (b == '\n') {
|
||||||
|
// 去掉末尾的 \r
|
||||||
|
byte[] bytes = bos.toByteArray();
|
||||||
|
int len = bytes.length;
|
||||||
|
if (len > 0 && bytes[len - 1] == '\r') {
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
return new String(bytes, 0, len, StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
bos.write(b);
|
||||||
|
prev = b;
|
||||||
|
}
|
||||||
|
// 流结束
|
||||||
|
if (bos.size() > 0) {
|
||||||
|
return bos.toString(StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 byte 数组中查找子数组的位置(Boyer-Moore 简化版)。
|
||||||
|
*/
|
||||||
|
private static int indexOf(byte[] haystack, byte[] needle) {
|
||||||
|
if (needle.length == 0) return 0;
|
||||||
|
if (haystack.length < needle.length) return -1;
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for (int i = 0; i <= haystack.length - needle.length; i++) {
|
||||||
|
for (int j = 0; j < needle.length; j++) {
|
||||||
|
if (haystack[i + j] != needle[j]) {
|
||||||
|
continue outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 结果 DTO ====================
|
||||||
|
|
||||||
|
public static class StreamedFile {
|
||||||
|
private final String originalFileName;
|
||||||
|
private final File targetFile;
|
||||||
|
private final long fileSize;
|
||||||
|
|
||||||
|
StreamedFile(String originalFileName, File targetFile, long fileSize) {
|
||||||
|
this.originalFileName = originalFileName;
|
||||||
|
this.targetFile = targetFile;
|
||||||
|
this.fileSize = fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOriginalFileName() {
|
||||||
|
return originalFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getTargetFile() {
|
||||||
|
return targetFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFileSize() {
|
||||||
|
return fileSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,8 +24,11 @@ spring:
|
|||||||
matching-strategy: ant_path_matcher
|
matching-strategy: ant_path_matcher
|
||||||
servlet:
|
servlet:
|
||||||
multipart:
|
multipart:
|
||||||
max-file-size: 30MB
|
max-file-size: 300MB
|
||||||
max-request-size: 100MB
|
max-request-size: 500MB
|
||||||
|
file-size-threshold: 1KB
|
||||||
|
location: /tmp/upload
|
||||||
|
resolve-lazily: true
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
|
|||||||
@ -113,10 +113,9 @@ spring:
|
|||||||
multipart:
|
multipart:
|
||||||
max-file-size: 300MB
|
max-file-size: 300MB
|
||||||
max-request-size: 500MB
|
max-request-size: 500MB
|
||||||
# 关键:文件超过 1KB 就写入磁盘临时文件,避免内存积压
|
|
||||||
file-size-threshold: 1KB
|
file-size-threshold: 1KB
|
||||||
# 指定临时目录(确保 Docker 容器内该目录可写且有空间)
|
|
||||||
location: /tmp/upload
|
location: /tmp/upload
|
||||||
|
resolve-lazily: true
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
name: logs/platform-dev.log
|
name: logs/platform-dev.log
|
||||||
|
|||||||
@ -29,6 +29,9 @@ spring:
|
|||||||
multipart:
|
multipart:
|
||||||
max-file-size: 300MB
|
max-file-size: 300MB
|
||||||
max-request-size: 500MB
|
max-request-size: 500MB
|
||||||
|
file-size-threshold: 1KB
|
||||||
|
location: /tmp/upload
|
||||||
|
resolve-lazily: true
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
file:
|
file:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: devtw
|
active: prod
|
||||||
|
|
||||||
jasypt:
|
jasypt:
|
||||||
encryptor:
|
encryptor:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user