简介:这是一份基于SpringBoot的在线音乐播放系统完整毕业设计项目,面向计算机、通信、人工智能、自动化等专业的在校学生及从业者,可作为期末课程设计、课程大作业或毕业设计参考。项目是个人毕业设计的成果,答辩评审达98分,包含系统源码与配套毕业论文,演示了一个在线音乐播放平台从后端接口设计到前端交互实现的整体过程。代码经过调试测试,能够稳定运行,学习者可以从中掌握SpringBoot框架的项目搭建、业务分层、数据交互与功能整合等关键技能,基础较好的读者还能在此基础上扩展功能,二次开发成不同主题的音乐应用。资源压缩包大小约11.33MB,以项目源码和论文文档为主体,便于直接下载使用和对照学习。目前已有366人学习下载,适合希望快速上手Java Web开发、完成学业任务的读者参考实践。
1. 毕设选题为什么会落在SpringBoot在线音乐播放系统上
SpringBoot在线音乐播放系统在Java毕业设计中的出现频率长期靠前,不是因为它技术新颖,而是它恰好覆盖了评审爱看的几个能力点:SpringBoot负责Web层和自动配置,MyBatis负责持久化,MySQL提供数据支撑,前端再用一个播放器页面就能展示完整交互。对比电商系统的订单状态机和社交系统的消息推送,音乐系统的核心业务完全围绕用户、歌曲、歌单之间的关联关系展开,一张歌单表和一张多对多关联表就能把主流程跑通,对新手友好,也给高分型选手预留了音频流传输、歌词同步、热榜缓存这样的深度话题。
对想快速起步的同学,网上能搜到大量SpringBoot音乐系统源码资料,但照搬之前要分清哪些代码可以复用、哪些只是教学层面的简化写法;对想冲优秀毕设的同学,加分项往往集中在几个非模板化的技术决策上。这几个决策分别落在表结构设计、核心链路实现、论文组织和答辩验证四个环节,正好构成一条可执行的主线。
2. 先把数据模型立住:SpringBoot音乐系统的表结构与模块划分
2.1 为什么建表顺序要排在Controller之前
动工写Controller之前,先把表结构定下来,能省掉至少一次Service层重构。Controller是典型薄层,只承担参数校验和结果包装,业务判断集中在Service层;一旦表关系不确定,Service层很快就需要大改。音乐系统的实体关系不算多,但关联方式各有不同:歌曲和歌手是多对一,歌单和歌曲是多对多,用户和播放记录是一对多,评论同时挂在歌曲和歌单两个对象下。这几种关系如果只在代码里用临时集合兜着,后面做分页查询和论文的E-R图时都会返工。
一个可运行的SpringBoot音乐系统,表设计通常落在七张表,职责划分如下:
| 表名 | 职责 | 核心字段 |
|---|---|---|
| user | 用户身份与角色区分 | username / password / role |
| singer | 歌手基础资料,与song多对一 | name / avatar / intro |
| song | 歌曲元数据与音频路径 | song_name / singer_id / url / lyric / play_count |
| song_list | 用户创建的歌单 | user_id / name / cover |
| list_song | 歌单与歌曲的多对多关联表 | list_id / song_id |
| play_record | 播放记录,支撑最近播放页面 | user_id / song_id / played_at |
| comment | 歌曲评论与歌单评论共用 | user_id / song_id / list_id / content |
这张表本身就能直接作为论文“数据库设计”章节的素材。评审如果按表逐张问,回答也有清晰的依据。
2.2 七张核心表的SQL与字段参数说明
CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT '密码密文', `avatar` VARCHAR(255) DEFAULT '' COMMENT '头像路径', `role` TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `singer` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `avatar` VARCHAR(255) DEFAULT '', `intro` TEXT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `song` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `song_name` VARCHAR(100) NOT NULL, `singer_id` BIGINT NOT NULL, `album` VARCHAR(100) DEFAULT '', `duration` INT DEFAULT 0 COMMENT '歌曲时长,单位秒', `url` VARCHAR(255) NOT NULL COMMENT '音频文件存放路径', `cover` VARCHAR(255) DEFAULT '', `lyric` TEXT COMMENT 'LRC歌词原文', `play_count` BIGINT DEFAULT 0 COMMENT '播放热度', KEY `idx_singer` (`singer_id`), KEY `idx_song_name` (`song_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `song_list` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `name` VARCHAR(50) NOT NULL, `cover` VARCHAR(255) DEFAULT '', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `list_song` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `list_id` BIGINT NOT NULL, `song_id` BIGINT NOT NULL, UNIQUE KEY `uk_list_song` (`list_id`, `song_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `play_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `song_id` BIGINT NOT NULL, `played_at` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_user_time` (`user_id`, `played_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `comment` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `song_id` BIGINT DEFAULT NULL, `list_id` BIGINT DEFAULT NULL, `content` VARCHAR(500) NOT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套字段里有几个位置会直接影响后续代码的写法。user表的password不能设计成存明文,注册时用BCrypt加密,登录时调用checkpw比对,这是安全评审的最低要求;song表的url建议存相对路径,比如/files/xxx.mp3,部署时把上传目录映射出去,避免服务器路径变更后数据库里全是失效的绝对路径;duration单位固定为秒,前端进度条显示的mm:ss由后端换算后返回,或在前端自行格式化,不要在数据库里同时存两个版本的时长字段;play_count在播放接口里做自增,不依赖对play_record表做count,页面加载排行榜时少一次聚合查询。
list_song表加联合唯一约束,防止前端重复点击收藏造成重复数据。comment表让song_id和list_id同时允许为空,业务上用“二选一”判断,而不是拆成两张几乎完全一样的表,这样论文E-R图也能少画一对关系。
2.3 SpringBoot配置与自动建表:表结构如何和代码对齐
开发期建表的过程不需要手工回到MySQL客户端重复敲SQL。常见做法是把schema.sql放进src/main/resources目录,然后通过SpringBoot的SQL初始化机制在应用启动时执行。如果希望做到“当表不存在自动建表”,可以这样配置application.yml:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/music?useSSL=false&serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=true username: root password: 123456 sql: init: mode: always schema-locations: classpath:schema.sql encoding: utf-8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: autodatasource url里的createDatabaseIfNotExist=true解决MySQL实例中不存在music库时自动创建数据库的问题。sql.init.schema-locations指定建表脚本位置,mode=always表示每次启动都执行。为了让脚本幂等,SQL里使用CREATE TABLE IF NOT EXISTS,重复执行也不会报错。MyBatis-Plus配置里必须打开map-underscore-to-camel-case,否则singer_id不会自动映射到实体的singerId字段,查询结果会大量为null;id-type: auto让实体主键跟随数据库自增,避免每次插入都手动装配主键。
提示:自动建表适合毕设和本地演示,工程化项目一般会换成Flyway或Liquibase管理迁移。论文里写一句“本系统采用启动初始化建表,便于部署验证”即可,答辩不会在这里纠缠。
2.4 包结构与模块边界
建表之后,SpringBoot项目的包结构建议直接按职责切分,让论文的模块图可以照着画:
src/main/java/com/example/music/ ├── MusicApplication.java ├── common/ // 统一返回结果Result、全局异常处理 ├── config/ // WebMvc配置、JWT拦截器注册、跨域 ├── controller/ // 接口层:参数接收和响应封装 ├── service/ // 业务层:核心逻辑与事务控制 ├── mapper/ // MyBatis-Plus Mapper接口 └── entity/ // 与数据库表对应的实体类这个划分没有引入CQRS或充血模型这类复杂概念,但每一层的职责是清楚的:controller不写SQL,service不出现HttpServletRequest,mapper不写业务判断。论文“系统设计”章节的功能模块图、包结构说明可以复用这段代码块的目录树,再配合E-R图就能形成完整的设计章节。代码规模控制在十个左右的核心类,对毕业设计的评审尺度来说恰好合适。
3. 播放核心链路的后端实现:鉴权、音频流与歌词同步
3.1 登录鉴权:JWT接口与拦截器的最小实现
在线音乐系统的绝大多数接口需要登录态才能操作,歌单、评论、播放记录都不能让匿名用户读写。传统Session方案在前后端分离的部署结构下需要额外配置跨域携带Cookie,答辩时反而要多解释一次;用JWT的最大好处是无状态,后端不存会话,前端在Authorization请求头里带一个token即可,这也符合SpringBoot项目的常见实践。
登录Controller只做一件事:校验用户名密码,通过后签发token。代码结构如下:
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user != null && BCrypt.checkpw(dto.getPassword(), user.getPassword())) { String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(token); } return Result.fail("用户名或密码错误"); }密码比较用BCrypt.checkpw而不是password.equals(user.getPassword()),因为注册时已经用BCrypt加密,直接比字符串永远对不上。lambdaQuery是MyBatis-Plus的查询语法,eq方法对应WHERE username=?,结果用one()而不是list(),能利用username唯一约束直接拿到单条记录。登录成功后返回的token由三部分组成:Header、Payload、Signature,签发时把userId和role放进去,后续接口从token里直接取。
光有登录接口还不够,要用拦截器把token校验和业务逻辑解耦。JwtInterceptor实现HandlerInterceptor的preHandle方法,在Controller执行之前完成解析:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } Claims claims = JwtUtil.parse(token.substring(7)); request.setAttribute("userId", claims.get("userId", Long.class)); return true; } }拦截器注册在WebConfig里:
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private JwtInterceptor jwtInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/song/list", "/api/song/*/stream"); } }注册参数说明:addPathPatterns("/api/**")拦截所有以/api开头的请求;excludePathPatterns里必须保留/api/song/*/stream,音频流是HTML的audio标签直接发起的请求,浏览器拿不到自定义请求头,如果强制校验token就会导致播放失败;登录和注册接口不拦截;前端跨域预检的OPTIONS请求要直接放行,否则拦截器这一层就把请求挡掉了。request.setAttribute让Controller通过getAttribute就能拿到当前用户,无需每个业务接口再解析一次token。
3.2 音频文件上传与HTTP Range请求
歌曲数据的管理员上传接口,边界校验比业务逻辑更重要。上传代码的常见写法:
@PostMapping("/admin/song/upload") public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("songId") Long songId) { String ext = StringUtils.getFilenameExtension(file.getOriginalFilename()); if (!Set.of("mp3", "flac", "wav").contains(ext)) { return Result.fail("仅支持MP3/FLAC/WAV"); } String newName = UUID.randomUUID() + "." + ext; String path = uploadDir + newName; file.transferTo(new File(path)); songService.lambdaUpdate() .eq(Song::getId, songId) .set(Song::getUrl, path) .update(); return Result.ok("上传成功"); }扩展名白名单是必须的,不能只依赖前端做类型检查,防止任意文件上传;UUID重命名避免同名文件互相覆盖。transferTo写入的目录要在代码里提前判断,File.exists()不存在时先创建目录,否则上传会直接抛异常。数据库只存相对路径,磁盘中的真实根目录放到配置项里,部署时通过配置文件切换。
播放是另一个容易翻车的地方。如果用@GetMapping("/song/{id}/audio")把整个文件以InputStream形式写进Response,拖动进度条时会反复下载完整文件。HTML5的audio标签在进度条拖动时会发送Range请求,服务器需要支持HTTP的范围请求并返回206 Partial Content。Spring MVC里用ResourceRegion处理,代码比手动操作InputStream简单得多:
@GetMapping("/api/song/{id}/stream") public ResponseEntity<ResourceRegion> stream(@PathVariable Long id, @RequestHeader(value = "Range", required = false) String range) throws IOException { Song song = songService.getById(id); File file = new File(song.getUrl()); Resource resource = new FileSystemResource(file); long fileLength = file.length(); if (range == null) { return ResponseEntity.ok() .contentType(MediaType.parseMediaType("audio/mpeg")) .body(new ResourceRegion(resource, 0, fileLength)); } long[] rangeArray = parseRange(range, fileLength); long start = rangeArray[0]; long end = rangeArray[1]; return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) .header("Accept-Ranges", "bytes") .header("Content-Range", "bytes " + start + "-" + end + "/" + fileLength) .contentLength(end - start + 1) .contentType(MediaType.parseMediaType("audio/mpeg")) .body(new ResourceRegion(resource, start, end - start + 1)); }parseRange方法需要自己处理三种情况:只有起始位置的bytes=100-,有起始和结束的bytes=100-200,以及超出文件末尾的越界请求。Content-Range的字符串格式是固定的bytes 起始-结束/总大小,空格、斜杠、连字符不能写错,前端解析失败会直接导致播放器认为音频无法定位。正常播放时浏览器第一次请求不带Range,接口返回200和整个文件长度;拖动进度条后再次请求,接口返回206并只返回从start到end的一小段数据。
提示:把这段逻辑作为毕设亮点时,在论文里写“支持HTTP Range协议,实现音频流断点续传与拖动播放”,答辩时值得展开讲。
3.3 LRC歌词解析和播放记录落库
歌词同步功能需要先把LRC文本解析成带时间戳的结构。LRC的原始格式是一行“时间标签加文本”:
[00:12.34]慢慢喜欢你 [00:15.10]慢慢的亲密 [00:18.02]慢慢和你走在一起解析代码用一个正则取时间,一个replaceAll去标签:
public class LrcParser { private static final Pattern TIME = Pattern.compile("\\[(\\d{1,2}):(\\d{1,2})(?:\\.(\\d{1,3}))?]"); public static List<LyricLine> parse(String text) { List<LyricLine> lines = new ArrayList<>(); for (String row : text.split("\r?\n")) { Matcher m = TIME.matcher(row); int time = -1; while (m.find()) { int minute = Integer.parseInt(m.group(1)); int second = Integer.parseInt(m.group(2)); int milli = m.group(3) == null ? 0 : Integer.parseInt(m.group(3)); time = minute * 60_000 + second * 1_000 + milli; } if (time < 0) { continue; } String content = row.replaceAll(TIME.pattern(), "").trim(); lines.add(new LyricLine(time, content)); } lines.sort(Comparator.comparingInt(LyricLine::getTime)); return lines; } }解析时把分钟、秒、毫秒统一换算成毫秒时间戳。前端在audio的timeupdate事件里拿到当前播放位置,二分匹配时间戳小于当前进度且差值最小的那一行歌词。正则里用非捕获组(?:\\.\\d{1,3})处理可选的毫秒部分,避免为两种情况各写一个分支。排序是防御性写法,防止原歌词文件里时间标签乱序。LyricLine是一个只有time和content两个字段的简单POJO,不需要额外处理。
播放记录接口通常和实际播放动作绑定,保存记录并更新热度:
@PostMapping("/api/song/{id}/play") public Result play(@PathVariable Long id, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); PlayRecord record = new PlayRecord(); record.setUserId(userId); record.setSongId(id); playRecordService.save(record); songService.lambdaUpdate() .eq(Song::getId, id) .setSql("play_count = play_count + 1") .update(); return Result.ok(); }播放接口需要登录态,userId来自拦截器放入request的attribute。play_count自增使用setSql直接在SQL层执行增量,避免“先查出来再加一”的并发覆盖问题。play_record表按user_id和played_at加联合索引,查询“最近播放”时走索引排序,数据量在毕设规模下完全够用。
4. 从源码到论文:SpringBoot音乐系统毕设文档的写作框架
4.1 论文目录与工程模块的对应关系
写代码只是毕业设计的一半,论文大纲要能对应到代码,答辩时不用背稿。把论文的章节和SpringBoot工程模块对应起来,评审翻到任何一页都能在代码里找到支撑:
| 论文章节 | 对应工程内容 | 写作要点 |
|---|---|---|
| 绪论 | 选题背景、工程README | 写清要解决的问题和同类系统对比 |
| 相关技术 | pom.xml依赖列表 | SpringBoot、MyBatis-Plus、JWT、LRC |
| 需求分析 | controller接口清单 | 角色划分、用例图、核心流程 |
| 系统设计 | entity、schema.sql、config | 架构图、E-R图、表结构说明 |
| 系统实现 | service、controller核心代码 | 关键模块配流程图和运行截图 |
| 系统测试 | 接口测试记录、测试数据 | 测试用例、边界情况、结果分析 |
这个表格说明一个原则:论文不要凭空写,集中在controller的接口清单、entity和mapper的表结构、config的拦截器配置,都是最直接的论文素材。把每个模块跟论文引用对应上,评审看到的是系统设计和代码实现的一致性,而不是两套各说各话的东西。
4.2 从代码反推关键技术点的描述模板
写技术描述的时候,不需要把每一行代码贴进论文,而是写清楚设计思路和取舍。比如“为什么用JWT”:用户连续点击两次登录,服务器端无需保存Session,因为token本身携带userId和role;请求到达时只需验签和解析,不需要额外查会话状态,Redis在毕设规模下也不需要引入。
统一返回体是另一个好写的点。代码里定义一个Result类,提供success和fail两个静态方法:
public class Result<T> { private int code; private String message; private T data; public static <T> Result<T> ok(T data) { /* 返回code=200 */ } public static <T> Result<T> fail(String msg) { /* 返回code=500 */ } }论文里给一个JSON示例就足够说明问题:
{ "code": 200, "message": "OK", "data": { "token": "eyJhbGciOiJIUzI1NiJ9..." } }接着解释code字段的约定:200成功、401未登录、500系统异常,前端axios在响应拦截器中统一判断,而不是在每个页面重复处理。这部分放在“系统实现”章节的通用模块设计小节,既撑篇幅又不出错。
4.3 测试数据、截图与工作量安排的技巧
毕业设计论文里最容易被看出水分的部分是测试章节,因为很多人的测试用例只有“添加成功、删除成功”六个字。这里有个投入产出比很高的做法:数据库里准备一批结构化的演示数据,十名歌手、每个歌手配三到五首歌、每首歌都带LRC歌词和封面,再创建两个不同角色的用户。测试章节围绕这些真实数据写用例,比如“创建包含3首歌曲的新歌单”“播放一首时长超过5分钟的歌曲验证进度条拖动”“登录后未带token请求评论接口返回401”,每一类都有对应的数据库记录做前后对照。
截图质量对评审印象的影响比预想大。页面截图要在本地调试完成后统一补,避免出现测试数据和正式数据混在一起的情况;浏览器地址栏保持localhost:8080即可,录屏或截图不要暴露本机的其他目录信息。论文的每个功能模块至少配两张图:操作前页面状态、操作后页面状态,代码片段不要超过十行,核心方法贴签名加三行关键实现就够。
5. 答辩前夜:四个接口的验证与演示顺序
5.1 用curl验证鉴权、上传、流播放和播放记录
答辩前不用打开完整的前端页面,四个curl命令就能把核心链路全部验证一遍。假设服务跑在本地8080端口:
BASE=http://localhost:8080 # 1. 登录,把返回的token保存到文件 curl -s -X POST "$BASE/api/user/login" \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"123456"}' # 2. 带上token创建歌单 curl -s -X POST "$BASE/api/list" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"我的收藏"}' # 3. 请求歌曲流的前1024字节,验证是否返回206 curl -s -I -H "Range: bytes=0-1023" "$BASE/api/song/1/stream" # 4. 写一条播放记录,验证play_count自增 curl -s -X POST "$BASE/api/song/1/play" \ -H "Authorization: Bearer $TOKEN"第一个命令返回的JSON里包含token字符串,手动复制到环境变量TOKEN再执行后三条。第二个命令如果返回401,先检查token是否已过期、Authorization头是否带了Bearer前缀。第三个命令用-I只拿响应头,重点看三行:HTTP/1.1 206、Accept-Ranges: bytes、Content-Range: bytes 0-1023/文件总长度,任何一行缺失都说明Range请求处理有bug。第四个命令执行后再查一次song表,play_count比之前多1,说明播放记录链路是完整的。
5.2 演示顺序与答辩追问点
演示顺序决定答辩节奏。建议按下述顺序:先展示登录页和注册逻辑,进入主页面后按“歌单列表→添加歌曲→播放→歌词滚动→查看最近播放”走完整条链路,最后再打开数据库表结构和接口文档页面。播放页面是整个演示的高潮,歌词随进度滚动、拖动进度条后歌词能重新对齐,这两个效果比一百行PPT都有说服力。
答辩老师习惯追问三个方向。一是并发播放怎么做,回答时说明Range请求只传输请求区间、不是整个文件读入内存,这就是一次明确的技术选型;二是为什么用JWT而不用Session,回答无状态、不需要后端会话存储,同时诚实说明毕设没有做token黑名单和自动续期,这是范围取舍;三是数据库表怎么设计,按第二章节的七张表结构用自己的话复述一遍,重点讲list_song联合唯一约束和play_count自增字段。每一次演示都准备好在接口文档页面上指认当前操作对应的HTTP方法和参数,从HTTP请求到音频字节的完整链路都解释清楚,前面的项目准备就不再是评审质疑的理由。
本文还有配套的精品资源,点击获取