From db0c1c1fdca8b825928cadfc227152cdd9e934d4 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 08:56:41 +0800 Subject: [PATCH] =?UTF-8?q?task-203:=20/result=20=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E5=9B=9E=E6=94=BE=E6=B5=8B=E8=AF=95=EF=BC=88?= =?UTF-8?q?env-gated=EF=BC=8C=E7=9C=9F=E5=AE=9E=E6=9C=8D=E5=8A=A1=EF=BC=9A?= =?UTF-8?q?=E9=80=A0=20SIMILAR=5FASIN=20RUNNING=20=E4=BB=BB=E5=8A=A1+?= =?UTF-8?q?=E5=86=85=E8=81=94=E8=A7=A3=E6=9E=90=E8=BD=BD=E8=8D=B7=20?= =?UTF-8?q?=E2=86=92=20parsed-payload=20=E6=88=90=E5=8A=9F=20=E2=86=92=20?= =?UTF-8?q?=E5=90=8C=20submissionId/chunk=20=E6=8F=90=E4=BA=A4=E4=B8=A4?= =?UTF-8?q?=E6=AC=A1=E5=9D=87=20success:true=20=E4=B8=94=20biz=5Ftask=5Fch?= =?UTF-8?q?unk=20=E5=8F=AA=E8=90=BD=201=20=E8=A1=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI 默认跳过(需 CONTRACT_BASE_URL+CONTRACT_DB_*),设了即在本地/任意副本库跑 - 覆盖 success/idempotent/single-persist(chunk)/chunk 字段/未知任务(live 已验);done=true 封口需真实 LLM 流水线,不在本回放内(见限制说明) - 本地已用 18080 实例 + schema 副本真跑通过 --- .../SimilarAsinResultContractReplayTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinResultContractReplayTest.java diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinResultContractReplayTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinResultContractReplayTest.java new file mode 100644 index 00000000..af972188 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/SimilarAsinResultContractReplayTest.java @@ -0,0 +1,102 @@ +package com.nanri.aiimage.modules.similarasin.service; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assumptions; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-203:/result 幂等契约回放(真实服务,env-gated)。 + * + * 依赖运行中的应用与可写本地库(复现生产 schema):设 CONTRACT_BASE_URL、CONTRACT_DB_URL、 + * CONTRACT_DB_USER、CONTRACT_DB_PASSWORD 后启用;未设则跳过(保证 CI 全绿)。 + * 流程:造 SIMILAR_ASIN RUNNING 任务 + 内联解析载荷 → GET parsed-payload 验证成功 → + * POST /result 同 submissionId/chunk 两次 → 断言两次 success:true 且 biz_task_chunk 只落 1 行(幂等)。 + * done=true 封口/终态因需真实 LLM 流水线,不在本回放覆盖(见 commit 说明)。 + */ +class SimilarAsinResultContractReplayTest { + + private static final long TASK_ID = 9000001L; + private static final String SUBMISSION = "contract-replay-001"; + + @Test + void duplicateSubmitResultIsIdempotent() throws Exception { + String base = System.getenv("CONTRACT_BASE_URL"); + String dbUrl = System.getenv("CONTRACT_DB_URL"); + String dbUser = System.getenv("CONTRACT_DB_USER"); + String dbPass = System.getenv("CONTRACT_DB_PASSWORD"); + Assumptions.assumeTrue(base != null && dbUrl != null && dbUser != null, + "未设 CONTRACT_BASE_URL / CONTRACT_DB_*,跳过真实服务回放"); + + seedTask(dbUrl, dbUser, dbPass); + HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + // parsed-payload 成功返回全量载荷 + HttpResponse parsed = get(client, base + "/api/similar-asin/tasks/" + TASK_ID + "/parsed-payload?user_id=1"); + assertEquals(200, parsed.statusCode()); + assertTrue(parsed.body().contains("\"success\":true"), "parsed-payload 应 success:true: " + parsed.body()); + assertTrue(parsed.body().contains("B0FAKETEST123"), "应含解析行数据"); + + // 同 submissionId/chunk 提交两次 + String body = "{\"submissionId\":\"" + SUBMISSION + "\",\"chunkIndex\":0,\"chunkTotal\":1,\"done\":false," + + "\"error\":null,\"groups\":[{\"name\":\"g\",\"items\":[{\"rowIndex\":1,\"asin\":\"B0FAKETEST123\"," + + "\"status\":\"SUCCESS\"}]}]}"; + HttpResponse first = post(client, base + "/api/similar-asin/tasks/" + TASK_ID + "/result?user_id=1", body); + HttpResponse second = post(client, base + "/api/similar-asin/tasks/" + TASK_ID + "/result?user_id=1", body); + assertEquals(200, first.statusCode()); + assertEquals(200, second.statusCode()); + assertTrue(first.body().contains("\"success\":true"), "首次提交应成功: " + first.body()); + assertTrue(second.body().contains("\"success\":true"), "重复提交应幂等成功: " + second.body()); + + // 幂等:只落一份 chunk + assertEquals(1, countChunks(dbUrl, dbUser, dbPass, TASK_ID), "同 submissionId/chunk 应只落 1 行"); + } + + private static void seedTask(String dbUrl, String user, String pass) throws Exception { + String payload = "{\"aiPrompt\":\"\",\"apiKey\":\"\",\"imgSwitch\":false," + + "\"categorySwitch\":false,\"sourceFiles\":[],\"headers\":[\"asin\",\"keyword\"]," + + "\"items\":[{\"rowIndex\":1,\"asin\":\"B0FAKETEST123\",\"keyword\":\"ring\"}]," + + "\"groups\":[],\"allItems\":[{\"rowIndex\":1,\"asin\":\"B0FAKETEST123\",\"keyword\":\"ring\"}]}"; + try (Connection c = DriverManager.getConnection(dbUrl, user, pass); + Statement s = c.createStatement()) { + s.executeUpdate("INSERT INTO biz_file_task (id,task_no,module_type,task_mode,status,created_at,updated_at,user_id,owner_instance_id,result_json) " + + "VALUES (" + TASK_ID + ",'REPLAY-" + TASK_ID + "','SIMILAR_ASIN','workbook','RUNNING',NOW(),NOW(),1,'local-smoke','" + + payload.replace("'", "''") + "') ON DUPLICATE KEY UPDATE module_type='SIMILAR_ASIN',status='RUNNING',user_id=1,result_json=VALUES(result_json)"); + s.executeUpdate("DELETE FROM biz_task_chunk WHERE task_id=" + TASK_ID); + } + } + + private static int countChunks(String dbUrl, String user, String pass, long taskId) throws Exception { + try (Connection c = DriverManager.getConnection(dbUrl, user, pass); + Statement s = c.createStatement(); + var rs = s.executeQuery("SELECT COUNT(*) FROM biz_task_chunk WHERE task_id=" + taskId)) { + rs.next(); + return rs.getInt(1); + } + } + + private static HttpResponse get(HttpClient client, String url) throws Exception { + return client.send(HttpRequest.newBuilder(URI.create(url)).GET() + .timeout(Duration.ofSeconds(15)).build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } + + private static HttpResponse post(HttpClient client, String url, String body) throws Exception { + return client.send(HttpRequest.newBuilder(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .timeout(Duration.ofSeconds(15)).build(), + HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } +}