
做完整套SpringBootVue专辑鉴赏网站平台我最直接的感受是这不仅是能交差的毕设还是把前后端分离、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配置全部干掉内嵌Tomcatjava -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字段类型说明idbigint主键自增usernamevarchar(50)唯一登录名passwordvarchar(100)加密后密码avatarvarchar(255)头像地址roletinyint0普通用户1管理员create_timedatetime注册时间专辑表album字段类型说明idbigint主键titlevarchar(100)专辑名称artist_idbigint所属艺人covervarchar(255)封面图地址type_idbigint流派分类publish_datedate发行日期descriptiontext专辑简介total_scorebigint评分总分score_countint评分人数statustinyint上架/下架状态评论表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 CHARSETutf8mb4 COLLATEutf8mb4_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、把结果包成ResultT返回。业务代码绝不写在Controller里。这个边界意识从第一天就要养成——答辩时老师抽查代码最烦看到Controller又臭又长、业务全堆在接口方法里的写法。ResultT统一响应体长这样public class ResultT { private Integer code; // 200成功400业务失败401未登录500异常 private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.msg success; result.data data; return result; } public static T ResultT error(Integer code, String msg) { ResultT result new Result(); result.code code; result.msg msg; return result; } }前端就根据这个code做拦截和提示前后端约定清爽不会出现接口返回格式各家不同的问题。3.2 专辑浏览、评分与收藏的接口设计接口设计坚持以资源为中心URL用名词复数配合HTTP方法表达动作。这一点答辩时讲到很有底气因为这是RESTful风格的核心约定。举几个实际接口方法路径说明GET/api/album/page?pageNum1pageSize10分页查询专辑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把磁盘路径映射成URLConfiguration 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?albumIdxxx接口返回true或false。页面加载时查一次收藏时显示为已收藏状态点击取消收藏后恢复。交互体验方面收藏成功用Message.success弹轻提示不要用弹窗打断用户。评论列表我用的是无限滚动改为传统的分页模式每页10条。每次新增评论后手动重新请求第一页数据这样能立即看到自己的评论出现在最上面。4.4 管理后台页面的快速搭建管理后台是展现代码规范的好地方。用Element UI的el-cardel-tableel-formel-dialog组合一小时左右就能搭完一个完整的专辑管理页面。专辑管理页的逻辑是顶部是新增专辑按钮和搜索框中间是表格展示专辑列表每行有编辑删除操作按钮点击编辑弹出对话框表单内容包含专辑名、艺人、分类、发行日期、简介、封面图。封面上传用el-upload配合actionhttp://localhost:8080/api/admin/upload并且加请求头token上传成功后在回调里把返回的图片地址放到隐藏字段中。需要注意的一点表格里的封面图片列用el-table-column配合作用域插槽渲染成缩略图而不是显示一串字符串路径。el-table-column label封面 width100 template slot-scopescope img :srcscope.row.cover classcover-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代理无此问题登录后接口返回401token未放入请求头检查axios请求拦截器是否从localStorage取值并设置Authorization上传图片失败提示超出大小Spring默认单文件最大1MB在application.yml配置max-file-size5MBmax-request-size5MB中文乱码数据库连接未指定编码JDBC URL加上useUnicodetruecharacterEncodingutf8时间显示T格式Jackson默认格式配置spring.jackson.date-format全局格式化部署时找不到上传目录服务器无该目录先手动创建上传目录并赋予写权限管理员接口能被普通用户调用拦截器未鉴权确保JwtInterceptor中解析role并阻止非管理员请求6.3 三种快速改造出个人特色的方案如果直接交原版年年都有相同的项目导师容易审美疲劳。我整理过三种低成本改造方案每种的代码改动量都不大但效果立竿见影。方案一把专辑替换成你更熟悉的领域。比如改成独立游戏鉴赏平台字段从专辑名换成游戏名艺人换成开发商流派换成游戏分类。这样只需要修改部分实体字段和前端展示文案业务骨架完全不用动。方案二增加编辑推荐模块。在首页增加一个精选推荐位后台可以设置推荐的专辑并排定顺序需要加一张recommend表和两个CRUD接口。这个功能在答辩时能充分讲你的关联表设计和权重排序思路。方案三升级搜索功能。原项目一般用LIKE %keyword%模糊查询你可以换成Elasticsearch或者MySQL全文索引并聊一聊分词、倒排索引的原理。这一项内容就能撑起一个专题阐述论文也可以扩出一章。改造时最需要注意的是一定要在彻底理解原代码逻辑之后再动手否则你改了字段名但Mapper里手写SQL没同步改整个模块直接崩溃这种低级错误很容易发生在答辩前夜。7. 写在最后的个人体会做这个项目过程中我最大的感触是前后端分离的真正难点不在某个单独格式怎么写、某个框架怎么用在于你脑子里能不能建立一条完整的链路——数据从MySQL表出发经过Mapper映射成实体Service处理业务Controller暴露成接口前端Axios拿到JSONVue渲染成页面用户操作再反过来走一遍。这条链路每跑通一次你对Java Web的理解就深一层。录制视频和写接口文档的过程也让我获益不少。给项目写文档时你会发现很多以为懂了其实没懂的地方比如为什么评分表不单独建、为什么上传目录要映射成虚拟路径、为什么用线程本地变量保存用户信息。每个问题逼着你查资料、想理由这个过程远比跑通代码本身值钱。最后分享一个小细节JWT密钥一定不要用代码里那个默认的album_secret_key_please_change随便换一个长字符串就行。因为文档里、博客里、答辩展示时你的代码都可能被传到公共仓库密钥一旦泄露别人就能伪造任意用户身份调用接口这是真实可被利用的安全漏洞。同理数据库账号密码在提交作业时虽然无法完全隐藏但至少不要在README里再高调写一遍默认密码。这套项目的扩展空间还很大比如接入第三方登录、增加专辑播放试听、做数据可视化报表、用Redis缓存热门榜单每一个方向都能让项目再上一个档次。抓住这套骨架把它当成自己的东西去改、去讲、去完善毕业设计这关就稳了。