☰
Spring Boot在线音乐系统搭建:MyBatis-Plus数据库设计与REST接口实战
2026/10/22 16:48:59 网站建设 项目流程

毕业设计和课程设计项目中,在线音乐系统是一个经常出现的题目。很多同学最开始想到的是用 Servlet + JSP 实现,但功能一多,登录、歌曲管理、收藏、歌单、评论都堆在一起,代码会越来越难维护。换成 Spring Boot 之后,内嵌容器、自动配置、Starter 依赖和 REST 接口让整个过程变得直接很多。这篇内容就以一个可运行的 Java 网易云音乐系统为例,说明如何用 IntelliJ IDEA 打开 Spring Boot 源码,从数据库设计到核心接口,把一个能演示的后端项目跑起来。完成之后,你既能用于毕业设计演示,也能理解 Spring Boot + MyBatis-Plus + MySQL 的实际用法。

整个实现按“概念、环境、数据库、接口、验证、排错、扩展”的顺序推进。代码示例以 Spring Boot 2.7 + JDK 8 为主,因为这是目前校园项目里兼容性最好的组合。如果你手里已经有一套源码,也可以对照这个思路确认自己的项目结构,而不是只看代码跑起来。

1. 先想清楚:在线音乐系统到底要做什么

1.1 用最小业务闭环拆解功能模块

一个完整的在线音乐系统往往包含推荐、排行榜、歌单、评论、收藏、搜索、个人中心等模块。作为毕业设计或者课程设计,不建议一开始就追求大而全,先做一个能讲清楚业务闭环的版本要更重要。

所谓最小业务闭环,可以这样定义:用户能够注册登录;登录后能够浏览歌曲;能够收藏歌曲;能够创建歌单;能够把歌曲加入歌单;能够在个人中心看到收藏列表和歌单列表。这套链路覆盖了用户、歌曲、收藏、歌单四类核心数据,也覆盖了注册、登录、分页查询、新增、删除、多对多关联等常见开发场景。

在这个闭环里,可以拆出以下几张核心表:

  • 用户表:保存用户名、加密后的密码、昵称、头像等。
  • 歌曲表:保存歌名、歌手、专辑、时长、封面、歌词、播放地址。
  • 歌单表:保存歌单名称、描述、创建人。
  • 歌单歌曲表:歌单和歌曲的多对多关系表。
  • 收藏表:记录用户收藏了哪些歌曲。

为什么不把收藏直接做成“在歌曲表里加一个收藏字段”?因为一个用户收藏的歌曲属于个人数据,和多用户共享的歌曲数据并不在同一个维度。收藏行为必须有一个独立的表来记录“谁收藏了哪首歌”,否则同一首歌被多个用户收藏时,字段会互相覆盖。

1.2 Spring Boot 在这个系统里的位置

Spring Boot 解决的是“如何快速把 Java Web 服务跑起来”的问题。它把 Spring 框架中重复的 Bean 配置、组件扫描、数据源配置、内嵌容器启动等步骤自动化了。对于音乐系统这类以 CRUD 和业务流程为主的项目,Spring Boot 的价值体现在几个方面:

  • 内嵌 Tomcat,不需要单独部署 War 包。
  • Starter 依赖统一管理版本,减少 Maven 坐标冲突。
  • 自动配置数据源、MyBatis、Jackson 等常用组件。
  • REST 接口开发简单,前端和后端可以分离。
  • 社区资料多,遇到问题容易搜索到解决方案。

如果使用传统的 SSM 框架,需要手工配置web.xml、Spring 容器、MyBatis 的 SqlSessionFactory,很多东西在课程设计阶段会消耗大量时间。Spring Boot 把这些约定成了默认行为,所以更适合作为快速出成果的基础框架。

下面用一张表对比设计思路的差异:

对比点Servlet/JSP 传统做法Spring Boot 做法
页面渲染JSP 在服务端渲染前后端分离,返回 JSON
启动方式部署到外部 Tomcat内嵌 Tomcat,直接运行 main
数据库访问JDBC 或 MyBatis 手写配置MyBatis-Plus 自动 CRUD
接口规范方法名和返回类型不统一Controller + Result 统一包装
适合场景小规模老课程设计可扩展、易维护的毕设项目

2. 环境准备:IDEA、JDK、Maven、MySQL

2.1 建议的版本组合

在开始写代码之前,先确认环境版本。很多项目启动失败并不是代码问题,而是 JDK、Spring Boot 和 MyBatis-Plus 版本不兼容。推荐按下面的组合选择:

场景JDKSpring BootMaven数据库
学习/毕设常用组合JDK 8Spring Boot 2.7.xMaven 3.6+MySQL 5.7/8.0
新版组合JDK 17Spring Boot 3.2.xMaven 3.8+MySQL 8.0
不想安装 MySQL 时JDK 8Spring Boot 2.7.xMaven 3.6+H2 内置数据库

如果没有特别原因,优先选择 Spring Boot 2.7.x + JDK 8。Spring Boot 3.x 包名和部分依赖有变化,MyBatis-Plus 需要引入mybatis-plus-spring-boot3-starter,对新手来说多一点兼容成本。

如果使用的是 IDEA Community 版,也可以在 start.spring.io 上生成项目压缩包,再通过 IDEA 的 Open 导入。不会影响后续开发,只是没有 Spring Initializr 的图形化快捷入口。社区版对学习 Spring Boot 完全够用。

2.2 创建项目并引入所需依赖

在 IDEA 中创建项目时,可以在 Spring Initializr 页面选择 Java 8、Spring Boot 2.7.x,然后添加以下依赖:

  • Spring Web
  • MySQL Driver
  • Lombok
  • Validation

还需要在pom.xml中手动添加 MyBatis-Plus 依赖,因为它不在 Spring Initializr 默认选项中。完整依赖如下:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

这里需要注意:Spring Boot 2.7.x 使用的 MySQL 驱动坐标是mysql:mysql-connector-java,不同版本可能坐标名称不同。上面的mysql-connector-j是 MySQL 官方新坐标,在 Spring Boot 2.7 中也能被管理版本。如果发现驱动类找不到,可以改成:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>

关键点在于,Spring Boot 父 POM 会管理这些依赖的版本,一般不需要手动指定版本。MyBatis-Plus 不在 Spring Boot 管理范围内,必须写版本号。

2.3 配置文件:数据源和 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/music_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

map-underscore-to-camel-case的作用是把数据库的create_time自动映射成 Java 实体的createTime,避免每一列都写映射关系。

logic-delete-field表示逻辑删除字段。用户、歌曲这类数据不适合直接物理删除,删除后还需要保留历史记录,所以使用逻辑删除字段。查询时 MyBatis-Plus 会自动追加WHERE deleted = 0。

配置完成后,启动一次项目确认能够连接数据库。如果还没有建库,会看到数据库连接失败的错误,这是正常的,下一步先建库建表。

3. 数据库设计:表和关系一次建好

3.1 表结构和关系设计

核心关系如下:

  • 用户表user和歌单表playlist是一对多:一个用户可以创建多个歌单。
  • 歌单表playlist和歌曲表song是多对多:一个歌单可以包含多首歌曲,一首歌曲可以出现在多个歌单中。
  • 用户表user和歌曲表song通过收藏表favorite建立多对多关系:一个用户可以收藏多首歌曲,一首歌曲可以被多个用户收藏。

设计表时,不推荐大量使用物理外键。外键约束会影响插入、删除性能,也会让删除逻辑变得复杂。实际项目中经常保留逻辑关系,由 Service 层保证一致性。但在表设计阶段,仍然可以通过索引来保证唯一性。

3.2 建表 SQL

进入 MySQL,执行下面的 SQL:

CREATE DATABASE IF NOT EXISTS music_db DEFAULT CHARACTER SET utf8mb4; USE music_db; CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删除,1已删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `song` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '歌曲名', `singer` varchar(50) DEFAULT NULL COMMENT '歌手', `album` varchar(100) DEFAULT NULL COMMENT '专辑', `duration` int DEFAULT NULL COMMENT '时长,单位秒', `cover_url` varchar(255) DEFAULT NULL COMMENT '封面地址', `play_url` varchar(255) DEFAULT NULL COMMENT '播放地址', `lyric` text COMMENT '歌词', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌曲表'; CREATE TABLE `playlist` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '创建人id', `name` varchar(100) NOT NULL COMMENT '歌单名称', `description` varchar(255) DEFAULT NULL COMMENT '描述', `cover_url` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌单表'; CREATE TABLE `playlist_song` ( `id` bigint NOT NULL AUTO_INCREMENT, `playlist_id` bigint NOT NULL, `song_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_playlist_song` (`playlist_id`, `song_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='歌单歌曲关联表'; CREATE TABLE `favorite` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `song_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_song` (`user_id`, `song_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表'; INSERT INTO `song` (`name`, `singer`, `album`, `duration`, `play_url`) VALUES ('晴天', '周杰伦', '叶惠美', 269, '/audio/qingtian.mp3'), ('起风了', '买辣椒也用券', '起风了', 326, '/audio/qifengle.mp3'), ('平凡之路', '朴树', '猎户星座', 302, '/audio/pingfanzhilu.mp3');

这段 SQL 用utf8mb4字符集,原因是要兼容中文和 emoji 字符。歌曲表的初始数据用于分页和收藏测试,没有真实音频文件时,播放地址可以先写成相对路径。

唯一索引非常重要。收藏表里的uk_user_song确保同一个用户不会重复收藏同一首歌曲,歌单歌曲表的uk_playlist_song确保同一首歌不会重复加入同一个歌单。如果没有唯一索引,代码里即使做了判断,并发请求下仍然可能插入重复数据。

3.3 实体类和 MyBatis-Plus 映射

创建User.java:

package com.example.music.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("`user`") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; @JsonIgnore private String password; private String nickname; private String avatar; private LocalDateTime createTime; @TableLogic @JsonIgnore private Integer deleted; }

这里有两个关键点:

  • @TableName("user")使用反引号,因为user是 MySQL 的保留字,不加反引号会报语法错误。
  • @JsonIgnore加在密码字段上,防止用户对象返回给前端时把密码也带出去。

创建Song.java:

package com.example.music.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("song") public class Song { @TableId(type = IdType.AUTO) private Long id; private String name; private String singer; private String album; private Integer duration; private String coverUrl; private String playUrl; private String lyric; private LocalDateTime createTime; @TableLogic private Integer deleted; }

创建Playlist.java和Favorite.java时同理。Favorite对应favorite表,包含userId和songId。MyBatis-Plus 的驼峰映射会把userId对应到user_id列,不需要额外配置。

创建实体类时,常见错误是漏了@TableLogic,导致删除时变成真正物理删除。如果使用了逻辑删除配置,实体类里必须写上对应字段。

4. 核心接口实现:从注册登录到收藏歌单

4.1 注册与登录:密码一定要加密

如果直接把明文密码存进数据库,演示时一旦出现安全相关提问,这会成为明显减分项。登录注册模块要做到两件事:注册时把密码用 BCrypt 加密后存入数据库;登录时用 BCrypt 校验密码,不比较明文。

创建一个配置类,提供PasswordEncoder:

package com.example.music.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class PasswordConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

这里没有引入 Spring Security,只使用了 Spring Security 中的加密类。依赖spring-security-crypto会通过项目中的其他依赖传递进来。如果项目中没有这个类,可以在pom.xml中添加:

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency>

用户注册和登录的 Controller:

package com.example.music.controller; import com.example.music.common.Result; import com.example.music.entity.User; import com.example.music.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.NotBlank; @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/register") public Result register(@RequestBody @Valid RegisterDTO dto) { return userService.register(dto.getUsername(), dto.getPassword()); } @PostMapping("/login") public Result login(@RequestBody @Valid LoginDTO dto) { return userService.login(dto.getUsername(), dto.getPassword()); } public static class RegisterDTO { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; // getter/setter 省略 } public static class LoginDTO { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "密码不能为空") private String password; // getter/setter 省略 } }

Service 层代码:

package com.example.music.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.music.common.Result; import com.example.music.entity.User; import com.example.music.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; public Result register(String username, String password) { Long count = userMapper.selectCount( new LambdaQueryWrapper<User>().eq(User::getUsername, username)); if (count > 0) { return Result.error("用户名已存在"); } User user = new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); userMapper.insert(user); return Result.success("注册成功"); } public Result login(String username, String password) { User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getUsername, username)); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { return Result.error("用户名或密码错误"); } user.setPassword(null); return Result.success(user); } }

推荐用Result统一返回结构,例如code、message、data三个字段。前后端联调时,不用一会儿返回true,一会儿返回1。

4.2 歌曲分页查询:分页插件要注册

歌曲列表推荐使用分页查询。先配置 MyBatis-Plus 分页插件:

package com.example.music.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }

setMaxLimit(100L)是防止一次查询过多数据,学习项目不用太大。

歌曲查询接口:

@RestController @RequestMapping("/api/song") public class SongController { @Autowired private SongService songService; @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") long pageNum, @RequestParam(defaultValue = "10") long pageSize, @RequestParam(required = false) String keyword) { return songService.page(pageNum, pageSize, keyword); } }

Service 实现:

public Result page(long pageNum, long pageSize, String keyword) { LambdaQueryWrapper<Song> wrapper = new LambdaQueryWrapper<>(); if (keyword != null && !keyword.isBlank()) { wrapper.like(Song::getName, keyword).or().like(Song::getSinger, keyword); } wrapper.orderByDesc(Song::getCreateTime); Page<Song> page = songMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); return Result.success(page); }

这里有个常见坑:如果忘记注册PaginationInnerInterceptor,selectPage不会报错,但返回的records是全部数据,total也有问题。注册分页插件后,再验证总数和每页数量。

4.3 收藏与取消收藏:唯一索引和事务

收藏接口需要三个方法:收藏、取消收藏、查询收藏列表。

收藏接口:

@PostMapping("/favorite/add") public Result add(@RequestParam Long userId, @RequestParam Long songId) { return favoriteService.add(userId, songId); } @PostMapping("/favorite/cancel") public Result cancel(@RequestParam Long userId, @RequestParam Long songId) { return favoriteService.cancel(userId, songId); } @GetMapping("/favorite/list") public Result list(@RequestParam Long userId) { return favoriteService.listByUser(userId); }

Service 层使用@Transactional:

@Transactional(rollbackFor = Exception.class) public Result add(Long userId, Long songId) { Long count = favoriteMapper.selectCount( new LambdaQueryWrapper<Favorite>() .eq(Favorite::getUserId, userId) .eq(Favorite::getSongId, songId)); if (count > 0) { return Result.error("已经收藏过了"); } Favorite favorite = new Favorite(); favorite.setUserId(userId); favorite.setSongId(songId); favoriteMapper.insert(favorite); return Result.success("收藏成功"); }

取消收藏时,可以直接按userId和songId删除:

@Transactional(rollbackFor = Exception.class) public Result cancel(Long userId, Long songId) { favoriteMapper.delete( new LambdaQueryWrapper<Favorite>() .eq(Favorite::getUserId, userId) .eq(Favorite::getSongId, songId)); return Result.success("取消收藏成功"); }

收藏表不需要逻辑删除,因为取消收藏后记录可以直接删除。这里使用事务是为了后续扩展时间更复杂时,多个表操作可以保持一致。

查询收藏列表时,如果只需要歌曲列表,可以分两步:先查收藏表中的songId,再根据songId批量查询歌曲。也可以直接写一个联表 SQL,把收藏表和歌曲表 join 起来。用 MyBatis-Plus 的话,推荐先查 ID 列表再查询歌曲,逻辑更清晰。

4.4 歌单创建与添加歌曲:多对多关联

歌单和歌曲是多对多关系,操作时涉及三张表:歌单表、歌曲表、关联表。

创建歌单:

@PostMapping("/playlist/create") public Result create(@RequestParam Long userId, @RequestParam String name, @RequestParam(required = false) String description) { Playlist playlist = new Playlist(); playlist.setUserId(userId); playlist.setName(name); playlist.setDescription(description); playlistMapper.insert(playlist); return Result.success(playlist); }

向歌单添加歌曲:

@PostMapping("/playlist/song/add") @Transactional(rollbackFor = Exception.class) public Result addSongToPlaylist(@RequestParam Long playlistId, @RequestParam Long songId) { // 1. 判断歌单是否存在 Playlist playlist = playlistMapper.selectById(playlistId); if (playlist == null) { return Result.error("歌单不存在"); } // 2. 判断歌曲是否存在 Song song = songMapper.selectById(songId); if (song == null) { return Result.error("歌曲不存在"); } // 3. 判断是否已经加入 Long count = playlistSongMapper.selectCount( new LambdaQueryWrapper<PlaylistSong>() .eq(PlaylistSong::getPlaylistId, playlistId) .eq(PlaylistSong::getSongId, songId)); if (count > 0) { return Result.error("该歌曲已在歌单中"); } // 4. 插入关联数据 PlaylistSong playlistSong = new PlaylistSong(); playlistSong.setPlaylistId(playlistId); playlistSong.setSongId(songId); playlistSongMapper.insert(playlistSong); return Result.success("添加成功"); }

在 Service 层先判断歌单和歌曲是否存在,再操作关联表,可以避免产生脏数据。虽然关联表已经有唯一索引,但业务层的提示信息会更友好,不会直接抛出数据库异常。

同时,这里要注意所有数据库写操作类方法都建议加上@Transactional(rollbackFor = Exception.class),避免第一步插入了歌单、第二步插入关联数据失败时,留下不完整的数据。

5. 在 IDEA 里运行和验证

5.1 启动项目和确认日志

找到项目主类,例如MusicApplication.java,点击 main 方法前的运行按钮。启动成功后,控制台会看到类似下面的日志:

Tomcat started on port(s): 8080 (http) Started MusicApplication in 3.5 seconds

如果项目启动失败,优先看启动日志的第一段异常。不要只看最下面的提示,很多错误原因在堆栈顶部。

启动成功不代表接口可用。还要检查数据库表中的初始数据是否已经插入。如果歌曲表有 3 条数据,就可以开始接口验证。

5.2 使用 curl 验证核心接口

在 IDEA 终端或系统终端中执行下面的请求。

注册用户:

curl -X POST http://localhost:8080/api/user/register \ -H "Content-Type: application/json" \ -d '{"username":"zhangsan","password":"123456"}'

预期返回:

{ "code": 200, "message": "注册成功", "data": null }

登录:

curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"zhangsan","password":"123456"}'

预期返回用户信息,其中密码字段不会出现,因为已经加了@JsonIgnore。

分页查询歌曲:

curl "http://localhost:8080/api/song/page?pageNum=1&pageSize=2"

预期返回一页两条歌曲记录,同时包含total总数。

收藏歌曲:

curl -X POST "http://localhost:8080/api/favorite/add?userId=1&songId=1"

再次执行同样的请求,会返回“已经收藏过了”。取消收藏后再执行,又可以收藏。

创建歌单并加入歌曲:

curl -X POST "http://localhost:8080/api/playlist/create?userId=1&name=我的最爱&description=测试歌单" curl -X POST "http://localhost:8080/api/playlist/song/add?playlistId=1&songId=1"

如果使用 Postman,注意参数类型和请求方式。使用@RequestParam接收的参数,在 Postman 中放到 Params 里;使用@RequestBody接收的参数,放到 Body 的 raw JSON 里。

5.3 常见运行错误排查

用表格整理几个容易遇到的问题:

问题现象常见原因检查方式处理建议
Tomcat 启动失败,提示端口被占用8080 端口已被其他程序使用执行 `netstat -anofindstr 8080`
数据库连接失败,Access denied用户名或密码错误检查application.yml确认 MySQL 账号密码,并确认库已创建
找不到驱动类ClassNotFoundExceptionmysql 驱动坐标不对检查pom.xmlSpring Boot 2.7 使用mysql-connector-java,3.x 使用mysql-connector-j
分页返回全部数据没有注册分页插件检查MybatisPlusConfig添加PaginationInnerInterceptor
插入的中文变成问号字符集设置不对检查连接 URL 和表字符集URL 加characterEncoding=utf8,表使用utf8mb4
Lombok 报错,getter/setter 找不到IDEA 没有启用注解处理检查 Settings > Build > Compiler > Annotation Processors勾选 Enable annotation processing

搜索这个问题时,很多同学会到处改代码,但没有检查 IDEA 的注解处理器配置。Lombok 如果配置不对,代码在编译阶段就找不到getter和setter,但项目本身不需要改任何代码。

6. 从“跑通”到“能答辩”:常见坑与优化方向

6.1 最值得避开的几个坑

第一个坑是返回密码字段。就算数据库里存的是 BCrypt 加密结果,也不应该返回给前端。解决方案是实体类字段上加@JsonIgnore,或者在返回 DTO 时不复制密码字段。

第二个坑是 MyBatis-Plus 分页不生效。很多同学发现selectPage返回的records是全部记录,以为代码写错了,实际上是没有注册MybatisPlusInterceptor。分页插件是拦截器机制,不注入拦截器,Page对象虽然创建了,但分页 SQL 不会生成。

第三个坑是关联表重复插入。用户重复点击收藏按钮、重复添加歌曲到歌单时,没有唯一索引会插入多条相同记录。解决方案是表上建立联合唯一索引,同时 Service 层做存在性判断,两个手段结合使用。

第四个坑是删除操作误用逻辑删除。@TableLogic会让deleteById变成update deleted = 1,但对关联表来说可能不是期望行为。收藏表和歌单歌曲表更适合物理删除,关联记录删掉不影响主数据。主数据表如用户、歌曲、歌单可以使用逻辑删除。

6.2 学习环境到生产环境的差异

课程设计和真实生产环境差别很大,可以在文档或答辩中主动说出来,会成为加分项。

学习环境通常会把数据库配置写在application.yml,生产环境应该使用环境变量或配置中心;学习环境的 token 放在内存 Map,生产环境应该用 Redis 或 JWT;学习环境的日志打印到控制台,生产环境要把日志接入文件或集中日志系统。

可以用下面这张表总结:

关注点学习项目做法生产项目建议
配置硬编码在 yml配置中心或环境变量
密码BCrypt 加密BCrypt 加密,并增加登录失败限制
身份认证登录直接返回用户信息JWT + Redis 会话管理
日志控制台输出按天滚动文件,分级别归档
异常处理Controller 内部分散处理全局异常处理器 + 统一错误码
文件存储本地路径对象存储 OSS 或自建文件服务
数据库单表 CRUD主从分离、读写分离、慢查询监控

实际项目里,上线前还要考虑数据备份、回滚方案和资源监控。比如歌曲播放地址如果失效,怎么补偿;歌单被删除后,关联的歌单歌曲怎么清理;这些不写进需求里,但答辩时被问到是常见的加分点。

6.3 功能扩展方向

如果基础功能已经跑通,可以按下面顺序扩展:

  • 评论功能:增加comment表,用户可以对歌曲发表评论。
  • 搜索功能:在歌曲分页里增加关键词模糊搜索,已经实现了基础版本。
  • 排行榜:统计歌曲收藏数量,按收藏数倒序返回榜单。
  • 歌单详情:返回歌单基本信息,连同歌单内歌曲列表一起返回。
  • 播放功能:前端播放音频,需要提供真实音频资源和播放地址。
  • 前端页面:用 Vue 3 + Element Plus 对接后端接口,形成前后端分离完整项目。

如果时间紧张,评论功能是性价比比较高的扩展。它涉及一张新表和简单的 CRUD,又能体现你对业务关系的理解。

7. 可复用检查清单

7.1 开发过程检查清单

  • 确认 JDK、Spring Boot、MyBatis-Plus 版本组合。
  • 确认数据库连接 URL 包含characterEncoding=utf8和serverTimezone。
  • 确认表名是 MySQL 保留字时使用反引号。
  • 确认用户密码使用 BCrypt 加密存储。
  • 确认用户对象不返回密码字段。
  • 确认 MyBatis-Plus 分页插件已经注册。
  • 确认逻辑删除字段已经加上@TableLogic。
  • 确认收藏表和歌单歌曲表有唯一索引。
  • 确认写操作涉及多张表时使用@Transactional。
  • 确认前端请求参数与 Controller 接收方式一致。

7.2 演示和答辩前检查清单

  • 提前准备好测试账号,并确认账号可以登录。
  • 歌曲表中插入至少 3 到 5 条背景音乐相关数据,让演示看起来真实。
  • 演示顺序建议:注册 -> 登录 -> 歌曲分页 -> 收藏 -> 取消收藏 -> 创建歌单 -> 添加歌曲 -> 查询歌单。
  • 准备一张简单的表结构图,用于解释用户、歌曲、收藏、歌单之间的关系。
  • 准备 2 到 3 个你在开发中踩过的坑,说明现象、原因和解决方式。
  • 如果使用了网络图片资源,提前确认访问地址可用。

一个能跑的 Spring Boot 音乐系统后端,本质上并不复杂。真正要花时间的是把表关系设计清楚、把接口返回结构统一、把用户密码和事务处理做好。把这几个点做到位,课程设计演示、毕业设计答辩或者简历上的项目描述都会更有底气。接下来可以继续往前端页面、评论模块和部署方向扩展,整个系统的能力会越来越完整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询