refactor(similar-asin): SimilarAsinTaskService 拆分为 9 个 support 类(5914→3455 行)

从 5914 行的巨型 Service 中按内聚单元抽出 9 个 support 类 + 1 个顶层 record,
净减 2459 行(-41.6%)。全部为等价搬移(Javadoc 标注搬移来源),未改任何行为:

- SimilarAsinLimits            阈值/开关解析(parse 上限、chunk merge 上限、LLM batch/缓冲/flush)
- SimilarAsinPayloadSupport    解析载荷编解码与读取门面
- SimilarAsinPoisonTracker     毒行滑窗熔断状态(Service 从此无进程内可变状态)
- SimilarAsinChunkMergeSupport 行键族 + chunk 合并纯计算
- SimilarAsinResultTextSupport 结果行判定与用户可见文本渲染
- SimilarAsinChunkPayloadSupport chunk 载荷读取、读失败诊断、orphan 兜底
- SimilarAsinResultWorkbookAssembler 结果文件装配(xlsx/zip、POI、DISPIMG、并发)
- SimilarAsinTaskOwnershipSupport    实例归属判定与 per-task 分布式锁
- SimilarAsinTaskProgressSupport     文件构建进度、任务视图映射与计数
- LlmBatchContext              从内部 record 提为顶层,供归属与 LLM 流水线共用

顺带清理 7 处死代码(imageUrlCellValue、resolveResultDownloadUrl/Filename、
applyLlmToPersistedChunks、countCompletedLlmStates、hasPromptFields、
userFacingConclusion、@PreDestroy import)。

assembleExecutor 仍由 Service 持有,shutdownAssembleExecutor 语义不变(18 个测试未动)。
验证:在干净 HEAD worktree 上叠加本改动跑 similarasin 全量测试,结果与裸 HEAD 一致
(386 测试,仅 3 个 SimilarAsinSnapshotTest 既有失败),零新增失败。
This commit is contained in:
2026-09-14 01:30:11 +08:00
parent db6869b77e
commit 9d92fb4af2
18 changed files with 3262 additions and 2770 deletions
@@ -2,6 +2,7 @@ package com.nanri.aiimage.modules.similarasin.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinChunkMergeSupport;
import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper;
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
import org.junit.jupiter.api.Test;
@@ -92,7 +93,7 @@ class SimilarAsinTaskServiceChunkKeysetTest {
// 正常输入:chunk 数小于 pageSize,一轮拉完,结果全且按 chunkIndex 有序
List<TaskChunkEntity> all = chunks(1, 1, 2, 2, 3, 3);
stubKeysetPages(all, 500);
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
assertEquals(3, result.size());
assertEquals(List.of(1, 2, 3), result.stream().map(TaskChunkEntity::getChunkIndex).toList());
assertEquals(List.of(0L), keysetIdsFromRounds(1), "首轮 keyset 为 0");
@@ -103,7 +104,7 @@ class SimilarAsinTaskServiceChunkKeysetTest {
// 超过 pageSize:多轮拉取,keyset 逐轮推进,全部合并且按 chunkIndex 升序、无重复
List<TaskChunkEntity> all = chunks(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7);
stubKeysetPages(all, 3);
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
assertEquals(7, result.size());
assertEquals(List.of(1, 2, 3, 4, 5, 6, 7), result.stream().map(TaskChunkEntity::getChunkIndex).toList());
long distinctIds = result.stream().map(TaskChunkEntity::getId).distinct().count();
@@ -116,8 +117,8 @@ class SimilarAsinTaskServiceChunkKeysetTest {
// 重复执行同一输入:每轮都从 keyset=0 开始,结果一致
List<TaskChunkEntity> all = chunks(1, 1, 2, 2, 3, 3);
stubKeysetPages(all, 500);
List<TaskChunkEntity> first = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
List<TaskChunkEntity> second = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
List<TaskChunkEntity> first = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
List<TaskChunkEntity> second = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
assertEquals(first.size(), second.size());
for (int i = 0; i < first.size(); i++) {
assertEquals(first.get(i).getId(), second.get(i).getId());
@@ -129,7 +130,7 @@ class SimilarAsinTaskServiceChunkKeysetTest {
void test_task_009_chunk_boundary_empty_input() {
// 空集合:返回空列表,不创建无效资源,且只查一轮
stubKeysetPages(List.of(), 500);
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500);
assertNotNull(result);
assertEquals(0, result.size());
verify(taskChunkMapper, times(1)).selectList(any());
@@ -140,7 +141,7 @@ class SimilarAsinTaskServiceChunkKeysetTest {
// 单 chunk:一轮返回后 keyset 推进即拉空,不依赖批量路径
List<TaskChunkEntity> all = chunks(42, 9);
stubKeysetPages(all, 1);
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 1);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 1);
assertEquals(1, result.size());
assertEquals(9, result.get(0).getChunkIndex());
assertEquals(42L, result.get(0).getId());
@@ -151,28 +152,28 @@ class SimilarAsinTaskServiceChunkKeysetTest {
// chunk 数恰好等于 pageSize 的倍数:最后一轮仍返回非空才继续,全部取回
List<TaskChunkEntity> all = chunks(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6);
stubKeysetPages(all, 3);
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
assertEquals(6, result.size());
// pageSize 为 0/负数:回退默认 500,不抛异常
stubKeysetPages(all, 0);
List<TaskChunkEntity> fallback = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 0);
List<TaskChunkEntity> fallback = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 0);
assertEquals(6, fallback.size());
stubKeysetPages(all, -5);
List<TaskChunkEntity> negative = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, -5);
List<TaskChunkEntity> negative = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, -5);
assertEquals(6, negative.size());
}
@Test
void test_task_009_chunk_invalid_input_rejected() {
// taskId 为 null:安全返回空列表,不发起查询
List<TaskChunkEntity> result = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, null, MODULE, 500);
List<TaskChunkEntity> result = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, null, MODULE, 500);
assertNotNull(result);
assertEquals(0, result.size());
verify(taskChunkMapper, times(0)).selectList(any());
// mapper 查询抛异常:转项目约定异常,消息可识别
when(taskChunkMapper.selectList(any())).thenThrow(new IllegalStateException("db down"));
BusinessException ex = assertThrows(BusinessException.class,
() -> SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500));
() -> SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 500));
assertTrue(ex.getMessage() != null && ex.getMessage().contains("chunk"),
"异常消息必须可识别,实际: " + ex.getMessage());
}
@@ -192,12 +193,12 @@ class SimilarAsinTaskServiceChunkKeysetTest {
return all.subList(3, 5);
}).when(taskChunkMapper).selectList(any());
BusinessException ex = assertThrows(BusinessException.class,
() -> SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3));
() -> SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3));
assertTrue(ex.getMessage() != null && ex.getMessage().contains("chunk"),
"异常消息必须可识别,实际: " + ex.getMessage());
// 恢复后重试成功:5 个 chunk 全部取回
stubKeysetPages(all, 3);
List<TaskChunkEntity> recovered = SimilarAsinTaskService.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
List<TaskChunkEntity> recovered = SimilarAsinChunkMergeSupport.loadChunksKeyset(taskChunkMapper, 7004L, MODULE, 3);
assertEquals(5, recovered.size());
assertEquals(List.of(1, 2, 3, 4, 5), recovered.stream().map(TaskChunkEntity::getChunkIndex).toList());
}
@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.similarasin.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.similarasin.service.support.LlmBatchContext;
import com.nanri.aiimage.config.SimilarAsinProperties;
import com.nanri.aiimage.modules.similarasin.mapper.SimilarAsinFilterConditionMapper;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
@@ -189,8 +190,8 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
return new ObjectMapper().writeValueAsString(rows);
}
private SimilarAsinTaskService.LlmBatchContext context(int batchTotal) {
return new SimilarAsinTaskService.LlmBatchContext(
private LlmBatchContext context(int batchTotal) {
return new LlmBatchContext(
7101L, 7201L, null, null, 1, batchTotal, "test-instance", 0, CREDENTIAL, null);
}
@@ -227,7 +228,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
bufferOrMerge.invoke(service, state(task, 1L, "DONE", 1), context(1), rows, task, Map.of());
@@ -281,7 +282,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
TaskScopeStateEntity state = state(task, 1L, "DONE", 2);
@@ -300,7 +301,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
FileTaskEntity task = task();
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
bufferOrMerge.invoke(service, state(task, 1L, "DONE", 1), context(1), null, task, Map.of());
@@ -317,7 +318,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
List<SimilarAsinResultRowDto> rows = List.of(row("r1", "1", "B0A0000001", "英国", "Title 1"));
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
bufferOrMerge.invoke(service, state(task, 1L, "DONE", 1), context(1), rows, task, Map.of());
@@ -336,7 +337,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
List<SimilarAsinResultRowDto> rows = List.of(row("r1", "1", "B0A0000001", "英国", "Title 1"));
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
bufferOrMerge.invoke(service, state(task, 1L, "DONE", 1), context(1), rows, task, Map.of());
@@ -356,7 +357,7 @@ class SimilarAsinTaskServiceCozeBufferScopeTest {
List<SimilarAsinResultRowDto> rows = List.of(row("r1", "1", "B0A0000001", "英国", "Title 1"));
Method bufferOrMerge = SimilarAsinTaskService.class.getDeclaredMethod("bufferLlmRowsOrMerge",
TaskScopeStateEntity.class,
SimilarAsinTaskService.LlmBatchContext.class,
LlmBatchContext.class,
List.class, FileTaskEntity.class, Map.class);
bufferOrMerge.setAccessible(true);
bufferOrMerge.invoke(service, state(task, 1L, "DONE", 1), context(1), rows, task, Map.of());
@@ -10,6 +10,7 @@ import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinSourceFileDto;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParseVo;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedGroupVo;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinPayloadSupport;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinImageEmbedder;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
@@ -244,7 +245,7 @@ class SimilarAsinTaskServiceGroupRefTest {
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
payload.setItems(List.of());
payload.setGroups(List.of());
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(payload);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.resolveAllRows(payload);
assertNotNull(restored);
assertEquals(0, restored.size());
// 引用越界(startIndex 超出 items 范围):安全跳过该组,不抛异常
@@ -255,7 +256,7 @@ class SimilarAsinTaskServiceGroupRefTest {
group.setStartIndex(5);
group.setEndIndex(7);
badRef.setGroups(List.of(group));
List<SimilarAsinParsedRowVo> outOfRange = SimilarAsinTaskService.resolveAllRows(badRef);
List<SimilarAsinParsedRowVo> outOfRange = SimilarAsinPayloadSupport.resolveAllRows(badRef);
assertEquals(0, outOfRange.size(), "越界引用必须安全跳过");
}
@@ -288,7 +289,7 @@ class SimilarAsinTaskServiceGroupRefTest {
String legacy = "{\"allItems\":[{\"rowToken\":\"t1\",\"asin\":\"B0OLD00001\"}],"
+ "\"groups\":[{\"groupKey\":\"g1\",\"startIndex\":0,\"endIndex\":1,\"itemCount\":1}]}";
SimilarAsinParsedPayloadDto legacyPayload = objectMapper.readValue(legacy, SimilarAsinParsedPayloadDto.class);
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(legacyPayload);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.resolveAllRows(legacyPayload);
assertEquals(1, restored.size());
assertEquals("B0OLD00001", restored.get(0).getAsin());
}
@@ -303,7 +304,7 @@ class SimilarAsinTaskServiceGroupRefTest {
bad.setStartIndex(1);
bad.setEndIndex(1);
payload.setGroups(List.of(bad));
assertEquals(0, SimilarAsinTaskService.resolveAllRows(payload).size());
assertEquals(0, SimilarAsinPayloadSupport.resolveAllRows(payload).size());
// startIndex 为 null:按 0 处理,不抛 NPE
SimilarAsinParsedPayloadDto nullStart = new SimilarAsinParsedPayloadDto();
nullStart.setItems(List.of(row("f.xlsx", 1, "f.xlsx::1")));
@@ -312,7 +313,7 @@ class SimilarAsinTaskServiceGroupRefTest {
g.setStartIndex(null);
g.setEndIndex(1);
nullStart.setGroups(List.of(g));
assertEquals(1, SimilarAsinTaskService.resolveAllRows(nullStart).size());
assertEquals(1, SimilarAsinPayloadSupport.resolveAllRows(nullStart).size());
}
@Test
@@ -363,7 +364,7 @@ class SimilarAsinTaskServiceGroupRefTest {
"组 items 首行必须是 items[startIndex]");
}
// Java 内部展开路径不受内嵌 items 影响:仍按引用切片
List<SimilarAsinParsedRowVo> expanded = SimilarAsinTaskService.expandGroupRefs(stored);
List<SimilarAsinParsedRowVo> expanded = SimilarAsinPayloadSupport.expandGroupRefs(stored);
assertEquals(12, expanded.size(), "Java 内部展开仍按引用,不受内嵌 items 影响");
}
@@ -376,7 +377,7 @@ class SimilarAsinTaskServiceGroupRefTest {
+ "{\"rowToken\":\"t2\",\"asin\":\"B0GRP00002\",\"sourceFileKey\":\"f.xlsx\",\"country\":\"英国\"}"
+ "],\"groups\":[{\"groupKey\":\"g1\",\"startIndex\":0,\"endIndex\":2,\"itemCount\":2}]}";
SimilarAsinParsedPayloadDto payload = objectMapper.readValue(legacy, SimilarAsinParsedPayloadDto.class);
SimilarAsinParsedPayloadDto hydrated = SimilarAsinTaskService.hydrateForPythonCompat(payload);
SimilarAsinParsedPayloadDto hydrated = SimilarAsinPayloadSupport.hydrateForPythonCompat(payload);
assertEquals(1, hydrated.getGroups().size());
assertNotNull(hydrated.getGroups().get(0).getItems());
assertEquals(2, hydrated.getGroups().get(0).getItems().size(), "hydrate 后组内嵌 items 必须为 2 行");
@@ -423,6 +424,6 @@ class SimilarAsinTaskServiceGroupRefTest {
copy.setItems(payload.getItems());
copy.setAllItems(payload.getAllItems());
copy.setGroups(payload.getGroups());
return SimilarAsinTaskService.expandGroupRefs(copy);
return SimilarAsinPayloadSupport.expandGroupRefs(copy);
}
}
@@ -4,6 +4,7 @@ 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 com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinPayloadSupport;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -61,7 +62,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> items = rows("uploads/20260829/base.xlsx", 100);
payload.setItems(items);
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(payload);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.resolveAllRows(payload);
assertEquals(100, restored.size());
assertEquals("uploads/20260829/base.xlsx::row::1", restored.get(0).getRowToken());
}
@@ -72,7 +73,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> items = rows("uploads/20260829/multi.xlsx", 1000);
payload.setItems(items);
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(payload);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.resolveAllRows(payload);
assertEquals(1000, restored.size());
for (int i = 0; i < restored.size(); i++) {
assertEquals(i + 1, restored.get(i).getRowIndex());
@@ -80,7 +81,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
// 旧结构 1000 行:allItems 全量恢复
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
legacy.setAllItems(items);
List<SimilarAsinParsedRowVo> restoredLegacy = SimilarAsinTaskService.resolveAllRows(legacy);
List<SimilarAsinParsedRowVo> restoredLegacy = SimilarAsinPayloadSupport.resolveAllRows(legacy);
assertEquals(1000, restoredLegacy.size());
assertEquals("uploads/20260829/multi.xlsx::row::1", restoredLegacy.get(0).getRowToken());
}
@@ -91,8 +92,8 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
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);
List<SimilarAsinParsedRowVo> first = SimilarAsinPayloadSupport.resolveAllRows(payload);
List<SimilarAsinParsedRowVo> second = SimilarAsinPayloadSupport.resolveAllRows(payload);
assertEquals(first.size(), second.size());
for (int i = 0; i < first.size(); i++) {
assertEquals(first.get(i).getRowToken(), second.get(i).getRowToken());
@@ -105,7 +106,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
void test_task_003_payload_boundary_empty_input() {
// 空 payload:返回空列表而非 null,不创建无效资源
SimilarAsinParsedPayloadDto empty = new SimilarAsinParsedPayloadDto();
List<SimilarAsinParsedRowVo> restored = SimilarAsinTaskService.resolveAllRows(empty);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.resolveAllRows(empty);
assertNotNull(restored);
assertEquals(0, restored.size());
// items/allItems/groups 均为空的 payload
@@ -113,7 +114,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
allEmpty.setItems(List.of());
allEmpty.setAllItems(List.of());
allEmpty.setGroups(List.of());
assertEquals(0, SimilarAsinTaskService.resolveAllRows(allEmpty).size());
assertEquals(0, SimilarAsinPayloadSupport.resolveAllRows(allEmpty).size());
}
@Test
@@ -121,11 +122,11 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
// 单行新结构
SimilarAsinParsedPayloadDto payload = new SimilarAsinParsedPayloadDto();
payload.setItems(rows("uploads/20260829/single.xlsx", 1));
assertEquals(1, SimilarAsinTaskService.resolveAllRows(payload).size());
assertEquals(1, SimilarAsinPayloadSupport.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());
assertEquals(1, SimilarAsinPayloadSupport.resolveAllRows(legacy).size());
}
@Test
@@ -133,35 +134,35 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
// 旧结构 allItems 5000 行全量恢复,不丢失
SimilarAsinParsedPayloadDto legacy = new SimilarAsinParsedPayloadDto();
legacy.setAllItems(rows("uploads/20260829/max.xlsx", 5000));
assertEquals(5000, SimilarAsinTaskService.resolveAllRows(legacy).size());
assertEquals(5000, SimilarAsinPayloadSupport.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);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.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());
assertEquals(10, SimilarAsinPayloadSupport.resolveAllRows(both).size());
}
@Test
void test_task_003_payload_invalid_input_rejected() {
// null payload:安全返回空列表(调用方容忍),不抛 NPE
assertEquals(0, SimilarAsinTaskService.resolveAllRows(null).size());
assertEquals(0, SimilarAsinPayloadSupport.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());
assertEquals(5, SimilarAsinPayloadSupport.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());
assertEquals(3, SimilarAsinPayloadSupport.resolveAllRows(nullItems).size());
}
@Test
@@ -173,7 +174,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
+ "{\"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);
List<SimilarAsinParsedRowVo> fromLegacy = SimilarAsinPayloadSupport.resolveAllRows(payload);
assertEquals(2, fromLegacy.size());
assertEquals("t1", fromLegacy.get(0).getRowToken());
assertEquals("B0AAA00002", fromLegacy.get(1).getAsin());
@@ -183,7 +184,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
+ "\"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);
List<SimilarAsinParsedRowVo> fromNew = SimilarAsinPayloadSupport.resolveAllRows(newPayload);
assertEquals(1, fromNew.size());
assertEquals("n1", fromNew.get(0).getRowToken());
// 两种格式行数之和互不影响,恢复结果稳定
@@ -199,7 +200,7 @@ class SimilarAsinTaskServiceResolveAllRowsTest {
// 组内行号各自独立从 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);
List<SimilarAsinParsedRowVo> restored = SimilarAsinPayloadSupport.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());
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.config.SimilarAsinProperties;
import com.nanri.aiimage.modules.similarasin.mapper.SimilarAsinFilterConditionMapper;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinChunkMergeSupport;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinImageEmbedder;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
@@ -166,7 +167,7 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
row("r1", "1", "B0A0000001", "英国"),
row("r1", "1", "B0A0000001", "英国"));
List<SimilarAsinResultRowDto> deduped = service.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> deduped = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(llmRows);
assertEquals(1, deduped.size(), "重复行必须按稳定 rowKey 去重");
assertEquals("r1", deduped.get(0).getRowToken());
@@ -200,7 +201,7 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
llmRows.add(row("r1", "1", "B0A0000001", "英国"));
llmRows.add(row("r2", "2", "B0A0000002", "英国"));
}
List<SimilarAsinResultRowDto> deduped = service.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> deduped = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(llmRows);
assertEquals(2, deduped.size(), "3 轮重复输入去重后只剩 2 个唯一行");
assertEquals(List.of("r1", "r2"), deduped.stream().map(SimilarAsinResultRowDto::getRowToken).toList(),
"去重必须保留首次出现顺序");
@@ -221,8 +222,8 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
row("r1", "1", "B0A0000001", "英国"),
row("r2", "2", "B0A0000002", "英国"),
row("r1", "1", "B0A0000001", "英国"));
List<SimilarAsinResultRowDto> first = service.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> second = service.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> first = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> second = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(llmRows);
assertEquals(first.size(), second.size());
for (int i = 0; i < first.size(); i++) {
assertEquals(first.get(i).getRowToken(), second.get(i).getRowToken());
@@ -233,21 +234,21 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
@Test
void test_task_011_merge_row_key_boundary_empty_input() {
// 空输入:null/空列表安全返回空结果,不创建无效资源
assertNotNull(service.dedupeRowsByRowKey(null));
assertTrue(service.dedupeRowsByRowKey(null).isEmpty());
assertTrue(service.dedupeRowsByRowKey(List.of()).isEmpty());
assertNotNull(SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(null));
assertTrue(SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(null).isEmpty());
assertTrue(SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(List.of()).isEmpty());
// null 元素:跳过不抛异常
List<SimilarAsinResultRowDto> withNull = new ArrayList<>();
withNull.add(null);
withNull.add(row("r1", "1", "B0A0000001", "英国"));
assertEquals(1, service.dedupeRowsByRowKey(withNull).size());
assertEquals(1, SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(withNull).size());
}
@Test
void test_task_011_merge_row_key_boundary_single_item() {
// 单行:不依赖批量路径,去重后结果正确
List<SimilarAsinResultRowDto> single = List.of(row("r1", "1", "B0A0000001", "英国"));
List<SimilarAsinResultRowDto> deduped = service.dedupeRowsByRowKey(single);
List<SimilarAsinResultRowDto> deduped = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(single);
assertEquals(1, deduped.size());
assertEquals("r1", deduped.get(0).getRowToken());
}
@@ -259,14 +260,14 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
for (int i = 0; i < 1000; i++) {
llmRows.add(row("r1", "1", "B0A0000001", "英国"));
}
List<SimilarAsinResultRowDto> deduped = service.dedupeRowsByRowKey(llmRows);
List<SimilarAsinResultRowDto> deduped = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(llmRows);
assertEquals(1, deduped.size());
// 1000 行唯一:全部保留且顺序稳定
List<SimilarAsinResultRowDto> unique = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
unique.add(row("r" + String.format("%04d", i), String.valueOf(i), "B0U" + String.format("%06d", i), "英国"));
}
List<SimilarAsinResultRowDto> dedupedUnique = service.dedupeRowsByRowKey(unique);
List<SimilarAsinResultRowDto> dedupedUnique = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(unique);
assertEquals(1000, dedupedUnique.size());
assertEquals("r0001", dedupedUnique.get(1).getRowToken());
}
@@ -277,13 +278,13 @@ class SimilarAsinTaskServiceRowKeyDedupeTest {
List<SimilarAsinResultRowDto> noToken = List.of(
row("", "1", "B0A0000001", "英国"),
row("", "1", "b0a0000001", " 英国 "));
List<SimilarAsinResultRowDto> deduped = service.dedupeRowsByRowKey(noToken);
List<SimilarAsinResultRowDto> deduped = SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(noToken);
assertEquals(1, deduped.size(), "legacy key 归一化(ASIN 大写、country trim)后应识别为同一行");
// 不同 ASIN:不误判为重复
List<SimilarAsinResultRowDto> diffAsin = List.of(
row("", "1", "B0A0000001", "英国"),
row("", "2", "B0A0000002", "英国"));
assertEquals(2, service.dedupeRowsByRowKey(diffAsin).size());
assertEquals(2, SimilarAsinChunkMergeSupport.dedupeRowsByRowKey(diffAsin).size());
}
@Test
@@ -5,6 +5,7 @@ import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.SimilarAsinProperties;
import com.nanri.aiimage.modules.similarasin.mapper.SimilarAsinFilterConditionMapper;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinChunkMergeSupport;
import com.nanri.aiimage.modules.similarasin.util.SimilarAsinImageEmbedder;
import com.nanri.aiimage.modules.file.service.LocalFileStorageService;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
@@ -217,8 +218,8 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
}
}
List<SimilarAsinResultRowDto> orphans = new ArrayList<>();
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = service.assignLlmRowsToChunks(
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, llmRows, index, null, null, orphans);
assertEquals(3, merged.size());
assertEquals(9, assignedRowKeys(merged).size());
@@ -234,10 +235,10 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
Map<String, Map<String, SimilarAsinResultRowDto>> rowsByChunk = rowsByChunkOf("hashA", 1,
List.of(row("r1", "1", "B0A0000001", "英国"), row("r2", "2", "B0A0000002", "英国")));
List<SimilarAsinResultRowDto> llmRows = List.of(row("r1", "1", "B0A0000001", "英国"));
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> first = service.assignLlmRowsToChunks(
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> first = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, llmRows, index, null, null, new ArrayList<>());
Map<String, Map<String, SimilarAsinResultRowDto>> second = service.assignLlmRowsToChunks(
Map<String, Map<String, SimilarAsinResultRowDto>> second = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, llmRows, index, null, null, new ArrayList<>());
assertEquals(assignedRowKeys(first), assignedRowKeys(second));
assertEquals(first.size(), second.size());
@@ -249,20 +250,20 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
@Test
void test_task_010_chunk_row_key_boundary_empty_input() {
// 空输入:null/空 rowsByChunk 与 llmRows 均安全返回空结果,不创建无效资源
assertNotNull(service.indexRowsByChunkKey(null));
assertTrue(service.indexRowsByChunkKey(null).isEmpty());
assertTrue(service.indexRowsByChunkKey(Map.of()).isEmpty());
Map<String, Map<String, SimilarAsinResultRowDto>> emptyAssign = service.assignLlmRowsToChunks(
assertNotNull(SimilarAsinChunkMergeSupport.indexRowsByChunkKey(null));
assertTrue(SimilarAsinChunkMergeSupport.indexRowsByChunkKey(null).isEmpty());
assertTrue(SimilarAsinChunkMergeSupport.indexRowsByChunkKey(Map.of()).isEmpty());
Map<String, Map<String, SimilarAsinResultRowDto>> emptyAssign = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
Map.of(), List.of(), Map.of(), null, null, new ArrayList<>());
assertTrue(emptyAssign.isEmpty());
assertTrue(service.assignLlmRowsToChunks(
assertTrue(SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
Map.of(), null, Map.of(), null, null, new ArrayList<>()).isEmpty());
// 无可匹配行(rowKey 不存在于任何 chunk)→ 进 orphan 兜底,不产生 merge
Map<String, Map<String, SimilarAsinResultRowDto>> rowsByChunk = rowsByChunkOf("hashA", 1, List.of(row("r1", "1", "B0A0000001", "英国")));
List<SimilarAsinResultRowDto> blankRow = List.of(row("", "", "", ""));
List<SimilarAsinResultRowDto> orphans = new ArrayList<>();
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = service.assignLlmRowsToChunks(
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, blankRow, index, null, null, orphans);
assertTrue(assignedRowKeys(merged).isEmpty());
assertEquals(1, orphans.size(), "全空行生成 legacy key :::: 不命中任何 chunk,按既有语义进 orphan");
@@ -274,8 +275,8 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
Map<String, Map<String, SimilarAsinResultRowDto>> rowsByChunk = rowsByChunkOf("hashA", 1,
List.of(row("r1", "1", "B0A0000001", "英国")));
List<SimilarAsinResultRowDto> orphans = new ArrayList<>();
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = service.assignLlmRowsToChunks(
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, List.of(row("r1", "1", "B0A0000001", "英国")), index, null, null, orphans);
assertEquals(1, merged.size());
assertEquals(List.of("r1"), assignedRowKeys(merged));
@@ -294,14 +295,14 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
bigChunk.put("r" + String.format("%04d", i), row("r" + String.format("%04d", i), String.valueOf(i), "B0L" + String.format("%06d", i), "英国"));
}
rowsByChunk.put("hashBig:1", bigChunk);
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
assertEquals(1000, index.size());
List<SimilarAsinResultRowDto> llmRows = new ArrayList<>();
for (int i = 1; i <= 500; i++) {
llmRows.add(row("r" + String.format("%04d", i), String.valueOf(i), "B0L" + String.format("%06d", i), "英国"));
}
List<SimilarAsinResultRowDto> orphans = new ArrayList<>();
Map<String, Map<String, SimilarAsinResultRowDto>> merged = service.assignLlmRowsToChunks(
Map<String, Map<String, SimilarAsinResultRowDto>> merged = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, llmRows, index, null, null, orphans);
assertEquals(1, merged.size());
assertEquals(500, assignedRowKeys(merged).size());
@@ -314,11 +315,11 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
Map<String, Map<String, SimilarAsinResultRowDto>> rowsByChunk = new LinkedHashMap<>();
rowsByChunk.putAll(rowsByChunkOf("hashA", 1, List.of(row("dup", "1", "B0A0000001", "英国"))));
rowsByChunk.putAll(rowsByChunkOf("hashB", 2, List.of(row("dup", "1", "B0A0000001", "英国"))));
Map<String, String> index = service.indexRowsByChunkKey(rowsByChunk);
Map<String, String> index = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
assertEquals("hashA:1", index.get("dup"), "重复 rowKey 应保留第一个 chunk");
// fallback 缺失:llm 行未命中且无有效 fallback → 进 orphan,不产生 merge
List<SimilarAsinResultRowDto> orphans = new ArrayList<>();
Map<String, Map<String, SimilarAsinResultRowDto>> merged = service.assignLlmRowsToChunks(
Map<String, Map<String, SimilarAsinResultRowDto>> merged = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, List.of(row("ghost", "9", "B0A0000009", "英国")), Map.of(), "missingHash", 99, orphans);
assertTrue(assignedRowKeys(merged).isEmpty());
assertEquals(1, orphans.size());
@@ -328,8 +329,8 @@ class SimilarAsinTaskServiceRowKeyIndexTest {
withNull.add(null);
withNull.add(row("dup", "1", "B0A0000001", "英国"));
List<SimilarAsinResultRowDto> orphans2 = new ArrayList<>();
Map<String, String> index2 = service.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged2 = service.assignLlmRowsToChunks(
Map<String, String> index2 = SimilarAsinChunkMergeSupport.indexRowsByChunkKey(rowsByChunk);
Map<String, Map<String, SimilarAsinResultRowDto>> merged2 = SimilarAsinChunkMergeSupport.assignLlmRowsToChunks(
rowsByChunk, withNull, index2, null, null, orphans2);
assertEquals(1, merged2.size());
assertEquals(List.of("dup"), assignedRowKeys(merged2));
@@ -2,6 +2,9 @@ package com.nanri.aiimage.modules.similarasin.service;
import com.nanri.aiimage.modules.similarasin.model.dto.SimilarAsinResultRowDto;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinParsedRowVo;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinResultTextSupport;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinTaskProgressSupport;
import com.nanri.aiimage.modules.similarasin.service.support.SimilarAsinResultWorkbookAssembler;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
@@ -17,7 +20,7 @@ class SimilarAsinTaskServiceTest {
@Test
@SuppressWarnings("unchecked")
void resultWorkbookRestoresPriceAfterCountryAndShiftsImageColumns() throws Exception {
Field headersField = SimilarAsinTaskService.class.getDeclaredField("RESULT_HEADERS");
Field headersField = SimilarAsinResultWorkbookAssembler.class.getDeclaredField("RESULT_HEADERS");
headersField.setAccessible(true);
List<String> headers = (List<String>) headersField.get(null);
@@ -32,26 +35,26 @@ class SimilarAsinTaskServiceTest {
@Test
void resultStatusUsesReturnedCozeDataAndImages() {
assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(null));
assertEquals("\u5931\u8d25", SimilarAsinResultTextSupport.resolveResultStatus(null));
SimilarAsinResultRowDto empty = new SimilarAsinResultRowDto();
assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(empty));
assertEquals("\u5931\u8d25", SimilarAsinResultTextSupport.resolveResultStatus(empty));
SimilarAsinResultRowDto withImage = new SimilarAsinResultRowDto();
withImage.setStatus("FAILED");
withImage.setMainUrl("https://example.com/main.jpg");
assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withImage));
assertEquals("\u6210\u529f", SimilarAsinResultTextSupport.resolveResultStatus(withImage));
SimilarAsinResultRowDto withCozeStatus = new SimilarAsinResultRowDto();
withCozeStatus.setStatus("success");
assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withCozeStatus));
assertEquals("\u6210\u529f", SimilarAsinResultTextSupport.resolveResultStatus(withCozeStatus));
SimilarAsinResultRowDto failedStatus = new SimilarAsinResultRowDto();
failedStatus.setStatus("FAILED");
assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(failedStatus));
assertEquals("\u5931\u8d25", SimilarAsinResultTextSupport.resolveResultStatus(failedStatus));
SimilarAsinResultRowDto withVisibleResultData = new SimilarAsinResultRowDto();
assertEquals("\u6210\u529f", SimilarAsinTaskService.resolveResultStatus(withVisibleResultData, "", "80%", "", "", ""));
assertEquals("\u6210\u529f", SimilarAsinResultTextSupport.resolveResultStatus(withVisibleResultData, "", "80%", "", "", ""));
}
@Test
@@ -62,39 +65,39 @@ class SimilarAsinTaskServiceTest {
notFound.setError("404 Not Found");
notFound.setStatus("FAILED");
notFound.setReason("required field missing (asin/title/url), skip LLM");
assertEquals("ASIN\u672a\u627e\u5230", SimilarAsinTaskService.resolveResultStatus(notFound));
assertEquals("ASIN\u672a\u627e\u5230", SimilarAsinResultTextSupport.resolveResultStatus(notFound));
// \u5176\u5b83\u7b49\u4ef7\u6587\u6848\u540c\u6837\u547d\u4e2d
SimilarAsinResultRowDto chinese = new SimilarAsinResultRowDto();
chinese.setError("\u5546\u54c1\u672a\u627e\u5230");
assertEquals("ASIN\u672a\u627e\u5230", SimilarAsinTaskService.resolveResultStatus(chinese));
assertEquals("ASIN\u672a\u627e\u5230", SimilarAsinResultTextSupport.resolveResultStatus(chinese));
// \u666e\u901a\u6280\u672f\u6027\u5931\u8d25\u4e0d\u8bef\u5224\uff1aerror \u4e0d\u542b 404/not found
SimilarAsinResultRowDto llmFailure = new SimilarAsinResultRowDto();
llmFailure.setError("LLM \u8c03\u7528\u8d85\u65f6");
llmFailure.setStatus("FAILED");
assertEquals("\u5931\u8d25", SimilarAsinTaskService.resolveResultStatus(llmFailure));
assertEquals("\u5931\u8d25", SimilarAsinResultTextSupport.resolveResultStatus(llmFailure));
}
@Test
void asinNotFoundDetectionMatchesPythonErrors() {
assertTrue(SimilarAsinTaskService.isAsinNotFound("404 Not Found"));
assertTrue(SimilarAsinTaskService.isAsinNotFound("404 not found"));
assertTrue(SimilarAsinTaskService.isAsinNotFound("404"));
assertTrue(SimilarAsinTaskService.isAsinNotFound("\u5546\u54c1\u672a\u627e\u5230"));
assertTrue(SimilarAsinTaskService.isAsinNotFound("ASIN \u672a\u627e\u5230"));
assertFalse(SimilarAsinTaskService.isAsinNotFound(null));
assertFalse(SimilarAsinTaskService.isAsinNotFound(""));
assertFalse(SimilarAsinTaskService.isAsinNotFound("LLM \u8c03\u7528\u8d85\u65f6"));
assertFalse(SimilarAsinTaskService.isAsinNotFound("\u4e0a\u4f20\u5931\u8d25"));
assertTrue(SimilarAsinResultTextSupport.isAsinNotFound("404 Not Found"));
assertTrue(SimilarAsinResultTextSupport.isAsinNotFound("404 not found"));
assertTrue(SimilarAsinResultTextSupport.isAsinNotFound("404"));
assertTrue(SimilarAsinResultTextSupport.isAsinNotFound("\u5546\u54c1\u672a\u627e\u5230"));
assertTrue(SimilarAsinResultTextSupport.isAsinNotFound("ASIN \u672a\u627e\u5230"));
assertFalse(SimilarAsinResultTextSupport.isAsinNotFound(null));
assertFalse(SimilarAsinResultTextSupport.isAsinNotFound(""));
assertFalse(SimilarAsinResultTextSupport.isAsinNotFound("LLM \u8c03\u7528\u8d85\u65f6"));
assertFalse(SimilarAsinResultTextSupport.isAsinNotFound("\u4e0a\u4f20\u5931\u8d25"));
}
@Test
void fileBuildProgressIsTerminalOnlyAfterTaskAndStageComplete() {
assertTrue(SimilarAsinTaskService.isTerminalFileBuildProgress("SUCCESS", 3, 3));
assertTrue(SimilarAsinTaskService.isTerminalFileBuildProgress("FAILED", 3, 3));
assertFalse(SimilarAsinTaskService.isTerminalFileBuildProgress("RUNNING", 3, 3));
assertFalse(SimilarAsinTaskService.isTerminalFileBuildProgress("SUCCESS", 2, 3));
assertTrue(SimilarAsinTaskProgressSupport.isTerminalFileBuildProgress("SUCCESS", 3, 3));
assertTrue(SimilarAsinTaskProgressSupport.isTerminalFileBuildProgress("FAILED", 3, 3));
assertFalse(SimilarAsinTaskProgressSupport.isTerminalFileBuildProgress("RUNNING", 3, 3));
assertFalse(SimilarAsinTaskProgressSupport.isTerminalFileBuildProgress("SUCCESS", 2, 3));
}
@Test
@@ -149,7 +152,7 @@ class SimilarAsinTaskServiceTest {
}
private int staticIntField(String name) throws Exception {
Field field = SimilarAsinTaskService.class.getDeclaredField(name);
Field field = SimilarAsinResultWorkbookAssembler.class.getDeclaredField(name);
field.setAccessible(true);
return field.getInt(null);
}