jinshihui 1 ヶ月 前
コミット
0a9defdbd6

+ 0 - 5
nightFragrance-admin/src/main/java/com/ylx/web/controller/common/CommonController.java

@@ -130,24 +130,19 @@ public class CommonController {
         Map<String, Object> response = new HashMap<>();
         try {
             log.info("开始处理视频上传,文件名: {}, 大小: {} bytes", file.getOriginalFilename(), file.getSize());
-
             // 截取视频封面
             String thumbnailPath = videoThumbnailService.extractThumbnail(file);
-
             // 返回成功响应
             response.put("success", true);
             response.put("message", "视频上传成功");
             response.put("thumbnailPath", thumbnailPath);
             response.put("originalFilename", file.getOriginalFilename());
-
-            log.info("视频处理成功,封面路径: {}", thumbnailPath);
             return AjaxResult.success(response);
         } catch (VideoThumbnailService.VideoProcessException e) {
             log.error("视频处理失败", e);
             response.put("success", false);
             response.put("message", e.getMessage());
             return AjaxResult.error(response.toString());
-
         } catch (Exception e) {
             log.error("系统错误", e);
             response.put("success", false);

+ 1 - 3
nightFragrance-admin/src/main/java/com/ylx/web/controller/massage/TechnicianMomentController.java

@@ -184,9 +184,7 @@ public class TechnicianMomentController extends BaseController {
      */
     @GetMapping("/detail")
     @ApiOperation("获取动态详情")
-    public R<MomentDetailVO> getMomentDetail(@ApiParam("动态ID") @RequestParam Long momentId,
-                                             @ApiParam("用户经度") @RequestParam(required = false) BigDecimal longitude,
-                                             @ApiParam("用户纬度") @RequestParam(required = false) BigDecimal latitude) {
+    public R<MomentDetailVO> getMomentDetail(@ApiParam("动态ID") @RequestParam Long momentId, @ApiParam("用户经度") @RequestParam(required = false) BigDecimal longitude, @ApiParam("用户纬度") @RequestParam(required = false) BigDecimal latitude) {
         try {
             if (momentId == null) {
                 return R.fail("动态ID不能为空");

+ 7 - 5
nightFragrance-common/src/main/java/com/ylx/common/utils/file/FileUploadUtils.java

@@ -6,6 +6,7 @@ import java.io.IOException;
 import java.nio.file.Paths;
 import java.util.Objects;
 
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FilenameUtils;
 import org.apache.http.entity.ContentType;
 import org.springframework.mock.web.MockMultipartFile;
@@ -24,6 +25,7 @@ import com.ylx.common.utils.uuid.Seq;
  *
  * @author ylx
  */
+@Slf4j
 public class FileUploadUtils {
     /**
      * 默认大小 50M
@@ -96,7 +98,7 @@ public class FileUploadUtils {
     }
 
     /**
-     * 文件上传
+     * 上传文件
      *
      * @param baseDir          相对应用的基目录
      * @param file             上传的文件
@@ -108,8 +110,7 @@ public class FileUploadUtils {
      * @throws InvalidExtensionException            文件校验异常
      */
     public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
-            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
-            InvalidExtensionException {
+            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, InvalidExtensionException {
         int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
         if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
             throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
@@ -118,8 +119,9 @@ public class FileUploadUtils {
         assertAllowed(file, allowedExtension);
 
         String fileName = extractFilename(file);
-
+        log.info("新的文件名: {}", fileName);
         String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
+        log.info("文件绝对路径: {}", absPath);
         file.transferTo(Paths.get(absPath));
         return getPathFileName(baseDir, fileName);
     }
@@ -209,7 +211,7 @@ public class FileUploadUtils {
      * 获取文件名的后缀
      *
      * @param file 表单文件
-     * @return 后缀名
+     * @return String 后缀名
      */
     public static final String getExtension(MultipartFile file) {
         String extension = FilenameUtils.getExtension(file.getOriginalFilename());

+ 9 - 15
nightFragrance-common/src/main/java/com/ylx/common/utils/uuid/Seq.java

@@ -1,14 +1,14 @@
 package com.ylx.common.utils.uuid;
 
 import java.util.concurrent.atomic.AtomicInteger;
+
 import com.ylx.common.utils.DateUtils;
 import com.ylx.common.utils.StringUtils;
 
 /**
  * @author ylx 序列生成类
  */
-public class Seq
-{
+public class Seq {
     // 通用序列类型
     public static final String commSeqType = "COMMON";
 
@@ -29,8 +29,7 @@ public class Seq
      *
      * @return 序列值
      */
-    public static String getId()
-    {
+    public static String getId() {
         return getId(commSeqType);
     }
 
@@ -39,11 +38,9 @@ public class Seq
      *
      * @return 序列值
      */
-    public static String getId(String type)
-    {
+    public static String getId(String type) {
         AtomicInteger atomicInt = commSeq;
-        if (uploadSeqType.equals(type))
-        {
+        if (uploadSeqType.equals(type)) {
             atomicInt = uploadSeq;
         }
         return getId(atomicInt, 3);
@@ -53,11 +50,10 @@ public class Seq
      * 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串
      *
      * @param atomicInt 序列数
-     * @param length 数值长度
+     * @param length    数值长度
      * @return 序列值
      */
-    public static String getId(AtomicInteger atomicInt, int length)
-    {
+    public static String getId(AtomicInteger atomicInt, int length) {
         String result = DateUtils.dateTimeNow();
         result += machineCode;
         result += getSeq(atomicInt, length);
@@ -69,15 +65,13 @@ public class Seq
      *
      * @return 序列值
      */
-    private synchronized static String getSeq(AtomicInteger atomicInt, int length)
-    {
+    private synchronized static String getSeq(AtomicInteger atomicInt, int length) {
         // 先取值再+1
         int value = atomicInt.getAndIncrement();
 
         // 如果更新后值>=10 的 (length)幂次方则重置为1
         int maxSeq = (int) Math.pow(10, length);
-        if (atomicInt.get() >= maxSeq)
-        {
+        if (atomicInt.get() >= maxSeq) {
             atomicInt.set(1);
         }
         // 转字符串,用0左补齐

+ 14 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/TbFile.java

@@ -26,6 +26,12 @@ public class TbFile extends Model<TbFile> {
     private String md5;
     //虚拟文件路径
     private String fileUrl;
+
+    /**
+     * 封面图URL路径(针对视频文件)
+     */
+    private String coverUrl;
+
     //文件名
     private String fileName;
     //系统创建时间
@@ -93,6 +99,14 @@ public class TbFile extends Model<TbFile> {
         this.isDelete = isDelete;
     }
 
+    public String getCoverUrl() {
+        return coverUrl;
+    }
+
+    public void setCoverUrl(String coverUrl) {
+        this.coverUrl = coverUrl;
+    }
+
     /**
      * 获取主键值
      *

+ 1 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/domain/TechnicianMoment.java

@@ -110,10 +110,7 @@ public class TechnicianMoment {
     private String rejectReason;
 
     /**
-     * 可见范围:1-公开,2-仅粉丝,3-私密
+     * 可见范围:1-公开
      */
     private Integer visibleRange;
-
-
-
 }

+ 16 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/domain/vo/MomentDetailVO.java

@@ -4,6 +4,7 @@ import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -35,6 +36,16 @@ public class MomentDetailVO {
     @ApiModelProperty("发布时间")
     private LocalDateTime publishTime;
 
+    /**
+     * 经度
+     */
+    private BigDecimal longitude;
+
+    /**
+     * 纬度
+     */
+    private BigDecimal latitude;
+
     @ApiModelProperty("发布地址")
     private String address;
 
@@ -59,4 +70,9 @@ public class MomentDetailVO {
 
     @ApiModelProperty("距离(km)")
     private Double distance;
+
+    /**
+     * 可见范围:1-公开
+     */
+    private Integer visibleRange;
 }

+ 169 - 4
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/TbFileServiceImpl.java

@@ -17,7 +17,17 @@ import org.springframework.stereotype.Service;
 import com.ylx.massage.service.TbFileService;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 文件管理服务实现类
@@ -35,6 +45,15 @@ import java.io.IOException;
 @Slf4j
 public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> implements TbFileService {
 
+    // 封面图片存储路径
+    private static final String THUMBNAIL_SAVE_PATH = "E:/tmp/thumbnails/";
+    // FFmpeg 命令路径(Windows 下需要使用 .exe 扩展名)
+    private static final String FFMPEG_PATH = "E:\\ffmpeg-8.0.1-essentials_build\\bin\\ffmpeg.exe";
+    // 截取时间点(秒)
+    private static final int CAPTURE_TIME_SECOND = 1;
+    // 命令执行超时时间(秒)
+    private static final int COMMAND_TIMEOUT = 30;
+
     @Autowired
     private ServerConfig serverConfig;
 
@@ -95,7 +114,6 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
         try {
             // 计算文件 MD5 值
             String md5 = calculateMD5(file);
-
             // 检查是否已存在相同文件(通过 MD5 去重)
             TbFile dbFile = this.getByMd5(md5);
             if (null != dbFile) {
@@ -104,28 +122,46 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
                 ajax.put("fileName", dbFile.getFileUrl());
                 ajax.put("newFileName", FileUtils.getName(dbFile.getFileUrl()));
                 ajax.put("originalFilename", dbFile.getFileName());
+                // 封面图URL路径(针对视频文件)
+                ajax.put("coverUrl", serverConfig.getUrl()+dbFile.getCoverUrl());
                 return ajax;
             }
 
             // 文件不存在,上传新文件
             // 获取上传文件路径配置
             String filePath = RuoYiConfig.getUploadPath();
-            log.info("上传文件路径:{}", filePath);
-
-            // 上传文件到服务器并返回新文件名称
+            // 上传文件到服务器并返回新的文件名称
             String fileName = FileUploadUtils.upload(filePath, file);
+            log.info("上传文件到服务器并返回新的文件名称:{}", fileName);
+            //获取新的文件子路径
+            String subFileName= fileName.substring(fileName.indexOf("/upload") + 7);
+            // 构建完整的文件访问路径
+            subFileName = filePath + subFileName;
+            log.info("上传文件路径:{},返回的新文件路径:{}", filePath, subFileName);
+
+            //生成视频的封面图片文件(该方法内部会执行 FFmpeg 命令并验证)
+            String thumbnailFilePath = generateThumbnailFilePath(subFileName, file.getOriginalFilename());
+            String substring = thumbnailFilePath.substring(thumbnailFilePath.lastIndexOf("\\upload"));
+
+            substring = "/profile" + substring;
+            log.info("生成视频的封面图片文件路径:{}", substring);
+            substring = substring.replace("\\", "/");
+
             // 构建文件访问的完整 URL
             String url = serverConfig.getUrl() + fileName;
             ajax.put("url", url);
             ajax.put("fileName", fileName);
             ajax.put("newFileName", FileUtils.getName(fileName));
             ajax.put("originalFilename", file.getOriginalFilename());
+            // 封面图URL路径(针对视频文件)
+            ajax.put("coverUrl",  serverConfig.getUrl() +substring);
 
             // 保存文件记录到数据库
             TbFile tbFile = new TbFile();
             tbFile.setMd5(md5);
             tbFile.setFileName(file.getOriginalFilename());
             tbFile.setFileUrl(fileName);
+            tbFile.setCoverUrl(substring);
             this.save(tbFile);
             return ajax;
         } catch (Exception e) {
@@ -133,5 +169,134 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
             return AjaxResult.error(e.getMessage());
         }
     }
+
+    /**
+     * 生成封面图片文件到指定路径
+     * <p>
+     * 该方法会完整地执行以下操作:
+     * 1. 确保保存目录存在
+     * 2. 生成唯一的封面文件路径
+     * 3. 使用 FFmpeg 从视频中截取封面图
+     * 4. 验证生成的图片文件有效性
+     * </p>
+     *
+     * @param videoPath 视频文件路径
+     * @param originalFilename 原始视频文件名(用于生成缩略图文件名)
+     * @return 生成的封面图片文件绝对路径
+     * @throws IOException 如果目录创建失败
+     * @throws VideoThumbnailService.VideoProcessException 如果封面生成失败或验证失败
+     */
+    private String generateThumbnailFilePath(String videoPath, String originalFilename) throws IOException, VideoThumbnailService.VideoProcessException {
+        // 1. 确保保存目录存在
+        Path saveDir = Paths.get(videoPath);
+        // 2. 生成唯一的封面文件路径
+        String baseFilename = getFileNameWithoutExtension(originalFilename);
+        String thumbnailFileName = baseFilename + "_" + System.currentTimeMillis() + "_thumbnail.jpg";
+
+        String result = videoPath.substring(0, videoPath.lastIndexOf("/") + 1);
+        Path thumbnailPath = Paths.get(result + thumbnailFileName);
+        String thumbnailPathStr = thumbnailPath.toString();
+        log.info("开始生成封面图片,视频文件的路径: {}, 封面图片的路径: {}", videoPath, thumbnailPathStr);
+
+        // 3. 执行 FFmpeg 命令生成封面
+        try {
+            executeFfmpegCommand(videoPath, thumbnailPathStr);
+            log.info("FFmpeg 命令执行成功");
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new VideoThumbnailService.VideoProcessException("封面生成过程被中断", e);
+        }
+
+        // 4. 验证生成的封面文件
+        validateThumbnailFile(thumbnailPathStr);
+        log.info("封面图片生成成功: {}", thumbnailPathStr);
+        return thumbnailPathStr;
+    }
+
+    /**
+     * 获取不带扩展名的文件名
+     */
+    private String getFileNameWithoutExtension(String filename) {
+        if (filename == null || filename.isEmpty()) {
+            return UUID.randomUUID().toString();
+        }
+        int lastDotIndex = filename.lastIndexOf('.');
+        return lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename;
+    }
+
+
+    /**
+     * 执行 FFmpeg 命令截取封面
+     */
+    private void executeFfmpegCommand(String videoPath, String thumbnailPath) throws IOException, InterruptedException, VideoThumbnailService.VideoProcessException {
+        // 构建 FFmpeg 命令
+        // -ss: 截取时间点
+        // -i: 输入文件
+        // -vframes 1: 只截取一帧
+        // -q:v 2: 图片质量(1-31,数字越小质量越高)
+        // -y: 覆盖输出文件
+        List<String> command = new ArrayList<>();
+        command.add(FFMPEG_PATH);
+        command.add("-ss");
+        command.add(String.valueOf(CAPTURE_TIME_SECOND));
+        command.add("-i");
+        command.add(videoPath);
+        command.add("-vframes");
+        command.add("1");
+        command.add("-q:v");
+        command.add("2");
+        command.add("-y");
+        command.add(thumbnailPath);
+        log.info("执行FFmpeg命令: {}", String.join(" ", command));
+
+        // 执行命令
+        ProcessBuilder processBuilder = new ProcessBuilder(command);
+        processBuilder.redirectErrorStream(true);
+        Process process = processBuilder.start();
+
+        // 读取命令输出(用于日志和调试)
+        StringBuilder output = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                output.append(line).append("\n");
+            }
+        }
+        // 等待命令执行完成(设置超时)
+        boolean finished = process.waitFor(COMMAND_TIMEOUT, TimeUnit.SECONDS);
+        if (!finished) {
+            process.destroyForcibly();
+            throw new VideoThumbnailService.VideoProcessException("FFmpeg 命令执行超时");
+        }
+        int exitCode = process.exitValue();
+        if (exitCode != 0) {
+            log.error("FFmpeg 执行失败,输出: {}", output);
+            throw new VideoThumbnailService.VideoProcessException("FFmpeg 执行失败,退出码: " + exitCode);
+        }
+        log.debug("FFmpeg 输出: {}", output);
+    }
+
+
+    /**
+     * 验证生成的封面文件
+     */
+    private void validateThumbnailFile(String thumbnailPath) throws VideoThumbnailService.VideoProcessException {
+        File thumbnailFile = new File(thumbnailPath);
+        if (!thumbnailFile.exists()) {
+            throw new VideoThumbnailService.VideoProcessException("封面文件生成失败");
+        }
+        if (thumbnailFile.length() == 0) {
+            throw new VideoThumbnailService.VideoProcessException("生成的封面文件为空");
+        }
+        // 可选:验证文件是否为有效的图片
+        try {
+            String mimeType = Files.probeContentType(thumbnailFile.toPath());
+            if (mimeType == null || !mimeType.startsWith("image/")) {
+                throw new VideoThumbnailService.VideoProcessException("生成的文件不是有效的图片");
+            }
+        } catch (IOException e) {
+            log.warn("无法验证文件 MIME 类型", e);
+        }
+    }
 }
 

+ 54 - 22
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/VideoThumbnailService.java

@@ -25,8 +25,8 @@ public class VideoThumbnailService {
     private static final String VIDEO_TEMP_PATH = "E:/tmp/videos/";
     // 封面图片存储路径
     private static final String THUMBNAIL_SAVE_PATH = "E:/tmp/thumbnails/";
-    // FFmpeg 命令路径(根据实际安装路径调整
-    private static final String FFMPEG_PATH = "ffmpeg";
+    // FFmpeg 命令路径(Windows 下需要使用 .exe 扩展名
+    private static final String FFMPEG_PATH = "E:\\ffmpeg-8.0.1-essentials_build\\bin\\ffmpeg.exe";
     // 截取时间点(秒)
     private static final int CAPTURE_TIME_SECOND = 1;
     // 命令执行超时时间(秒)
@@ -41,37 +41,23 @@ public class VideoThumbnailService {
      */
     public String extractThumbnail(MultipartFile videoFile) throws VideoProcessException {
         validateVideoFile(videoFile);
-
         String videoTempPath = null;
-        String thumbnailPath = null;
-
         try {
             // 1. 保存上传的视频文件到临时目录
             videoTempPath = saveVideoToTemp(videoFile);
             log.info("视频文件已保存到临时目录: {}", videoTempPath);
 
-            // 2. 生成封面图片保存路径
-            thumbnailPath = generateThumbnailPath(videoFile.getOriginalFilename());
-            log.info("封面图片将保存到: {}", thumbnailPath);
-
-            // 3. 使用 FFmpeg 截取封面
-            executeFfmpegCommand(videoTempPath, thumbnailPath);
-            log.info("封面图片生成成功: {}", thumbnailPath);
-            // 4. 验证生成的封面文件
-            validateThumbnailFile(thumbnailPath);
-            return thumbnailPath;
+            // 2. 生成封面图片文件(该方法内部会执行 FFmpeg 命令并验证)
+            String thumbnailFilePath = generateThumbnailFilePath(videoTempPath, videoFile.getOriginalFilename());
+            return thumbnailFilePath;
         } catch (Exception e) {
-            // 如果失败,删除可能生成的封面文件
-            /*if (thumbnailPath != null) {
-                deleteFileQuietly(thumbnailPath);
-            }*/
             log.error("视频封面截取失败", e);
             throw new VideoProcessException("视频封面截取失败: " + e.getMessage(), e);
         } finally {
             // 清理临时视频文件
-            /*if (videoTempPath != null) {
+            if (videoTempPath != null) {
                 deleteFileQuietly(videoTempPath);
-            }*/
+            }
         }
     }
 
@@ -171,6 +157,52 @@ public class VideoThumbnailService {
         return saveDir.resolve(thumbnailFileName).toString();
     }
 
+    /**
+     * 生成封面图片文件到指定路径
+     * <p>
+     * 该方法会完整地执行以下操作:
+     * 1. 确保保存目录存在
+     * 2. 生成唯一的封面文件路径
+     * 3. 使用 FFmpeg 从视频中截取封面图
+     * 4. 验证生成的图片文件有效性
+     * </p>
+     *
+     * @param videoPath 视频文件路径
+     * @param originalFilename 原始视频文件名(用于生成缩略图文件名)
+     * @return 生成的封面图片文件绝对路径
+     * @throws IOException 如果目录创建失败
+     * @throws VideoProcessException 如果封面生成失败或验证失败
+     */
+    private String generateThumbnailFilePath(String videoPath, String originalFilename) throws IOException, VideoProcessException {
+        // 1. 确保保存目录存在
+        Path saveDir = Paths.get(THUMBNAIL_SAVE_PATH);
+        if (!Files.exists(saveDir)) {
+            Files.createDirectories(saveDir);
+            log.info("创建封面图片保存目录: {}", saveDir.toAbsolutePath());
+        }
+
+        // 2. 生成唯一的封面文件路径
+        String baseFilename = getFileNameWithoutExtension(originalFilename);
+        String thumbnailFileName = baseFilename + "_" + System.currentTimeMillis() + "_thumbnail.jpg";
+        Path thumbnailPath = saveDir.resolve(thumbnailFileName).toAbsolutePath();
+        String thumbnailPathStr = thumbnailPath.toString();
+        log.info("开始生成封面图片,视频: {}, 目标路径: {}", videoPath, thumbnailPathStr);
+
+        // 3. 执行 FFmpeg 命令生成封面
+        try {
+            executeFfmpegCommand(videoPath, thumbnailPathStr);
+            log.info("FFmpeg 命令执行成功");
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new VideoProcessException("封面生成过程被中断", e);
+        }
+
+        // 4. 验证生成的封面文件
+        validateThumbnailFile(thumbnailPathStr);
+        log.info("封面图片生成成功: {}", thumbnailPathStr);
+        return thumbnailPathStr;
+    }
+
     /**
      * 执行 FFmpeg 命令截取封面
      */
@@ -193,7 +225,7 @@ public class VideoThumbnailService {
         command.add("2");
         command.add("-y");
         command.add(thumbnailPath);
-        log.info("执行 FFmpeg 命令: {}", String.join(" ", command));
+        log.info("执行FFmpeg命令: {}", String.join(" ", command));
 
         // 执行命令
         ProcessBuilder processBuilder = new ProcessBuilder(command);