|
|
@@ -1,7 +1,6 @@
|
|
|
package shop.alien.store.service.impl;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
-import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
@@ -11,9 +10,14 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
+import shop.alien.entity.store.StoreOfficialAlbum;
|
|
|
import shop.alien.entity.store.StoreVideo;
|
|
|
+import shop.alien.entity.store.dto.StoreVideoSaveDto;
|
|
|
+import shop.alien.mapper.StoreOfficialAlbumMapper;
|
|
|
import shop.alien.mapper.StoreVideoMapper;
|
|
|
import shop.alien.store.service.StoreVideoService;
|
|
|
+import shop.alien.store.util.CommonConstant;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
import shop.alien.util.ali.AliOSSUtil;
|
|
|
import shop.alien.util.common.RandomCreateUtil;
|
|
|
import shop.alien.util.common.VideoUtils;
|
|
|
@@ -42,6 +46,8 @@ public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVi
|
|
|
|
|
|
private final AliOSSUtil aliOSSUtil;
|
|
|
|
|
|
+ private final StoreOfficialAlbumMapper storeOfficialAlbumMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 视频文件类型列表
|
|
|
*/
|
|
|
@@ -51,42 +57,214 @@ public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVi
|
|
|
private String uploadDir;
|
|
|
|
|
|
/**
|
|
|
- * 保存视频,自动截取第一帧作为封面
|
|
|
+ * 保存视频(单个或批量)
|
|
|
*
|
|
|
- * @param entity 视频实体
|
|
|
+ * @param dto 视频保存DTO,包含门店ID、相册ID和视频URL列表
|
|
|
* @return 是否保存成功
|
|
|
*/
|
|
|
@Override
|
|
|
- public boolean save(StoreVideo entity) {
|
|
|
+ public boolean saveOrSaveBatch(StoreVideoSaveDto dto) {
|
|
|
// 参数验证
|
|
|
- if (entity == null) {
|
|
|
- log.error("StoreVideoServiceImpl.save ERROR: entity is null");
|
|
|
+ if (dto == null) {
|
|
|
+ log.error("StoreVideoServiceImpl.saveOrSaveBatch ERROR: dto is null");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (dto.getStoreId() == null || dto.getStoreId() <= 0) {
|
|
|
+ log.error("StoreVideoServiceImpl.saveOrSaveBatch ERROR: storeId is invalid");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (dto.getVideoUrls() == null || dto.getVideoUrls().isEmpty()) {
|
|
|
+ log.error("StoreVideoServiceImpl.saveOrSaveBatch ERROR: videoUrls is empty");
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- // 如果imgUrl不为空,尝试处理视频和封面
|
|
|
- if (StringUtils.isNotBlank(entity.getImgUrl())) {
|
|
|
- try {
|
|
|
- // 处理视频URL,截取封面并更新imgUrl字段
|
|
|
- String processedImgUrl = processVideoAndCover(entity.getImgUrl());
|
|
|
- if (StringUtils.isNotBlank(processedImgUrl)) {
|
|
|
- entity.setImgUrl(processedImgUrl);
|
|
|
+ // 解析视频URL列表,前端只传入视频URL,后端自动匹配或生成封面
|
|
|
+ // 排序号将根据传入数组的顺序设置(从1开始),用于支持编辑排序功能
|
|
|
+ List<StoreVideo> videoList = new java.util.ArrayList<>();
|
|
|
+ List<String> videoUrls = dto.getVideoUrls();
|
|
|
+ List<Integer> videoIds = dto.getVideoIds(); // 可选的视频ID列表,用于编辑
|
|
|
+
|
|
|
+ // 遍历URL列表,每个URL都是视频URL
|
|
|
+ int videoIndex = 0;
|
|
|
+ for (int i = 0; i < videoUrls.size(); i++) {
|
|
|
+ String videoUrl = videoUrls.get(i);
|
|
|
+ if (StringUtils.isBlank(videoUrl)) {
|
|
|
+ log.warn("视频URL为空,跳过该视频");
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取对应的视频ID(如果存在)
|
|
|
+ Integer videoId = null;
|
|
|
+ if (videoIds != null && i < videoIds.size()) {
|
|
|
+ videoId = videoIds.get(i);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果ID存在且有效,尝试查询现有视频进行更新
|
|
|
+ StoreVideo storeVideo = null;
|
|
|
+ if (videoId != null && videoId > 0) {
|
|
|
+ try {
|
|
|
+ storeVideo = this.getById(videoId);
|
|
|
+ if (storeVideo != null && storeVideo.getDeleteFlag() == CommonConstant.DELETE_FLAG_UNDELETE) {
|
|
|
+ log.info("找到现有视频,ID:{},将进行更新,排序号将根据数组顺序设置为:{}", videoId, videoIndex + 1);
|
|
|
+ } else {
|
|
|
+ log.warn("视频ID:{} 不存在或已删除,将作为新增处理", videoId);
|
|
|
+ storeVideo = null;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询视频失败,ID:{},错误:{},将作为新增处理", videoId, e.getMessage());
|
|
|
+ storeVideo = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果不存在现有视频,创建新对象
|
|
|
+ if (storeVideo == null) {
|
|
|
+ storeVideo = new StoreVideo();
|
|
|
+ storeVideo.setId(null); // 确保ID为null,表示新增
|
|
|
+ // 新增时,根据数组顺序设置排序号(从1开始)
|
|
|
+ storeVideo.setImgSort(videoIndex + 1);
|
|
|
+ storeVideo.setImgDescription("相册视频");
|
|
|
+ } else {
|
|
|
+ // 更新时,根据数组顺序更新排序号(从1开始)
|
|
|
+ // 这样可以根据传入的数组顺序来调整视频的排序
|
|
|
+ storeVideo.setImgSort(videoIndex + 1);
|
|
|
+ // 更新时,如果描述未设置,使用默认值
|
|
|
+ if (StringUtils.isBlank(storeVideo.getImgDescription())) {
|
|
|
+ storeVideo.setImgDescription("相册视频");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置基本信息
|
|
|
+ storeVideo.setStoreId(dto.getStoreId());
|
|
|
+ storeVideo.setBusinessId(dto.getBusinessId());
|
|
|
+
|
|
|
+ // 尝试根据视频URL自动匹配封面URL(从/file/uploadMore接口获取的封面URL格式)
|
|
|
+ // 封面URL格式:视频URL的扩展名从.mp4等替换为.jpg
|
|
|
+ String coverUrl = tryMatchCoverUrl(videoUrl);
|
|
|
+
|
|
|
+ // 如果匹配不到封面URL,尝试处理视频生成封面
|
|
|
+ if (StringUtils.isBlank(coverUrl)) {
|
|
|
+ log.info("未找到匹配的封面URL,视频URL:{},将尝试处理生成封面", videoUrl);
|
|
|
+ try {
|
|
|
+ // 调用processVideoAndCover处理,生成封面
|
|
|
+ String processedImgUrl = processVideoAndCover(videoUrl);
|
|
|
+ if (StringUtils.isNotBlank(processedImgUrl)) {
|
|
|
+ // 如果处理成功,processedImgUrl已经是JSON格式,直接使用
|
|
|
+ storeVideo.setImgUrl(processedImgUrl);
|
|
|
+ videoList.add(storeVideo);
|
|
|
+ videoIndex++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理视频封面失败,视频URL:{},错误:{},将保存视频但不包含封面", videoUrl, e.getMessage(), e);
|
|
|
+ // 处理失败时,继续执行下面的逻辑,保存视频但不包含封面
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 组合成JSON对象格式(即使没有封面也保存)
|
|
|
+ JSONObject videoJson = new JSONObject();
|
|
|
+ videoJson.put("video", videoUrl);
|
|
|
+ if (StringUtils.isNotBlank(coverUrl)) {
|
|
|
+ videoJson.put("cover", coverUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ storeVideo.setImgUrl(JSON.toJSONString(videoJson));
|
|
|
+
|
|
|
+ videoList.add(storeVideo);
|
|
|
+ videoIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量保存或更新
|
|
|
+ if (videoList.isEmpty()) {
|
|
|
+ log.warn("没有有效的视频数据需要保存");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ boolean saveResult;
|
|
|
+ // 分离新增和更新的视频
|
|
|
+ List<StoreVideo> insertList = new java.util.ArrayList<>();
|
|
|
+ List<StoreVideo> updateList = new java.util.ArrayList<>();
|
|
|
+
|
|
|
+ for (StoreVideo video : videoList) {
|
|
|
+ if (video.getId() != null && video.getId() > 0) {
|
|
|
+ updateList.add(video);
|
|
|
+ } else {
|
|
|
+ insertList.add(video);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行新增和更新
|
|
|
+ try {
|
|
|
+ if (!insertList.isEmpty()) {
|
|
|
+ if (insertList.size() == 1) {
|
|
|
+ saveResult = super.save(insertList.get(0));
|
|
|
+ } else {
|
|
|
+ saveResult = super.saveBatch(insertList);
|
|
|
+ }
|
|
|
+ log.info("新增视频数量:{}", insertList.size());
|
|
|
+ } else {
|
|
|
+ saveResult = true; // 如果没有新增,默认成功
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!updateList.isEmpty()) {
|
|
|
+ boolean updateResult;
|
|
|
+ if (updateList.size() == 1) {
|
|
|
+ updateResult = super.updateById(updateList.get(0));
|
|
|
+ } else {
|
|
|
+ updateResult = super.updateBatchById(updateList);
|
|
|
+ }
|
|
|
+ saveResult = saveResult && updateResult;
|
|
|
+ log.info("更新视频数量:{}", updateList.size());
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("保存或更新视频失败,错误:{}", e.getMessage(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存成功后,更新相册的imgCount
|
|
|
+ if (saveResult && dto.getBusinessId() != null && dto.getBusinessId() > 0) {
|
|
|
+ try {
|
|
|
+ updateAlbumImgCount(dto.getBusinessId());
|
|
|
} catch (Exception e) {
|
|
|
- log.error("StoreVideoServiceImpl.save 处理视频封面失败, imgUrl: {}", entity.getImgUrl(), e);
|
|
|
- // 如果处理失败,记录错误日志但不影响保存操作
|
|
|
+ log.error("更新相册imgCount失败,相册ID:{},错误:{}", dto.getBusinessId(), e.getMessage(), e);
|
|
|
+ // 不影响保存结果,只记录错误日志
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 调用父类的save方法保存
|
|
|
- return super.save(entity);
|
|
|
+ return saveResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新相册的imgCount(统计该相册下的视频数量)
|
|
|
+ *
|
|
|
+ * @param albumId 相册ID
|
|
|
+ */
|
|
|
+ private void updateAlbumImgCount(Integer albumId) {
|
|
|
+ try {
|
|
|
+ // 统计该相册下的视频数量
|
|
|
+ LambdaQueryWrapper<StoreVideo> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(StoreVideo::getBusinessId, albumId)
|
|
|
+ .eq(StoreVideo::getDeleteFlag, CommonConstant.DELETE_FLAG_UNDELETE);
|
|
|
+ long videoCount = this.count(queryWrapper);
|
|
|
+
|
|
|
+ // 更新相册的imgCount
|
|
|
+ LambdaUpdateWrapper<StoreOfficialAlbum> updateWrapper = new LambdaUpdateWrapper<>();
|
|
|
+ updateWrapper.eq(StoreOfficialAlbum::getId, albumId)
|
|
|
+ .set(StoreOfficialAlbum::getImgCount, (int) videoCount);
|
|
|
+ storeOfficialAlbumMapper.update(null, updateWrapper);
|
|
|
+
|
|
|
+ log.info("更新相册imgCount成功,相册ID:{},视频数量:{}", albumId, videoCount);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("更新相册imgCount失败,相册ID:{},错误:{}", albumId, e.getMessage(), e);
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 处理视频URL,截取第一帧作为封面并生成JSON数组
|
|
|
+ * 处理视频URL,截取第一帧作为封面并生成JSON对象
|
|
|
+ * 只支持JSON对象格式:{"video": "...", "cover": "..."}
|
|
|
*
|
|
|
- * @param imgUrl 视频URL或JSON字符串
|
|
|
- * @return 处理后的JSON数组字符串
|
|
|
+ * @param imgUrl 视频URL或JSON对象字符串
|
|
|
+ * @return 处理后的JSON对象字符串,格式:{"video": "...", "cover": "..."}
|
|
|
*/
|
|
|
private String processVideoAndCover(String imgUrl) {
|
|
|
log.info("StoreVideoServiceImpl.processVideoAndCover imgUrl={}", imgUrl);
|
|
|
@@ -98,20 +276,18 @@ public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVi
|
|
|
}
|
|
|
|
|
|
String videoUrl = null;
|
|
|
+ String coverUrl = null;
|
|
|
|
|
|
- // 判断imgUrl是否为JSON格式
|
|
|
+ // 判断imgUrl是否为JSON对象格式
|
|
|
try {
|
|
|
- JSONArray jsonArray = JSONArray.parseArray(imgUrl);
|
|
|
- // 如果已经是JSON数组格式,检查是否包含video和cover
|
|
|
- if (jsonArray != null && !jsonArray.isEmpty()) {
|
|
|
- JSONObject firstItem = jsonArray.getJSONObject(0);
|
|
|
- if (firstItem != null && firstItem.containsKey("video")) {
|
|
|
- videoUrl = firstItem.getString("video");
|
|
|
- // 如果已经有cover,则直接返回
|
|
|
- if (firstItem.containsKey("cover") && StringUtils.isNotBlank(firstItem.getString("cover"))) {
|
|
|
- log.info("StoreVideoServiceImpl.processVideoAndCover 已有封面,无需重新生成");
|
|
|
- return imgUrl;
|
|
|
- }
|
|
|
+ JSONObject jsonObject = JSON.parseObject(imgUrl);
|
|
|
+ if (jsonObject != null && jsonObject.containsKey("video")) {
|
|
|
+ videoUrl = jsonObject.getString("video");
|
|
|
+ coverUrl = jsonObject.getString("cover");
|
|
|
+ // 如果已经有cover,则直接返回
|
|
|
+ if (StringUtils.isNotBlank(coverUrl)) {
|
|
|
+ log.info("StoreVideoServiceImpl.processVideoAndCover 已有封面,无需重新生成");
|
|
|
+ return imgUrl;
|
|
|
}
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
@@ -159,21 +335,19 @@ public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVi
|
|
|
// 上传封面到OSS
|
|
|
String coverFileName = generateCoverFileName(videoUrl);
|
|
|
String coverOssPath = "video/" + coverFileName + ".jpg";
|
|
|
- String coverUrl = aliOSSUtil.uploadFile(coverFile, coverOssPath);
|
|
|
+ coverUrl = aliOSSUtil.uploadFile(coverFile, coverOssPath);
|
|
|
if (StringUtils.isBlank(coverUrl)) {
|
|
|
log.error("StoreVideoServiceImpl.processVideoAndCover 上传封面失败: {}", coverOssPath);
|
|
|
return imgUrl;
|
|
|
}
|
|
|
|
|
|
- // 构建JSON数组
|
|
|
- JSONArray resultArray = new JSONArray();
|
|
|
- JSONObject videoObject = new JSONObject();
|
|
|
- videoObject.put("video", videoUrl);
|
|
|
- videoObject.put("cover", coverUrl);
|
|
|
- resultArray.add(videoObject);
|
|
|
+ // 构建JSON对象(格式:{"video": "...", "cover": "..."})
|
|
|
+ JSONObject resultObject = new JSONObject();
|
|
|
+ resultObject.put("video", videoUrl);
|
|
|
+ resultObject.put("cover", coverUrl);
|
|
|
|
|
|
log.info("StoreVideoServiceImpl.processVideoAndCover 处理成功, videoUrl: {}, coverUrl: {}", videoUrl, coverUrl);
|
|
|
- return JSON.toJSONString(resultArray);
|
|
|
+ return JSON.toJSONString(resultObject);
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
log.error("StoreVideoServiceImpl.processVideoAndCover 处理异常", e);
|
|
|
@@ -387,5 +561,41 @@ public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVi
|
|
|
.orderByAsc(StoreVideo::getImgSort);
|
|
|
return this.list(lambdaQueryWrapper);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 尝试根据视频URL匹配封面URL
|
|
|
+ * 从/file/uploadMore接口获取的封面URL格式:视频URL的扩展名从.mp4等替换为.jpg
|
|
|
+ *
|
|
|
+ * @param videoUrl 视频URL
|
|
|
+ * @return 封面URL,如果匹配不到则返回null
|
|
|
+ */
|
|
|
+ private String tryMatchCoverUrl(String videoUrl) {
|
|
|
+ if (StringUtils.isBlank(videoUrl)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 检查是否为视频URL
|
|
|
+ if (!isVideoUrl(videoUrl)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取视频文件名(不含扩展名)
|
|
|
+ int lastDotIndex = videoUrl.lastIndexOf('.');
|
|
|
+ int lastSlashIndex = Math.max(videoUrl.lastIndexOf('/'), videoUrl.lastIndexOf('\\'));
|
|
|
+
|
|
|
+ if (lastDotIndex > lastSlashIndex && lastDotIndex > 0) {
|
|
|
+ // 构建封面URL:将扩展名替换为.jpg
|
|
|
+ String coverUrl = videoUrl.substring(0, lastDotIndex) + ".jpg";
|
|
|
+ log.debug("尝试匹配封面URL,视频URL:{},封面URL:{}", videoUrl, coverUrl);
|
|
|
+ return coverUrl;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("匹配封面URL失败,视频URL:{},错误:{}", videoUrl, e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|