|
|
@@ -0,0 +1,301 @@
|
|
|
+package com.tryon.service.impl;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
+import com.tryon.entity.TryOnAvatar;
|
|
|
+import com.tryon.entity.VirtualModel;
|
|
|
+import com.tryon.mapper.TryOnAvatarMapper;
|
|
|
+import com.tryon.mapper.VirtualModelMapper;
|
|
|
+import com.tryon.service.AvatarService;
|
|
|
+import com.tryon.vo.AvatarVO;
|
|
|
+import com.tryon.vo.ModelVO;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 形象管理服务实现类
|
|
|
+ * 实现格式大小校验、落库、模特列表查询、形象绑定等逻辑
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class AvatarServiceImpl implements AvatarService {
|
|
|
+
|
|
|
+ /** 允许的图片格式 */
|
|
|
+ private static final List<String> ALLOWED_MIME_TYPES = new ArrayList<>();
|
|
|
+ /** 允许的扩展名 */
|
|
|
+ private static final List<String> ALLOWED_EXTENSIONS = new ArrayList<>();
|
|
|
+ /** 最大文件大小 10MB */
|
|
|
+ private static final long MAX_FILE_SIZE = 10L * 1024 * 1024;
|
|
|
+ /** 签名有效期(秒) */
|
|
|
+ private static final long SIGN_EXPIRE_SECONDS = 300L;
|
|
|
+
|
|
|
+ static {
|
|
|
+ ALLOWED_MIME_TYPES.add("image/jpeg");
|
|
|
+ ALLOWED_MIME_TYPES.add("image/jpg");
|
|
|
+ ALLOWED_MIME_TYPES.add("image/png");
|
|
|
+ ALLOWED_EXTENSIONS.add("jpg");
|
|
|
+ ALLOWED_EXTENSIONS.add("jpeg");
|
|
|
+ ALLOWED_EXTENSIONS.add("png");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TryOnAvatarMapper tryOnAvatarMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private VirtualModelMapper virtualModelMapper;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取上传签名:校验格式与大小后下发直传签名
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public AvatarVO getUploadSign(String fileName, Long fileSize, String mimeType) {
|
|
|
+ // 校验文件名
|
|
|
+ if (fileName == null || fileName.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("文件名不能为空");
|
|
|
+ }
|
|
|
+ // 校验格式
|
|
|
+ String ext = extractExtension(fileName);
|
|
|
+ if (!ALLOWED_EXTENSIONS.contains(ext)) {
|
|
|
+ throw new IllegalArgumentException("仅支持 JPG/PNG 格式的图片");
|
|
|
+ }
|
|
|
+ if (mimeType != null && !mimeType.trim().isEmpty()
|
|
|
+ && !ALLOWED_MIME_TYPES.contains(mimeType.toLowerCase())) {
|
|
|
+ throw new IllegalArgumentException("仅支持 JPG/PNG 格式的图片");
|
|
|
+ }
|
|
|
+ // 校验大小
|
|
|
+ if (fileSize == null || fileSize <= 0) {
|
|
|
+ throw new IllegalArgumentException("文件大小不合法");
|
|
|
+ }
|
|
|
+ if (fileSize > MAX_FILE_SIZE) {
|
|
|
+ throw new IllegalArgumentException("图片大小不能超过 10MB");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成对象存储 key 与签名 URL(此处为模拟实现,实际对接 OSS/COS)
|
|
|
+ String objectKey = "avatar/" + UUID.randomUUID().toString().replace("-", "") + "." + ext;
|
|
|
+ long expireAt = System.currentTimeMillis() / 1000 + SIGN_EXPIRE_SECONDS;
|
|
|
+ String uploadUrl = "https://oss.example.com/upload?key=" + objectKey
|
|
|
+ + "&sign=" + UUID.randomUUID().toString().replace("-", "")
|
|
|
+ + "&expire=" + expireAt;
|
|
|
+
|
|
|
+ AvatarVO vo = new AvatarVO();
|
|
|
+ vo.setUploadUrl(uploadUrl);
|
|
|
+ vo.setObjectKey(objectKey);
|
|
|
+ vo.setExpireAt(expireAt);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传完成回调:落库并触发异步质检
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public AvatarVO confirmUpload(String objectKey, String previewUrl) {
|
|
|
+ if (objectKey == null || objectKey.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("objectKey 不能为空");
|
|
|
+ }
|
|
|
+ Long userId = 0L;
|
|
|
+
|
|
|
+ TryOnAvatar avatar = new TryOnAvatar();
|
|
|
+ avatar.setUserId(userId);
|
|
|
+ avatar.setType(1); // 1=上传照片
|
|
|
+ avatar.setObjectKey(objectKey);
|
|
|
+ avatar.setPreviewUrl(previewUrl != null
|
|
|
+ ? previewUrl
|
|
|
+ : "https://cdn.example.com/" + objectKey);
|
|
|
+ avatar.setStatus(0); // 0=待质检
|
|
|
+ avatar.setIsCurrent(0);
|
|
|
+ avatar.setCreatedAt(LocalDateTime.now());
|
|
|
+ tryOnAvatarMapper.insert(avatar);
|
|
|
+
|
|
|
+ // 异步质检(此处简化为同步标记,实际应投递消息队列)
|
|
|
+ // 质检通过后回写 status=1
|
|
|
+
|
|
|
+ AvatarVO vo = new AvatarVO();
|
|
|
+ vo.setAvatarId(avatar.getId());
|
|
|
+ vo.setType(avatar.getType());
|
|
|
+ vo.setPreviewUrl(avatar.getPreviewUrl());
|
|
|
+ vo.setStatus(avatar.getStatus());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询质检结果
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public AvatarVO getStatus(Long avatarId) {
|
|
|
+ if (avatarId == null) {
|
|
|
+ throw new IllegalArgumentException("avatarId 不能为空");
|
|
|
+ }
|
|
|
+ TryOnAvatar avatar = tryOnAvatarMapper.selectById(avatarId);
|
|
|
+ if (avatar == null) {
|
|
|
+ throw new IllegalArgumentException("形象不存在");
|
|
|
+ }
|
|
|
+ AvatarVO vo = new AvatarVO();
|
|
|
+ vo.setAvatarId(avatar.getId());
|
|
|
+ vo.setType(avatar.getType());
|
|
|
+ vo.setPreviewUrl(avatar.getPreviewUrl());
|
|
|
+ vo.setStatus(avatar.getStatus());
|
|
|
+ vo.setReason(avatar.getRejectReason());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取虚拟模特列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<ModelVO> listModels(Integer gender, String bodyType) {
|
|
|
+ LambdaQueryWrapper<VirtualModel> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(VirtualModel::getEnabled, 1);
|
|
|
+ if (gender != null) {
|
|
|
+ wrapper.eq(VirtualModel::getGender, gender);
|
|
|
+ }
|
|
|
+ if (bodyType != null && !bodyType.trim().isEmpty()) {
|
|
|
+ wrapper.eq(VirtualModel::getBodyType, bodyType);
|
|
|
+ }
|
|
|
+ wrapper.orderByAsc(VirtualModel::getSortOrder);
|
|
|
+ List<VirtualModel> models = virtualModelMapper.selectList(wrapper);
|
|
|
+
|
|
|
+ List<ModelVO> result = new ArrayList<>();
|
|
|
+ for (VirtualModel model : models) {
|
|
|
+ ModelVO vo = new ModelVO();
|
|
|
+ vo.setModelId(model.getModelId());
|
|
|
+ vo.setName(model.getName());
|
|
|
+ vo.setGender(model.getGender());
|
|
|
+ vo.setBodyType(model.getBodyType());
|
|
|
+ vo.setThumbUrl(model.getThumbUrl());
|
|
|
+ vo.setPreviewUrl(model.getPreviewUrl());
|
|
|
+ result.add(vo);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确认使用形象:绑定当前用户试衣形象
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public AvatarVO confirmUse(Long avatarId, String type, Integer status) {
|
|
|
+ Long userId = 0L;
|
|
|
+ Integer typeValue = parseType(type);
|
|
|
+ if (typeValue == null) {
|
|
|
+ throw new IllegalArgumentException("type 不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除该用户原有当前形象标记
|
|
|
+ LambdaUpdateWrapper<TryOnAvatar> clearWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ clearWrapper.eq(TryOnAvatar::getUserId, userId)
|
|
|
+ .eq(TryOnAvatar::getIsCurrent, 1)
|
|
|
+ .set(TryOnAvatar::getIsCurrent, 0);
|
|
|
+ tryOnAvatarMapper.update(null, clearWrapper);
|
|
|
+
|
|
|
+ TryOnAvatar avatar;
|
|
|
+ if (typeValue == 1) {
|
|
|
+ // 上传照片:使用已有 avatarId
|
|
|
+ if (avatarId == null) {
|
|
|
+ throw new IllegalArgumentException("avatarId 不能为空");
|
|
|
+ }
|
|
|
+ avatar = tryOnAvatarMapper.selectById(avatarId);
|
|
|
+ if (avatar == null) {
|
|
|
+ throw new IllegalArgumentException("形象不存在");
|
|
|
+ }
|
|
|
+ if (avatar.getStatus() != null && avatar.getStatus() == 2) {
|
|
|
+ throw new IllegalArgumentException("该照片质检不合格,请重新上传");
|
|
|
+ }
|
|
|
+ } else if (typeValue == 2) {
|
|
|
+ // 虚拟模特:根据 avatarId 作为 modelId 创建或复用形象记录
|
|
|
+ if (avatarId == null) {
|
|
|
+ throw new IllegalArgumentException("modelId 不能为空");
|
|
|
+ }
|
|
|
+ String modelId = String.valueOf(avatarId);
|
|
|
+ VirtualModel model = virtualModelMapper.selectById(modelId);
|
|
|
+ if (model == null || model.getEnabled() == null || model.getEnabled() != 1) {
|
|
|
+ throw new IllegalArgumentException("虚拟模特不存在或已下架");
|
|
|
+ }
|
|
|
+ // 查询是否已存在该用户该模特的形象记录
|
|
|
+ LambdaQueryWrapper<TryOnAvatar> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(TryOnAvatar::getUserId, userId)
|
|
|
+ .eq(TryOnAvatar::getType, 2)
|
|
|
+ .eq(TryOnAvatar::getModelId, modelId)
|
|
|
+ .last("limit 1");
|
|
|
+ avatar = tryOnAvatarMapper.selectOne(queryWrapper);
|
|
|
+ if (avatar == null) {
|
|
|
+ avatar = new TryOnAvatar();
|
|
|
+ avatar.setUserId(userId);
|
|
|
+ avatar.setType(2);
|
|
|
+ avatar.setModelId(model.getModelId());
|
|
|
+ avatar.setPreviewUrl(model.getPreviewUrl());
|
|
|
+ avatar.setStatus(1); // 虚拟模特默认合格
|
|
|
+ avatar.setIsCurrent(1);
|
|
|
+ avatar.setCreatedAt(LocalDateTime.now());
|
|
|
+ tryOnAvatarMapper.insert(avatar);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new IllegalArgumentException("type 不合法");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标记为当前形象
|
|
|
+ avatar.setIsCurrent(1);
|
|
|
+ tryOnAvatarMapper.updateById(avatar);
|
|
|
+
|
|
|
+ AvatarVO vo = new AvatarVO();
|
|
|
+ vo.setAvatarId(avatar.getId());
|
|
|
+ vo.setType(avatar.getType());
|
|
|
+ vo.setPreviewUrl(avatar.getPreviewUrl());
|
|
|
+ vo.setStatus(avatar.getStatus());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前形象
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public AvatarVO getCurrent() {
|
|
|
+ Long uid = 0L;
|
|
|
+ LambdaQueryWrapper<TryOnAvatar> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(TryOnAvatar::getUserId, uid)
|
|
|
+ .eq(TryOnAvatar::getIsCurrent, 1)
|
|
|
+ .orderByDesc(TryOnAvatar::getCreatedAt)
|
|
|
+ .last("limit 1");
|
|
|
+ TryOnAvatar avatar = tryOnAvatarMapper.selectOne(wrapper);
|
|
|
+ if (avatar == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ AvatarVO vo = new AvatarVO();
|
|
|
+ vo.setAvatarId(avatar.getId());
|
|
|
+ vo.setType(avatar.getType());
|
|
|
+ vo.setPreviewUrl(avatar.getPreviewUrl());
|
|
|
+ vo.setStatus(avatar.getStatus());
|
|
|
+ vo.setModelId(avatar.getModelId());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 type 字符串为整数
|
|
|
+ */
|
|
|
+ private Integer parseType(String type) {
|
|
|
+ if (type == null || type.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.valueOf(type.trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ throw new IllegalArgumentException("type 不合法");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提取文件扩展名(小写)
|
|
|
+ */
|
|
|
+ private String extractExtension(String fileName) {
|
|
|
+ int idx = fileName.lastIndexOf('.');
|
|
|
+ if (idx < 0 || idx == fileName.length() - 1) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return fileName.substring(idx + 1).toLowerCase();
|
|
|
+ }
|
|
|
+}
|