news 2026/9/2 22:50:17

Spring Boot + Vue 实现个人视频管理:从上传到播放的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot + Vue 实现个人视频管理:从上传到播放的完整链路

在实际开发里,“我的视频”这类个人视频管理项目经常被低估。很多开发者以为只要有一个 HTML5<video>标签,把视频地址填进去就能播放;真正动手做时才发现,视频文件存在哪里、接口怎么返回、浏览器用什么编码格式播放、上传大文件会不会超时、前端页面怎么刷新列表,每一条链路都会影响最终效果。这篇文章就以“我的视频”为一个最小可运行的个人视频管理项目,拆开完整的实现过程:后端负责上传和文件存储,前端负责列表展示和播放器集成。读者跟着做完后,能掌握一个可复用的 Spring Boot + Vue 视频管理雏形,并能在本地环境完成上传、列表、播放验证。

项目不依赖付费服务,也不接入第三方存储,重点是把“本地视频文件 -> HTTP 接口 -> 浏览器播放器”这条链路跑通。生产化需要考虑的转码、对象存储、CDN 和权限问题,会放在最后单独说明。这样处理既适合刚开始接触前后端联调的学习者,也适合想快速搭建内部视频管理工具的后端工程师。

1. 先理清“我的视频”项目的需求与技术链路

1.1 需求拆解:本地视频怎么变成在线可播放

一个个人视频管理页面的核心需求可以拆成四部分:上传视频、保存视频信息、展示视频列表、点击视频后能在线播放。看似简单,但每一部分背后都有细节。

上传视频时,前端表单需要把文件通过 HTTP 发送给后端。后端不能只把文件写到磁盘就结束,还要保存视频的元数据,比如标题、文件大小、上传时间、存储路径。这样列表页才有内容可展示。

展示列表时,前端从后端拉取结构化数据,渲染成卡片或表格。为了能播放,后端必须把视频文件通过可访问的 URL 暴露给前端,比如/upload/2024/05/xxx.mp4。前端拿到这个 URL 后,交给<video>标签播放。

这里最关键的问题是“浏览器能不能播放”。浏览器对视频格式有严格要求,不是所有.mp4都能播放。最常见的原因是视频编码不是 H.264,或者音频编码不是 AAC。缺少转码模块时,项目中要在上传环节做提示,或在文档中明确推荐使用 H.264 视频编码。

1.2 技术选型:为什么用 Spring Boot + Vue + HTML5 video

这个项目选择 Spring Boot 作为后端,Vue 3 作为前端,播放器使用浏览器原生<video>,原因主要有三点。

第一,Spring Boot 能快速提供文件上传、静态资源映射和 JSON 接口。文件上传只需要处理MultipartFile,不需要额外引入重型框架。静态资源映射可以配置本地目录,让视频文件直接通过 URL 访问。

第二,Vue 3 的响应式列表渲染非常适合这个场景。上传后返回新的视频记录,列表会自动更新;切换播放器地址时,不需要刷新页面。

第三,原生<video>已经覆盖个人项目的播放需求。相比接入第三方播放器,原生标签的兼容性更可控,代码量也更少。

这个组合不是唯一选择,Python 的 FastAPI 配 React 同样可行。这里的重点是讲解通用链路,技术栈只作为示例。

1.3 目录结构和数据模型要提前设计

项目建议采用前后端分离目录,但本地开发时可以放在同一个父目录下,方便统一管理:

my-video/ ├── backend/ # Spring Boot 工程 │ ├── src/main/java/... │ └── src/main/resources/application.yml ├── frontend/ # Vue 3 工程 │ ├── src/ │ │ ├── api/video.js │ │ ├── App.vue │ │ └── main.js │ ├── index.html │ └── package.json └── upload/ # 视频文件实际存储目录

数据模型方面,数据库只需要一张video_info表。字段设计不追求多,但要把播放必需的信息都包含进来:

字段类型说明
idBIGINT主键,自增
titleVARCHAR视频标题,展示用
descriptionVARCHAR视频描述,可选
file_nameVARCHAR上传时的原始文件名
stored_nameVARCHAR后端重命名后的存储文件名
file_pathVARCHAR相对于上传根目录的路径
file_sizeBIGINT文件大小,单位字节
content_typeVARCHARMIME 类型
created_atDATETIME创建时间
updated_atDATETIME更新时间

这里的stored_namefile_name分开存储,是为了避免直接使用用户上传的文件名写入磁盘。原始文件名只用于展示,存储文件名由后端生成 UUID,避免重名和路径注入风险。

2. 环境准备与项目初始化

2.1 后端环境版本说明

示例工程以 Spring Boot 3.2 和 JDK 17 为基础。如果本地是 Spring Boot 2.x,需要注意两点:第一,包名是javax.*而不是jakarta.*;第二,跨域配置和文件上传配置的写法基本相同。

推荐环境如下:

软件版本/用途
JDK17 或以上
Maven3.8 或以上
Spring Boot3.2.x
Node.js18 或以上
npm9 或以上
Vue3.x
Vite4.x 或 5.x

不强制所有读者都用相同版本。只要理解代码中的jakarta.servlet.http.HttpServletResponseorg.springframework.web.multipart.MultipartFile等关键类,换版本时也能快速调整。

2.2 初始化 Spring Boot 后端工程

在 start.spring.io 或 IDE 中创建项目时,只需要添加一个依赖:Spring Web。其他依赖如 MySQL 驱动、MyBatis 或 JPA,可以按自己的持久化方案添加。

为了让示例足够简洁,这里先使用 Spring Data JPA 和 H2 内存数据库。这样不需要安装 MySQL 就能跑通流程。若要在生产环境切换 MySQL,只需要修改数据源配置。

pom.xml的核心依赖如下:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

如果不想使用 JPA,也可以直接使用 Spring JDBC 或 MyBatis。这里选 JPA 是因为实体类代码最少,更适合快速演示。

2.3 配置文件:上传目录、文件大小和静态资源映射

src/main/resources/application.yml是后端的核心配置。文件上传目录建议放在项目根目录之外,便于备份,也避免打包进 JAR。

server: port: 8080 spring: application: name: my-video servlet: multipart: max-file-size: 500MB max-request-size: 500MB h2: console: enabled: true path: /h2-console datasource: url: jdbc:h2:file:./data/myvideo driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: update show-sql: true my-video: upload-dir: ./upload

max-file-sizemax-request-size直接影响大文件上传。Spring Boot 默认的 1MB 限制对于视频文件远远不够。这里设置到 500MB,只适合本地演示。生产环境一般不会用同步上传接 500MB 文件,而是走分片上传或对象存储直传。

my-video.upload-dir是自定义配置项,用于读取上传存储目录。这样配置和代码可以分离,部署时通过环境变量覆盖。

2.4 创建数据库表结构

使用 JPA 后,可以直接用实体类自动建表。也可以准备一份 DDL,方便切换到 MySQL:

CREATE TABLE video_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description VARCHAR(1000), file_name VARCHAR(255) NOT NULL, stored_name VARCHAR(255) NOT NULL, file_path VARCHAR(500) NOT NULL, file_size BIGINT NOT NULL, content_type VARCHAR(255), created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL );

这张表不存视频二进制内容,只存文件路径。视频文件本身在upload目录,数据库里的file_path用于拼接访问 URL。这样数据库不会迅速膨胀,备份时可以只备份数据库,视频文件通过文件系统备份。

3. 后端实现:视频元数据接口和静态资源服务

3.1 实体类与 Repository

先创建视频信息实体类。字段与前面 DDL 对齐。

package com.example.myvideo.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDateTime; @Data @Entity @Table(name = "video_info") public class VideoInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String description; @Column(name = "file_name") private String fileName; @Column(name = "stored_name") private String storedName; @Column(name = "file_path") private String filePath; @Column(name = "file_size") private Long fileSize; @Column(name = "content_type") private String contentType; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist public void prePersist() { LocalDateTime now = LocalDateTime.now(); this.createdAt = now; this.updatedAt = now; } @PreUpdate public void preUpdate() { this.updatedAt = LocalDateTime.now(); } }

Repository 接口只使用内置方法即可。

package com.example.myvideo.repository; import com.example.myvideo.entity.VideoInfo; import org.springframework.data.jpa.repository.JpaRepository; public interface VideoInfoRepository extends JpaRepository<VideoInfo, Long> { }

这个接口提供findAll()save()方法,已经足够覆盖列表和上传场景。

3.2 业务服务:保存文件并写入元数据

VideoService负责文件存储和数据库写入。核心方法有三步:生成存储名、保存文件、写入数据库。

package com.example.myvideo.service; import com.example.myvideo.entity.VideoInfo; import com.example.myvideo.repository.VideoInfoRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.UUID; @Service public class VideoService { private final VideoInfoRepository repository; @Value("${my-video.upload-dir}") private String uploadDir; public VideoService(VideoInfoRepository repository) { this.repository = repository; } public VideoInfo uploadVideo(String title, String description, MultipartFile file) { try { Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize(); Files.createDirectories(uploadPath); String originalFilename = Objects.requireNonNull(file.getOriginalFilename(), "文件名不能为空"); String extension = getExtension(originalFilename); String storedName = UUID.randomUUID() + extension; Path targetPath = uploadPath.resolve(storedName); file.transferTo(targetPath); VideoInfo info = new VideoInfo(); info.setTitle(title); info.setDescription(description); info.setFileName(originalFilename); info.setStoredName(storedName); info.setFilePath(storedName); info.setFileSize(file.getSize()); info.setContentType(file.getContentType()); return repository.save(info); } catch (Exception e) { throw new RuntimeException("视频保存失败", e); } } public List<VideoInfo> listVideos() { return repository.findAll(); } private String getExtension(String filename) { int dotIndex = filename.lastIndexOf('.'); return dotIndex >= 0 ? filename.substring(dotIndex).toLowerCase() : ""; } }

这里有几个关键点。storedName使用 UUID,避免两个文件重名。Files.createDirectories保证目录存在。file.transferTo是 Spring 推荐的文件保存方式,能自动处理临时文件和资源释放。getExtension只提取后缀,不信任原始文件名本身。

3.3 控制器:上传接口和列表接口

控制器对外暴露两个接口:POST /api/videos用于上传,GET /api/videos用于列表。视频文件访问不放在控制器里,而是交给静态资源映射处理。

package com.example.myvideo.controller; import com.example.myvideo.entity.VideoInfo; import com.example.myvideo.service.VideoService; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @RestController @RequestMapping("/api/videos") public class VideoController { private final VideoService videoService; public VideoController(VideoService videoService) { this.videoService = videoService; } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public VideoInfo upload(@RequestParam("title") String title, @RequestParam(value = "description", required = false) String description, @RequestParam("file") MultipartFile file) { return videoService.uploadVideo(title, description, file); } @GetMapping public List<VideoInfo> list() { return videoService.listVideos(); } }

使用@RequestParam接收上传表单时,前端必须按下划线方式传参。后面 Vue 代码会用到FormData,字段名要与这里保持一致。

3.4 静态资源映射:让视频文件能通过 URL 播放

视频不能只保存在本地,前端必须能请求到文件。Spring Boot 可以通过WebMvcConfigurer配置将本地目录映射到/upload/**

package com.example.myvideo.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.nio.file.Paths; @Configuration public class WebConfig implements WebMvcConfigurer { @Value("${my-video.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String location = Paths.get(uploadDir).toAbsolutePath().normalize().toUri().toString(); registry.addResourceHandler("/upload/**") .addResourceLocations(location); } }

配置完成后,假设上传目录是./upload,存储文件是abc.mp4,那么访问地址为http://localhost:8080/upload/abc.mp4

注意addResourceLocations必须以/结尾。toUri().toString()会返回类似file:///User/xxx/upload/的路径,符合要求。很多视频无法播放的问题,都是因为这个配置漏了结尾斜杠,或路径拼接错误。

3.5 跨域配置

前端开发服务器运行在5173端口,后端运行在8080端口,浏览器会拦截跨域请求。这里需要配置后端允许跨域,或者在前端使用代理。简单演示时,可以在后端加一个全局 CORS 配置。

package com.example.myvideo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } }

allowedOrigins在开发环境可以直接写前端地址。生产环境上线后,建议改成具体域名,或直接通过 Nginx 反向代理,避免跨域问题从源头出现。

4. 前端实现:视频列表与播放器页面

4.1 初始化 Vue 3 项目

使用 Vite 创建前端工程:

npm create vite@latest frontend -- --template vue cd frontend npm install

安装请求库 axios:

npm install axios

开发环境下,为了避免跨域,可以在vite.config.js中配置代理。这样前端请求/api时,Vite 会转发到后端8080

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true }, '/upload': { target: 'http://localhost:8080', changeOrigin: true } } } })

代理的好处是前端代码里不需要写后端完整地址,统一使用相对路径。发布时再把静态文件交给 Nginx,同样代理/api/upload到后端服务即可。

4.2 封装请求模块

src/api/video.js把后端接口封装成两个函数。

import axios from 'axios' const http = axios.create({ baseURL: '/api', timeout: 60000 }) export function listVideos() { return http.get('/videos') } export function uploadVideo(formData) { return http.post('/videos', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) }

上传时间太长时,60 秒可能不够。如果文件较大,建议把请求timeout设置更长,或者采用分片上传。这个示例为了简单,只调整了超时时间。

4.3 视频播放器组件

播放器组件接收一个url属性,根据地址变化切换播放内容。使用<video controls>是原生播放器,不需要额外引入库。

<template> <div class="player"> <video v-if="url" :src="url" controls preload="metadata" style="width: 100%; max-height: 480px; background: #000" ></video> <div v-else class="empty"> 请选择视频 </div> </div> </template> <script setup> defineProps({ url: { type: String, default: '' } }) </script>

preload="metadata"表示只预加载视频元数据,避免一打开页面就下载整个视频文件。controls属性提供浏览器自带的播放控制条,包含播放、音量、全屏等功能。

4.4 主页面:上传表单和视频列表

App.vue负责页面整体布局。页面分成两部分:左侧是上传区,右侧是列表和播放器。为了演示完整流程,不引入 UI 组件库,直接用原生表单。

<template> <main class="page"> <h1>我的视频</h1> <section class="upload-box"> <h2>上传视频</h2> <form @submit.prevent="handleUpload"> <input v-model="title" placeholder="视频标题" required /> <input v-model="description" placeholder="视频描述" /> <input type="file" accept="video/*" @change="handleFileChange" required /> <button type="submit" :disabled="uploading"> {{ uploading ? '上传中...' : '开始上传' }} </button> </form> </section> <section class="video-panel"> <aside class="video-list"> <h2>全部视频</h2> <ul> <li v-for="video in videos" :key="video.id" :class="{ active: currentUrl === buildPlayUrl(video) }" @click="playVideo(video)" > <span class="title">{{ video.title }}</span> <span class="size">{{ formatSize(video.fileSize) }}</span> </li> </ul> </aside> <section class="video-stage"> <VideoPlayer :url="currentUrl" /> </section> </section> </main> </template> <script setup> import { onMounted, ref } from 'vue' import { listVideos, uploadVideo } from './api/video' import VideoPlayer from './components/VideoPlayer.vue' const videos = ref([]) const title = ref('') const description = ref('') const selectedFile = ref(null) const uploading = ref(false) const currentUrl = ref('') async function loadVideos() { const { data } = await listVideos() videos.value = data } function handleFileChange(event) { selectedFile.value = event.target.files[0] } async function handleUpload() { if (!selectedFile.value) return uploading.value = true try { const formData = new FormData() formData.append('title', title.value) formData.append('description', description.value) formData.append('file', selectedFile.value) await uploadVideo(formData) title.value = '' description.value = '' selectedFile.value = null await loadVideos() } finally { uploading.value = false } } function buildPlayUrl(video) { return `/upload/${video.filePath}` } function playVideo(video) { currentUrl.value = buildPlayUrl(video) } function formatSize(size) { const mb = size / 1024 / 1024 if (mb >= 1024) { return (mb / 1024).toFixed(2) + ' GB' } return mb.toFixed(2) + ' MB' } onMounted(loadVideos) </script> <style scoped> .page { max-width: 1200px; margin: 0 auto; padding: 20px; } .upload-box { margin-bottom: 20px; padding: 16px; border: 1px solid #ddd; border-radius: 8px; } form { display: flex; gap: 12px; flex-wrap: wrap; } input[type="text"], input[type="file"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; } .video-panel { display: flex; gap: 20px; } .video-list { width: 300px; } .video-list ul { list-style: none; padding: 0; } .video-list li { padding: 10px; border: 1px solid #eee; border-radius: 6px; cursor: pointer; margin-bottom: 8px; } .video-list li.active { border-color: #1890ff; background: #e6f7ff; } .video-stage { flex: 1; } </style>

注意buildPlayUrl使用相对路径/upload/...,开发时 Vite 代理会转发到后端。这一步是很多初学容易忽略的地方:前端拿到的视频地址必须是浏览器可直接访问的 URL,而不是数据库里的物理路径。

5. 运行验证与请求链路分析

5.1 启动后端和前端

backend目录启动 Spring Boot:

mvn spring-boot:run

frontend目录启动前端:

npm run dev

浏览器访问http://localhost:5173,页面应该显示“我的视频”,此时视频列表为空。

5.2 上传视频验证

准备一个 MP4 文件,建议使用 H.264 编码、编码格式为 AAC 的普通视频。填写标题,选择文件,点击上传。

预期结果:

  • 页面提示“上传中...”后自动刷新列表。
  • 后端日志出现INSERTSQL 语句。
  • upload目录出现新的视频文件,文件名是一串 UUID。

可以通过接口验证:

curl -X GET http://localhost:5173/api/videos

返回结果类似:

[ { "id": 1, "title": "我的旅行记录", "description": "假期视频", "fileName": "travel.mp4", "storedName": "9f8c2f4e-1a2b-4c3d-8e9f-abcdef123456.mp4", "filePath": "9f8c2f4e-1a2b-4c3d-8e9f-abcdef123456.mp4", "fileSize": 12582912, "contentType": "video/mp4", "createdAt": "2025-05-01T12:00:00", "updatedAt": "2025-05-01T12:00:00" } ]

5.3 播放视频验证

点击列表中的视频条目,播放器区域应该出现可播放的视频画面。

可以直接访问视频地址验证:

curl -I http://localhost:5173/upload/xxx.mp4

如果返回200 OK,并且响应头包含Content-Type: video/mp4,说明静态资源映射生效。如果返回404,需要检查后端WebConfig中的路径配置。

播放时打开浏览器开发者工具,重点观察 Network 请求状态。视频首次播放时,浏览器会发起Range请求,响应状态通常是206 Partial Content,这是正常的,表示浏览器支持分段加载视频内容。

6. 常见问题与排查链路

6.1 视频文件能上传,但播放器一直转圈或报错

现象:上传成功后列表有数据,点击播放视频没有画面,控制台显示请求失败或video format not supported

排查顺序:

  1. 直接用浏览器访问视频 URL,检查是否能下载文件。
  2. 检查响应头的Content-Type是否是video/mp4
  3. 检查视频编码。打开视频文件的编码信息,确认视频轨是 H.264,音频轨是 AAC。如果是 H.265、VP9 或其他编码,浏览器可能无法播放。
  4. 检查是否存在大小超过 2GB 的情况。本地文件系统对FileOutputStream不敏感,但播放器对文件范围请求有要求,遇到异常需要看响应头。

解决建议:

问题现象常见原因处理方式
播放器转圈URL 返回 404 或后端未启动检查静态资源映射和代理配置
格式不支持视频编码非 H.264使用 FFmpeg 转码为 H.264 + AAC
卡顿严重本地网络或文件过大压缩码率或使用 CDN 分发

6.2 上传或请求接口出现跨域问题

现象:浏览器控制台出现No 'Access-Control-Allow-Origin' header,接口请求失败。

可能原因:前端5173直接请求了后端8080,而代理没有生效,或者代理配置了但请求路径不匹配。

检查方式:

  • 确认访问的是http://localhost:5173/api/videos,不是http://localhost:8080/api/videos
  • 确认vite.config.js中的代理配置已经重启 Vite。
  • 确认后端 CORS 配置允许的源包含http://localhost:5173

推荐做法是使用代理,而不是把baseURL写成http://localhost:8080。代理能减少大量跨域问题。

6.3 上传大文件时提示文件大小超过限制

现象:上传小于 1MB 的视频没有问题,超过一定大小后报错FileSizeLimitExceededException

原因:Spring Boot 默认限制单次上传文件大小为 1MB。示例中已经在application.yml配置了 500MB,但如果读者复制时遗漏,或使用 Spring Boot 2.x 的默认配置,就会命中限制。

解决方式:

spring: servlet: multipart: max-file-size: 500MB max-request-size: 500MB

同时要注意 Nginx 层也有client_max_body_size限制。如果经过 Nginx,需要在 Nginx 配置中增加:

client_max_body_size 500m;

6.4 重新部署后上传文件丢失或目录不存在

现象:重启后端后,列表里有记录,但打开视频 404,或者上传报系统找不到指定的路径

可能原因:上传目录使用了相对路径,而启动目录变化导致路径不一致。

解决方式:不要使用相对路径作为生产存储目录。最稳妥的方式是用 Spring Boot 配置项指定绝对路径,例如:

my-video: upload-dir: /data/my-video/upload

部署前要确认该目录存在,并且运行后端的用户有读写权限。强烈建议在配置中增加目录检查,启动时找不到目录就创建并记录日志。

7. 生产化扩展与最佳实践

7.1 本地项目可以先跑通,但生产环境要补安全底线

示例中的接口没有鉴权,任何人只要能访问后端,就能上传和删除视频。如果项目要对外提供访问,至少需要增加一层登录认证。推荐做法:

  • 使用 Spring Security 登录认证,上传和删除接口需要管理员权限。
  • 视频访问地址不要直接暴露完整存储路径,可以增加签名 URL 或鉴权拦截器。
  • 只允许特定扩展名,比如.mp4.webm,并在上传时校验 MIME 类型和文件头。
  • 限制单文件大小,避免磁盘被恶意文件塞满。

7.2 视频转码是必不可少的扩展

个人项目可以手动准备 H.264 视频,但真实用户上传的视频编码五花八门。为了兼容浏览器,建议引入 FFmpeg 转码流程。最简单的转码命令:

ffmpeg -i input.mp4 -c:v libx264 -c:a aac -preset fast -movflags +faststart output.mp4

-movflags +faststart会把 MP4 的元数据移动到文件开头,让浏览器无需下载完整文件就可以开始播放。这个参数对 Web 播放非常关键。

如果不想自己维护转码集群,可以接入云厂商的媒体处理服务。但即便使用云服务,也要理解“浏览器要求 H.264 编码”这个前提,否则排错时无从下手。

7.3 存储与分发:从本地磁盘走向对象存储和 CDN

本地磁盘在文件量少时够用,但它有两个明显瓶颈:容量有限、单点故障。生产环境建议分两步演进。

第一步,把视频文件迁移到对象存储,数据库只保存对象存储的 Key。上传接口不再直接保存到本地,而是流式写入对象存储。播放地址直接使用对象存储提供的 URL。

第二步,给播放地址接入 CDN。视频文件流量大,通过 CDN 可以降低源站压力,同时提升不同地区用户的播放体验。需要注意的是,私有视频不能直接在 CDN 上公开,需要配置 URL 鉴权或者临时签名。

7.4 上线前可复用的检查清单

下面的清单可以直接作为项目上线前的手工检查表:

检查项检查内容
数据库表结构是否创建,索引是否合理,备份策略是否生效
存储目录是否使用绝对路径,目录权限是否正确,磁盘空间是否充足
文件上传单文件大小限制是否符合业务,是否校验扩展名和 MIME
视频格式是否包含转码流程,是否输出 H.264 + AAC
访问地址视频 URL 是否为可公开访问地址,是否包含鉴权
代理层Nginx 是否配置client_max_body_size,代理路径是否正确
日志监控上传失败日志是否完整,磁盘和带宽是否纳入监控
回滚方案后端版本回滚是否能保证数据库和文件目录兼容

这个项目最大的价值不是“做一个视频网站”,而是把上传、存储、接口、播放、排错这一整条链路完整走通。掌握这条链路之后,无论是改成图片管理、音频管理,还是接入云存储,思路都是一样的。下一步最值得做的扩展,是给项目加入 FFmpeg 转码队列,把“能存文件”升级成“能处理用户的真实视频文件”。对新手来说,先把本节的最小闭环跑通,再逐步补上安全、转码和存储优化,学习曲线会平滑很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 22:46:03

嵌入式C语言与STM32入门:从寄存器操作到GPIO实战

各位刚接触嵌入式开发的朋友&#xff0c;大家好。很多初学者在入门时都会面临同样的困惑&#xff1a;C语言语法好像看懂了&#xff0c;但一碰到硬件就不知道从何下手&#xff1b;听说 STM32 很流行&#xff0c;但打开开发环境看到满屏的寄存器又瞬间劝退。这篇文章就是为了解决…

作者头像 李华
网站建设 2026/9/2 22:42:31

Smic40nm PDK中pmos器件识别与版图层次查看实战指南

刚接手一颗 Smic40nm 工艺的项目时&#xff0c;第一件事往往不是急着画版图&#xff0c;而是先把 PDK 里的器件认清楚。尤其是模拟电路里最常用的 pmos&#xff0c;如果连器件层次、CDF 参数、symbol 与 layout 的对应关系都没弄明白&#xff0c;后面做 LVS、跑 Monte Carlo、做…

作者头像 李华
网站建设 2026/9/2 22:42:21

Windows 安装 Codex 桌面端全攻略:CLI 配置与经典报错排查

简介&#xff1a;Codex App Windows 安装包面向需要在 Windows 离线环境中安装并使用 AI 编程代理的开发者。Codex App 是 OpenAI 推出的智能编程代理控制中心&#xff0c;支持并行管理多个 AI 代理&#xff0c;通过独立线程与 worktree 避免任务冲突&#xff0c;可连接设计、项…

作者头像 李华
网站建设 2026/9/2 22:41:36

进销存源码深度解析:从核心模块到并发控制与部署实践

简介&#xff1a;这是一份基于VS2010与SQL Server开发的弘晶进销存源码&#xff0c;面向需要学习商业管理系统开发的程序员、相关专业学生及中小企业信息化实施者&#xff0c;可用于理解采购、销售、库存、应收应付四大核心模块的真实落地实现。源码覆盖供应商与客户信息管理、…

作者头像 李华
网站建设 2026/9/2 22:40:56

iApp全开源PHP后台源码部署与二次开发实战指南

简介&#xff1a;这是一套iApp后台及PHP文件全开源源码包&#xff0c;面向移动应用开发者和PHP后端学习者&#xff0c;解决iApp前端与服务器端数据交互、功能接口搭建等需求。包体共419个文件&#xff0c;压缩后仅5.27MB&#xff0c;以278个PHP接口脚本为核心&#xff0c;配合4…

作者头像 李华