ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot在线音乐播放系统毕业设计:从表结构到音频流播放全解析

SpringBoot在线音乐播放系统毕业设计:从表结构到音频流播放全解析 简介这是一份基于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 / rolesinger歌手基础资料与song多对一name / avatar / introsong歌曲元数据与音频路径song_name / singer_id / url / lyric / play_countsong_list用户创建的歌单user_id / name / coverlist_song歌单与歌曲的多对多关联表list_id / song_idplay_record播放记录支撑最近播放页面user_id / song_id / played_atcomment歌曲评论与歌单评论共用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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE singer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, avatar VARCHAR(255) DEFAULT , intro TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套字段里有几个位置会直接影响后续代码的写法。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.ymlspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/music?useSSLfalseserverTimezoneAsia/ShanghaicreateDatabaseIfNotExisttrue 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里的createDatabaseIfNotExisttrue解决MySQL实例中不存在music库时自动创建数据库的问题。sql.init.schema-locations指定建表脚本位置modealways表示每次启动都执行。为了让脚本幂等SQL里使用CREATE TABLE IF NOT EXISTS重复执行也不会报错。MyBatis-Plus配置里必须打开map-underscore-to-camel-case否则singer_id不会自动映射到实体的singerId字段查询结果会大量为nullid-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不写SQLservice不出现HttpServletRequestmapper不写业务判断。论文“系统设计”章节的功能模块图、包结构说明可以复用这段代码块的目录树再配合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 ResponseEntityResourceRegion 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方法需要自己处理三种情况只有起始位置的bytes100-有起始和结束的bytes100-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 ListLyricLine parse(String text) { ListLyricLine 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 ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { /* 返回code200 */ } public static T ResultT fail(String msg) { /* 返回code500 */ } }论文里给一个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端口BASEhttp://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: bytes0-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请求到音频字节的完整链路都解释清楚前面的项目准备就不再是评审质疑的理由。本文还有配套的精品资源点击获取
返回列表