Files
peipei-backend/play-admin/src/test/java/com/starry/admin/api/WxClerkAlbumUpdateApiTest.java
irving 036e8156d5
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
fix: allow legacy clerk album entries
2025-12-05 23:31:46 -05:00

322 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.PlayClerkMediaAssetEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.MediaOwnerType;
import com.starry.admin.modules.media.enums.MediaStatus;
import com.starry.admin.modules.media.service.IPlayMediaService;
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
import com.starry.admin.modules.weichat.service.WxTokenService;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
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;
/**
* 专门验证 /wx/clerk/user/updateAlbum 在“只有删除/排序”时不会创建新的审核记录,
* 并且会立即更新顾客端视图。
*/
class WxClerkAlbumUpdateApiTest extends AbstractApiTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private WxTokenService wxTokenService;
@Autowired
private IPlayClerkUserInfoService clerkUserInfoService;
@Autowired
private IPlayClerkDataReviewInfoService dataReviewInfoService;
@Autowired
private IPlayClerkMediaAssetService mediaAssetService;
@Autowired
private IPlayMediaService mediaService;
@Test
void reorderExistingApprovedMediaDoesNotCreateNewReviewAndUpdatesOrder() throws Exception {
ensureTenantContext();
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
String clerkToken = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
PlayMediaEntity media1 = seedMedia(clerkId);
PlayMediaEntity media2 = seedMedia(clerkId);
PlayMediaEntity media3 = seedMedia(clerkId);
seedApprovedAsset(clerkId, media1.getId(), 0);
seedApprovedAsset(clerkId, media2.getId(), 1);
seedApprovedAsset(clerkId, media3.getId(), 2);
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
// 仅调整顺序album 只包含已审核通过的媒资 id不引入任何新媒资
List<String> reordered = List.of(media3.getId(), media1.getId(), media2.getId());
ObjectNode payload = objectMapper.createObjectNode();
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
reordered.forEach(albumArray::add);
mockMvc.perform(post("/wx/clerk/user/updateAlbum")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
assertThat(reviewCountAfter)
.as("reordering without introducing new media should not create review records")
.isEqualTo(reviewCountBefore);
List<PlayClerkMediaAssetEntity> assets = mediaAssetService
.listByState(clerkId, ClerkMediaUsage.PROFILE,
Collections.singletonList(ClerkMediaReviewState.APPROVED));
PlayClerkMediaAssetEntity asset1 = assets.stream()
.filter(a -> media1.getId().equals(a.getMediaId()))
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity asset2 = assets.stream()
.filter(a -> media2.getId().equals(a.getMediaId()))
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity asset3 = assets.stream()
.filter(a -> media3.getId().equals(a.getMediaId()))
.max(java.util.Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
assertThat(asset3).as("asset for media3 should exist").isNotNull();
assertThat(asset1).as("asset for media1 should exist").isNotNull();
assertThat(asset2).as("asset for media2 should exist").isNotNull();
assertThat(asset3.getOrderIndex()).isEqualTo(0);
assertThat(asset1.getOrderIndex()).isEqualTo(1);
assertThat(asset2.getOrderIndex()).isEqualTo(2);
PlayClerkUserInfoResultVo detail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
List<String> customerMediaIds = detail.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
int index3 = customerMediaIds.indexOf(media3.getId());
int index1 = customerMediaIds.indexOf(media1.getId());
int index2 = customerMediaIds.indexOf(media2.getId());
assertThat(index3).isGreaterThanOrEqualTo(0);
assertThat(index1).isGreaterThanOrEqualTo(0);
assertThat(index2).isGreaterThanOrEqualTo(0);
assertThat(index3).isLessThan(index1);
assertThat(index1).isLessThan(index2);
}
@Test
void deleteAndReorderAlbumDoesNotCreateNewReviewAndIsImmediatelyVisible() throws Exception {
ensureTenantContext();
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
String clerkToken = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
// 预置两条已审核通过的 PROFILE 媒资
PlayMediaEntity media1 = seedMedia(clerkId);
PlayMediaEntity media2 = seedMedia(clerkId);
seedApprovedAsset(clerkId, media1.getId(), 0);
seedApprovedAsset(clerkId, media2.getId(), 1);
// 顾客端初始视图应包含两条媒资
PlayClerkUserInfoResultVo beforeDetail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
List<String> beforeIds = beforeDetail.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(beforeIds).contains(media1.getId(), media2.getId());
// 记录当前相册审核记录数量
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
// 现在通过 updateAlbum 只保留 media2相当于“删除 media1 + 不引入新媒资”
ObjectNode payload = objectMapper.createObjectNode();
payload.putArray("album").add(media2.getId());
mockMvc.perform(post("/wx/clerk/user/updateAlbum")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
ensureTenantContext();
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
assertThat(reviewCountAfter)
.as("Deleting/reordering without new media should not create new review records")
.isEqualTo(reviewCountBefore);
// 资产表中仅剩 media2 且仍为 APPROVED 状态
List<PlayClerkMediaAssetEntity> assets = mediaAssetService
.listByState(clerkId, ClerkMediaUsage.PROFILE,
Collections.singletonList(ClerkMediaReviewState.APPROVED));
List<String> assetMediaIds = assets.stream()
.map(PlayClerkMediaAssetEntity::getMediaId)
.collect(Collectors.toList());
assertThat(assetMediaIds)
.contains(media2.getId())
.doesNotContain(media1.getId());
// 顾客端视图应立即反映删除结果
PlayClerkUserInfoResultVo afterDetail = clerkUserInfoService.buildCustomerDetail(clerkId, "");
List<String> afterIds = afterDetail.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(afterIds)
.contains(media2.getId())
.doesNotContain(media1.getId());
}
@Test
void updateAlbumRejectsEmptyAlbumPayload() throws Exception {
ensureTenantContext();
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
String clerkToken = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
ObjectNode payload = objectMapper.createObjectNode();
payload.putArray("album"); // 空数组
MvcResult result = mockMvc.perform(post("/wx/clerk/user/updateAlbum")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andReturn();
String body = result.getResponse().getContentAsString();
com.fasterxml.jackson.databind.JsonNode root = new ObjectMapper().readTree(body);
assertThat(root.path("code").asInt())
.as("empty album should be rejected, response=%s", body)
.isEqualTo(500);
assertThat(root.path("message").asText())
.as("error message for empty album should be present, response=%s", body)
.isNotBlank();
}
@Test
void updateAlbumAllowsMixedLegacyUrlsAndNewMediaIdsForReview() throws Exception {
ensureTenantContext();
String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID;
String clerkToken = wxTokenService.createWxUserToken(clerkId);
clerkUserInfoService.updateTokenById(clerkId, clerkToken);
// 预置一条已就绪的媒资,模拟“新上传的视频/图片”
PlayMediaEntity media = seedMedia(clerkId);
// 模拟老相册中的 URL未媒资化的历史数据
String legacyUrl1 = "https://oss.apitest/legacy-1.png";
String legacyUrl2 = "https://oss.apitest/legacy-2.png";
long reviewCountBefore = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
ObjectNode payload = objectMapper.createObjectNode();
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
albumArray.add(legacyUrl1);
albumArray.add(legacyUrl2);
albumArray.add(media.getId());
MvcResult result = mockMvc.perform(post("/wx/clerk/user/updateAlbum")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.APPLICATION_JSON)
.content(payload.toString()))
.andExpect(status().isOk())
.andReturn();
String body = result.getResponse().getContentAsString();
com.fasterxml.jackson.databind.JsonNode root = new ObjectMapper().readTree(body);
assertThat(root.path("code").asInt())
.as("mixed legacy URLs and new media ids should be accepted for review, response=%s", body)
.isEqualTo(200);
ensureTenantContext();
long reviewCountAfter = dataReviewInfoService.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId, clerkId)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.count();
assertThat(reviewCountAfter)
.as("mixed legacy URLs and new media ids should create exactly one new review record")
.isEqualTo(reviewCountBefore + 1);
}
private PlayMediaEntity seedMedia(String clerkId) {
String mediaId = "media-" + java.util.UUID.randomUUID().toString().substring(0, 16);
PlayMediaEntity entity = new PlayMediaEntity();
entity.setId(mediaId);
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
entity.setOwnerType(MediaOwnerType.CLERK);
entity.setOwnerId(clerkId);
entity.setKind("image");
entity.setStatus(MediaStatus.READY.getCode());
entity.setUrl("https://oss.apitest/" + mediaId + ".png");
mediaService.save(entity);
return entity;
}
private void seedApprovedAsset(String clerkId, String mediaId, int orderIndex) {
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
asset.setId("asset-" + mediaId);
asset.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
asset.setClerkId(clerkId);
asset.setMediaId(mediaId);
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
asset.setOrderIndex(orderIndex);
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
asset.setDeleted(false);
mediaAssetService.save(asset);
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
}