feat(media): clerk profile media flow
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
version: "3.9"
|
version: "3.9"
|
||||||
services:
|
services:
|
||||||
mysql-apitest:
|
mysql-apitest:
|
||||||
image: mysql:8.0.24
|
image: mysql:8.0
|
||||||
container_name: peipei-mysql-apitest
|
container_name: peipei-mysql-apitest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ public class PlayClerkDataReviewReturnVo {
|
|||||||
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
|
@ApiModelProperty(value = "资料内容", example = "[\"https://example.com/photo1.jpg\"]", notes = "资料内容,根据资料类型有不同格式")
|
||||||
private List<String> dataContent;
|
private List<String> dataContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资对应的视频地址(仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应)
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(
|
||||||
|
value = "媒资视频地址列表",
|
||||||
|
example = "[\"https://example.com/video1.mp4\"]",
|
||||||
|
notes = "仅当资料类型为头像/相册且为视频时有值,顺序与 dataContent 一一对应")
|
||||||
|
private List<String> mediaVideoUrls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审核状态(0:未审核:1:审核通过,2:审核不通过)
|
* 审核状态(0:未审核:1:审核通过,2:审核不通过)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -190,6 +190,17 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
|
|||||||
*/
|
*/
|
||||||
IPage<PlayClerkUserInfoResultVo> selectPlayClerkUserInfoByPage(PlayClerkUserInfoQueryVo vo);
|
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.PlayClerkDataReviewInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
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.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.PlayClerkDataReviewQueryVo;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewReturnVo;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
|
||||||
@@ -126,8 +127,11 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
|
lambdaQueryWrapper.between(PlayClerkDataReviewInfoEntity::getAddTime, vo.getAddTime().get(0),
|
||||||
vo.getAddTime().get(1));
|
vo.getAddTime().get(1));
|
||||||
}
|
}
|
||||||
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
|
IPage<PlayClerkDataReviewReturnVo> page = this.baseMapper.selectJoinPage(
|
||||||
PlayClerkDataReviewReturnVo.class, lambdaQueryWrapper);
|
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkDataReviewReturnVo.class,
|
||||||
|
lambdaQueryWrapper);
|
||||||
|
enrichDataContentWithMediaPreview(page);
|
||||||
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +152,72 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
return save(playClerkDataReviewInfo);
|
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
|
@Override
|
||||||
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
|
public void updateDataReviewState(PlayClerkDataReviewStateEditVo vo) {
|
||||||
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
|
PlayClerkDataReviewInfoEntity entity = this.selectPlayClerkDataReviewInfoById(vo.getId());
|
||||||
@@ -233,13 +303,13 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
media.setOwnerType(MediaOwnerType.CLERK);
|
media.setOwnerType(MediaOwnerType.CLERK);
|
||||||
media.setOwnerId(clerkInfo.getId());
|
media.setOwnerId(clerkInfo.getId());
|
||||||
media.setKind(MediaKind.IMAGE.getCode());
|
media.setKind(MediaKind.IMAGE.getCode());
|
||||||
media.setStatus(MediaStatus.APPROVED.getCode());
|
media.setStatus(MediaStatus.READY.getCode());
|
||||||
media.setUrl(url);
|
media.setUrl(url);
|
||||||
Map<String, Object> metadata = new HashMap<>();
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
metadata.put("legacySource", "album_review");
|
metadata.put("legacySource", "album_review");
|
||||||
media.setMetadata(metadata);
|
media.setMetadata(metadata);
|
||||||
mediaService.normalizeAndSave(media);
|
mediaService.normalizeAndSave(media);
|
||||||
media.setStatus(MediaStatus.APPROVED.getCode());
|
media.setStatus(MediaStatus.READY.getCode());
|
||||||
mediaService.updateById(media);
|
mediaService.updateById(media);
|
||||||
return media;
|
return media;
|
||||||
}
|
}
|
||||||
@@ -279,4 +349,28 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
public int deletePlayClerkDataReviewInfoById(String id) {
|
public int deletePlayClerkDataReviewInfoById(String id) {
|
||||||
return playClerkDataReviewInfoMapper.deleteById(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;
|
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 com.starry.common.utils.IdUtils;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -79,10 +77,8 @@ public class ClerkAlbumMigrationRunner implements ApplicationRunner {
|
|||||||
if (media == null) {
|
if (media == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!MediaStatus.APPROVED.getCode().equals(media.getStatus())) {
|
media.setStatus(MediaStatus.READY.getCode());
|
||||||
media.setStatus(MediaStatus.APPROVED.getCode());
|
mediaService.updateById(media);
|
||||||
mediaService.updateById(media);
|
|
||||||
}
|
|
||||||
clerkMediaAssetService.linkDraftAsset(clerk.getTenantId(), clerk.getId(), media.getId(),
|
clerkMediaAssetService.linkDraftAsset(clerk.getTenantId(), clerk.getId(), media.getId(),
|
||||||
ClerkMediaUsage.PROFILE);
|
ClerkMediaUsage.PROFILE);
|
||||||
approvedMediaIds.add(media.getId());
|
approvedMediaIds.add(media.getId());
|
||||||
@@ -90,11 +86,6 @@ public class ClerkAlbumMigrationRunner implements ApplicationRunner {
|
|||||||
|
|
||||||
clerkMediaAssetService.applyReviewDecision(clerk.getId(), ClerkMediaUsage.PROFILE, approvedMediaIds,
|
clerkMediaAssetService.applyReviewDecision(clerk.getId(), ClerkMediaUsage.PROFILE, approvedMediaIds,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
|
|
||||||
update.setId(clerk.getId());
|
|
||||||
update.setAlbum(new ArrayList<>());
|
|
||||||
clerkUserInfoService.update(update);
|
|
||||||
migratedOwners.incrementAndGet();
|
migratedOwners.incrementAndGet();
|
||||||
migratedMedia.addAndGet(approvedMediaIds.size());
|
migratedMedia.addAndGet(approvedMediaIds.size());
|
||||||
log.info("[ClerkAlbumMigration] processed {} media for clerk {}", 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.setOwnerType(MediaOwnerType.CLERK);
|
||||||
media.setOwnerId(clerk.getId());
|
media.setOwnerId(clerk.getId());
|
||||||
media.setKind(MediaKind.IMAGE.getCode());
|
media.setKind(MediaKind.IMAGE.getCode());
|
||||||
media.setStatus(MediaStatus.APPROVED.getCode());
|
media.setStatus(MediaStatus.READY.getCode());
|
||||||
media.setUrl(value);
|
media.setUrl(value);
|
||||||
Map<String, Object> metadata = new HashMap<>();
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
metadata.put("legacySource", "album_migration");
|
metadata.put("legacySource", "album_migration");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class PlayMediaServiceImpl extends ServiceImpl<PlayMediaMapper, PlayMedia
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId) {
|
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
|
@Override
|
||||||
|
|||||||
@@ -151,16 +151,9 @@ public class WxCustomController {
|
|||||||
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PlayClerkUserInfoResultVo.class)})
|
@ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = PlayClerkUserInfoResultVo.class)})
|
||||||
@GetMapping("/queryClerkDetailedById")
|
@GetMapping("/queryClerkDetailedById")
|
||||||
public R queryClerkDetailedById(@RequestParam("id") String id) {
|
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();
|
String loginUserId = customUserService.getLoginUserId();
|
||||||
if (StringUtils.isNotEmpty(loginUserId)) {
|
PlayClerkUserInfoResultVo vo = clerkUserInfoService.buildCustomerDetail(id,
|
||||||
vo.setFollowState(playCustomFollowInfoService.queryFollowState(loginUserId, vo.getId()));
|
StringUtils.isNotEmpty(loginUserId) ? loginUserId : "");
|
||||||
}
|
|
||||||
// 服务项目
|
|
||||||
vo.setCommodity(playClerkCommodityService.getClerkCommodityList(vo.getId(), "1"));
|
|
||||||
return R.ok(vo);
|
return R.ok(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import ws.schild.jave.info.VideoSize;
|
|||||||
public class MediaUploadService {
|
public class MediaUploadService {
|
||||||
|
|
||||||
private static final long MAX_VIDEO_BYTES = 30L * 1024 * 1024;
|
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 IMAGE_OUTPUT_FORMAT = "image2";
|
||||||
private static final String VIDEO_OUTPUT_FORMAT = "mp4";
|
private static final String VIDEO_OUTPUT_FORMAT = "mp4";
|
||||||
|
|
||||||
@@ -85,9 +85,6 @@ public class MediaUploadService {
|
|||||||
isVideo ? MediaKind.VIDEO : MediaKind.IMAGE);
|
isVideo ? MediaKind.VIDEO : MediaKind.IMAGE);
|
||||||
entity.getMetadata().put("detectedType", detectedType);
|
entity.getMetadata().put("detectedType", detectedType);
|
||||||
entity.getMetadata().put("isVideo", isVideo);
|
entity.getMetadata().put("isVideo", isVideo);
|
||||||
mediaService.normalizeAndSave(entity);
|
|
||||||
entity.setStatus(MediaStatus.PROCESSING.getCode());
|
|
||||||
mediaService.updateById(entity);
|
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
handleImageUpload(tempFile, entity, clerkInfo, originalFilename);
|
handleImageUpload(tempFile, entity, clerkInfo, originalFilename);
|
||||||
@@ -97,7 +94,7 @@ public class MediaUploadService {
|
|||||||
handleVideoUpload(tempFile, processedVideoFile, coverFile, entity, clerkInfo, originalFilename);
|
handleVideoUpload(tempFile, processedVideoFile, coverFile, entity, clerkInfo, originalFilename);
|
||||||
}
|
}
|
||||||
entity.setStatus(MediaStatus.READY.getCode());
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
mediaService.updateById(entity);
|
mediaService.normalizeAndSave(entity);
|
||||||
PlayClerkMediaAssetEntity asset = clerkMediaAssetService.linkDraftAsset(
|
PlayClerkMediaAssetEntity asset = clerkMediaAssetService.linkDraftAsset(
|
||||||
clerkInfo.getTenantId(),
|
clerkInfo.getTenantId(),
|
||||||
clerkInfo.getId(),
|
clerkInfo.getId(),
|
||||||
@@ -161,7 +158,7 @@ public class MediaUploadService {
|
|||||||
}
|
}
|
||||||
long durationMs = info.getDuration();
|
long durationMs = info.getDuration();
|
||||||
if (durationMs > MAX_VIDEO_DURATION_MS) {
|
if (durationMs > MAX_VIDEO_DURATION_MS) {
|
||||||
throw new CustomException("视频时长不能超过30秒");
|
throw new CustomException("视频时长不能超过45秒");
|
||||||
}
|
}
|
||||||
VideoInfo videoInfo = info.getVideo();
|
VideoInfo videoInfo = info.getVideo();
|
||||||
VideoSize size = videoInfo.getSize();
|
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