最近被问得最多的一个问题:毕业设计或课程设计想做一个在线音乐播放系统,技术栈选什么、代码拿到手后怎么跑起来、播放器怎么调通、推荐功能怎么做才不会显得“假”。这套“Java + Spring Boot + Vue 前后端分离”的组合,几乎是当前最主流也最能打的方案,既能覆盖后端接口开发、数据库设计,又能体现前端交互和工程化能力,写在简历上也不寒碜。
这篇东西我就按实际做项目的顺序来聊,从架构拆分、后端数据访问、前端播放器,到联调部署和推荐模块,最后附上我踩过的坑。不管是刚拿到源码想跑通,还是打算从零自己写一版,照着这条路走基本不会卡死。
1. 系统架构拆解:前后端分离到底拆什么
1.1 为什么Spring Boot + Vue是当前主流组合
在线音乐系统天然适合前后端分离,因为它的核心场景是:用户在浏览器里浏览歌单、点击播放、收藏评论,管理员在后台上传歌曲和维护信息。前端负责交互和播放器控制,后端负责歌曲资源管理、用户数据和推荐计算。两边通过JSON接口通信,互不掺和。
Spring Boot做的事情相当于“后厨的标准餐线”:食材(数据库)进来,按菜单(Controller接口)加工,出餐成JSON。它把Tomcat内嵌在应用里,依赖管理也做得很省心,一个spring-boot-starter-web就把Web环境拉起来了。Vue则是“前厅的点菜界面”,组件化开发让页面拆成一块块积木,列表页、播放器、歌单详情各自独立,状态管理再把这些积木串起来。
这套组合之所以流行,还因为它好学、好找工作。Spring Boot的生态成熟,社区资料多,遇到问题基本都能搜到答案;Vue在国内前后端分离项目里的占有率一直很高,企业里大量系统都在用。对做毕设的人来说,选这个技术栈,答辩时老师问“为什么前后端分离”,你可以直接回答:解耦、并行开发、独立部署、接口复用,这四个词比什么都实在。
1.2 工程结构与核心模块划分
拿到或者自己创建项目时,我建议把工程分成两个独立目录:online-music-backend 和 online-music-frontend。不要放到同一个Maven模块里硬凑,前后端分离的意义就在于互不依赖、可以独立启动。
后端按职责分层,包结构大致这样:
text复制com.example.music
├── controller // 接收请求,返回JSON
├── service // 业务逻辑,推荐计算、播放统计
├── mapper // MyBatis-Plus的BaseMapper接口
├── entity // 数据库实体类
├── config // 跨域配置、拦截器配置
├── common // 统一返回结果、异常处理
└── utils // 推荐算法、JWT工具类
前端则按页面和组件划分:
text复制src
├── api // axios请求封装
├── views // 页面:首页、歌单详情、播放页、后台管理
├── components // 组件:播放器、歌曲卡片、评论列表
├── router // 路由配置
├── store // Pinia或Vuex状态管理
└── assets // 静态资源
模块划分的核心思想是“关注点分离”。后端Service里不要堆SQL,Mapper里不要写业务判断;前端组件里不要直接发axios请求,统一走src/api。这样做的直接好处是,出问题的时候你能很快定位——播放器卡了去查components/Player.vue,接口返回500去查后端日志,而不是在几百行代码里大海捞针。
1.3 数据表设计:从实体到建表
音乐系统至少要有这些表:用户表、歌手表、歌曲表、歌单表、歌单歌曲关联表、评论表、收藏表、播放记录表。重点是歌曲表,它得承载播放器需要的所有信息:
sql复制CREATE TABLE `song` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '歌曲名称',
`singer_id` bigint DEFAULT NULL COMMENT '歌手ID',
`album` varchar(100) DEFAULT NULL COMMENT '专辑',
`duration` int DEFAULT NULL COMMENT '时长,秒',
`url` varchar(500) DEFAULT NULL COMMENT '播放地址',
`cover` varchar(500) DEFAULT NULL COMMENT '封面图',
`lyric` text COMMENT '歌词',
`tags` varchar(255) DEFAULT NULL COMMENT '风格标签,逗号分隔',
`play_count` int DEFAULT 0 COMMENT '播放次数',
`status` tinyint DEFAULT 1 COMMENT '1上架 0下架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计表的时候有一个坑:别把播放地址只设计成“上传到本地的文件路径”。实际开发中,歌曲资源可能放在服务器磁盘、OSS对象存储或者其他CDN源上,所以我一般存的是完整的可访问URL,播放器直接拿这个URL去播。封面图和歌词同理,存URL比存Base64字符串优雅得多,数据库不会迅速膨胀。
用户行为相关的表要注意冗余字段的权衡。比如播放记录表,既要有user_id、song_id、play_time,还可以冗余一个song_tags字段,这样做推荐时少一次关联查询。毕设阶段没必要过度设计,三范式不是死规矩,查询效率和分析方便才是真需求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Spring Boot后端搭建与数据访问
2.1 版本选择:Spring Boot 2.7还是3.x
这一步是很多人拿到源码后第一个翻车点。Spring Boot 3.x发布后,新教程都喜欢直接上3.x,但如果你自己搭项目或者拿到的源码还是2.x,就会遇到“springboot版本太高”带来的连锁反应。
最核心的差异在javax和jakarta包名。Spring Boot 2.x用的还是javax.servlet,Spring Boot 3.x把包名整体换成了jakarta.servlet。如果你的项目里引了老版本的MyBatis-Plus(3.5.3以下)或者老版本的其他依赖,启动时会直接报ClassNotFoundException: javax.servlet.Servlet之类的错。
我个人的建议:稳定优先,用Spring Boot 2.7.x。它对JDK 8和JDK 11都友好,MyBatis-Plus、Druid这些配套依赖几乎不用操心兼容性。如果你非要体验Spring Boot 3.x,也可以,但必须把MyBatis-Plus升到3.5.3以上,并且检查所有第三方依赖是否完成了jakarta迁移。还是那句话,对毕设和中小型项目来说,版本新不代表体验好,运行稳定才是王道。
pom.xml里最核心的依赖长这样:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 根据实体类生成建表SQL的两种实用方案
标题里的热词搜出来有“mybatisplus根据java实体类生成创建表的sql语句”,这确实是很多人的痛点:写好了实体类,还得手工去数据库里建表,字段一多就容易对不上。MyBatis-Plus本身不强制你手写SQL,它提供了两种能落地的方案。
第一种,使用MyBatis-Plus的DDL模块。引入对应的starter后,在配置文件中开启自动建表,启动时它会扫描带有@TableName注解的实体类,自动帮你把表建出来。大致配置是这样:
yaml复制mybatis-plus:
ddl:
run: true # 启动时执行DDL
auto: true # 缺少字段时自动更新
对应的实体类上正常写@TableName("song")、字段上写@TableField即可。这个方案适合开发阶段,表结构频繁变动时能省不少事。但生产环境我强烈不建议开auto: true,线上数据表被自动改字段,风险太大。
第二种,也是最稳的做法:写一个工具方法,在系统启动时用SqlRunner读取实体类元信息,拼出CREATE TABLE IF NOT EXISTS语句。你可以基于TableInfoHelper.getTableInfo(Class)拿到表名和字段列表,再通过SqlRunner.db().insert()执行。核心逻辑不复杂,就是遍历字段拼SQL,字段类型根据Java类型映射。
java复制// 简单示例:根据实体类生成建表SQL的辅助方法
public static String generateCreateTableSql(Class<?> entityClass) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS ")
.append(tableInfo.getTableName()).append(" (");
// 遍历字段,拼装字段名、类型、主键信息
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
for (TableFieldInfo fieldInfo : fieldList) {
sql.append("`").append(fieldInfo.getColumn())
.append("` ").append(mapToSqlType(fieldInfo.getPropertyType()));
}
return sql.toString();
}
平时我更喜欢直接在Navicat或者IDEA的Database面板里建表,然后用数据库的逆向功能生成SQL脚本存档。实体类、数据库表、接口文档这三处必须保持一致,靠人工记是最不可靠的,所以无论用哪种方式,最后都要回归到“以实体类为唯一事实来源”的原则。
2.3 Mapper层、Service层与聚合统计查询
数据访问这块,MyBatis-Plus的BaseMapper<T>能覆盖九成以上的单表操作。你只需要定义一个接口继承它,selectById、selectList、insert、updateById就全都有了,根本不用写XML。
java复制@Mapper
public interface SongMapper extends BaseMapper<Song> {
// 单表操作直接用BaseMapper自带方法
}
但音乐系统的播放排行榜、推荐候选集、歌单详情这些场景,往往需要多表关联或聚合统计。比如“查询播放量前10的歌曲并带上歌手名”,这种查询建议手写SQL,不要硬用QueryWrapper拼。在Mapper接口里声明方法,然后在resources/mapper目录下写XML文件:
xml复制<select id="selectHotSongs" resultType="com.example.music.vo.SongVO">
SELECT s.id, s.title, s.cover, s.url, s.play_count,
si.name AS singerName
FROM song s
LEFT JOIN singer si ON s.singer_id = si.id
WHERE s.status = 1
ORDER BY s.play_count DESC
LIMIT #{limit}
</select>
记得在application.yml里配mapper扫描路径。很多人Service层代码看着别扭,是因为把所有逻辑都堆在Controller里,正确的姿势是Controller只做参数校验和结果返回,业务计算放Service,数据查询放Mapper,这样层次感清楚,也方便后续加事务和缓存。
另一个实用细节是统一返回结构。我习惯定义Result<T>,包含code、msg、data三个字段,所有Controller都返回它。播放器前端拿到数据后,先判断code是否为200再解析data,否则弹统一的错误提示。这个习惯能让你后面联调少吵很多架。
事务和逻辑删除也值得顺手配置。歌曲下架和评论删除用逻辑删除而不是物理删除,MyBatis-Plus里加@TableLogic注解就行,这样数据不会真没,还能保留用户行为记录供推荐算法使用。
3. Vue前端工程搭建与播放器实现
3.1 环境准备:Node、Vite与依赖安装
前端这块,我建议直接上Vue 3 + Vite,不要再从Vue 2的Webpack模板开始。Vue 3的组合式API写起来舒服,Vite的启动速度也远快于Webpack。拿项目源码时,如果对方给的是Vue 2项目,可以跑但心里要有数:Element UI和Vuex都是老一套,想加新功能时组件写法会不一样。
环境准备其实就三件事。第一,安装Node.js,版本建议16以上,Vite 4要求Node 14.18+/16+,太低会直接报错。第二,把npm仓库切成国内镜像,不然npm install能卡到你怀疑人生:
bash复制npm config set registry https://registry.npmmirror.com
第三,用Vite创建项目骨架:
bash复制npm create vite@latest online-music-frontend -- --template vue
cd online-music-frontend
npm install
npm install axios vue-router pinia element-plus hls.js
这里有一个常见的坑:npm install时报ERESOLVE unable to resolve dependency tree。这多半是某个依赖的peerDependencies版本冲突,比如Element Plus要求的Vue版本和当前项目不一致。临时办法是加--legacy-peer-deps安装,根治办法是统一版本号,尤其是vue和element-plus之间别用两个大版本。
3.2 路由与页面骨架:列表页和播放页
路由配置是前端项目的骨架。音乐系统至少需要四个页面:首页(推荐歌单)、歌单详情页、播放页(或播放器嵌入布局)、后台管理页。使用createRouter时,注意history模式的选择。
createWebHistory是HTML5 History模式,URL干净,但部署到Nginx后刷新子路由会404,必须配置try_files回退到index.html。如果你图省事,可以直接用createWebHashHistory,URL里多一个#,但部署时不用额外处理。我一般生产环境用History模式,配合Nginx配置,开发阶段用Hash模式,省心。
页面之间传参用路由的query或params。比如从歌单卡片跳转到详情页,携带sheetId参数,详情页再根据这个ID去请求接口。这里要提醒的一点是:不要把整个歌曲对象塞进路由参数里,URL会被撑爆,而且刷新页面后参数就丢了,正确的做法是只传ID,页面自己拉数据。
组件化的好处在播放器这里体现得最明显。我的做法是把播放器做成一个全局组件,放在页面最底部,无论你在逛首页还是看歌单详情,播放器始终存在,切歌不中断。这就要用到状态管理(Pinia),把当前播放列表、当前歌曲、播放状态都放到Store里,任何组件都能触发播放。
3.3 在线播放m3u8的核心代码与避坑
在线音乐系统里,歌曲格式最常见的是MP3,用<audio>标签就能直接播。但如果你拿到的歌曲地址是m3u8格式的直播流或转码切片流(很多视频类资源都这德行,包括一些演示环境里给的“欢乐谷”类测试流地址),浏览器原生不支持直接播放,必须借助hls.js。
m3u8本质是一个文本索引文件,里面记录了一串TS分片文件的地址,播放器先读索引,再按顺序拉取分片。实现起来分三步:
第一步,安装hls.js:
bash复制npm install hls.js
第二步,创建播放组件,核心逻辑放在onMounted里:
vue复制<template>
<div class="player-wrapper">
<video ref="playerRef" controls autoplay class="video-player"></video>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import Hls from 'hls.js'
const props = defineProps({
src: { type: String, required: true }
})
const playerRef = ref(null)
function playM3u8(url) {
const video = playerRef.value
if (Hls.isSupported()) {
const hls = new Hls()
hls.loadSource(url)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play())
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari原生支持HLS
video.src = url
video.addEventListener('loadedmetadata', () => video.play())
}
}
onMounted(() => {
if (props.src) playM3u8(props.src)
})
watch(() => props.src, (val) => {
if (val) playM3u8(val)
})
</script>
第三步,也是最容易踩坑的一步:跨域CORS。hls.js在拉取m3u8索引和每个TS分片时,浏览器会发起跨域请求。如果后端或资源服务器没配Access-Control-Allow-Origin,播放器会一直黑屏,控制台报CORS错误。解决方式是在后端加一个全局CORS配置,或者在Nginx层加:
nginx复制add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
除了跨域,还有一个隐藏的坑:m3u8里的分片地址是相对路径,比如/segment/001.ts,播放器解析时基于当前页面域名去拼,如果你的页面部署域名和资源域名不一致,会拼出错误的地址。处理办法是在后端生成播放地址时,保证m3u8内部使用的是绝对地址,或者在返回前主动重写分片地址。
4. 前后端联调、跨域与上线部署
4.1 开发联调中的代理转发配置
后端的接口地址是http://localhost:8080,前端开发服务器是http://localhost:5173,直接请求必然跨域。开发阶段最简单的方案不是去后端一个个加@CrossOrigin,而是用Vite的代理把请求转发过去。
在vite.config.js里加一段:
js复制export default defineConfig({
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
这样前端发请求时统一用/api/song/list,Vite开发服务器会把请求转发到http://localhost:8080/song/list,既解决了跨域,也让前端代码里的接口地址不写死。联调时有一个小技巧:把axios的baseURL设为/api,封装一个request.js,统一处理超时、错误码和Token注入,后面所有页面调用接口都走这一层。
如果后端非要自己配跨域,也可以。在Spring Boot里写一个WebMvcConfigurer,实现addCorsMappings方法,允许的源、方法、Header都放开,但要注意放行OPTIONS请求,否则前端预检请求会直接挂掉。
4.2 Nginx部署与History路由刷新404
开发完要上线,前后端分离的部署方式一般是一台服务器加Nginx。前端打包:
bash复制npm run build
打包产物在dist目录,把整个dist上传到服务器,比如放在/opt/online-music/frontend,后端打包成online-music.jar,然后用java -jar启动,端口8080。
Nginx配置这样写:
nginx复制server {
listen 80;
server_name your-domain.com;
root /opt/online-music/frontend;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
try_files $uri $uri/ /index.html;这一句就是解决History路由404的关键。前端路由切换是浏览器端行为,服务器不知道/song/123是什么路径,所以全部回退到index.html,由Vue Router接管。如果你的接口没带/api前缀,把location /api/换成location /,但注意这会导致静态资源请求也打到后端,所以还是建议统一接口前缀。
接口代理里的proxy_pass http://127.0.0.1:8080/;末尾的斜杠有讲究,它表示把/api/song/list去掉/api前缀后转发为/song/list,和后端Controller路径对上。如果后端的路径本来就带/api,就去掉末尾斜杠,写成proxy_pass http://127.0.0.1:8080;,二者效果完全不同,配置前先想清楚。
4.3 静态资源与播放接口的安全策略
音乐系统的歌曲文件如果直接放在公网服务器某个目录下,别人知道URL后就能随意下载或盗链。虽然毕设阶段不一定有人攻击,但养成安全的习惯很重要。
最简单的防盗链手段是Nginx校验Referer:
nginx复制location /music/ {
valid_referers your-domain.com;
if ($invalid_referer) {
return 403;
}
}
稍微复杂一点的做法是接口鉴权,播放接口要求请求头携带Token,前端在axios拦截器里统一加上。对于m3u8这种分片资源,真正的商业系统会用带时效签名的播放地址防扩散,但毕业设计做到校验Token和防盗链这两层已经足够了。
另外,上传功能的实现如果没做类型校验,会留下大隐患。歌曲上传接口只允许.mp3、.flac、.m4a等音频格式,且不要相信前端传过来的文件名,后端要自己判断扩展名和MIME类型。这一条是我反复强调的,简历上写“系统有上传功能”却带着任意文件上传漏洞,反而减分。
5. 推荐模块与榜单的工程化实现
5.1 基于标签相似度的推荐算法落地
音乐推荐系统的核心不是算法多高级,而是“推荐”这个行为能讲清楚、能演示出效果。最容易被老师和面试官接受的方案是:基于标签的内容推荐,也叫基于物品的协同过滤。
思路很简单:
- 每首歌有
tags字段,比如“流行、摇滚、民谣”。 - 用户听歌时,系统记录行为,生成用户的偏好标签向量。
- 推荐时,计算候选歌曲与用户偏好向量的相似度,取Top N返回。
标签相似度计算用余弦相似度就行,不用上深度学习。假设用户偏好向量是[流行:3, 摇滚:1, 民谣:0],某首歌标签是“摇滚、民谣”,那这首歌的向量是[流行:0, 摇滚:1, 民谣:1],计算两个向量的余弦值,值越接近1越相似。
Java实现可以这样写:
java复制public class RecommendUtil {
public static double cosineSimilarity(Map<String, Integer> userVec, Map<String, Integer> songVec) {
Set<String> union = new HashSet<>(userVec.keySet());
union.addAll(songVec.keySet());
double dot = 0;
double normA = 0;
double normB = 0;
for (String tag : union) {
int a = userVec.getOrDefault(tag, 0);
int b = songVec.getOrDefault(tag, 0);
dot += a * b;
normA += a * a;
normB += b * b;
}
if (normA == 0 || normB == 0) return 0;
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
}
实际推荐流程是:从播放记录表里聚合用户的标签偏好,再遍历这批候选歌曲计算相似度,按分数倒序返回。冷启动阶段用户没有播放记录,就退化为“按播放量倒序”的默认推荐,这也是实际产品里最常用的兜底策略,演示时非常好讲。
5.2 播放统计与热门榜单
榜单和数据看板能直观体现系统的“智能感”。我在后端加了一个定时任务,每小时把歌曲表的play_count和最近7天播放记录聚合,生成热门榜单缓存。查询热门歌曲、热门歌手、热门歌单时,直接从缓存拿,减少了高频查询对数据库的压力。
Spring Boot里用@Scheduled注解就能实现定时任务,在启动类上加@EnableScheduling。播放行为的上报接口是关键,前端播放器暴露一个play事件,切歌时调用POST /api/play-record,后端异步写入播放记录并更新歌曲表计数。
一个值得注意的点:不要为了减少请求数而在前端攒一批播放记录再统一上报,这样会丢失真实的播放节奏,推荐算法拿到的数据也不准。直接每条播放都打一个轻量接口,接口里面做异步处理,不会拖慢播放器。数据库层面给播放记录表加好idx_user_id和idx_song_id索引,统计查询不会慢。
6. 高频报错与实战排查
6.1 常见报错速查表
跑这种全栈项目,下面这些报错几乎人人都会碰到一次,我把原因和解决方式整理成了表格,方便你对症下药。
| 报错现象 | 根本原因 | 解决方式 |
|---|---|---|
启动时报ClassNotFoundException: javax.servlet |
Spring Boot 3.x与旧依赖包名不一致 | 使用Spring Boot 2.7.x版本,或升级MyBatis-Plus到3.5.3+ |
启动时报Failed to configure a DataSource: 'url' attribute is not specified |
application.yml里数据源配置缺失或缩进错误 | 检查spring.datasource.url、username、password配置和YAML缩进 |
数据库连接报The server time zone value is unrecognized |
MySQL时区未设置 | 在JDBC URL加serverTimezone=Asia/Shanghai |
npm install报ERESOLVE错误 |
依赖版本冲突 | 使用--legacy-peer-deps,或升级Node版本 |
| 播放m3u8黑屏,控制台报CORS | 资源服务器未允许跨域 | 在后端或Nginx配置Access-Control-Allow-Origin |
| 刷新页面404,路由失效 | History模式下Nginx未配置try_files |
配置location / { try_files $uri $uri/ /index.html; } |
| 接口请求返回404,后端日志无输出 | 前端/api代理重写路径不对 |
检查rewrite和后端Controller路径是否匹配 |
| 列表查询时逻辑删除的数据也出现 | 实体类未加@TableLogic |
在逻辑删除字段上加注解并配置全局逻辑删除配置 |
| 序列化JSON时出现死循环或报错 | 实体类存在双向关联引用 | 在关联字段上使用@JsonIgnoreProperties或转成VO返回 |
| 上传音频后无法播放 | 文件被识别为错误MIME类型 | 后端校验扩展名和Content-Type,只允许音频格式 |
6.2 我在实战中踩过的三个坑
第一个坑是Spring Boot版本和MyBatis-Plus版本搭配不当。一开始图新鲜用了Spring Boot 3.1,结果老版本的MyBatis-Plus启动就报错,我花了一晚上排查,最后发现是javax换成jakarta的问题。后来我学乖了,所有毕设和课程设计项目一律用Spring Boot 2.7.x,把精力放在业务实现上,而不是和依赖做斗争。
第二个坑是播放器组件生命周期没处理好。组件销毁时没有调用hls.destroy(),导致切换歌曲时旧实例仍然在拉流,页面越用越卡,控制台一堆警告。正确做法是在onUnmounted里销毁Hls实例,同时监听props.src变化时先销毁再重建。这个小问题在演示时特别容易暴露,卡顿感一出来就很尴尬。
第三个坑是Nginx的try_files加错了位置。我开始只配了前端静态托管,没加try_files,结果首页能打开,一点“歌单详情”再刷新就404。折腾半天才发现是History路由的问题,加上回退规则就好了。现在我的习惯是:任何用createWebHistory的项目,部署配置里第一件事就写try_files。
结尾
我个人的体会是,这个系统真正难的不是写代码,而是把“播放”和“推荐”这两件事想透彻。播放涉及前端组件、协议兼容、跨域策略,推荐涉及用户行为采集和相似度计算,每一块都值得在答辩或面试时展开讲。源码跑通只是起点,能说出为什么这样设计、遇到问题怎么排查,才是这套项目给你带来的真正价值。
最后再分享一个小技巧:把数据库初始化脚本、项目运行的README、启动顺序说明都整理好,放到源码根目录。别看这些不起眼,它对你自己以后回看项目、对老师验收、对简历上附项目链接的人,都是实打实的加分项。如果后续有空,你可以给这个系统再加上用户歌单收藏、评论互动、后台歌曲审核这几块,整个项目的完整度会再上一个台阶。
