feat(media): clerk profile media flow
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:审核不通过)
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 确认店员处于可用状态,否则抛出异常
|
||||
*
|
||||
|
||||
@@ -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 中现在可能是媒资 ID(mediaId)或历史 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增店员
|
||||
*
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user