每年这个时候,计算机专业的朋友们就开始为毕业设计发愁了。音乐网站系统算是Java Web方向最经典的题目之一,乍一看到处都是,但真正能跑通、能讲清楚原理、能过答辩的项目其实不多。我前阵子刚帮人完整梳理过一套基于SpringBoot + Vue的音乐网站项目的代码,从数据库设计到前后端联调一步步走了一遍,在这里把整套思路和实操经验完整整理出来,希望对正在做类似毕设或者想学全栈开发的朋友有点实际帮助。
这套系统的核心就是两条线:后端用SpringBoot做接口服务,前端用Vue做页面交互,中间通过RESTful API通信,数据落在MySQL里。用户能注册登录、浏览歌曲、搜索歌手、管理歌单、收藏音乐、在线播放;管理员可以维护歌曲、歌手、封面、歌词等基础数据。功能不算花哨,但麻雀虽小五脏俱全,正好覆盖了一个网站系统该有的核心链路:用户体系、资源管理、关联关系、文件上传、前端路由与状态管理。
1. 项目定位与模块拆解
1.1 这套系统到底在做什么
先说清楚它要解决什么问题。音乐网站本质上是一个内容管理 + 用户互动的系统。你可以把它拆成两个视角来看:普通用户希望快速找到想听的歌、建自己的歌单;管理员希望方便地录入歌曲信息、管理歌手、控制上传的文件。
所以项目的功能边界大概是这样的:
- 用户端:注册登录、浏览歌手和歌曲、搜索、创建歌单、收藏歌曲、听歌(实际上就是HTML5的audio播放)、写评论。
- 管理端:歌曲CRUD、歌手CRUD、歌单管理、用户管理(通常只做到禁用或重置密码)。
至于要不要做成后台管理界面,大部分毕设是分开做的,也就是前台展示用Vue,后台管理用另一个Vue页面或同一套系统的不同路由。我见过不少参考项目把管理端做成admin子应用,这样职责清晰,答辩的时候也好讲。
1.2 技术选型背后的逻辑
有人问,为什么这类题目几乎都选SpringBoot + Vue?其实答案很实际。
后端用SpringBoot而不是SSH、SSM,是因为它在极大程度上省掉了繁琐的XML配置。我记得以前用SSM搭一个项目,光配置文件就要写四五份,什么spring-mvc.xml、mybatis-config.xml、web.xml,光应付这些就能耗掉一个新手两周时间。SpringBoot用自动配置和起步依赖把配置量压到了极低,让我们能把精力放在业务代码上。对于毕设周期来说,这是绝对优势。
前端选Vue而不是React,是因为Vue的上手曲线平缓,模板语法直观,还有Element UI这一整套现成的组件库,页面做出来不至于太难看。Vue的生态在面对这种管理系统类型的项目时效率确实高。
数据库必然用MySQL,免费、资料多、各种工具链成熟,遇到问题一搜就有答案。ORM层面我推荐用MyBatis-Plus,不是因为别的,就是因为它自带CRUD方法,不用写一堆重复的Mapper XML,对赶进度的人非常友好。
1.3 角色权限与模块划分
这个系统我建议分成三种角色,虽然很多参考代码只做用户和管理员,但把“普通用户”和“管理员”严格分开会更好:
| 角色 | 能做什么 |
|---|---|
| 游客 | 看首页、看榜单、搜索、听歌(部分项目允许) |
| 注册用户 | 收藏、评论、建歌单、管理自己的歌单 |
| 管理员 | 维护歌曲歌手数据、管理用户、上传文件 |
权限控制这块,后端用拦截器 + JWT,前端用路由守卫,两个地方都要做,不能只靠前端隐藏按钮来“假装”有权限。
2. 数据库设计:先想清楚数据怎么落地
数据库是这类系统最不能糊弄的部分。我见过很多毕设代码表结构混乱,字段含义不清,关联查询也写不明白,答辩的时候被老师一问就露馅。这里把我的表设计思路完整列出来。
2.1 六张核心表结构
一个音乐网站最核心的表,我认为有六张:用户表、歌手表、歌曲表、歌单表、歌单歌曲关联表、收藏表、评论表(如果用心做,评论表也加上)。下面逐个拆解。
用户表:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `role` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1普通用户 2管理员', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里特别提醒几个字段的用意:role用于区分用户和管理员,status用于管理员禁用违规账号,password字段长度留255是因为BCrypt加密后的字符串超过50位,用长度50的字段会存不进去。
歌手表和歌曲表:
CREATE TABLE `singer` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '歌手名', `avatar` varchar(255) DEFAULT NULL COMMENT '歌手头像', `introduction` text COMMENT '歌手简介', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `song` ( `id` int(11) NOT NULL AUTO_INCREMENT, `singer_id` int(11) NOT NULL COMMENT '歌手ID', `name` varchar(100) NOT NULL COMMENT '歌曲名', `album` varchar(100) DEFAULT NULL COMMENT '专辑名', `url` varchar(255) NOT NULL COMMENT '音频文件地址', `cover` varchar(255) DEFAULT NULL COMMENT '封面图地址', `lyric` text COMMENT '歌词', `duration` int(11) DEFAULT '0' COMMENT '时长(秒)', `play_count` int(11) DEFAULT '0' COMMENT '播放次数', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;歌曲表里有一个play_count字段,很多人的代码里根本没用到,但排行榜功能就靠它。虽然真正的大型音乐平台会引入Redis来计数,但毕设用MySQL直接UPDATE就能应付,页面按播放次数倒序排列即可。
歌单、关联、收藏、评论表:
CREATE TABLE `song_list` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '创建者ID', `title` varchar(100) NOT NULL COMMENT '歌单标题', `cover` varchar(255) DEFAULT NULL, `description` varchar(500) DEFAULT NULL, `play_count` int(11) DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `song_list_song` ( `id` int(11) NOT NULL AUTO_INCREMENT, `song_list_id` int(11) NOT NULL, `song_id` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `favorite` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `song_id` int(11) 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; CREATE TABLE `comment` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `song_id` int(11) NOT NULL, `content` varchar(500) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个设计细节值得注意:song_list_song是典型的关联表,用来实现歌单和歌曲的多对多关系,前端传一个歌单ID和歌曲ID数组,后端就是对这个表做批量插入和批量删除;favorite表加了一个联合唯一索引uk_user_song,防止同一个用户重复收藏同一首歌,这个约束在数据库层面做最稳妥,比在Java代码里先查再判断靠谱得多。
2.2 表关系与业务联动
表之间的关系画在脑子里是这样的:user一端连着song_list、favorite、comment;singer是一方,song是多方;song又通过song_list_song关联到song_list。查询的时候,最复杂的SQL也就是两三张表联查。
我一个同事当初在歌单详情接口上卡了两天,其实就是没有想清楚关联表。后来逻辑理清了就很简单:查歌单基本信息一张表,通过song_list_song查出song_id列表,再用IN语句查歌曲信息,外加LEFT JOIN查出歌手的名字。SQL长这样:
SELECT s.id, s.name, s.album, s.url, s.cover, s.duration, sg.name AS singer_name FROM song s LEFT JOIN singer sg ON s.singer_id = sg.id WHERE s.id IN (SELECT song_id FROM song_list_song WHERE song_list_id = #{songListId})用子查询而不是三表JOIN,可读性高,也方便后面分页。
2.3 初始化数据怎么准备
初始化数据也有讲究。第一,至少准备10个以上歌手、50首以上歌曲,数据太少页面看着空旷;第二,歌曲文件和封面图注意版权和质量,毕设展示尽量用自己录制或版权开放的素材,不要在论文和PPT里放明显有版权争议的商用歌曲截图;第三,默认管理员账号要固定一个,比如admin / 123456,初始化SQL里直接用BCrypt加密后的密码字符串写入,方便后端启动后直接登录。
3. 后端:SpringBoot实现的核心模块
3.1 项目结构与分层策略
这套系统的后端项目结构,我建议严格按照标准Maven结构来组织,包名按业务分层:
com.example.music ├── controller # 接口层 ├── service # 业务层 │ └── impl ├── mapper # MyBatis-Plus持久层 ├── entity # 实体类 ├── dto # 请求/响应对象 ├── config # 配置类,如CORS拦截器 ├── common # 统一返回结果、异常处理 └── utils # JWT、文件上传工具为什么要分层?因为答辩的时候老师一定会问“你项目的架构是什么样的”。能清楚讲出Controller只接收参数、Service处理业务逻辑、Mapper负责数据库交互,这种基础的三层架构认知,在老师眼里就算合格了。很多C盘代码全堆在Controller里,200行一个方法,虽然能跑,但一被追问就露馅了。
3.2 JWT登录鉴权实现
登录鉴权是每个系统都绕不开的核心。现在基本不用Session了,主流做法是JWT。流程是这样的:用户提交用户名密码 -> 后端校验 -> 生成Token返回前端 -> 前端存到localStorage -> 每次请求带在Header里 -> 后端拦截器解析Token并放行。
JWT工具类里需要三样东西:密钥、过期时间、生成和解析的方法。代码如下:
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }配置文件里我用的密钥是一个足够长的随机字符串,过期时间设为24小时。实际开发中密钥要放在配置里,不要写死在代码中。拦截器里对所有需要登录的接口做校验,注意两个坑:一是放行登录注册接口和静态资源,二是对管理员接口额外校验role字段。
拦截器的写法是在SpringBoot里实现HandlerInterceptor接口,然后在WebMvcConfigurer中注册,并指定拦截路径。核心逻辑就是获取Header里的Authorization,去掉"Bearer "前缀,解析Token,成功就放行并把用户信息存入ThreadLocal或者request的attribute中,方便后续业务取当前用户ID。
3.3 歌曲管理、上传与播放统计
歌曲和歌手的增删改查用MyBatis-Plus的IService接口就顺手完成了。真正要用心的是文件上传。毕设里歌曲文件、封面图都是通过上传接口保存的。上传路径我踩过一个很典型的坑:用绝对路径保存,导致换一台电脑运行就找不到文件。
正确的姿势是在配置文件中定义一个上传根目录,应用启动时确保目录存在,然后把文件保存进去,同时把访问路径映射成静态资源。SpringBoot配置里加一行:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceHandler("file:" + uploadPath + "/"); } }这样上传后的文件对外访问路径就是http://localhost:8080/files/xxx.mp3,前端直接拼URL就能用。播放次数统计也不复杂,在播放接口里对play_count做自增更新,但这要控制频率。我见过实现比较简单的情况:每次点播放都调一个计数接口。对毕设来说完全够用,但对生产来说肯定不行,答辩时如果有人问,就说可以引入Redis做缓存计数、定时刷入数据库,这个回答可以显示你有进阶思考。
3.4 歌单、收藏与评论的接口设计
这类接口的关键是搞清楚返回结构。我整理一下常见的端点:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/song/list | 歌曲列表,支持关键词搜索、分页 |
| GET | /api/song/detail/{id} | 歌曲详情,含歌手名 |
| POST | /api/song | 新增歌曲(管理员) |
| PUT | /api/song | 修改歌曲(管理员) |
| DELETE | /api/song/{id} | 删除歌曲(管理员) |
| GET | /api/songList | 歌单列表 |
| POST | /api/songList | 创建歌单(登录) |
| POST | /api/songList/{id}/songs | 歌单添加歌曲 |
| DELETE | /api/songList/{id}/songs/{songId} | 歌单移除歌曲 |
| POST | /api/favorite | 收藏歌曲 |
| DELETE | /api/favorite/{songId} | 取消收藏 |
| POST | /api/comment | 发布评论 |
接口路径要有语义,统一返回结构。我定义一个Result<T>类,里面放code、message、data三个字段,成功code是200,失败是500,未登录是401,这样前端axios拦截器可以统一处理错误提示,而不是每个请求单独写一遍。
3.5 统一返回、异常处理与跨域配置
写接口时最忌讳每个方法返回类型五花八门,有的返回Map,有的直接返回实体。老老实实统一用Result包装。异常处理用@RestControllerAdvice做全局异常捕获,业务异常抛自定义的BusinessException,系统异常统一返回500并记录日志。
跨域配置是前后端分离项目必配的。用CorsFilter注册一个Bean,允许的前端端口是http://localhost:5173(Vite默认)或http://localhost:8081(Vue CLI默认),允许的Header里必须包含Authorization,否则前端带的Token会被浏览器拦截。
4. 前端:Vue页面与交互实现
4.1 项目搭建与目录划分
前端我建议用Vue CLI或Vite创建项目,UI组件库用Element UI。如果你是Vue 3项目,对应版本是Element Plus;如果是Vue 2项目,就用Element UI老版本。两者写法略有差异,但整体思路一样。我这里的示例以Vue 2 + Element UI为主,因为它资料最多,踩坑也好查。
前端目录结构参考:
src ├── api # 接口封装 ├── assets # 静态资源 ├── components # 通用组件,如播放条 ├── router # 路由配置 ├── store # Vuex状态管理 ├── views # 页面组件 ├── utils # 工具类,如axios封装 └── App.vue4.2 路由设计与登录守卫
路由配置要预设好页面。常见页面有:首页(推荐位 + 热门歌单)、歌手列表、歌手详情、歌曲搜索、歌单详情、个人信息页、后台管理页。这里我特别说明一个点:播放页到底要不要单独一个路由。我的做法是不单独开页面,而是做一个全局固定的底部播放条组件,全局只有一个audio元素。这样切换页面音乐不会中断,用户体验也舒服。这个组件的逻辑是:点击任何歌曲时,把歌曲信息提交到Vuex,底部播放条监听Vuex状态变化,然后设置audio的src并播放。
路由守卫负责登录校验:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role && to.meta.role !== store.state.user.role) { next('/') } else { next() } })这里要提一个容易忽略的细节:刷新页面时Vuex数据会丢失,所以用户信息要同时存一份到localStorage,页面初始化时再从localStorage恢复。否则用户登录后一刷新,页面就不知道当前用户是谁了,很多人的毕设都有这个bug。
4.3 axios封装与接口对接
axios封装的核心是请求拦截器加Token、响应拦截器处理错误码。登录接口返回的Token存在localStorage里,请求拦截器每次把Token塞进Header。响应拦截器如果拿到401,就清除本地登录信息并跳转登录页。
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.clear() router.push('/login') } return res }, error => { Message.error(error.message || '请求失败') return Promise.reject(error) } ) export default service注意baseURL这里用的是/api,开发环境下需要在Vue CLI的vue.config.js里配代理转发,把/api开头请求转发到8080端口。这是前端遇到跨域问题的最简解法,比后端配CORS还省事。
4.4 播放器组件的核心逻辑
播放条算是这个项目前端最有含金量的组件。核心代码并不复杂,但逻辑要理清楚:audio是HTML元素,Vue通过ref拿到它,然后监听Vuex中的当前歌曲变化,更新audio的src并调用play方法。播放/暂停按钮控制audio的play和pause,进度条更新时监听timeupdate事件,拖动进度条时修改currentTime,音量控制同理。
<template> <div class="player-bar"> <audio ref="audioRef" :src="currentSong.url"></audio> <div class="song-info"> <img :src="currentSong.cover" alt=""> <div> <p>{{ currentSong.name }}</p> <span>{{ currentSong.singerName }}</span> </div> </div> <div class="controls"> <i class="el-icon-video-pause" v-if="isPlaying" @click="pause"></i> <i class="el-icon-video-play" v-else @click="play"></i> <span>{{ formatTime(currentTime) }}</span> <el-slider :max="duration" v-model="currentTime" @change="seek"></el-slider> <span>{{ formatTime(duration) }}</span> </div> </div> </template>这里有几个常见的坑:audio的src一旦变化,要重新load才能生效;歌曲可能因为CORS问题不能播放,这就需要后端对音频接口也放行;播放失败时要catch住Promise,否则会在控制台刷一堆Uncaught的错误。还有一个体验细节:el-slider在拖动过程中频繁更新currentTime会卡顿,正确的做法是拖动时只更新本地临时值,松手时才写audio的currentTime。
另外歌词展示很多人忽略。其实很简单:歌词格式是LRC,解析成正则匹配时间戳的数组,audio的timeupdate事件里去匹配当前时间对应的歌词行。算是一个加分项,写出来效果很好。
4.5 页面渲染与交互细节
页面渲染用Element UI的列表组件很顺手。首页的推荐位用一个轮播图组件展示专辑封面;歌单卡片用row和col布局实现栅格排列;歌单详情页用表格展示歌曲列表,每行有一个播放按钮,点击调用Vuex的setCurrentSong提交歌曲信息。
搜索功能建议做防抖,用户在输入框打字时不要每敲一个字符都请求一次接口,用setTimeout和clearTimeout把请求频率降下来,或者简单点,用el-input的@keyup.enter触发搜索。虽然毕设不要求高并发性能,但写防抖在答辩中是个加分细节,可以表示你不是完全没考虑过性能。
5. 本地跑通全套代码的完整步骤
拿到这套源码,很多人第一步就卡住。SpringBoot版本不匹配、Node版本太高、Maven依赖下载不下来,这些坑我都见过。下面按顺序走一遍。
5.1 环境准备与版本检查
建议环境版本固定为:
- JDK 1.8(如果用的是SpringBoot 2.x)或 JDK 17(SpringBoot 3.x)
- Maven 3.6+
- Node.js 16.x(Vue 2项目用太新的Node会报OpenSSL错误)
- MySQL 5.7或8.0
这里最需要注意的就是Node版本。用Vue CLI创建的老项目在Node 17以上版本运行npm run serve时会报ERR_OSSL_EVP_UNSUPPORTED,原因就是Webpack 4依赖的OpenSSL算法在新版Node里被禁用了。解决办法是装Node 16,或者在package.json的scripts里加NODE_OPTIONS=--openssl-legacy-provider,但加参数的方式在Windows上还有兼容问题,不如直接切Node版本省心。
JDK版本同理。SpringBoot 2.x在JDK 17下虽然能跑,但一些旧依赖会有兼容问题,稳妥起见用JDK 8。
5.2 引入数据库初始化脚本
先在MySQL里建一个库,名字可以叫music_db,字符集utf8mb4,排序规则utf8mb4_general_ci。然后执行项目里的sql脚本。如果脚本里没有管理员账号的密码,需要自己手动用BCrypt工具生成一段密文插入。这个工具可以自己写个main方法调SpringSecurity的BCrypt,也可以直接用网上的在线工具,注意生成后核对前缀是$2a$开头即可。
数据导入后,可以在服务器上直接用SQL查一下关键表的行数,确认singer、song、user三张表都有数据,避免启动后端后前端页面一片空白。
5.3 后端启动与常见启动问题
后端启动的步骤,在IntelliJ IDEA中打开pom.xml所在的目录,等待Maven加载依赖(第一次下载依赖耗时较长),然后修改application.yml里的数据库连接配置:
spring: datasource: url: jdbc:mysql://localhost:3306/music_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 servlet: multipart: max-file-size: 100MB max-request-size: 100MB jwt: secret: 一个足够长的随机字符串 expire: 86400000启动时最常遇到的报错是时区问题,一般表现为serverTimezone相关SQL异常,记得在连接串上追加serverTimezone=Asia/Shanghai。如果提示数据库连接失败,先用Navicat测试本地连接,排除账号密码错误和MySQL服务没启动的问题。如果Maven依赖下载缓慢,可以换阿里云镜像,这个在settings.xml里配置,属于常规操作了。
启动成功后,后端默认端口8080,在浏览器访问http://localhost:8080/api/song/list,如果返回JSON数据,说明后端正常。
5.4 前端启动与代理配置
在前面代码部分提过,前端开发环境配的是代理,所以vue.config.js里这样写:
module.exports = { devServer: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }这样一个代理就解决了跨域问题,前端里所有的请求路径都以/api开头,实际上被转发到了后端。changeOrigin必须设置为true,否则后端可能会因为Host头对不上而拒绝请求。
启动时如果提示node_modules不存在,先执行npm install。这个过程可能遇到警告found 100 vulnerabilities,不用慌,这是依赖版本的安全提示,对本地开发运行没实际影响。安装完成后执行npm run serve,看到Compiled successfully提示后打开浏览器访问即可。
5.5 功能验证清单
系统跑起来后按这个清单快速过一遍:
- 注册一个新账号,确认能登录、能跳转。
- 用管理员账号登录,添加一首歌,确认歌曲列表能看到、能播放。
- 普通用户收藏歌曲,去个人中心确认收藏列表有数据。
- 创建一个歌单,往歌单里加几首歌,再提交时刷新页面确认数据持久化了。
- 搜索一个不存在的关键词,确认返回空列表且不报错。
每一步都走通,基本上功能就齐了。后端的SQL日志可以在application.yml里配置mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl,这样控制台会打印SQL,排查问题非常有用。
6. 常见问题与排查技巧实录
6.1 前端请求被拦截,页面报跨域错误
这个是我看到最多的问题。报错特点明显:浏览器控制台出现Access-Control-Allow-Origin相关字样,Network面板里请求显示为CORS error。解决策略分两个方向:一是前端配proxy代理,二是后端配CorsFilter。如果你已经配了proxy还报错,检查是不是直接把axios的baseURL写成了http://localhost:8080,绕过了代理。前端如果配了代理,请求路径必须写相对路径/api/xxx,而不是完整的后端地址。
6.2 请求401或403,Token没带上
Token的传递链路是这样的:登录接口返回Token -> 前端存到localStorage -> 下次请求时在拦截器里取出来放Header。常见问题有两个:一是localStorage的key写错了,存的时候叫token,取的时候写成了Token;二是后端拦截器放行的路径写错,把需要登录的接口也放行了,导致接口拿不到当前用户信息,返回空指针。调试技巧:在浏览器F12的Network里看请求头,确认有没有Authorization字段,没有就是前端问题,有但后端还不认,就是后端解析逻辑问题。
6.3 图片和音频上传成功但访问404
这个问题的根因几乎永远是路径映射没配好。文件保存到了本地磁盘的某个目录,但SpringBoot没有把这个目录映射成静态资源路径,前端访问/files/xxx.jpg自然404。解决办法参考前面WebConfig代码,把uploadPath和/files/**正确对应上。另外一个坑是文件保存时用了绝对路径拼接,Windows下路径分隔符是\,Linux下是/,在网页URL里只能用/。保存数据库的时候一定统一存储为相对路径,例如/files/20240101/xxx.jpg。
6.4 音频跨域导致无法播放
音频地址和后端接口不在同一个域时,audio元素的请求会被浏览器拦截,控制台报跨域错误。如果前端用了proxy,而后端又配了CORS,要注意两边的Header是否冲突。更稳妥的做法是后端对/files/**的静态资源也设置跨域允许,用addCorsMappings在资源映射里同时配置,或者直接用http.cors()配置全局。还有一个容易忽略的点:音频请求有时不经过axios,是audio自己发起的,所以axios里配的Header不会带上去,后端如果对音频接口要求鉴权,就会返回401导致播放失败。我的处理是:音频接口不做强制鉴权,或者用签名URL的方式,但对于毕设,直接放行最省事。
6.5 MyBatis-Plus分页不生效
用了分页插件后如果发现分页没生效,返回的还是全部数据,八成是没配置分页拦截器。MyBatis-Plus在SpringBoot里要手动注册一个PaginationInterceptor(3.4版本后叫MybatisPlusInterceptor),遗漏这步的话Page参数会被当成普通参数忽略。检查一下配置类里有没有写这个Bean,注意版本对应的类名。
写到这里,这套系统的核心链路算是完整走了一遍。我自己的体会是,音乐网站这种题目虽然常见,但想做得扎实并没那么容易。很多同学拿到一个源码就急着跑,跑通了就认为万事大吉,结果答辩时连“你的表结构是怎么设计的”都答不上来。既然这套代码里凝聚了前后端的完整知识链路,与其背代码,不如沉下心把每张表、每个接口、每条数据流都理清楚。这样到了答辩现场,无论老师从哪条线追问,你都能接得住。最后再分享一个小技巧:把项目跑通后,挑一个功能点做一次完整的“数据流讲解”练习,例如从用户点击播放按钮到audio出声,中间经历了哪些方法和接口,把这个讲顺了,整个项目你就真正吃透了。