1. 项目概述与整体设计定位
1.1 这个平台要解决什么问题
做动漫交流与推荐平台这个项目,说到底是源于我自己看番时的一个真实痛点:每个季度新番几十部,BD发售、剧场版、OVA各种资源分散在不同站点,想找一部符合口味的动画,往往要在好几个地方来回折腾。即便找到了,评论区、弹幕、推荐列表又是割裂的,看完一部想找同类型的下一部,基本靠缘分。所以干脆自己动手做一个集“动漫信息展示、用户交流讨论、个性化推荐”于一体的平台,把“看什么”和“和谁聊”这两个需求放在同一个系统里解决。
这个项目定位很明确,就是课程设计或毕业设计级别的全栈实战项目,技术栈采用Spring Boot + Vue,覆盖前后端分离开发、数据库设计、接口联调、打包部署的完整链路。对于正在学Java后端、想找一个完整项目练手的同学来说,它的性价比非常高——麻雀虽小五脏俱全,用户系统、内容管理、评论互动、推荐算法、视频播放这些业务场景,几乎包含了企业级Web开发最常见的技术点。
1.2 技术栈选型为什么是Spring Boot + Vue
先说说技术栈的问题。市面上做这类系统的组合很多,SSM、SSH、Django、Flask都能做,但我最终选了Spring Boot + Vue,理由很实在。
后端选Spring Boot,是因为它在Java生态里已经是事实上的默认起点。Spring Boot通过自动配置把Spring MVC、MyBatis、事务管理这些东西全部串起来了,我不用再去写一堆XML配置。尤其是做课程设计这种时间紧、任务重的项目,Spring Boot能让我把精力放在业务逻辑而不是环境搭建上。而且Spring Boot内置Tomcat,打包成jar直接跑,部署成本很低,这对后面交作业或者给老师演示都很方便。
前端选Vue,是因为它的学习曲线相对平缓,而且社区资料特别多。Vue的双向数据绑定让表单交互变得非常简单,组件化开发也方便我把导航栏、动漫卡片、评论区这些UI拆成独立组件复用。更重要的是,Vue生态里的Element UI组件库能直接提供表格、分页、弹窗、表单校验这些现成的组件,整个后台管理界面可以很快搭出来,视觉效果也不会太差。
当然,前后端分离带来的跨域问题、鉴权问题、联调问题也是必须要面对的,这些后面我单独细说。
1.3 功能模块拆解
这个平台按业务域拆,大概分这么几块:
- 用户模块:注册、登录、个人信息查看与修改。登录用JWT做无状态鉴权,用户密码用MD5加盐存储,权限上区分普通用户和管理员。
- 动漫内容模块:动漫信息的管理与展示,包括封面、简介、类型标签、集数、评分、播放地址等字段。管理员可以新增、修改、下架动漫;普通用户可以查看详情、搜索、按分类筛选。
- 交流模块:围绕单部动漫的评论区,用户可以发表评论、回复他人评论,管理员可以删除违规评论。
- 收藏与评分模块:用户可以收藏感兴趣的动漫,可以给动漫打分,评分会汇总到动漫的平均分中。
- 推荐模块:基于用户的收藏记录、评分记录和浏览记录,结合动漫的标签特征做个性化推荐,首页展示“猜你喜欢”和“热门推荐”两个列表。
整个项目的核心亮点其实在推荐模块。课程设计类的项目很多都是简单的CRUD,但如果推荐模块能跑起来,整个项目的完成度会明显高一个档次,答辩时也更有说头。
2. 数据库设计与核心表结构
2.1 数据模型规划
数据库设计是整个项目的地基。我这版用的是MySQL 8.0,数据库名直接叫anime_platform,字符集选utf8mb4。这里特别提醒一下,千万不要用utf8,因为动漫简介和评论里经常会有生僻字或者特殊符号,utf8mb4才能完整支持四字节的Unicode字符,比如emoji。
我在建表之前,先把实体关系梳理了一遍:用户和动漫是多对多的关系,中间通过收藏表、评分表连接;用户和评论是一对多的关系;动漫和标签是多对多的关系,通过动漫标签关联表连接;推荐结果可以基于用户画像实时计算,也可以预计算后存表。为了方便演示和答辩讲解,我采用的是“实时计算为主、预计算结果表为辅”的混合方案。
2.2 用户与权限表
用户表字段不多,但有几个关键点需要想清楚:
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码(加盐MD5)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `role` tinyint DEFAULT '0' COMMENT '角色:0普通用户 1管理员', `status` tinyint DEFAULT '0' COMMENT '状态:0正常 1封禁', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;密码存储我用的是MD5加盐,盐值就存在密码字段里,格式是盐值:MD5(盐值+密码)。虽然在生产环境更推荐BCrypt,但课程设计里MD5加盐足够应付,而且实现起来非常简单,答辩时能清晰讲出盐值的作用就可以了。
JWT这块,我用的是jjwt库,生成token时把用户id和role塞进去,过期时间设7天。后端写一个拦截器,从请求头的Authorization字段取出token并解析,解析成功就把用户信息放到ThreadLocal里供后续业务使用。
2.3 动漫与标签表
动漫表是整个系统内容层的核心,字段设计直接决定了前端展示的完整度。
CREATE TABLE `anime` ( `id` int NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '动漫名称', `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图地址', `description` text COMMENT '简介', `type` varchar(50) DEFAULT NULL COMMENT '类型:TV/剧场版/OVA', `region` varchar(50) DEFAULT NULL COMMENT '地区:日本/中国/欧美', `year` int DEFAULT NULL COMMENT '播出年份', `episodes` int DEFAULT NULL COMMENT '总集数', `status` tinyint DEFAULT '0' COMMENT '连载状态:0连载中 1已完结', `play_url` varchar(500) DEFAULT NULL COMMENT '播放地址(m3u8)', `avg_score` decimal(3,1) DEFAULT '0.0' COMMENT '平均评分', `favorite_count` int DEFAULT '0' COMMENT '收藏数', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有个设计心得:avg_score和favorite_count这两个字段是冗余字段。本来评分和收藏都可以通过联表查询实时统计,但那样在首页列表展示时会产生大量SQL查询,性能很难看。我的做法是写一个定时任务,没半小时重新聚合一次评分和收藏数,更新回冗余字段。数据库表结构里加上这两个字段,后面写推荐算法的热度排序就直接用,省去了联表查询的开销。
标签这块,我建了tag表和anime_tag关联表,一部动漫可以有多个标签,一个标签也可以对应多部动漫。标签字段类似“热血”“校园”“恋爱”“科幻”“治愈”“冒险”“搞笑”“战斗”这样的维度。标签一定要在数据库初始化时就配合示例数据一起备好,否则后面推荐算法没有数据可以算。
2.4 评分、收藏、评论等交互表
交互表的共同特点是需要做唯一约束,防止同一个用户对同一部动漫产生多条记录,这是很多新手容易踩的坑。
CREATE TABLE `favorite` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `anime_id` int NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_anime` (`user_id`,`anime_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `rating` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `anime_id` int NOT NULL, `score` tinyint DEFAULT NULL COMMENT '1-10分', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_anime` (`user_id`,`anime_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `comment` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `anime_id` int NOT NULL, `parent_id` int DEFAULT '0' COMMENT '0表示顶级评论,否则为回复的评论id', `content` varchar(1000) NOT NULL, `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;评论表里加parent_id字段是为了支持楼中楼回复。只做一级评论的话,两个字段就够:评论对象和评论内容。但加了parent_id之后,评论区可以递归查询出完整对话链,展示效果会好很多,这个字段强烈建议保留。
评分表里的score字段是1到10分制。之所以不用5星,是因为动漫评分站的惯例普遍是10分制,而且10分制的粒度更细,对推荐算法的输入也更友好。
2.5 推荐算法对表结构的依赖
推荐模块的数据来源,本质上就是用户在交互表中留下的行为轨迹。用户在favorite表里收藏了哪些动漫,在rating表里给哪些动漫打了高分,这些数据构成了用户兴趣的原始信号。
我在设计表结构时特意预留了一个user_preference表,用来存放每个用户的标签偏好权重,比如“用户A在热血标签上的权重是0.8,在恋爱标签上的权重是0.3”。这个表可以离线计算后批量更新,推荐接口查询时直接从这张表取数据,避免了推荐计算阻塞主流程。如果答辩时老师问推荐算法怎么优化,就可以说:用户行为实时写入流水表,标签偏好离线批量聚合,线上推荐只做TopN排序,这就把在线和离线很好地拆开了。
3. 后端Spring Boot实现要点
3.1 项目结构与初始化
后端项目是用IDEA直接创建的Spring Initializr项目。这里有个热词大家搜得很多——“idea创建springboot项目”,我多说一句:创建时Spring Boot版本不要一味追新,建议选择2.7.x系列。因为3.x版本之后javax包名改成了jakarta,很多老教程和依赖的写法都不兼容,课程设计阶段用2.7.x最稳,依赖好找、教程也对得上。
后端包结构我按领域划分:
com.anime.platform ├── common // 统一返回结果、异常处理、常量 ├── config // 跨域配置、拦截器注册 ├── controller // 控制器层 ├── service // 业务逻辑层 ├── mapper // MyBatis数据访问层 ├── entity // 实体类 ├── dto // 入参出参对象 ├── util // JWT工具类、MD5工具类 └── task // 定时任务创建完项目后有几个最基本的依赖需要加:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、jjwt、hutool-all。hutool这个工具库强烈推荐,里面有很多现成的工具方法,比如Redis缓存封装、日期处理、随机数生成,能省下大量重复代码。
配置文件application.yml里,有几点需要特别注意:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/anime_platform?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.anime.platform.entity configuration: map-underscore-to-camel-case: trueserverTimezone=Asia/Shanghai这个参数特别重要,不加的话数据库连接会报时区错误。map-underscore-to-camel-case必须设为true,数据库的蛇形命名(user_name)才能自动映射到实体类的驼峰属性(userName)。
3.2 登录鉴权与统一返回
统一返回结果是我在所有Controller里都要用到的基础类,我用泛型定义了一个Result类,返回格式固定为状态码、消息、数据三个字段。这样做的好处是前后端联调时接口格式高度一致,前端axios的响应拦截器只需要处理一种结构。
前端登录成功的流程是这样:后端接收用户名密码,校验通过后生成JWT返回给前端,前端把token存在localStorage里。之后每次请求,axios请求拦截器都会从localStorage取token并加到请求头的Authorization字段。后端的拦截器再统一解析token,如果没有token或token过期,直接返回401。
这里有一个很实用的处理方式:我写了一个注解叫@RequireLogin,标注在需要登录才能访问的接口上。拦截器根据HandlerMethod是否标注了该注解来决定要不要校验token,这样就避免了写一堆繁琐的路径白名单配置。
3.3 推荐接口的实现思路
推荐接口是整个项目最有技术含量的部分,也是我答辩时的重点展示内容。实现方式分三层,由浅入深:
第一层是热度推荐,对所有用户一视同仁。计算公式是:
hotScore = avg_score * 0.5 + log(favorite_count + 1) * 10 + (now - publish_time) / 86400 * 0.1平均分越高、收藏越多、发布时间越近,热度值越高。这个公式里用log是为了让收藏量大幅增长时热度不会线性膨胀,老番不会被新番碾压得太惨。这个公式没有标准答案,核心逻辑要能自圆其说,答辩时能解释清楚每个系数为什么这么设就行。
第二层是标签匹配推荐,计算用户和动漫之间的偏好匹配度。用户在收藏和评分过的动漫中提取标签分布,得到每个标签的权重,然后和候选动漫的标签集合做向量点积。比如用户喜欢热血和战斗,权重分别为0.8和0.6,候选动漫A也带这两个标签,匹配度就很高。
第三层是协同过滤的简化版。找到与当前用户收藏行为最相似的K个用户,把这K个用户收藏过但当前用户没收藏过的动漫按出现频次排序推荐出来。完整版协同过滤需要算用户相似度矩阵,数据量大,课程设计里我写了个简化版本,只查最近登录的100个用户两两之间的共同收藏数做相似度,效果已经足够说明算法原理了。
推荐接口的实现大致是这样:
public List<AnimeVO> recommend(Integer userId, int size) { if (userId == null) { return getHotAnime(size); } List<AnimeVO> tagBased = getTagBasedRecommend(userId, size / 2); List<AnimeVO> cfBased = getCollaborativeRecommend(userId, size / 2); // 合并去重 Map<Integer, AnimeVO> result = new LinkedHashMap<>(); for (AnimeVO vo : tagBased) result.putIfAbsent(vo.getId(), vo); for (AnimeVO vo : cfBased) result.putIfAbsent(vo.getId(), vo); // 如果不足,用热度推荐补齐 if (result.size() < size) { for (AnimeVO vo : getHotAnime(size * 2)) { result.putIfAbsent(vo.getId(), vo); if (result.size() >= size) break; } } return new ArrayList<>(result).subList(0, Math.min(size, result.size())); }这个分层策略的好处是:没登录也能推荐(热度兜底),登录了但历史行为少也能推荐(标签匹配有数据),历史行为丰富后协同过滤逐渐发挥效用。整个推荐逻辑层层递进,任何状态下的用户都能拿到像样的推荐列表。
3.4 分页、联表与性能优化
动漫列表页、搜索结果页、后台管理列表都需要分页。我这里直接用MyBatis手工写分页SQL,没有引入PageHelper依赖。原因很简单:手工写limit可以更好地控制查询逻辑,也容易排查问题。
分页接口统一返回PageResult对象,包含total、pageNum、pageSize、list四个字段。前端配合Element UI的el-pagination组件使用,非常顺滑。
动漫列表页的关键SQL是带条件筛选的联表查询:
SELECT a.*, GROUP_CONCAT(t.name SEPARATOR ',') AS tags FROM anime a LEFT JOIN anime_tag at ON a.id = at.anime_id LEFT JOIN tag t ON at.tag_id = t.id WHERE (#{type} IS NULL OR a.type = #{type}) AND (#{region} IS NULL OR a.region = #{region}) AND (#{keyword} IS NULL OR a.title LIKE CONCAT('%', #{keyword}, '%')) GROUP BY a.id ORDER BY a.avg_score DESC, a.favorite_count DESC LIMIT #{offset}, #{pageSize}这里有一个需要特别注意的坑:GROUP BY a.id配合ORDER BY时,如果MySQL的sql_mode包含ONLY_FULL_GROUP_BY,查询可能会报错。解决办法是修改MySQL的sql_mode,或者把GROUP BY扩展成GROUP BY a.id, a.title, ...等于所有查询字段全列一遍。我是直接改sql_mode去掉了ONLY_FULL_GROUP_BY,开发时省事,答辩时也容易解释。
另外,LIKE CONCAT('%', #{keyword}, '%')一定要用CONCAT来拼接,而不是直接用'%#{keyword}%'——后者等于在SQL里写死了%#{keyword}%这个字符串,查不到任何数据。这个坑非常经典,几乎每届新生都会踩一次。
4. 前端Vue实现与播放痛点
4.1 前端项目结构与路由
前端我用的是Vue 2 + Element UI,因为这套组合的教程最丰富,遇到问题随便一搜就能找到答案,对课程设计来说效率最高。如果你的项目时间充裕,也可以上Vue 3 + Element Plus,但配套组件的写法差异不小,需要多花点时间适应。
前端项目结构:
src ├── api // axios请求封装 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Vuex状态管理 ├── views // 页面组件 │ ├── Home.vue │ ├── AnimeList.vue │ ├── AnimeDetail.vue │ ├── Login.vue │ ├── Register.vue │ ├── Profile.vue │ └── admin │ ├── AnimeManage.vue │ └── CommentManage.vue └── main.js路由配置里需要做两件事:一是给需要登录的页面加meta: { requiresAuth: true },路由前置守卫里判断有没有token;二是给管理员相关页面加meta: { role: 1 },进一步校验用户角色。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role && JSON.parse(localStorage.getItem('user')).role !== 1) { next('/') } else { next() } })4.2 视频播放:m3u8格式的接入方案
动漫平台免不了要处理视频播放。你如果搜过“vue播放m3u8”或“vue视频m3u8”这类热词,应该知道m3u8是HLS流媒体协议的索引文件格式,视频被切成很多个.ts小分片,播放器通过索引文件按顺序拉取播放。
浏览器原生video标签不支持m3u8格式,必须借助插件。我试过两种方案:
一种是video.js配合videojs-contrib-hls插件,优点是社区成熟、文档多,缺点是配置项繁琐,样式定制麻烦。
另一种是hls.js,轻量且直接,使用起来非常顺手:
import Hls from 'hls.js' // 在Vue组件中 if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(this.playUrl) // this.playUrl就是m3u8地址 hls.attachMedia(this.$refs.video) hls.on(Hls.Events.MANIFEST_PARSED, () => { this.$refs.video.play() }) }我最终选的是hls.js,代码量少很多,出问题也容易排查。这里要提醒一下:如果m3u8文件和前端页面部署在不同的域名下,跨域问题可能导致播放黑屏,解决办法是在视频资源的响应头加上Access-Control-Allow-Origin。
4.3 评论、收藏与推荐列表的交互实现
评论区的实现核心是组件递归。顶级评论渲染一层CommentItem组件,如果该评论有回复,组件内部再递归渲染CommentItem。Vue组件是支持自身递归调用的,只要在组件里设置name属性就可以。
收藏和评分做成一个联动交互:用户点击收藏按钮,向后端发送POST请求,成功后按钮状态变成“已收藏”,同时动漫详情的收藏数字加一。评分用的是Element UI的el-rate组件,设置允许半分,用户评分后立刻调接口,成功后刷新平均分展示。
推荐列表的渲染不建议一次性请求10条以上数据,最好是首页只展示8条,用横向卡片滚动展示。每个卡片包含封面、标题、标签、平均分。卡片点击跳转到动漫详情页。接口返回的推荐列表已经按得分排序,前端只需要顺序渲染。
这里说得直白一点:推荐模块的UI展示,本质上就是拿后端推荐接口的数据去做卡片列表。但答辩时重点不是UI,而是推荐逻辑的完整闭环——用户行为如何采集、标签权重如何计算、推荐结果如何排序。
4.4 打包部署与布局异常处理
前端开发完以后,执行npm run build打包,会生成dist目录。这里有一个非常经典的问题,如果你搜过“vue打包后布局异常”,八成是遇到了以下两种情况之一。
第一种是静态资源路径不对。默认配置下build后的CSS和JS引用路径是绝对路径/js/app.js,但部署到子目录时根本找不到文件。解决办法是在vue.config.js里设置:
module.exports = { publicPath: './' }这样资源引用会变成相对路径,无论是部署在Nginx根目录还是子目录都能正常工作。
第二种是路由使用了history模式但服务端没有配置对应的fallback。Vue Router的history模式刷新页面时会向服务器请求真实路径,如果Nginx只配置了location /指向index.html,次级路径刷新就会404。两种解决办法:Nginx加配置try_files $uri $uri/ /index.html;,或者前端改用hash模式。课程设计演示阶段,用hash模式最省心,永远不会404。
5. 运行环境配置与部署流程
5.1 本地开发环境准备
拿到源码或者自己重新搭项目时,环境版本对齐是非常影响心态的一件事。我这套项目使用的版本是:JDK 1.8、Maven 3.8、Node 16、MySQL 8.0、Spring Boot 2.7。如果你机器上装的是JDK 17以上的版本,建议还是装一个JDK 8配上,因为Spring Boot 2.7在JDK 8下最稳定,学校机房和大部分同学的机器也普遍是这个版本。
Maven依赖下载慢是国内开发者的老难题,解决方式就是配置阿里云镜像。在Maven的settings.xml里加:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>前端依赖安装同理,用npm config set registry https://registry.npmmirror.com切换淘宝源,npm install的速度会快很多。
5.2 数据库初始化与数据填充
项目自带的SQL文件一般是两个:一个是schema.sql,建表语句;一个是data.sql,初始化数据。导入时直接用Navicat或其他数据库工具执行SQL脚本即可。
初始化数据这块,我强烈建议至少准备50部动漫和200条评论。动漫数据如果自己手动录太费劲,可以找公开的动漫列表数据然后写脚本转换。每部动漫至少打3个标签,这样推荐算法才有足够的标签匹配空间。200条评论分布在50部动漫里,每部平均4条,评论区看起来不空旷,冷启动的“猜你喜欢”也有数据可以算。
数据库导入完成后,建议写一个简单的验证SQL,统计表行数和关键字段的非空率:
SELECT COUNT(*) FROM anime; SELECT COUNT(*) FROM user; SELECT COUNT(*) FROM comment;如果动漫表和评论表数量级正常,就说明数据导入成功了。
5.3 前后端联调与Nginx部署
开发阶段,前端需要通过代理解决跨域问题。在vue.config.js里配置devServer:
devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端请求/api/user/login,开发服务器会代理到后端的http://localhost:8080/user/login。
生产部署,Nginx配置直接贴出来参考:
server { listen 80; server_name localhost; root /usr/share/nginx/html/dist; index index.html; location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里的关键是location /api/的代理规则,把前端发往/api的请求转发到后端8080端口,同时注意proxy_pass后面的/会把/api前缀去掉。如果你的后端Controller里没有统一加/api前缀,这个配置正好,省一层路径前缀。
6. 常见问题与排查实录
6.1 后端启动与数据库连接问题
最常遇到的启动失败就是数据库连不上。Access denied for user 'root'@'localhost'十有八九是密码不对或者账号没有远程权限。本地开发用root账号就够了,密码别用默认的123456,容易被各种安全工具扫描到。
时区报错The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,是因为数据库时区没有设置。两种解决办法,一是在启动类里加参数-Duser.timezone=GMT+08,二是JDBC URL里加serverTimezone=Asia/Shanghai,我选择后者,改一处就够。
端口占用的报错是Port 8080 was already in use,Windows下用netstat -ano | findstr 8080找到占用进程,然后taskkill /PID 进程号 /F杀掉即可。这条命令几乎每次复盘都会用到。
6.2 前端依赖与环境问题
前端最常见的报错就是npm ERR! code ERESOLVE,通常是依赖版本冲突。解决办法是升级Node或换用npm install --legacy-peer-deps。课程设计阶段不建议追求太新的依赖版本,锁定在Node 16 + Vue 2.7 + Element UI 2.15是经过验证的稳定组合。
另外就是“idea创建springboot项目”时Initializr选择不到合适的Spring Boot版本。IDEA里新建项目时选择Spring Initializr,Server URL可以切换成https://start.spring.io或阿里云的https://start.aliyun.com,前者是最新版本,后者有很多国内开发者常用的稳定版本,两者结合使用基本不会卡住。
6.3 m3u8播放黑屏排查
m3u8播放黑屏,我排查过三轮,核心原因基本锁定在这三个方向:
第一,URL是否正确。先用VLC播放器直接打开m3u8地址,如果能播放说明视频源没问题;VLC都放不了,就去检查资源地址本身。
第二,CORS跨域。检查浏览器开发者工具Network标签页里,m3u8请求和ts分片请求是否因为跨域被拦截。如果是,需要在Nginx或后端给视频资源加跨域响应头。
第三,hls.js版本问题。有些老版本hls.js对HLS AES-128加密流的支持不完整,会出现音画不同步或黑屏。升级到最新版本,或者换用video.js重试。
排查顺序建议是:先用VLC确认视频源,再看浏览器Network确认请求状态,最后排查JavaScript报错。从源到端一层一层查,通常10分钟能定位。
6.4 其他杂项问题
数据库同步相关的问题,很多人用Navicat同步远程数据库时遇到卡死,多半是网络延迟或表结构冲突。本地开发不需要搞复杂的同步工具,直接导出SQL文件拿到本地执行最可靠。
另外一个非常隐蔽的问题:MySQL 8.x默认认证插件是caching_sha2_password,而一些老版本JDBC驱动不支持,连接时报错Unable to load authentication plugin。解决办法是换最新的mysql-connector-java驱动,或者在创建用户时指定mysql_native_password加密方式。
7. 项目复用与二次开发建议
这套源码的价值不只在于交作业,它其实是一套可以不断扩展的基础框架。如果你拿到源码后不满足于跑通演示,我建议从这几个方向去扩展。
第一个方向是加缓存。现在每次查询动漫详情都会命中数据库,并发上来之后扛不住。把热点动漫的详情、首页推荐列表用Redis缓存起来,TTL设置5分钟,QPS能提升一个数量级。这个扩展点的技术价值非常大,答辩时是加分项。
第二个方向是把推荐算法做得更细。目前用的标签匹配和简化协同过滤,可以升级成基于用户的完整协同过滤,引入用户相似度矩阵的离线计算;也可以引入TF-IDF做内容关键词提取,构建更精准的动漫画像。
第三个方向是后台管理功能的完善。目前后台能管理动漫和评论,还可以增加用户管理、标签管理、数据看板。数据看板用ECharts展示用户增长曲线、动漫评分分布、热门标签排行,整个平台的“产品感”会强很多。
第四个方向是社区的深度互动。现在的评论区只有文字,可以扩展点赞功能、用户关注的动态流、动漫专题推荐页等。每加一个功能,数据库就要新增对应的表或字段,整个项目逻辑会越来越完整。
我在做这个项目时,最大的体会是:全栈开发最难的往往不是单个技术点,而是把数据链路打通——从数据库表设计,到后端接口输出,再到前端页面渲染,一条业务线完整走通后,其他功能都是在复制这个模式。做“动漫详情页”是这样,做“评论列表”也是这样,做“推荐列表”还是这样。掌握了一个完整闭环,整个项目的扩展就只是时间问题。