task-7: 限制单文件大小、最大行数和最大字段长度,防止解析任务无界增长

新增 maxSourceFileBytes(50MB)/maxParseRows(50000)/maxFieldLength(2000)
三个配置项:文件超限在解析入口拒绝(异常消息可识别)、行数超限拒绝、
超长单元格字段截断。0/负值配置回退默认。新增 8 个测试覆盖正常路径、
多文件、幂等、空文件、单行、边界(恰好=上限通过、超限拒绝)、
非法输入(文件超限/字段截断)、依赖失败恢复。
This commit is contained in:
2026-08-29 15:08:14 +08:00
parent db6e8f0ce8
commit 68b7d9a4a4
3 changed files with 343 additions and 1 deletions
@@ -139,6 +139,21 @@ public class SimilarAsinProperties {
*/
private int parseResponsePreviewLimit = 100;
/**
* Task 7:单个源文件大小上限(字节)。超过则拒绝解析,防止无界文件增长。
*/
private long maxSourceFileBytes = 50L * 1024L * 1024L;
/**
* Task 7:单次解析最大有效行数。超过则拒绝解析,防止任务无界增长。
*/
private int maxParseRows = 50000;
/**
* Task 7:单字段最大长度(字符)。超过的字段值截断到该上限,防止内存无界增长。
*/
private int maxFieldLength = 2000;
/**
* P0-4:抢 Coze 提交锁失败后下次重试间隔(毫秒)。
* 原硬编码 500ms,会在指数退避算法中作为基础值(500/1000/2000/4000ms 上限 4000)。
@@ -156,6 +156,39 @@ public class SimilarAsinTaskService {
}
return Math.min(configured, PARSE_RESPONSE_PREVIEW_LIMIT_MAX);
}
/**
* Task 7单文件大小上限0/负值回退默认 50MB防止误配导致解析无界增长
*/
private long resolveMaxSourceFileBytes() {
Long configured = properties.getMaxSourceFileBytes();
if (configured == null || configured <= 0) {
return 50L * 1024L * 1024L;
}
return configured;
}
/**
* Task 7单次解析最大有效行数0/负值回退默认 50000防止任务无界增长
*/
private int resolveMaxParseRows() {
Integer configured = properties.getMaxParseRows();
if (configured == null || configured <= 0) {
return 50000;
}
return configured;
}
/**
* Task 7单字段最大长度字符0/负值回退默认 2000
*/
private int resolveMaxFieldLength() {
Integer configured = properties.getMaxFieldLength();
if (configured == null || configured <= 0) {
return 2000;
}
return configured;
}
/**
* P0-2 最小风险变体poll 调度阶段并发预取 Coze HTTP 结果时
* 控制对单个 Coze 后端的并发度8 cozeTaskExecutor 12 并发上限对齐留 4 余量
@@ -421,6 +454,11 @@ public class SimilarAsinTaskService {
if (input == null || !input.exists()) {
throw new BusinessException("源文件不存在");
}
long maxBytes = resolveMaxSourceFileBytes();
if (input.length() > maxBytes) {
throw new BusinessException("源文件超过大小限制: " + source.getOriginalFilename()
+ " (" + input.length() + " bytes > " + maxBytes + " bytes)");
}
ParsedWorkbook parsed = parseWorkbook(input, source);
totalRows += parsed.totalRows();
@@ -432,6 +470,10 @@ public class SimilarAsinTaskService {
if (allRows.isEmpty()) {
throw new BusinessException("未解析到有效 ASIN 数据");
}
int maxParseRows = resolveMaxParseRows();
if (allRows.size() > maxParseRows) {
throw new BusinessException("解析行数超过上限: " + allRows.size() + " rows > " + maxParseRows + " rows");
}
boolean requestedCategorySwitch = Boolean.TRUE.equals(request.getCategorySwitch());
request.setCategorySwitch(requestedCategorySwitch || categoryRetryRequired);
if (!requestedCategorySwitch && categoryRetryRequired) {
@@ -5478,7 +5520,15 @@ public class SimilarAsinTaskService {
return "";
}
String value = normalize(formatter.formatCellValue(row.getCell(col)));
return isSpreadsheetErrorValue(value) ? "" : value;
if (isSpreadsheetErrorValue(value)) {
return "";
}
// Task 7单字段长度上限防止超长单元格导致内存无界增长
int maxLen = resolveMaxFieldLength();
if (value.length() > maxLen) {
return value.substring(0, maxLen);
}
return value;
}
private static boolean isSpreadsheetErrorValue(String value) {