|
@@ -17,7 +17,17 @@ import org.springframework.stereotype.Service;
|
|
|
import com.ylx.massage.service.TbFileService;
|
|
import com.ylx.massage.service.TbFileService;
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
+import java.io.BufferedReader;
|
|
|
|
|
+import java.io.File;
|
|
|
import java.io.IOException;
|
|
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
|
|
@Slf4j
|
|
|
public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> implements TbFileService {
|
|
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
|
|
@Autowired
|
|
|
private ServerConfig serverConfig;
|
|
private ServerConfig serverConfig;
|
|
|
|
|
|
|
@@ -95,7 +114,6 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
try {
|
|
try {
|
|
|
// 计算文件 MD5 值
|
|
// 计算文件 MD5 值
|
|
|
String md5 = calculateMD5(file);
|
|
String md5 = calculateMD5(file);
|
|
|
-
|
|
|
|
|
// 检查是否已存在相同文件(通过 MD5 去重)
|
|
// 检查是否已存在相同文件(通过 MD5 去重)
|
|
|
TbFile dbFile = this.getByMd5(md5);
|
|
TbFile dbFile = this.getByMd5(md5);
|
|
|
if (null != dbFile) {
|
|
if (null != dbFile) {
|
|
@@ -104,28 +122,46 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
ajax.put("fileName", dbFile.getFileUrl());
|
|
ajax.put("fileName", dbFile.getFileUrl());
|
|
|
ajax.put("newFileName", FileUtils.getName(dbFile.getFileUrl()));
|
|
ajax.put("newFileName", FileUtils.getName(dbFile.getFileUrl()));
|
|
|
ajax.put("originalFilename", dbFile.getFileName());
|
|
ajax.put("originalFilename", dbFile.getFileName());
|
|
|
|
|
+ // 封面图URL路径(针对视频文件)
|
|
|
|
|
+ ajax.put("coverUrl", serverConfig.getUrl()+dbFile.getCoverUrl());
|
|
|
return ajax;
|
|
return ajax;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 文件不存在,上传新文件
|
|
// 文件不存在,上传新文件
|
|
|
// 获取上传文件路径配置
|
|
// 获取上传文件路径配置
|
|
|
String filePath = RuoYiConfig.getUploadPath();
|
|
String filePath = RuoYiConfig.getUploadPath();
|
|
|
- log.info("上传文件路径:{}", filePath);
|
|
|
|
|
-
|
|
|
|
|
- // 上传文件到服务器并返回新文件名称
|
|
|
|
|
|
|
+ // 上传文件到服务器并返回新的文件名称
|
|
|
String fileName = FileUploadUtils.upload(filePath, file);
|
|
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
|
|
// 构建文件访问的完整 URL
|
|
|
String url = serverConfig.getUrl() + fileName;
|
|
String url = serverConfig.getUrl() + fileName;
|
|
|
ajax.put("url", url);
|
|
ajax.put("url", url);
|
|
|
ajax.put("fileName", fileName);
|
|
ajax.put("fileName", fileName);
|
|
|
ajax.put("newFileName", FileUtils.getName(fileName));
|
|
ajax.put("newFileName", FileUtils.getName(fileName));
|
|
|
ajax.put("originalFilename", file.getOriginalFilename());
|
|
ajax.put("originalFilename", file.getOriginalFilename());
|
|
|
|
|
+ // 封面图URL路径(针对视频文件)
|
|
|
|
|
+ ajax.put("coverUrl", serverConfig.getUrl() +substring);
|
|
|
|
|
|
|
|
// 保存文件记录到数据库
|
|
// 保存文件记录到数据库
|
|
|
TbFile tbFile = new TbFile();
|
|
TbFile tbFile = new TbFile();
|
|
|
tbFile.setMd5(md5);
|
|
tbFile.setMd5(md5);
|
|
|
tbFile.setFileName(file.getOriginalFilename());
|
|
tbFile.setFileName(file.getOriginalFilename());
|
|
|
tbFile.setFileUrl(fileName);
|
|
tbFile.setFileUrl(fileName);
|
|
|
|
|
+ tbFile.setCoverUrl(substring);
|
|
|
this.save(tbFile);
|
|
this.save(tbFile);
|
|
|
return ajax;
|
|
return ajax;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
@@ -133,5 +169,134 @@ public class TbFileServiceImpl extends ServiceImpl<TbFileMapper, TbFile> impleme
|
|
|
return AjaxResult.error(e.getMessage());
|
|
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);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|