浏览代码

Merge branch 'BugFix-panzhilin' into sit

panzhilin 2 月之前
父节点
当前提交
3c9ed228bc

+ 5 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreVideo.java

@@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.annotation.*;
 import com.baomidou.mybatisplus.extension.activerecord.Model;
 import com.baomidou.mybatisplus.extension.activerecord.Model;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.EqualsAndHashCode;
+import shop.alien.entity.store.dto.deserializer.JsonStringDeserializer;
 
 
 import java.util.Date;
 import java.util.Date;
 
 
@@ -40,8 +43,9 @@ public class StoreVideo extends Model<StoreVideo> {
     @TableField("img_sort")
     @TableField("img_sort")
     private Integer imgSort;
     private Integer imgSort;
 
 
-    @ApiModelProperty(value = "视频链接")
+    @ApiModelProperty(value = "视频链接和封面地址")
     @TableField("img_url")
     @TableField("img_url")
+    @JsonDeserialize(using = JsonStringDeserializer.class)
     private String imgUrl;
     private String imgUrl;
 
 
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackDto.java

@@ -24,7 +24,7 @@ public class LifeFeedbackDto implements Serializable {
     @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
     @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
     private Integer feedbackWay;
     private Integer feedbackWay;
 
 
-    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-功能反馈")
     private Integer feedbackType;
     private Integer feedbackType;
 
 
     @ApiModelProperty(value = "反馈内容")
     @ApiModelProperty(value = "反馈内容")

+ 56 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/deserializer/JsonStringDeserializer.java

@@ -0,0 +1,56 @@
+package shop.alien.entity.store.dto.deserializer;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+
+/**
+ * 自定义反序列化器:将字符串或JSON数组/对象转换为字符串
+ * 支持以下格式:
+ * - 字符串:"xxx" 或 "[{\"video\":\"...\",\"cover\":\"...\"}]"
+ * - JSON数组:[{"video":"...","cover":"..."}]
+ * - JSON对象:{"video":"...","cover":"..."}
+ * - null:返回null
+ * 
+ * @author system
+ * @since 2025-01-16
+ */
+public class JsonStringDeserializer extends JsonDeserializer<String> {
+
+    @Override
+    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        JsonNode node = p.getCodec().readTree(p);
+        
+        // 如果为null,返回null
+        if (node == null || node.isNull()) {
+            return null;
+        }
+        
+        // 如果是字符串,直接返回
+        if (node.isTextual()) {
+            return node.asText();
+        }
+        
+        // 如果是数组或对象,转换为JSON字符串
+        if (node.isArray() || node.isObject()) {
+            try {
+                // 使用ObjectMapper将JsonNode转换为JSON字符串
+                ObjectMapper mapper = (ObjectMapper) p.getCodec();
+                return mapper.writeValueAsString(node);
+            } catch (JsonProcessingException e) {
+                // 如果转换失败,使用toString()方法(注意:这可能不会生成标准的JSON格式)
+                // 但作为兜底方案,仍然返回
+                return node.toString();
+            }
+        }
+        
+        // 其他类型(数字、布尔值等),转换为字符串
+        return node.asText();
+    }
+}
+

+ 11 - 5
alien-store/src/main/java/shop/alien/store/controller/StoreImgController.java

@@ -85,15 +85,21 @@ public class StoreImgController {
         boolean isHeadImage = (imgType == 20 || imgType == 21);
         boolean isHeadImage = (imgType == 20 || imgType == 21);
         if(imgType==4){
         if(imgType==4){
             Integer storeId = storeImgInfoVo.getStoreId();
             Integer storeId = storeImgInfoVo.getStoreId();
-            StoreOfficialAlbum album = storeOfficialAlbumService.lambdaQuery()
-                    .eq(StoreOfficialAlbum::getStoreId, storeId).one();
-            if (null == album) {
+            // 查询名称为"环境"的相册,因为一个门店可能有多个相册
+            List<StoreOfficialAlbum> albumList = storeOfficialAlbumService.lambdaQuery()
+                    .eq(StoreOfficialAlbum::getStoreId, storeId)
+                    .eq(StoreOfficialAlbum::getAlbumName, "环境")
+                    .orderByAsc(StoreOfficialAlbum::getId)
+                    .list();
+            if (albumList == null || albumList.isEmpty()) {
                 return R.fail("没有默认环境相册");
                 return R.fail("没有默认环境相册");
             }
             }
+            // 如果有多条记录,取第一条(按ID升序)
+            StoreOfficialAlbum album = albumList.get(0);
             storeImgList.forEach(storeImg -> storeImg.setBusinessId(album.getId()));
             storeImgList.forEach(storeImg -> storeImg.setBusinessId(album.getId()));
-            // 添加图片时 ,修改数量
+            // 添加图片时,修改数量(只更新当前使用的相册)
             storeOfficialAlbumService.lambdaUpdate()
             storeOfficialAlbumService.lambdaUpdate()
-                    .eq(StoreOfficialAlbum::getStoreId, storeId)
+                    .eq(StoreOfficialAlbum::getId, album.getId())
                     .setSql("img_count = img_count + " + storeImgList.size())
                     .setSql("img_count = img_count + " + storeImgList.size())
                     .update();
                     .update();
         }
         }

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackServiceImpl.java

@@ -400,7 +400,7 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
             case 1:
             case 1:
                 return "优化反馈";
                 return "优化反馈";
             case 2:
             case 2:
-                return "新增功能反馈";
+                return "功能反馈";
             default:
             default:
                 return "";
                 return "";
         }
         }

+ 37 - 1
alien-store/src/main/java/shop/alien/store/service/impl/StoreImgServiceImpl.java

@@ -15,11 +15,13 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeUser;
 import shop.alien.entity.store.LifeUser;
 import shop.alien.entity.store.StoreImg;
 import shop.alien.entity.store.StoreImg;
 import shop.alien.entity.store.StoreOfficialAlbum;
 import shop.alien.entity.store.StoreOfficialAlbum;
+import shop.alien.entity.store.StoreVideo;
 import shop.alien.entity.store.vo.StoreImgTypeVo;
 import shop.alien.entity.store.vo.StoreImgTypeVo;
 import shop.alien.mapper.LifeUserMapper;
 import shop.alien.mapper.LifeUserMapper;
 import shop.alien.mapper.StoreImgMapper;
 import shop.alien.mapper.StoreImgMapper;
 import shop.alien.mapper.StoreOfficialAlbumMapper;
 import shop.alien.mapper.StoreOfficialAlbumMapper;
 import shop.alien.store.service.StoreImgService;
 import shop.alien.store.service.StoreImgService;
+import shop.alien.store.service.StoreVideoService;
 import shop.alien.store.util.ai.AiImageColorExtractUtil;
 import shop.alien.store.util.ai.AiImageColorExtractUtil;
 import shop.alien.util.common.Constants;
 import shop.alien.util.common.Constants;
 
 
@@ -44,6 +46,7 @@ public class StoreImgServiceImpl extends ServiceImpl<StoreImgMapper, StoreImg> i
     private final StoreOfficialAlbumMapper storeOfficialAlbumMapper;
     private final StoreOfficialAlbumMapper storeOfficialAlbumMapper;
     private final LifeUserMapper lifeUserMapper;
     private final LifeUserMapper lifeUserMapper;
     private final AiImageColorExtractUtil aiImageColorExtractUtil;
     private final AiImageColorExtractUtil aiImageColorExtractUtil;
+    private final StoreVideoService storeVideoService;
 
 
     /**
     /**
      * 获取门店图片
      * 获取门店图片
@@ -181,7 +184,40 @@ public class StoreImgServiceImpl extends ServiceImpl<StoreImgMapper, StoreImg> i
         LambdaUpdateWrapper<StoreOfficialAlbum> updateWrapper = new LambdaUpdateWrapper<>();
         LambdaUpdateWrapper<StoreOfficialAlbum> updateWrapper = new LambdaUpdateWrapper<>();
         updateWrapper.eq(StoreOfficialAlbum::getId, businessId).set(StoreOfficialAlbum::getImgCount, imgCount);
         updateWrapper.eq(StoreOfficialAlbum::getId, businessId).set(StoreOfficialAlbum::getImgCount, imgCount);
         storeOfficialAlbumMapper.update(null, updateWrapper);
         storeOfficialAlbumMapper.update(null, updateWrapper);
-        return this.list(lambdaQueryWrapper);
+        List<StoreImg> resList = this.list(lambdaQueryWrapper);
+        // 查询视频列表,只获取第一个视频的封面
+        List<StoreVideo> byStoreId = storeVideoService.getByStoreId(storeId);
+        if (!CollectionUtils.isEmpty(byStoreId)) {
+            StoreVideo storeVideo = byStoreId.get(0);
+            String imgUrl = storeVideo.getImgUrl();
+            if (imgUrl != null && !imgUrl.trim().isEmpty()) {
+                try {
+                    // 解析JSON数组格式的imgUrl
+                    com.alibaba.fastjson.JSONArray jsonArray = com.alibaba.fastjson.JSONArray.parseArray(imgUrl);
+                    if (jsonArray != null && !jsonArray.isEmpty()) {
+                        com.alibaba.fastjson.JSONObject firstItem = jsonArray.getJSONObject(0);
+                        if (firstItem != null && firstItem.containsKey("cover")) {
+                            String coverUrl = firstItem.getString("cover");
+                            if (coverUrl != null && !coverUrl.trim().isEmpty()) {
+                                // 创建StoreImg对象,设置封面URL
+                                StoreImg storeImg = new StoreImg();
+                                storeImg.setImgUrl(coverUrl);
+                                storeImg.setStoreId(storeId);
+                                storeImg.setImgType(imgType);
+                                storeImg.setBusinessId(businessId);
+                                storeImg.setImgDescription("视频");
+                                storeImg.setImgSort(resList.size() + 1);
+                                resList.add(storeImg);
+                                log.info("从第一个视频中提取封面成功,coverUrl: {}", coverUrl);
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("解析视频imgUrl失败,imgUrl: {}, error: {}", imgUrl, e.getMessage());
+                }
+            }
+        }
+        return resList;
     }
     }
 
 
     @Override
     @Override

+ 0 - 3
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -305,9 +305,6 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         Integer headImgStatus = hasHeadImg ? 1 : 2;
         Integer headImgStatus = hasHeadImg ? 1 : 2;
         // 获取数据库中的head_img_status状态
         // 获取数据库中的head_img_status状态
         Integer headImgStatusFromDb = storeInfo.getHeadImgStatus();
         Integer headImgStatusFromDb = storeInfo.getHeadImgStatus();
-        // 记录查询结果日志
-        log.info("门店头图审核状态判断,storeId={}, 查询到头图数量={}, 是否有有效头图={}, 审核状态={}, 数据库原状态={}", 
-                id, headImgList != null ? headImgList.size() : 0, hasHeadImg, headImgStatus, headImgStatusFromDb);
         // 实时更新数据库:如果数据库中的状态与实际情况不一致,或者为null,则更新数据库
         // 实时更新数据库:如果数据库中的状态与实际情况不一致,或者为null,则更新数据库
         if (headImgStatusFromDb == null || !headImgStatus.equals(headImgStatusFromDb)) {
         if (headImgStatusFromDb == null || !headImgStatus.equals(headImgStatusFromDb)) {
             try {
             try {

+ 334 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreVideoServiceImpl.java

@@ -1,15 +1,29 @@
 package shop.alien.store.service.impl;
 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.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.apache.commons.lang3.StringUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.store.StoreVideo;
 import shop.alien.entity.store.StoreVideo;
 import shop.alien.mapper.StoreVideoMapper;
 import shop.alien.mapper.StoreVideoMapper;
 import shop.alien.store.service.StoreVideoService;
 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;
 import java.util.List;
 
 
 /**
 /**
@@ -24,6 +38,326 @@ import java.util.List;
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 public class StoreVideoServiceImpl extends ServiceImpl<StoreVideoMapper, StoreVideo> implements StoreVideoService {
 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获取视频列表
      * 根据门店ID获取视频列表
      *
      *

+ 55 - 1
alien-util/src/main/java/shop/alien/util/common/VideoUtils.java

@@ -29,7 +29,7 @@ public class VideoUtils {
     public String getImg(String videoFilePath) {
     public String getImg(String videoFilePath) {
         log.info("VideoUtils.getImg videoFilePath={}", videoFilePath);
         log.info("VideoUtils.getImg videoFilePath={}", videoFilePath);
         String[] file = videoFilePath.split("\\.");
         String[] file = videoFilePath.split("\\.");
-        log.info("VideoUtils.getImg file={}", file);
+        log.info("VideoUtils.getImg file={}", (Object) file);
         // 截图保存位置
         // 截图保存位置
         String imgFilePath = file[0] + ".jpg";
         String imgFilePath = file[0] + ".jpg";
         log.info("VideoUtils.getImg imgFilePath={}", imgFilePath);
         log.info("VideoUtils.getImg imgFilePath={}", imgFilePath);
@@ -66,6 +66,60 @@ public class VideoUtils {
     }
     }
 
 
     /**
     /**
+     * 视频截取第一帧作为封面
+     *
+     * @param videoFilePath 视频媒体文件路径
+     * @return 封面图片文件路径
+     */
+    public String getFirstFrame(String videoFilePath) {
+        log.info("VideoUtils.getFirstFrame videoFilePath={}", videoFilePath);
+        // 验证参数
+        if (videoFilePath == null || videoFilePath.isEmpty()) {
+            log.error("VideoUtils.getFirstFrame ERROR: videoFilePath is null or empty");
+            return "";
+        }
+        
+        String[] file = videoFilePath.split("\\.");
+        log.info("VideoUtils.getFirstFrame file={}", (Object) file);
+        // 截图保存位置
+        String imgFilePath = file[0] + ".jpg";
+        log.info("VideoUtils.getFirstFrame imgFilePath={}", imgFilePath);
+        
+        ProcessBuilder processBuilder = new ProcessBuilder();
+        // 重定向错误流,防阻塞
+        processBuilder.redirectErrorStream(true);
+
+        // 如果为本地测试,ffmpegPath地址需求修改为本地安装程序的地址(环境变量不好用)
+        String ffmpegPath = "ffmpeg";
+        if ("windows".equals(OSUtil.getOsName())) {
+            // ffmpegPath = "C:/Program Files (x86)/ffmpeg-6.0/bin/ffmpeg.exe";
+            ffmpegPath = "C:/project/ext/ffmpeg-6.0/bin/ffmpeg.exe";
+        }
+
+        // 调用ffmpeg 执行截取命令,截取第一帧(00:00:00),需要服务器中安装了ffmpeg并配置了环境变量
+        processBuilder.command(ffmpegPath, "-i", videoFilePath, "-ss", "00:00:00", "-vframes", "1", imgFilePath);
+        try {
+            Process process = processBuilder.start();
+            // 获取流信息
+            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                log.debug("VideoUtils.getFirstFrame ffmpeg output: {}", line);
+            }
+            int exitCode = process.waitFor();
+            log.info("VideoUtils.getFirstFrame ffmpeg exit code: {}", exitCode);
+            if (0 == exitCode) {
+                return imgFilePath;
+            }
+            log.error("VideoUtils.getFirstFrame ERROR: ffmpeg exit code is not 0");
+            return "";
+        } catch (IOException | InterruptedException e) {
+            log.error("VideoUtils.getFirstFrame ERROR Msg={}", e.getMessage(), e);
+            return "";
+        }
+    }
+
+    /**
      * FFMpeg视频截取图片
      * FFMpeg视频截取图片
      *
      *
      * @param videoFilePath 视频媒体文件
      * @param videoFilePath 视频媒体文件