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:
+14
-13
@@ -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());
|
||||
}
|
||||
|
||||
+9
-8
@@ -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());
|
||||
|
||||
+9
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-18
@@ -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());
|
||||
|
||||
+14
-13
@@ -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
|
||||
|
||||
+21
-20
@@ -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));
|
||||
|
||||
+27
-24
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user