搞过这类课题项目的朋友应该都有感受:音乐播放器这个题目听起来简单,实际上牵扯到的技术点一点都不少——文件上传、音频流输出、用户登录、歌单管理、播放历史、后台管理,每一块都能单独拆出来做文章。而把题目限定在“SpringBoot + JavaEE”这个组合下,其实非常贴合当前企业级开发的主流套路:服务端以SpringBoot为底座,前端不局限于纯静态页面,既可以做模板渲染,也可以做成前后端分离的接口服务,整套技术栈成熟、资料多、踩坑成本低。
这次我打算围绕这个项目标题,把整个音乐播放器系统的设计思路、核心实现、数据库规划、播放链路、部署经验和常见的坑一次性讲透。不管你是拿它做课程设计,还是想写成简历上的项目亮点,都可以直接对照下面的方案来落地。
1. 项目整体拆解与技术选型
1.1 核心需求解析
从标题看,这个系统的关键词有三个:SpringBoot、JavaEE、音乐播放器。放在一起,其实项目功能是可以预料到的:用户注册登录、歌曲列表浏览、在线播放、歌曲搜索、歌单收藏、播放记录,外加一个后台管理模块来维护歌曲资源和用户信息。这些功能彼此独立却又相互依赖,非常适合用分层架构来实现。
先解释一下为什么这套组合特别适合做学习型项目。SpringBoot本身是对JavaEE开发体验的一次大简化,它把繁琐的XML配置、部署流程都收进了自动配置机制里,让你能更专注于业务代码。但对开发者来说,理解它背后对应的JavaEE规范(Servlet、JSP、Filter、Session这些东西)反而更能帮你搞清楚框架到底帮你做了什么。对于刚接触企业级开发的人来说,先跑通一套SpringBoot项目,回头再看JavaEE底层概念,理解成本会低很多。
我用的是SpringBoot 2.x系列,原因很直接:稳定、资料多、遇到问题搜索引擎一搜基本都有答案。Java版本选了JDK 8,虽然现在已经有很多新版本可选,但考虑到兼容性,JDK 8+SpringBoot 2.x依然是非常稳妥的组合,尤其在课程设计和实训环境下,这套搭配几乎不会出幺蛾子。
1.2 为什么选前后端不分离
近两年前后端分离是大趋势,但在这个项目里我建议采用服务端渲染+局部Ajax的混合方案。原因有三:第一,题目本身以JavaEE为背景,那Servlet处理页面请求、Session维持登录态这套思路一定要体现出来,这是课程设计或面试聊项目时很重要的脉络;第二,音乐播放器页面数量不多,主要是首页、歌单页、播放页、后台管理页,用Thymeleaf做模板渲染开发效率很高,不需要单独维护一套前端工程;第三,真正涉及到播放、搜索这类交互时,通过Ajax调用后端接口完全可以满足体验要求。
这样选择并不意味着完全抛弃接口化思维。我依然把业务能力全部封装成Service层接口,Controller层尽量薄,这样后续如果要拆出纯前端项目,只需要在Controller上补一套REST接口即可。这就是所谓的“前可攻、退可守”,对学习型项目来说非常关键。
实际上我见过不少同学一开始盲目上前后端分离,结果前端项目调接口调不通、跨域问题一堆、登录态处理不来,最后交的项目两头都没做好。用混合方案可以少走很多弯路。
1.3 技术栈明细与版本搭配
我的环境搭配如下:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定且兼容性最好 |
| SpringBoot | 2.3.4.RELEASE | 自动配置完善,社区资料丰富 |
| MyBatis | 3.5.x + mybatis-spring-boot-starter | 手写SQL更直观,适合自定义查询 |
| MySQL | 5.7+ | 关系型数据库,适合结构化数据 |
| Thymeleaf | 3.0.x | 服务端模板引擎,语法简单 |
| Bootstrap | 4.x + 原生JS | 界面框架,不引入多余前端工程 |
| 音频文件 | MP3为主 | 文件存储在本地磁盘 |
这个组合里最值得说的是MyBatis的选择。SpringBoot官方其实推荐Spring Data JPA,但考虑到音乐播放器系统里有大量自定义查询场景——比如多条件搜索、歌单关联歌曲、播放量排名——手写SQL会更加直观可控。而且MyBatis在国内企业里使用率很高,学这个对未来工作帮助更直接。
2. 数据库设计与核心表结构解析
2.1 六张核心表的逻辑关系
音乐播放器系统的数据模型不复杂,但设计不好也会埋坑。我最终规划了六张表:用户表、歌曲表、歌手表、歌单表、歌单歌曲关联表、播放记录表。如果做后台管理,通常还会加管理员表,但考虑到可以复用用户表并添加角色字段,我就没有单独建表,而是给用户表增加了一个user_type字段用于区分普通用户和管理员。
表之间的关联关系如下:歌手和歌曲是一对多,一个歌手可以有多首歌曲;歌单和歌曲是多对多,通过关联表连接;用户和播放记录是一对多;用户和歌单是一对多(歌单归属某个用户,私有歌单)。
这套设计虽然没有复杂的级联关系,但每一张表都对应一个明确的功能模块,在后续实现时,Mapper接口方法的规划也基本按照表来划分,改动成本很低。
2.2 建表SQL与关键字段说明
直接给出一份我在项目中实际使用过的建表SQL,字段命名尽量规范,方便后续代码直接映射:
CREATE TABLE `tb_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像路径', `user_type` tinyint(4) DEFAULT '0' COMMENT '0普通用户 1管理员', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`user_id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `tb_singer` ( `singer_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '歌手ID', `singer_name` varchar(50) NOT NULL COMMENT '歌手姓名', `singer_pic` varchar(255) DEFAULT NULL COMMENT '歌手头像', `singer_intro` text COMMENT '歌手简介', PRIMARY KEY (`singer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌手表'; CREATE TABLE `tb_song` ( `song_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '歌曲ID', `song_name` varchar(100) NOT NULL COMMENT '歌曲名', `singer_id` int(11) NOT NULL COMMENT '歌手ID', `album_name` varchar(100) DEFAULT NULL COMMENT '专辑名', `duration` int(11) DEFAULT NULL COMMENT '时长(秒)', `song_url` varchar(255) NOT NULL COMMENT '歌曲文件路径', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面路径', `play_count` int(11) DEFAULT '0' COMMENT '播放次数', `publish_time` datetime DEFAULT NULL COMMENT '发布时间', PRIMARY KEY (`song_id`), KEY `idx_singer_id` (`singer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌曲表';这里有几个字段值得特别说明。第一个是password字段,我推荐存储加密后的值,哪怕是学习项目也不要明文保存,直接引入Spring Security的BCryptPasswordEncoder或自写一个MD5加盐逻辑都行。第二个是song_url字段,存的是服务器相对路径或基于某个目录的文件名,不要直接存完整的文件系统绝对路径,否则项目换个目录就崩。第三个是duration字段,存整数秒数,可以写一个工具类在后台导入歌曲时自动解析音频文件时长,避免手动录入。
2.3 关联表与播放记录的写法
歌单歌曲关联表是整个系统里最容易被忽略但也很重要的一张表。它只需要三个字段:关联ID、歌单ID、歌曲ID。在做歌单详情查询时,需要联查歌曲表拿到完整歌曲信息,SQL大概是这样:
SELECT s.song_id, s.song_name, s.duration, s.song_url, s.cover_url, si.singer_name FROM tb_song s LEFT JOIN tb_singer si ON s.singer_id = si.singer_id WHERE s.song_id IN ( SELECT song_id FROM tb_playlist_song WHERE playlist_id = #{playlistId} )播放记录表的设计逻辑是保留每个用户最近播放的若干条记录,方便“最近播放”模块展示。为了控制数据量,我在插入新记录时先删除该用户超过50条的旧记录,这种处理方式放在实际生产环境可能不够优雅,但对课程设计和中小型系统来说简单有效。
播放记录表字段:record_id, user_id, song_id, play_time3. 后端核心功能与接口设计思路
3.1 登录鉴权:Session方案还是JWT方案
这是很多人在做类似系统时会纠结的点。我最终选了Session方案,技术理由是这个系统的页面渲染与应用状态都在服务端,SpringBoot内嵌了Tomcat,可以直接使用HttpSession来保存登录状态,实现简单且符合JavaEE的经典做法。如果是跨端场景,我会建议JWT,但在这个项目里没有这个必要。
实现逻辑也不复杂:用户提交用户名和密码,后端校验通过后放入session,并且加一个拦截器来统一拦截需要登录才能访问的路径。拦截器里对放行路径做好区分,像登录页、注册页、静态资源和歌曲播放接口这三类必须放行,否则会出现登录跳转的死循环。
为了提升一点安全性,密码校验不要只查一次数据库,我习惯在Service层先根据用户名查出用户,再用加密匹配工具类来做校验。这样做的好处是即使用户名不存在,也能走同样的流程,避免通过响应时间差来判断用户是否存在。
3.2 歌曲上传与文件存储方案
后台管理系统里最重要的功能就是歌曲资源上传。歌曲文件一般比较大,需要特别注意SpringBoot的文件上传大小限制。默认限制是1MB,如果不上调,上传一个3MB的MP3就会直接报错。在application.yml里配置如下:
spring: servlet: multipart: max-file-size: 100MB max-request-size: 100MB文件存到哪里很关键。我在项目里用了两种方式共存:开发时直接存到项目本地的upload目录,方便调试;部署时通过配置项指定一个外部磁盘目录,避免打包后找不到路径。如果你用的是IDEA开发,注意上传目录最好在target之外,否则重启可能会被清理或覆盖。
一个非常实用的细节:上传成功后,数据库里保存的是相对路径,如/upload/music/xxx.mp3,而前端播放器直接使用这个相对路径拼接本地服务地址。这样即使以后把文件迁移到OSS或CDN,只需要全局替换一层路径前缀就够了。
3.3 搜索功能的多条件SQL拼接
搜索功能看起来简单,做起来很容易踩坑。用户的搜索条件可能有:歌曲名称、歌手名称、歌曲ID,甚至可能几个条件同时填。为了应对这种情况,我并没有为每种组合写死SQL,而是通过MyBatis动态SQL来做条件拼接。这样既不会漏条件,也方便扩展。
核心Mapper大概是这样:
<select id="searchSongs" resultType="map"> SELECT s.song_id, s.song_name, s.duration, s.song_url, si.singer_name FROM tb_song s LEFT JOIN tb_singer si ON s.singer_id = si.singer_id <where> <if test="songName != null and songName != ''"> AND s.song_name LIKE CONCAT('%', #{songName}, '%') </if> <if test="singerName != null and singerName != ''"> AND si.singer_name LIKE CONCAT('%', #{singerName}, '%') </if> </where> ORDER BY s.play_count DESC </select>注意我用了LEFT JOIN而不是INNER JOIN,原因是歌曲表里可能存在歌手被误删的情况,这时候歌曲信息仍然应该能查出来,只是歌手名为空。这种细节在交项目时会显得你考虑得比别人周全。
3.4 播放量统计接口
播放量统计这个功能点不大,但在项目里很重要,因为排行榜模块就是靠它实现的。我的做法是提供一个更新播放次数的接口,前端播放器在每次成功开始播放时调用一次,后端在Service层把play_count加一。为了避免刷播放量的问题(虽然项目里不一定考虑这个),可以简单地在短时间内相同IP或同一用户不重复计数。
最终排行榜的SQL就是按播放次数倒序查出前十条歌曲,配合封面和歌手名一起返回。这里记得给歌曲表加一个覆盖查询的联合索引,否则歌曲数量一多查询会变慢,索引这样加就可以了:
ALTER TABLE tb_song ADD INDEX idx_play_count (play_count);4. 在线播放链路与音频流输出的实现细节
4.1 音频文件怎么通过接口返回
音乐播放器的核心无疑是播放功能。常见的做法是把音频文件放在静态目录下,前端直接用<audio src="/upload/music/xxx.mp3">播放。这个方式最简单,但我没有直接采用,因为如果将来需要做权限控制(比如只有VIP用户能听付费歌曲),静态文件路径暴露之后就无法控制了。所以我写了一个Controller接口来输出音频数据。
播放接口的实现思路是利用HttpServletResponse的输出流把文件字节写出,同时设置好ContentType和缓存响应头。示例代码逻辑如下:
@GetMapping("/song/play/{songId}") public void playSong(@PathVariable Integer songId, HttpServletResponse response) throws IOException { Song song = songService.findById(songId); String filePath = song.getSongUrl(); // 解析为磁盘绝对路径 File file = new File(basePath + filePath); if (!file.exists()) { response.setStatus(404); return; } response.setContentType("audio/mpeg"); response.setHeader("Accept-Ranges", "bytes"); // 播放时顺手增加播放次数 songService.increasePlayCount(songId); try (InputStream is = new FileInputStream(file); OutputStream os = response.getOutputStream()) { byte[] buffer = new byte[1024 * 4]; int len; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } os.flush(); } }这个实现放在开发环境跑没问题,但它确实没处理HTTP的Range请求头,意味着播放器无法拖拽进度。如果你想要更专业的播放体验,就需要支持Range请求,那样播放器才能随机跳转音频的任意位置。后面我会单独讲怎么实现。
4.2 一次踩坑记录:音频文件无法拖动进度条
这是我当初调试播放功能时遇到的一个典型问题:歌曲能自动播放,但进度条拖不动,一拖就回到开头或者直接卡死。排查了一圈,Host发现核心原因有两个。
第一个是接口没有返回Content-Length响应头,浏览器不知道文件总大小,自然无法定位拖动位置。第二个是没有解析Range请求头,浏览器请求时带着Range: bytes=0-,服务端如果不理会直接返回整个文件,浏览器就按完整重新加载处理,进度条当然就废了。
正确的处理方式是:通过请求头获取Range信息,然后读取指定区间的字节返回,配合Content-Range响应头标记当前输出的区间。可以参考下面的核心实现思路:
long fileLength = file.length(); String range = request.getHeader("Range"); long start = 0; long end = fileLength - 1; if (range != null && range.startsWith("bytes=")) { String[] arr = range.replace("bytes=", "").split("-"); start = Long.parseLong(arr[0]); if (arr.length > 1 && !arr[1].isEmpty()) { end = Long.parseLong(arr[1]); } } response.setStatus(206); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength); response.setHeader("Content-Length", String.valueOf(end - start + 1));实现后半段还是通过输出流分段写出,配合一个response.flushBuffer()刷新缓冲区。前端使用<audio>标签,拖动进度条时它会自动带上Range头,整套交互就顺滑了。
4.3 多格式音频兼容与转码建议
音频格式方面,MP3是兼容性最好的,基本上所有浏览器都能直接播放,所以我建议项目里的测试数据统一用MP3。如果你需要支持其他格式,比如WAV、AAC、OGG,也可以直接在播放时根据文件后缀设置不同的ContentType。
还有个小建议:上传歌曲时,我习惯同时解析出音频时长并写入数据库。JDK自带AudioSystem类可以读取部分格式的时长,但对于MP3这种压缩格式不靠谱,可以考虑引入开源库来解析元数据。如果不想增加依赖,也可以在后台录入歌曲时手动填一个大概时长,反正对播放本身没有影响。
不过要提醒一下,在前端播放器页面,音频加载完毕之后不要忘了获取并更新当前播放时间,否则“当前时间/总时长”这个显示可能是错的。很多人会忽略这一点,导致总时长一直显示00:00,体验就很奇怪。
5. 前端页面与播放器交互实现
5.1 页面模板规划:从首页到后台管理
页面结构上我重点开发了这样几个页面:用户首页(歌曲推荐与最新入库)、歌手列表页、歌单详情页、歌曲搜索页、播放器页、用户登录注册页、后台管理页(歌曲列表与上传、用户管理、数据统计)。
为了减少重复代码,每个页面都要抽取公共头部和尾部片段,Thymeleaf的th:fragment非常方便。目录上大致是:
templates/ ├── index.html ├── login.html ├── register.html ├── song/ │ ├── list.html │ └── detail.html ├── playlist/ │ └── detail.html └── admin/ ├── song-manage.html ├── song-upload.html └── user-manage.html每个页面通过Thymeleaf表达式直接从Model中取值,比如歌曲列表:
<audio controls preload="metadata" th:src="@{/song/play/{id}(id=${song.songId})}">5.2 播放器组件的交互思路
播放器部分我没有引入第三方组件库,直接使用原生HTML5<audio>元素加少量JS,因为原生标签已经支持了播放暂停、进度条、音量调节等能力。需要自己动手做的其实是歌曲切换和播放列表。
我的做法是把当前播放列表的歌曲数据渲染到页面上,记录当前播放索引,点击歌曲时更新audio的src并调用play()方法。播放结束后自动切换到下一首,到最后一首时根据需求选择停止还是循环。核心逻辑如下:
let playIndex = 0; let playList = []; // 由服务端渲染传入 function playSong(index) { playIndex = index; const song = playList[index]; audio.src = '/song/play/' + song.songId; audio.play(); // 更新当前播放状态 } audio.addEventListener('ended', function () { if (playIndex < playList.length - 1) { playSong(playIndex + 1); } else { currentTime = 0; } });每首歌播放时,把歌曲ID传给后端进行播放次数更新,用Ajax即可。不要用同步请求,否则会影响播放的流畅度。
5.3 搜索页面与播放页面的交互衔接
搜索页面展示搜索结果时,我会在每首歌曲后面加一个播放按钮,同时把搜索结果的歌曲数组拼成一个播放列表。这样一来,用户点击搜索结果就可以连续播放该结果列表下的所有歌曲,体验会比单曲播放好不少。
具体做法是:搜索结果通过Map结构返回,歌曲基本信息足够即可;前端读取列表时,将整个结果数组赋给playList变量,并记录第一个播放的位置。另外搜索框也可以做一个防抖处理,用户停止输入后延迟500毫秒再发请求,既减少服务端压力,也避免频繁刷新页面。
5.4 界面细节:封面、头像与默认图处理
歌曲封面和歌手头像这类图片资源,项目初期数据量少的时候可能容易漏配,这时页面会显示一个破图标,非常难看。我的处理方式是给所有图片标签加上onerror回调,加载失败时自动切换成默认图:
<img th:src="${song.coverUrl}" onerror="this.onerror=null; this.src='/img/default-cover.png'" />这种细节点缀能明显提升整体观感。后台管理页面我也会设置一个统一的表格样式和操作按钮组,再加上一个上传进度提示,界面基本拿得出手了。
6. 后台管理模块与数据安全
6.1 管理员身份与拦截器级别的访问控制
后台管理页面绝不能只靠前端隐藏入口,后端一定要做访问控制。我在User对象里用user_type区分普通用户和管理员,在配置拦截器的时候,除了登录拦截,还要单独维护一个管理员拦截器或扩展同一种拦截器,并传入可访问路径集合。
拦截器判断的逻辑是这样的:先确认用户是否已登录,再判断请求路径是否以/admin开头,如果是则校验用户type是否为1。如果登录了但type不足,直接重定向到首页,不要给出太具体的提示信息,避免泄露后台路径。
这里还有一个容易被忽略的细节:静态资源路径一定要放行,比如/upload/**、/css/**、/js/**、/img/**,同时也要放行歌曲播放接口和图片查看接口。不然登录页因为没加载出CSS样式,可能第一眼就是错乱的。拦截器配置需要自己逐条核对,不要盲目复制网上的配置。
6.2 文件上传的安全校验
后台上传歌曲时不能只校验扩展名是不是.mp3,因为攻击者完全可以伪造扩展名上传一个恶意脚本。稳妥做法是双重校验:前端显示格式校验,后端在接收文件后除了看扩展名,也要检查MIME类型或者读一下文件头部字节(MP3文件的头部通常是ID3标记或FFFx的帧同步)。
上传文件名一定要重写。我使用的是UUID或时间戳拼接原始后缀,避免中文文件名和特殊字符引发乱码或路径穿越问题。举例来说,原始文件名歌名/../evil.jsp这种内容绝对不能直接拼到路径里。
还有一个上传安全点:把上传目录设置为无法执行脚本。在Linux部署环境下,可以关闭upload目录的执行权限或通过SpringBoot静态资源处理方式,让上传文件只作为资源被读取而不是被容器解析执行。
6.3 敏感数据脱敏与日志记录
后台用户管理页面会展示所有注册用户,密码字段绝对不能查询出来。DAO层查询时就要排除password列,或者查询结果中把password置空,双保险。日志里也不能打印用户的密码、邮箱、手机号这类完整信息。
另外我在后台管理模块里加了一个简单的操作日志记录表,记录管理员上传歌曲、删除歌曲、封禁用户等主要操作。这个功能在代码层面同样可以很轻量,通过在Service层对应方法里调用一个日志Service即可。如果时间不够,可以先放弃,但做了的话对项目评分和表现都是加分项。
7. 部署、运维与常见问题排查
7.1 打包部署与配置文件分离
项目完成后需要在服务器上跑起来,打包方式直接用Maven打包成可执行Jar文件。要注意的一点是,本地开发时的application.yml和服务器环境往往不同,尤其是数据库地址、用户名密码、上传文件路径这三类配置。
我的做法是使用SpringBoot多环境配置文件,比如application-dev.yml和application-prod.yml,打包后启动时用参数指定环境:
java -jar music-player.jar --spring.profiles.active=prod这样本地开发用dev环境,服务器部署用prod环境,互不影响。而且可以避免把数据库密码写死在代码里后被打包进Jar,部署时通过启动参数或环境变量注入。
7.2 数据库初始化与数据初始化脚本
为了能让项目在一台新机器上跑起来,我准备了一个初始化SQL脚本,里面不仅建好库、建好表,还插入了几条初始数据:两三个歌手、十几首歌曲、一个测试账号和一个管理员账号。这样部署完直接就能看到效果,不需要自己一条条往库里造数据。
上传歌曲文件的同步也很重要。如果歌曲文件单独放到了upload目录,部署时要把该目录整个带到服务器上,并确保Jar启动用户对该目录有读写权限。否则会出现歌曲能看到,但播放接口报404或IO异常的问题。
7.3 几个高频Bug与解决速查
结合我自己调试项目时的经验,把遇到频率最高的问题整理成一张速查表,供参考:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录后页面不断跳回登录页 | 拦截器放行路径没配全,静态资源被拦截 | 核对拦截器配置,放行css/js/img/upload路径 |
| 歌曲上传报文件大小超限 | SpringBoot默认1MB限制 | 在配置中调大max-file-size |
| 歌曲无法播放,控制台404 | song_url存的是绝对路径,部署后找不到 | 统一改为相对路径,拼接外部存储目录 |
| 进度条无法拖动 | 服务端未支持Range请求 | 按本章节方案实现Range输出 |
| 数据库中文乱码 | 连接URL没指定编码 | JDBC连接串加useUnicode=true&characterEncoding=utf8 |
| 上传中文文件名乱码 | 部分环境解码不一致 | 上传后重命名为UUID文件名 |
7.4 项目扩展方向建议
项目交付之后如果想让它更有亮点,有几个扩展方向可以尝试。一是把播放量统计改为定时任务,每天凌晨汇总前一天的播放数据并缓存到Redis,排行榜直接查缓存,减少数据库压力。二是增加歌单标签和分类,在平台上开放更多维度的浏览入口。三是可以在播放接口上加一层访控,比如未登录用户只能试听前60秒,这会逼着你把Range处理做得更完善。
这些方向不一定在课程设计时间里全部实现,但可以在项目的README或文档里写清楚后续规划,展现出你考虑问题的系统性。
8. 个人心得与复盘
这个项目做完之后,我最大的体会是:评估一个项目好不好,不在于它用了多新的技术,而在于它把所有基础功能是否都做成了一条闭合链路。从用户注册登录到浏览歌曲,再到播放、统计、后台管理,这中间涉及到的每一个环节,几乎都是企业级开发里天天见到的业务场景,能把这些串起来并且稳定跑通,就已经很有价值了。
复盘下来,有不少代码实现我还可以写得更好。比如播放接口直接采用SpringBoot的ResourceHttpRequestHandler或者配合Range请求的ResourceRegion来做,代码会更简洁;比如播放记录表的时间字段可以建索引方便查询;再比如歌曲播放次数的并发更新问题,需要用乐观锁保证不出现丢失更新。这些都属于在实际项目迭代中会遇到的性能话题,放在这个学习项目里属于加分内容。
最后分享一个小技巧:开发过程中我会给所有接口统一加上一个简单的响应包装类,定义code、message、data三个字段。页面通过Ajax拿到结果先判断code,再做后续处理。这样做的好处是,以后就算把项目改成前后端分离,接口也不会有大的破坏性改动。这个习惯在很多企业项目里都很常见,越早建立越好。