☰
SpringBoot+Vue音乐平台毕设实战:从架构设计到部署避坑
2026/10/14 4:01:48 网站建设 项目流程

最近一段时间,前前后后帮几位同学梳理毕业设计的架构和源码,发现很多人的选题都挤在管理系统、电商商城这几个老方向上。有一位同学选的是音乐平台,用的是 SpringBoot + Vue 这套组合,前后端分离,数据库落在 MySQL 上。这个选题不算罕见,但真正动手做的时候,要处理的事情比想象中多一些:音乐文件的上传与回放、播放器状态同步、歌单收藏、评论楼层,随便拆一块出来都够写不少代码。我打算把这类项目的完整思路展开聊一聊,给正在选毕设题目、或者准备拿这套代码复现的同学一条可以照走的路线。

这个项目比较适合三类人看:第一类是毕业设计选了“内容平台 + 用户交互”方向的,第二类是刚学完 SpringBoot 和 Vue、想找一个完整前后端分离项目练手的,第三类是单纯想理解音乐平台里文件流、播放器状态、点赞收藏这类逻辑怎么落地的开发者。下面从头拆到尾,我会把模块划分、表结构、关键接口、前端组件、踩过的坑都放进来,尽量做到你看完可以直接打开工程对着写。

1. 项目整体设计与思路拆解

1.1 需求边界怎么定,才不至于做成烂尾工程

做毕设最容易犯的错,是刚开始就想把所有功能都塞进系统里。音乐平台一旦对标主流的在线音乐软件,那涉及推荐算法、版权治理、社交关系链、支付会员体系,一个人根本做不完,而且写出来也很难自圆其说。我一般建议把边界划在一个“最小完整闭环”上。

什么叫最小完整闭环?一个用户能走通完整使用路径:注册登录、浏览歌曲列表、搜索、点击播放、暂停切歌、收藏歌单、对歌曲评论。做到这些,核心业务逻辑其实已经很完整。

在这个基础上,再补一个后台管理端用来维护歌曲数据,通常作为加分项。为什么需要后台?因为前端用户看到的歌曲列表、歌曲封面、音频文件地址,总得有一个来源。你可以直接在数据库里手动插数据,但评审老师更愿意看到有一个可视化入口去管理系统资源。参考往年的做法,用一个独立的 Vue 管理页面来上传音乐、设置歌曲信息,是比较稳妥的方案。

我把这套系统的功能边界总结成一张清单,后面写代码时就不会东一榔头西一棒子:

  • 前台用户端:注册、登录、歌曲列表、歌曲搜索、播放器(播放/暂停/上一首/下一首)、收藏歌单
  • 用户交互端:歌曲评论、评论展示、取消收藏
  • 后台管理端:歌曲信息维护、音频文件上传、删除歌曲、用户列表查看
  • 通用能力:登录拦截、统一返回结构、异常兜底、图片与音频静态资源访问

1.2 模块划分与前台路由的对应关系

前端页面一般用 Vue Router 组织,代码结构上按视图和组件两层来放。视图页面数量不用太多,但每一个都必须对应真实业务。我做过一版比较合理的前台路由设计:

  • /login:登录页
  • /register:注册页
  • /home:首页,含推荐歌曲列表和热门歌单入口
  • /songDetail/:songId:歌曲详情页,内含播放器操作和评论区
  • /playlistDetail/:playlistId:歌单详情页,展示歌单内歌曲
  • /search:搜索结果页,按歌名或歌手模糊匹配
  • /admin:后台管理页面,通常用单独路由嵌套

页面拆分得干净,后端接口才能跟着清晰。接口层面按业务域分,形成下面这组 RESTful 风格接口,毕设和真实项目在这一点上没有区别。

业务模块接口路径举例核心作用
用户模块POST/api/user/register、/api/user/login、GET/api/user/info注册、登录、会话获取
歌曲模块GET/api/song/list、GET/api/song/detail/{id}、GET/api/song/search歌曲列表、详情、搜索
歌单模块POST/api/playlist/create、GET/api/playlist/detail/{id}创建歌单、查看歌单
收藏模块POST/api/favorite/add、DELETE/api/favorite/{songId}收藏歌曲、取消收藏
评论模块POST/api/comment/add、GET/api/comment/list/{songId}添加和展示评论
文件模块POST/api/admin/upload/song、/upload/image上传音频和封面图片

接口能不能跑通,很大程度上取决于数据库表设计得稳不稳。下面一节重点说表。

2. 技术栈选型与后端核心实现

2.1 SpringBoot 后端分层结构与选型理由

后端选择 SpringBoot 几乎是毕业设计里最不折腾的选项。SpringBoot 内置 Tomcat,自动化配置多,写一个接口只需要 Controller、Service、Mapper 三层,不用像 Spring MVC 时代那样配一堆 XML。再加上现在很多学校对 Java 技术栈更熟悉,出了问题网上资料也好找。

分层上尽量保持简单,不要引入过度设计。工程代码通常按以下包结构组织:

  • controller:接收前端请求、校验参数、调用 service
  • service:业务逻辑处理,比如播放量加一、评论数同步
  • mapper:数据库操作,配合 MyBatis-Plus 使用
  • entity:数据库实体类
  • dto:前端传入的参数对象
  • vo:返回给前端的视图对象
  • config:跨域配置、拦截器配置、资源映射配置
  • common:统一结果类、异常处理、工具类

MyBatis-Plus 这个选型很推荐。它内置单表 CRUD,写简单接口时几乎不用写 XML SQL,selectById、selectPage、LambdaQueryWrapper这些方法拿来即用。对于赶毕设节奏的同学来说,可以把时间省下来去打磨评论、收藏这类有“核心逻辑”的功能。

一个典型的歌曲服务实现里,查询列表可以直接用 QueryWrapper 完成,不需要手动拼 SQL:

@Override public List<SongVO> searchSongs(String keyword) { LambdaQueryWrapper<Song> wrapper = new LambdaQueryWrapper<>(); wrapper.like(Song::getSongName, keyword) .or().like(Song::getSinger, keyword); List<Song> songs = songMapper.selectList(wrapper); return songs.stream().map(songConvert::toVO).collect(Collectors.toList()); }

这段代码看着短,但注意or()和like()的连用,在 MyBatis-Plus 里很容易写成(name like ? or singer like ?)这种错误语义。正确写法是wrapper.and(w -> w.like(...).or().like(...)),不然你的搜索条件会变成先过滤 name 再过滤 singer,和预期完全不一样。

2.2 数据库表结构设计:六张表足够支撑整个项目

表设计是毕设项目里能看出“工程素养”的位置。很多同学一上来就建了二十多张表,实际写接口时根本管不过来。这套音乐平台把核心表控制在六张左右,逻辑非常清楚。

用户表(user)

字段包括id、username、password、nickname、avatar、create_time。密码一定要加密存储,不能明文入库。推荐用 BCrypt 加密,Spring Security 里有现成的BCryptPasswordEncoder,也可以用 Hutool 封装的 BCrypt 工具。登录时比对 hash 而不是比原文。

歌曲表(song)

字段包括id、song_name、singer、album、cover_url、audio_url、duration、play_count、create_time。这里有两个关键点:一是audio_url存储相对路径或完整可访问 URL,二是play_count用于首页热门歌曲排序。播放次数最好不要每次播放都 update 一次数据库,而是先加到内存里,再用定时任务批量刷到数据库,否则高并发下面会锁表。

歌单表(playlist)

字段包括id、playlist_name、description、cover_url、user_id、create_time。一个用户创建多个歌单,用户和歌单一对多。

歌单歌曲关联表(playlist_song)

字段包括id、playlist_id、song_id。歌单和歌曲是多对多关系,必须有中间表。写查询时,通过playlist_id关联song_id,再回表查歌曲表。

收藏表(user_favorite)

字段包括id、user_id、song_id、create_time。这个表记录用户收藏了哪些歌曲,需要注意加唯一索引(user_id, song_id),防止同一首歌被重复收藏。

评论表(comment)

字段包括id、song_id、user_id、content、parent_id、create_time。parent_id支持楼中楼,如果毕设不需要二级评论,可以不加这个字段。

这里专门提醒一句:所有表的字符集务必统一设置成utf8mb4,否则评论里出现表情符号时,存储会报错。utf8mb4与utf8并不是一回事,前者是后者的超集,能完整支持四字节的 emoji 字符。

2.3 登录鉴权与拦截器的正确姿势

前后端分离项目里,最常见的登录方案是 JWT + Token 拦截器。用户在登录接口验证通过后,后端生成一个 token 返回给前端,前端存到 localStorage 里。后续请求在 header 中携带Authorization字段,后端拦截器解析 token,获取当前用户 ID。

在 SpringBoot 里常用HandlerInterceptor来实现:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token) || !TokenUtil.verify(token)) { response.setStatus(401); return false; } Integer userId = TokenUtil.getUserId(token); UserContext.set(userId); return true; } }

拦截器只对需要登录的接口生效,像登录、注册、歌曲列表、首页推荐这些不需要登录就能访问的接口应该放行。如果用 Spring Security 做全局拦截,配置顺序很讲究,一不小心就会出现注册接口也被拦截的问题。个人建议在毕设阶段写一个轻量级 HandlerInterceptor 就够了,不引入 Spring Security 那套复杂的过滤链。

要注意的还有一点:UserContext.set()在请求结束后要及时清理,不然线程池复用时会出现用户 A 的 ID 串到用户 B 的请求上。可以在afterCompletion里调用UserContext.clear()。

CORS 跨域配置也要单独处理。前端部署在localhost:5173,后端在localhost:8080,前后端端口不一致,请求就是跨域。一般用WebMvcConfigurer注册全局跨域规则:

@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); }

这里有一个很隐蔽的坑:如果同时用了拦截器,预检请求OPTIONS也会被拦截,导致前端报 401。解决办法是在拦截器里放行OPTIONS方法,或者把跨域配置写在过滤器层面而不是拦截器层面。

3. 前端页面与核心交互实录

3.1 用 Pinia 管理播放状态的完整思路

Vue 3 项目一般配合 Pinia 做状态管理。音乐平台里最需要全局状态的就是播放器。播放器组件可能放在页面的 constant 区域,但它的状态需要被歌曲列表页、详情页、搜索页共同控制。

播放器的核心状态可以定义成一个 store:

export const usePlayerStore = defineStore('player', { state: () => ({ currentSong: null, isPlaying: false, audioInstance: null, currentTime: 0, duration: 0 }), actions: { playSong(song) { this.currentSong = song; if (this.audioInstance) { this.audioInstance.src = song.audioUrl; this.audioInstance.play(); this.isPlaying = true; } }, togglePlay() { if (!this.currentSong) return; this.isPlaying = !this.isPlaying; if (this.isPlaying) { this.audioInstance.play(); } else { this.audioInstance.pause(); } } } });

有同学把audioInstance直接放在组件的data里,切到另一个页面后,播放器就断掉了,这是不对的。播放器本质上是全局唯一的,要把它放到 Pinia 中统一管理。

封装一个播放器组件是推荐的用法:在App.vue中挂载全局播放器,播放器组件内部监听audio的timeupdate事件,更新进度条。至于上一首、下一首的逻辑,需要提前维护一个播放列表数组,这也很适合放在 store 里。

播放列表从哪来?可以约定一个数组playQueue,歌曲列表页点击播放时,把当前列表所有歌曲塞进去,再从当前歌曲的 index 开始播放。这样一个简单的队列模型,能让页面切换后播放不中断。

3.2 音乐文件上传与资源访问

后端上传音乐文件和上传图片差不多,核心是用 MultipartFile 接收文件,然后写入服务器本地磁盘。上传代码要处理的是路径规划和文件名生成两个点。

路径规划上,建议不要把文件直接放在src/main/resources下,否则打包成 jar 后不好读取。可以定义到一个外部目录,比如D:/music-platform/audio/,同时把这个目录映射成静态资源路径:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/audio/**") .addResourceLocations("file:D:/music-platform/audio/"); } }

这样做的好处是:代码中存储相对路径/audio/xxx.mp3,前端拼接一个固定的服务器地址,就能直接播放。注意在 Windows 和 Linux 上文件路径分隔符不一致的问题,习惯上统一用绝对路径拼接工具类来生成 URL。

文件名生成不要直接用用户上传的原始文件名,原因有三个:中文名可能被 URL 编码破坏;重名会覆盖旧文件;../这种路径穿越字符可能被写入文件名里。建议用UUID生成新名字,保留原始扩展名,例如:

String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + ext;

上传成功后,还需要把文件大小、时长等信息写入数据库。其中音频时长这个字段比较特殊,前端拿到音频文件后,可以等loadedmetadata事件触发后读取audio.duration再传给后端,也可以在 Java 端解析音频文件头。前者实现简单,推荐优先采用。

3.3 评论功能与收藏功能的数据一致性

评论区大概是毕设里最容易暴露问题的模块。评论涉及多表操作:插入评论数据、更新评论数。很多同学直接写两次 update,结果第一次成功、第二次失败,数据就乱了。

这个场景不需要引入消息队列,但至少保证两条 SQL 在同一事务里执行。SpringBoot 中在 service 方法上加@Transactional注解就能保证事务。还需要注意@Transactional只回滚RuntimeException,如果手写了 try-catch,需要在Transactional里指定rollbackFor = Exception.class,否则异常被吞掉后事务不会回滚。

收藏功能同样要注意唯一性控制。用户重复点击收藏按钮时,如果表里已经有记录,接口应该幂等返回,而不是报错。实现方式是在数据库层做唯一索引,同时在 service 层先查询一次记录再决定插入或删除。

评论数据返回给前端时,还需要关联出用户名和头像。比较简单的做法是评论表查出列表后,用 user_id 批量查用户信息,再组装成 VO。千万不要在循环里逐条查数据库,那样会出现经典的 N+1 查询问题。

4. 常见问题、避坑技巧与排错实录

4.1 端口占用和数据库连接问题

后端启动时报端口被占用,是出现频率最高的问题。Windows 下查哪个进程占用端口,可以打开命令行执行netstat -ano | findstr 8080,找到 PID 后在任务管理器结束对应进程。更省心的办法是在application.yml里换一个端口,比如server.port=9000。

数据库连接失败的原因基本集中在三处:一是 MySQL 没启动;二是用户名密码不对;三是useSSL和时区参数缺失。连接串建议写成:

jdbc:mysql://localhost:3306/mymusic?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai

注意 MySQL 8.x 版本的驱动是com.mysql.cj.jdbc.Driver,而 5.x 是com.mysql.jdbc.Driver。两者不要混用,否则启动时会直接报 ClassNotFoundException 或版本错误。

4.2 拦截器放行和资源路径冲突

拦截器配置顺序造成的问题,通常表现为登录页面能打开、但验证码接口拿不到数据,或者上传的图片加载不出来。原因是拦截器把所有/api/**都拦住了,而后台管理的资源上传接口也在这个路径下。

解决办法是在拦截器注册时明确放行规则:

registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/song/**");

如果后台接口和前台接口不区分,我建议把管理员接口统一放在/api/admin/**下,单独加一个 AdminInterceptor,避免所有请求都复用一套鉴权逻辑,后面扩展权限时也好改。

4.3 前端播放器跨域与音频格式兼容

前端用<audio>标签播放后端地址时,最常见的问题是播放器一直转圈不加载。排查顺序应当是:先看网络请求是否返回 200,再看返回头的 Content-Type 是否为audio/mpeg。

有时候后端静态资源映射做了,但 SpringBoot 返回的是application/octet-stream,部分浏览器播放器会拒绝播放。可以通过设置资源映射时的 Content-Type 来解决,或者在 Nginx 层面对/audio/路径配置正确 MIME 类型。

音频格式兼容上,建议统一上传 MP3 格式文件。某些平台常见的 M4A、FLAC 格式在浏览器中支持度不够稳定,Windows 自带录音机导出的 WAV 文件体积又太大。如果必须要支持,就需要在前端对文件格式做一次转码,这是相对麻烦的方案,毕设阶段尽量避免。

4.4 路由刷新 404 与打包部署

Vue Router 默认使用 history 模式时,后端如果没有配置 fallback,刷新页面会直接 404。SpringBoot 中可以用过滤器把所有非静态资源请求转发到index.html:

@RequestMapping(value = "/{path:[^\\.]*}") public String redirect() { return "forward:/index.html"; }

这个方案在前后端不分离的打包部署模式里很管用。如果前后端完全分离,前端由 Nginx 托管,那只需要在 Nginx 配置里添加try_files $uri $uri/ /index.html;即可。

前端 build 后的 dist 目录,可以放到后端resources/static下打包一起运行,也可以单独放到 Nginx。毕设答辩时省事一点,建议打包进 SpringBoot,这样只需要跑一个 Java 进程,部署时讲起来也顺。

4.5 拼装的常见报错速查表

现象大概率原因解决方案
后端启动报“Application run failed”端口被占或数据库连接失败换端口、检查 MySQL 服务
前端请求接口报 401token 未携带或过期、拦截器误拦检查 header,确认放行规则
前端请求接口报 403CORS 配置没允许跨域或 allowCredentials 冲突检查 CorsRegistry 配置
播放器无法播放音频文件路径不对、MIME 类型错误检查返回头 Content-Type
上传文件报“file is empty”前端 FormData 字段名不对确保字段名与后端参数名一致
评论 emoji 存不进数据库字符集不是 utf8mb4修改表和数据库字符集
同一首歌重复收藏缺少唯一索引添加(user_id, song_id) 唯一索引
刷新页面出现 404前端 router history 模式未配置 fallback配置转发或 Nginx fallback

排查问题时,我个人的习惯是先看后端控制台日志,确认请求到底进没进 Controller;再看数据库 SQL,确认是不是查询条件写错;最后才看前端网络面板。很多人一拿到问题就去看前端 console,其实后端日志给的信息量要直白得多。

5. 源码组织与答辩要点

这套项目的源码目录组织方式,我整理了一个相对清爽的结构。后端按模块分包,前端按页面和组件分离,不要全堆在views下面。

后端目录建议:

com.example.mymusic ├── common → 统一返回结果、全局异常处理 ├── config → 跨域配置、拦截器、静态资源映射 ├── controller → 接口层 ├── service → 业务层,含 impl ├── mapper → MyBatis-Plus Mapper ├── entity → 数据库实体 ├── dto → 前端传参对象 ├── vo → 返回对象 └── utils → JWT工具、文件工具、用户上下文

前端目录建议:

src ├── api → 所有HTTP请求统一封装 ├── assets ├── components → 播放器、歌曲卡片、评论区组件 ├── router ├── stores → Pinia状态 ├── views → 页面级组件 └── utils → 格式化、鉴权工具

答辩时重点讲三个方面:需求分析的闭环逻辑、数据库表之间的关联、播放器全局状态管理。这个是整套代码里最能体现个人理解的部分。

还有一个非常实用的建议:项目里引入一个统一的返回结果对象Result<T>。所有接口返回{ code: 0, message: "success", data: ... },前端封装 axios 拦截器统一判断 code。评审老师浏览代码时,看到这种规范化的返回结构,会比看到裸返回对象要舒服很多。别小看这种细节,它决定了代码给人的第一印象。

至于源码交付,建议使用 Git 做版本管理。答辩前把前后端完整的 README 写好,里面写清环境要求、数据库初始化脚本、启动步骤。很多老师答辩时不看代码,只要求现场把项目跑起来。一个能一键启动的前后端工程,比什么都管用。

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

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

立即咨询