wip: media migration progress

This commit is contained in:
irving
2025-11-16 11:33:58 -05:00
parent 69909a3b83
commit 8558d203af
37 changed files with 2314 additions and 11 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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> {
}

View File

@@ -1,7 +1,10 @@
package com.starry.admin.modules.clerk.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.github.yulichang.base.MPJBaseMapper;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import java.util.List;
import org.apache.ibatis.annotations.Select;
/**
* 店员Mapper接口
@@ -11,4 +14,7 @@ import com.starry.admin.modules.clerk.module.entity.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();
}

View File

@@ -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;
}

View File

@@ -3,6 +3,7 @@ package com.starry.admin.modules.clerk.module.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
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.ApiModelProperty;
import java.math.BigDecimal;
@@ -94,6 +95,12 @@ public class PlayClerkUserReturnVo {
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
private List<String> album = new ArrayList<>();
/**
* 媒资列表
*/
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
private List<MediaVo> mediaList = new ArrayList<>();
/**
* 个性签名
*/

View File

@@ -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);
}

View File

@@ -252,5 +252,12 @@ public interface IPlayClerkUserInfoService extends IService<PlayClerkUserInfoEnt
List<PlayClerkUserInfoEntity> simpleList();
/**
* 查询存在相册字段数据的店员(忽略租户隔离)
*
* @return 店员集合
*/
List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant();
JSONObject getPcData(PlayClerkUserInfoEntity entity);
}

View File

@@ -1,5 +1,6 @@
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.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.github.yulichang.wrapper.MPJLambdaWrapper;
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.module.entity.PlayClerkDataReviewInfoEntity;
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.PlayClerkDataReviewStateEditVo;
import com.starry.admin.modules.clerk.service.IPlayClerkDataReviewInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkMediaAssetService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.media.entity.PlayMediaEntity;
import com.starry.admin.modules.media.enums.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.utils.IdUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -42,6 +55,12 @@ public class PlayClerkDataReviewInfoServiceImpl
@Resource
private IPlayClerkUserInfoService playClerkUserInfoService;
@Resource
private IPlayClerkMediaAssetService clerkMediaAssetService;
@Resource
private IPlayMediaService mediaService;
/**
* 查询店员资料审核
*
@@ -147,7 +166,8 @@ public class PlayClerkDataReviewInfoServiceImpl
userInfo.setAvatar(entity.getDataContent().get(0));
}
if ("2".equals(entity.getDataType())) {
userInfo.setAlbum(entity.getDataContent());
userInfo.setAlbum(new ArrayList<>());
synchronizeApprovedAlbumMedia(entity);
}
if ("3".equals(entity.getDataType())) {
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;
}
/**
* 修改店员资料审核
*

View File

@@ -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;
}
}

View File

@@ -1,5 +1,6 @@
package com.starry.admin.modules.clerk.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
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.domain.LoginUser;
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.module.entity.PlayClerkCommodityEntity;
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.PlayClerkMediaAssetEntity;
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.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.IPlayClerkDataReviewInfoService;
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.custom.entity.PlayCustomFollowInfoEntity;
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.service.IPlayOrderInfoService;
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.statistics.module.vo.PlayClerkPerformanceInfoQueryVo;
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.clerk.MediaVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoQueryVo;
import com.starry.admin.modules.weichat.entity.clerk.PlayClerkUserInfoResultVo;
import com.starry.admin.utils.SecurityUtils;
@@ -53,6 +63,7 @@ import com.starry.common.utils.StringUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -86,6 +97,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
@Resource
private IPlayCustomFollowInfoService customFollowInfoService;
@Resource
private IPlayClerkMediaAssetService clerkMediaAssetService;
@Resource
private IPlayMediaService mediaService;
@Resource
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@Resource
private IPlayOrderInfoService playOrderInfoService;
@@ -220,6 +235,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
result.setPcData(this.getPcData(userInfo));
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;
}
@@ -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(PlayClerkLevelInfoEntity::getOrderNumber)
.orderByAsc(PlayClerkUserInfoEntity::getCreatedTime)
.orderByAsc(PlayClerkUserInfoEntity::getNickname)
.orderByAsc(PlayClerkUserInfoEntity::getId);
IPage<PlayClerkUserInfoResultVo> rawPage = this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
if (rawPage != null && rawPage.getRecords() != null) {
IPage<PlayClerkUserInfoResultVo> pageResult = this.baseMapper.selectJoinPage(page,
PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
if (pageResult != null && pageResult.getRecords() != null) {
List<PlayClerkUserInfoResultVo> deduped = new ArrayList<>();
Set<String> seen = new HashSet<>();
for (PlayClerkUserInfoResultVo record : rawPage.getRecords()) {
for (PlayClerkUserInfoResultVo record : pageResult.getRecords()) {
String id = record.getId();
if (id == null || !seen.add(id)) {
continue;
}
deduped.add(record);
}
rawPage.setRecords(deduped);
pageResult.setRecords(deduped);
}
return rawPage;
if (pageResult != null) {
attachMediaToResultVos(pageResult.getRecords(), false);
}
return pageResult;
}
@Override
@@ -500,6 +523,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
record.setOrderContinueNumber(String.valueOf(orderContinueNumber));
}
attachMediaToAdminVos(page.getRecords());
return page;
}
@@ -591,6 +615,11 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
return this.baseMapper.selectList(lambdaQueryWrapper);
}
@Override
public List<PlayClerkUserInfoEntity> listWithAlbumIgnoringTenant() {
return playClerkUserInfoMapper.selectAllWithAlbumIgnoringTenant();
}
@Override
public JSONObject getPcData(PlayClerkUserInfoEntity entity) {
JSONObject data = new JSONObject();
@@ -628,4 +657,97 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
package com.starry.admin.modules.media.enums;
public final class MediaOwnerType {
private MediaOwnerType() {
}
public static final String CLERK = "clerk";
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -41,6 +41,7 @@ import com.starry.admin.utils.SecurityUtils;
import com.starry.admin.utils.SmsUtils;
import com.starry.common.redis.RedisCache;
import com.starry.common.result.R;
import com.starry.common.result.TypedR;
import com.starry.common.utils.ConvertUtil;
import com.starry.common.utils.StringUtils;
import com.starry.common.utils.VerificationCodeUtils;
@@ -394,10 +395,10 @@ public class WxClerkController {
* @return 店员列表
*/
@PostMapping("/user/queryByPage")
public R queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
public TypedR<IPage<PlayClerkUserInfoResultVo>> queryByPage(@RequestBody PlayClerkUserInfoQueryVo vo) {
IPage<PlayClerkUserInfoResultVo> page = playClerkUserInfoService.selectByPage(vo,
customUserService.getLoginUserId());
return R.ok(page);
return TypedR.ok(page);
}
/**

View File

@@ -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));
}
}

View File

@@ -3,6 +3,7 @@ package com.starry.admin.modules.weichat.entity;
import com.alibaba.fastjson2.JSONObject;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
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.List;
import lombok.Data;
@@ -45,6 +46,11 @@ public class PlayClerkUserLoginResponseVo {
*/
private List<String> album = new ArrayList<>();
/**
* 新媒资列表
*/
private List<MediaVo> mediaList = new ArrayList<>();
/**
* 相册是否运行编辑
*/

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -75,6 +75,12 @@ public class PlayClerkUserInfoResultVo {
@ApiModelProperty(value = "相册列表", notes = "店员相册图片URL列表")
private List<String> album = new ArrayList<>();
/**
* 媒资列表
*/
@ApiModelProperty(value = "媒资列表", notes = "结构化媒资数据")
private List<MediaVo> mediaList = new ArrayList<>();
/**
* 个性签名
*/

View File

@@ -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();
}
}