diff --git a/docker/docker-compose-apitest.yml b/docker/docker-compose-apitest.yml
index 1292586..dd4abae 100644
--- a/docker/docker-compose-apitest.yml
+++ b/docker/docker-compose-apitest.yml
@@ -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:
diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/module/vo/PlayClerkDataReviewReturnVo.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/module/vo/PlayClerkDataReviewReturnVo.java
index 9f6d3b5..2f0583b 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/clerk/module/vo/PlayClerkDataReviewReturnVo.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/module/vo/PlayClerkDataReviewReturnVo.java
@@ -60,6 +60,15 @@ public class PlayClerkDataReviewReturnVo {
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
private List dataContent;
+ /**
+ * 媒资对应的视频地址(仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应)
+ */
+ @ApiModelProperty(
+ value = "媒资视频地址列表",
+ example = "[\"https://example.com/video1.mp4\"]",
+ notes = "仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应")
+ private List mediaVideoUrls;
+
/**
* 审核状态(0:未审核:1:审核通过,2:审核不通过)
*/
diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/IPlayClerkUserInfoService.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/IPlayClerkUserInfoService.java
index dd14d4a..732575b 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/IPlayClerkUserInfoService.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/IPlayClerkUserInfoService.java
@@ -190,6 +190,17 @@ public interface IPlayClerkUserInfoService extends IService selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
+ /**
+ * 构建面向顾客的店员详情视图对象(包含媒资与兼容相册)。
+ *
+ * @param clerkId
+ * 店员ID
+ * @param customUserId
+ * 顾客ID(可为空,用于标记关注状态)
+ * @return 店员详情视图对象
+ */
+ PlayClerkUserInfoResultVo buildCustomerDetail(String clerkId, String customUserId);
+
/**
* 确认店员处于可用状态,否则抛出异常
*
diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImpl.java
index fba3ed9..b3392b6 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImpl.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkDataReviewInfoServiceImpl.java
@@ -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 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。
+ *
+ * dataContent 中现在可能是媒资 ID(mediaId)或历史 URL,这里做一次向前兼容:
+ *
+ * - 如果是 mediaId,则解析到 play_media 记录,并返回封面或原始 URL;
+ * - 如果查不到媒资,则保留原值。
+ *
+ * 这样 PC 端审核页面始终可以正确预览图片/视频。
+ */
+ private void enrichDataContentWithMediaPreview(IPage 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 content = row.getDataContent();
+ if (CollectionUtil.isEmpty(content)) {
+ continue;
+ }
+ List previewUrls = new ArrayList<>();
+ List 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 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;
+ }
+ }
}
diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImpl.java
index d005604..e414fd3 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImpl.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/service/impl/PlayClerkUserInfoServiceImpl.java
@@ -548,6 +548,33 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl 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 mediaList = loadMediaForClerk(clerkId, false);
+ vo.setMediaList(mergeLegacyAlbum(entity.getAlbum(), mediaList));
+ return vo;
+ }
+
/**
* 新增店员
*
diff --git a/play-admin/src/main/java/com/starry/admin/modules/clerk/task/ClerkAlbumMigrationRunner.java b/play-admin/src/main/java/com/starry/admin/modules/clerk/task/ClerkAlbumMigrationRunner.java
index bc04d81..d786e1b 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/clerk/task/ClerkAlbumMigrationRunner.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/clerk/task/ClerkAlbumMigrationRunner.java
@@ -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());
- mediaService.updateById(media);
- }
+ 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 metadata = new HashMap<>();
metadata.put("legacySource", "album_migration");
diff --git a/play-admin/src/main/java/com/starry/admin/modules/media/service/impl/PlayMediaServiceImpl.java b/play-admin/src/main/java/com/starry/admin/modules/media/service/impl/PlayMediaServiceImpl.java
index 7e3714a..1eba014 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/media/service/impl/PlayMediaServiceImpl.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/media/service/impl/PlayMediaServiceImpl.java
@@ -40,7 +40,7 @@ public class PlayMediaServiceImpl extends ServiceImpl listApprovedByOwner(String ownerType, String ownerId) {
- return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.APPROVED.getCode()));
+ return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.READY.getCode()));
}
@Override
diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCustomController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCustomController.java
index 93b5e37..1e6552e 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCustomController.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxCustomController.java
@@ -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);
}
diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MediaUploadService.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MediaUploadService.java
index 95c0e5d..5abf034 100644
--- a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MediaUploadService.java
+++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/MediaUploadService.java
@@ -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();
diff --git a/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java
new file mode 100644
index 0000000..bd2d79a
--- /dev/null
+++ b/play-admin/src/test/java/com/starry/admin/api/WxClerkMediaControllerApiTest.java
@@ -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 allMediaIds = List.of(mediaIdA, mediaIdB, mediaIdC, mediaIdD);
+
+ submitAlbumUpdate(allMediaIds, clerkToken);
+ ensureTenantContext();
+ approveLatestAlbumReview();
+
+ List assetsAfterFirstApprove = mediaAssetService
+ .listActiveByUsage(ApiTestDataSeeder.DEFAULT_CLERK_ID, ClerkMediaUsage.PROFILE);
+ assertThat(assetsAfterFirstApprove)
+ .extracting(PlayClerkMediaAssetEntity::getMediaId)
+ .containsAll(allMediaIds);
+ List reviewStatesForNewMedia = assetsAfterFirstApprove.stream()
+ .filter(asset -> allMediaIds.contains(asset.getMediaId()))
+ .map(PlayClerkMediaAssetEntity::getReviewState)
+ .collect(Collectors.toList());
+ assertThat(reviewStatesForNewMedia).containsOnly(ClerkMediaReviewState.APPROVED.getCode());
+
+ List 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 customerMediaIdsAfterFirst = customerDetailAfterFirst.getMediaList().stream()
+ .map(MediaVo::getMediaId)
+ .collect(Collectors.toList());
+ assertThat(customerMediaIdsAfterFirst).containsAll(allMediaIds);
+
+ List keptMedia = List.of(mediaIdA, mediaIdC);
+ submitAlbumUpdate(keptMedia, clerkToken);
+ ensureTenantContext();
+ approveLatestAlbumReview();
+
+ List 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 customerMediaIdsAfterSecond = customerDetailAfterSecond.getMediaList().stream()
+ .map(MediaVo::getMediaId)
+ .collect(Collectors.toList());
+ assertThat(customerMediaIdsAfterSecond)
+ .contains(mediaIdA, mediaIdC)
+ .doesNotContain(mediaIdB, mediaIdD);
+
+ List 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 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 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);
+ }
+}