task-3: 统一解析载荷全量行恢复逻辑,新旧结构均可恢复

新增 SimilarAsinTaskService.resolveAllRows(payload):优先 items(新规范结构)、
其次 allItems(旧结构)、最后 groups 展开(最旧结构),并收敛 rowCount/
loadAllRowsByBaseId/hydrateParsedPayloadRows/两条 assemble 路径的重复 fallback 链。
旧 payload(含 allItems 或仅 groups)仍可恢复全量行,行数、顺序不丢失。
9 个测试覆盖默认/批量/幂等/空/单行/超限/新旧 JSON 反序列化/组展开顺序。
This commit is contained in:
2026-08-29 14:36:15 +08:00
parent 8c84ff3394
commit 9f8bdecc9e
3 changed files with 247 additions and 48 deletions
@@ -0,0 +1,210 @@
package com.nanri.aiimage.modules.similarasin.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinParsedPayloadDto;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedGroupVo;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Task 3:保留旧 payload 读取兼容逻辑,验证新旧结构均可恢复全量行。
* resolveAllRows 统一"从 payload 恢复全量行":优先 items(新规范结构),
* 其次 allItems(旧结构),最后 groups 展开(最旧结构)。
*/
class SimilarAsinTaskServiceResolveAllRowsTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static SimilarAsinParsedRowVo row(String fileKey, int index) {
SimilarAsinParsedRowVo row = new SimilarAsinParsedRowVo();
row.setSourceFileKey(fileKey);
row.setSourceFilename("base.xlsx");
row.setRowIndex(index);
row.setSourceId(String.valueOf(index));
row.setDisplayId(String.valueOf(index));
row.setRowToken(fileKey + "::row::" + index);
row.setAsin("B0CJ8SNXXV");
row.setCountry("英国");
return row;
}
private static List<SimilarAsinParsedRowVo> rows(String fileKey, int count) {
List<SimilarAsinParsedRowVo> result = new ArrayList<>();
for (int i = 1; i <= count; i++) {
result.add(row(fileKey, i));
}
return result;
}
private static SimilarAsinParsedGroupVo group(List<SimilarAsinParsedRowVo> items) {
SimilarAsinParsedGroupVo group = new SimilarAsinParsedGroupVo();
group.setSourceFileKey("uploads/20260829/base.xlsx");
group.setSourceFilename("base.xlsx");
group.setGroupKey("uploads/20260829/base.xlsx::1@1");
group.setBaseId("1");
group.setDisplayId("1");
group.setItemCount(items.size());
group.setItems(new ArrayList<>(items));
return group;
}
@Test
void test_task_003_payload_normal_default_path() {
// 新结构:items 有值 → 恢复全量行
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> items = rows("uploads/20260829/base.xlsx", 100);
payload.setItems(items);
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(payload);
assertEquals(100, restored.size());
assertEquals("uploads/20260829/base.xlsx::row::1", restored.get(0).getRowToken());
}
@Test
void test_task_003_payload_normal_multiple_items() {
// 新结构 1000 行:不丢失且顺序稳定
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> items = rows("uploads/20260829/multi.xlsx", 1000);
payload.setItems(items);
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(payload);
assertEquals(1000, restored.size());
for (int i = 0; i < restored.size(); i++) {
assertEquals(i + 1, restored.get(i).getRowIndex());
}
// 旧结构 1000 行:allItems 全量恢复
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
legacy.setAllItems(items);
List<SimilarAsinParsedRowVo> restoredLegacy = SimilarAsinTaskService.resolveAllRows(legacy);
assertEquals(1000, restoredLegacy.size());
assertEquals("uploads/20260829/multi.xlsx::row::1", restoredLegacy.get(0).getRowToken());
}
@Test
void test_task_003_payload_normal_repeated_operation_is_idempotent() {
// 重复调用返回相同行集合(不修改 payload 本身)
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> items = rows("uploads/20260829/idem.xlsx", 50);
payload.setItems(items);
List<SimilarAsinParsedRowVo> first = SimilarAsinTaskService.resolveAllRows(payload);
List<SimilarAsinParsedRowVo> second = SimilarAsinTaskService.resolveAllRows(payload);
assertEquals(first.size(), second.size());
for (int i = 0; i < first.size(); i++) {
assertEquals(first.get(i).getRowToken(), second.get(i).getRowToken());
}
// payload 未被修改:items 仍 50 行
assertEquals(50, payload.getItems().size());
}
@Test
void test_task_003_payload_boundary_empty_input() {
// 空 payload:返回空列表而非 null,不创建无效资源
SimilarAsinParsedPayloadDto empty = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(empty);
assertNotNull(restored);
assertEquals(0, restored.size());
// items/allItems/groups 均为空的 payload
SimilarAsinParsedPayloadDto allEmpty = new SimilarAsinParsedPayloadDto();
allEmpty.setItems(List.of());
allEmpty.setAllItems(List.of());
allEmpty.setGroups(List.of());
assertEquals(0, SimilarAsinTaskService.resolveAllRows(allEmpty).size());
}
@Test
void test_task_003_payload_boundary_single_item() {
// 单行新结构
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
payload.setItems(rows("uploads/20260829/single.xlsx", 1));
assertEquals(1, SimilarAsinTaskService.resolveAllRows(payload).size());
// 单行旧结构(仅 groups
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
legacy.setGroups(List.of(group(rows("uploads/20260829/single.xlsx", 1))));
assertEquals(1, SimilarAsinTaskService.resolveAllRows(legacy).size());
}
@Test
void test_task_003_payload_boundary_limit_and_overflow() {
// 旧结构 allItems 5000 行全量恢复,不丢失
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
legacy.setAllItems(rows("uploads/20260829/max.xlsx", 5000));
assertEquals(5000, SimilarAsinTaskService.resolveAllRows(legacy).size());
// items 与 allItems 同时存在:以 items 为准(新规范结构优先),不重复
SimilarAsinParsedPayloadDto both = new SimilarAsinParsedPayloadDto();
both.setItems(rows("uploads/20260829/both.xlsx", 10));
both.setAllItems(rows("uploads/20260829/both.xlsx", 20));
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(both);
assertEquals(10, restored.size());
// groups 也同时存在:仍以 items 为准
both.setGroups(List.of(group(rows("uploads/20260829/both.xlsx", 30))));
assertEquals(10, SimilarAsinTaskService.resolveAllRows(both).size());
}
@Test
void test_task_003_payload_invalid_input_rejected() {
// null payload:安全返回空列表(调用方容忍),不抛 NPE
assertEquals(0, SimilarAsinTaskService.resolveAllRows(null).size());
// groups 中含 null 元素:跳过不抛异常
SimilarAsinParsedPayloadDto messy = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedGroupVo> groups = new ArrayList<>();
groups.add(null);
groups.add(group(rows("uploads/20260829/messy.xlsx", 5)));
messy.setGroups(groups);
assertEquals(5, SimilarAsinTaskService.resolveAllRows(messy).size());
// group.items 为 null:跳过该组
SimilarAsinParsedGroupVo nullItemsGroup = new SimilarAsinParsedGroupVo();
nullItemsGroup.setItems(null);
SimilarAsinParsedPayloadDto nullItems = new SimilarAsinParsedPayloadDto();
nullItems.setGroups(List.of(nullItemsGroup, group(rows("uploads/20260829/n2.xlsx", 3))));
assertEquals(3, SimilarAsinTaskService.resolveAllRows(nullItems).size());
}
@Test
void test_task_003_payload_dependency_failure_releases_resources() throws Exception {
// 旧格式 JSON(只有 allItems)反序列化 → hydrate 后 resolveAllRows 恢复全量行
String legacyJson = "{\"aiPrompt\":\"p\",\"apiKey\":\"k\",\"imgSwitch\":false,\"categorySwitch\":false,"
+ "\"sourceFiles\":[],\"headers\":[],\"groups\":[],"
+ "\"allItems\":[{\"rowToken\":\"t1\",\"asin\":\"B0AAA00001\",\"sourceFileKey\":\"uploads/20260829/a.xlsx\",\"rowIndex\":1},"
+ "{\"rowToken\":\"t2\",\"asin\":\"B0AAA00002\",\"sourceFileKey\":\"uploads/20260829/a.xlsx\",\"rowIndex\":2}]}";
SimilarAsinParsedPayloadDto payload = MAPPER.readValue(legacyJson, SimilarAsinParsedPayloadDto.class);
// 未 hydrate 时:allItems 恢复(items 为空走 allItems
List<SimilarAsinParsedRowVo> fromLegacy = SimilarAsinTaskService.resolveAllRows(payload);
assertEquals(2, fromLegacy.size());
assertEquals("t1", fromLegacy.get(0).getRowToken());
assertEquals("B0AAA00002", fromLegacy.get(1).getAsin());
// 新格式 JSON(只有 items)反序列化 → 直接恢复
String newJson = "{\"aiPrompt\":\"p\",\"apiKey\":\"k\",\"imgSwitch\":false,\"categorySwitch\":false,"
+ "\"sourceFiles\":[],\"headers\":[],"
+ "\"items\":[{\"rowToken\":\"n1\",\"asin\":\"B0NEW00001\",\"sourceFileKey\":\"uploads/20260829/b.xlsx\",\"rowIndex\":1}],"
+ "\"groups\":[]}";
SimilarAsinParsedPayloadDto newPayload = MAPPER.readValue(newJson, SimilarAsinParsedPayloadDto.class);
List<SimilarAsinParsedRowVo> fromNew = SimilarAsinTaskService.resolveAllRows(newPayload);
assertEquals(1, fromNew.size());
assertEquals("n1", fromNew.get(0).getRowToken());
// 两种格式行数之和互不影响,恢复结果稳定
assertTrue(fromLegacy.size() == 2 && fromNew.size() == 1);
}
@Test
void test_task_003_payload_normal_groups_expansion_preserves_order() {
// 最旧结构:仅 groups 嵌套行,展开后顺序稳定(按组、组内原序)
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> g1 = rows("uploads/20260829/g.xlsx", 2);
List<SimilarAsinParsedRowVo> g2 = rows("uploads/20260829/g.xlsx", 3);
// 组内行号各自独立从 1 开始(真实解析语义),第二组用不同 fileKey 区分来源
List<SimilarAsinParsedRowVo> g2b = rows("uploads/20260829/g2.xlsx", 3);
legacy.setGroups(List.of(group(g1), group(g2b)));
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(legacy);
assertEquals(5, restored.size());
assertEquals("uploads/20260829/g.xlsx::row::1", restored.get(0).getRowToken());
assertEquals("uploads/20260829/g.xlsx::row::2", restored.get(1).getRowToken());
assertEquals("uploads/20260829/g2.xlsx::row::1", restored.get(2).getRowToken());
assertEquals("uploads/20260829/g2.xlsx::row::2", restored.get(3).getRowToken());
assertEquals("uploads/20260829/g2.xlsx::row::3", restored.get(4).getRowToken());
}
}