From e683ef68639aa6ddc0d3a156011620ba6460ddbf Mon Sep 17 00:00:00 2001
From: irving
Date: Thu, 4 Dec 2025 23:12:50 -0500
Subject: [PATCH] test(media): legacy album compatibility for user list and
detail
---
.../api/WxClerkUserBackwardCompatApiTest.java | 206 ++++++++++++++++++
1 file changed, 206 insertions(+)
create mode 100644 play-admin/src/test/java/com/starry/admin/api/WxClerkUserBackwardCompatApiTest.java
diff --git a/play-admin/src/test/java/com/starry/admin/api/WxClerkUserBackwardCompatApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxClerkUserBackwardCompatApiTest.java
new file mode 100644
index 0000000..fac9bd3
--- /dev/null
+++ b/play-admin/src/test/java/com/starry/admin/api/WxClerkUserBackwardCompatApiTest.java
@@ -0,0 +1,206 @@
+package com.starry.admin.api;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.starry.admin.common.apitest.ApiTestDataSeeder;
+import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
+import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
+import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
+import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
+import com.starry.admin.modules.media.enums.MediaStatus;
+import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
+import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
+import com.starry.admin.utils.SecurityUtils;
+import com.starry.common.utils.IdUtils;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MvcResult;
+
+/**
+ * 覆盖微信顾客端店员列表 / 详情在仅存在历史相册数据时的兼容行为。
+ *
+ * 重点校验:
+ *
+ * - 旧字段 {@code album} 在列表接口中仍然可见;
+ * - 同时会被折叠进 {@code mediaList},且不会破坏后续媒资结构(id/url/usage/status/reviewState);
+ * - 详情接口中 {@link PlayClerkUserInfoResultVo#mediaList} 也包含这些历史相册。
+ *
+ *
+ */
+class WxClerkUserBackwardCompatApiTest extends AbstractApiTest {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Autowired
+ private IPlayClerkUserInfoService clerkUserInfoService;
+
+ private final List clerkIdsToCleanup = new ArrayList<>();
+
+ @AfterEach
+ void tearDown() {
+ ensureTenantContext();
+ if (!clerkIdsToCleanup.isEmpty()) {
+ clerkUserInfoService.removeByIds(clerkIdsToCleanup);
+ clerkIdsToCleanup.clear();
+ }
+ }
+
+ /**
+ * 场景:只有旧相册(album) 而没有 play_media / play_clerk_media_asset 记录时,
+ * 微信顾客端列表接口仍然返回可用的 album 和 mediaList。
+ */
+ @Test
+ void customerListIncludesLegacyAlbumAsMediaList() throws Exception {
+ ensureTenantContext();
+ String marker = "legacy-album-" + IdUtils.getUuid().substring(0, 8);
+ List legacyAlbum = List.of(
+ "https://example.com/apitest/legacy/" + marker + "-a.png",
+ "https://example.com/apitest/legacy/" + marker + "-b.png");
+ String clerkId = createLegacyAlbumClerk(marker, legacyAlbum);
+
+ ObjectNode payload = objectMapper.createObjectNode();
+ payload.put("pageNum", 1);
+ payload.put("pageSize", 20);
+ payload.put("nickname", marker);
+ payload.put("typeId", "");
+ payload.put("levelId", "");
+ payload.put("sex", "");
+ payload.put("province", "");
+
+ MvcResult result = mockMvc.perform(post("/wx/clerk/user/queryByPage")
+ .header(TENANT_HEADER, DEFAULT_TENANT)
+ .header(USER_HEADER, DEFAULT_USER)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(payload.toString()))
+ .andExpect(status().isOk())
+ .andReturn();
+
+ String body = result.getResponse().getContentAsString();
+ JsonNode root = objectMapper.readTree(body);
+ assertThat(root.path("code").asInt()).as("response=%s", body).isEqualTo(200);
+
+ JsonNode records = root.path("data").path("records");
+ assertThat(records.isArray()).as("records should be array, response=%s", body).isTrue();
+
+ JsonNode target = null;
+ for (JsonNode record : records) {
+ if (clerkId.equals(record.path("id").asText())) {
+ target = record;
+ break;
+ }
+ }
+ assertThat(target)
+ .withFailMessage("列表中未找到目标店员 %s, response=%s", clerkId, body)
+ .isNotNull();
+
+ // album 字段仍然保留原有的历史值
+ JsonNode albumNode = target.path("album");
+ assertThat(albumNode.isArray()).isTrue();
+ Set albumUrls = new HashSet<>();
+ albumNode.forEach(node -> albumUrls.add(node.asText()));
+ assertThat(albumUrls).containsExactlyInAnyOrderElementsOf(legacyAlbum);
+
+ // mediaList 中应当包含折叠后的 Legacy 媒资,并为其补齐 usage/status/reviewState 等字段
+ JsonNode mediaListNode = target.path("mediaList");
+ assertThat(mediaListNode.isArray()).isTrue();
+ assertThat(mediaListNode.size()).isEqualTo(legacyAlbum.size());
+
+ Set mediaUrls = new HashSet<>();
+ for (JsonNode media : mediaListNode) {
+ String url = media.path("url").asText();
+ mediaUrls.add(url);
+ if (!legacyAlbum.contains(url)) {
+ continue;
+ }
+ // 对于从 album 折叠出来的条目,我们要求:
+ // id 与 url 相同;usage/status/reviewState 均为兼容值。
+ assertThat(media.path("id").asText()).isEqualTo(url);
+ assertThat(media.path("usage").asText()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
+ assertThat(media.path("status").asText()).isEqualTo(MediaStatus.READY.getCode());
+ assertThat(media.path("reviewState").asText())
+ .isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
+ }
+ assertThat(mediaUrls).containsAll(legacyAlbum);
+ }
+
+ /**
+ * 场景:只有旧相册(album) 数据时,顾客端店员详情接口 buildCustomerDetail 也会将其折叠进 mediaList,
+ * 同时保留 album 字段不变。
+ */
+ @Test
+ void customerDetailMergesLegacyAlbumIntoMediaList() {
+ ensureTenantContext();
+ String marker = "legacy-detail-" + IdUtils.getUuid().substring(0, 8);
+ List legacyAlbum = List.of(
+ "https://example.com/apitest/legacy/" + marker + "-a.png",
+ "https://example.com/apitest/legacy/" + marker + "-b.png");
+ String clerkId = createLegacyAlbumClerk(marker, legacyAlbum);
+
+ PlayClerkUserInfoResultVo detail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
+
+ assertThat(detail.getAlbum()).containsExactlyElementsOf(legacyAlbum);
+ List mediaList = detail.getMediaList();
+ assertThat(mediaList).isNotNull();
+ assertThat(mediaList).hasSize(legacyAlbum.size());
+
+ Set mediaUrls = new HashSet<>();
+ for (MediaVo media : mediaList) {
+ String url = media.getUrl();
+ mediaUrls.add(url);
+ if (!legacyAlbum.contains(url)) {
+ continue;
+ }
+ assertThat(media.getId()).isEqualTo(url);
+ assertThat(media.getUsage()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
+ assertThat(media.getStatus()).isEqualTo(MediaStatus.READY.getCode());
+ assertThat(media.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
+ }
+ assertThat(mediaUrls).containsAll(legacyAlbum);
+ }
+
+ private String createLegacyAlbumClerk(String marker, List album) {
+ String clerkId = IdUtils.getUuid();
+ PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
+ clerk.setId(clerkId);
+ clerk.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
+ clerk.setNickname("兼容测试店员-" + marker);
+ clerk.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
+ clerk.setClerkState("1");
+ clerk.setOnboardingState("1");
+ clerk.setListingState("1");
+ clerk.setDisplayState("1");
+ clerk.setRecommendationState("0");
+ clerk.setPinToTopState("0");
+ clerk.setRandomOrderState("1");
+ clerk.setFixingLevel("0");
+ clerk.setOnlineState("1");
+ clerk.setPhone("138" + clerkId.substring(0, 8));
+ clerk.setOpenid("openid-legacy-" + marker);
+ clerk.setWeiChatCode("wx-code-legacy-" + marker);
+ clerk.setTypeId("api-type-legacy");
+ clerk.setProvince("API省");
+ clerk.setCity("API市");
+ clerk.setEntryTime(LocalDateTime.now());
+ clerk.setAddTime(LocalDateTime.now());
+ clerk.setAlbum(new ArrayList<>(album));
+ clerkUserInfoService.save(clerk);
+ clerkIdsToCleanup.add(clerkId);
+ return clerkId;
+ }
+
+ private void ensureTenantContext() {
+ SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
+ }
+}