SpringBoot+Vue3全栈实战:从零搭建视频点播网站
1. 项目概述与核心价值最近有不少朋友在后台私信想了解如何从零开始搭建一个功能完整的视频播放网站。这确实是一个挺有意思的练手项目它几乎涵盖了现代Web开发中前后端分离架构的所有核心知识点。我自己也基于SpringBoot和Vue3完整地走了一遍这个流程踩了不少坑也积累了一些心得。今天我就把这个项目的实现思路、技术选型、关键代码以及那些“教科书上不会写”的实操细节系统地分享出来。这个项目本质上是一个小型的在线视频点播平台。用户可以在前端页面浏览视频列表、搜索视频、观看视频支持进度条拖拽、清晰度切换等而后台管理员则能上传视频、管理视频信息。听起来简单但里面涉及的技术栈相当立体后端需要处理视频文件上传、存储、转码、生成播放信息接口前端要构建流畅的播放器交互前后端之间还要考虑跨域、鉴权、大文件上传断点续传等问题。选择SpringBoot和Vue3这对组合是因为它们分别代表了当前Java后端和JavaScript前端领域最主流、最成熟的解决方案生态丰富社区活跃能让我们把精力更多地集中在业务逻辑本身而不是框架的疑难杂症上。2. 技术栈选型与项目架构设计2.1 后端技术栈深度解析后端我们选用SpringBoot 3.x。它最大的好处就是“开箱即用”通过Starter依赖能快速集成我们需要的几乎所有组件。对于视频网站以下几个依赖是核心Spring Web Spring MVC: 提供RESTful API的基础框架。Spring Data JPA / MyBatis-Plus: 用于数据持久化。我个人更倾向于MyBatis-Plus它在MyBatis的基础上做了大量增强单表CRUD几乎不用写SQL但对于复杂联表查询又保留了原生MyBatis的灵活性。视频表、用户表、分类表之间的关联查询用它很合适。Spring Security JWT: 处理用户认证与授权。这是项目的安全基石。我们采用无状态的JWTJSON Web Token方案用户登录后服务器生成一个Token返回给前端前端后续请求都在Header中携带此Token。这样后端服务本身就不需要维护Session更易于扩展。Spring Boot DevTools: 开发热重启提升效率。数据库驱动: 根据你选的数据库来MySQL 8.x或PostgreSQL 14都是不错的选择。第三方库:Hutool: 国产工具类库处理日期、加密、IO等操作非常方便。Lombok: 通过注解自动生成Getter/Setter、构造方法等让实体类代码更简洁。JJWT: 用于生成和解析JWT Token。注意在SpringBoot 3.x中Jakarta EE 9是默认标准所以引入相关依赖时如Servlet API、JPA要注意版本避免与旧的javax包冲突。2.2 前端技术栈构建思路前端我们使用Vue 3的组合式APIComposition API和script setup语法配合Vite构建工具开发体验非常流畅。核心框架: Vue 3。相比于Vue 2其组合式API在逻辑复用和组织复杂组件时优势明显。构建工具: Vite。启动速度和热更新速度远超Webpack对于开发阶段体验提升巨大。UI组件库: Element Plus。它是基于Vue 3的桌面端组件库成熟、美观、文档齐全。我们的后台管理系统界面可以大量使用它的表格、表单、弹窗等组件快速搭建。路由: Vue Router 4。状态管理: Pinia。这是Vue官方推荐的新一代状态管理库比Vuex更简洁TypeScript支持更好。我们可以用它来管理用户登录状态、播放历史等全局数据。HTTP客户端: Axios。用于发起对后端API的请求配合拦截器Interceptor可以统一处理Token添加、错误响应等。视频播放器: 这是前端核心。经过对比我选择了video.js配合videojs-contrib-hls插件。原因如下video.js功能强大UI可定制性高社区插件丰富。对于HLS.m3u8格式的视频流支持非常好这是目前网络视频播放的主流格式能实现自适应码率和流畅播放。虽然Vue也有像vue-video-player这样的封装但直接使用video.js能获得更底层的控制权方便处理一些高级需求比如自定义控制栏、插播广告等。2.3 前后端分离架构与部署考量我们采用经典的前后端分离架构。前端Vue项目通过Vite打包后生成静态资源HTML, JS, CSS, 图片等。后端SpringBoot项目提供REST API。生产环境部署有两种常见模式完全分离部署前端静态资源部署在Nginx或CDN上后端Java应用部署在Tomcat或直接使用SpringBoot内嵌容器。这种模式前后端完全解耦可以独立伸缩。合并部署将Vite打包后的dist目录下的静态文件复制到SpringBoot项目的src/main/resources/static目录下。SpringBoot应用同时充当API服务器和静态资源服务器。这种模式部署简单适合小型项目或个人项目。我这次分享的示例将采用第二种“合并部署”模式因为它更易于演示和本地运行。但在实际生产环境中尤其是稍有规模的项目我强烈推荐第一种“完全分离部署”。3. 后端核心功能实现详解3.1 数据库设计与实体类映射视频网站的核心数据表并不复杂但设计时需要考虑扩展性。以下是核心表结构-- 视频信息表 CREATE TABLE video ( id bigint PRIMARY KEY AUTO_INCREMENT COMMENT 主键, title varchar(255) NOT NULL COMMENT 视频标题, description text COMMENT 视频描述, cover_url varchar(500) COMMENT 封面图URL, video_url varchar(500) COMMENT 原始视频文件存储路径如OSS的Key, m3u8_url varchar(500) COMMENT 转码后的HLS索引文件(.m3u8)路径, duration int DEFAULT 0 COMMENT 视频时长秒, status tinyint DEFAULT 1 COMMENT 状态0:转码中, 1:正常, 2:下架, category_id bigint COMMENT 分类ID, user_id bigint COMMENT 上传用户ID, view_count bigint DEFAULT 0 COMMENT 播放次数, like_count bigint DEFAULT 0 COMMENT 点赞数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT视频信息表; -- 视频分类表 CREATE TABLE category ( id bigint PRIMARY KEY AUTO_INCREMENT, name varchar(100) NOT NULL, parent_id bigint DEFAULT 0 COMMENT 父级ID用于多级分类, sort int DEFAULT 0 COMMENT 排序, create_time datetime DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表简化版 CREATE TABLE user ( id bigint PRIMARY KEY AUTO_INCREMENT, username varchar(50) UNIQUE NOT NULL, password varchar(255) NOT NULL COMMENT 加密后的密码, nickname varchar(100), avatar varchar(500), role varchar(20) DEFAULT USER COMMENT 角色ADMIN, USER, create_time datetime DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;对应的在SpringBoot项目中我们使用MyBatis-Plus的注解来定义实体类。以Video实体为例import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(video) public class Video { TableId(type IdType.AUTO) private Long id; private String title; private String description; private String coverUrl; private String videoUrl; // 原始文件路径 private String m3u8Url; // HLS文件路径 private Integer duration; private Integer status; private Long categoryId; private Long userId; private Long viewCount; private Long likeCount; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; // 非数据库字段用于关联查询 TableField(exist false) private String categoryName; TableField(exist false) private String uploaderName; }这里我使用了TableField(fill FieldFill.INSERT)等注解配合MyBatis-Plus的元数据处理器MetaObjectHandler可以自动填充创建和更新时间避免手动设置。3.2 视频上传与云存储集成直接让用户上传视频到应用服务器是极不推荐的这会给服务器带来巨大的磁盘I/O和带宽压力。主流做法是集成对象存储服务如阿里云OSS、腾讯云COS、七牛云Kodo等。它们提供高可用、高扩展、低成本的存储并且自带CDN加速。以阿里云OSS为例集成步骤引入OSS的Java SDK依赖。在配置文件中配置OSS的Endpoint、AccessKey、Bucket名称等信息。创建一个FileStorageService服务类封装上传、删除、生成签名URL等方法。核心上传逻辑Service public class OssStorageService implements FileStorageService { Value(${oss.bucket-name}) private String bucketName; Autowired private OSS ossClient; Override public String upload(InputStream inputStream, String fileName) { // 生成一个在OSS中唯一的对象键Key避免文件名冲突 // 常用格式video/{yyyy}/{MM}/{dd}/{uuid}_{originalFileName} String key generateFileKey(fileName); try { // 创建上传对象的元信息 ObjectMetadata metadata new ObjectMetadata(); metadata.setContentType(getContentType(fileName)); // 如果是大文件可以考虑使用分片上传UploadFileRequest ossClient.putObject(bucketName, key, inputStream, metadata); // 返回文件的访问URL可以设置过期时间或者直接返回永久URL如果Bucket是公共读 return generateUrl(key); } catch (Exception e) { throw new RuntimeException(文件上传至OSS失败, e); } } private String generateFileKey(String originalFileName) { String uuid UUID.randomUUID().toString().replace(-, ); String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM/dd)); String extension originalFileName.substring(originalFileName.lastIndexOf(.)); return String.format(video/%s/%s%s, datePath, uuid, extension); } }实操心得务必在服务端生成最终的文件存储路径Key永远不要信任客户端上传的文件名这可以防止文件名冲突、路径遍历攻击以及特殊字符导致的问题。同时按日期分目录存储有利于后期管理和迁移。3.3 视频转码与HLS生成服务用户上传的原始视频格式如MP4、MOV和编码参数千差万别为了确保在所有浏览器和设备上都能流畅播放我们需要将视频转码成标准的、支持自适应码率的格式。HLSHTTP Live Streaming是目前最通用的方案。我们不可能在SpringBoot应用内直接进行耗时的视频转码这会阻塞主线程。正确的做法是用户上传视频到OSS后后端记录一条视频记录状态为“转码中”。后端向一个异步任务队列如RabbitMQ、RocketMQ发送一条转码任务消息。一个独立的转码服务Worker监听这个队列获取任务。转码服务从OSS下载原始视频使用FFmpeg进行转码生成多码率的.ts分片文件和.m3u8索引文件。转码完成后将生成的HLS文件上传回OSS或另一个Bucket并回调通知SpringBoot服务更新视频状态和m3u8_url。这里给出一个简化的转码任务消息体设计和FFmpeg命令示例// 转码任务消息 Data public class TranscodeTask { private Long videoId; private String sourceFileUrl; // 原始文件OSS URL private String targetM3u8Key; // 目标m3u8文件在OSS的Key }# 一个基本的FFmpeg命令将输入视频转码为HLS # 生成三种清晰度1080p, 720p, 480p ffmpeg -i input.mp4 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 22 -c:a aac -ar 44100 \ -vf scalew1920:h1080:force_original_aspect_ratiodecrease -b:v 5000k -maxrate 5350k -bufsize 7500k -hls_time 10 -hls_playlist_type vod -hls_segment_filename output_1080p_%03d.ts output_1080p.m3u8 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 23 -c:a aac -ar 44100 \ -vf scalew1280:h720:force_original_aspect_ratiodecrease -b:v 2500k -maxrate 2675k -bufsize 3750k -hls_time 10 -hls_playlist_type vod -hls_segment_filename output_720p_%03d.ts output_720p.m3u8 \ -map 0:v:0 -map 0:a:0 \ -c:v libx264 -crf 23 -c:a aac -ar 44100 \ -vf scalew854:h480:force_original_aspect_ratiodecrease -b:v 1000k -maxrate 1070k -bufsize 1500k -hls_time 10 -hls_playlist_type vod -hls_segment_filename output_480p_%03d.ts output_480p.m3u8注意事项FFmpeg参数调优是个深水区需要根据实际视频内容和目标质量平衡码率、CRF值等。在生产环境转码服务最好部署在带GPU的机器上或者直接使用云厂商提供的视频处理服务如阿里云MPS、腾讯云VOD它们封装了这些复杂操作虽然成本更高但稳定性和效率有保障。3.4 RESTful API设计与实现API设计要遵循RESTful风格清晰易懂。以下是一些核心接口设计模块请求方法路径描述权限视频GET/api/videos分页查询视频列表公开视频GET/api/videos/{id}获取视频详情播放地址公开视频POST/api/videos上传视频信息触发转码管理员视频PUT/api/videos/{id}更新视频信息管理员视频DELETE/api/videos/{id}删除视频管理员分类GET/api/categories获取全部分类树公开用户POST/api/auth/login用户登录公开用户POST/api/auth/register用户注册公开用户GET/api/users/me获取当前用户信息用户在SpringBoot中我们使用RestController和RequestMapping等注解来定义控制器。以视频列表接口为例RestController RequestMapping(/api/videos) public class VideoController { Autowired private VideoService videoService; GetMapping public ResultPageVideoVO listVideos( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Long categoryId) { PageVideo page new Page(pageNum, pageSize); LambdaQueryWrapperVideo queryWrapper new LambdaQueryWrapper(); queryWrapper.like(StringUtils.isNotBlank(keyword), Video::getTitle, keyword) .eq(categoryId ! null categoryId 0, Video::getCategoryId, categoryId) .eq(Video::getStatus, 1) // 只查询正常状态的视频 .orderByDesc(Video::getCreateTime); PageVideo videoPage videoService.page(page, queryWrapper); // 将PageVideo转换为PageVideoVO并填充关联信息如分类名 PageVideoVO voPage videoService.convertToVoPage(videoPage); return Result.success(voPage); } }这里我使用了MyBatis-Plus的Page对象进行分页LambdaQueryWrapper构建查询条件代码非常简洁。返回的Result是一个统一响应包装类包含code、msg、data字段便于前端统一处理。3.5 安全与权限控制实现使用Spring Security JWT进行安全控制是行业标准做法。核心配置步骤如下创建JWT工具类用于生成Token、解析Token、校验Token有效性。实现UserDetailsService从数据库加载用户信息用于认证。配置SecurityFilterChain这是Spring Security 5.7推荐的方式替代了旧的WebSecurityConfigurerAdapter。Configuration EnableWebSecurity public class SecurityConfig { Autowired private JwtTokenProvider jwtTokenProvider; Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 禁用CSRF因为我们是无状态的API使用JWT .csrf().disable() // 设置Session管理为无状态 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 配置请求授权规则 .authorizeHttpRequests(authz - authz // 公开接口 .requestMatchers(/api/auth/**, /api/videos, /api/videos/{id}, /api/categories).permitAll() // 静态资源前端打包后的文件 .requestMatchers(/, /index.html, /assets/**, /vite.svg).permitAll() // 管理员接口 .requestMatchers(/api/videos, /api/videos/**).hasRole(ADMIN) // 其他所有请求都需要认证 .anyRequest().authenticated() ) // 添加我们自定义的JWT过滤器 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) // 异常处理 .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) // 未登录处理 .accessDeniedHandler(new JwtAccessDeniedHandler()); // 权限不足处理 return http.build(); } // 密码编码器使用BCrypt Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }自定义的JwtAuthenticationFilter会拦截请求从Header中取出Token进行验证如果有效则将用户信息设置到Spring Security的上下文中供后续控制器使用。4. 前端核心功能实现详解4.1 项目初始化与基础配置使用Vite快速初始化一个Vue 3 TypeScript项目npm create vuelatest my-video-site # 按照提示选择TypeScript, Vue Router, Pinia, ESLint等 cd my-video-site npm install安装核心依赖npm install element-plus axios pinia video.js videojs-contrib-hls # 安装图标库可选 npm install element-plus/icons-vue配置Vite设置代理解决开发环境跨域// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src) } }, server: { port: 3000, // 前端开发服务器端口 proxy: { /api: { target: http://localhost:8080, // 后端SpringBoot地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })4.2 集成Video.js播放器组件创建一个可复用的视频播放器组件VideoPlayer.vuetemplate div classvideo-player-container div refvideoContainer/div /div /template script setup langts import { ref, onMounted, onUnmounted, watch } from vue import videojs from video.js import video.js/dist/video-js.css import videojs-contrib-hls // 引入HLS插件 interface Props { src: string // m3u8地址 poster?: string // 封面图 options?: any // 额外的video.js配置 } const props withDefaults(definePropsProps(), { options: () ({}) }) const videoContainer refHTMLElement() let player: any null const initPlayer () { if (!videoContainer.value) return // 销毁旧的播放器实例 if (player) { player.dispose() } // 合并默认配置和传入的配置 const videoOptions { controls: true, autoplay: false, preload: auto, fluid: true, // 自适应容器宽度 sources: [{ src: props.src, type: application/x-mpegURL // HLS的MIME类型 }], poster: props.poster, ...props.options } player videojs(videoContainer.value, videoOptions, function onPlayerReady() { console.log(播放器准备就绪) // 可以在这里监听播放器事件 this.on(error, (e: any) { console.error(视频播放错误:, e) }) this.on(ended, () { console.log(视频播放结束) // 可以触发播放完成的后端记录逻辑 }) }) } // 监听src变化重新初始化播放器 watch(() props.src, (newSrc) { if (newSrc) { // 这里可以添加一个延迟确保DOM更新完毕 setTimeout(initPlayer, 100) } }) onMounted(() { if (props.src) { initPlayer() } }) onUnmounted(() { if (player) { player.dispose() } }) /script style scoped .video-player-container { width: 100%; max-width: 1200px; margin: 0 auto; } /style实操心得video.js的初始化必须在DOM元素挂载完成后进行。在Vue的script setup中使用onMounted生命周期钩子来确保这一点。另外当视频源src动态变化时比如切换清晰度需要先销毁旧的播放器实例再创建新的否则可能会产生冲突或内存泄漏。4.3 状态管理与API请求封装使用Pinia创建全局状态管理store。例如创建一个userStore来管理用户登录状态// stores/user.ts import { defineStore } from pinia import { ref, computed } from vue import type { UserInfo } from /types/user import { loginApi, getUserInfoApi, logoutApi } from /api/user export const useUserStore defineStore(user, () { const token refstring | null(localStorage.getItem(token)) const userInfo refUserInfo | null(null) const isLogin computed(() !!token.value) const login async (username: string, password: string) { const res await loginApi({ username, password }) token.value res.data.token userInfo.value res.data.userInfo localStorage.setItem(token, token.value) // 可以在这里将token设置到axios的默认请求头中 } const fetchUserInfo async () { if (!token.value) return try { const res await getUserInfoApi() userInfo.value res.data } catch (error) { // 如果获取用户信息失败如token过期则清空状态 logout() } } const logout () { token.value null userInfo.value null localStorage.removeItem(token) // 清除axios默认请求头中的token } return { token, userInfo, isLogin, login, fetchUserInfo, logout } })封装Axios实例统一处理请求拦截添加Token、响应拦截处理错误和基础配置// utils/request.ts import axios from axios import { useUserStore } from /stores/user import { ElMessage } from element-plus const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( (config) { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response) { const res response.data // 假设后端统一返回格式为 { code: number, msg: string, data: any } if (res.code 200) { return res } else { // 业务逻辑错误 ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg || Error)) } }, (error) { // HTTP状态码错误 if (error.response?.status 401) { // 未授权跳转到登录页 const userStore useUserStore() userStore.logout() ElMessage.warning(登录已过期请重新登录) // 可以使用router进行跳转注意避免循环依赖 window.location.href /login } else if (error.response?.status 403) { ElMessage.error(权限不足) } else { ElMessage.error(error.message || 网络错误) } return Promise.reject(error) } ) export default service4.4 视频列表页与播放页开发视频列表页主要使用Element Plus的ElRow、ElCol和ElCard组件进行布局通过分页组件ElPagination加载数据。!-- VideoList.vue 部分代码 -- template div classvideo-list-page div classfilter-bar el-input v-modelsearchKeyword placeholder搜索视频... keyup.enterhandleSearch / el-select v-modelselectedCategory placeholder全部分类 changehandleCategoryChange el-option label全部分类 :valuenull / el-option v-forcat in categories :keycat.id :labelcat.name :valuecat.id / /el-select /div el-row :gutter20 el-col v-forvideo in videoList :keyvideo.id :xs24 :sm12 :md8 :lg6 el-card classvideo-card clickgoToDetail(video.id) img :srcvideo.coverUrl classvideo-cover alt封面 / div classvideo-info h3 classtitle{{ video.title }}/h3 div classmeta spanel-iconView //el-icon {{ video.viewCount }}/span spanel-iconClock //el-icon {{ formatDuration(video.duration) }}/span /div /div /el-card /el-col /el-row div classpagination el-pagination v-model:current-pagecurrentPage v-model:page-sizepageSize :totaltotal :page-sizes[12, 24, 48] layouttotal, sizes, prev, pager, next, jumper size-changehandleSizeChange current-changehandleCurrentChange / /div /div /template script setup langts import { ref, onMounted } from vue import { useRouter } from vue-router import { getVideoListApi } from /api/video import type { VideoItem } from /types/video const router useRouter() const videoList refVideoItem[]([]) const total ref(0) const currentPage ref(1) const pageSize ref(12) const searchKeyword ref() const selectedCategory refnumber | null(null) const loadVideoList async () { const res await getVideoListApi({ pageNum: currentPage.value, pageSize: pageSize.value, keyword: searchKeyword.value, categoryId: selectedCategory.value }) videoList.value res.data.records total.value res.data.total } const goToDetail (id: number) { router.push(/video/${id}) } onMounted(() { loadVideoList() }) /script播放页VideoDetail.vue则主要包含播放器组件、视频信息、推荐列表等。关键点在于从路由参数中获取视频ID然后调用接口获取视频详情包含m3u8_url最后传递给播放器组件。4.5 大文件分片上传组件实现对于视频上传必须实现分片上传以支持大文件、断点续传和网络稳定性。我们可以使用Element Plus的ElUpload组件并自定义上传行为。核心思路前端将文件切割成固定大小的分片如5MB。为每个分片计算MD5或SHA-256哈希用于标识和秒传。调用后端接口检查哪些分片已上传实现断点续传。并发上传未完成的分片。所有分片上传完成后通知后端合并分片。由于代码较长这里给出关键步骤的伪代码和思路// 1. 文件分片 const chunkSize 5 * 1024 * 1024 // 5MB const chunks: Blob[] [] let start 0 while (start file.size) { const chunk file.slice(start, start chunkSize) chunks.push(chunk) start chunkSize } // 2. 计算文件整体和每个分片的哈希可以使用spark-md5库 const fileHash await calculateFileHash(file) const chunkHashes await Promise.all(chunks.map(chunk calculateChunkHash(chunk))) // 3. 调用后端预检接口传递fileHash和chunkHashes // 后端返回已上传成功的分片索引列表 const { uploadedIndexes } await checkChunks(fileHash, chunkHashes) // 4. 上传未完成的分片 const uploadPromises chunks .filter((_, index) !uploadedIndexes.includes(index)) .map((chunk, index) uploadChunk(fileHash, index, chunk, chunkHashes[index])) await Promise.all(uploadPromises) // 5. 通知后端合并分片 await mergeChunks(fileHash, file.name, chunks.length)后端需要提供三个接口/api/upload/check检查分片、/api/upload/chunk上传分片、/api/upload/merge合并分片。合并时使用Java的NIOFiles.createFile和FileChannel将临时分片文件按顺序合并成最终文件效率更高。5. 项目部署与运维要点5.1 前端构建与后端打包前端构建 在项目根目录运行npm run buildVite会在dist目录下生成优化后的静态资源。为了与SpringBoot集成我们需要将这些文件复制到SpringBoot项目的src/main/resources/static目录下。可以写一个简单的脚本或使用Maven插件如frontend-maven-plugin在打包阶段自动完成。后端打包 使用Spring Boot Maven插件生成可执行的JAR文件。mvn clean package -DskipTests打包后会在target目录下生成一个your-project-0.0.1-SNAPSHOT.jar文件。这个JAR包内嵌了Tomcat服务器可以直接运行。5.2 生产环境配置与优化配置文件分离使用application-prod.yml来覆盖开发环境的配置如数据库连接、OSS密钥、日志级别等。通过启动参数--spring.profiles.activeprod激活。数据库连接池调优使用HikariCP根据实际并发量调整maximum-pool-size、connection-timeout等参数。JVM参数优化在启动JAR时指定JVM参数例如设置堆内存大小、垃圾回收器等。java -Xms512m -Xmx1024m -XX:UseG1GC -jar your-project.jar --spring.profiles.activeprod静态资源缓存在SpringBoot配置中可以为静态资源设置HTTP缓存头减少重复请求。# application-prod.yml spring: web: resources: cache: cachecontrol: max-age: 365d public: true # 针对带哈希的文件名如main.abc123.js设置永久缓存 chain: strategy: content: enabled: true paths: /**5.3 使用Nginx进行反向代理与负载均衡在生产环境更推荐使用Nginx作为反向代理服务器。将前端静态文件交由Nginx直接服务将API请求代理到后端的SpringBoot应用。这样性能更好也便于配置SSL、Gzip压缩等。一个简单的Nginx配置示例server { listen 80; server_name your-domain.com; # 重定向到HTTPS如果有SSL证书 # return 301 https://$server_name$request_uri; # 前端静态文件 location / { root /path/to/your/vue/dist; index index.html; try_files $uri $uri/ /index.html; # 支持Vue Router的history模式 } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:8080/; # SpringBoot应用地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 如果后端服务需要较长时间处理如大文件上传需要调整超时时间 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # 开启Gzip压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss text/javascript; }5.4 常见问题排查与性能优化问题1视频播放卡顿或加载慢排查首先检查网络。其次查看浏览器开发者工具的Network面板观察.m3u8和.ts文件的加载情况。如果.ts文件加载时间过长可能是服务器带宽不足或CDN未生效。解决确保视频已正确转码为多码率HLS播放器能根据网速自适应切换。将HLS文件.m3u8和.ts放置在CDN上。检查Nginx或对象存储服务是否对视频文件正确配置了Content-Typevideo/MP2Tfor .ts,application/vnd.apple.mpegurlfor .m3u8。问题2大文件上传失败排查检查浏览器控制台和服务器日志。常见原因是Nginx或SpringBoot的请求大小限制、请求超时限制。解决Nginx在配置文件中增加client_max_body_size 100m;和超时时间。SpringBoot在application.yml中配置spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB实现分片上传这是解决大文件上传最稳健的方式。问题3前端路由在刷新后404History模式解决确保Web服务器如Nginx为所有非静态文件请求都返回index.html这就是上面Nginx配置中try_files $uri $uri/ /index.html;的作用。问题4数据库连接数耗尽排查查看应用日志是否有大量数据库连接超时的错误。使用监控工具如Druid连接池的监控界面查看活跃连接数。解决优化连接池配置maximum-pool-size不要设置过大。检查代码中是否有数据库连接未正确关闭的情况MyBatis-Plus通常会自动管理。对于复杂的查询确保数据库表已建立合适的索引。性能优化建议数据库层面为video表的category_id,status,create_time等常用查询字段建立索引。避免SELECT *只查询需要的字段。应用层面对热点数据如首页视频列表使用Redis进行缓存。注意设置合理的过期时间和缓存更新策略如删除更新。静态资源务必开启Nginx的Gzip压缩并对图片、视频等资源设置长期缓存通过文件名哈希实现。视频服务强烈建议将视频转码和存储完全托管给云服务商的对象存储和媒体处理服务自己只管理元数据。这能节省大量服务器成本和运维精力。整个项目从技术选型到部署上线的链路比较长每个环节都有需要注意的细节。建议在开发时先打通核心流程上传-转码-播放再逐步完善用户管理、评论、点赞等周边功能。遇到问题多查看官方文档和社区讨论大部分坑都有前人踩过。