task-7: 限制单文件大小、最大行数和最大字段长度,防止解析任务无界增长
新增 maxSourceFileBytes(50MB)/maxParseRows(50000)/maxFieldLength(2000) 三个配置项:文件超限在解析入口拒绝(异常消息可识别)、行数超限拒绝、 超长单元格字段截断。0/负值配置回退默认。新增 8 个测试覆盖正常路径、 多文件、幂等、空文件、单行、边界(恰好=上限通过、超限拒绝)、 非法输入(文件超限/字段截断)、依赖失败恢复。
This commit is contained in:
@@ -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)。
|
||||
|
||||
+51
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user