task-6: 分组数据改为索引/范围引用,避免 groups 嵌套复制完整行对象

buildParsedGroups 只写 [startIndex, endIndex) 半开区间引用(同 baseId 行在
items 中天然连续),行对象仅存在于 items 一次;resolveAllRows 优先按引用
展开并安全跳过越界/非法区间;响应组仍内嵌预览行兼容前端。旧 payload 内嵌
items 格式继续可读。新增 8 个测试覆盖引用写入、批量、幂等、空输入、单组、
越界 clamp、非法区间、依赖失败恢复。
This commit is contained in:
2026-08-29 15:02:15 +08:00
parent 16bec1c0fa
commit db6e8f0ce8
3 changed files with 468 additions and 5 deletions
@@ -1,5 +1,6 @@
package com.nanri.aiimage.modules.similarasin.model.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -27,6 +28,21 @@ public class SimilarAsinParsedGroupVo {
@Schema(description = "分组内行数")
private Integer itemCount;
@Schema(description = "分组内全部行,顺序与原 Excel 保持一致")
/**
* 组内行在载荷 items 中的起始下标(含)。写入侧只输出引用,
* 行对象仅存在于 items 一次,避免 groups 嵌套复制完整行对象。
*/
@Schema(description = "组内行在 items 中的起始下标(含)")
private Integer startIndex;
/**
* 组内行在载荷 items 中的结束下标(不含),半开区间 [startIndex, endIndex)。
*/
@Schema(description = "组内行在 items 中的结束下标(不含),半开区间 [startIndex, endIndex)")
private Integer endIndex;
/** 兼容字段:旧 payload 内嵌的完整行。新写入不再输出,仅读取旧 payload 时使用。 */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Schema(description = "兼容旧链路字段:旧 payload 内嵌的完整行,新写入不再输出", hidden = true)
private List<SimilarAsinParsedRowVo> items = new ArrayList<>();
}
@@ -511,7 +511,7 @@ public class SimilarAsinTaskService {
vo.setImgSwitch(Boolean.TRUE.equals(request.getImgSwitch()));
vo.setCategorySwitch(Boolean.TRUE.equals(request.getCategorySwitch()));
vo.setItems(buildResponsePreviewRows(allRows));
vo.setGroups(buildResponsePreviewGroups(groups));
vo.setGroups(buildResponsePreviewGroups(groups, allRows));
long finishedAt = System.nanoTime();
log.info("[similar-asin] parse timing taskId={} files={} rows={} groups={} totalMs={} parseMs={} groupMs={} taskInsertMs={} payloadJsonMs={} payloadStoreMs={} persistMs={} responseMs={}",
task.getId(),
@@ -963,6 +963,10 @@ public class SimilarAsinTaskService {
if (payload == null) {
return List.of();
}
if (containsGroupRefs(payload)) {
// 新格式分组携带索引引用信任引用展开结果越界/非法区间安全跳过
return expandGroupRefs(payload);
}
List<SimilarAsinParsedRowVo> rows = payload.getItems();
if (rows == null || rows.isEmpty()) {
rows = payload.getAllItems();
@@ -976,6 +980,48 @@ public class SimilarAsinTaskService {
return rows == null ? List.of() : rows;
}
private static boolean containsGroupRefs(SimilarAsinParsedPayloadDto payload) {
if (payload.getGroups() == null) {
return false;
}
for (SimilarAsinParsedGroupVo group : payload.getGroups()) {
if (group != null && (group.getStartIndex() != null || group.getEndIndex() != null)) {
return true;
}
}
return false;
}
/**
* Task 6按分组引用 [startIndex, endIndex) items 展开组内行
* 引用越界或区间非法时安全跳过不抛异常展开不修改 payload 内部状态
*/
static List<SimilarAsinParsedRowVo> expandGroupRefs(SimilarAsinParsedPayloadDto payload) {
if (payload == null || payload.getGroups() == null || payload.getGroups().isEmpty()) {
return List.of();
}
List<SimilarAsinParsedRowVo> rows = payload.getItems();
if (rows == null || rows.isEmpty()) {
rows = payload.getAllItems();
}
if (rows == null || rows.isEmpty()) {
return List.of();
}
List<SimilarAsinParsedRowVo> expanded = new ArrayList<>();
for (SimilarAsinParsedGroupVo group : payload.getGroups()) {
if (group == null) {
continue;
}
int start = group.getStartIndex() == null ? 0 : group.getStartIndex();
int end = group.getEndIndex() == null ? 0 : group.getEndIndex();
if (start < 0 || end <= start || end > rows.size()) {
continue;
}
expanded.addAll(rows.subList(start, end));
}
return expanded;
}
private PersistSubmittedChunkResult persistSubmittedChunk(PreparedSubmittedChunk prepared) {
Long taskId = prepared.taskId();
FileTaskEntity task = fileTaskMapper.selectById(taskId);
@@ -1461,8 +1507,15 @@ public class SimilarAsinTaskService {
return candidates;
}
/**
* Task 6分组数据改为索引/范围引用组内行在原 items 中必然连续
* parseWorkbook 按行遍历 baseId 块连续收集因此只需要
* [startIndex, endIndex) 半开区间即可唯一定位组内行
* 行对象仅存在于 items 一次避免 groups 嵌套复制完整行对象
*/
private List<SimilarAsinParsedGroupVo> buildParsedGroups(List<SimilarAsinParsedRowVo> rows) {
List<SimilarAsinParsedGroupVo> groups = new ArrayList<>();
int cursor = 0;
for (List<SimilarAsinParsedRowVo> siblings : groupRowsByBaseId(rows).values()) {
if (siblings == null || siblings.isEmpty()) {
continue;
@@ -1475,13 +1528,16 @@ public class SimilarAsinTaskService {
group.setBaseId(baseId(first.getDisplayId()));
group.setDisplayId(firstNonBlank(first.getDisplayId(), first.getSourceId()));
group.setItemCount(siblings.size());
group.setItems(new ArrayList<>(siblings));
int end = cursor + siblings.size();
group.setStartIndex(cursor);
group.setEndIndex(end);
cursor = end;
groups.add(group);
}
return groups;
}
private List<SimilarAsinParsedGroupVo> buildResponsePreviewGroups(List<SimilarAsinParsedGroupVo> groups) {
private List<SimilarAsinParsedGroupVo> buildResponsePreviewGroups(List<SimilarAsinParsedGroupVo> groups, List<SimilarAsinParsedRowVo> allRows) {
if (groups == null || groups.isEmpty()) {
return List.of();
}
@@ -1499,7 +1555,18 @@ public class SimilarAsinTaskService {
copy.setBaseId(group.getBaseId());
copy.setDisplayId(group.getDisplayId());
copy.setItemCount(group.getItemCount());
copy.setItems(buildResponsePreviewRows(group.getItems()));
copy.setStartIndex(group.getStartIndex());
copy.setEndIndex(group.getEndIndex());
// 响应组携带内嵌预览行切片自全量行不暴露索引引用语义
int start = group.getStartIndex() == null ? 0 : group.getStartIndex();
int end = group.getEndIndex() == null ? 0 : group.getEndIndex();
if (start >= 0 && end > start && end <= allRows.size()) {
copy.setItems(buildResponsePreviewRows(allRows.subList(start, end)));
} else if (group.getItems() != null && !group.getItems().isEmpty()) {
copy.setItems(buildResponsePreviewRows(group.getItems()));
} else {
copy.setItems(List.of());
}
preview.add(copy);
}
return preview;