☰
Spring Boot+Vue构建线上影院系统:推荐算法与m3u8播放实践
2026/10/9 6:38:20 网站建设 项目流程

做这个项目之前,我一直觉得影院系统无非就是CRUD加个视频播放页,直到自己动手把Spring Boot和Vue前后端拆开、把推荐算法揉进去、再把m3u8流媒体播放打通,才发现这里面的坑远比想象中多。这篇文章把我从零搭建一个线上影院系统的完整过程记录下来,包括推荐系统怎么落地、Vue播放器怎么接m3u8分片视频、Spring Boot接口怎么设计才不会被前端骂,以及我实际踩过的各种雷。如果你是准备做毕设、或者想自己搞一个完整的全栈项目练手,可以参考这套思路。

1. 项目整体设计与技术选型

1.1 核心需求解析

线上影院系统,说白了就是三件事:电影资源的管理与展示、用户能在线流畅看视频、系统能猜到你喜欢什么并推给你。如果只是把这三件事做出来,那是个及格的项目;如果要做得像样,就要把每一件事都拆开细看。

先说电影资源管理。这不是简单丢几张海报、写个简介就完事。实际做的时候你会发现,电影信息字段比你想象的多:片名、原名、导演、演员、类型、地区、语言、上映日期、片长、评分、海报图、简介、预告片地址、正片地址、热度值、上架状态。这些字段放在一张表里也行,但你要是想按类型筛选、按地区筛选、按年份筛选,查询条件一多,SQL就会变得很啰嗦。所以设计表结构的时候,我直接把类型、地区这类多值字段单独拆出来,用关联表维护,查询时用JOIN,这样统一管理更清晰。

再说视频播放。这是整个系统里最容易让人翻车的地方。现在线上的视频基本不会用MP4直链播放,一方面是文件太大,加载慢,另一方面是不好做防盗链和清晰度切换。主流的做法是转成HLS协议,也就是m3u8分片流。Vue前端要播放这类视频,不能用原生video标签直接怼一个MP4地址,需要引入支持HLS的播放器,比如video.js加videojs-contrib-hls插件,或者直接用hls.js。这一块我后面专门用一整节来讲。

最后是推荐系统。这是让影院系统从“视频仓库”变成“智能推荐平台”的关键。很多人的毕设项目推荐功能就是“按分类推荐”或“按热度推荐”,那根本不是推荐系统,顶多算筛选。真正的推荐,要基于用户的历史行为去预测他可能喜欢什么。常见做法有两种:基于协同过滤,或者基于内容相似度。我在这个项目里做了一个折中的方案,既考虑用户行为,又考虑电影内容特征,效果在数据量不大的情况下已经足够好。

1.2 技术选型与版本锁定

这个项目用的是经典的前后端分离架构:

  • 后端:Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis(可选)
  • 前端:Vue 3 + Vue Router + Pinia + Element Plus + video.js
  • 推荐算法:基于用户行为评分的协同过滤(Java实现)

特别提醒:Spring Boot版本不要一上来就选3.x,除非你已经很清楚自己在做什么。Spring Boot 3.0以后底层是Jakarta EE,很多老项目的依赖坐标从javax.*变成了jakarta.*,网上大量教程和博客都是基于2.x写的,你照着做大概率会报一堆包找不到的错误。我一开始也手贱试过3.x,结果折腾了一晚上,最后老老实实回到2.7.x。不用纠结版本不是最新的,对于做项目而言,稳定、有大量参考资料才是最重要的。

前端这块,Vue 3是现在的绝对主流。但如果教程是Vue 2的写法,你拿Vue 3去写会发现很多API对不上,比如v-model的用法、路由的创建方式、全局挂载方式全变了。这里建议直接看Vue 3官方文档,别偷懒。

2. 数据库设计与表结构详解

2.1 电影信息表设计

电影信息表我命名为film,核心字段如下:

字段名类型说明
idbigint主键
titlevarchar(100)片名
original_titlevarchar(100)原名
directorvarchar(50)导演
actorsvarchar(255)主演
summarytext简介
cover_urlvarchar(255)海报URL
video_urlvarchar(255)正片播放地址(m3u8)
trailer_urlvarchar(255)预告片地址
durationint片长(分钟)
ratingdecimal(3,1)评分
hot_valueint热度值
statustinyint状态(0下架,1上架)
create_timedatetime创建时间
update_timedatetime更新时间

这里有个细节:video_url存的应该是m3u8的地址,如果你是自己转码推流,那这个地址就是类似于http://your-server:8080/hls/movie123.m3u8的形式。如果你用的是第三方存储,那存的就是CDN或者对象存储的URL。无论哪种,前端播放器的逻辑都不变,它只需要一个url就能播放。

2.2 类型与关联表

电影类型不能直接存一个字符串拉倒。你想想,一个电影可能是“动作+科幻”,另一个可能是“剧情+爱情”,你要查“所有动作片”,用LIKE '%动作%'去匹配字符串,听着就很不靠谱。所以我把类型拆出来:

  • film_type表:存类型id和类型名(动作、科幻、剧情、喜剧等)
  • film_type_rel表:存film_id和type_id的关联关系

2.3 用户行为表设计

推荐系统依赖的用户行为数据,核心是三张表:

  • user:用户基本信息
  • user_rating:用户对电影的评分记录(用户主动打分或系统根据观看时长折算)
  • user_like:用户收藏/点赞记录

用户行为表是推荐算法的原料库,没有这些数据,算法就是空转。我在设计的时候给user_rating加了一个score字段(1到5分),同时加了一个source字段,标记这条评分是用户主动评的,还是系统根据观看行为折算的。为什么要区分?因为主动评分的权重应该更高,用户特意给5分,说明真的很喜欢,这个信号比看了30分钟折算出的4分更可靠。

2.4 表结构设计的心得

做这个项目我最大的体会是:表结构设计不要一上来就追求完美,但核心字段一定要想清楚再动手。比如status这个字段,很多人会忽略,等做后台管理的时候才发现没法“下架”一部电影,只能删数据,这就很难受了。

另外,时间字段统一用datetime,不要用timestamp,也不要存字符串。排序、筛选、统计都要靠时间字段的语义正确性,字符串比较时间早晚会出各种奇葩问题。

3. 推荐系统实现思路

3.1 协同过滤算法选型

推荐算法原理看起来高大上,落到实处就是算“相似度”。协同过滤分两类:

  • 基于用户的协同过滤:跟你口味相似的人也喜欢这部电影,所以你也可能喜欢
  • 基于物品的协同过滤:你喜欢看的这部电影,和另一部电影有相似的特征(比如同时被很多人喜欢),所以你也可能喜欢另一部

在电影网站这个场景下,我推荐用基于物品的协同过滤。为什么?因为电影的数量相对用户来说是稳定的,一天可能新增几千用户,但电影一天最多新增几十部。物品之间的相似度矩阵可以离线计算,更新频率低,实时计算压力小。而且基于物品的协同过滤还有一个天然优势:可解释性强,你可以直接告诉用户“因为你看了《流浪地球2》,所以推荐给你《星际穿越》”,用户看得懂,接受度就高。

3.2 相似度计算公式

协同过滤的核心是计算物品之间的相似度,最常用的公式是余弦相似度:

在两个电影的评分向量之间计算余弦夹角。实际操作中,不用把全部用户都拉进来算,只取有共同评分行为的用户即可。假设电影A的评分向量是[5, 4, 0, 3](四个用户的评分,0表示未评分),电影B是[4, 5, 0, 2],那余弦相似度就是这两个向量的点积除以模长的乘积。

但这里有个严重问题:原始余弦相似度没有考虑用户评分习惯的差异。有的人给谁都是5分,有的人挑剔得不行,能打个3分就算不错。这就导致评分向量失真。所以我在项目里用了一个改进版:把用户对每部电影的评分减去该用户的历史平均评分,得到“偏差评分向量”,再算余弦相似度。这样每个用户的打分尺度就被归一化了,计算出来的相似度才更有意义。

3.3 推荐结果生成流程

整个推荐流程分三步走:

  1. 离线计算:每天晚上算一次物品相似度矩阵,存到Redis里(或者数据库表),供第二天实时推荐查询使用。数据量小的时候直接内存计算也没问题。

  2. 实时推荐:当用户访问推荐页时,取这个用户最近有正向行为的N部电影(比如最近看过的、评分高的),从相似度矩阵里查每部电影的Top-K相似电影。

  3. 结果融合与过滤:把所有相似电影攒在一起,去掉用户已经看过的、已经评分过的,按“相似度×用户对源电影的评分”加权求和算出预测分,按预测分倒序输出。

这里有个小细节:如果用户是新用户,没有任何行为数据怎么办?不能让他看到空空如也的推荐页。我在系统里做了兜底策略:新用户直接按热度值倒序推,等他有了一两条行为记录后,再切换到协同过滤。这个策略虽然朴素,但非常实用。

3.4 推荐接口设计

后端推荐接口返回的数据结构大概是这样的:

{ "code": 200, "data": [ { "filmId": 101, "title": "星际穿越", "coverUrl": "https://xxx.com/poster.jpg", "rating": 9.4, "reason": "因为你看过《流浪地球2》" }, ... ] }

我特意加了一个reason字段,前端把这句话展示出来,推荐功能的“智能感”一下子就出来了。说白了,推荐算法再牛,用户感受不到,加一句推荐理由,用户就会觉得这个系统真的在“懂我”。

4. 视频流处理与播放

4.1 视频文件转码与切片

线上影院系统的视频播放,我强烈建议走HLS方案。在动手写代码前,得先把视频素材处理成HLS分片格式。这一步可以在服务器上用FFmpeg完成,命令大概是这样的:

ffmpeg -i input.mp4 -codec copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8

解释一下参数:-hls_time 10表示每个分片10秒,-hls_list_size 0表示生成完整的m3u8索引文件(不是直播用的滑动窗口),-codec copy表示不重新编码,直接复制流,速度很快,前提是源文件编码格式是H.264 + AAC。

如果你的源视频不是H.264编码(比如是MKV封装、HEVC编码),浏览器直接播放大概率不支持,这时候就需要转码:

ffmpeg -i input.mkv -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 -f hls output.m3u8

转码会非常吃CPU,一部2小时的电影,在普通服务器上可能要跑几个小时。所以最好提前在本地处理完,把生成好的m3u8和ts分片文件一起传到服务器。这里我不推荐在项目里用Java调FFmpeg做实时转码,一来性能扛不住,二来逻辑复杂,对做项目来说没有必要。

4.2 Vue端播放m3u8的踩坑记录

这是整个项目里我踩坑最多的地方。Vue播放m3u8,最稳妥的方案是引入video.js配合videojs-contrib-hls插件。装依赖:

npm install video.js videojs-contrib-hls

在Vue组件里这样写:

<template> <div> <video ref="videoPlayer" class="video-js vjs-big-play-centered" ></video> </div> </template> <script setup> import { ref, onMounted } from 'vue' import videojs from 'video.js' import 'video.js/dist/video-js.css' const videoPlayer = ref(null) let player = null onMounted(() => { player = videojs(videoPlayer.value, { sources: [{ src: 'https://your-server.com/hls/movie123.m3u8', type: 'application/x-mpegURL' }], controls: true, autoplay: false, preload: 'auto' }) }) </script>

就这么简单?不,实际跑起来会有各种问题,这里列几个典型的:

第一个问题是跨域。如果视频文件和前端页面不在同一个域名下,浏览器会拦截m3u8请求。解决办法是在视频服务端配置CORS头,或者用Nginx做一个反向代理,让前端请求的URL和页面同源。我在这里卡了很久,因为本地开发环境视频服务器地址是localhost:8081,前端是localhost:5173,一播放就报跨域错。最后直接用Nginx代理转发搞定。

第二个问题是videojs-contrib-hls对低版本浏览器兼容性差。你在本地Chrome测试没问题,换成Safari,或者某些老的安卓浏览器,可能就黑屏了。原因是这个插件依赖Media Source Extensions(MSE),老浏览器不支持。如果你要兼容Safari,其实Safari原生支持HLS,可以直接用原生<video>标签播m3u8。这也是为什么很多人看到网上说“video.js不用配hls插件也能播m3u8”——那是Safari的环境。稳妥做法是封装一个播放器组件,检测到Safari就用原生播放,其他浏览器用video.js。

第三个问题是m3u8里的分片路径是相对路径。FFmpeg默认生成的m3u8里,ts分片路径是output0.ts这种相对路径,播放器会基于当前的m3u8地址来拼接。如果你的m3u8地址是http://server.com/hls/movie123.m3u8,而分片文件在/hls/output0.ts,那没问题;但如果分片文件在CDN的另一个路径下,播放器就找不到了。解决方法是转码时在m3u8里写绝对路径,或者用FFmpeg的-hls_base_url参数指定分片的基础URL。

4.3 视频防盗链与流媒体方案

线上影院,视频资源被别站直接盗链播放是很常见的坑。最简单的防盗链手段:HTTP请求头校验Referer,只允许自己的域名过来。后端接口记录用户对电影视频的访问时间,每次下发视频地址时校验状态。更严谨的做法是用带时效的签名URL,比如给m3u8地址拼接一个加密参数,过期时间设置为5分钟。

对于做项目而言,Nginx层配一个Referer校验就够了。如果你有条件,可以考虑用SRS或Nginx-rtmp模块搭流媒体服务器。但如果你只是做毕设或个人项目,Nginx静态文件服务 + FFmpeg预转HLS已经足够,别过度设计。

5. Spring Boot后端核心实现

5.1 项目结构划分

Spring Boot后端我推荐按功能模块划分,而不是按技术层划分。网上大量教程喜欢把项目结构搞成controller、service、mapper三层,然后所有业务代码都往里塞。这种方式在小项目里没啥问题,但一旦模块多了,你会发现controller下面几十个类,找起来非常痛苦。

我用的结构是:

com.example.cinema ├── common // 通用返回结果、异常处理、工具类 ├── config // 配置类(跨域、拦截器、文件上传等) ├── module │ ├── film // 电影模块 │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ ├── entity │ │ └── dto │ ├── user // 用户模块 │ ├── rating // 评分模块 │ └── recommend // 推荐模块 └── CinemaApplication.java

这样做的最大好处是模块自治。你改电影模块的代码,不影响用户模块;你做推荐模块的时候,只需要通过接口调用电影和用户的基础数据,不用去翻别人的Controller。多人协作时尤其好用。

5.2 数据访问层与MyBatis-Plus

数据访问层我用的是MyBatis-Plus,不是原生MyBatis也不是Spring Data JPA。为什么?因为MyBatis-Plus几乎不用写SQL,BaseMapper已经把单表的CRUD全包了,而且还带分页插件,做后台管理系统的列表查询非常省事。

比如电影分页查询,直接在Service里写:

Page<Film> page = new Page<>(current, size); LambdaQueryWrapper<Film> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Film::getStatus, 1) .like(StringUtils.isNotBlank(keyword), Film::getTitle, keyword) .orderByDesc(Film::getHotValue); filmMapper.selectPage(page, wrapper);

这段代码不用写一行XML,条件拼接、排序、分页全解决了。LambdaQueryWrapper的优势是可以用方法引用,字段名不是字符串,编译期就能发现错误,重构的时候也不会因为改字段名导致SQL报错。

复杂查询还是要写SQL的。比如推荐系统要算“电影A被哪些用户评分过”,或者“同一批用户还看过哪些电影”,这些SQL涉及聚合、子查询、多表JOIN。我的做法是:单表简单操作交给MyBatis-Plus,复杂统计查询写在XML里。你别指望一个ORM把所有的SQL都省了,该手写SQL的时候老老实实手写,性能更好,逻辑也更清晰。

5.3 接口设计与统一返回格式

后端接口返回格式不统一,前端联调时想骂人。我在项目里定义了一个统一的返回包装类Result<T>:

@Data public class Result<T> { private Integer code; // 200成功,其他失败 private String message; // 提示信息 private T data; // 业务数据 }

所有接口,不管是成功还是失败,返回的都是这个结构。前端axios封装一个响应拦截器,判断code === 200再处理数据,否则弹出message。这样做的好处是,前端处理异常的逻辑收敛到一个地方,不用每个接口都写一堆错误判断。

RESTful API设计上有个细节:资源用名词复数,不要用动词。/api/films表示电影列表,/api/films/1表示ID为1的电影,/api/films/1/rating表示给ID为1的电影评分。前端看到这样的接口结构,基本不用问后端就能猜出来。

5.4 跨域问题处理

前后端分离项目,跨域是跑不掉的。我的Vue开发服务器跑在5173端口,后端在8080,两个端口不一样就构成了跨域。解决方式有两种:

方式一:后端配置CORS

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

方式二:前端Nginx反向代理(推荐生产环境用)

location /api/ { proxy_pass http://localhost:8080/api/; }

开发环境用方式一,简单直接,本地调试时哪里不对一眼就能看出来。生产环境用方式二,整个系统对外只有一个域名,从源头上规避了跨域问题。注意addAllowedOriginPattern("*")和setAllowCredentials(true)不能随便组合,浏览器会报错,如果你需要携带Cookie跨域,必须明确指定允许的域名,不能全用通配符。

6. Vue前端实现与系统集成

6.1 项目创建与环境变量

前端用Vue3 + Vite创建项目是最快的:

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

装完基础依赖后,还需要装路由、状态管理和UI组件库:

npm install vue-router@4 pinia element-plus

这里就容易踩坑了,网上大量教程还是vue-router@3的写法,你按Vue2习惯去写new Router()会直接报错。Vue3对应的路由是4.x版本,API变成了createRouter({ history: createWebHistory() })。装的时候一定要留意版本号,最好直接指定版本装。

环境变量这块也很重要。前端请求后端,不同的环境有不同的API地址。我在项目根目录创建.env.development和.env.production两个文件:

# .env.development VITE_API_BASE_URL=http://localhost:8080/api
# .env.production VITE_API_BASE_URL=/api

开发环境直连后端端口,生产环境走Nginx同源代理。在代码里用import.meta.env.VITE_API_BASE_URL来读取。注意:只有以VITE_开头的变量才会被Vite暴露给前端代码,其他变量写进去也是白搭。

6.2 路由设计

前端路由分两部分:不需要登录的页面(首页、电影列表、电影详情)和需要登录的页面(个人中心、收藏、评分管理)。我在路由配置里做了一个前置守卫,校验用户是否登录:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

登录后跳回原页面,这个细节很影响体验。用户没登录时点“收藏”,被踢到登录页,登录完又得重新找那个电影,如果系统能直接把他带回刚才的页面,好感度会提升一个档次。

6.3 电影列表页的筛选与分页

电影列表页是整个前端交互最多的页面,包含类型筛选、排序、关键词搜索、分页加载。在Vue3中我用了reactive来管理筛选条件:

const queryParams = reactive({ page: 1, pageSize: 12, keyword: '', typeId: null, sortBy: 'hotValue' })

筛选条件一变,调用后端接口重新拉列表。请求封装在api/film.js里:

export function getFilmList(params) { return request({ url: '/films', method: 'get', params }) }

这里有个容易忽视的细节:分页组件切换页码时要判断是从哪个状态过来的。如果筛选条件和上次一样,只是页码变了,那追加列表就好;如果是新筛选条件,页码要重置为1。不加判断会导致用户筛选完之后,列表停留在之前的高页码,搜出来的结果永远是空的。

6.4 播放器组件的封装与复用

播放器是整个前端最复杂的交互组件。我把它封装成了一个独立的FilmPlayer.vue组件,传入videoUrl和poster,内部自动选择播放方案:

<template> <div class="player-wrapper"> <video ref="playerRef" :poster="poster" controls></video> </div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch } from 'vue' import videojs from 'video.js' import 'video.js/dist/video-js.css' const props = defineProps({ videoUrl: { type: String, required: true }, poster: { type: String, default: '' } }) const playerRef = ref(null) let player = null function isSafari() { return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) } function initPlayer() { if (player) { player.dispose() } if (isSafari()) { player = videojs(playerRef.value, { sources: [{ src: props.videoUrl, type: 'application/x-mpegURL' }], controls: true, autoplay: false }) } else { player = videojs(playerRef.value, { sources: [{ src: props.videoUrl, type: 'application/x-mpegURL' }], controls: true, autoplay: false, html5: { vhs: { overrideNative: true } } }) } } onMounted(initPlayer) watch(() => props.videoUrl, initPlayer) onBeforeUnmount(() => player && player.dispose()) </script>

封装组件后,电影详情页、后台预览页、首页推荐预览都能复用同一个播放器逻辑,不用每个页面都写一遍初始化代码。

6.5 后端接口与前端对接

前后端对接最容易出问题的是字段命名不匹配。Java后端习惯用驼峰命名hotValue,前端如果刚好写成了hotvalue或hot_value,取出来的数据就是undefined,页面渲染出来一堆空。我建议前后端在启动联调之前,先拉一个统一的接口文档,把字段名定死。项目小的话用一个在线表格就行,项目大上Swagger。

前端请求封装时,axios拦截器也是个关键点。我在拦截器里统一做了三件事:给所有请求带上token、对响应码做统一处理、拦截登录失效的状态并跳转登录页:

request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('登录已过期')) } return res }, error => { return Promise.reject(error) } )

这样前端各业务页面的代码就清爽了,不用每个请求都判断状态码。

7. 开发与部署中的常见问题排查

7.1 数据库连接与性能问题

开发阶段遇到最多的是数据库连接问题。MySQL 8默认的认证插件是caching_sha2_password,老版本的驱动连不上,如果你用的是mysql-connector-java5.x驱动,会在启动时报Unable to load authentication plugin。解决方式两种:换8.x驱动,或者在MySQL里把用户的认证插件改回mysql_native_password。

分页查询性能是另一个容易出问题的点。MyBatis-Plus的分页插件在做大偏移量分页时,底层是LIMIT offset, size。当用户翻到第100页的时候,数据库还是要从头数过去,非常慢。优化方式是改为“游标分页”或“时间线分页”,也就是前端传的上一次最后一条记录的ID,后端用WHERE id < ?来取下一页。这在视频列表页很重要,用户一直往下刷,深度翻页场景下性能差异非常明显。但对做毕设来说,普通的分页插件就够用了,知道有这个优化思路就行。

7.2 Vue项目常见报错与解决方案

先列三个我最常遇到的报错:

tsconfig not found这个报错出现在Vite创建Vue项目之后,如果你手动改了tsconfig相关文件又没配对路径,启动时会直接挂掉。解决办法是检查tsconfig.json、tsconfig.node.json里的extends路径,确保指向的@vue/tsconfig/tsconfig.web.json确实存在。

failed to load tsconfig的另一种情况,是你用了pnpm但没装@vue/tsconfig这个包。直接用npm install -D @vue/tsconfig补上即可。

Module not found: 'video.js/dist/video-js.css'这个错,多半是video.js版本或者路径的问题。检查一下node_modules/video.js/dist/目录,确认css文件真的在这里。如果实在找不到,尝试import 'video.js/dist/video-js.min.css'。

还有一个是播放m3u8时控制台报Failed to fetch,这大概率是跨域或m3u8地址错误。先用浏览器直接访问这个m3u8地址,看能不能打开。如果浏览器直接访问都打不开,那就是URL写错了或服务端文件路径不对。如果浏览器能打开,但播放器加载不到,那重点查跨域。

7.3 前后端分离部署细节

部署环节最容易翻车的是前端打包路径和服务器路由配置。

前端打包默认生成的是相对路径引用的SPA应用,如果你把dist目录部署在Nginx根目录下,一切正常。但如果部署在子路径下,比如http://server.com/cinema/,打包的时候必须指定base:

npm run build -- --base=/cinema/

否则所有JS、CSS资源都指向根路径,页面打开之后白屏一片,控制台全是404。

Nginx配置SPA路由回退也是一个必踩的坑。Vue Router用的是history模式,URL路径是/film/123这种,不是#/film/123。用户直接访问这个地址,Nginx会去找server.com/film/123这个文件,当然找不到,返回404。必须在Nginx里加:

location / { try_files $uri $uri/ /index.html; }

意思是不管用户访问什么路径,只要不是真实文件,就把请求交给index.html,让它来决定渲染哪个页面。如果你忘了加这一行,用户的每个深链接分享出去,对方打开全都是404。

后端部署我用的是Docker加docker-compose,配置在宝塔面板里管理。有一说一,宝塔的Docker面板确实降低了不少部署门槛,不用在命令行里搞一堆Linux操作。但要注意一点:后端容器里的MySQL、Redis、Spring Boot服务之间通信用容器内网IP或服务名,别用localhost,否则容器之间互相访问不到。

7.4 推荐系统数据过少时的兜底策略

最后聊一个推荐系统上线之初的尴尬局面:系统刚部署,没有任何用户行为数据,协同过滤算法根本跑不起来。这时候如果用户打开推荐页,看到的是一个空列表,那体验就太差了。

我做了两层兜底。第一层,新用户直接推热度榜Top20,也就是按hot_value倒序取数据,这算是一个“冷启动推荐”。第二层,当用户产生了行为数据但数量很少(比如只看了2部电影),就基于这些影片所属的类型做扩充,把同类型热度最高的电影推过来。等用户行为攒到一定量级后,再切换成协同过滤。这个递进策略让我在数据稀少的情况下也保住了推荐页的可用性。

很多人在做这类项目时,容易在算法上死磕,觉得要搞深度学习才算推荐系统。其实对于Spring Boot加Vue这种技术栈的阶段,能把协同过滤吃透、把推荐流程跑通、把冷启动兜住,已经超过了绝大多数同类项目。真正的工程能力,不在于算法有多高级,而在于系统在各种极端情况下都还能正常为用户服务。

在项目运行过程中我还加了一个小功能:定时统计每部电影的观看次数,每播放一次就更新hot_value。这个动作虽然简单,却让热度榜、冷启动推荐和新用户首屏都显得“活”了起来。前端首页放着“热门电影”和“猜你喜欢”两个板块,前者看热度,后者看推荐。配合播放器组件和m3u8分片流的流畅播放,整个项目跑起来之后,观感上已经相当接近一个能上线的产品了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询