
最近很多开发者都在讨论如何快速搭建一个完整的视频内容管理系统特别是针对多集剧集的管理和展示。虽然市面上有很多成熟的CMS系统但往往过于臃肿不适合快速部署和定制开发。今天我们就来实战一个轻量级的解决方案用Spring Boot Vue.js构建一个剧集管理系统。这个项目看似简单但涉及的技术栈相当完整前端路由、后端API、数据库设计、文件管理、用户权限等核心功能一应俱全。更重要的是我们将采用前后端分离架构这种设计在现代Web开发中已经成为标配但很多教程只讲概念缺少完整的落地示例。1. 项目需求分析与技术选型首先明确我们的核心需求管理多季剧集、每季包含多集视频、支持多种字幕版本、提供用户观看界面。这听起来像是一个典型的视频网站后台但我们要做的是开发者的角度关注技术实现而非业务逻辑。技术栈选择理由Spring Boot 2.7快速构建RESTful API内置Tomcat简化部署Vue 3 Vite现代前端开发体验组件化开发MySQL 8.0成熟稳定JSON类型支持剧集元数据存储MyBatis-Plus简化数据库操作提供强大的CRUD接口为什么不用现成CMS学习价值理解完整开发流程比使用黑盒系统更有意义定制灵活可以根据需求随时调整功能模块技术积累掌握企业级项目架构设计能力2. 数据库设计与核心表结构数据库设计是整个系统的基石我们需要考虑剧集、季、集的多级关系以及多版本字幕的支持。-- 剧集主表 CREATE TABLE series ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 剧集标题, description TEXT COMMENT 剧集描述, cover_url VARCHAR(500) COMMENT 封面图URL, status TINYINT DEFAULT 1 COMMENT 状态1-上线, 0-下线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 季表 CREATE TABLE seasons ( id BIGINT PRIMARY KEY AUTO_INCREMENT, series_id BIGINT NOT NULL, season_number INT NOT NULL COMMENT 季数, title VARCHAR(200) COMMENT 季标题, air_date DATE COMMENT 播出日期, FOREIGN KEY (series_id) REFERENCES series(id) ); -- 集表核心表 CREATE TABLE episodes ( id BIGINT PRIMARY KEY AUTO_INCREMENT, season_id BIGINT NOT NULL, episode_number INT NOT NULL COMMENT 集数, title VARCHAR(200) NOT NULL COMMENT 集标题, duration INT COMMENT 时长(秒), video_url VARCHAR(500) NOT NULL COMMENT 视频文件URL, subtitles JSON COMMENT 字幕文件信息, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (season_id) REFERENCES seasons(id) );设计要点说明使用JSON字段存储字幕信息避免过度规范化建立合适的索引提升查询性能考虑未来扩展性如多语言支持3. 后端Spring Boot项目搭建3.1 项目结构规划src/main/java/com/dramasystem/ ├── DramasystemApplication.java ├── config/ # 配置类 ├── controller/ # 控制器层 ├── entity/ # 实体类 ├── mapper/ # 数据访问层 ├── service/ # 业务逻辑层 └── dto/ # 数据传输对象3.2 核心依赖配置!-- pom.xml -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency /dependencies3.3 实体类设计// Episode.java Data TableName(episodes) public class Episode { TableId(type IdType.AUTO) private Long id; private Long seasonId; private Integer episodeNumber; private String title; private Integer duration; private String videoUrl; TableField(typeHandler JacksonTypeHandler.class) private ListSubtitle subtitles; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; } // Subtitle.java Data public class Subtitle { private String language; // 如en, zh, es private String type; // 如srt, vtt private String url; // 字幕文件URL private Boolean isDefault; }4. RESTful API设计与实现4.1 控制器层代码RestController RequestMapping(/api/episodes) Api(tags 剧集管理接口) public class EpisodeController { Autowired private EpisodeService episodeService; GetMapping(/season/{seasonId}) ApiOperation(根据季ID获取剧集列表) public ResultListEpisodeVO getEpisodesBySeason( PathVariable Long seasonId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 20) Integer size) { PageEpisode episodePage new Page(page, size); IPageEpisodeVO result episodeService.getEpisodesBySeason(seasonId, episodePage); return Result.success(result.getRecords(), result.getTotal()); } GetMapping(/{id}) ApiOperation(根据ID获取剧集详情) public ResultEpisodeDetailVO getEpisodeDetail(PathVariable Long id) { EpisodeDetailVO detail episodeService.getEpisodeDetail(id); return Result.success(detail); } }4.2 服务层实现Service public class EpisodeServiceImpl extends ServiceImplEpisodeMapper, Episode implements EpisodeService { Override public IPageEpisodeVO getEpisodesBySeason(Long seasonId, PageEpisode page) { LambdaQueryWrapperEpisode queryWrapper new LambdaQueryWrapper(); queryWrapper.eq(Episode::getSeasonId, seasonId) .orderByAsc(Episode::getEpisodeNumber); IPageEpisode episodePage page(page, queryWrapper); return episodePage.convert(this::convertToVO); } private EpisodeVO convertToVO(Episode episode) { EpisodeVO vo new EpisodeVO(); BeanUtils.copyProperties(episode, vo); // 处理业务逻辑转换 return vo; } }5. 前端Vue.js项目搭建5.1 项目初始化与路由配置// main.js import { createApp } from vue import { createRouter, createWebHistory } from vue-router import App from ./App.vue const routes [ { path: /, name: Home, component: () import(./views/Home.vue) }, { path: /series/:id, name: SeriesDetail, component: () import(./views/SeriesDetail.vue) }, { path: /episode/:id, name: EpisodePlayer, component: () import(./views/EpisodePlayer.vue) } ] const router createRouter({ history: createWebHistory(), routes }) createApp(App).use(router).mount(#app)5.2 剧集列表组件!-- EpisodeList.vue -- template div classepisode-list div v-forepisode in episodes :keyepisode.id classepisode-item div classepisode-number第{{ episode.episodeNumber }}集/div div classepisode-title{{ episode.title }}/div div classepisode-duration{{ formatDuration(episode.duration) }}/div button clickplayEpisode(episode.id) classplay-btn播放/button /div /div /template script import { ref, onMounted } from vue import { useRoute } from vue-router import api from /api/episode export default { name: EpisodeList, setup() { const route useRoute() const episodes ref([]) const loading ref(false) const loadEpisodes async () { loading.value true try { const response await api.getEpisodesBySeason(route.params.seasonId) episodes.value response.data } catch (error) { console.error(加载剧集失败:, error) } finally { loading.value false } } const formatDuration (seconds) { const minutes Math.floor(seconds / 60) return ${minutes}分钟 } onMounted(() { loadEpisodes() }) return { episodes, loading, formatDuration } } } /script6. 视频播放器集成与字幕支持6.1 视频播放器组件!-- VideoPlayer.vue -- template div classvideo-player video refvideoPlayer :srccurrentEpisode.videoUrl controls crossoriginanonymous loadedmetadataonVideoLoaded track v-forsubtitle in subtitles :keysubtitle.language kindsubtitles :srclangsubtitle.language :srcsubtitle.url :labelgetLanguageLabel(subtitle.language) :defaultsubtitle.isDefault /video div classsubtitle-selector select v-modelselectedSubtitle changechangeSubtitle option valueoff关闭字幕/option option v-forsubtitle in subtitles :keysubtitle.language :valuesubtitle.language {{ getLanguageLabel(subtitle.language) }} /option /select /div /div /template script import { ref, watch } from vue export default { name: VideoPlayer, props: { currentEpisode: { type: Object, required: true } }, setup(props) { const videoPlayer ref(null) const selectedSubtitle ref(off) const subtitles ref([]) const getLanguageLabel (lang) { const labels { en: 英语, zh: 中文, es: 西班牙语 } return labels[lang] || lang } const changeSubtitle () { const tracks videoPlayer.value.textTracks for (let i 0; i tracks.length; i) { tracks[i].mode selectedSubtitle.value tracks[i].language ? showing : disabled } } watch(() props.currentEpisode, (newEpisode) { if (newEpisode newEpisode.subtitles) { subtitles.value newEpisode.subtitles // 设置默认字幕 const defaultSub newEpisode.subtitles.find(s s.isDefault) selectedSubtitle.value defaultSub ? defaultSub.language : off } }, { immediate: true }) return { videoPlayer, selectedSubtitle, subtitles, getLanguageLabel, changeSubtitle } } } /script7. 系统配置与部署方案7.1 应用配置文件# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/drama_system?useUnicodetruecharacterEncodingutf8 username: ${DB_USERNAME:root} password: ${DB_PASSWORD:123456} driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 100MB max-request-size: 100MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 文件存储配置 file: storage: type: local local: path: /data/videos access-url: http://localhost:8080/files/7.2 Docker部署配置# Dockerfile FROM openjdk:11-jre-slim WORKDIR /app COPY target/drama-system.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]# docker-compose.yml version: 3.8 services: app: build: . ports: - 8080:8080 environment: - DB_URLjdbc:mysql://mysql:3306/drama_system - DB_USERNAMEroot - DB_PASSWORDyour_password depends_on: - mysql mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: your_password MYSQL_DATABASE: drama_system volumes: - mysql_data:/var/lib/mysql volumes: mysql_data:8. 性能优化与安全考虑8.1 数据库查询优化// 使用MyBatis-Plus的查询优化 Service public class EpisodeService { public IPageEpisodeVO getEpisodesWithOptimization(Long seasonId, PageEpisode page) { return baseMapper.selectEpisodeWithSeasonInfo(page, seasonId); } } // Mapper接口定义 public interface EpisodeMapper extends BaseMapperEpisode { Select(SELECT e.*, s.title as season_title FROM episodes e LEFT JOIN seasons s ON e.season_id s.id WHERE e.season_id #{seasonId} ORDER BY e.episode_number) IPageEpisodeVO selectEpisodeWithSeasonInfo(PageEpisode page, Param(seasonId) Long seasonId); }8.2 接口安全防护Configuration public class SecurityConfig { Bean public FilterRegistrationBeanCorsFilter corsFilter() { UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin(http://localhost:3000); config.addAllowedHeader(*); config.addAllowedMethod(*); source.registerCorsConfiguration(/api/**, config); FilterRegistrationBeanCorsFilter bean new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } }9. 常见问题排查与解决方案在实际开发过程中可能会遇到各种问题。这里总结几个典型场景问题1视频文件无法播放检查文件路径配置是否正确验证视频文件格式是否被浏览器支持MP4、WebM确认服务器MIME类型配置问题2字幕文件加载失败检查字幕文件编码推荐UTF-8验证CORS配置是否允许跨域访问字幕文件确认字幕文件路径权限问题3数据库连接超时# 增加连接池配置 spring: datasource: hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5问题4前端路由刷新404// 配置nginx处理前端路由 location / { try_files $uri $uri/ /index.html; }10. 项目扩展与进阶功能完成基础功能后可以考虑以下扩展方向用户系统集成JWT token认证用户收藏记录观看进度保存后台管理系统剧集上传界面数据统计分析用户行为监控移动端适配PWA渐进式Web应用响应式设计优化离线观看支持这个项目虽然以剧集管理为场景但技术栈和架构设计适用于各种内容管理系统。通过完整的实战开发你不仅能掌握前后端分离开发的全流程还能深入理解现代Web应用的最佳实践。建议在实际开发中先从最小可行产品开始逐步迭代功能。每个技术决策都要考虑可维护性和扩展性这样才能构建出真正有价值的项目。