很多人问,学完 Spring Boot 基础之后,到底做什么项目才能把框架真正串起来?我的建议是:别只盯着那些纯增删改查的后台管理模板,最好找一个场景稍微丰富、有文件交互和真实业务链路的项目来练,比如音乐网站。今天我会把一套编号 38382 的 Spring Boot 音乐网站源码拆开讲,从技术选型、数据库设计,到前后端交互、部署启动,把值得复用的点都拎出来。这套项目包含用户端和管理端,覆盖登录、歌手、歌曲、歌单、评论、收藏、播放器等常见功能,适合做课程设计、毕业设计,也适合想快速上手 Spring Boot + Vue 这套前后端分离组合的人参考。
1. 项目整体设计与技术选型
1.1 这个项目能做什么,为什么选音乐网站
音乐网站是一个很典型的“业务边界清晰,但功能链路完整”的项目。它不像博客只有文章增删改查,也不像商城那样订单库存特别复杂。音乐场景里既有基础的用户体系,又有文件上传(歌曲、封面、歌词),有资源的分类关联(歌手、歌曲、歌单),还有用户行为(收藏、评论、评分、播放排行)。这些模块放在一起,几乎覆盖了日常开发中 80% 的常见问题。
这套源码 38382 里已经包含了两套前端页面:一套给普通用户听歌,一套给管理员维护数据。后端则使用 Spring Boot 统一提供接口。也就是说,拿到源码以后不是只能对着代码干看,而是可以直接启动、注册账号、传歌、建歌单、评论,把所有功能在浏览器里真实跑一遍。这种“能跑起来、能玩起来”的学习项目,比抽象的 demo 有价值得多。
1.2 技术栈选型与取舍
先上一份这个项目里的核心技术栈清单:
| 层次 | 技术 | 说明 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 稳定版本,兼容 JDK 1.8,资料多问题少 |
| 持久层 | MyBatis-Plus 3.5.x | 单表 CRUD 不用写 SQL,复杂查询保留 XML |
| 数据库 | MySQL 5.7 | 经典组合,初始化 SQL 直接导入 |
| 鉴权方式 | JWT + 拦截器 | 适合前后端分离、无状态接口 |
| 文件存储 | 本地磁盘 + 静态资源映射 | 学习成本低,后期可替换为 OSS |
| 前端用户端 | Vue 2 + Element UI + Axios | 后台管理界面常见组合 |
| 前端管理端 | Vue 2 + Element UI | 与用户端共用一套组件风格 |
| 构建工具 | Maven / npm | 后端依赖和前端依赖各自管理 |
为什么没有用 Spring Cloud、微服务、Elasticsearch 这些更“高级”的东西?因为这套项目的定位是让学习者快速理解业务系统是怎么跑起来的。一上来就拆一堆服务,反而会把核心逻辑淹没在分布式各个组件里。Spring Boot 负责接口,MyBatis-Plus 负责数据库操作,JWT 负责身份识别,文件走本地磁盘,所有链路都清晰可见。等你把这条链路吃透了,再往 Redis、对象存储、消息队列方向上扩展,会顺手很多。
1.3 源码目录结构解读
拿到源码之后,第一件事不是急着双击运行,而是先看目录结构。这套项目的后端采用常见的分层结构:
src/main/java/com/music ├── common // 统一返回结果、全局异常、JWT工具 ├── config // 跨域配置、拦截器配置、静态资源映射 ├── controller // 接口层,接收参数并返回结果 ├── service // 业务逻辑层,处理具体规则 ├── mapper // 数据访问层,MyBatis-Plus 接口 ├── entity // 数据库实体 ├── utils // 工具类 └── MusicApplication.java // 启动类resources 目录下除了application.yml,还有mapper文件夹,里面放的是联表查询这类复杂 SQL。前端源码通常分成vue-music-web和vue-music-admin两个目录,分别对应前台听歌和管理后台。
分层架构的好处是边界清楚。Controller 保持很薄,只做参数接收、调用 service、返回 Result;Service 里写业务规则,比如注册时检查用户名是否重复、歌曲上传时校验文件类型;Mapper 只负责数据查询。这样写的好处是你在答辩或者面试时,可以很流畅地讲清楚每一层在干什么,而不是说“所有代码都堆在 Controller 里”。
2. 数据库设计与核心细节
2.1 核心数据表与关系
音乐网站的数据模型并不复杂,但表和表之间的关联关系很典型。源码 38382 里主要包含下面这些表:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 前台用户 | id, username, password, avatar, create_time |
| admin | 管理员 | id, username, password |
| singer | 歌手 | id, name, pic, introduction |
| song | 歌曲 | id, singer_id, name, pic, url, lyric |
| song_list | 歌单 | id, title, pic, introduction, style |
| list_song | 歌单-歌曲关联表 | id, song_list_id, song_id |
| comment | 评论 | id, user_id, song_id, content, create_time |
| rank | 评分 | id, user_id, song_id, score |
以歌曲表song为例,核心结构大致如下:
CREATE TABLE `song` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `singer_id` bigint(20) DEFAULT NULL COMMENT '歌手id', `name` varchar(255) NOT NULL COMMENT '歌曲名', `introduction` varchar(1024) DEFAULT '' COMMENT '简介', `pic` varchar(255) DEFAULT '' COMMENT '封面图片URL', `lyric` text COMMENT '歌词文本', `url` varchar(255) NOT NULL COMMENT '歌曲文件URL', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有一个值得注意的细节:虽然song表有singer_id,但项目里一般不建物理外键,只保留逻辑关联。原因是物理外键在插入、删除时容易产生锁竞争,而且做联表查询和迁移数据时会比较麻烦。实际开发里,按 ID 关联、用 join 查询就够了,外键约束交给应用层去保证。
2.2 文件信息的存储方式
很多新手在做这类项目时,容易踩的一个坑是:想直接把音乐文件塞进数据库里。这种做法非常不推荐。音频文件体积大,数据库存二进制不仅拖慢查询速度,还会让备份和迁移变得很痛苦。
这套项目的做法是:文件本体上传到服务器磁盘,数据库里只存一个相对路径。比如上传完一首歌后,库里url字段存的值大概是/music/2024/05/21/xxxxxx.mp3,前端拿到这个 URL 后拼上服务器地址就能播放。图片、歌词也是同样的处理方式。
为了避免上传文件重名,上传时最好用 UUID 重新生成文件名。我自己一般会这样处理:
String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix;然后按日期分目录保存,比如upload/2024/05/21,这样每天的文件都分开,后续清理和管理都很方便。
2.3 用户密码与 Token 校验设计
用户模块如果要放到真实环境,密码一定不能明文存储。这套源码里可以使用 BCrypt 对密码做哈希处理,注册时加密,登录时用matches方法校验。
登录成功后,后端生成一个 JWT Token 返回给前端:
String token = JwtUtil.createToken(user.getId(), user.getUsername());前端把 Token 存在本地,之后发请求时在 Header 里带上Authorization: Bearer <token>。后端通过拦截器统一校验,不需要在每个接口里重复写判断逻辑。
拦截器配置时,要放行登录、注册、歌曲列表这类公开接口。比如:
registry.addInterceptor(jwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/user/login", "/user/register", "/song/list", "/singer/list", "/error" );这样既能保护评论、收藏、后台管理等接口,又不会给公开页面访问造成障碍。
3. 核心功能实现与实操要点
3.1 歌曲上传接口的完整流程
歌曲上传是音乐网站的关键功能。管理员在后台选择音频文件、封面、填写歌曲名,选择所属歌手,点击提交后接口需要完成一系列工作:
文件校验、文件保存、字段入库。Controller 里的上传接口接收MultipartFile文件,同时接收歌曲名、歌手 ID、简介、歌词等参数。
核心逻辑大致如下:
@PostMapping("/admin/song/add") public Result addSong(@RequestParam("file") MultipartFile file, @RequestParam("singerId") Long singerId, @RequestParam("name") String name) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } // 校验文件类型,只允许 mp3、flac、wav 等后缀 String originalFilename = file.getOriginalFilename(); if (!checkAudioSuffix(originalFilename)) { return Result.error("不支持的音频格式"); } // 保存文件到本地磁盘 String url = fileStorageService.store(file); // 写入数据库 Song song = new Song(); song.setSingerId(singerId); song.setName(name); song.setUrl(url); songService.save(song); return Result.success(song); }这里面的fileStorageService.store就是封装了生成 UUID 文件名、按日期建目录、调用file.transferTo()保存文件的过程。
前端管理端用el-upload组件,配一个 action 指向这个接口,并加上 Token 请求头。注意在application.yml里调整 Spring Boot 的文件上传限制,默认 1MB 往往不够用:
spring: servlet: multipart: max-file-size: 50MB max-request-size: 100MB3.2 播放器与歌单展示的联动
歌曲列表接口返回的数据里,不能只有歌曲名和 URL。前端展示列表时需要封面、歌手名,播放时需要能拿到url和lyric。所以这个接口往往会联查歌手表,把歌手名一起返回。
如果使用 MyBatis-Plus,可以在自定义 SQL 里写:
<select id="selectSongWithSinger" resultType="map"> SELECT s.id, s.name, s.url, s.pic, s.lyric, g.name AS singerName FROM song s LEFT JOIN singer g ON s.singer_id = g.id ORDER BY s.create_time DESC </select>前端拿到列表之后,用户点击某一行,就动态更新<audio>标签的 src 和封面地址。播放列表、歌词、歌曲详情这些都可以通过当前选中行的数据来驱动,不需要额外请求接口。这是播放器体验比较流畅的关键点。
3.3 搜索、评论与排行榜的实现
搜索功能比较常规,前台搜索框把关键字传给后端,后端对歌名做模糊查询。如果想让结果更好,可以把歌手名也带上:
QueryWrapper<Song> wrapper = new QueryWrapper<>(); wrapper.like("name", keyword) .or() .like("singer_id", keyword);更好的做法是联表查询后在内存里组合,但作为第一版音乐网站,直接用LIKE完全可以。等数据量到了一定程度再考虑引入搜索组件。
评论功能要注意两点:内容必须做转义,防止 XSS 脚本注入;返回评论列表时,需要联查用户表,把用户名和头像带给前端,否则没法显示“谁说了什么”。评论分页使用 PageHelper 或 MyBatis-Plus 分页插件都可以,我习惯在配置类里统一加一个 MybatisPlusInterceptor。
排行榜可以直接用评分表做聚合:
SELECT song_id, AVG(score) AS avg_score FROM rank GROUP BY song_id ORDER BY avg_score DESC LIMIT 10要注意防止没有评分的歌曲不被聚合到,也注意分数为空的情况,用IFNULL(score, 0)处理一下。
3.4 统一返回体与前后端分离联调
前后端分离项目里,接口返回格式如果不统一,前端处理数据会非常痛苦。这套源码中有一个统一的Result类:
public class Result { private Integer code; private String message; private Object data; }成功时返回 code=200,业务错误返回 code=400,未登录返回 code=401,服务器异常返回 code=500。全局异常处理器用@RestControllerAdvice捕获异常,避免把异常堆栈直接抛给前端:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handleException(Exception e) { return Result.error(500, "服务器异常"); } }还有一个常见问题是跨域。前后端分离开发时,后端跑在 8088 端口,前端跑在 8080 端口,如果不做处理,浏览器会拦截请求。解决方式是在后端配置一个全局跨域配置类,允许前端地址访问。也可以让前端用代理转发,但我更喜欢后端统一配置,生产环境再用网关或 Nginx 处理。
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }4. 源码部署与启动全过程
4.1 环境准备与配置项修改
本地跑这套源码 38382 之前,先确认环境:
- JDK 1.8 或更高版本
- Maven 3.6+
- MySQL 5.7 或 MySQL 8.0
- Node.js 12/14(用于前端)
拿到源码后,首先到sql目录里找到初始化 SQL 脚本,用 Navicat 或命令行导入数据库。导入完成后,修改后端application.yml里的数据库连接信息:
spring: datasource: url: jdbc:mysql://localhost:3306/music_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 50MB music: upload-path: /Users/yourname/music-data/如果使用 MySQL 8.0,驱动类名要改为com.mysql.cj.jdbc.Driver,URL 里最好加上allowPublicKeyRetrieval=true,否则可能报公钥检索错误。
4.2 后端和前端分别如何启动
后端启动比较简单。用 IDEA 打开项目根目录,等待 Maven 下载完依赖之后,直接运行MusicApplication启动类。启动成功后可以先在浏览器访问一个公开接口,比如:
http://localhost:8088/song/list如果能返回 JSON 数据,说明后端已经正常工作了。
前端需要先安装依赖:
cd vue-music-web npm install npm run serve如果npm install过程中出现网络问题,可以临时切换镜像源:
npm config set registry https://registry.npmmirror.com前端启动后,浏览器访问http://localhost:8080,用数据库里的用户账号登录,或者直接注册一个新账号。管理端一般在http://localhost:8081,默认管理员账号密码在初始化 SQL 里可以查到,常见是admin / 123456。
4.3 常见启动报错与解决办法
很多新手第一次跑源码时,会被几个固定问题卡住。我把最常见的几个整理成表格,遇到问题可以直接对照排查:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| APPLICATION FAILED TO START | 端口被占用 | 杀占用进程,或修改server.port |
| Access denied for user ‘root’@‘localhost’ | 数据库密码错误 | 检查 application.yml 中的密码、用户名 |
| Unknown database ‘music_db’ | 数据库没有创建 | 先用 CREATE DATABASE music_db 建库,再导 SQL |
| Invalid bound statement | Mapper XML 没被扫描 | 确认 XML 在 resources/mapper 下,且 MapperScan 路径正确 |
| 前端页面接口 404 | 后端接口路径与前端不一致 | 检查前端 api 文件里的 baseURL 和后端 Controller 路径 |
| 歌曲、图片加载不出来 | 静态资源映射不对 | 检查上传路径和 addResourceHandlers 中的路径是否一致 |
补充一个我自己的经验:如果遇到歌曲上传成功后访问 404,先看数据库里的url字段值,再手动在浏览器里打开这个地址。如果 404,基本就是虚拟路径映射没配置好,重点检查file:前缀和目录末尾的斜杠。
5. 从源码到独立项目的扩展建议
5.1 给这个项目“加分”的几个优化方向
这套音乐网站源码作为起步项目完全够用,但要写到简历上,或者作为毕业设计冲刺优秀,还可以做几个方向上的扩展。
第一,加 Redis 缓存。把歌手列表、歌曲排行榜、热门歌单这类高频读接口缓存起来,减少数据库压力。登录 Token 也可以改成 Redis 存储,方便做主动失效和续期。
第二,把文件存储换成对象存储。学习阶段本地磁盘没问题,但真实部署时磁盘扩容麻烦,而且多台服务器之间文件不同步。接入 MinIO 或者云上 OSS,代码改动其实很小,只要把上传逻辑抽成一个 StorageService 接口,本地和 OSS 各写一个实现。
第三,搜索功能升级。当前用 LIKE 已经够用,但歌曲量上来以后,可以用 Elasticsearch 做中文分词搜索,把歌名、歌手名、专辑名都建立索引。这个扩展会显著提升搜索体验。
第四,增加用户行为分析。基于用户收藏和播放记录,做一个“每日推荐”或者“相似歌曲”模块。最基础的协同过滤算法并不复杂,关键是能把 Spark 或简单 Python 脚本产生的结果回写到推荐表里。
5.2 动手改项目时的避坑提示
我自己在给这套源码做二次开发时,踩过几次坑,分享几个比较实用的避坑提示。
第一,先跑通再动代码。很多同学拿到源码直接改数据表结构,结果前端页面全部报错,又说不清楚是哪里改坏了。正确做法是先原封不动跑一遍,再逐步改,每改一步都启动验证一次。
第二,不要随便删表。音乐网站涉及歌单-歌曲关联、评分、评论多张关联表,删表前一定先看被哪些接口引用。尤其是中间表list_song,删错会导致歌单页打不开。
第三,改前端接口地址时,记得同步修改后端跨域配置。常见错误是前端页面换了个端口访问,但后端CorsConfig还是只允许原来的端口,结果接口被浏览器拦截。
第四,连接数据库之前,确认表名、字段名大小写问题。MySQL 在 Linux 上对表名大小写敏感,如果导入 SQL 时把表名写成大写,而代码里查的是小写,会立刻报表不存在。
最后再说一个小的实操习惯:开发时把 MyBatis 的 SQL 日志打开,这样一旦接口数据不对,可以直接在控制台看到实际执行的 SQL,排查效率能提升不少。在application.yml里加上:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl我个人整理这份源码的经验是:学项目不要急着把每个类都读完,而是先用一条主线走通——“用户登录 -> 看到歌曲列表 -> 点击播放”。把这条链路里的 Controller、Service、Mapper 全部跟一遍,再向外扩展去研究上传、评论、排行榜,你会突然发现 Spring Boot 这层连接方式全通了。之后再动手加一个“最近播放”或者“每日推荐”功能,把新接口加入原有前端页面,这才算真正把源码变成了自己的东西。