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; + +/** + * 覆盖微信顾客端店员列表 / 详情在仅存在历史相册数据时的兼容行为。 + * + *

重点校验: + *

+ *

+ */ +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); + } +}