feat(media): clerk profile media flow

This commit is contained in:
irving
2025-12-04 22:27:03 -05:00
parent 8558d203af
commit 086aa47226
10 changed files with 483 additions and 33 deletions

View File

@@ -1,7 +1,7 @@
version: "3.9"
services:
mysql-apitest:
image: mysql:8.0.24
image: mysql:8.0
container_name: peipei-mysql-apitest
restart: unless-stopped
environment:

View File

@@ -60,6 +60,15 @@ public class PlayClerkDataReviewReturnVo {
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
private List<String> dataContent;
/**
* 媒资对应的视频地址(仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应)
*/
@ApiModelProperty(
value = "媒资视频地址列表",
example = "[\"https://example.com/video1.mp4\"]",
notes = "仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应")
private List<String> mediaVideoUrls;
/**
* 审核状态0未审核:1审核通过2审核不通过
*/

View File

@@ -190,6 +190,17 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
*/
IPage<PlayClerkUserInfoResultVo> selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
/**
* 构建面向顾客的店员详情视图对象(包含媒资与兼容相册)。
*
* @param clerkId
* 店员ID
* @param customUserId
* 顾客ID可为空用于标记关注状态
* @return 店员详情视图对象
*/
PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId);
/**
* 确认店员处于可用状态,否则抛出异常
*

View File

@@ -13,6 +13,7 @@ import com.starry.admin.modules.clerk.mapper.PlayClerkDataReviewInfoMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReviewInfoEntity;
import com.starry.admin.modules.clerk.module.enums.ClerkDataType;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewQueryVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
@@ -126,8 +127,11 @@ public class PlayClerkDataReviewInfoServiceImpl
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
vo.getAddTime().get(1));
}
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
PlayClerkDataReviewReturnVo.class, lambdaQueryWrapper);
IPage<PlayClerkDataReviewReturnVo> page = this.baseMapper.selectJoinPage(
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkDataReviewReturnVo.class,
lambdaQueryWrapper);
enrichDataContentWithMediaPreview(page);
return page;
}
/**
@@ -148,6 +152,72 @@ public class PlayClerkDataReviewInfoServiceImpl
return save(playClerkDataReviewInfo);
}
/**
* 为头像 / 相册审核记录补充可预览的 URL。
*
* <p>dataContent 中现在可能是媒资 IDmediaId或历史 URL这里做一次向前兼容
* <ul>
* <li>如果是 mediaId则解析到 play_media 记录,并返回封面或原始 URL</li>
* <li>如果查不到媒资,则保留原值。</li>
* </ul>
* 这样 PC 端审核页面始终可以正确预览图片/视频。</p>
*/
private void enrichDataContentWithMediaPreview(IPage<PlayClerkDataReviewReturnVo> page) {
if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) {
return;
}
for (PlayClerkDataReviewReturnVo row : page.getRecords()) {
ClerkDataType type = row.getDataTypeEnum();
if (type == null) {
continue;
}
if (type == ClerkDataType.AVATAR || type == ClerkDataType.PHOTO_ALBUM) {
List<String> content = row.getDataContent();
if (CollectionUtil.isEmpty(content)) {
continue;
}
List<String> previewUrls = new ArrayList<>();
List<String> videoUrls = new ArrayList<>();
for (String value : content) {
if (StrUtil.isBlank(value)) {
continue;
}
MediaPreviewPair pair = resolvePreviewPair(value);
if (pair == null || StrUtil.isBlank(pair.getPreviewUrl())) {
continue;
}
previewUrls.add(pair.getPreviewUrl());
videoUrls.add(pair.getVideoUrl());
}
row.setDataContent(previewUrls);
row.setMediaVideoUrls(videoUrls);
}
}
}
private MediaPreviewPair resolvePreviewPair(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
PlayMediaEntity media = mediaService.getById(value);
if (media == null) {
MediaPreviewPair fallback = new MediaPreviewPair();
fallback.setPreviewUrl(value);
fallback.setVideoUrl(null);
return fallback;
}
MediaPreviewPair pair = new MediaPreviewPair();
if (MediaKind.VIDEO.getCode().equals(media.getKind())) {
String coverUrl = StrUtil.isNotBlank(media.getCoverUrl()) ? media.getCoverUrl() : media.getUrl();
pair.setPreviewUrl(coverUrl);
pair.setVideoUrl(media.getUrl());
} else {
pair.setPreviewUrl(media.getUrl());
pair.setVideoUrl(null);
}
return pair;
}
@Override
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
@@ -233,13 +303,13 @@ public class PlayClerkDataReviewInfoServiceImpl
media.setOwnerType(MediaOwnerType.CLERK);
media.setOwnerId(clerkInfo.getId());
media.setKind(MediaKind.IMAGE.getCode());
media.setStatus(MediaStatus.APPROVED.getCode());
media.setStatus(MediaStatus.READY.getCode());
media.setUrl(url);
Map<String, Object> metadata = new HashMap<>();
metadata.put("legacySource", "album_review");
media.setMetadata(metadata);
mediaService.normalizeAndSave(media);
media.setStatus(MediaStatus.APPROVED.getCode());
media.setStatus(MediaStatus.READY.getCode());
mediaService.updateById(media);
return media;
}
@@ -279,4 +349,28 @@ public class PlayClerkDataReviewInfoServiceImpl
public int deletePlayClerkDataReviewInfoById(String id) {
return playClerkDataReviewInfoMapper.deleteById(id);
}
/**
* 简单的预览地址 / 视频地址对,避免在主体逻辑中使用 Map 或魔法下标。
*/
private static class MediaPreviewPair {
private String previewUrl;
private String videoUrl;
String getPreviewUrl() {
return previewUrl;
}
void setPreviewUrl(String previewUrl) {
this.previewUrl = previewUrl;
}
String getVideoUrl() {
return videoUrl;
}
void setVideoUrl(String videoUrl) {
this.videoUrl = videoUrl;
}
}
}

View File

@@ -548,6 +548,33 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
return voPage;
}
@Override
public PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId) {
PlayClerkUserInfoEntity entity = this.baseMapper.selectById(clerkId);
if (entity == null) {
throw new CustomException("店员不存在");
}
PlayClerkUserInfoResultVo vo = ConvertUtil.entityToVo(entity, PlayClerkUserInfoResultVo.class);
vo.setAddress(entity.getCity());
vo.setCommodity(playClerkCommodityService.getClerkCommodityList(vo.getId(), "1"));
String followState = "0";
if (StrUtil.isNotBlank(customUserId)) {
LambdaQueryWrapper<PlayCustomFollowInfoEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId)
.eq(PlayCustomFollowInfoEntity::getClerkId, clerkId);
PlayCustomFollowInfoEntity followInfo = customFollowInfoService.getOne(wrapper, false);
if (followInfo != null && "1".equals(followInfo.getFollowState())) {
followState = "1";
}
}
vo.setFollowState(followState);
List<MediaVo> mediaList = loadMediaForClerk(clerkId, false);
vo.setMediaList(mergeLegacyAlbum(entity.getAlbum(), mediaList));
return vo;
}
/**
* 新增店员
*

View File

@@ -15,10 +15,8 @@ import com.starry.admin.utils.SecurityUtils;
import com.starry.common.utils.IdUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
@@ -79,10 +77,8 @@ public class ClerkAlbumMigrationRunner implements ApplicationRunner {
if (media == null) {
continue;
}
if (!MediaStatus.APPROVED.getCode().equals(media.getStatus())) {
media.setStatus(MediaStatus.APPROVED.getCode());
media.setStatus(MediaStatus.READY.getCode());
mediaService.updateById(media);
}
clerkMediaAssetService.linkDraftAsset(clerk.getTenantId(), clerk.getId(), media.getId(),
ClerkMediaUsage.PROFILE);
approvedMediaIds.add(media.getId());
@@ -90,11 +86,6 @@ public class ClerkAlbumMigrationRunner implements ApplicationRunner {
clerkMediaAssetService.applyReviewDecision(clerk.getId(), ClerkMediaUsage.PROFILE, approvedMediaIds,
null, null);
PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
update.setId(clerk.getId());
update.setAlbum(new ArrayList<>());
clerkUserInfoService.update(update);
migratedOwners.incrementAndGet();
migratedMedia.addAndGet(approvedMediaIds.size());
log.info("[ClerkAlbumMigration] processed {} media for clerk {}", approvedMediaIds.size(),
@@ -130,7 +121,7 @@ public class ClerkAlbumMigrationRunner implements ApplicationRunner {
media.setOwnerType(MediaOwnerType.CLERK);
media.setOwnerId(clerk.getId());
media.setKind(MediaKind.IMAGE.getCode());
media.setStatus(MediaStatus.APPROVED.getCode());
media.setStatus(MediaStatus.READY.getCode());
media.setUrl(value);
Map<String, Object> metadata = new HashMap<>();
metadata.put("legacySource", "album_migration");

View File

@@ -40,7 +40,7 @@ public class PlayMediaServiceImpl extends ServiceImpl<PlayMediaMapper, PlayMedia
@Override
public List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId) {
return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.APPROVED.getCode()));
return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.READY.getCode()));
}
@Override

View File

@@ -151,16 +151,9 @@ public class WxCustomController {
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PlayClerkUserInfoResultVo.class)})
@GetMapping("/queryClerkDetailedById")
public R queryClerkDetailedById(@RequestParam("id") String id) {
PlayClerkUserInfoEntity entity = clerkUserInfoService.selectById(id);
PlayClerkUserInfoResultVo vo = ConvertUtil.entityToVo(entity, PlayClerkUserInfoResultVo.class);
vo.setAddress(entity.getCity());
// 查询是否关注,未登录情况下,默认为未关注
String loginUserId = customUserService.getLoginUserId();
if (StringUtils.isNotEmpty(loginUserId)) {
vo.setFollowState(playCustomFollowInfoService.queryFollowState(loginUserId, vo.getId()));
}
// 服务项目
vo.setCommodity(playClerkCommodityService.getClerkCommodityList(vo.getId(), "1"));
PlayClerkUserInfoResultVo vo = clerkUserInfoService.buildCustomerDetail(id,
StringUtils.isNotEmpty(loginUserId) ? loginUserId : "");
return R.ok(vo);
}

View File

@@ -47,7 +47,7 @@ import ws.schild.jave.info.VideoSize;
public class MediaUploadService {
private static final long MAX_VIDEO_BYTES = 30L * 1024 * 1024;
private static final long MAX_VIDEO_DURATION_MS = 30_000;
private static final long MAX_VIDEO_DURATION_MS = 45_000;
private static final String IMAGE_OUTPUT_FORMAT = "image2";
private static final String VIDEO_OUTPUT_FORMAT = "mp4";
@@ -85,9 +85,6 @@ public class MediaUploadService {
isVideo ? MediaKind.VIDEO : MediaKind.IMAGE);
entity.getMetadata().put("detectedType", detectedType);
entity.getMetadata().put("isVideo", isVideo);
mediaService.normalizeAndSave(entity);
entity.setStatus(MediaStatus.PROCESSING.getCode());
mediaService.updateById(entity);
if (isImage) {
handleImageUpload(tempFile, entity, clerkInfo, originalFilename);
@@ -97,7 +94,7 @@ public class MediaUploadService {
handleVideoUpload(tempFile, processedVideoFile, coverFile, entity, clerkInfo, originalFilename);
}
entity.setStatus(MediaStatus.READY.getCode());
mediaService.updateById(entity);
mediaService.normalizeAndSave(entity);
PlayClerkMediaAssetEntity asset = clerkMediaAssetService.linkDraftAsset(
clerkInfo.getTenantId(),
clerkInfo.getId(),
@@ -161,7 +158,7 @@ public class MediaUploadService {
}
long durationMs = info.getDuration();
if (durationMs > MAX_VIDEO_DURATION_MS) {
throw new CustomException("视频时长不能超过30");
throw new CustomException("视频时长不能超过45");
}
VideoInfo videoInfo = info.getVideo();
VideoSize size = videoInfo.getSize();

View File

@@ -0,0 +1,328 @@
package com.starry.admin.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
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.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.common.oss.service.IOssFileService;
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.service.IPlayMediaService;
import com.starry.admin.modules.weichat.entity.PlayClerkUserLoginResponseVo;
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 com.starry.common.enums.ClerkReviewState;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.imageio.ImageIO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MvcResult;
/**
* 覆盖店员端媒资上传与相册审核的关键业务路径(图片/视频 + 删除后不复活)。
*/
class WxClerkMediaControllerApiTest extends AbstractApiTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private WxTokenService wxTokenService;
@Autowired
private IPlayClerkUserInfoService clerkUserInfoService;
@Autowired
private IPlayMediaService mediaService;
@Autowired
private IPlayClerkDataReviewInfoService dataReviewInfoService;
@Autowired
private IPlayClerkMediaAssetService mediaAssetService;
@MockBean
private IOssFileService ossFileService;
@Test
void clerkCanUploadImageMediaAndPersistUrl() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
String ossUrl = "https://oss.mock/apitest/avatar.png";
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(ossUrl);
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
MockMultipartFile file = new MockMultipartFile(
"file",
"avatar.png",
"image/png",
baos.toByteArray());
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.url").value(ossUrl))
.andExpect(jsonPath("$.data.kind").value("image"))
.andExpect(jsonPath("$.data.usage").value("profile"))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
JsonNode data = root.path("data");
String mediaId = data.path("mediaId").asText(null);
assertThat(mediaId).isNotBlank();
ensureTenantContext();
PlayMediaEntity persisted = mediaService.getById(mediaId);
assertThat(persisted).isNotNull();
assertThat(persisted.getOwnerType()).isEqualTo(MediaOwnerType.CLERK);
assertThat(persisted.getOwnerId()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
assertThat(persisted.getUrl()).isEqualTo(ossUrl);
}
@Test
void clerkCanUploadVideoMediaAndPersistUrl() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
String videoUrl = "https://oss.mock/apitest/video.mp4";
String coverUrl = "https://oss.mock/apitest/video-cover.jpg";
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(videoUrl, coverUrl);
byte[] videoBytes = Files.readAllBytes(Paths.get("/Volumes/main/code/yunpei/sample_data/sample_video.mp4"));
MockMultipartFile file = new MockMultipartFile(
"file",
"sample_video.mp4",
"video/mp4",
videoBytes);
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.kind").value("video"))
.andExpect(jsonPath("$.data.usage").value("profile"))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
JsonNode data = root.path("data");
String mediaId = data.path("mediaId").asText(null);
assertThat(mediaId).isNotBlank();
ensureTenantContext();
PlayMediaEntity persisted = mediaService.getById(mediaId);
assertThat(persisted).isNotNull();
assertThat(persisted.getKind()).isEqualTo("video");
assertThat(persisted.getUrl()).isEqualTo(videoUrl);
assertThat(persisted.getCoverUrl()).isEqualTo(coverUrl);
}
@Test
void aggressiveAlbumLifecycleWithFourMediaAndDeletionReflectedForClerkAndCustomer() throws Exception {
ensureTenantContext();
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
when(ossFileService.upload(any(), eq(ApiTestDataSeeder.DEFAULT_TENANT_ID), anyString()))
.thenReturn(
"https://oss.mock/apitest/album-a.png",
"https://oss.mock/apitest/album-b.png",
"https://oss.mock/apitest/album-c.png",
"https://oss.mock/apitest/album-d.png");
String mediaIdA = extractMediaIdFromUpload(buildTinyPng("album-a.png"), clerkToken);
String mediaIdB = extractMediaIdFromUpload(buildTinyPng("album-b.png"), clerkToken);
String mediaIdC = extractMediaIdFromUpload(buildTinyPng("album-c.png"), clerkToken);
String mediaIdD = extractMediaIdFromUpload(buildTinyPng("album-d.png"), clerkToken);
List<String> allMediaIds = List.of(mediaIdA, mediaIdB, mediaIdC, mediaIdD);
submitAlbumUpdate(allMediaIds, clerkToken);
ensureTenantContext();
approveLatestAlbumReview();
List<PlayClerkMediaAssetEntity> assetsAfterFirstApprove = mediaAssetService
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
assertThat(assetsAfterFirstApprove)
.extracting(PlayClerkMediaAssetEntity::getMediaId)
.containsAll(allMediaIds);
List<String> reviewStatesForNewMedia = assetsAfterFirstApprove.stream()
.filter(asset -> allMediaIds.contains(asset.getMediaId()))
.map(PlayClerkMediaAssetEntity::getReviewState)
.collect(Collectors.toList());
assertThat(reviewStatesForNewMedia).containsOnly(ClerkMediaReviewState.APPROVED.getCode());
List<String> clerkVisibleMediaIdsAfterFirst = assetsAfterFirstApprove.stream()
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
.map(PlayClerkMediaAssetEntity::getMediaId)
.collect(Collectors.toList());
assertThat(clerkVisibleMediaIdsAfterFirst).containsAll(allMediaIds);
PlayClerkUserInfoResultVo customerDetailAfterFirst =
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
List<String> customerMediaIdsAfterFirst = customerDetailAfterFirst.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(customerMediaIdsAfterFirst).containsAll(allMediaIds);
List<String> keptMedia = List.of(mediaIdA, mediaIdC);
submitAlbumUpdate(keptMedia, clerkToken);
ensureTenantContext();
approveLatestAlbumReview();
List<PlayClerkMediaAssetEntity> assetsAfterSecondApprove = mediaAssetService
.listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
PlayClerkMediaAssetEntity assetA = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdA.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity assetB = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdB.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity assetC = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdC.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
PlayClerkMediaAssetEntity assetD = assetsAfterSecondApprove.stream()
.filter(a -> mediaIdD.equals(a.getMediaId()))
.max(Comparator.comparing(PlayClerkMediaAssetEntity::getCreatedTime))
.orElse(null);
assertThat(assetA).isNotNull();
assertThat(assetA.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
assertThat(assetC).isNotNull();
assertThat(assetC.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
assertThat(assetB).isNotNull();
assertThat(assetB.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
assertThat(assetD).isNotNull();
assertThat(assetD.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
PlayClerkUserInfoResultVo customerDetailAfterSecond =
clerkUserInfoService.buildCustomerDetail(ApiTestDataSeeder.DEFAULT_CLERK_ID, "");
List<String> customerMediaIdsAfterSecond = customerDetailAfterSecond.getMediaList().stream()
.map(MediaVo::getMediaId)
.collect(Collectors.toList());
assertThat(customerMediaIdsAfterSecond)
.contains(mediaIdA, mediaIdC)
.doesNotContain(mediaIdB, mediaIdD);
List<String> clerkVisibleMediaIdsAfterSecond = assetsAfterSecondApprove.stream()
.filter(asset -> !ClerkMediaReviewState.REJECTED.getCode().equals(asset.getReviewState()))
.map(PlayClerkMediaAssetEntity::getMediaId)
.collect(Collectors.toList());
assertThat(clerkVisibleMediaIdsAfterSecond)
.contains(mediaIdA, mediaIdC)
.doesNotContain(mediaIdB, mediaIdD);
}
private MockMultipartFile buildTinyPng(String filename) throws Exception {
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
return new MockMultipartFile("file", filename, "image/png", baos.toByteArray());
}
private String extractMediaIdFromUpload(MockMultipartFile file, String clerkToken) throws Exception {
MvcResult result = mockMvc.perform(multipart("/wx/clerk/media/upload")
.file(file)
.param("usage", "profile")
.header(USER_HEADER, DEFAULT_USER)
.header(TENANT_HEADER, DEFAULT_TENANT)
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andReturn();
String body = result.getResponse().getContentAsString();
JsonNode root = objectMapper.readTree(body);
return root.path("data").path("mediaId").asText();
}
private void submitAlbumUpdate(List<String> mediaIds, String clerkToken) throws Exception {
ObjectNode payload = objectMapper.createObjectNode();
com.fasterxml.jackson.databind.node.ArrayNode albumArray = payload.putArray("album");
mediaIds.forEach(albumArray::add);
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.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));
}
private void approveLatestAlbumReview() {
List<com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity> reviews = dataReviewInfoService
.lambdaQuery()
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getClerkId,
ApiTestDataSeeder.DEFAULT_CLERK_ID)
.eq(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getDataType, "2")
.orderByDesc(com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity::getAddTime)
.list();
assertThat(reviews).isNotEmpty();
com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity latest = reviews.get(0);
com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo vo =
new com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo();
vo.setId(latest.getId());
vo.setReviewState(ClerkReviewState.APPROVED);
vo.setReviewCon("ok");
vo.setReviewTime(LocalDateTime.now());
dataReviewInfoService.updateDataReviewState(vo);
}
private void ensureTenantContext() {
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
}
}