当前位置: 首页 > news >正文

RuoYi-Plus(前后端分离)视频上传实战:从Vue3组件到SpringBoot后端的完整实现

1. 前端Vue3视频上传组件实战

在RuoYi-Plus前后端分离项目中,视频上传功能的前端实现主要依赖Element Plus的el-upload组件。这个组件已经封装了大部分上传逻辑,我们只需要进行适当配置就能满足业务需求。先来看一个完整的实现方案:

<template> <el-upload class="video-uploader" :headers="uploadConfig.headers" :action="uploadConfig.uploadUrl" :drag="true" :multiple="false" :show-file-list="false" :on-success="handleSuccess" :before-upload="beforeUpload" :on-progress="handleProgress" > <div v-if="!videoUrl" class="upload-area"> <i class="el-icon-upload"></i> <div class="el-upload__text"> 将视频拖到此处,或<em>点击上传</em> </div> </div> <div v-else class="preview-area"> <video :src="videoUrl" controls class="preview-video"></video> <button class="close-btn" @click="handleRemove">移除视频</button> </div> <el-progress v-if="showProgress" type="circle" :percentage="progressPercent" :width="120" ></el-progress> <div class="el-upload__tip" slot="tip"> 支持MP4/AVI/FLV格式,大小不超过5GB </div> </el-upload> </template>

这个组件实现了几个关键功能点:

  • 拖拽上传和点击上传两种交互方式
  • 上传过程中的圆形进度条显示
  • 上传成功后的视频预览
  • 自定义的移除按钮

1.1 上传配置与状态管理

在script部分,我们需要设置上传的配置参数和管理组件状态:

import { reactive, ref } from 'vue' import { getToken } from '@/utils/auth' import { ElMessage } from 'element-plus' export default { setup() { const uploadConfig = reactive({ headers: { Authorization: `Bearer ${getToken()}` }, uploadUrl: import.meta.env.VITE_APP_BASE_API + '/api/video/upload' }) const videoUrl = ref('') const showProgress = ref(false) const progressPercent = ref(0) const beforeUpload = (file) => { const validTypes = [ 'video/mp4', 'video/ogg', 'video/flv', 'video/avi' ] const isTypeValid = validTypes.includes(file.type) const isSizeValid = file.size / 1024 / 1024 < 5000 if (!isTypeValid) { ElMessage.error('请上传MP4/AVI/FLV格式的视频文件') return false } if (!isSizeValid) { ElMessage.error('视频大小不能超过5GB') return false } showProgress.value = true return true } const handleProgress = (event) => { progressPercent.value = Math.floor(event.percent) } const handleSuccess = (response) => { if (response.code === 200) { videoUrl.value = response.data.url ElMessage.success('视频上传成功') } else { ElMessage.error(response.msg || '上传失败') } showProgress.value = false } const handleRemove = () => { videoUrl.value = '' } return { uploadConfig, videoUrl, showProgress, progressPercent, beforeUpload, handleProgress, handleSuccess, handleRemove } } }

这里有几个关键点需要注意:

  1. 上传地址需要拼接基础API地址,建议通过环境变量配置
  2. 请求头需要携带认证token,这里使用项目封装的getToken方法
  3. 文件类型校验既要在前端做,也要在后端做,双重保障
  4. 上传进度事件中的percent属性是小数,需要转换为整数百分比

1.2 样式优化与交互细节

为了让上传组件更美观实用,我们可以添加一些CSS样式:

.video-uploader { width: 600px; margin: 0 auto; text-align: center; } .upload-area { padding: 40px 0; border: 2px dashed #dcdfe6; border-radius: 6px; cursor: pointer; transition: border-color 0.3s; } .upload-area:hover { border-color: #409eff; } .preview-area { position: relative; margin-top: 20px; } .preview-video { width: 100%; max-height: 400px; border-radius: 6px; background: #000; } .close-btn { position: absolute; right: 10px; top: 10px; padding: 5px 10px; background: rgba(255, 0, 0, 0.7); color: white; border: none; border-radius: 4px; cursor: pointer; } .close-btn:hover { background: rgba(255, 0, 0, 0.9); } .el-progress { margin: 20px auto; }

这些样式实现了:

  • 上传区域明显的拖拽提示效果
  • 视频预览时的黑色背景(更适合视频播放)
  • 悬浮在视频右上角的移除按钮
  • 进度条居中显示

2. SpringBoot后端文件接收处理

前端完成上传组件后,我们需要一个可靠的后端接口来处理文件上传。在RuoYi-Plus框架中,可以通过以下方式实现:

2.1 基础文件上传接口

@RestController @RequestMapping("/api/video") public class VideoUploadController { private static final Logger log = LoggerFactory.getLogger(VideoUploadController.class); @PostMapping("/upload") public AjaxResult uploadVideo(@RequestParam("file") MultipartFile file) { try { // 校验文件是否为空 if (file.isEmpty()) { return AjaxResult.error("请选择要上传的视频"); } // 校验文件类型 String contentType = file.getContentType(); if (!isVideoContentType(contentType)) { return AjaxResult.error("不支持的文件类型"); } // 校验文件大小 long size = file.getSize(); if (size > 5 * 1024 * 1024 * 1024L) { // 5GB return AjaxResult.error("文件大小不能超过5GB"); } // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String fileExt = FilenameUtils.getExtension(originalFilename); String newFilename = UUID.randomUUID() + "." + fileExt; // 获取上传路径 String uploadDir = RuoYiConfig.getUploadPath(); Path uploadPath = Paths.get(uploadDir); // 创建上传目录(如果不存在) if (!Files.exists(uploadPath)) { Files.createDirectories(uploadPath); } // 保存文件 Path filePath = uploadPath.resolve(newFilename); file.transferTo(filePath.toFile()); // 构造返回结果 String accessUrl = ServletUtils.getRequest().getScheme() + "://" + ServletUtils.getRequest().getServerName() + ":" + ServletUtils.getRequest().getServerPort() + "/profile/upload/" + newFilename; Map<String, String> result = new HashMap<>(); result.put("filename", newFilename); result.put("url", accessUrl); result.put("size", String.valueOf(size)); return AjaxResult.success("上传成功", result); } catch (IOException e) { log.error("视频上传失败", e); return AjaxResult.error("上传失败:" + e.getMessage()); } } private boolean isVideoContentType(String contentType) { return contentType != null && (contentType.startsWith("video/") || contentType.equals("application/octet-stream")); } }

这个基础版本已经实现了:

  1. 文件非空校验
  2. 文件类型校验(通过Content-Type)
  3. 文件大小限制
  4. 唯一文件名生成(避免冲突)
  5. 自动创建上传目录
  6. 文件保存到指定位置
  7. 返回可访问的URL

2.2 文件存储路径配置优化

在实际项目中,我们需要更灵活地配置文件存储路径。可以在RuoYiConfig中添加相关配置:

public class RuoYiConfig { // ...其他配置 /** * 获取上传路径 */ public static String getUploadPath() { return getProfile() + "/upload"; } /** * 获取视频上传路径 */ public static String getVideoUploadPath() { return getProfile() + "/upload/videos"; } /** * 获取资源映射路径 */ public static String getResourcePrefix() { return "/profile"; } }

然后在application.yml中添加配置:

# 文件路径配置 file: # 上传路径 profile: D:/ruoyi/uploadPath # 资源映射路径 resource-prefix: /profile

这样配置的好处是:

  1. 不同环境可以轻松修改上传路径
  2. 不同类型的文件可以分开存储
  3. 资源映射路径可配置,便于部署

2.3 文件重命名策略

为了避免文件名冲突和提高安全性,我们需要一个好的文件重命名策略。除了使用UUID,还可以考虑:

public class FileNameUtils { /** * 生成带日期路径的文件名 */ public static String generateDatePathFilename(String originalFilename) { // 文件扩展名 String extension = FilenameUtils.getExtension(originalFilename); // 日期路径 String datePath = DateUtils.datePath(); // UUID文件名 String uuid = UUID.randomUUID().toString().replace("-", ""); return datePath + "/" + uuid + "." + extension; } /** * 获取文件扩展名 */ public static String getFileExtension(String filename) { return filename.substring(filename.lastIndexOf(".") + 1); } }

然后在控制器中使用:

String newFilename = FileNameUtils.generateDatePathFilename(originalFilename);

这样生成的文件名会包含日期路径,如:"2023/08/15/550e8400e29b41d4a716446655440000.mp4",具有以下优点:

  1. 按日期自动分类存储
  2. 避免单目录文件过多
  3. 文件名仍然保持唯一性

3. 前后端联调与优化

3.1 跨域问题解决

在前后端分离架构中,跨域是常见问题。RuoYi-Plus已经配置了全局跨域支持,通常不需要额外处理。但如果遇到问题,可以检查以下配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .maxAge(3600); } }

对于文件上传这种特殊请求,还需要确保:

  1. 请求头中包含Authorization
  2. Content-Type是multipart/form-data
  3. 不要忘记OPTIONS方法的支持

3.2 上传进度显示优化

前端进度显示可以更加精细化,区分上传进度和处理进度:

const handleProgress = (event) => { // 上传进度占80%(假设后端处理占20%) progressPercent.value = Math.floor(event.percent * 0.8) } const handleSuccess = (response) => { // 上传完成,开始处理阶段 progressPercent.value = 80 // 模拟处理进度 const timer = setInterval(() => { progressPercent.value += 2 if (progressPercent.value >= 100) { clearInterval(timer) showProgress.value = false videoUrl.value = response.data.url ElMessage.success('视频上传并处理成功') } }, 100) }

这种优化让用户感知到上传完成后服务器仍在处理文件,体验更好。

3.3 断点续传实现

对于大文件上传,实现断点续传可以提升用户体验。前端需要修改:

const beforeUpload = (file) => { // 生成文件唯一hash return new Promise((resolve) => { const reader = new FileReader() reader.readAsArrayBuffer(file) reader.onload = () => { const spark = new SparkMD5.ArrayBuffer() spark.append(reader.result) const hash = spark.end() file.uniqueHash = hash resolve(true) } }) }

后端需要增加接口:

@PostMapping("/checkChunk") public AjaxResult checkChunk(@RequestParam String hash, @RequestParam String filename) { String uploadDir = RuoYiConfig.getUploadPath(); String chunkDir = uploadDir + "/chunks/" + hash; File chunkFolder = new File(chunkDir); if (!chunkFolder.exists()) { return AjaxResult.success("可以上传", Map.of("exist", false)); } File[] chunks = chunkFolder.listFiles(); return AjaxResult.success("已存在分片", Map.of( "exist", true, "uploaded", Arrays.stream(chunks) .map(f -> Integer.parseInt(f.getName())) .collect(Collectors.toList()) )); }

完整实现断点续传需要:

  1. 前端文件分片
  2. 分片上传接口
  3. 分片合并接口
  4. 分片校验接口

4. 生产环境优化建议

4.1 文件存储方案

本地存储简单但存在局限性,生产环境建议考虑:

  1. 分布式文件系统:如FastDFS、MinIO
  2. 云存储服务:阿里云OSS、腾讯云COS
  3. CDN加速:对视频文件特别重要

以MinIO为例的配置:

@Configuration public class MinIOConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.accessKey}") private String accessKey; @Value("${minio.secretKey}") private String secretKey; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }

4.2 视频处理能力

上传后通常需要:

  1. 生成缩略图
  2. 转码为通用格式
  3. 提取元数据

可以使用FFmpeg集成:

public class VideoUtils { public static void generateThumbnail(String videoPath, String thumbnailPath) throws IOException, InterruptedException { String cmd = String.format("ffmpeg -i %s -ss 00:00:01 -vframes 1 %s", videoPath, thumbnailPath); Process process = Runtime.getRuntime().exec(cmd); process.waitFor(); } }

4.3 安全防护措施

  1. 文件类型校验:不要仅依赖扩展名
  2. 病毒扫描:集成ClamAV等工具
  3. 权限控制:细粒度的访问权限
  4. 日志审计:记录所有上传下载操作
public boolean isSafeFile(MultipartFile file) { try { // 校验魔数 byte[] bytes = file.getBytes(); if (bytes.length < 4) return false; // 常见视频文件魔数 Set<String> videoMagicNumbers = Set.of( "00000020", // MP4 "464C5601", // FLV "52494646" // AVI ); String magic = bytesToHex(Arrays.copyOfRange(bytes, 0, 4)); return videoMagicNumbers.contains(magic); } catch (IOException e) { return false; } }
http://www.jsqmd.com/news/647300/

相关文章:

  • STM32F4串口烧录实战:FlyMCU高效配置指南
  • 从一道CTF题看Python原型链污染:手把手教你用Flask靶场复现DSACTF EzFlask漏洞
  • LeetCode刷题 day10
  • ONNX模型转换实战:从PyTorch到TensorRT的完整优化指南
  • Ubuntu 20.04离线环境下的NFS服务部署与配置指南
  • OpenHarmony-L2开发全流程实战指南:从源码到应用部署
  • Git冷命令拯救崩溃现场:从灾难到重生的终极指南
  • 【生成式AI架构设计黄金法则】:20年架构师亲授5大避坑指南与3套可落地的高可用方案
  • ESP8266+Tasmota智能电表DIY:从硬件选型到Home Assistant接入全流程(附避坑指南)
  • 用Matlab搞定偏微分方程数值解:从Poisson方程五点差分到Gauss-Seidel迭代的保姆级实战
  • OpenCV形态学处理实战:用C++手搓腐蚀膨胀算法,对比库函数效果
  • 智能问数大模型调用的4种部署方式
  • 国民技术 N32WB031KEQ6-2 QFN-32 蓝牙模块
  • 招生数据看不明白?大数据分析让智慧招生平台帮你理清思路
  • 网吧 / 营业厅实名核验更严了,帮你合规
  • 3分钟搞定PDF找茬:diff-pdf视觉对比神器完全指南
  • 基于COMSOL的BIC本征态计算通用算法:直观出图,适用于多种场景,附论文研究链接
  • XXL-JOB调度中心集群部署实战:从编译到反向代理全流程解析
  • 如何快速掌握ESP-CSI技术:无线感知的完整入门指南
  • 【生死心法】别用 assert() 谋杀物理世界!撕碎软件异常的“停机幻觉”,论“失效安全”与硬件级绝对熔断
  • Cursor+Apifox MCP Server:智能接口自动化测试的实践与突破
  • ThreeJS实战:如何优雅地给3D模型添加点击弹窗(附完整代码)
  • Win10 LTSC 1809(Hyper-V)环境下Docker与CVAT的兼容性部署指南
  • Node.js 日志选型指南:Winston vs Log4js 全方位对比与实战
  • 揭秘Stable Diffusion 3.5企业级部署瓶颈:3类GPU资源浪费模式及实时优化方案
  • 人工智能技术生成对抗网络图像合成与风格迁移应用
  • 给Pixel4注入新灵魂:手把手教你定制Android 12内核,开启隐藏功能与性能调优
  • JavaScript对象、原型与继承知识体系综合实战案例
  • 西门子S7-1200 PLC与Node-RED数据互通实战:从硬件接线到Web可视化(V18+TIA Portal)
  • 利用Emacs verilog-mode的AUTOINST与AUTOWIRE加速Verilog模块集成