ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+JavaEE音乐播放器系统设计与实现详解

SpringBoot+JavaEE音乐播放器系统设计与实现详解

搞过这类课题项目的朋友应该都有感受:音乐播放器这个题目听起来简单,实际上牵扯到的技术点一点都不少——文件上传、音频流输出、用户登录、歌单管理、播放历史、后台管理,每一块都能单独拆出来做文章。而把题目限定在“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 技术栈明细与版本搭配

我的环境搭配如下:

组件版本说明
JDK1.8稳定且兼容性最好
SpringBoot2.3.4.RELEASE自动配置完善,社区资料丰富
MyBatis3.5.x + mybatis-spring-boot-starter手写SQL更直观,适合自定义查询
MySQL5.7+关系型数据库,适合结构化数据
Thymeleaf3.0.x服务端模板引擎,语法简单
Bootstrap4.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_time

3. 后端核心功能与接口设计思路

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
歌曲无法播放,控制台404song_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,再做后续处理。这样做的好处是,以后就算把项目改成前后端分离,接口也不会有大的破坏性改动。这个习惯在很多企业项目里都很常见,越早建立越好。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进