做完整套SpringBoot+Vue专辑鉴赏网站平台,我最直接的感受是:这不仅是能交差的毕设,还是把前后端分离、RESTful接口设计、JWT认证、文件上传这些Java Web核心技能串起来的一条完整链路。项目交付物是源码、SQL脚本和接口文档三件套,后端SpringBoot + MyBatis Plus,前端Vue + Element UI,数据库MySQL。专辑鉴赏这个选题很讨巧——它不像电商系统那么庞大,也不像纯管理系统那么平淡,天然覆盖内容展示、评分互动、收藏社交、后台管理这几条业务线,同时界面有发挥空间,答辩时能讲的东西非常多,尤其适合Java Web方向的毕业设计。
我见过太多同学拿到这类项目后直接蒙头跑代码,忽略了最该搞懂的设计思路,结果答辩一问三不知。这篇就按我当时的实施顺序,把数据表设计、后端模块、前端页面、接口约定、部署排错从头到尾拆一遍,每一步都告诉你为什么这么做。无论你是准备直接用于毕设,还是打算仿照它练手前后端分离,都能少走不少弯路。
1. 项目概述与技术选型
1.1 业务定位:专辑鉴赏网站到底做什么
专辑鉴赏平台,核心是把"专辑"这种音乐内容实体做完整的展示与互动闭环。用户访问网站后,可以在首页看到推荐专辑、最新上架、评分榜等模块;进入专辑详情页后,能看到专辑封面、发行信息、艺人简介、用户评论,并且可以进行评分、收藏、发表评论;同时每个人有自己的个人中心,可以管理自己的收藏列表和评论记录。管理员端负责专辑的发布、编辑、下架,以及用户和评论的审核管理。
功能听起来不复杂,但稍微一拆,就会发现它天然包含了电商系统的雏形逻辑:专辑就是商品,评分就是评价功能,收藏就是购物车/关注列表的简化版,后台管理就是运营后台。这也是为什么这类项目被很多导师认可——它麻雀虽小,五脏俱全,业务深度刚好够一个学期的设计量。
我当时定的功能清单如下:
- 用户端:注册登录、专辑分类浏览、关键词搜索、专辑详情、评分、评论、收藏、个人中心
- 管理员端:专辑CRUD、封面图片上传、分类管理、用户管理、评论删除
- 公共能力:统一登录校验、异常拦截、分页与排序、跨域支持
这套功能看起来有不少,但落到代码层面,没有一处是高不可攀的难点,都是Java Web开发里的常规操作。
1.2 为什么选SpringBoot + Vue这套组合
技术栈选择是答辩时必被问的问题,所以出发点必须想清楚。
后端选SpringBoot,理由很直接:它把Spring繁琐的XML配置全部干掉,内嵌Tomcat,java -jar就能跑,极大降低了部署成本。MyBatis Plus在原生MyBatis之上封装了通用CRUD方法,单表操作几乎不用写SQL,这对于赶进度、写量大的毕设项目来说非常友好。我项目中大概80%的数据库操作都靠BaseMapper自带方法解决,只有多表关联统计才手写SQL。
前端选Vue,是因为它的组件化开发方式很适合这种中型应用。专辑卡片、评论列表、分页组件都可以抽成独立组件,维护起来思路很清楚。Element UI提供了现成的表格、表单、消息提示组件,能让页面快速达到"看起来像个正经产品"的水准。
很多人纠结要不要上前后端分离。我的建议是:如果对自己能力有信心,务必用分离架构。前后端分离意味着你同时证明了掌握Vue工程化、RESTful接口约定、跨域处理、Token认证这一串如今企业级的开发习惯,这在答辩时的加分效果远大于一个"前后端不分离的SpringBoot大而全项目"。
1.3 这套项目适合谁、需要什么基础
如果你是以下三类人,这套项目的参考价值很高:
- Java Web方向的大四学生:需要一份能讲解清楚、能改出亮点的毕设项目
- 刚学完SpringBoot基础,想找完整实战的初学者:这个项目的复杂度和代码量适中,不至于让人劝退
- 想快速搭建一个内容展示+UGC互动网站的人:换壳换成电影、书籍、游戏,骨架完全通用
前提基础要求不高:Java语法熟悉、会简单SQL、了解HTML/CSS/JavaScript基本使用、Maven能构建项目、Node环境能跑npm命令,就足够了。如果你能独立写一个HelloWorld级别的SpringBoot接口,再写过几行Vue代码,那这套项目理解起来毫无压力。
2. 数据库设计与SQL脚本编写
2.1 核心表结构的设计逻辑
SQL脚本是整个项目的基石,如果表设计不合理,后面后端代码写起来会极其痛苦。我当时定了5张核心业务表,外加一张专辑类别表。
用户表user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 唯一,登录名 |
| password | varchar(100) | 加密后密码 |
| avatar | varchar(255) | 头像地址 |
| role | tinyint | 0普通用户,1管理员 |
| create_time | datetime | 注册时间 |
专辑表album:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| title | varchar(100) | 专辑名称 |
| artist_id | bigint | 所属艺人 |
| cover | varchar(255) | 封面图地址 |
| type_id | bigint | 流派分类 |
| publish_date | date | 发行日期 |
| description | text | 专辑简介 |
| total_score | bigint | 评分总分 |
| score_count | int | 评分人数 |
| status | tinyint | 上架/下架状态 |
评论表comment、收藏表collection、艺人表artist的字段就按常规设计来,其中收藏表我加了user_id + album_id的唯一索引,避免同一用户对同一张专辑重复收藏。评分这里我采取的做法是不单独建评分表,直接复用评论表:用户评分为内容 + 星数,然后通过后端事务同步更新专辑表里的total_score和score_count。因为SELECT AVG(score)在数据量几万条内性能没有任何问题,这样省去一张表,逻辑也简单。
2.2 表关系与外键的处理经验
表关系就四条,很直接:
- 一个艺人可以有多张专辑(一对多)
- 一张专辑属于一种流派类型(多对一)
- 一个用户可以评论多张专辑,一张专辑有多个评论(多对多通过评论表连接)
- 一个用户可收藏多张专辑,通过收藏表维护(多对多)
物理外键我全都没加。这个是新手最容易踩的坑,看到表关系第一反应就是FOREIGN KEY。但实际上项目里所有关联关系都在Service业务层控制,物理外键反而会带来插入顺序限制、删除时外键约束报错的麻烦。表之间用普通索引即可,逻辑关系交给Java代码维护,这一条在企业开发里也是通行的做法。
2.3 初始化数据和SQL脚本的坑
SQL脚本交付时要做到"导入即可跑",所以要注意三个非常实际的问题。
第一是字符集。建库语句必须显式指定utf8mb4,而不是只写utf8。因为utf8在MySQL里最多存3字节,像"🎵"这类4字节emoji字符根本存不进去,会造成导入时警告甚至报错。我项目的charset统一写成DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci。
第二是数据量不能太少。导师打开你的网站如果看到空空如也,印象分会大打折扣。我在SQL脚本里预置了8个流派、10个艺人、50张专辑、20条评论,每张专辑的封面都指向本地的一个示例图片路径。这样首页、详情页、榜单页一打开就有内容,看着非常完整。
第三是脚本要有顺序保障。如果脚本是分批导入的,需要保证先插入艺人、流派数据,再插入专辑数据,最后插入评论收藏。最稳妥的做法是导出一份完整的album_system.sql,里面包含建库、建表、插入数据的完整顺序,用户用Navicat或者命令行一次性导入即可。
3. 后端核心模块的实现细节
3.1 工程分层与包结构设计
后端我采用的是标准四层结构,package命名用项目名简写,直观且好讲:
com.example.album ├── controller // 接收请求、参数校验、返回结果 ├── service // 业务逻辑层,事务边界在这里 ├── mapper // MyBatis Plus的Mapper接口 ├── entity // 数据库实体类 ├── common // 统一返回结果、异常处理、常量 └── config // 跨域、静态资源配置、拦截器注册控制层只做三件事:接收参数、调用Service、把结果包成Result<T>返回。业务代码绝不写在Controller里。这个边界意识从第一天就要养成——答辩时老师抽查代码,最烦看到Controller又臭又长、业务全堆在接口方法里的写法。
Result<T>统一响应体长这样:
public class Result<T> { private Integer code; // 200成功,400业务失败,401未登录,500异常 private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.msg = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String msg) { Result<T> result = new Result<>(); result.code = code; result.msg = msg; return result; } }前端就根据这个code做拦截和提示,前后端约定清爽,不会出现"接口返回格式各家不同"的问题。
3.2 专辑浏览、评分与收藏的接口设计
接口设计坚持以资源为中心,URL用名词复数,配合HTTP方法表达动作。这一点答辩时讲到很有底气,因为这是RESTful风格的核心约定。举几个实际接口:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/album/page?pageNum=1&pageSize=10 | 分页查询专辑 |
| GET | /api/album/{id} | 专辑详情 |
| POST | /api/album | 新增专辑(管理员) |
| PUT | /api/album/{id} | 修改专辑(管理员) |
| DELETE | /api/album/{id} | 删除专辑(管理员) |
| POST | /api/comment | 发表评论(含评分) |
| POST | /api/collection/{albumId} | 收藏专辑 |
| DELETE | /api/collection/{albumId} | 取消收藏 |
分页查询用MyBatis Plus的Page对象非常顺手,一个selectPage就能搞定,配合PageResult返回total和records,前端拿到后渲染分页组件直接对号入座。
评分功能最关键的是事务。我在评论表插入记录后,必须同步更新专辑表的total_score和score_count。这两步要么都成功,要么都失败,所以Service方法上必须加@Transactional注解,否则就会出现用户评论写进去了、专辑平均分却没变化的尴尬情况。
@Transactional(rollbackFor = Exception.class) public void addComment(CommentDTO dto, Long userId) { Comment comment = new Comment(); comment.setAlbumId(dto.getAlbumId()); comment.setUserId(userId); comment.setContent(dto.getContent()); comment.setScore(dto.getScore()); commentMapper.insert(comment); Album album = albumMapper.selectById(dto.getAlbumId()); album.setTotalScore(album.getTotalScore() + dto.getScore()); album.setScoreCount(album.getScoreCount() + 1); albumMapper.updateById(album); }收藏的接口设计成POST和DELETE两个细分动作,而不是用PUT改状态,这样语义更清晰,前端调用也更符合直觉。
3.3 JWT认证与权限控制
登录认证我用了JWT方案,不依赖Session,真正体现前后端分离的思路。用户登录成功后,后端根据用户ID和角色生成一个有效期2小时的token字符串返回给前端,前端把它存在localStorage里,之后请求时放进请求头Authorization。
项目里我引入的是jjwt 0.9.1依赖,工具类里做两件事:生成token、解析token。
public class JwtUtil { private static final String SECRET = "album_secret_key_please_change"; private static final long EXPIRE = 1000 * 60 * 60 * 2L; public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { try { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } catch (Exception e) { return null; // token过期或非法 } } }然后用Spring的HandlerInterceptor写一个JwtInterceptor,在preHandle方法里取出请求头token并解析,解析失败就返回401,成功则把用户信息放进ThreadLocal供业务层使用。注册拦截器时要注意:对/api/auth/login、/api/album/page、/api/album/{id}这些公开接口设置excludePathPatterns,不拦截;其余接口统一校验。
管理员权限则是在拦截器解析出role后做判断,如果要访问的接口路径以/api/admin/开头且role != 1,拒绝访问。这个方案简单有效,而且可以完整回答"你怎么控制权限的"这个问题,不至于被问到傻眼。
3.4 封面图片上传的处理方式
专辑封面必须支持上传,不然管理员新增专辑时无法配图。我在后端写了一个/api/admin/upload接口,接收MultipartFile,把它保存到服务器磁盘的指定目录,并返回可访问的URL地址。
具体实现上,我一共做了三件事。第一,在application.yml里配置上传路径:
file: upload-dir: /Users/me/album/uploads/ static-prefix: /upload/**第二,实现WebMvcConfigurer,把磁盘路径映射成URL:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceMapping("file:" + uploadDir); } }第三,对上传接口做大小和类型限制,比如只允许jpg/png/webp,最大5MB,否则直接返回错误。前端上传组件el-upload拿到返回的图片路径后,把它和专辑表单一起提交,这样封面就持久化了。注意文件名一定不能保留用户上传时的原始文件名,要用UUID + 扩展名重命名,否则会出现中文文件名乱码和路径注入问题,这是我踩过的实际坑。
4. 前端Vue工程实现要点
4.1 前端项目结构与路由划分
前端我用Vue CLI构建,配合Vue Router的history模式。项目结构如下:
src ├── api // 所有接口调用方法,按模块拆分 ├── assets // 静态资源、全局样式 ├── components // 可复用组件:专辑卡片、分页、评论列表 ├── router // 路由配置 ├── store // Vuex:保存用户登录状态 ├── views // 页面组件 │ ├── Home.vue │ ├── AlbumList.vue │ ├── AlbumDetail.vue │ ├── Login.vue │ ├── Register.vue │ ├── Profile.vue │ └── admin // 后台管理页面 └── utils // axios实例、工具函数路由配置里我特别设置了meta信息,标记哪些页面需要登录、哪些页面需要管理员权限。
{ path: '/admin/album', name: 'AdminAlbum', component: () => import('@/views/admin/AlbumManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } }然后在全局路由守卫里做拦截:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { Message.warning('请先登录'); next('/login'); return; } if (to.meta.requiresAdmin && JSON.parse(localStorage.getItem('userInfo')).role !== 1) { Message.error('无管理员权限'); next('/'); return; } next(); });这样未登录用户访问个人信息页会被踢回登录页,普通用户访问管理后台直接被拦截,整个权限链路是通的。
4.2 Axios封装与接口调用约定
Axios必须封装,绝不可以在每个组件里直接axios.get。我在utils/request.js里创建了一个axios实例,配置基础URL和请求拦截器、响应拦截器。
const request = axios.create({ baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:8080/api' }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = token; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code === 200) { return res; // 直接返回整个结果体 } Message.error(res.msg); return Promise.reject(res); }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error('网络异常,请稍后重试'); return Promise.reject(error); } );封装后每个接口方法就非常干净,例如专辑分页查询:
export function getAlbumPage(params) { return request.get('/album/page', { params }); }组件里调用时,res.data就是后端返回的数据体,res.data.records就是当前页的专辑列表。这种统一封装方式,被反复调用的价值在项目后期才体会明显——换个弹窗提示、加个统一登录失效处理,只改一个文件就行。
4.3 核心页面的交互实现:详情、评分、收藏、评论
专辑详情页是互动重点,我把它拆成了三个模块:顶部是专辑封面与基本信息,中间是评分与收藏操作区,下方是评论列表。
评分组件我用Element UI的el-rate,星星数对应1-5分。用户点击星星后,把分数暂存,等填写完评论内容一起提交。提交成功后就刷新详情页的评分信息和评论列表。这里比较关键的是:如果当前用户已经评论过这张专辑,后端在评论插入前要查一次评论表,发现存在就返回"您已评论过该专辑"的提示,避免一个人反复刷分。
收藏按钮的状态要和用户是否已收藏联动。后端提供了一个GET /api/collection/check?albumId=xxx接口,返回true或false。页面加载时查一次,收藏时显示为"已收藏"状态,点击取消收藏后恢复。交互体验方面,收藏成功用Message.success弹轻提示,不要用弹窗打断用户。
评论列表我用的是无限滚动改为传统的分页模式,每页10条。每次新增评论后手动重新请求第一页数据,这样能立即看到自己的评论出现在最上面。
4.4 管理后台页面的快速搭建
管理后台是展现代码规范的好地方。用Element UI的el-card+el-table+el-form+el-dialog组合,一小时左右就能搭完一个完整的专辑管理页面。
专辑管理页的逻辑是:顶部是"新增专辑"按钮和搜索框,中间是表格展示专辑列表,每行有"编辑""删除"操作按钮,点击编辑弹出对话框,表单内容包含专辑名、艺人、分类、发行日期、简介、封面图。封面上传用el-upload配合action="http://localhost:8080/api/admin/upload",并且加请求头token,上传成功后在回调里把返回的图片地址放到隐藏字段中。
需要注意的一点:表格里的封面图片列用el-table-column配合作用域插槽渲染成缩略图,而不是显示一串字符串路径。
<el-table-column label="封面" width="100"> <template slot-scope="scope"> <img :src="scope.row.cover" class="cover-thumb" /> </template> </el-table-column>后台管理页面的代码风格要和用户端统一,都用封装好的request对象,不要让管理端再单独弄一套请求逻辑。
5. 接口文档规范与前后端联调心得
5.1 接口文档管什么、怎么写
接口文档是这次交付物里的标配,也是我平时写项目的习惯。一套可用的接口文档必须包含以下五块:
- 接口路径和请求方法
- 请求参数(名称、类型、是否必填、说明)
- 请求示例(JSON格式)
- 响应示例(JSON格式)
- 错误码说明
举一个我整理的POST /api/comment文档片段:
请求参数: albumId: Long,必填,专辑ID content: String,必填,评论内容,最长500字 score: Integer,必填,评分,1-5 请求示例: { "albumId": 3, "content": "这张专辑的编曲层次太丰富了,推荐!", "score": 5 } 响应示例: { "code": 200, "msg": "success", "data": null } 错误码: 401:未登录 400:已评论过该专辑我同步在项目中集成了Swagger2,这样Controller上写的注解能自动生成在线文档。但在交付给别人的时候,一份独立的Markdown格式接口文档仍然是必须的,因为对方不一定启动项目去看Swagger页面,也不一定愿意手动调接口。
5.2 前后端联调时最容易踩的四个坑
联调阶段踩得我头昏的问题,总结起来就四类。
第一是跨域。前端开发服务器跑在8080,后端跑在8080会出现端口冲突,一般我让后端跑在9090,那前端请求http://localhost:9090/api必然触发CORS。解决方案是在后端写一个CorsConfig配置类,设置允许的来源为http://localhost:8080,允许所有请求头和请求方法。注意:前端通过Vue CLI代理也能解决,部署时反而是后端放开跨域更省事。
第二是默认值不一致。后端返回的分页数据是records,前端刚开始以为是list,明明请求成功了列表就是空。根因就是对接前没看文档。所以我建议联调前,双方先拿文档对齐两三个接口的字段命名。
第三是日期格式。Java后端返回的LocalDate默认是2024-12-10T11:22:33这种带T的格式,前端展示很丑。我统一在配置里加了Jackson全局设置yyyy-MM-dd HH:mm:ss格式,这样前端直接展示字符串就好。
第四是上传图片后刷新404。因为前端图片地址是http://localhost:9090/upload/xxx.jpg,如果静态资源映射配置不当就会404。排查时要分清是后端没映射对,还是前端URL拼错。我一般直接用浏览器地址栏访问图片路径来快速定位。
6. 环境准备、打包部署与常见问题排查
6.1 本地运行完整步骤
收到源码后,从零跑起来的顺序很重要,我整理了一个"傻瓜级"操作流程,按这个步骤走能最快看到效果。
第一步,准备环境:JDK 1.8、Maven 3.6+、Node 14+、MySQL 5.7或8.0、IDEA/VSCode,这些装好后检查java -version、npm -v能输出。
第二步,导入数据库:打开Navicat或命令行,执行album_system.sql,执行完毕后有5张业务表加初始数据,可以SHOW TABLES;验证。
第三步,启动后端:用IDEA打开后端项目,等待Maven依赖下载完毕,修改application.yml里的数据库账号密码,然后直接在IDEA里运行主类AlbumApplication。控制台出现"Started AlbumApplication"字样说明启动成功,浏览器访问http://localhost:9090/api/album/page能看到JSON返回。
第四步,启动前端:在frontend目录下执行npm install安装依赖,下载完毕后执行npm run serve,控制台提示App running at http://localhost:8080,打开浏览器访问就能看到页面。
第五步,登录体验:使用预置的管理员账号(admin/123456)登录进入后台,新增一张专辑并上传封面,刷新用户端看是否同步出现。
前端安装依赖时容易因为网络原因卡住,建议先配置npm淘宝镜像源,速度会快非常多。
6.2 常见问题速查表
我把项目运行过程中出现过的问题做成一个速查表,每一条都是实际发生过的,不是纸上谈兵。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 前端页面报404 | 路由history模式下刷新丢失 | 部署时用Nginx配置try_files;本地用Vue CLI代理无此问题 |
| 登录后接口返回401 | token未放入请求头 | 检查axios请求拦截器是否从localStorage取值并设置Authorization |
| 上传图片失败提示超出大小 | Spring默认单文件最大1MB | 在application.yml配置max-file-size=5MB,max-request-size=5MB |
| 中文乱码 | 数据库连接未指定编码 | JDBC URL加上useUnicode=true&characterEncoding=utf8 |
| 时间显示T格式 | Jackson默认格式 | 配置spring.jackson.date-format全局格式化 |
| 部署时找不到上传目录 | 服务器无该目录 | 先手动创建上传目录并赋予写权限 |
| 管理员接口能被普通用户调用 | 拦截器未鉴权 | 确保JwtInterceptor中解析role并阻止非管理员请求 |
6.3 三种快速改造出个人特色的方案
如果直接交原版,年年都有相同的项目,导师容易审美疲劳。我整理过三种低成本改造方案,每种的代码改动量都不大,但效果立竿见影。
方案一:把"专辑"替换成你更熟悉的领域。比如改成"独立游戏鉴赏平台",字段从专辑名换成游戏名,艺人换成开发商,流派换成游戏分类。这样只需要修改部分实体字段和前端展示文案,业务骨架完全不用动。
方案二:增加"编辑推荐"模块。在首页增加一个精选推荐位,后台可以设置推荐的专辑并排定顺序,需要加一张recommend表和两个CRUD接口。这个功能在答辩时能充分讲你的关联表设计和权重排序思路。
方案三:升级搜索功能。原项目一般用LIKE %keyword%模糊查询,你可以换成Elasticsearch或者MySQL全文索引,并聊一聊分词、倒排索引的原理。这一项内容就能撑起一个专题阐述,论文也可以扩出一章。
改造时最需要注意的是:一定要在彻底理解原代码逻辑之后再动手,否则你改了字段名,但Mapper里手写SQL没同步改,整个模块直接崩溃,这种低级错误很容易发生在答辩前夜。
7. 写在最后的个人体会
做这个项目过程中我最大的感触是:前后端分离的真正难点,不在某个单独格式怎么写、某个框架怎么用,在于你脑子里能不能建立一条完整的链路——数据从MySQL表出发,经过Mapper映射成实体,Service处理业务,Controller暴露成接口,前端Axios拿到JSON,Vue渲染成页面,用户操作再反过来走一遍。这条链路每跑通一次,你对Java Web的理解就深一层。
录制视频和写接口文档的过程也让我获益不少。给项目写文档时你会发现很多"以为懂了其实没懂"的地方,比如为什么评分表不单独建、为什么上传目录要映射成虚拟路径、为什么用线程本地变量保存用户信息。每个问题逼着你查资料、想理由,这个过程远比跑通代码本身值钱。
最后分享一个小细节:JWT密钥一定不要用代码里那个默认的album_secret_key_please_change,随便换一个长字符串就行。因为文档里、博客里、答辩展示时你的代码都可能被传到公共仓库,密钥一旦泄露,别人就能伪造任意用户身份调用接口,这是真实可被利用的安全漏洞。同理,数据库账号密码在提交作业时虽然无法完全隐藏,但至少不要在README里再高调写一遍默认密码。
这套项目的扩展空间还很大,比如接入第三方登录、增加专辑播放试听、做数据可视化报表、用Redis缓存热门榜单,每一个方向都能让项目再上一个档次。抓住这套骨架,把它当成自己的东西去改、去讲、去完善,毕业设计这关就稳了。