Spring Boot仿网易云音乐系统实战:Java毕设项目与面试加分项
想写一个既能应付毕业设计又能写进简历的 Java 项目很多人第一反应是“做个商城”或者“做个博客”。这两个方向不是不行而是太常见了。面试官看到这类项目几乎不用细问就能猜到里面的大致结构无非就是商品表、订单表、用户表或者文章表、分类表、评论表。真正到了简历筛选和技术面环节这种项目的区分度很低。相比之下仿“网易云音乐”这类带明确产品形态的系统反而更容易让人眼前一亮。它表面上是一个音乐播放平台实际却把 Java Web 开发里最常见的用户、内容、关联关系、搜索、权限、前后台交互全串起来了。更关键的是这个项目的功能边界非常清楚不会像商城系统那样越做越复杂也不会像博客项目那样做完之后觉得没什么可讲。这篇文章要做的就是基于 Spring Boot 和 IDEA 开发环境把“Java 网易云音乐系统”的完整构建思路讲透。我会先给出功能设计和架构选型再拆解开发流程然后用一套最小可运行示例带你跑通登录、歌曲搜索、歌单创建、评论收藏这些核心链路。标题说“一小时搞定”对熟悉 Spring Boot 的人来说确实可以但对刚接触的同学来说更重要的是知道每一步为什么要这样做踩了坑之后怎么排查。读完这篇文章你不仅能搭出一个能演示的毕设项目还能在简历和答辩时讲清楚设计思路。1. 这篇文章真正要解决的问题刚开始做项目的人通常会遇到两个问题一是不知道选什么题目二是选了一个看起来简单的题目做到一半才发现坑很深。“网易云音乐系统”这个选题之所以值得做就是因为它处在一个很合适的位置上。它不像电商系统那样需要处理库存、支付、物流等复杂业务也不像博客系统那样业务逻辑基本就是 CRUD。它的核心业务——用户听歌、管理歌单、收藏歌曲、发表评论——既有真实的产品价值又适合用一套标准的分层架构来实现。从技术学习的角度看这个项目覆盖的范围非常全。你要设计用户表、歌曲表、歌单表、评论表这训练的是数据库建模能力你要实现注册登录和权限拦截这训练的是 Session、Token 或 Spring Security 相关能力你要做歌曲的模糊搜索和歌单的分类筛选这训练的是 MyBatis 或 MyBatis-Plus 的动态 SQL你要把歌曲列表和播放页在前端展示出来这训练的又是模板引擎或前后端分离的整合能力。一个项目下来Java 基础知识、Spring Boot 常用组件、数据库设计、前后端联调基本上都过了一遍。这篇文章的读者主要分三类。第一类是准备毕业设计的学生需要一个能独立完成、功能完整、答辩有内容可讲的项目。第二类是准备实习或校招的 Java 开发者需要一个能写进简历、面试时能展开细讲的实战项目。第三类是刚开始学 Spring Boot想找一个完整案例把理论知识串起来的人。对于第一类读者我会把开发步骤和常见坑讲清楚对于第二类读者我会在最佳实践部分补充设计思路和答辩注意点对于第三类读者你可以跟着核心流程一节先把最小系统跑起来再逐步扩展。这里有一个很重要的判断这类项目的核心价值不是“仿”这个产品本身而是通过一个像样的业务场景把你掌握的 Java 技术真实地用起来。真正让你通过答辩或面试的不是界面上有一个播放按钮而是你能说清楚用户和歌单之间为什么用中间表评论查询为什么要分页登录信息保存在哪里、为什么安全。2. 系统功能拆解与项目架构在写代码之前先得把系统要做什么定下来。一个适合毕设和中高级实战练习的仿网易云音乐系统通常包含以下几个模块。2.1 功能模块用户模块注册、登录、退出登录、个人信息查看。这是几乎所有系统的入口也是权限控制的基础。注册时密码要做加密处理不能明文入库登录后要保存会话状态后续接口需要做登录校验。歌曲模块歌曲列表、歌曲搜索、歌曲详情、歌曲上传可选。歌曲的字段一般包括歌曲名、歌手、专辑、封面图地址、音频文件地址、歌词、播放时长等。搜索是这块的核心需要支持按歌曲名、歌手名模糊查询。歌单模块创建歌单、查看歌单、歌单添加/移除歌曲、收藏歌单。歌单和歌曲是多对多关系歌单和用户也是多对多关系需要设计中间表。这个模块直接考察你的关联查询能力。评论模块对歌曲发表评论、查看评论列表、删除自己发布的评论。评论是典型的一对多场景也是分页查询的练习点。播放记录模块记录用户的播放历史按时间倒序展示最近播放。这个模块虽然简单但能体现项目的完整性。2.2 技术选型技术选的不用太花哨重点是稳定、文档多、能讲清楚。层次技术选型说明开发语言Java 8 或 Java 11兼容性最好学习资料最多后端框架Spring Boot 2.x / 3.x快速搭建内置 Tomcat持久层MyBatis-Plus 或 Spring Data JPA推荐 MyBatis-Plus写 SQL 灵活学习资料多数据库MySQL 5.7 或 8.x免费、通用、面试常问前端Thymeleaf Bootstrap或 Vue CDN不要求复杂前端能展示数据即可构建工具Maven项目管理标准IDEIntelliJ IDEA社区版即可完成全部开发附加组件Lombok简化实体类代码如果不熟悉可以不用这里要说明一下很多毕设项目喜欢引入 Redis、RabbitMQ、Elasticsearch 这些中间件。这本身不是错但如果你对这些中间件的原理不熟悉面试或答辩时反而容易被追问到失控。我的建议是先把 MySQL 和 Spring Boot 这套基础链路做扎实有余力再考虑引入 Redis 做验证码缓存或热门歌单缓存。2.3 项目分层项目采用标准的 Controller-Service-Mapper 三层架构。Controller 层负责接收请求、参数校验、调用 Service、返回结果Service 层负责业务逻辑比如注册时检查用户名是否重复、创建歌单时建立关联关系Mapper 层负责和数据库交互写 SQL 或使用 MyBatis-Plus 提供的方法。这个分层的好处是职责清晰Controller 不直接写 SQLMapper 不写业务判断Service 不关心 HTTP 请求长什么样。项目变大之后这种分层能让你快速定位问题也方便写单元测试。3. 环境准备与前置条件动手写代码之前先把环境准备好。这里列一下我建议的最低配置。软件版本建议用途JDK1.8 及以上Java 运行环境Maven3.6 及以上依赖管理和构建IntelliJ IDEACommunity 版即可开发 IDEMySQL5.7 或 8.x数据库Navicat 或 MySQL Workbench任意数据库可视化管理Postman 或 Apifox任意接口测试版本这块要特别提醒Spring Boot 3.x 要求 JDK 17 及以上如果你本机装的是 JDK 8那我建议直接用 Spring Boot 2.7.x不要盲目追新。如果你用的 Spring Boot 版本比较高和一些 starter 的整合出现兼容问题优先检查版本对应关系而不是去看网上那些“万能配置”。另外IDEA 社区版就够用不需要破解版。官方下载入口直接搜 IntelliJ IDEA 官网选择 Community Edition 安装即可。项目创建时社区版同样支持 Spring Initializr 导入项目。数据库方面需要提前建好库。这里以数据库名netease_music为例CREATE DATABASE IF NOT EXISTS netease_music DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;用utf8mb4而不是utf8是因为歌曲名、评论内容可能包含特殊符号utf8mb4能完整存储。4. 核心流程拆解从零跑通一个 Spring Boot 项目这一节是整个项目的开发主线。我会按照实际开发顺序一步步说明做什么、为什么、要注意什么。4.1 创建项目和引入依赖最简单的创建方式是到 IDEA 里新建 Spring Initializr 项目或者直接在 start.spring.io 生成后导入 IDEA。基础依赖只需要四个Spring Web提供 MVC 和内置 Tomcat。Thymeleaf做服务端页面渲染。MySQL Driver数据库驱动。MyBatis-Plus或 MyBatis做数据持久化。如果你的构建方式是 Maven那么核心依赖如下。这里用pom.xml片段展示!-- 文件路径pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- MyBatis-Plus注意版本要和 Spring Boot 版本匹配 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Lombok简化实体类代码 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies如果你的环境是 Spring Boot 3.x那么mysql-connector-java的 groupId 会变为com.mysql:mysql-connector-j版本也要对应调整。遇到这类问题直接看官方 Maven 仓库的说明即可不要在不确定的情况下写死坐标。4.2 配置数据源和端口在src/main/resources/application.yml中配置数据源、MyBatis-Plus 和端口。# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/netease_music?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里把thymeleaf.cache设置为false开发时改页面不用重启就能看到效果生产环境再改回true或者不配置。MyBatis-Plus 的log-impl会在控制台打印 SQL调试时很有用。真正上线时建议去掉否则日志会很多。数据库账号密码用你自己本机的我这里只是一个示例。如果连接失败先去检查url、username、password三个位置有没有写错。4.3 设计数据库表数据库设计是这类项目的重点也是答辩时一定会被问到的部分。核心表设计如下。用户表user存放账号、密码、昵称、头像。密码字段用password命名但存的值必须是加密后的密文。歌曲表song存放歌曲名、歌手、专辑、封面图、音频地址、歌词、播放时长。歌单表playlist存放歌单名称、创建者、歌单介绍、封面图。歌单歌曲关联表playlist_song因为一首歌可以出现在多个歌单一个歌单也有多首歌所以需要一个中间表。评论表comment关联歌曲和用户记录评论内容和评论时间。一张最小但完整的建表 SQL 如下-- 文件路径sql/netease_music.sql USE netease_music; CREATE TABLE IF NOT EXISTS user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(128) NOT NULL COMMENT 加密后的密码, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE IF NOT EXISTS song ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 歌曲ID, name VARCHAR(100) NOT NULL COMMENT 歌曲名, singer VARCHAR(100) DEFAULT NULL COMMENT 歌手, album VARCHAR(100) DEFAULT NULL COMMENT 专辑, cover_url VARCHAR(255) DEFAULT NULL COMMENT 封面图地址, audio_url VARCHAR(255) DEFAULT NULL COMMENT 音频文件地址, lyric TEXT COMMENT 歌词, duration INT DEFAULT 0 COMMENT 时长单位秒 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT歌曲表; CREATE TABLE IF NOT EXISTS playlist ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 歌单ID, user_id BIGINT NOT NULL COMMENT 创建者ID, name VARCHAR(100) NOT NULL COMMENT 歌单名称, description VARCHAR(500) DEFAULT NULL COMMENT 歌单介绍, cover_url VARCHAR(255) DEFAULT NULL COMMENT 封面图地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT歌单表; CREATE TABLE IF NOT EXISTS playlist_song ( id BIGINT PRIMARY KEY AUTO_INCREMENT, playlist_id BIGINT NOT NULL, song_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_playlist_song (playlist_id, song_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT歌单歌曲关联表; CREATE TABLE IF NOT EXISTS comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 评论ID, user_id BIGINT NOT NULL COMMENT 评论用户ID, song_id BIGINT NOT NULL COMMENT 评论歌曲ID, content VARCHAR(500) NOT NULL COMMENT 评论内容, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 评论时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT歌曲评论表;这里有一个新手容易忽略的点user表名的反引号。因为user在 MySQL 里是系统关键字直接执行CREATE TABLE user会报错需要加上反引号。为避免这个问题也可以把表命名为sys_user或music_user在代码里对应修改即可。4.4 编写实体类和 Mapper实体类对应数据库表。以用户表为例使用 Lombok 简化代码// 文件路径src/main/java/com/example/music/entity/User.java package com.example.music.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String password; private String nickname; private String avatar; private LocalDateTime createTime; }Mapper 接口直接继承 MyBatis-Plus 的BaseMapper这样简单的增删改查就不用写 SQL 了。// 文件路径src/main/java/com/example/music/mapper/UserMapper.java package com.example.music.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.music.entity.User; import org.apache.mapper.stereotype.Mapper; Mapper public interface UserMapper extends BaseMapperUser { }注意这里的Mapper注解或者你可以在启动类上加MapperScan(com.example.music.mapper)两种方式任选其一。如果 Mapper 接口没有被扫描到启动时会报“找不到 bean”的错误。4.5 Service 和 ControllerService 层负责业务逻辑。以注册为例需要判断用户名是否已经存在密码用 BCrypt 加密后再入库。// 文件路径src/main/java/com/example/music/service/UserService.java package com.example.music.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.music.entity.User; import com.example.music.mapper.UserMapper; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.Resource; Service public class UserService { Resource private UserMapper userMapper; private final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); public boolean register(String username, String password, String nickname) { Long count userMapper.selectCount( new QueryWrapperUser().eq(username, username)); if (count 0) { return false; } User user new User(); user.setUsername(username); user.setPassword(encoder.encode(password)); user.setNickname(nickname null ? username : nickname); userMapper.insert(user); return true; } public User login(String username, String password) { User user userMapper.selectOne( new QueryWrapperUser().eq(username, username)); if (user ! null encoder.matches(password, user.getPassword())) { return user; } return null; } }这里用到了spring-security-crypto的BCryptPasswordEncoder但你不需要引入完整的 Spring Security只需要添加这个加密依赖即可。可以在pom.xml加dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependencyController 层把 HTTP 请求转成 Service 调用。登录成功后把用户 ID 和用户名放入 Session。// 文件路径src/main/java/com/example/music/controller/UserController.java package com.example.music.controller; import com.example.music.entity.User; import com.example.music.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpSession; Controller public class UserController { Resource private UserService userService; GetMapping(/register) public String registerPage() { return register; } PostMapping(/register) public String register(RequestParam String username, RequestParam String password, RequestParam(required false) String nickname, RedirectAttributes attributes) { boolean success userService.register(username, password, nickname); if (!success) { attributes.addFlashAttribute(error, 用户名已存在); return redirect:/register; } return redirect:/login; } PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpSession session, RedirectAttributes attributes) { User user userService.login(username, password); if (user null) { attributes.addFlashAttribute(error, 用户名或密码错误); return redirect:/login; } session.setAttribute(currentUser, user); return redirect:/songs; } GetMapping(/logout) public String logout(HttpSession session) { session.invalidate(); return redirect:/login; } }这里有一个典型误区有人会把密码校验逻辑放到 Controller 里或者在 Controller 里直接调用 Mapper。这样写虽然也能跑但业务逻辑散落在各层项目一复杂就不好维护。正确的节奏是 Controller 只做参数接收和页面跳转业务判断全部下沉到 Service。5. 完整示例与代码实现上一节已经把用户模块的核心代码展示了。这一节补充歌曲搜索、歌单创建、评论三个核心功能的最小实现凑成一个“能演示 80% 业务”的系统。5.1 歌曲搜索接口歌曲搜索最常见的场景是在首页输入关键词按歌名或歌手模糊匹配返回歌曲列表。使用 MyBatis-Plus 的QueryWrapper.like即可。// 文件路径src/main/java/com/example/music/service/SongService.java package com.example.music.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.music.entity.Song; import com.example.music.mapper.SongMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; Service public class SongService { Resource private SongMapper songMapper; public ListSong search(String keyword) { QueryWrapperSong wrapper new QueryWrapper(); if (keyword ! null !keyword.trim().isEmpty()) { wrapper.like(name, keyword) .or() .like(singer, keyword); } wrapper.orderByDesc(id); return songMapper.selectList(wrapper); } }Controller// 文件路径src/main/java/com/example/music/controller/SongController.java package com.example.music.controller; import com.example.music.entity.Song; import com.example.music.service.SongService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Resource; import java.util.List; Controller public class SongController { Resource private SongService songService; GetMapping(/songs) public String list(RequestParam(required false) String keyword, Model model) { ListSong songs songService.search(keyword); model.addAttribute(songs, songs); model.addAttribute(keyword, keyword); return songs; } }在QueryWrapper里同时用like和or时要注意如果后续还有别的查询条件or的优先级可能会和预期不一致。更稳妥的写法是用and(w - w.like(...).or().like(...))包裹这里因为只有两个条件直接写不会出问题但你在扩展的时候要留意这一点。5.2 歌单创建和添加歌曲歌单是典型的“中间表”业务。创建歌单只需要插入一条playlist记录添加歌曲则是往playlist_song里插入关联数据。// 文件路径src/main/java/com/example/music/service/PlaylistService.java package com.example.music.service; import com.example.music.entity.Playlist; import com.example.music.entity.PlaylistSong; import com.example.music.mapper.PlaylistMapper; import com.example.music.mapper.PlaylistSongMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; Service public class PlaylistService { Resource private PlaylistMapper playlistMapper; Resource private PlaylistSongMapper playlistSongMapper; public Long createPlaylist(Long userId, String name, String description) { Playlist playlist new Playlist(); playlist.setUserId(userId); playlist.setName(name); playlist.setDescription(description); playlistMapper.insert(playlist); return playlist.getId(); } Transactional(rollbackFor Exception.class) public void addSongToPlaylist(Long playlistId, Long songId) { PlaylistSong relation new PlaylistSong(); relation.setPlaylistId(playlistId); relation.setSongId(songId); playlistSongMapper.insert(relation); } }这里两个操作都加了Transactional。特别是addSongToPlaylist如果不加事务插入半路失败就会留下脏数据。这是面试常问的点也是实际项目里必须养成的习惯。5.3 评论模块评论模块的关键点有两个一是保存评论时要把登录用户的 ID 带上二是展示评论时要“查出评论者的昵称”。// 文件路径src/main/java/com/example/music/service/CommentService.java package com.example.music.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.example.music.entity.Comment; import com.example.music.mapper.CommentMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; Service public class CommentService { Resource private CommentMapper commentMapper; public void addComment(Long userId, Long songId, String content) { Comment comment new Comment(); comment.setUserId(userId); comment.setSongId(songId); comment.setContent(content); commentMapper.insert(comment); } public ListComment listBySongId(Long songId) { return commentMapper.selectList( new QueryWrapperComment() .eq(song_id, songId) .orderByDesc(create_time)); } }上面这个listBySongId只能查出评论记录拿不到评论者的昵称和头像。要想在页面上显示评论人的信息有两种做法简单一点直接在 SQL 里 joinuser表复杂一点查出评论后再根据 userId 批量查用户。对于毕设项目直接在 Mapper XML 里写一个 join 查询更直观。5.4 前端页面示例后端接口写好后前端用 Thymeleaf 渲染。以一个最简单的歌曲列表页为例!DOCTYPE html !-- 文件路径src/main/resources/templates/songs.html -- html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title歌曲列表/title /head body h2歌曲列表/h2 form th:action{/songs} methodget input typetext namekeyword th:value${keyword} placeholder输入歌曲名或歌手 button typesubmit搜索/button /form table border1 tr th歌曲名/th th歌手/th th专辑/th th操作/th /tr tr th:eachsong : ${songs} td th:text${song.name}歌曲名/td td th:text${song.singer}歌手/td td th:text${song.album}专辑/td td a th:href{/play/{id}(id${song.id})}播放/a /td /tr /table /body /html这里没有写播放器组件原因很简单演示时你可用audio标签播放自己准备的测试音频。但在实际部署或答辩时要注意不要在项目里直接使用他人的版权音乐文件建议用自己准备的无版权音乐或本地测试音频。5.5 完整项目目录结构做完上述模块项目的目录结构大致如下netease-music/ ├── pom.xml ├── sql/ │ └── netease_music.sql └── src/main/ ├── java/com/example/music/ │ ├── MusicApplication.java │ ├── controller/ │ │ ├── UserController.java │ │ ├── SongController.java │ │ ├── PlaylistController.java │ │ └── CommentController.java │ ├── service/ │ │ ├── UserService.java │ │ ├── SongService.java │ │ ├── PlaylistService.java │ │ └── CommentService.java │ ├── mapper/ │ │ ├── UserMapper.java │ │ ├── SongMapper.java │ │ ├── PlaylistMapper.java │ │ └── CommentMapper.java │ └── entity/ │ ├── User.java │ ├── Song.java │ ├── Playlist.java │ └── Comment.java └── resources/ ├── application.yml └── templates/ ├── login.html ├── register.html ├── songs.html └── playlist.html6. 运行结果与效果验证代码写完之后最重要的是把项目跑起来并且验证每个功能点。6.1 启动项目在 IDEA 里直接运行启动类MusicApplication或者在项目根目录执行mvn spring-boot:run看到类似下面的日志说明启动成功Tomcat started on port(s): 8080 (http) Started MusicApplication in 2.1 seconds然后打开浏览器访问http://localhost:8080/login能看到登录页面。6.2 接口验证用 Postman 或 Apifox 测试登录接口也可以直接用 curlcurl -X POST http://localhost:8080/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadminpassword123456如果密码正确会返回 302 重定向到/songs并设置JSESSIONIDCookie。如果密码错误会重定向回/login并携带错误提示。6.3 功能验证清单建议按下面这个清单逐项验证确保项目在演示时不出意外功能操作步骤预期结果注册填写新用户名和密码注册成功跳转登录页重复用户名提示已存在登录输入正确账号密码跳转歌曲列表页页面显示当前用户信息搜索歌曲输入歌曲名关键词列表显示匹配歌曲创建歌单登录后创建歌单数据库新增记录页面可看到歌单添加歌曲到歌单在歌曲列表点击“收藏到歌单”关联表新增记录发表评论登录后输入评论内容评论列表出现新评论并显示评论人昵称如果你在验证过程中发现某个功能表现不符合预期第一步不是改代码而是先看控制台有没有 SQL 日志确认是否真的执行了语句、执行的是不是期望的 SQL。MyBatis-Plus 开启log-impl后这条链路会非常透明。7. 常见问题与排查思路这一类项目最容易踩的坑很多和代码本身没关系而是环境或者配置问题。下面是一份高频问题排查表。问题现象可能原因排查方式解决方案启动报Port 8080 was already in use端口被占用查看占用端口的进程关闭占用进程或修改server.port启动报Failed to configure a DataSource数据库配置错误或未引入 JDBC 依赖检查application.yml的连接地址、账号、密码修正配置确认 MySQL 已启动报Unknown database netease_music数据库未创建在 MySQL 中执行建库 SQL先建库再启动项目user表相关 SQL 报语法错误user是 MySQL 关键字检查 SQL 是否加了反引号用user或改表名为sys_user启动报Invalid bound statement (not found)Mapper XML 路径或 namespace 不对检查mapper-locations和 XML 文件路径修正 XML 位置或减少 XML 使用改用 MyBatis-Plus 内置方法访问页面样式丢失静态资源路径不对查看浏览器 Network 面板看 404 资源确认静态文件放在src/main/resources/static注册时密码变成不可读的加密串BCrypt 加密正常现象不需要处理用encoder.matches()校验而不是明文对比Lombok 报错编译不通过IDEA 未启用注解处理查看 IDEA 的Settings Build Compiler Annotation Processors勾选 Enable annotation processing还有一个非常常见的坑java.lang.OutOfMemoryError: Insufficient memory这类问题往往是因为 IDE 或 Maven 分配堆内存过小或者本机内存确实不足。优先检查 IDEA 的 VM 参数和 Maven 运行参数不要一上来就改代码。8. 最佳实践与工程建议项目能跑通只是第一步。下面这些实践建议能让你的项目从“能跑”变成“能讲、能答辩、能进简历”。8.1 数据库设计要有意识不要只追求“表建得多”而是要让每个表都有存在的理由。比如歌单和歌曲是多对多关系所以需要中间表评论属于某个用户所以外键user_id不能为空用户名必须唯一所以要有唯一索引。答辩时能把这些设计原因讲清楚比堆功能更能体现你的水平。8.2 密码安全必须处理绝对不要把用户密码明文存在数据库里。使用 BCrypt 是当前最常见、也足够安全的方案。面试时如果被问到“密码怎么存的”可以顺着说BCrypt 是加盐哈希每次生成的密文都不同校验用matches方法更复杂的场景可以引入独立的认证中心或者 JWT 方案。8.3 登录状态的实现方式我上面示例用的是 Session这也是大多数毕设项目的选择。它会带来一个跨域问题如果前端和后端是分离部署的Session 基于 Cookie跨域请求往往带不上 Cookie这时候可以考虑 JWT 方案。如果你在简历里写的是前后端分离项目建议把 JWT 登录流程弄懂两种方案各能应对什么问题这是面试高频题。8.4 不要滥用中间件Redis、RabbitMQ、Elasticsearch 这类组件确实能提升项目档次但前提是你真的能说清楚它们在这个项目里解决了什么问题。比如你可以说“我用 Redis 缓存了热门歌单减少数据库压力”但你要能解释缓存穿透、缓存击穿、缓存雪崩的基本概念。如果只是为了让简历好看而硬加中间件面试官追问时反而容易露馅。8.5 日志和异常处理Controller 层要有统一的异常处理机制。最简单的做法是写一个RestControllerAdvice或ControllerAdvice类捕获业务异常和未知异常返回统一格式的错误信息。这样前端处理错误会比较省事代码也不会到处是 try-catch。8.6 演示数据准备答辩或面试前一定要准备一套真实的演示数据插入十几首歌曲、创建一个包含多首歌曲的歌单、给某首歌发几条评论。不要用空数据库去演示否则整个页面看起来很空功能也看不出效果。演示数据提前在数据库里准备好比现场敲 SQL 可靠得多。8.7 版权与合规提醒仿网易云音乐项目重点在“系统的实现”而不是搬运原平台的歌曲资源。不要在你的项目里直接打包或展示他人的版权音乐、专辑图片、歌词文本。建议使用自己录制或用无版权许可的音频文件作为演示数据并在项目 README 里注明音频文件的来源。这样既合规也能在答辩时体现你的职业意识。9. 总结与后续学习方向做这个项目的过程本质上就是用 Spring Boot 重新走了一遍“用户管理、内容管理、关联关系管理、搜索、评论”的完整开发链路。你会接触到 Session 和权限拦截、MyBatis-Plus 的条件查询、多表关联、事务控制、Thymeleaf 页面渲染这些知识点几乎是 Java 后端日常开发里最高频的那部分。项目跑通之后可以继续往两个方向扩展。第一个方向是做深把登录升级成 JWT 前后端分离引入 Redis 做验证码和热门数据缓存给文件上传加上对象存储或本地磁盘存储。第二个方向是做宽加后台管理模块让管理员可以管理歌曲、审核评论再做一些简单的数据统计图表。无论选哪个方向每一步改动都要想清楚“它解决了什么问题”。如果你是基于这套流程写自己的毕设建议先把源码整体跑通再根据自己想法改功能和界面。不要拿过来直接交项目里的表和代码细节如果自己都不熟悉答辩时很容易被问倒。这篇教程的核心内容就到这里。建议收藏备用等你准备搭 Spring Boot 项目的时候按照数据库设计到 Controller 的链路顺序一步步推进大概率能省下不少自己踩坑的时间。