feat(media): refine clerk album review and tests

This commit is contained in:
irving
2025-12-05 22:16:01 -05:00
parent e683ef6863
commit 21bbd0386d
7 changed files with 469 additions and 30 deletions

View File

@@ -0,0 +1,271 @@
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();
}
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);
}
}