diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java index bc4b8ef9..4f8e7086 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/collectdata/service/CollectDataService.java @@ -803,10 +803,15 @@ public class CollectDataService { CollectDataBrandBatchFilter.BrandBatchOutcome outcome = brandBatchFilter.filter(rows); stats.brandRejectedCount += outcome.rejected().size(); stats.brandQueryFailedCount += outcome.queryFailed().size(); - List invalidRows = new ArrayList<>(outcome.rejected().size() + outcome.queryFailed().size()); - invalidRows.addAll(outcome.rejected()); - invalidRows.addAll(outcome.queryFailed()); - invalidAsinBatchWriter.writeBatch(invalidRows); + // 只有被服务端判定为无效品牌(rejected)的行才写入无效品牌表; + // queryFailed 是品牌检测服务端到端失败(如缺 X-Token 422/超时/限流), + // 一并写入会把故障期间被误伤的品牌永久拉黑,后续同品牌商品全部被 + // invalidFiltered 过滤(task-27265 实测:通过 Python 过滤的 21 行中 + // 8 行查询失败被写表后误杀,最终结果 Excel 0 行)。rejected 为空时 + // 不触发任何写入,避免空批次无意义调用。 + if (!outcome.rejected().isEmpty()) { + invalidAsinBatchWriter.writeBatch(outcome.rejected()); + } return outcome.accepted(); } diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTxBoundaryTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTxBoundaryTest.java index 3af94eda..f83113d3 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTxBoundaryTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/collectdata/service/CollectDataServiceTxBoundaryTest.java @@ -208,6 +208,30 @@ class CollectDataServiceTxBoundaryTest { verify(invalidAsinBatchWriter).writeBatch(any()); } + @Test + void brandQueryFailedRowsNotWrittenToInvalidWriter() { + CollectDataResultRowVo queryFailed = new CollectDataResultRowVo(); + queryFailed.setAsin("B0QUERYFAIL"); + when(brandBatchFilter.filter(any())).thenReturn( + new CollectDataBrandBatchFilter.BrandBatchOutcome(List.of(), List.of(queryFailed), List.of())); + + service.submitResult(TASK_ID, submitRequest()); + + verify(invalidAsinBatchWriter, never()).writeBatch(any()); + } + + @Test + void emptyRejectedOutcomeSkipsInvalidWriter() { + CollectDataResultRowVo accepted = new CollectDataResultRowVo(); + accepted.setAsin("B0OK"); + when(brandBatchFilter.filter(any())).thenReturn( + new CollectDataBrandBatchFilter.BrandBatchOutcome(List.of(), List.of(), List.of(accepted))); + + service.submitResult(TASK_ID, submitRequest()); + + verify(invalidAsinBatchWriter, never()).writeBatch(any()); + } + @Test void rustfsFailureAbortsWithoutPartialChunk() { when(transientPayloadStorageService.extractPointer(anyString())).thenReturn("local:/tmp/x");