wip: media migration progress
This commit is contained in:
24
media-migration-to-test.md
Normal file
24
media-migration-to-test.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## 媒資/相簿相容性:手動驗證清單
|
||||||
|
|
||||||
|
1. **WeChat 店員端 - 個資頁預覽**
|
||||||
|
- 登入店員帳號進入「我的資料」,確認相簿縮圖顯示為合併後的 `mediaList + album`,不應出現重覆 URL。
|
||||||
|
- 點擊「照片」進入管理介面,確認 legacy album 中的圖片仍存在;上傳新媒資後應立即出現於列表。
|
||||||
|
|
||||||
|
2. **媒資上傳與排序**
|
||||||
|
- 上傳圖片與影片各一,測試格式/大小超限的錯誤提示與成功上傳後的狀態。
|
||||||
|
- 排序、刪除媒資並提交審核,確認前端列表與預覽更新,且不會重複顯示相同 URL。
|
||||||
|
|
||||||
|
3. **後台店員列表**
|
||||||
|
- 在管理端店員列表中,確認每位店員的照片區塊都展示合併後的媒資清單(舊 album + 新媒資)。
|
||||||
|
- 點擊圖片預覽,確認輪播順序正確、無重覆 URL。
|
||||||
|
|
||||||
|
4. **後台店員審核詳情**
|
||||||
|
- 查看一筆含多張舊相簿照片的申請,確認圖片區塊已改用 `mediaGallery`,兼容新舊媒資。
|
||||||
|
- 點擊照片預覽,確認圖片來源為合併後的清單。
|
||||||
|
|
||||||
|
5. **API 回應驗證**
|
||||||
|
- 呼叫 `/clerk/user/list` 或 WeChat 前端使用的 API,檢查 `album` 欄位仍保留原值,`mediaList` 會包含新媒資並附帶 legacy URL(無重複)。
|
||||||
|
- 若資料仍未遷移,確保 `mediaList` 仍會帶上舊 `album` 的 URL。
|
||||||
|
|
||||||
|
6. **媒資審核流程**
|
||||||
|
- 走一次「上傳 → 排序 → 提交 → 審核」流程,確認審核通過後 `mediaList` 只保留 Approved 的媒資,但 `album` 不會被清除,舊客端仍能看到舊資料。
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.starry.admin.modules.clerk.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum ClerkMediaReviewState {
|
||||||
|
|
||||||
|
DRAFT("draft"),
|
||||||
|
PENDING("pending"),
|
||||||
|
APPROVED("approved"),
|
||||||
|
REJECTED("rejected");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
ClerkMediaReviewState(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClerkMediaReviewState fromCode(String code) {
|
||||||
|
if (code == null || code.isEmpty()) {
|
||||||
|
return DRAFT;
|
||||||
|
}
|
||||||
|
for (ClerkMediaReviewState state : values()) {
|
||||||
|
if (state.code.equalsIgnoreCase(code) || state.name().equalsIgnoreCase(code)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DRAFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.starry.admin.modules.clerk.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum ClerkMediaUsage {
|
||||||
|
|
||||||
|
PROFILE("profile"),
|
||||||
|
AVATAR("avatar"),
|
||||||
|
MOMENTS("moments"),
|
||||||
|
VOICE_INTRO("voice_intro"),
|
||||||
|
PROMO("promo"),
|
||||||
|
OTHER("other");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
ClerkMediaUsage(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClerkMediaUsage fromCode(String code) {
|
||||||
|
if (code == null || code.isEmpty()) {
|
||||||
|
return PROFILE;
|
||||||
|
}
|
||||||
|
for (ClerkMediaUsage usage : values()) {
|
||||||
|
if (usage.code.equalsIgnoreCase(code) || usage.name().equalsIgnoreCase(code)) {
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PROFILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.clerk.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
|
||||||
|
public interface PlayClerkMediaAssetMapper extends BaseMapper<PlayClerkMediaAssetEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.starry.admin.modules.clerk.mapper;
|
package com.starry.admin.modules.clerk.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||||
import com.github.yulichang.base.MPJBaseMapper;
|
import com.github.yulichang.base.MPJBaseMapper;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 店员Mapper接口
|
* 店员Mapper接口
|
||||||
@@ -11,4 +14,7 @@ import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
|||||||
*/
|
*/
|
||||||
public interface PlayClerkUserInfoMapper extends MPJBaseMapper<PlayClerkUserInfoEntity> {
|
public interface PlayClerkUserInfoMapper extends MPJBaseMapper<PlayClerkUserInfoEntity> {
|
||||||
|
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
@Select("SELECT id, tenant_id, album FROM play_clerk_user_info WHERE deleted = 0 AND album IS NOT NULL")
|
||||||
|
List<PlayClerkUserInfoEntity> selectAllWithAlbumIgnoringTenant();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.starry.admin.modules.clerk.module.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.starry.common.domain.BaseEntity;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName(value = "play_clerk_media_asset")
|
||||||
|
public class PlayClerkMediaAssetEntity extends BaseEntity<PlayClerkMediaAssetEntity> {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String clerkId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租戶 ID,供 TenantLine 過濾
|
||||||
|
*/
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
private String mediaId;
|
||||||
|
|
||||||
|
@TableField("`usage`")
|
||||||
|
private String usage;
|
||||||
|
|
||||||
|
private String reviewState;
|
||||||
|
|
||||||
|
private Integer orderIndex;
|
||||||
|
|
||||||
|
private LocalDateTime submittedTime;
|
||||||
|
|
||||||
|
private String reviewRecordId;
|
||||||
|
|
||||||
|
private String note;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.clerk.module.entity;
|
|||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
import io.swagger.annotations.ApiModel;
|
import io.swagger.annotations.ApiModel;
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -94,6 +95,12 @@ public class PlayClerkUserReturnVo {
|
|||||||
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资列表
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个性签名
|
* 个性签名
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
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 java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IPlayClerkMediaAssetService extends IService<PlayClerkMediaAssetEntity> {
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity linkDraftAsset(String tenantId, String clerkId, String mediaId, ClerkMediaUsage usage);
|
||||||
|
|
||||||
|
void submitWithOrder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds);
|
||||||
|
|
||||||
|
void reorder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds);
|
||||||
|
|
||||||
|
void softDelete(String clerkId, String mediaId);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> listByState(String clerkId, ClerkMediaUsage usage, Collection<ClerkMediaReviewState> states);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> listActiveByUsage(String clerkId, ClerkMediaUsage usage);
|
||||||
|
|
||||||
|
void applyReviewDecision(String clerkId, ClerkMediaUsage usage, List<String> approvedValues, String reviewRecordId, String note);
|
||||||
|
}
|
||||||
@@ -252,5 +252,12 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
|
|||||||
|
|
||||||
List<PlayClerkUserInfoEntity> simpleList();
|
List<PlayClerkUserInfoEntity> simpleList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询存在相册字段数据的店员(忽略租户隔离)
|
||||||
|
*
|
||||||
|
* @return 店员集合
|
||||||
|
*/
|
||||||
|
List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant();
|
||||||
|
|
||||||
JSONObject getPcData(PlayClerkUserInfoEntity entity);
|
JSONObject getPcData(PlayClerkUserInfoEntity entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.starry.admin.modules.clerk.service.impl;
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
@@ -7,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
import com.starry.admin.modules.clerk.mapper.PlayClerkDataReviewInfoMapper;
|
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;
|
||||||
@@ -15,12 +17,23 @@ 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;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
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.clerk.service.IPlayClerkUserInfoService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaKind;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
import com.starry.common.enums.ClerkReviewState;
|
import com.starry.common.enums.ClerkReviewState;
|
||||||
import com.starry.common.utils.IdUtils;
|
import com.starry.common.utils.IdUtils;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -42,6 +55,12 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayClerkUserInfoService playClerkUserInfoService;
|
private IPlayClerkUserInfoService playClerkUserInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询店员资料审核
|
* 查询店员资料审核
|
||||||
*
|
*
|
||||||
@@ -147,7 +166,8 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
userInfo.setAvatar(entity.getDataContent().get(0));
|
userInfo.setAvatar(entity.getDataContent().get(0));
|
||||||
}
|
}
|
||||||
if ("2".equals(entity.getDataType())) {
|
if ("2".equals(entity.getDataType())) {
|
||||||
userInfo.setAlbum(entity.getDataContent());
|
userInfo.setAlbum(new ArrayList<>());
|
||||||
|
synchronizeApprovedAlbumMedia(entity);
|
||||||
}
|
}
|
||||||
if ("3".equals(entity.getDataType())) {
|
if ("3".equals(entity.getDataType())) {
|
||||||
userInfo.setAudio(entity.getDataContent().get(0));
|
userInfo.setAudio(entity.getDataContent().get(0));
|
||||||
@@ -159,6 +179,71 @@ public class PlayClerkDataReviewInfoServiceImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void synchronizeApprovedAlbumMedia(PlayClerkDataReviewInfoEntity reviewInfo) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = playClerkUserInfoService.getById(reviewInfo.getClerkId());
|
||||||
|
if (clerkInfo == null) {
|
||||||
|
throw new CustomException("店员信息不存在,无法同步媒资");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> rawContent = reviewInfo.getDataContent();
|
||||||
|
List<String> sanitized = CollectionUtil.isEmpty(rawContent)
|
||||||
|
? Collections.emptyList()
|
||||||
|
: rawContent.stream().filter(StrUtil::isNotBlank).map(String::trim).distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<String> resolvedMediaIds = new ArrayList<>();
|
||||||
|
for (String value : sanitized) {
|
||||||
|
PlayMediaEntity media = resolveMediaEntity(clerkInfo, value);
|
||||||
|
if (media == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
clerkMediaAssetService.linkDraftAsset(clerkInfo.getTenantId(), clerkInfo.getId(), media.getId(),
|
||||||
|
ClerkMediaUsage.PROFILE);
|
||||||
|
resolvedMediaIds.add(media.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
clerkMediaAssetService.applyReviewDecision(clerkInfo.getId(), ClerkMediaUsage.PROFILE, resolvedMediaIds,
|
||||||
|
reviewInfo.getId(), reviewInfo.getReviewCon());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity resolveMediaEntity(PlayClerkUserInfoEntity clerkInfo, String value) {
|
||||||
|
if (StrUtil.isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlayMediaEntity media = mediaService.getById(value);
|
||||||
|
if (media != null) {
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
media = mediaService.lambdaQuery()
|
||||||
|
.eq(PlayMediaEntity::getOwnerType, MediaOwnerType.CLERK)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, clerkInfo.getId())
|
||||||
|
.eq(PlayMediaEntity::getUrl, value)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
if (media != null) {
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
return createMediaFromLegacyUrl(clerkInfo, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity createMediaFromLegacyUrl(PlayClerkUserInfoEntity clerkInfo, String url) {
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId(IdUtils.getUuid());
|
||||||
|
media.setTenantId(clerkInfo.getTenantId());
|
||||||
|
media.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
media.setOwnerId(clerkInfo.getId());
|
||||||
|
media.setKind(MediaKind.IMAGE.getCode());
|
||||||
|
media.setStatus(MediaStatus.APPROVED.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());
|
||||||
|
mediaService.updateById(media);
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改店员资料审核
|
* 修改店员资料审核
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.mapper.PlayClerkMediaAssetMapper;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PlayClerkMediaAssetServiceImpl extends ServiceImpl<PlayClerkMediaAssetMapper, PlayClerkMediaAssetEntity>
|
||||||
|
implements IPlayClerkMediaAssetService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public PlayClerkMediaAssetEntity linkDraftAsset(String tenantId, String clerkId, String mediaId,
|
||||||
|
ClerkMediaUsage usage) {
|
||||||
|
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
|
||||||
|
.eq(StrUtil.isNotBlank(tenantId), PlayClerkMediaAssetEntity::getTenantId, tenantId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getMediaId, mediaId);
|
||||||
|
PlayClerkMediaAssetEntity existing = this.getOne(wrapper, false);
|
||||||
|
if (existing != null) {
|
||||||
|
if (StrUtil.isBlank(existing.getTenantId()) && StrUtil.isNotBlank(tenantId)) {
|
||||||
|
existing.setTenantId(tenantId);
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(existing.getDeleted())) {
|
||||||
|
existing.setDeleted(false);
|
||||||
|
}
|
||||||
|
existing.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
|
||||||
|
if (existing.getOrderIndex() == null) {
|
||||||
|
existing.setOrderIndex(resolveNextOrderIndex(clerkId, usage));
|
||||||
|
}
|
||||||
|
this.updateById(existing);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity entity = new PlayClerkMediaAssetEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setClerkId(clerkId);
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setMediaId(mediaId);
|
||||||
|
entity.setUsage(usage.getCode());
|
||||||
|
entity.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
|
||||||
|
entity.setOrderIndex(resolveNextOrderIndex(clerkId, usage));
|
||||||
|
entity.setDeleted(false);
|
||||||
|
this.save(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void submitWithOrder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds) {
|
||||||
|
List<String> ordered = distinctMediaIds(mediaIds);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
|
||||||
|
if (CollectionUtil.isEmpty(assets)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, PlayClerkMediaAssetEntity> assetsByMediaId = assets.stream()
|
||||||
|
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
|
||||||
|
int order = 0;
|
||||||
|
for (String mediaId : ordered) {
|
||||||
|
PlayClerkMediaAssetEntity asset = assetsByMediaId.get(mediaId);
|
||||||
|
if (asset == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
asset.setOrderIndex(order++);
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.PENDING.getCode());
|
||||||
|
asset.setSubmittedTime(LocalDateTime.now());
|
||||||
|
updates.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> keepSet = ordered.stream().collect(Collectors.toSet());
|
||||||
|
for (PlayClerkMediaAssetEntity asset : assets) {
|
||||||
|
if (!keepSet.contains(asset.getMediaId())) {
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
asset.setOrderIndex(0);
|
||||||
|
updates.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updates)) {
|
||||||
|
this.updateBatchById(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void reorder(String clerkId, ClerkMediaUsage usage, List<String> mediaIds) {
|
||||||
|
List<String> ordered = distinctMediaIds(mediaIds);
|
||||||
|
if (CollectionUtil.isEmpty(ordered)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
|
||||||
|
if (CollectionUtil.isEmpty(assets)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, PlayClerkMediaAssetEntity> assetsByMediaId = assets.stream()
|
||||||
|
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
|
||||||
|
int order = 0;
|
||||||
|
for (String mediaId : ordered) {
|
||||||
|
PlayClerkMediaAssetEntity asset = assetsByMediaId.get(mediaId);
|
||||||
|
if (asset == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(asset.getOrderIndex(), order)) {
|
||||||
|
asset.setOrderIndex(order);
|
||||||
|
updates.add(asset);
|
||||||
|
}
|
||||||
|
order++;
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updates)) {
|
||||||
|
this.updateBatchById(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void softDelete(String clerkId, String mediaId) {
|
||||||
|
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getMediaId, mediaId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getDeleted, false);
|
||||||
|
PlayClerkMediaAssetEntity asset = this.getOne(wrapper, false);
|
||||||
|
if (asset == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
asset.setDeleted(true);
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
this.updateById(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayClerkMediaAssetEntity> listByState(String clerkId, ClerkMediaUsage usage,
|
||||||
|
Collection<ClerkMediaReviewState> states) {
|
||||||
|
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
|
||||||
|
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
|
||||||
|
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime);
|
||||||
|
if (CollectionUtil.isNotEmpty(states)) {
|
||||||
|
wrapper.in(PlayClerkMediaAssetEntity::getReviewState,
|
||||||
|
states.stream().map(ClerkMediaReviewState::getCode).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
return this.list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayClerkMediaAssetEntity> listActiveByUsage(String clerkId, ClerkMediaUsage usage) {
|
||||||
|
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
|
||||||
|
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
|
||||||
|
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime);
|
||||||
|
return this.list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void applyReviewDecision(String clerkId, ClerkMediaUsage usage, List<String> approvedValues,
|
||||||
|
String reviewRecordId, String note) {
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = listActiveByUsage(clerkId, usage);
|
||||||
|
if (CollectionUtil.isEmpty(assets)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> normalized = distinctMediaIds(approvedValues);
|
||||||
|
Map<String, PlayClerkMediaAssetEntity> byMediaId = assets.stream()
|
||||||
|
.collect(Collectors.toMap(PlayClerkMediaAssetEntity::getMediaId, item -> item));
|
||||||
|
Map<String, PlayClerkMediaAssetEntity> byUrl = buildAssetByUrlMap(assets);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = new ArrayList<>();
|
||||||
|
Set<String> approvedAssetIds = new java.util.HashSet<>();
|
||||||
|
int order = 0;
|
||||||
|
for (String value : normalized) {
|
||||||
|
PlayClerkMediaAssetEntity asset = byMediaId.get(value);
|
||||||
|
if (asset == null) {
|
||||||
|
asset = byUrl.get(value);
|
||||||
|
}
|
||||||
|
if (asset == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
asset.setOrderIndex(order++);
|
||||||
|
asset.setReviewRecordId(reviewRecordId);
|
||||||
|
if (StrUtil.isNotBlank(note)) {
|
||||||
|
asset.setNote(note);
|
||||||
|
}
|
||||||
|
updates.add(asset);
|
||||||
|
approvedAssetIds.add(asset.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (PlayClerkMediaAssetEntity asset : assets) {
|
||||||
|
if (approvedAssetIds.contains(asset.getId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
asset.setReviewRecordId(reviewRecordId);
|
||||||
|
updates.add(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(updates)) {
|
||||||
|
this.updateBatchById(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveNextOrderIndex(String clerkId, ClerkMediaUsage usage) {
|
||||||
|
LambdaQueryWrapper<PlayClerkMediaAssetEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayClerkMediaAssetEntity::getClerkId, clerkId)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getUsage, usage.getCode())
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
|
||||||
|
.orderByDesc(PlayClerkMediaAssetEntity::getOrderIndex)
|
||||||
|
.last("limit 1");
|
||||||
|
PlayClerkMediaAssetEntity last = this.getOne(wrapper, false);
|
||||||
|
if (last == null || last.getOrderIndex() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return last.getOrderIndex() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> distinctMediaIds(List<String> mediaIds) {
|
||||||
|
if (CollectionUtil.isEmpty(mediaIds)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return mediaIds.stream()
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.map(String::trim)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PlayClerkMediaAssetEntity> buildAssetByUrlMap(List<PlayClerkMediaAssetEntity> assets) {
|
||||||
|
if (CollectionUtil.isEmpty(assets)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).collect(Collectors.toList());
|
||||||
|
List<PlayMediaEntity> mediaList = mediaService.listByIds(mediaIds);
|
||||||
|
if (CollectionUtil.isEmpty(mediaList)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, String> mediaIdToUrl = mediaList.stream()
|
||||||
|
.filter(item -> StrUtil.isNotBlank(item.getUrl()))
|
||||||
|
.collect(Collectors.toMap(PlayMediaEntity::getId, PlayMediaEntity::getUrl, (left, right) -> left));
|
||||||
|
Map<String, PlayClerkMediaAssetEntity> map = new HashMap<>();
|
||||||
|
for (PlayClerkMediaAssetEntity asset : assets) {
|
||||||
|
String url = mediaIdToUrl.get(asset.getMediaId());
|
||||||
|
if (StrUtil.isNotBlank(url)) {
|
||||||
|
map.put(url, asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.starry.admin.modules.clerk.service.impl;
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
@@ -12,10 +13,13 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper;
|
|||||||
import com.starry.admin.common.component.JwtToken;
|
import com.starry.admin.common.component.JwtToken;
|
||||||
import com.starry.admin.common.domain.LoginUser;
|
import com.starry.admin.common.domain.LoginUser;
|
||||||
import com.starry.admin.common.exception.CustomException;
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaReviewState;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
import com.starry.admin.modules.clerk.mapper.PlayClerkUserInfoMapper;
|
import com.starry.admin.modules.clerk.mapper.PlayClerkUserInfoMapper;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
|
||||||
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.PlayClerkLevelInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
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.PlayClerkUserQueryVo;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserQueryVo;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReturnVo;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserReturnVo;
|
||||||
@@ -29,9 +33,13 @@ import com.starry.admin.modules.clerk.module.vo.PlayClerkUnsettledWagesInfoRetur
|
|||||||
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
|
||||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
import com.starry.admin.modules.custom.entity.PlayCustomFollowInfoEntity;
|
import com.starry.admin.modules.custom.entity.PlayCustomFollowInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomFollowInfoService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
|
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelAdminInfoEntity;
|
||||||
@@ -43,7 +51,9 @@ import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService
|
|||||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelWaiterInfoService;
|
import com.starry.admin.modules.personnel.service.IPlayPersonnelWaiterInfoService;
|
||||||
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
|
||||||
import com.starry.admin.modules.system.service.LoginService;
|
import com.starry.admin.modules.system.service.LoginService;
|
||||||
|
import com.starry.admin.modules.weichat.assembler.ClerkMediaAssembler;
|
||||||
import com.starry.admin.modules.weichat.entity.PlayClerkUserLoginResponseVo;
|
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.PlayClerkUserInfoQueryVo;
|
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoQueryVo;
|
||||||
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
|
||||||
import com.starry.admin.utils.SecurityUtils;
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
@@ -53,6 +63,7 @@ import com.starry.common.utils.StringUtils;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -86,6 +97,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
@Resource
|
@Resource
|
||||||
private IPlayCustomFollowInfoService customFollowInfoService;
|
private IPlayCustomFollowInfoService customFollowInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@Resource
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Resource
|
||||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
private IPlayOrderInfoService playOrderInfoService;
|
private IPlayOrderInfoService playOrderInfoService;
|
||||||
@@ -220,6 +235,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
|
|
||||||
result.setPcData(this.getPcData(userInfo));
|
result.setPcData(this.getPcData(userInfo));
|
||||||
result.setLevelInfo(playClerkLevelInfoService.selectPlayClerkLevelInfoById(userInfo.getLevelId()));
|
result.setLevelInfo(playClerkLevelInfoService.selectPlayClerkLevelInfoById(userInfo.getLevelId()));
|
||||||
|
List<MediaVo> mediaList = loadMediaForClerk(userInfo.getId(), true);
|
||||||
|
result.setMediaList(mergeLegacyAlbum(userInfo.getAlbum(), mediaList));
|
||||||
|
result.setAlbum(CollectionUtil.isEmpty(userInfo.getAlbum()) ? new ArrayList<>() : new ArrayList<>(userInfo.getAlbum()));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,22 +351,27 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
|
.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
|
||||||
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
|
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
|
||||||
.orderByAsc(PlayClerkUserInfoEntity::getCreatedTime)
|
.orderByAsc(PlayClerkUserInfoEntity::getCreatedTime)
|
||||||
|
.orderByAsc(PlayClerkUserInfoEntity::getNickname)
|
||||||
.orderByAsc(PlayClerkUserInfoEntity::getId);
|
.orderByAsc(PlayClerkUserInfoEntity::getId);
|
||||||
|
|
||||||
IPage<PlayClerkUserInfoResultVo> rawPage = this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
|
IPage<PlayClerkUserInfoResultVo> pageResult = this.baseMapper.selectJoinPage(page,
|
||||||
if (rawPage != null && rawPage.getRecords() != null) {
|
PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
|
||||||
|
if (pageResult != null && pageResult.getRecords() != null) {
|
||||||
List<PlayClerkUserInfoResultVo> deduped = new ArrayList<>();
|
List<PlayClerkUserInfoResultVo> deduped = new ArrayList<>();
|
||||||
Set<String> seen = new HashSet<>();
|
Set<String> seen = new HashSet<>();
|
||||||
for (PlayClerkUserInfoResultVo record : rawPage.getRecords()) {
|
for (PlayClerkUserInfoResultVo record : pageResult.getRecords()) {
|
||||||
String id = record.getId();
|
String id = record.getId();
|
||||||
if (id == null || !seen.add(id)) {
|
if (id == null || !seen.add(id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
deduped.add(record);
|
deduped.add(record);
|
||||||
}
|
}
|
||||||
rawPage.setRecords(deduped);
|
pageResult.setRecords(deduped);
|
||||||
}
|
}
|
||||||
return rawPage;
|
if (pageResult != null) {
|
||||||
|
attachMediaToResultVos(pageResult.getRecords(), false);
|
||||||
|
}
|
||||||
|
return pageResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -500,6 +523,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
|
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachMediaToAdminVos(page.getRecords());
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,6 +615,11 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
return this.baseMapper.selectList(lambdaQueryWrapper);
|
return this.baseMapper.selectList(lambdaQueryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant() {
|
||||||
|
return playClerkUserInfoMapper.selectAllWithAlbumIgnoringTenant();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
|
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
@@ -628,4 +657,97 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
|||||||
invalidateClerkSession(beforeUpdate.getId());
|
invalidateClerkSession(beforeUpdate.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private void attachMediaToResultVos(List<PlayClerkUserInfoResultVo> records, boolean includePending) {
|
||||||
|
if (CollectionUtil.isEmpty(records)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(
|
||||||
|
records.stream().map(PlayClerkUserInfoResultVo::getId).collect(Collectors.toList()), includePending);
|
||||||
|
for (PlayClerkUserInfoResultVo record : records) {
|
||||||
|
List<MediaVo> mediaList = new ArrayList<>(mediaMap.getOrDefault(record.getId(), Collections.emptyList()));
|
||||||
|
record.setMediaList(mergeLegacyAlbum(record.getAlbum(), mediaList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachMediaToAdminVos(List<PlayClerkUserReturnVo> records) {
|
||||||
|
if (CollectionUtil.isEmpty(records)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(
|
||||||
|
records.stream().map(PlayClerkUserReturnVo::getId).collect(Collectors.toList()), true);
|
||||||
|
for (PlayClerkUserReturnVo record : records) {
|
||||||
|
List<MediaVo> mediaList = new ArrayList<>(mediaMap.getOrDefault(record.getId(), Collections.emptyList()));
|
||||||
|
record.setMediaList(mergeLegacyAlbum(record.getAlbum(), mediaList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MediaVo> loadMediaForClerk(String clerkId, boolean includePending) {
|
||||||
|
Map<String, List<MediaVo>> mediaMap = resolveMediaByAssets(Collections.singletonList(clerkId), includePending);
|
||||||
|
return new ArrayList<>(mediaMap.getOrDefault(clerkId, Collections.emptyList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<MediaVo>> resolveMediaByAssets(List<String> clerkIds, boolean includePending) {
|
||||||
|
if (CollectionUtil.isEmpty(clerkIds)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ClerkMediaReviewState> targetStates = includePending
|
||||||
|
? Arrays.asList(ClerkMediaReviewState.APPROVED, ClerkMediaReviewState.PENDING,
|
||||||
|
ClerkMediaReviewState.DRAFT, ClerkMediaReviewState.REJECTED)
|
||||||
|
: Collections.singletonList(ClerkMediaReviewState.APPROVED);
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.lambdaQuery()
|
||||||
|
.in(PlayClerkMediaAssetEntity::getClerkId, clerkIds)
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getUsage, ClerkMediaUsage.PROFILE.getCode())
|
||||||
|
.eq(PlayClerkMediaAssetEntity::getDeleted, false)
|
||||||
|
.in(CollectionUtil.isNotEmpty(targetStates), PlayClerkMediaAssetEntity::getReviewState,
|
||||||
|
targetStates.stream().map(ClerkMediaReviewState::getCode).collect(Collectors.toList()))
|
||||||
|
.orderByAsc(PlayClerkMediaAssetEntity::getOrderIndex)
|
||||||
|
.orderByDesc(PlayClerkMediaAssetEntity::getCreatedTime)
|
||||||
|
.list();
|
||||||
|
if (CollectionUtil.isEmpty(assets)) {
|
||||||
|
Map<String, List<MediaVo>> empty = new HashMap<>();
|
||||||
|
clerkIds.forEach(id -> empty.put(id, Collections.emptyList()));
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, PlayMediaEntity> mediaById = CollectionUtil.isEmpty(mediaIds)
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: mediaService.listByIds(mediaIds).stream()
|
||||||
|
.collect(Collectors.toMap(PlayMediaEntity::getId, item -> item, (left, right) -> left));
|
||||||
|
|
||||||
|
Map<String, List<PlayClerkMediaAssetEntity>> groupedAssets = assets.stream()
|
||||||
|
.collect(Collectors.groupingBy(PlayClerkMediaAssetEntity::getClerkId));
|
||||||
|
|
||||||
|
Map<String, List<MediaVo>> result = new HashMap<>(groupedAssets.size());
|
||||||
|
groupedAssets.forEach((clerkId, assetList) -> result.put(clerkId, ClerkMediaAssembler.toVoList(assetList, mediaById)));
|
||||||
|
|
||||||
|
clerkIds.forEach(id -> result.computeIfAbsent(id, key -> Collections.emptyList()));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<MediaVo> mergeLegacyAlbum(List<String> legacyAlbum, List<MediaVo> destination) {
|
||||||
|
if (CollectionUtil.isEmpty(legacyAlbum)) {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
Set<String> existingUrls = destination.stream()
|
||||||
|
.map(MediaVo::getUrl)
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
for (String url : legacyAlbum) {
|
||||||
|
if (StrUtil.isBlank(url) || !existingUrls.add(url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MediaVo legacyVo = new MediaVo();
|
||||||
|
legacyVo.setId(url);
|
||||||
|
legacyVo.setUrl(url);
|
||||||
|
legacyVo.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
legacyVo.setStatus(MediaStatus.READY.getCode());
|
||||||
|
legacyVo.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
destination.add(legacyVo);
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.starry.admin.modules.clerk.task;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
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.MediaKind;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.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;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一次性迁移旧相册数据到媒资表。启用方式:启动时配置
|
||||||
|
* {@code clerk.media.migration-enabled=true}。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(prefix = "clerk.media", name = "migration-enabled", havingValue = "true")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ClerkAlbumMigrationRunner implements ApplicationRunner {
|
||||||
|
|
||||||
|
private final IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
private final IPlayMediaService mediaService;
|
||||||
|
private final IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
log.info("[ClerkAlbumMigration] start migration from legacy album column");
|
||||||
|
List<PlayClerkUserInfoEntity> candidates = clerkUserInfoService.listWithAlbumIgnoringTenant();
|
||||||
|
|
||||||
|
if (CollectionUtil.isEmpty(candidates)) {
|
||||||
|
log.info("[ClerkAlbumMigration] no clerk records with legacy album found, skip");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicInteger migratedOwners = new AtomicInteger();
|
||||||
|
AtomicInteger migratedMedia = new AtomicInteger();
|
||||||
|
String originalTenantId = SecurityUtils.getTenantId();
|
||||||
|
for (PlayClerkUserInfoEntity clerk : candidates) {
|
||||||
|
String tenantId = StrUtil.blankToDefault(clerk.getTenantId(), originalTenantId);
|
||||||
|
SecurityUtils.setTenantId(tenantId);
|
||||||
|
try {
|
||||||
|
List<String> album = clerk.getAlbum();
|
||||||
|
if (CollectionUtil.isEmpty(album)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> sanitizedAlbum = album.stream()
|
||||||
|
.filter(StrUtil::isNotBlank)
|
||||||
|
.map(String::trim)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (CollectionUtil.isEmpty(sanitizedAlbum)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<String> approvedMediaIds = new ArrayList<>();
|
||||||
|
for (String value : sanitizedAlbum) {
|
||||||
|
PlayMediaEntity media = resolveMediaEntity(clerk, value);
|
||||||
|
if (media == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!MediaStatus.APPROVED.getCode().equals(media.getStatus())) {
|
||||||
|
media.setStatus(MediaStatus.APPROVED.getCode());
|
||||||
|
mediaService.updateById(media);
|
||||||
|
}
|
||||||
|
clerkMediaAssetService.linkDraftAsset(clerk.getTenantId(), clerk.getId(), media.getId(),
|
||||||
|
ClerkMediaUsage.PROFILE);
|
||||||
|
approvedMediaIds.add(media.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
clerk.getId());
|
||||||
|
} finally {
|
||||||
|
SecurityUtils.setTenantId(originalTenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[ClerkAlbumMigration] completed, owners migrated: {}, media migrated: {}", migratedOwners.get(),
|
||||||
|
migratedMedia.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity resolveMediaEntity(PlayClerkUserInfoEntity clerk, String value) {
|
||||||
|
if (StrUtil.isBlank(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
PlayMediaEntity byId = mediaService.getById(value);
|
||||||
|
if (byId != null) {
|
||||||
|
return byId;
|
||||||
|
}
|
||||||
|
PlayMediaEntity byUrl = mediaService.lambdaQuery()
|
||||||
|
.eq(PlayMediaEntity::getOwnerType, MediaOwnerType.CLERK)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, clerk.getId())
|
||||||
|
.eq(PlayMediaEntity::getUrl, value)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
if (byUrl != null) {
|
||||||
|
return byUrl;
|
||||||
|
}
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId(IdUtils.getUuid());
|
||||||
|
media.setTenantId(clerk.getTenantId());
|
||||||
|
media.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
media.setOwnerId(clerk.getId());
|
||||||
|
media.setKind(MediaKind.IMAGE.getCode());
|
||||||
|
media.setStatus(MediaStatus.APPROVED.getCode());
|
||||||
|
media.setUrl(value);
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("legacySource", "album_migration");
|
||||||
|
media.setMetadata(metadata);
|
||||||
|
mediaService.normalizeAndSave(media);
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.starry.admin.modules.media.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资表 play_media
|
||||||
|
*
|
||||||
|
* <p>存储各类业务(店员、顾客等)的图片/视频。</p>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName(value = "play_media", autoResultMap = true)
|
||||||
|
public class PlayMediaEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归属业务类型,例如 clerk/custom/order
|
||||||
|
*/
|
||||||
|
private String ownerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归属业务主键,例如店员ID
|
||||||
|
*/
|
||||||
|
private String ownerId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资类型 image / video
|
||||||
|
*/
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资状态 uploaded / processing / ready / approved / rejected
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源地址
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频封面地址
|
||||||
|
*/
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时长(毫秒)
|
||||||
|
*/
|
||||||
|
private Long durationMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资宽度
|
||||||
|
*/
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资高度
|
||||||
|
*/
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件大小(字节)
|
||||||
|
*/
|
||||||
|
private Long sizeBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序序号,从 0 开始
|
||||||
|
*/
|
||||||
|
private Integer orderIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展字段
|
||||||
|
*/
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
private Date createdTime;
|
||||||
|
|
||||||
|
private Date updatedTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum MediaKind {
|
||||||
|
|
||||||
|
IMAGE("image"),
|
||||||
|
VIDEO("video");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
MediaKind(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isVideo(String value) {
|
||||||
|
return VIDEO.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isImage(String value) {
|
||||||
|
return IMAGE.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaKind fromCode(String value) {
|
||||||
|
for (MediaKind kind : values()) {
|
||||||
|
if (kind.code.equalsIgnoreCase(value)) {
|
||||||
|
return kind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unsupported media kind: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
public final class MediaOwnerType {
|
||||||
|
|
||||||
|
private MediaOwnerType() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String CLERK = "clerk";
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.starry.admin.modules.media.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum MediaStatus {
|
||||||
|
UPLOADED("uploaded"),
|
||||||
|
PROCESSING("processing"),
|
||||||
|
READY("ready"),
|
||||||
|
APPROVED("approved"),
|
||||||
|
REJECTED("rejected");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
MediaStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isTerminal(String value) {
|
||||||
|
return APPROVED.code.equalsIgnoreCase(value) || REJECTED.code.equalsIgnoreCase(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.starry.admin.modules.media.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
|
||||||
|
public interface PlayMediaMapper extends BaseMapper<PlayMediaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.starry.admin.modules.media.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface IPlayMediaService extends IService<PlayMediaEntity> {
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId);
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses);
|
||||||
|
|
||||||
|
List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId);
|
||||||
|
|
||||||
|
PlayMediaEntity normalizeAndSave(PlayMediaEntity entity);
|
||||||
|
|
||||||
|
void updateOrder(String ownerType, String ownerId, List<String> orderedIds);
|
||||||
|
|
||||||
|
void softDelete(String ownerType, String ownerId, String mediaId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.starry.admin.modules.media.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.mapper.PlayMediaMapper;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PlayMediaServiceImpl extends ServiceImpl<PlayMediaMapper, PlayMediaEntity>
|
||||||
|
implements IPlayMediaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId) {
|
||||||
|
return listByOwner(ownerType, ownerId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listByOwner(String ownerType, String ownerId, Collection<String> statuses) {
|
||||||
|
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, ownerId)
|
||||||
|
.orderByAsc(PlayMediaEntity::getOrderIndex)
|
||||||
|
.orderByDesc(PlayMediaEntity::getCreatedTime);
|
||||||
|
if (CollectionUtil.isNotEmpty(statuses)) {
|
||||||
|
wrapper.in(PlayMediaEntity::getStatus, statuses);
|
||||||
|
}
|
||||||
|
return this.list(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PlayMediaEntity> listApprovedByOwner(String ownerType, String ownerId) {
|
||||||
|
return listByOwner(ownerType, ownerId, Collections.singleton(MediaStatus.APPROVED.getCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public PlayMediaEntity normalizeAndSave(PlayMediaEntity entity) {
|
||||||
|
Assert.notNull(entity, "媒资信息不能为空");
|
||||||
|
Assert.isTrue(StrUtil.isNotBlank(entity.getOwnerId()), "媒资归属ID不能为空");
|
||||||
|
// ownerType 默认 clerk
|
||||||
|
if (StrUtil.isBlank(entity.getOwnerType())) {
|
||||||
|
entity.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
}
|
||||||
|
if (entity.getOrderIndex() == null) {
|
||||||
|
entity.setOrderIndex(resolveNextOrderIndex(entity.getOwnerType(), entity.getOwnerId()));
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.UPLOADED.getCode());
|
||||||
|
}
|
||||||
|
boolean saved = this.save(entity);
|
||||||
|
if (!saved) {
|
||||||
|
throw new CustomException("媒资保存失败");
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateOrder(String ownerType, String ownerId, List<String> orderedIds) {
|
||||||
|
List<PlayMediaEntity> mediaList = listByOwner(ownerType, ownerId);
|
||||||
|
if (CollectionUtil.isEmpty(mediaList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, PlayMediaEntity> mediaById = mediaList.stream()
|
||||||
|
.collect(Collectors.toMap(PlayMediaEntity::getId, item -> item));
|
||||||
|
Set<String> keepSet = new LinkedHashSet<>();
|
||||||
|
if (CollectionUtil.isNotEmpty(orderedIds)) {
|
||||||
|
keepSet.addAll(orderedIds);
|
||||||
|
}
|
||||||
|
List<PlayMediaEntity> updates = new ArrayList<>();
|
||||||
|
int index = 0;
|
||||||
|
for (String mediaId : keepSet) {
|
||||||
|
PlayMediaEntity entity = mediaById.get(mediaId);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new CustomException("媒资不存在或已被删除");
|
||||||
|
}
|
||||||
|
entity.setOrderIndex(index++);
|
||||||
|
if (MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
|
}
|
||||||
|
updates.add(entity);
|
||||||
|
}
|
||||||
|
// 其他未保留的标记为 rejected
|
||||||
|
for (PlayMediaEntity entity : mediaList) {
|
||||||
|
if (!keepSet.contains(entity.getId())
|
||||||
|
&& !MediaStatus.REJECTED.getCode().equals(entity.getStatus())) {
|
||||||
|
entity.setStatus(MediaStatus.REJECTED.getCode());
|
||||||
|
entity.setOrderIndex(0);
|
||||||
|
updates.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(updates)) {
|
||||||
|
this.updateBatchById(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void softDelete(String ownerType, String ownerId, String mediaId) {
|
||||||
|
PlayMediaEntity entity = this.getById(mediaId);
|
||||||
|
if (entity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ownerType.equals(entity.getOwnerType()) || !ownerId.equals(entity.getOwnerId())) {
|
||||||
|
throw new CustomException("无权删除该媒资");
|
||||||
|
}
|
||||||
|
entity.setStatus(MediaStatus.REJECTED.getCode());
|
||||||
|
entity.setOrderIndex(0);
|
||||||
|
this.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int resolveNextOrderIndex(String ownerType, String ownerId) {
|
||||||
|
LambdaQueryWrapper<PlayMediaEntity> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(PlayMediaEntity::getOwnerType, ownerType)
|
||||||
|
.eq(PlayMediaEntity::getOwnerId, ownerId)
|
||||||
|
.ne(PlayMediaEntity::getStatus, MediaStatus.REJECTED.getCode())
|
||||||
|
.orderByDesc(PlayMediaEntity::getOrderIndex)
|
||||||
|
.last("limit 1");
|
||||||
|
PlayMediaEntity last = this.getOne(wrapper, false);
|
||||||
|
if (last == null || last.getOrderIndex() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return last.getOrderIndex() + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.starry.admin.modules.weichat.assembler;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class ClerkMediaAssembler {
|
||||||
|
|
||||||
|
private ClerkMediaAssembler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaVo toVo(PlayMediaEntity media, PlayClerkMediaAssetEntity asset) {
|
||||||
|
if (media == null || asset == null || Boolean.TRUE.equals(asset.getDeleted())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId(media.getId());
|
||||||
|
vo.setMediaId(media.getId());
|
||||||
|
vo.setAssetId(asset.getId());
|
||||||
|
vo.setKind(media.getKind());
|
||||||
|
vo.setStatus(media.getStatus());
|
||||||
|
vo.setUrl(media.getUrl());
|
||||||
|
vo.setCoverUrl(media.getCoverUrl());
|
||||||
|
vo.setDurationMs(media.getDurationMs());
|
||||||
|
vo.setWidth(media.getWidth());
|
||||||
|
vo.setHeight(media.getHeight());
|
||||||
|
vo.setSizeBytes(media.getSizeBytes());
|
||||||
|
vo.setOrderIndex(asset.getOrderIndex());
|
||||||
|
vo.setMetadata(media.getMetadata());
|
||||||
|
vo.setUsage(asset.getUsage());
|
||||||
|
vo.setReviewState(asset.getReviewState());
|
||||||
|
vo.setSubmittedTime(asset.getSubmittedTime());
|
||||||
|
vo.setReviewNote(asset.getNote());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediaVo> toVoList(List<PlayClerkMediaAssetEntity> assets,
|
||||||
|
Map<String, PlayMediaEntity> mediaById) {
|
||||||
|
if (assets == null || assets.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return assets.stream()
|
||||||
|
.map(asset -> toVo(mediaById.get(asset.getMediaId()), asset))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.starry.admin.modules.weichat.assembler;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class MediaAssembler {
|
||||||
|
|
||||||
|
private MediaAssembler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MediaVo toVo(PlayMediaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId(entity.getId());
|
||||||
|
vo.setMediaId(entity.getId());
|
||||||
|
vo.setKind(entity.getKind());
|
||||||
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setUrl(entity.getUrl());
|
||||||
|
vo.setCoverUrl(entity.getCoverUrl());
|
||||||
|
vo.setDurationMs(entity.getDurationMs());
|
||||||
|
vo.setWidth(entity.getWidth());
|
||||||
|
vo.setHeight(entity.getHeight());
|
||||||
|
vo.setSizeBytes(entity.getSizeBytes());
|
||||||
|
vo.setOrderIndex(entity.getOrderIndex());
|
||||||
|
vo.setMetadata(entity.getMetadata());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediaVo> toVoList(List<PlayMediaEntity> entities) {
|
||||||
|
if (entities == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return entities.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(MediaAssembler::toVo)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import com.starry.admin.utils.SecurityUtils;
|
|||||||
import com.starry.admin.utils.SmsUtils;
|
import com.starry.admin.utils.SmsUtils;
|
||||||
import com.starry.common.redis.RedisCache;
|
import com.starry.common.redis.RedisCache;
|
||||||
import com.starry.common.result.R;
|
import com.starry.common.result.R;
|
||||||
|
import com.starry.common.result.TypedR;
|
||||||
import com.starry.common.utils.ConvertUtil;
|
import com.starry.common.utils.ConvertUtil;
|
||||||
import com.starry.common.utils.StringUtils;
|
import com.starry.common.utils.StringUtils;
|
||||||
import com.starry.common.utils.VerificationCodeUtils;
|
import com.starry.common.utils.VerificationCodeUtils;
|
||||||
@@ -394,10 +395,10 @@ public class WxClerkController {
|
|||||||
* @return 店员列表
|
* @return 店员列表
|
||||||
*/
|
*/
|
||||||
@PostMapping("/user/queryByPage")
|
@PostMapping("/user/queryByPage")
|
||||||
public R queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
|
public TypedR<IPage<PlayClerkUserInfoResultVo>> queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
|
||||||
IPage<PlayClerkUserInfoResultVo> page = playClerkUserInfoService.selectByPage(vo,
|
IPage<PlayClerkUserInfoResultVo> page = playClerkUserInfoService.selectByPage(vo,
|
||||||
customUserService.getLoginUserId());
|
customUserService.getLoginUserId());
|
||||||
return R.ok(page);
|
return TypedR.ok(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.starry.admin.modules.weichat.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import com.starry.admin.common.aspect.ClerkUserLogin;
|
||||||
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
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.IPlayClerkMediaAssetService;
|
||||||
|
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.assembler.ClerkMediaAssembler;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaOrderRequest;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.MediaUploadService;
|
||||||
|
import com.starry.common.result.R;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Api(tags = "店员媒资接口")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/wx/clerk/media")
|
||||||
|
@Validated
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WxClerkMediaController {
|
||||||
|
|
||||||
|
private final MediaUploadService mediaUploadService;
|
||||||
|
private final IPlayMediaService mediaService;
|
||||||
|
private final IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@ApiOperation("上传媒资(图片/视频)")
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R upload(@RequestParam("file") MultipartFile file,
|
||||||
|
@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
MediaVo vo = mediaUploadService.upload(file, clerkInfo, ClerkMediaUsage.fromCode(usageCode));
|
||||||
|
return R.ok(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("更新媒资顺序并提交保留列表")
|
||||||
|
@PutMapping("/order")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R updateOrder(@Valid @RequestBody MediaOrderRequest request) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(request.getUsage());
|
||||||
|
List<String> mediaIds = CollUtil.isEmpty(request.getMediaIds()) ? Collections.emptyList()
|
||||||
|
: request.getMediaIds().stream().distinct().collect(Collectors.toList());
|
||||||
|
clerkMediaAssetService.submitWithOrder(clerkInfo.getId(), usage, mediaIds);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("删除媒资(软删除)")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R delete(@PathVariable("id") String mediaId) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
clerkMediaAssetService.softDelete(clerkInfo.getId(), mediaId);
|
||||||
|
mediaService.softDelete(MediaOwnerType.CLERK, clerkInfo.getId(), mediaId);
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("查询草稿媒资列表")
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R listDraft(@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(usageCode);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.listByState(clerkInfo.getId(), usage,
|
||||||
|
Arrays.asList(ClerkMediaReviewState.DRAFT, ClerkMediaReviewState.PENDING,
|
||||||
|
ClerkMediaReviewState.REJECTED));
|
||||||
|
Map<String, PlayMediaEntity> mediaMap = loadMediaMap(assets);
|
||||||
|
return R.ok(ClerkMediaAssembler.toVoList(assets, mediaMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("查询已审核通过的媒资")
|
||||||
|
@GetMapping("/approved")
|
||||||
|
@ClerkUserLogin
|
||||||
|
public R listApproved(@RequestParam(value = "usage", required = false) String usageCode) {
|
||||||
|
PlayClerkUserInfoEntity clerkInfo = requireClerkInfo();
|
||||||
|
ClerkMediaUsage usage = ClerkMediaUsage.fromCode(usageCode);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = clerkMediaAssetService.listByState(clerkInfo.getId(), usage,
|
||||||
|
Collections.singletonList(ClerkMediaReviewState.APPROVED));
|
||||||
|
Map<String, PlayMediaEntity> mediaMap = loadMediaMap(assets);
|
||||||
|
return R.ok(ClerkMediaAssembler.toVoList(assets, mediaMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity requireClerkInfo() {
|
||||||
|
PlayClerkUserInfoEntity clerk = ThreadLocalRequestDetail.getClerkUserInfo();
|
||||||
|
if (clerk == null) {
|
||||||
|
throw new CustomException("店员未登录");
|
||||||
|
}
|
||||||
|
return clerk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, PlayMediaEntity> loadMediaMap(List<PlayClerkMediaAssetEntity> assets) {
|
||||||
|
if (CollUtil.isEmpty(assets)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<String> mediaIds = assets.stream().map(PlayClerkMediaAssetEntity::getMediaId).distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<PlayMediaEntity> mediaList = mediaService.listByIds(mediaIds);
|
||||||
|
if (CollUtil.isEmpty(mediaList)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
return mediaList.stream().collect(Collectors.toMap(PlayMediaEntity::getId, item -> item));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.weichat.entity;
|
|||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkCommodityQueryVo;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -45,6 +46,11 @@ public class PlayClerkUserLoginResponseVo {
|
|||||||
*/
|
*/
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新媒资列表
|
||||||
|
*/
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 相册是否运行编辑
|
* 相册是否运行编辑
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.starry.admin.modules.weichat.entity.clerk;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MediaOrderRequest {
|
||||||
|
|
||||||
|
private String usage;
|
||||||
|
|
||||||
|
@NotNull(message = "媒资ID列表不能为空")
|
||||||
|
private List<String> mediaIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.starry.admin.modules.weichat.entity.clerk;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MediaVo implements Serializable {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String assetId;
|
||||||
|
|
||||||
|
private String mediaId;
|
||||||
|
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
private Long durationMs;
|
||||||
|
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
private Long sizeBytes;
|
||||||
|
|
||||||
|
private Integer orderIndex;
|
||||||
|
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
private String usage;
|
||||||
|
|
||||||
|
private String reviewState;
|
||||||
|
|
||||||
|
private LocalDateTime submittedTime;
|
||||||
|
|
||||||
|
private String reviewNote;
|
||||||
|
}
|
||||||
@@ -75,6 +75,12 @@ public class PlayClerkUserInfoResultVo {
|
|||||||
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
|
||||||
private List<String> album = new ArrayList<>();
|
private List<String> album = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒资列表
|
||||||
|
*/
|
||||||
|
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
|
||||||
|
private List<MediaVo> mediaList = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 个性签名
|
* 个性签名
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package com.starry.admin.modules.weichat.service;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileTypeUtil;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.IdUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.common.oss.service.IOssFileService;
|
||||||
|
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.IPlayClerkMediaAssetService;
|
||||||
|
import com.starry.admin.modules.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaKind;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaOwnerType;
|
||||||
|
import com.starry.admin.modules.media.enums.MediaStatus;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import com.starry.admin.modules.weichat.assembler.ClerkMediaAssembler;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import ws.schild.jave.Encoder;
|
||||||
|
import ws.schild.jave.MultimediaObject;
|
||||||
|
import ws.schild.jave.encode.AudioAttributes;
|
||||||
|
import ws.schild.jave.encode.EncodingAttributes;
|
||||||
|
import ws.schild.jave.encode.VideoAttributes;
|
||||||
|
import ws.schild.jave.info.MultimediaInfo;
|
||||||
|
import ws.schild.jave.info.VideoInfo;
|
||||||
|
import ws.schild.jave.info.VideoSize;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
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 String IMAGE_OUTPUT_FORMAT = "image2";
|
||||||
|
private static final String VIDEO_OUTPUT_FORMAT = "mp4";
|
||||||
|
|
||||||
|
private final IOssFileService ossFileService;
|
||||||
|
private final IPlayMediaService mediaService;
|
||||||
|
private final IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public MediaVo upload(MultipartFile file, PlayClerkUserInfoEntity clerkInfo, ClerkMediaUsage usage) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new CustomException("请选择要上传的文件");
|
||||||
|
}
|
||||||
|
if (clerkInfo == null) {
|
||||||
|
throw new CustomException("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = StrUtil.blankToDefault(file.getOriginalFilename(), file.getName());
|
||||||
|
File tempFile = null;
|
||||||
|
File processedVideoFile = null;
|
||||||
|
File coverFile = null;
|
||||||
|
try {
|
||||||
|
String suffix = resolveSuffix(originalFilename);
|
||||||
|
tempFile = createTempFile("media_", suffix);
|
||||||
|
file.transferTo(tempFile);
|
||||||
|
|
||||||
|
String detectedType = detectFileType(tempFile, file.getContentType());
|
||||||
|
boolean isVideo = isVideoType(detectedType, file.getContentType());
|
||||||
|
boolean isImage = isImageType(detectedType, file.getContentType());
|
||||||
|
if (!isVideo && !isImage) {
|
||||||
|
log.warn("Unsupported media type: {} / {}", detectedType, file.getContentType());
|
||||||
|
throw new CustomException("不支持的文件格式");
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayMediaEntity entity = buildSkeletonEntity(file, clerkInfo,
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
processedVideoFile = createTempFile("media_video_", ".mp4");
|
||||||
|
coverFile = createTempFile("media_cover_", ".jpg");
|
||||||
|
handleVideoUpload(tempFile, processedVideoFile, coverFile, entity, clerkInfo, originalFilename);
|
||||||
|
}
|
||||||
|
entity.setStatus(MediaStatus.READY.getCode());
|
||||||
|
mediaService.updateById(entity);
|
||||||
|
PlayClerkMediaAssetEntity asset = clerkMediaAssetService.linkDraftAsset(
|
||||||
|
clerkInfo.getTenantId(),
|
||||||
|
clerkInfo.getId(),
|
||||||
|
entity.getId(),
|
||||||
|
usage == null ? ClerkMediaUsage.PROFILE : usage);
|
||||||
|
return ClerkMediaAssembler.toVo(entity, asset);
|
||||||
|
} catch (CustomException customException) {
|
||||||
|
throw customException;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("媒资上传失败", ex);
|
||||||
|
throw new CustomException("媒资上传失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
deleteQuietly(processedVideoFile);
|
||||||
|
deleteQuietly(coverFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayMediaEntity buildSkeletonEntity(MultipartFile file, PlayClerkUserInfoEntity clerkInfo, MediaKind kind) {
|
||||||
|
PlayMediaEntity entity = new PlayMediaEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setTenantId(clerkInfo.getTenantId());
|
||||||
|
entity.setOwnerType(MediaOwnerType.CLERK);
|
||||||
|
entity.setOwnerId(clerkInfo.getId());
|
||||||
|
entity.setKind(kind.getCode());
|
||||||
|
entity.setStatus(MediaStatus.UPLOADED.getCode());
|
||||||
|
entity.setSizeBytes(file.getSize());
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("originalFilename", file.getOriginalFilename());
|
||||||
|
metadata.put("contentType", file.getContentType());
|
||||||
|
metadata.put("uploadTraceId", IdUtil.fastUUID());
|
||||||
|
metadata.put("sourceSizeBytes", file.getSize());
|
||||||
|
entity.setMetadata(metadata);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleImageUpload(File tempFile, PlayMediaEntity entity, PlayClerkUserInfoEntity clerkInfo,
|
||||||
|
String originalFilename) throws IOException {
|
||||||
|
BufferedImage image = ImageIO.read(tempFile);
|
||||||
|
if (image == null) {
|
||||||
|
throw new CustomException("图片文件已损坏或格式不受支持");
|
||||||
|
}
|
||||||
|
entity.setWidth(image.getWidth());
|
||||||
|
entity.setHeight(image.getHeight());
|
||||||
|
try (InputStream is = Files.newInputStream(tempFile.toPath())) {
|
||||||
|
String targetName = buildObjectName("img", originalFilename);
|
||||||
|
String url = ossFileService.upload(is, clerkInfo.getTenantId(), targetName);
|
||||||
|
entity.setUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleVideoUpload(File sourceFile, File targetFile, File coverFile, PlayMediaEntity entity,
|
||||||
|
PlayClerkUserInfoEntity clerkInfo, String originalFilename) throws Exception {
|
||||||
|
if (entity.getSizeBytes() != null && entity.getSizeBytes() > MAX_VIDEO_BYTES) {
|
||||||
|
throw new CustomException("视频大小不能超过30MB");
|
||||||
|
}
|
||||||
|
MultimediaObject multimediaObject = new MultimediaObject(sourceFile);
|
||||||
|
MultimediaInfo info = multimediaObject.getInfo();
|
||||||
|
if (info == null || info.getVideo() == null) {
|
||||||
|
throw new CustomException("无法读取视频信息");
|
||||||
|
}
|
||||||
|
long durationMs = info.getDuration();
|
||||||
|
if (durationMs > MAX_VIDEO_DURATION_MS) {
|
||||||
|
throw new CustomException("视频时长不能超过30秒");
|
||||||
|
}
|
||||||
|
VideoInfo videoInfo = info.getVideo();
|
||||||
|
VideoSize size = videoInfo.getSize();
|
||||||
|
if (size != null) {
|
||||||
|
entity.setWidth(size.getWidth());
|
||||||
|
entity.setHeight(size.getHeight());
|
||||||
|
}
|
||||||
|
entity.setDurationMs(durationMs);
|
||||||
|
|
||||||
|
AudioAttributes audioAttrs = new AudioAttributes();
|
||||||
|
audioAttrs.setCodec("aac");
|
||||||
|
audioAttrs.setBitRate(128_000);
|
||||||
|
audioAttrs.setChannels(2);
|
||||||
|
audioAttrs.setSamplingRate(44_100);
|
||||||
|
|
||||||
|
VideoAttributes videoAttrs = new VideoAttributes();
|
||||||
|
videoAttrs.setCodec("h264");
|
||||||
|
videoAttrs.setBitRate(1_500_000);
|
||||||
|
if (size != null) {
|
||||||
|
videoAttrs.setSize(size);
|
||||||
|
}
|
||||||
|
float frameRate = videoInfo.getFrameRate();
|
||||||
|
videoAttrs.setFrameRate(frameRate > 0 ? Math.round(frameRate) : 30);
|
||||||
|
|
||||||
|
Encoder encoder = new Encoder();
|
||||||
|
EncodingAttributes attrs = new EncodingAttributes();
|
||||||
|
attrs.setOutputFormat(VIDEO_OUTPUT_FORMAT);
|
||||||
|
attrs.setAudioAttributes(audioAttrs);
|
||||||
|
attrs.setVideoAttributes(videoAttrs);
|
||||||
|
encoder.encode(multimediaObject, targetFile, attrs);
|
||||||
|
|
||||||
|
long processedSize = targetFile.length();
|
||||||
|
entity.setSizeBytes(processedSize);
|
||||||
|
|
||||||
|
// 抽取首帧作为封面
|
||||||
|
EncodingAttributes coverAttrs = new EncodingAttributes();
|
||||||
|
VideoAttributes coverVideoAttrs = new VideoAttributes();
|
||||||
|
coverVideoAttrs.setCodec("mjpeg");
|
||||||
|
if (size != null) {
|
||||||
|
coverVideoAttrs.setSize(size);
|
||||||
|
}
|
||||||
|
coverAttrs.setOutputFormat(IMAGE_OUTPUT_FORMAT);
|
||||||
|
coverAttrs.setVideoAttributes(coverVideoAttrs);
|
||||||
|
coverAttrs.setDuration(0.01f);
|
||||||
|
coverAttrs.setOffset(0f);
|
||||||
|
coverAttrs.setAudioAttributes(null);
|
||||||
|
encoder.encode(new MultimediaObject(targetFile), coverFile, coverAttrs);
|
||||||
|
|
||||||
|
try (InputStream videoIs = Files.newInputStream(targetFile.toPath());
|
||||||
|
InputStream coverIs = Files.newInputStream(coverFile.toPath())) {
|
||||||
|
String videoName = buildObjectName("video", originalFilename);
|
||||||
|
String coverName = buildObjectName("cover", originalFilename + ".jpg");
|
||||||
|
String videoUrl = ossFileService.upload(videoIs, clerkInfo.getTenantId(), videoName);
|
||||||
|
String coverUrl = ossFileService.upload(coverIs, clerkInfo.getTenantId(), coverName);
|
||||||
|
entity.setUrl(videoUrl);
|
||||||
|
entity.setCoverUrl(coverUrl);
|
||||||
|
}
|
||||||
|
if (entity.getMetadata() != null) {
|
||||||
|
entity.getMetadata().put("durationMs", durationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String detectFileType(File file, String contentType) {
|
||||||
|
String type = null;
|
||||||
|
try {
|
||||||
|
type = FileTypeUtil.getType(file);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("Failed to read file type via signature, fallback to contentType: {}", contentType, ex);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(type)) {
|
||||||
|
return type.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(contentType)) {
|
||||||
|
return contentType.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isVideoType(String detectedType, String mime) {
|
||||||
|
if (StrUtil.isBlank(detectedType) && StrUtil.isBlank(mime)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower = StrUtil.blankToDefault(detectedType, "");
|
||||||
|
if (lower.contains("mp4") || lower.contains("mov") || lower.contains("quicktime")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String mimeLower = StrUtil.blankToDefault(mime, "").toLowerCase(Locale.ROOT);
|
||||||
|
return mimeLower.startsWith("video/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImageType(String detectedType, String mime) {
|
||||||
|
String lower = StrUtil.blankToDefault(detectedType, "");
|
||||||
|
if (lower.contains("jpg") || lower.contains("jpeg") || lower.contains("png") || lower.contains("gif")
|
||||||
|
|| lower.contains("webp")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String mimeLower = StrUtil.blankToDefault(mime, "").toLowerCase(Locale.ROOT);
|
||||||
|
return mimeLower.startsWith("image/");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildObjectName(String category, String originalFilename) {
|
||||||
|
String ext = resolveSuffix(originalFilename);
|
||||||
|
return StrUtil.join("/", "clerk", category, IdUtils.getUuid() + ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSuffix(String filename) {
|
||||||
|
if (StrUtil.isBlank(filename) || !filename.contains(".")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return filename.substring(filename.lastIndexOf('.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteQuietly(File file) {
|
||||||
|
if (file != null && file.exists()) {
|
||||||
|
FileUtil.del(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createTempFile(String prefix, String suffix) throws IOException {
|
||||||
|
String effectiveSuffix = StrUtil.isBlank(suffix) ? ".tmp" : suffix;
|
||||||
|
return Files.createTempFile(prefix, effectiveSuffix).toFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,10 @@ logging:
|
|||||||
org.springframework.web.servlet.DispatcherServlet: debug
|
org.springframework.web.servlet.DispatcherServlet: debug
|
||||||
org.springframework.security: debug
|
org.springframework.security: debug
|
||||||
|
|
||||||
|
clerk:
|
||||||
|
media:
|
||||||
|
migration-enabled: false
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
tokenHeader: X-Token #JWT存储的请求头
|
tokenHeader: X-Token #JWT存储的请求头
|
||||||
tokenHead: Bearer #JWT负载中拿到开头
|
tokenHead: Bearer #JWT负载中拿到开头
|
||||||
@@ -117,4 +121,3 @@ xl:
|
|||||||
authCode:
|
authCode:
|
||||||
# 登录验证码是否开启,开发环境配置false方便测试
|
# 登录验证码是否开启,开发环境配置false方便测试
|
||||||
enable: ${XL_LOGIN_AUTHCODE_ENABLE:false}
|
enable: ${XL_LOGIN_AUTHCODE_ENABLE:false}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE `play_media` (
|
||||||
|
`id` varchar(32) NOT NULL,
|
||||||
|
`tenant_id` varchar(32) NOT NULL,
|
||||||
|
`owner_type` varchar(32) NOT NULL COMMENT 'clerk/article/...',
|
||||||
|
`owner_id` varchar(32) NOT NULL,
|
||||||
|
`kind` varchar(16) NOT NULL COMMENT 'image | video',
|
||||||
|
`status` varchar(16) NOT NULL COMMENT 'uploaded|processing|ready|approved|rejected',
|
||||||
|
`url` varchar(1024) NOT NULL,
|
||||||
|
`cover_url` varchar(1024) DEFAULT NULL,
|
||||||
|
`duration_ms` bigint DEFAULT NULL,
|
||||||
|
`width` int DEFAULT NULL,
|
||||||
|
`height` int DEFAULT NULL,
|
||||||
|
`size_bytes` bigint DEFAULT NULL,
|
||||||
|
`order_index` int NOT NULL DEFAULT 0,
|
||||||
|
`metadata` json DEFAULT NULL,
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_media_owner` (`owner_type`,`owner_id`),
|
||||||
|
KEY `idx_media_order` (`tenant_id`,`owner_type`,`owner_id`,`order_index`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE `play_clerk_media_asset` (
|
||||||
|
`id` varchar(32) NOT NULL,
|
||||||
|
`clerk_id` varchar(32) NOT NULL,
|
||||||
|
`tenant_id` varchar(32) NOT NULL,
|
||||||
|
`media_id` varchar(32) NOT NULL,
|
||||||
|
`usage` varchar(32) NOT NULL,
|
||||||
|
`review_state` varchar(16) NOT NULL,
|
||||||
|
`order_index` int NOT NULL DEFAULT 0,
|
||||||
|
`submitted_time` datetime DEFAULT NULL,
|
||||||
|
`review_record_id` varchar(32) DEFAULT NULL,
|
||||||
|
`note` varchar(255) DEFAULT NULL,
|
||||||
|
`deleted` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_by` varchar(32) DEFAULT NULL,
|
||||||
|
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`updated_by` varchar(32) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_clerk_usage_media` (`clerk_id`,`usage`,`media_id`),
|
||||||
|
KEY `idx_clerk_usage_state` (`clerk_id`,`usage`,`review_state`,`deleted`),
|
||||||
|
KEY `idx_clerk_media_asset_media` (`media_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
@@ -43,8 +43,8 @@ class PlayClerkUserInfoApiTest extends AbstractApiTest {
|
|||||||
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
private final List<String> clerkIdsToCleanup = new ArrayList<>();
|
||||||
private int scenarioSequence = 0;
|
private int scenarioSequence = 0;
|
||||||
private static final Comparator<ClerkScenario> BACKEND_ORDERING = Comparator
|
private static final Comparator<ClerkScenario> BACKEND_ORDERING = Comparator
|
||||||
.comparing(ClerkScenario::isOnline).reversed()
|
.comparing(ClerkScenario::isOnline, Comparator.reverseOrder())
|
||||||
.thenComparing(ClerkScenario::isPinned).reversed()
|
.thenComparing(ClerkScenario::isPinned, Comparator.reverseOrder())
|
||||||
.thenComparingLong(ClerkScenario::getLevelOrder)
|
.thenComparingLong(ClerkScenario::getLevelOrder)
|
||||||
.thenComparingInt(ClerkScenario::getSequence)
|
.thenComparingInt(ClerkScenario::getSequence)
|
||||||
.thenComparing(ClerkScenario::getId);
|
.thenComparing(ClerkScenario::getId);
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.clerk.enums.ClerkMediaUsage;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkDataReviewInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkMediaAssetEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.vo.PlayClerkDataReviewStateEditVo;
|
||||||
|
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.service.IPlayMediaService;
|
||||||
|
import com.starry.common.enums.ClerkReviewState;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PlayClerkDataReviewInfoServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
private PlayClerkDataReviewInfoServiceImpl service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = spy(new PlayClerkDataReviewInfoServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "playClerkUserInfoService", clerkUserInfoService);
|
||||||
|
ReflectionTestUtils.setField(service, "clerkMediaAssetService", clerkMediaAssetService);
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
doReturn(true).when(service).update(any(PlayClerkDataReviewInfoEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateShouldSynchronizeAlbumOnApproval() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-1", "clerk-1", "2",
|
||||||
|
Arrays.asList("media-existing", "media-new"));
|
||||||
|
doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-1");
|
||||||
|
|
||||||
|
PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity();
|
||||||
|
clerk.setId("clerk-1");
|
||||||
|
clerk.setTenantId("tenant-1");
|
||||||
|
when(clerkUserInfoService.getById("clerk-1")).thenReturn(clerk);
|
||||||
|
|
||||||
|
PlayMediaEntity existingMedia = new PlayMediaEntity();
|
||||||
|
existingMedia.setId("media-existing");
|
||||||
|
when(mediaService.getById("media-existing")).thenReturn(existingMedia);
|
||||||
|
PlayMediaEntity newMedia = new PlayMediaEntity();
|
||||||
|
newMedia.setId("media-new");
|
||||||
|
when(mediaService.getById("media-new")).thenReturn(newMedia);
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity assetStub = new PlayClerkMediaAssetEntity();
|
||||||
|
assetStub.setId("asset-1");
|
||||||
|
when(clerkMediaAssetService.linkDraftAsset(any(), any(), any(), any())).thenReturn(assetStub);
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-1");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
vo.setReviewCon("ok");
|
||||||
|
|
||||||
|
service.updateDataReviewState(vo);
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(eq("tenant-1"), eq("clerk-1"), eq("media-existing"),
|
||||||
|
eq(ClerkMediaUsage.PROFILE));
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(eq("tenant-1"), eq("clerk-1"), eq("media-new"), eq(ClerkMediaUsage.PROFILE));
|
||||||
|
verify(clerkMediaAssetService).applyReviewDecision(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Arrays.asList("media-existing", "media-new")), eq("review-1"), eq("ok"));
|
||||||
|
verify(clerkUserInfoService).update(any(PlayClerkUserInfoEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateDataReviewStateThrowsWhenClerkMissing() {
|
||||||
|
PlayClerkDataReviewInfoEntity review = buildReview("review-2", "ghost", "2",
|
||||||
|
Collections.singletonList("media"));
|
||||||
|
doReturn(review).when(service).selectPlayClerkDataReviewInfoById("review-2");
|
||||||
|
when(clerkUserInfoService.getById("ghost")).thenReturn(null);
|
||||||
|
|
||||||
|
PlayClerkDataReviewStateEditVo vo = new PlayClerkDataReviewStateEditVo();
|
||||||
|
vo.setId("review-2");
|
||||||
|
vo.setReviewState(ClerkReviewState.APPROVED);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.updateDataReviewState(vo))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessageContaining("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkDataReviewInfoEntity buildReview(String id, String clerkId, String dataType,
|
||||||
|
java.util.List<String> payload) {
|
||||||
|
PlayClerkDataReviewInfoEntity entity = new PlayClerkDataReviewInfoEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setClerkId(clerkId);
|
||||||
|
entity.setDataType(dataType);
|
||||||
|
entity.setDataContent(payload);
|
||||||
|
entity.setAddTime(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
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.media.entity.PlayMediaEntity;
|
||||||
|
import com.starry.admin.modules.media.service.IPlayMediaService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PlayClerkMediaAssetServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submitWithOrderShouldReindexAndFlagPending() {
|
||||||
|
PlayClerkMediaAssetServiceImpl service = org.mockito.Mockito.spy(new PlayClerkMediaAssetServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = Arrays.asList(
|
||||||
|
buildAsset("A", "media-a", 0),
|
||||||
|
buildAsset("B", "media-b", 1),
|
||||||
|
buildAsset("C", "media-c", 2));
|
||||||
|
|
||||||
|
doReturn(assets).when(service).listActiveByUsage("clerk-1", ClerkMediaUsage.PROFILE);
|
||||||
|
|
||||||
|
java.util.List<java.util.List<PlayClerkMediaAssetEntity>> batches = new java.util.ArrayList<>();
|
||||||
|
org.mockito.Mockito.doAnswer(inv -> {
|
||||||
|
java.util.Collection<PlayClerkMediaAssetEntity> collection = inv.getArgument(0);
|
||||||
|
batches.add(new java.util.ArrayList<>(collection));
|
||||||
|
return true;
|
||||||
|
}).when(service).updateBatchById(any());
|
||||||
|
|
||||||
|
service.submitWithOrder("clerk-1", ClerkMediaUsage.PROFILE, Arrays.asList("media-b", "media-a"));
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = flatten(batches);
|
||||||
|
PlayClerkMediaAssetEntity b = find(updates, "B");
|
||||||
|
PlayClerkMediaAssetEntity a = find(updates, "A");
|
||||||
|
PlayClerkMediaAssetEntity c = find(updates, "C");
|
||||||
|
|
||||||
|
assertThat(b.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(b.getReviewState()).isEqualTo(ClerkMediaReviewState.PENDING.getCode());
|
||||||
|
assertThat(a.getOrderIndex()).isEqualTo(1);
|
||||||
|
assertThat(a.getReviewState()).isEqualTo(ClerkMediaReviewState.PENDING.getCode());
|
||||||
|
assertThat(c.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void applyReviewDecisionShouldApproveAndReject() {
|
||||||
|
PlayClerkMediaAssetServiceImpl service = org.mockito.Mockito.spy(new PlayClerkMediaAssetServiceImpl());
|
||||||
|
ReflectionTestUtils.setField(service, "mediaService", mediaService);
|
||||||
|
List<PlayClerkMediaAssetEntity> assets = Arrays.asList(
|
||||||
|
buildAsset("A", "media-1", 0),
|
||||||
|
buildAsset("B", "media-2", 1));
|
||||||
|
|
||||||
|
doReturn(assets).when(service).listActiveByUsage("clerk-1", ClerkMediaUsage.PROFILE);
|
||||||
|
when(mediaService.listByIds(any())).thenReturn(Arrays.asList(
|
||||||
|
buildMedia("media-1"),
|
||||||
|
buildMedia("media-2")));
|
||||||
|
|
||||||
|
java.util.List<java.util.List<PlayClerkMediaAssetEntity>> batches = new java.util.ArrayList<>();
|
||||||
|
org.mockito.Mockito.doAnswer(inv -> {
|
||||||
|
java.util.Collection<PlayClerkMediaAssetEntity> collection = inv.getArgument(0);
|
||||||
|
batches.add(new java.util.ArrayList<>(collection));
|
||||||
|
return true;
|
||||||
|
}).when(service).updateBatchById(any());
|
||||||
|
|
||||||
|
service.applyReviewDecision("clerk-1", ClerkMediaUsage.PROFILE, Arrays.asList("media-2"), "review-1", "ok");
|
||||||
|
|
||||||
|
List<PlayClerkMediaAssetEntity> updates = flatten(batches);
|
||||||
|
PlayClerkMediaAssetEntity approved = find(updates, "B");
|
||||||
|
PlayClerkMediaAssetEntity rejected = find(updates, "A");
|
||||||
|
|
||||||
|
assertThat(approved.getReviewState()).isEqualTo(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
assertThat(approved.getOrderIndex()).isEqualTo(0);
|
||||||
|
assertThat(approved.getReviewRecordId()).isEqualTo("review-1");
|
||||||
|
assertThat(rejected.getReviewState()).isEqualTo(ClerkMediaReviewState.REJECTED.getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayClerkMediaAssetEntity buildAsset(String id, String mediaId, int order) {
|
||||||
|
PlayClerkMediaAssetEntity entity = new PlayClerkMediaAssetEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setClerkId("clerk-1");
|
||||||
|
entity.setTenantId("tenant-1");
|
||||||
|
entity.setMediaId(mediaId);
|
||||||
|
entity.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
entity.setReviewState(ClerkMediaReviewState.DRAFT.getCode());
|
||||||
|
entity.setOrderIndex(order);
|
||||||
|
entity.setSubmittedTime(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayMediaEntity buildMedia(String id) {
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId(id);
|
||||||
|
media.setUrl("url-" + id);
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayClerkMediaAssetEntity find(List<PlayClerkMediaAssetEntity> updates, String id) {
|
||||||
|
return updates.stream()
|
||||||
|
.filter(asset -> id.equals(asset.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<PlayClerkMediaAssetEntity> flatten(List<? extends List<PlayClerkMediaAssetEntity>> captured) {
|
||||||
|
return captured.stream()
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.starry.admin.modules.clerk.service.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlayClerkUserInfoServiceImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergeLegacyAlbumAppendsUniqueUrls() {
|
||||||
|
List<String> legacy = Arrays.asList("https://oss/1.png", " ", "https://oss/2.png", "https://oss/1.png");
|
||||||
|
List<MediaVo> destination = new ArrayList<>();
|
||||||
|
MediaVo existing = new MediaVo();
|
||||||
|
existing.setUrl("https://oss/2.png");
|
||||||
|
destination.add(existing);
|
||||||
|
|
||||||
|
List<MediaVo> merged = PlayClerkUserInfoServiceImpl.mergeLegacyAlbum(legacy, destination);
|
||||||
|
|
||||||
|
assertThat(merged).hasSize(2);
|
||||||
|
assertThat(merged.stream().map(MediaVo::getUrl))
|
||||||
|
.containsExactlyInAnyOrder("https://oss/2.png", "https://oss/1.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package com.starry.admin.modules.weichat.controller;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||||
|
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.IPlayClerkMediaAssetService;
|
||||||
|
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.clerk.MediaOrderRequest;
|
||||||
|
import com.starry.admin.modules.weichat.entity.clerk.MediaVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.MediaUploadService;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class WxClerkMediaControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MediaUploadService mediaUploadService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
@InjectMocks
|
||||||
|
private WxClerkMediaController controller;
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity clerk;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
|
clerk = new PlayClerkUserInfoEntity();
|
||||||
|
clerk.setId("clerk-1");
|
||||||
|
clerk.setTenantId("tenant-1");
|
||||||
|
ThreadLocalRequestDetail.setRequestDetail(clerk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ThreadLocalRequestDetail.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void uploadShouldReturnMediaVo() throws Exception {
|
||||||
|
MediaVo vo = new MediaVo();
|
||||||
|
vo.setId("media-1");
|
||||||
|
vo.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
when(mediaUploadService.upload(any(), eq(clerk), eq(ClerkMediaUsage.PROFILE))).thenReturn(vo);
|
||||||
|
|
||||||
|
MockMultipartFile file = new MockMultipartFile("file", "avatar.png", "image/png", new byte[] {1, 2});
|
||||||
|
|
||||||
|
mockMvc.perform(multipart("/wx/clerk/media/upload")
|
||||||
|
.file(file)
|
||||||
|
.param("usage", ClerkMediaUsage.PROFILE.getCode()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.id").value("media-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateOrderShouldDelegateToService() throws Exception {
|
||||||
|
MediaOrderRequest request = new MediaOrderRequest();
|
||||||
|
request.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
request.setMediaIds(Arrays.asList("m1", "m2"));
|
||||||
|
|
||||||
|
mockMvc.perform(put("/wx/clerk/media/order")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(request)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).submitWithOrder(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Arrays.asList("m1", "m2")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listApprovedShouldReturnAssemblerOutput() throws Exception {
|
||||||
|
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
|
||||||
|
asset.setId("asset-1");
|
||||||
|
asset.setClerkId("clerk-1");
|
||||||
|
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
asset.setMediaId("media-1");
|
||||||
|
asset.setReviewState(ClerkMediaReviewState.APPROVED.getCode());
|
||||||
|
asset.setCreatedTime(java.sql.Timestamp.valueOf(LocalDateTime.now()));
|
||||||
|
when(clerkMediaAssetService.listByState(eq("clerk-1"), eq(ClerkMediaUsage.PROFILE),
|
||||||
|
eq(Collections.singletonList(ClerkMediaReviewState.APPROVED)))).thenReturn(
|
||||||
|
Collections.singletonList(asset));
|
||||||
|
|
||||||
|
PlayMediaEntity media = new PlayMediaEntity();
|
||||||
|
media.setId("media-1");
|
||||||
|
media.setUrl("https://oss/mock.png");
|
||||||
|
when(mediaService.listByIds(Collections.singletonList("media-1")))
|
||||||
|
.thenReturn(Collections.singletonList(media));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/media/approved"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data", hasSize(1)))
|
||||||
|
.andExpect(jsonPath("$.data[0].url").value("https://oss/mock.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteShouldInvokeServices() throws Exception {
|
||||||
|
mockMvc.perform(delete("/wx/clerk/media/{id}", "media-77"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).softDelete("clerk-1", "media-77");
|
||||||
|
verify(mediaService).softDelete(MediaOwnerType.CLERK, "clerk-1", "media-77");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.starry.admin.modules.weichat.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.common.oss.service.IOssFileService;
|
||||||
|
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.IPlayClerkMediaAssetService;
|
||||||
|
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.clerk.MediaVo;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class MediaUploadServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private IOssFileService ossFileService;
|
||||||
|
@Mock
|
||||||
|
private IPlayMediaService mediaService;
|
||||||
|
@Mock
|
||||||
|
private IPlayClerkMediaAssetService clerkMediaAssetService;
|
||||||
|
|
||||||
|
private MediaUploadService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
service = new MediaUploadService(ossFileService, mediaService, clerkMediaAssetService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("上传图片时应创建媒资与草稿资产,并返回完整的 MediaVo")
|
||||||
|
void uploadImageCreatesMediaAndAsset() throws Exception {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile file = buildImageFile("avatar.png", "image/png");
|
||||||
|
|
||||||
|
when(mediaService.normalizeAndSave(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
when(mediaService.updateById(any())).thenReturn(true);
|
||||||
|
when(ossFileService.upload(any(), eq(clerk.getTenantId()), any()))
|
||||||
|
.thenReturn("https://oss.mock/avatar.png");
|
||||||
|
|
||||||
|
PlayClerkMediaAssetEntity asset = new PlayClerkMediaAssetEntity();
|
||||||
|
asset.setId("asset-1");
|
||||||
|
asset.setUsage(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
asset.setReviewState("draft");
|
||||||
|
when(clerkMediaAssetService.linkDraftAsset(any(), any(), any(), any())).thenAnswer(invocation -> {
|
||||||
|
asset.setMediaId(invocation.getArgument(2));
|
||||||
|
return asset;
|
||||||
|
});
|
||||||
|
|
||||||
|
MediaVo result = service.upload(file, clerk, null);
|
||||||
|
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.getUrl()).isEqualTo("https://oss.mock/avatar.png");
|
||||||
|
assertThat(result.getUsage()).isEqualTo(ClerkMediaUsage.PROFILE.getCode());
|
||||||
|
assertThat(result.getAssetId()).isEqualTo("asset-1");
|
||||||
|
|
||||||
|
ArgumentCaptor<PlayMediaEntity> mediaCaptor = ArgumentCaptor.forClass(PlayMediaEntity.class);
|
||||||
|
verify(mediaService).normalizeAndSave(mediaCaptor.capture());
|
||||||
|
PlayMediaEntity persisted = mediaCaptor.getValue();
|
||||||
|
assertThat(persisted.getOwnerType()).isEqualTo(MediaOwnerType.CLERK);
|
||||||
|
assertThat(persisted.getOwnerId()).isEqualTo(clerk.getId());
|
||||||
|
assertThat(persisted.getMetadata()).containsKeys("originalFilename", "contentType", "uploadTraceId");
|
||||||
|
|
||||||
|
verify(clerkMediaAssetService).linkDraftAsset(
|
||||||
|
eq(clerk.getTenantId()),
|
||||||
|
eq(clerk.getId()),
|
||||||
|
eq(persisted.getId()),
|
||||||
|
eq(ClerkMediaUsage.PROFILE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("不支援的檔案格式應回報錯誤,且不呼叫外部服務")
|
||||||
|
void uploadRejectsUnsupportedFormat() {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile invalidFile = new MockMultipartFile("file", "note.txt", "text/plain", "oops".getBytes());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.upload(invalidFile, clerk, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("不支持的文件格式");
|
||||||
|
|
||||||
|
verify(mediaService, never()).normalizeAndSave(any());
|
||||||
|
verify(ossFileService, never()).upload(any(), any(), any());
|
||||||
|
verify(clerkMediaAssetService, never()).linkDraftAsset(any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("缺少檔案或店員資訊時應回報錯誤")
|
||||||
|
void uploadRejectsMissingInputs() {
|
||||||
|
PlayClerkUserInfoEntity clerk = buildClerk();
|
||||||
|
MultipartFile emptyFile = new MockMultipartFile("file", new byte[0]);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.upload(emptyFile, clerk, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("请选择要上传的文件");
|
||||||
|
|
||||||
|
MultipartFile file = new MockMultipartFile("file", "a.png", "image/png", new byte[] {1});
|
||||||
|
assertThatThrownBy(() -> service.upload(file, null, ClerkMediaUsage.PROFILE))
|
||||||
|
.isInstanceOf(CustomException.class)
|
||||||
|
.hasMessage("店员信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
private MultipartFile buildImageFile(String filename, String contentType) throws IOException {
|
||||||
|
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
|
||||||
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
ImageIO.write(image, "png", baos);
|
||||||
|
return new MockMultipartFile("file", filename, contentType, baos.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkUserInfoEntity buildClerk() {
|
||||||
|
PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
|
||||||
|
entity.setId("clerk-1");
|
||||||
|
entity.setTenantId("tenant-1");
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user