📢📢📢📣📣📣
哈喽!大家好,我是【Bug 终结者】 ,【CSDN新星创作者】🏆,阿里云技术博主🏆,51CTO人气博主🏆,INfoQ写作专家🏆
一位上进心十足,拥有极强学习力的【Java领域博主】😜😜😜
🏅【Bug 终结者】博客的领域是【面向后端技术】的学习,未来会持续更新更多的【后端技术】以及【学习心得】。 偶尔会分享些前端基础知识,会更新实战项目,面向企业级开发应用!
🏅 如果有对【后端技术】、【前端领域】感兴趣的【小可爱】,欢迎关注【Bug 终结者】💞💞💞❤️❤️❤️ 感谢各位大可爱小可爱! ❤️❤️❤️
文章目录
- 一、项目概述
- 二、需求说明
- 三、数据表
- 五、难点分析
-
- 🚨前端难点
- 🚨后端难点
- 六、跨域实现
-
- 🚡本地跨域
- 🚡Linux阿里云服务器跨域
- 七、本地测试
- 八、将项目部署上线至Linux服务器
-
- 📁上传目录
- 📍编译前端项目
- ✅测试访问前端页面
- 📌编译后端项目
- ✳️执行脚本启动程序
- 九、线上效果图
- ⚠️问题处理
- 📜项目源码获取
- ⛵小结
一、项目概述
本项目是一个视频管理系统,用户分为两类:管理员、学生
管理员登录后可以新增、修改、观看视频、搜索,普通用户登录可以观看视频,搜索
项目技术栈:Spring Boot、Spring Security、MySQL、MyBatis、Vue2、ElementUI
用户类别 | 管理员 | 权限 |
---|---|---|
管理员 | 新增、修改、观看、搜索视频 | admin |
普通用户(学生) | 观看、搜索视频 | student |
本项目基于 前后端分离 – SpringBoot + Vue实战项目 部署至阿里云服务器
二、需求说明
视频管理系统,使用SpringBoot + Vue 实现视频的 增删改查,分页,多条件搜索(根据视频标题查询)
-
上传视频格式必须为 yyyy-MM-dd/hhmmssxxxx.mp4 (例如:2022/04/24/2012333987.mp4) xxxx:随机的四位数字
-
上传视频采用 el-upload 手动上传实现,先将视频名称与日期存入数据表并返回id,携带返回的id再次调用上传视频方法
-
视频上传后需要截取上传视频的第一帧图片为封面图存入数据表内(思路:使用FFmpeg实现截取视频的封面图操作)
-
截取视频的封面图存储必须和
存储视频的格式一样
-
跨域采用Nginx反向代理实现,不依赖于tomcat
-
上传视频后点击观看视频按钮即可跳转至视频页,跳转后自动播放视频,视频页提供返回视频列表按钮
-
视频列表页
-
播放视频列表页
三、数据表
t_video_info
CREATE TABLE `t_video_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id',
`video_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '视频名称',
`video_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '视频路径',
`video_png` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图路径',
`create_time` datetime DEFAULT NULL COMMENT '上传日期',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '最后修改日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
五、难点分析
🚨前端难点
Vue视频封面图二次加载问题
这个项目前端难点主要的难点就是前端上传文件后会出现封面图加载失败的问题,是由于手动上传未获取到封面图地址,所以我们调用全局方法重新渲染页面,从而达到封面图正确显示的效果
这种方式我之前在我的博客中讲过,那就是 Vue的 provide / inject组合控制的显示
因为上传成功后需要再次刷新,如果不刷新,图片不出来,那就要重新刷新页面,但刷新页面对用户来说极不友好,而且时间慢,效率低,所以采用该显示方式,完美解决了二次加载的问题
具体可看前后端分离 – 深入浅出 Spring Boot + Vue + ElementUI 实现相册管理系统【文件上传 分页 】 文件上传也不过如此~
注意:
上传文件后加载封面图代码以及文件上传后的视频获取如下
Vue代码
<el-form-item label="上传视频">
<el-upload
class="upload-demo"
ref="upload"
:action="uploadUrl"
:data="uploadDataParam"
:before-upload="checkFileType"
accept=".mp4"
:auto-upload="false">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过300M</div>
</el-upload>
</el-form-item>
方法实现
//前置判断,上传的文件大小是否符合要求,最大限制300MB
checkFileType(file) {
let imgSize = file.size / 1024 / 1024;
console.log("文件类型:"+file.type);
console.log("文件大小:" + imgSize)
if (imgSize > 300) {
this.$message("文件超出规定上传大小,请重新上传文件!")
return false;
}
},
//保存视频文件
clkBtnSave() {
//非空判断
if (this.videoInfo.video_name == null || this.videoInfo.create_time == null) {
this.$message.warning("视频名称或上传日期不允许为空!")
return;
}
console.log(this.videoInfo)
let url = this.settings.apiUrl + "/video_info/save";
let data = this.videoInfo;
//先将video信息存储,并返回id,通过该id去修改实现文件上传
request.post(url, data).then((res) => {
if (res.code === 0) {
this.uploadDataParam.id = res.data.id;
//手动上传,提交表单上传
this.submitUpload();
}
//上传完成后执行加载视频列表的方法
}).finally(() => {
this.sleep(500)
this.showVideoInfoDigLog = false;
this.$message("保存成功~");
this.reload();
})
},
sleep(ms) { //sleep延迟方法2
var unixtime_ms = new Date().getTime();
while(new Date().getTime() < unixtime_ms + ms) {}
},
//保存表单提交
submitUpload() {
this.$refs.upload.submit();
},
加载封面图地址跨域加载
通过Nginx反向代理 windows/linux 文件地址,在浏览器测试可访问即可
前端我们通过 el-image 标签来展示封面图
跨域具体实现在下方详细讲解,这里大概讲解思路
🚨后端难点
Utils工具准备
PbFileUtils 文件上传工具类
该工具类主要为将文件新建到指定位置,File类新建指定文件夹及文件格式,通过transferTo 方法复制文件到指定位置
package com.wanshi.utils;
import com.wanshi.config.GlobalSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 文件上传类
*/
public class PbFileUtils {
/**
* 文件上传,
* @param file 上传文件
* @param uploadPath 要上传到的路径
* @return
* @throws IOException
*/
public static String upload(MultipartFile file, String uploadPath) throws IOException {
String finalFileName = "";
if (file.getSize() > 0) {
String originalFilename = file.getOriginalFilename();
//获取源文件的后缀名
String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
//获取年月
SimpleDateFormat sdfYyyyMMdd = new SimpleDateFormat("yyyy-MM");
finalFileName = sdfYyyyMMdd.format(new Date()) + "/";
//获取日
SimpleDateFormat sdfdd = new SimpleDateFormat("dd");
finalFileName += sdfdd.format(new Date()) + "/";
//获取时分秒
SimpleDateFormat sdfHHmmss = new SimpleDateFormat("HHmmss");
finalFileName += sdfHHmmss.format(new Date());
//生成4位随机数字
Integer rndNum = new Random().nextInt(1000)+9000;
//拼接随机数字和后缀名
finalFileName += rndNum + extName;
//目标文件
File f1 = new File(uploadPath+finalFileName);
//若文件夹不存在,则递归创建,全部创建文件夹
if (!f1.exists()) {
f1.mkdirs();
}
//开始上传
file.transferTo(f1);
}
return finalFileName;
}
}
PbFFmpegUtils 视频封面截取工具类
该类的主要作用就是截取指定视频文件的第一帧作为封面图并按照规定的格式写入指定文件夹
package com.wanshi.utils;
import com.wanshi.config.GlobalSet;
import org.apache.commons.lang3.StringUtils;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class PbFFmpegUtils {
/**
* @Description: 获取视频截图
*/
public static Map<String, Object> getScreenshot(String upload_prefix, String filePath) throws Exception{
System.out.println("截取视频截图开始:"+ System.currentTimeMillis());
Map<String, Object> result = new HashMap<String, Object>();
filePath = upload_prefix + filePath;
FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(filePath);
grabber.start();
//设置视频截取帧(默认取第一帧)
Frame frame = grabber.grabImage();
//视频旋转度
String rotate = grabber.getVideoMetadata("rotate");
Java2DFrameConverter converter = new Java2DFrameConverter();
//绘制图片
BufferedImage bi = converter.getBufferedImage(frame);
if (rotate != null) {
// 旋转图片
bi = rotate(bi, Integer.parseInt(rotate));
}
//图片的类型
String imageMat = "jpg";
//获取年月
SimpleDateFormat sdfYyyyMMdd = new SimpleDateFormat("yyyy-MM");
String targetFileName = sdfYyyyMMdd.format(new Date()) + "/";
//获取日
SimpleDateFormat sdfdd = new SimpleDateFormat("dd");
targetFileName += sdfdd.format(new Date()) + "/";
//获取时分秒
SimpleDateFormat sdfHHmmss = new SimpleDateFormat("HHmmss");
targetFileName += sdfHHmmss.format(new Date());
//生成4位随机数字
Integer rndNum = new Random().nextInt(1000)+9000;
targetFileName += rndNum + "." + imageMat;
//图片的完整路径
String imagePath = upload_prefix + File.separator + targetFileName;
//创建文件
File output = new File(imagePath);
if (!output.exists()) {
output.mkdirs();
}
ImageIO.write(bi, imageMat, output);
//拼接Map信息
result.put("videoWide", bi.getWidth());
result.put("videoHigh", bi.getHeight());
long duration = grabber.getLengthInTime() / (1000 * 1000);
result.put("rotate", StringUtils.isBlank(rotate)? "0" : rotate);
result.put("format", grabber.getFormat());
result.put("imgPath", targetFileName);
System.out.println("视频的宽:" + bi.getWidth());
System.out.println("视频的高:" + bi.getHeight());
System.out.println("视频的旋转度:" + rotate);
System.out.println("视频的格式:" + grabber.getFormat());
System.out.println("此视频时长(s/秒):" + duration);
grabber.stop();
System.out.println("截取视频截图结束:"+ System.currentTimeMillis());
return result;
}
/**
* @Description: 根据视频旋转度来调整图片
* @param src
* @param angel 视频旋转度
* @return BufferedImage
*/
public static BufferedImage rotate(BufferedImage src, int angel) {
int src_width = src.getWidth(null);
int src_height = src.getHeight(null);
int type = src.getColorModel().getTransparency();
Rectangle rect_des = calcRotatedSize(new Rectangle(new Dimension(src_width, src_height)), angel);
BufferedImage bi = new BufferedImage(rect_des.width, rect_des.height, type);
Graphics2D g2 = bi.createGraphics();
g2.translate((rect_des.width - src_width) / 2, (rect_des.height - src_height) / 2);
g2.rotate(Math.toRadians(angel), src_width / 2, src_height / 2);
g2.drawImage(src, 0, 0, null);
g2.dispose();
return bi;
}
/**
* @Description: 计算图片旋转大小
* @param src
* @param angel
* @return Rectangle
*/
public static Rectangle calcRotatedSize(Rectangle src, int angel) {
if (angel >= 90) {
if (angel / 90 % 2 == 1) {
int temp = src.height;
src.height = src.width;
src.width = temp;
}
angel = angel % 90;
}
double r = Math.sqrt(src.height * src.height + src.width * src.width) / 2;
double len = 2 * Math.sin(Math.toRadians(angel) / 2) * r;
double angel_alpha = (Math.PI - Math.toRadians(angel)) / 2;
double angel_dalta_width = Math.atan((double) src.height / src.width);
double angel_dalta_height = Math.atan((double) src.width / src.height);
int len_dalta_width = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_width));
int len_dalta_height = (int) (len * Math.cos(Math.PI - angel_alpha - angel_dalta_height));
int des_width = src.width + len_dalta_width * 2;
int des_height = src.height + len_dalta_height * 2;
return new Rectangle(new Dimension(des_width, des_height));
}
}
视频文件上传格式控制
视频上传文件格式规定为
yyyy/MM/dd/hhmmssxxxx.mp4
// 年/月/日/时分秒毫秒随机4为数字.mp4
//例如:2022/4/24/200002303987.mp4
文件大小不能超过300MB
application.yml约束
servlet:
# 文件上传约束大小
multipart:
# 支持 multipart 上传文件
enabled: true
# 最大上传文件大小 300MB
max-file-size: 300MB
# 最大请求大小 300MB
max-request-size: 300MB
视频文件上传后需要立刻通过FFmpeg截取当前上传视频的第一帧作为封面图并按照指定格式写入指定位置
上传完成后我们传入文件工具类返回的文件地址并拼接为绝对路径调用FFmpegUtils工具类方法进行截取图片
VideoInfoService 业务处理类
//上传视频文件并返回相对路径地址
String fileName = PbFileUtils.upload(file, globalSet.getUpload_root_path());
//判断传过来的id非空,因为我们是手动上传,所以上传文件时需要携带刚刚保存的数据id,然后根据id进行修改数据
if (!StringUtils.isEmpty((String)map.get("id"))) {
//生成视频封面图,传入参数
Map<String, Object> screenshot = PbFFmpegUtils.getScreenshot(globalSet.getUpload_root_path(), fileName);
VideoInfo videoInfo = new VideoInfo();
videoInfo.setId(Integer.valueOf((String) map.get("id")));
videoInfo.setVideo_path(fileName);
videoInfo.setVideo_png((String) screenshot.get("imgPath"));
videoInfoMapper.update(videoInfo);
}
Vue实现播放视频
下载 vue-video-player控件
npm install vue-video-player --save
新建Vue页面引入video.js并实现播放视频
VideoShow.vue
<template>
<div class="box">
<div class="title">
<el-button @click="clkBtnBack" type="warning" style="cursor:pointer">返回视频列表</el-button>
<span style="color:#3a8ee6; font-size: 25px;margin: 10px;">当前正在播放视频 <b>{{video.video_name}}</b></span>
</div>
<div class="box-video">
<videoPlayer class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
>
</videoPlayer>
</div>
</div>
</template>
<script>
import request from '@/common/utils/request';
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
export default {
components:{
videoPlayer
},
data(){
return{
// showFileUrl: 'http://localhost:8080/pics/',
showFileUrl: 'http://39.105.13.178:8369/images/',
video: {},
playerOptions : {
autoplay: true, //如果true,浏览器准备好时开始回放。
muted: false, // 是否静音。
loop: false, // 是否循环播放。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{
type: "video/mp4",//mp4格式视频,若为m3u8格式,type需设置为 application/x-mpegURL
src: '',//url地址
}],
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true,
durationDisplay: true,
remainingTimeDisplay: false,
fullscreenToggle: true //是否显示全屏按钮
}
},
}
},
watch: {
$route: {
handler: function (to, from) {
if (to.path == '/video-show') {
this.getVideo();
}
}, immediate: true
},
},
methods: {
getVideo() {
let url = this.settings.apiUrl + "/video_info/get";
let d1r = {id: this.$route.query.id};
console.log(d1r)
request.post(url, d1r).then((res) => {
if (res.code === 0) {
const url = this.showFileUrl + res.data.video_path;
this.playerOptions['sources'][0]['src'] = url;
this.video = res.data;
}
})
},
clkBtnBack() {
this.$router.push({
path: 'video-list'
})
}
},
}
</script>
<style>
.box{
margin: 1% 15%;
}
.title{
height: 70px;
}
box-video {
margin: 10% 10%;
}
.video-js .vjs-big-play-button{
margin-left:43%;
margin-top: 25%;
}
</style>
成功实现文件上传以及封面图截取~
六、跨域实现
首先我们了解什么叫做跨域,跨域就是两个程序不在一个ip地址上,调用不到后端接口数据,所以需要跨域调用!
那么实现跨域的方式有多少种呢?
共有9种,JSONP、CORS、PostMessage、WebSocket、Node中间件代理(两次跨域)、nginx反向代理、window.name + iframe、location.hash + iframe、document.domain + iframe
本文主要讲解Nginx反向代理来解决图片的跨域访问,后端使用了CORS解决跨域
🚡本地跨域
本地安装Nginx,并配置conf/nginx.conf文件
Nginx官网
下载windows版本直接解压缩即可
下载完成后打开文件夹,conf/nginx.conf配置文件
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#gzip on;
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
# 解决跨域访问图片以及视频
location /pics/ {
# alias和root有区别
# alias会去掉前置的路径,而root不会去除
alias D:/upload/;
}
}
}
双击nginx.exe 启动程序,一闪而过,代表启动
上传到 D:/upload/一张照片,测试访问
浏览器访问测试
成功访问
🚡Linux阿里云服务器跨域
Linux上安装Nginx可看3分钟搞懂阿里云服务器安装Nginx并配置静态访问页面
进入 usr/local/nginx/conf/nginx.conf 修改文件内容,配置跨域
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8369;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /work/class_manager/pc/dist;
index index.html index.htm;
#开启后不会导致刷新白屏
try_files $uri $uri/ /index.html;
}
# 接口访问路径
location /springbootajax/ {
proxy_pass http://39.105.13.178:8345/springbootajax;
}
# 代理视频以及图片访问
location /images {
rewrite ^/images/(.*)$ /$1 break;
root /work/class_manager/upload;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
注意:修改配置文件内容必须重新加载nginx
上传到Linux一张图片
通过Filezilla上传文件
测试访问
七、本地测试
启动后端和前端程序,并测试
本地测试无误,完美~
八、将项目部署上线至Linux服务器
📁上传目录
上传目录我们依旧上传至 /work/class_manager/下
📍编译前端项目
npm run build 编译前端项目
编译完成后生成dist文件夹,我们使用FileZilla上传文件至指定目录
✅测试访问前端页面
前面已经将跨域配置完毕,我们直接访问即可
📌编译后端项目
进入cmd使用mvn clean package 打包,生成zip文件,我们解压zip文件并上传至目录
使用filezilla上传文件
✳️执行脚本启动程序
上传成功后我们先停止运行着的java程序,然后启动程序
# 停止程序脚本
./stop.sh
# 启动程序脚本
./start.sh
九、线上效果图
SpringBoot + Vue 实现视频上传
⚠️问题处理
若线上出现问题我们应该怎么办?
查看控制台日志
首先毋庸置疑我们肯定会去查看日志的,我们采用脚本启动的方式直接看logs.txt 则可以看出日志信息,例如空指针一类的,也可以去查看日志
除了这种方式,我们也可以去 .logs文件夹看 日志
log4j自动生成的日志文件
查看Java程序的堆栈信息
# 找出当前运行的程序对应的pid
ps -ef |grep java
#查看堆栈信息指令的参数
jinfo --help
# 查看指定java程序通过pid查询堆栈信息
jinfo -flags pid
📜项目源码获取
SpringBoot + Vue 实现视频管理系统 实战项目
前端代码获取:
https://gitcode.net/weixin_45526437/class-manager-system-pc
后端代码获取
https://gitcode.net/weixin_45526437/class-manager-system-svr
使用Git爬取源码
使用Git爬取GitEE、GitLab、GitCode、GitHub源码
⛵小结
以上就是【Bug 终结者】对前后端分离 – Spring Boot + Vue实现视频管理系统 并部署阿里云服务器简单的概述,本案例主要实现了前后端分离项目线上环境的部署,处理线上问题、文件上传,截取封面图、跨域问题,增加自己的实战经验,本案例为真实企业项目部署案例,实战增加自己的经验,技术在手,天下我有~
如果这篇【文章】有帮助到你,希望可以给【Bug 终结者】点个赞👍,创作不易,如果有对【后端技术】、【前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【Bug 终结者】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💝💝💝!
转载请注明:前后端分离 -- Spring Boot + Vue实现视频管理系统 并部署阿里云服务器 | 胖虎的工具箱-编程导航