|
|
@@ -1,15 +1,29 @@
|
|
|
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;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
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.StoreVideo;
|
|
|
import shop.alien.mapper.StoreVideoMapper;
|
|
|
import shop.alien.store.service.StoreVideoService;
|
|
|
+import shop.alien.util.ali.AliOSSUtil;
|
|
|
+import shop.alien.util.common.RandomCreateUtil;
|
|
|
+import shop.alien.util.common.VideoUtils;
|
|
|
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.net.URL;
|
|
|
+import java.net.URLConnection;
|
|
|
+import java.util.Arrays;
|
|
|
import java.util.List;
|
|
|
|
|
|
/**
|
|
|
@@ -24,6 +38,326 @@ import java.util.List;
|
|
|
@RequiredArgsConstructor
|
|
|
public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVideo> implements StoreVideoService {
|
|
|
|
|
|
+ private final VideoUtils videoUtils;
|
|
|
+
|
|
|
+ private final AliOSSUtil aliOSSUtil;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 视频文件类型列表
|
|
|
+ */
|
|
|
+ private static final List<String> VIDEO_FILE_TYPES = Arrays.asList("mp4", "avi", "flv", "mkv", "rmvb", "wmv", "3gp", "mov");
|
|
|
+
|
|
|
+ @Value("${spring.web.resources.static-locations}")
|
|
|
+ private String uploadDir;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存视频,自动截取第一帧作为封面
|
|
|
+ *
|
|
|
+ * @param entity 视频实体
|
|
|
+ * @return 是否保存成功
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public boolean save(StoreVideo entity) {
|
|
|
+ // 参数验证
|
|
|
+ if (entity == null) {
|
|
|
+ log.error("StoreVideoServiceImpl.save ERROR: entity is null");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果imgUrl不为空,尝试处理视频和封面
|
|
|
+ if (StringUtils.isNotBlank(entity.getImgUrl())) {
|
|
|
+ try {
|
|
|
+ // 处理视频URL,截取封面并更新imgUrl字段
|
|
|
+ String processedImgUrl = processVideoAndCover(entity.getImgUrl());
|
|
|
+ if (StringUtils.isNotBlank(processedImgUrl)) {
|
|
|
+ entity.setImgUrl(processedImgUrl);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("StoreVideoServiceImpl.save 处理视频封面失败, imgUrl: {}", entity.getImgUrl(), e);
|
|
|
+ // 如果处理失败,记录错误日志但不影响保存操作
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用父类的save方法保存
|
|
|
+ return super.save(entity);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理视频URL,截取第一帧作为封面并生成JSON数组
|
|
|
+ *
|
|
|
+ * @param imgUrl 视频URL或JSON字符串
|
|
|
+ * @return 处理后的JSON数组字符串
|
|
|
+ */
|
|
|
+ private String processVideoAndCover(String imgUrl) {
|
|
|
+ log.info("StoreVideoServiceImpl.processVideoAndCover imgUrl={}", imgUrl);
|
|
|
+
|
|
|
+ // 参数验证
|
|
|
+ if (StringUtils.isBlank(imgUrl)) {
|
|
|
+ log.warn("StoreVideoServiceImpl.processVideoAndCover imgUrl is blank");
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ String videoUrl = null;
|
|
|
+
|
|
|
+ // 判断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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 如果不是JSON格式,则将imgUrl视为视频URL
|
|
|
+ log.debug("StoreVideoServiceImpl.processVideoAndCover imgUrl不是JSON格式,视为视频URL");
|
|
|
+ videoUrl = imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果videoUrl为空,无法处理
|
|
|
+ if (StringUtils.isBlank(videoUrl)) {
|
|
|
+ log.warn("StoreVideoServiceImpl.processVideoAndCover videoUrl is blank");
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否为视频URL
|
|
|
+ if (!isVideoUrl(videoUrl)) {
|
|
|
+ log.warn("StoreVideoServiceImpl.processVideoAndCover 不是视频URL: {}", videoUrl);
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下载视频并截取封面
|
|
|
+ File tempVideoFile = null;
|
|
|
+ File coverFile = null;
|
|
|
+ try {
|
|
|
+ // 从URL下载视频到临时文件
|
|
|
+ tempVideoFile = downloadVideoFromUrl(videoUrl);
|
|
|
+ if (tempVideoFile == null || !tempVideoFile.exists()) {
|
|
|
+ log.error("StoreVideoServiceImpl.processVideoAndCover 下载视频失败: {}", videoUrl);
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 截取第一帧作为封面
|
|
|
+ String coverPath = videoUtils.getFirstFrame(tempVideoFile.getAbsolutePath());
|
|
|
+ if (StringUtils.isBlank(coverPath)) {
|
|
|
+ log.error("StoreVideoServiceImpl.processVideoAndCover 截取封面失败: {}", tempVideoFile.getAbsolutePath());
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ coverFile = new File(coverPath);
|
|
|
+ if (!coverFile.exists()) {
|
|
|
+ log.error("StoreVideoServiceImpl.processVideoAndCover 封面文件不存在: {}", coverPath);
|
|
|
+ return imgUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传封面到OSS
|
|
|
+ String coverFileName = generateCoverFileName(videoUrl);
|
|
|
+ String coverOssPath = "video/" + coverFileName + ".jpg";
|
|
|
+ String 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);
|
|
|
+
|
|
|
+ log.info("StoreVideoServiceImpl.processVideoAndCover 处理成功, videoUrl: {}, coverUrl: {}", videoUrl, coverUrl);
|
|
|
+ return JSON.toJSONString(resultArray);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("StoreVideoServiceImpl.processVideoAndCover 处理异常", e);
|
|
|
+ return imgUrl;
|
|
|
+ } finally {
|
|
|
+ // 清理临时文件
|
|
|
+ if (tempVideoFile != null && tempVideoFile.exists()) {
|
|
|
+ boolean deleted = tempVideoFile.delete();
|
|
|
+ if (!deleted) {
|
|
|
+ log.warn("StoreVideoServiceImpl.processVideoAndCover 删除临时视频文件失败: {}", tempVideoFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (coverFile != null && coverFile.exists()) {
|
|
|
+ boolean deleted = coverFile.delete();
|
|
|
+ if (!deleted) {
|
|
|
+ log.warn("StoreVideoServiceImpl.processVideoAndCover 删除临时封面文件失败: {}", coverFile.getAbsolutePath());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查URL是否为视频URL
|
|
|
+ *
|
|
|
+ * @param url 视频URL
|
|
|
+ * @return 是否为视频URL
|
|
|
+ */
|
|
|
+ private boolean isVideoUrl(String url) {
|
|
|
+ if (StringUtils.isBlank(url)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 检查URL是否以视频文件扩展名结尾
|
|
|
+ String lowerUrl = url.toLowerCase();
|
|
|
+ for (String videoType : VIDEO_FILE_TYPES) {
|
|
|
+ if (lowerUrl.endsWith("." + videoType)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL下载视频到临时文件
|
|
|
+ *
|
|
|
+ * @param videoUrl 视频URL
|
|
|
+ * @return 临时文件对象
|
|
|
+ */
|
|
|
+ private File downloadVideoFromUrl(String videoUrl) {
|
|
|
+ log.info("StoreVideoServiceImpl.downloadVideoFromUrl videoUrl={}", videoUrl);
|
|
|
+
|
|
|
+ InputStream inputStream = null;
|
|
|
+ FileOutputStream outputStream = null;
|
|
|
+ File tempFile = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建URL连接
|
|
|
+ URL url = new URL(videoUrl);
|
|
|
+ URLConnection connection = url.openConnection();
|
|
|
+ connection.setConnectTimeout(10000); // 10秒连接超时
|
|
|
+ connection.setReadTimeout(60000); // 60秒读取超时
|
|
|
+
|
|
|
+ // 创建临时文件
|
|
|
+ String tempDir = uploadDir.endsWith("/") ? uploadDir : uploadDir + "/";
|
|
|
+ String tempFileName = "temp_video_" + System.currentTimeMillis() + "_" + RandomCreateUtil.getRandomNum(6);
|
|
|
+
|
|
|
+ // 从URL中提取文件扩展名
|
|
|
+ String fileExtension = getFileExtensionFromUrl(videoUrl);
|
|
|
+ if (StringUtils.isNotBlank(fileExtension)) {
|
|
|
+ tempFileName += "." + fileExtension;
|
|
|
+ } else {
|
|
|
+ tempFileName += ".mp4"; // 默认使用mp4扩展名
|
|
|
+ }
|
|
|
+
|
|
|
+ tempFile = new File(tempDir + tempFileName);
|
|
|
+
|
|
|
+ // 确保目录存在
|
|
|
+ File parentDir = tempFile.getParentFile();
|
|
|
+ if (parentDir != null && !parentDir.exists()) {
|
|
|
+ parentDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下载文件
|
|
|
+ inputStream = connection.getInputStream();
|
|
|
+ outputStream = new FileOutputStream(tempFile);
|
|
|
+
|
|
|
+ byte[] buffer = new byte[8192];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
+ outputStream.write(buffer, 0, bytesRead);
|
|
|
+ }
|
|
|
+ outputStream.flush();
|
|
|
+
|
|
|
+ log.info("StoreVideoServiceImpl.downloadVideoFromUrl 下载成功, tempFile: {}", tempFile.getAbsolutePath());
|
|
|
+ return tempFile;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("StoreVideoServiceImpl.downloadVideoFromUrl 下载失败, videoUrl: {}", videoUrl, e);
|
|
|
+ // 如果下载失败,删除可能创建的临时文件
|
|
|
+ if (tempFile != null && tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } finally {
|
|
|
+ // 关闭流
|
|
|
+ if (inputStream != null) {
|
|
|
+ try {
|
|
|
+ inputStream.close();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("StoreVideoServiceImpl.downloadVideoFromUrl 关闭输入流失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (outputStream != null) {
|
|
|
+ try {
|
|
|
+ outputStream.close();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("StoreVideoServiceImpl.downloadVideoFromUrl 关闭输出流失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL中提取文件扩展名
|
|
|
+ *
|
|
|
+ * @param url 文件URL
|
|
|
+ * @return 文件扩展名(不包含点)
|
|
|
+ */
|
|
|
+ private String getFileExtensionFromUrl(String url) {
|
|
|
+ if (StringUtils.isBlank(url)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除URL参数
|
|
|
+ int questionMarkIndex = url.indexOf('?');
|
|
|
+ if (questionMarkIndex > 0) {
|
|
|
+ url = url.substring(0, questionMarkIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取扩展名
|
|
|
+ int lastDotIndex = url.lastIndexOf('.');
|
|
|
+ if (lastDotIndex > 0 && lastDotIndex < url.length() - 1) {
|
|
|
+ return url.substring(lastDotIndex + 1).toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成封面文件名
|
|
|
+ *
|
|
|
+ * @param videoUrl 视频URL
|
|
|
+ * @return 封面文件名(不包含扩展名)
|
|
|
+ */
|
|
|
+ private String generateCoverFileName(String videoUrl) {
|
|
|
+ // 从视频URL中提取文件名(去除扩展名)
|
|
|
+ String fileName = "cover_" + System.currentTimeMillis() + "_" + RandomCreateUtil.getRandomNum(6);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 尝试从URL中提取原始文件名
|
|
|
+ String urlPath = new URL(videoUrl).getPath();
|
|
|
+ int lastSlashIndex = urlPath.lastIndexOf('/');
|
|
|
+ if (lastSlashIndex >= 0 && lastSlashIndex < urlPath.length() - 1) {
|
|
|
+ String originalFileName = urlPath.substring(lastSlashIndex + 1);
|
|
|
+ // 移除文件扩展名
|
|
|
+ int lastDotIndex = originalFileName.lastIndexOf('.');
|
|
|
+ if (lastDotIndex > 0) {
|
|
|
+ String nameWithoutExt = originalFileName.substring(0, lastDotIndex);
|
|
|
+ // 清理文件名,移除特殊字符
|
|
|
+ nameWithoutExt = nameWithoutExt.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5]", "");
|
|
|
+ if (StringUtils.isNotBlank(nameWithoutExt)) {
|
|
|
+ fileName = nameWithoutExt + RandomCreateUtil.getRandomNum(6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("StoreVideoServiceImpl.generateCoverFileName 从URL提取文件名失败", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理文件名,避免包含逗号等特殊字符
|
|
|
+ fileName = fileName.replaceAll(",", "");
|
|
|
+
|
|
|
+ return fileName;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 根据门店ID获取视频列表
|
|
|
*
|