From c282327a0b1b598d8db3a39d5f955bda8a750abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 09:10:51 +0800 Subject: [PATCH] =?UTF-8?q?task-207(=E6=9C=8D=E5=8A=A1=E5=B1=82):=20done?= =?UTF-8?q?=3Dtrue=20=E7=BB=88=E6=80=81=E5=86=B3=E7=AD=96=E7=BA=AF?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=EF=BC=88SimilarAsinSubmitResultSemantics?= =?UTF-8?q?=EF=BC=9Adone/error=E2=86=92=E7=BB=88=E6=80=81=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E3=80=81chunkIndex/Total=20=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E5=BD=92=E4=B8=80=EF=BC=89=E6=8E=A5=E7=BA=BF=E5=9B=9E=20submit?= =?UTF-8?q?=20+=208=20=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 3+2 约定:文件生成/终态落库依赖真实 LLM 流水线,留待真实运行时经 env-gated 回放(CONTRACT_* 已就绪) --- .../SimilarAsinSubmitResultSemantics.java | 31 ++++++++ .../service/SimilarAsinTaskService.java | 9 ++- .../SimilarAsinSubmitResultSemanticsTest.java | 76 +++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java new file mode 100644 index 00000000..6ef8294e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemantics.java @@ -0,0 +1,31 @@ +package com.nanri.aiimage.modules.similarasin.model; + +/** + * similarasin 结果提交语义的纯计算(task-207 提取,防业务漂移)。 + * + * done=true / error 非空 => 本批为终态回调(触发读元数据/终态处理); + * chunkIndex/chunkTotal 缺省归一(0/1)。纯函数,便于离线单测。 + */ +public final class SimilarAsinSubmitResultSemantics { + + private SimilarAsinSubmitResultSemantics() { + } + + /** 是否终态回调:done=true 或 error 非空。 */ + public static boolean isTerminalRequest(boolean done, String error) { + if (done) { + return true; + } + return error != null && !error.isBlank(); + } + + /** chunkIndex 缺省(含 <=0 之外的 null)归一为 0(与既有 submit 逻辑一致)。 */ + public static int chunkIndex(Integer chunkIndex) { + return chunkIndex == null ? 0 : chunkIndex; + } + + /** chunkTotal 缺省归一为 1。 */ + public static int chunkTotal(Integer chunkTotal) { + return chunkTotal == null ? 1 : chunkTotal; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java index 64c7ede9..809a0883 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinTaskService.java @@ -1017,12 +1017,15 @@ public class SimilarAsinTaskService { } ensureTaskOwnedByCurrentInstance(task, "submit result"); - int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex(); - int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal(); + int chunkIndex = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .chunkIndex(request.getChunkIndex()); + int chunkTotal = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .chunkTotal(request.getChunkTotal()); boolean done = Boolean.TRUE.equals(request.getDone()); String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId); String scopeHash = DigestUtil.sha256Hex(scopeKey); - boolean terminalCallback = done || request.getError() != null && !request.getError().isBlank(); + boolean terminalCallback = com.nanri.aiimage.modules.similarasin.model.SimilarAsinSubmitResultSemantics + .isTerminalRequest(done, request.getError()); SubmittedTaskMetadata taskMetadata = terminalCallback ? readSubmittedTaskMetadata(task) : null; diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java new file mode 100644 index 00000000..b9b5c360 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/model/SimilarAsinSubmitResultSemanticsTest.java @@ -0,0 +1,76 @@ +package com.nanri.aiimage.modules.similarasin.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-207(服务层部分):done=true 最后一批语义纯单测。 + * + * 覆盖 done/error→终态回调决策、chunkIndex/chunkTotal 缺省归一、末片判定;并解析 + * python-contract/result.request.json(done=true)断言终态触发。文件生成(xlsx/终态落库) + * 依赖真实 LLM 流水线,按 3+2 约定在真实运行时经 env-gated 回放验证(本类不依赖 DB)。 + */ +class SimilarAsinSubmitResultSemanticsTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void doneTrueIsTerminalEvenWithoutError() { + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(true, null)); + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(true, "")); + } + + @Test + void errorNonBlankIsTerminal() { + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, "source timeout")); + } + + @Test + void runningChunkWithoutDoneOrErrorIsNotTerminal() { + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, null)); + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, " ")); + } + + @Test + void blankOrNullErrorNotTerminalWhenNotDone() { + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, "")); + assertFalse(SimilarAsinSubmitResultSemantics.isTerminalRequest(false, " ")); + } + + @Test + void chunkIndexNullDefaultsZero() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(null) == 0); + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(3) == 3); + } + + @Test + void chunkTotalNullDefaultsOne() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(null) == 1); + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(5) == 5); + } + + @Test + void explicitValuesPassedThrough() { + assertTrue(SimilarAsinSubmitResultSemantics.chunkIndex(7) == 7); + assertTrue(SimilarAsinSubmitResultSemantics.chunkTotal(2) == 2); + } + + @Test + void fixtureDoneTrueRequestIsTerminal() throws Exception { + Path fixture = Path.of(System.getProperty("user.dir"), + "src/test/resources/python-contract/result.request.json"); + JsonNode req = MAPPER.readTree(Files.readAllBytes(fixture)); + boolean done = req.path("done").asBoolean(false); + String error = req.path("error").isNull() ? null : req.path("error").asText(); + assertTrue(done, "夹具 done 应为 true"); + assertTrue(SimilarAsinSubmitResultSemantics.isTerminalRequest(done, error), + "done=true 的提交应触发终态回调"); + } +}