diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/softwareversion/service/support/PublicVersionLookup.java b/backend-java/src/main/java/com/nanri/aiimage/modules/softwareversion/service/support/PublicVersionLookup.java new file mode 100644 index 00000000..83da6cd6 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/softwareversion/service/support/PublicVersionLookup.java @@ -0,0 +1,74 @@ +package com.nanri.aiimage.modules.softwareversion.service.support; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 客户端公开版本查询纯逻辑(module 09):空语义、created_at DESC + id 稳定排序、 + * 无 ApiResponse 包装;供 PublicVersionController/契约测试复用。 + */ +public final class PublicVersionLookup { + + /** web_config 版本记录投影(id 用于同时间稳定排序)。 */ + public record WebVersion(Long id, String version, String fileUrl, LocalDateTime createdAt) { + } + + private PublicVersionLookup() { + } + + /** 取最新版本记录:created_at DESC,同一时间按 id 倒序稳定;空/无有效回 null。 */ + public static WebVersion selectLatest(List rows) { + if (rows == null || rows.isEmpty()) { + return null; + } + List list = new ArrayList<>(rows); + list.sort((a, b) -> { + int time = compareTime(b.createdAt(), a.createdAt()); + if (time != 0) { + return time; + } + long ai = a.id() == null ? 0L : a.id(); + long bi = b.id() == null ? 0L : b.id(); + return Long.compare(bi, ai); + }); + return list.get(0); + } + + private static int compareTime(LocalDateTime a, LocalDateTime b) { + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + return a.compareTo(b); + } + + /** 当前服务版本响应:保留 version/desc/url 字段(可为空字符串),不包装 ApiResponse。 */ + public static Map currentVersionResponse(String version, String desc, String url) { + Map map = new LinkedHashMap<>(); + map.put("version", version == null ? "" : version); + map.put("desc", desc == null ? "" : desc); + map.put("url", url == null ? "" : url); + return map; + } + + /** 最新版本响应:无记录返回 version=null,file_url=null(客户端语义)。 */ + public static Map latestVersionResponse(WebVersion latest) { + Map map = new LinkedHashMap<>(); + if (latest == null) { + map.put("version", null); + map.put("file_url", null); + return map; + } + map.put("version", latest.version()); + map.put("file_url", latest.fileUrl()); + return map; + } +} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/softwareversion/service/support/Task161Test.java b/backend-java/src/test/java/com/nanri/aiimage/modules/softwareversion/service/support/Task161Test.java new file mode 100644 index 00000000..438feb34 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/softwareversion/service/support/Task161Test.java @@ -0,0 +1,63 @@ +package com.nanri.aiimage.modules.softwareversion.service.support; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** task-161:冻结 /api/version 响应契约(version/desc/url 保留且无 ApiResponse 包装)。 */ +class Task161Test { + + @Test + void test_task_161_freeze_public_version_normal_primary_path() { + Map resp = PublicVersionLookup.currentVersionResponse("1.0.46", "x", "https://a"); + assertEquals("1.0.46", resp.get("version")); + assertEquals("x", resp.get("desc")); + assertEquals("https://a", resp.get("url")); + } + + @Test + void test_task_161_freeze_public_version_normal_variant_input() { + Map resp = PublicVersionLookup.currentVersionResponse(null, null, null); + assertEquals("", resp.get("version")); + assertEquals("", resp.get("desc")); + assertEquals("", resp.get("url")); + } + + @Test + void test_task_161_freeze_public_version_repeated_is_idempotent() { + assertTrue(PublicVersionLookup.currentVersionResponse("v", "d", "u") + .equals(PublicVersionLookup.currentVersionResponse("v", "d", "u"))); + } + + @Test + void test_task_161_freeze_public_version_boundary_empty_input() { + assertTrue(PublicVersionLookup.currentVersionResponse("", "", "").values().stream().allMatch(v -> "".equals(v))); + } + + @Test + void test_task_161_freeze_public_version_boundary_single_item() { + Map resp = PublicVersionLookup.currentVersionResponse("1.0.0", "", ""); + assertEquals(3, resp.size()); + } + + @Test + void test_task_161_freeze_public_version_boundary_limit_or_missing_field() { + assertFalse(PublicVersionLookup.currentVersionResponse("v", "d", "u").containsKey("success"), "公开接口不包装信封"); + } + + @Test + void test_task_161_freeze_public_version_invalid_input_rejected() { + Map resp = PublicVersionLookup.currentVersionResponse("1", "a", "b"); + assertEquals("1", resp.get("version"), "version 不被转数字"); + } + + @Test + void test_task_161_freeze_public_version_dependency_failure_returns_actionable_message() { + // 保留字段即使为空;desc/url 不能丢弃 + assertEquals(3, PublicVersionLookup.currentVersionResponse("v", "", "").keySet().size()); + } +}