
做这个专辑鉴赏网站项目的时候我最大的感觉是技术栈看着不复杂但真正把 SpringBoot、Vue3、MyBatis、MySQL 这一套串起来从前端页面到后端接口再到数据库表每一步都需要想清楚为什么这么设计。这篇就用这个项目作为例子把前后端分离的专辑鉴赏系统从架构到落地完整拆开讲一遍包括数据库表怎么建、接口怎么设计、Vue3 页面怎么接数据、跨域怎么处理、部署有哪些坑希望能给正在做类似管理系统或者入门全栈开发的朋友一些能直接参考的思路。1. 项目定位与技术选型为什么是这套组合1.1 专辑鉴赏网站到底在做什么这个系统本质上是一个内容管理与展示类平台核心业务是让用户浏览专辑信息、查看专辑详情、发表评分和评论。听起来不算奇怪但它比单纯的 CRUD 多一点业务感因为涉及用户体系、内容展示、交互评论三层逻辑。这也是为什么它非常适合用来练习 SpringBoot Vue3 前后端分离开发复杂度适中既有值得设计的边界又不至于一上来就把人劝退。在使用这套源码的时候你需要先想清楚一个前提这个项目的核心价值不在算法而在信息组织。专辑的基础信息名称、艺术家、发行年份、风格、封面图、简介是静态数据而评分、评论、收藏是动态数据。要把这两类数据组织好需要做合理的数据库建模也需要对接口粒度做拆分。前端页面能不能流畅展示后端的接口设计占了很大比重。1.2 每个技术组件解决什么问题技术选型这块我先说结论SpringBoot Vue3 MyBatis MySQL 这套组合最大的优势是分工清晰、生态成熟、上手曲线平缓特别适合中小型业务系统和学习项目。SpringBoot 负责后端能力封装。它内置了 Tomcat 容器、自动配置、依赖管理等机制让你不用再手动写大量 XML 配置。做这个项目时只需要关注业务接口和数据访问而不需要从零搭建服务器环境。Vue3 负责前端交互。相比 Vue2Composition API 和script setup语法让组件逻辑复用变得直接很多。专辑列表、详情页、评论区域这些模块通过组件拆分可以很自然地组织起来。MyBatis 负责数据库操作。它相比 JPA 更透明SQL 掌握在自己手里。对于这个项目里的多表关联、动态查询、分页统计这类操作SQL 的可控性直接决定了性能优化空间。MySQL 负责数据存储。无论是用户资料、专辑元数据还是评论记录关系型表结构都能表达得很清楚。还有个隐藏优点是这套技术栈的学习资料极多。遇到问题随便一搜几乎都有现成案例这种可查性对开发者来说是很实际的效率红利。2. 架构设计与数据库建模2.1 前后端分离到底分离了什么很多人一说前后端分离就以为只是前端和后端代码分开两个目录。实际上分离的不仅是代码更是部署方式和职责边界。在这个项目里Vue3 前端通过 Vite 工程化构建最终生成静态文件可以部署在 Nginx 上SpringBoot 后端独立打包成 Jar 运行在服务器上提供 RESTful API。两者只通过 HTTP/JSON 通信。这样的设计有一个直接好处前端开发和后端开发可以并行推进。后端定义好接口文档前端工程师就可以用 Mock 数据先渲染页面等后端接口完成后只需要把前端请求的 BaseURL 切到真实地址就行。在我实际做这个项目时就是先和后端约定好接口格式然后前端把 Axios 封装好所有请求走统一拦截器后端的逻辑实现和前端页面的填充几乎是同时完成的。2.2 数据库表结构怎么设计才合理专辑鉴赏系统的数据库建模我建议从用户-内容-交互三个维度来拆。核心表可以分为以下几张表名用途关键字段sys_user系统用户id, username, password, nickname, avataralbum_info专辑信息id, album_name, artist, genre, release_date, cover_url, introalbum_comment用户评论id, album_id, user_id, content, rating, create_timeuser_favorite收藏记录id, user_id, album_id, create_time这样设计的核心逻辑是专辑信息表是静态基础数据评论表和收藏表都是关系表通过外键与用户表、专辑表关联。在查询专辑详情的时候你可以通过一句多表关联查询一次性拿到专辑信息、平均评分、评论数量而不需要在前端做多次请求。建表时需要注意几个细节。第一个是字符集要统一用 utf8mb4不然存中文和 emoji 表情容易出问题。第二个是评论表的 rating 字段建议用DECIMAL(2,1)能支持 4.5 分这种常见评分。第三个是 create_time 这类时间字段设置默认值为CURRENT_TIMESTAMP减少后端手动赋值。这些细节看似小但直接影响后续开发效率和数据质量。2.3 后端代码分层从 Controller 到 Mapper代码组织上我采用的是经典的三层架构Controller 层负责接收请求和参数校验Service 层负责业务逻辑处理Mapper 层负责数据库访问。这样做最大的好处是职责单一。比如专辑详情接口Controller 只做参数接收和结果封装真正的逻辑判断专辑是否存在、用户是否登录、评分是否在合法范围全部下沉到 Service 层。这种分层还有一个很实用的意义方便写单元测试。Service 层不依赖 Web 容器可以直接通过 Spring 容器加载然后测试业务方法而 Controller 层的测试则可以通过 MockMvc 来处理。对于复杂的业务逻辑这样的结构能让你在排查问题时少走很多弯路。3. 后端核心实现SpringBoot MyBatis 实战3.1 项目初始化和关键依赖配置创建 SpringBoot 项目这一步我推荐直接使用 Spring Initializr 生成基础工程选择 Java 8 或 11 都可以。核心依赖包括dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency配置文件方面我通常会把数据库连接单独放到 application.yml 中管理并且使用不同 profile 区分本地环境和线上环境spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/album_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.album.system.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置非常关键。数据库字段通常是下划线命名比如release_dateJava 实体类字段是驼峰命名releaseDate开启这个配置后 MyBatis 会自动完成映射省去大量手动resultMap配置。3.2 基于 MyBatis 的动态 SQL 实现复杂查询专辑列表页通常需要支持多条件筛选按专辑名称模糊搜索、按风格筛选、按发行年份排序。如果每个条件写一个方法代码会非常冗余。MyBatis 的动态 SQL 在这里发挥了巨大作用。以一个分页查询为例Mapper 接口定义如下public interface AlbumInfoMapper { ListAlbumInfo selectAlbumPage(Param(keyword) String keyword, Param(genre) String genre, Param(offset) int offset, Param(size) int size); long countAlbumPage(Param(keyword) String keyword, Param(genre) String genre); }对应的 XML 映射文件使用where和if标签动态拼接条件select idselectAlbumPage resultTypeAlbumInfo SELECT * FROM album_info where if testkeyword ! null and keyword ! AND album_name LIKE CONCAT(%, #{keyword}, %) /if if testgenre ! null and genre ! AND genre #{genre} /if /where ORDER BY release_date DESC LIMIT #{offset}, #{size} /select这样写的好处很明显前端传什么条件SQL 就拼什么条件没传的条件不会出现在语句里。我还习惯给模糊查询的字段加上索引否则数据量一大LIKE %keyword%会导致全表扫描性能骤降。3.3 用户登录与 Token 鉴权用户评论和收藏功能需要登录态。项目里我采用 JWT 做无状态鉴权登录成功后后端签发一个 token前端每次请求通过Authorization头携带后端用拦截器统一校验。登录接口的核心逻辑大概是这样Service public class UserService { Autowired private UserMapper userMapper; public String login(String username, String password) { User user userMapper.selectByUsername(username); if (user null || !password.equals(user.getPassword())) { throw new RuntimeException(用户名或密码错误); } // 生成 JWT有效期 24 小时 return JwtUtils.generateToken(user.getId(), user.getUsername()); } }重点提醒生产环境密码必须先 BCrypt 加密再存库绝不能明文保存。这个项目作为学习演示可以简化但如果你要放到公网或者写到简历里请务必把密码加密加上这是一个非常基础的底线问题。接口鉴权方面我写了一个LoginInterceptor拦截器基于 Spring 的 HandlerInterceptor 实现public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !JwtUtils.verify(token)) { response.setStatus(401); return false; } return true; } }在 WebMvc 配置类中注册拦截器并设置放行路径比如登录接口、专辑列表接口可以匿名访问而评论、收藏接口必须登录后访问。这样一个配置就把接口安全级别理清了。4. 前端核心实现Vue3 项目搭建与页面开发4.1 用 Vite 快速创建 Vue3 项目前端部分我使用的是 Vite 作为构建工具相比传统 Webpack它在开发环境下的冷启动速度和热更新体验要明显快很多。创建项目命令很简单npm create vitelatest album-frontend -- --template vue cd album-frontend npm install安装完基础依赖后还需要安装路由和状态管理相关的包npm install vue-router4 pinia axiosVue3 的项目结构我按功能模块来组织而不是按角色页面来组织。src/api目录专门放接口请求方法src/views放页面组件src/components放公共组件src/store放 Pinia 状态定义。4.2 Axios 封装与请求拦截前后端分离项目里Axios 封装几乎是必修课。我通常在src/api/request.js里创建一个 Axios 实例配置好基础 URL 和超时时间然后在请求拦截器里统一添加 token 头在响应拦截器里统一处理错误状态码。import axios from axios import { useUserStore } from ../store/user import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization userStore.token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { router.push(/login) } return Promise.reject(error) } ) export default request这里建议开发时把baseURL设为/api这样在本地开发时可以通过 Vite 的 proxy 配置把请求代理到后端端口生产环境再通过 Nginx 反向代理避免跨域问题。4.3 核心页面拆解专辑列表与详情页专辑列表页是最核心的展示页面。我采用网格布局展示专辑卡片每个卡片包含封面、专辑名、艺术家、评分。数据从后端分页接口获取使用onMounted生命周期函数调用 API 方法const albumList ref([]) const total ref(0) const currentPage ref(1) const pageSize ref(12) const fetchAlbums async () { const data await getAlbumPage({ page: currentPage.value, size: pageSize.value, keyword: keyword.value, genre: genre.value }) albumList.value data.records total.value data.total } onMounted(() { fetchAlbums() })这里有个经验接口返回的分页数据结构最好统一格式比如{ records: [], total: 0, current: 1, size: 10 }前端写起来就特别省心所有列表页都能套用同一个逻辑。专辑详情页需要考虑的信息层级比较多顶部是封面和基础信息下方是简介再往下是评论列表和评分表单。所以我会把详情页拆成几个子组件分别是AlbumHeader.vue、AlbumIntro.vue、CommentSection.vue。特别是评论组件它内部包含了登录后才能评论的判断逻辑如果没有 token 就加载登录提示弹窗这种子组件独立维护的模式在后期增加功能时能减少很多冲突。4.4 组件复用与状态管理Pinia 在这个项目里主要用来管理用户状态包括用户信息、登录状态、token。因为 Vue3 的组合式 API 用起来很顺手很多人会忽略状态管理的必要性但实际开发中如果不做状态集中管理多个组件之间共享同一个用户信息时就会非常别扭。示例 store 定义import { defineStore } from pinia export const useUserStore defineStore({ id: user, state: () ({ token: localStorage.getItem(token) || , nickname: localStorage.getItem(nickname) || }), actions: { setLoginInfo(token, nickname) { this.token token this.nickname nickname localStorage.setItem(token, token) localStorage.setItem(nickname, nickname) }, logout() { this.token this.nickname localStorage.removeItem(token) localStorage.removeItem(nickname) } } })把 token 持久化到 localStorage 的合理性在于刷新页面后 Vue 实例重建但 session 状态还能从 localStorage 恢复用户不需要重新登录。如果你追求更严格的安全可以用 sessionStorage看具体场景需求。5. 前后端联调、跨域处理与部署上线5.1 本地开发联调Vite Proxy 配置前后端分离开发时最容易遇到的就是跨域问题。浏览器默认禁止跨域请求而本地开发时前端跑在 5173 端口后端跑在 8080 端口这就构成了跨域。处理方案有两种后端加CrossOrigin注解或全局 CORS 配置前端配置 Vite proxy。我更推荐在前端配置 proxy因为这样前端的请求地址写/api即可后端不需要感知前端的存在。修改vite.config.jsexport default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这里有个坑要提一下如果后端 Controller 的 RequestMapping 写的是/album/list前端请求路径应该是/api/album/list通过 rewrite 把/api前缀去掉再转发到后端。这个约定要前后端一致否则接口 404 你会排查半天。5.2 生产环境部署Nginx 反向代理生产环境部署时前端是一个静态文件目录后端是一个 Jar 包。最稳妥的做法是用 Nginx 托管前端静态资源同时配置反向代理把/api路径的请求转发到后端的 8080 端口。参考配置片段server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这一行尤其重要因为 Vue Router 的 history 模式在没有服务端配合时刷新某个路由路径比如/album/1会直接 404加上这行配置后所有路径都回退到 index.html由前端路由接管。后端部署则是标准操作mvn clean package -DskipTests java -jar target/album-system-1.0.0.jar --spring.profiles.activeprod5.3 数据库初始化与数据初始化策略项目首次部署时需要一个初始化数据库的过程。我是把建表 SQL 和基础测试数据分开写的建表脚本使用标准的CREATE TABLE IF NOT EXISTS语句方便重复执行测试数据则单独放到 data.sql 中。这里有一个非常实用的建议给专辑表添加 20 条以上的测试数据你才有足够的内容去调试分页、搜索、评分排序这些功能。数据太少很多界面问题看不出来。INSERT INTO album_info (album_name, artist, genre, release_date, cover_url, intro) VALUES (Ablaze, Fragments, Rock, 2023-05-12, /covers/ablaze.jpg, Third album from the band...), (Midnight Waves, The Stellar, Electronic, 2022-11-03, /covers/midnight.jpg, A dreamy electronic journey...);5.4 打包构建全流程记录我把整个项目的完整运行流程按顺序记录一下方便你照着操作启动 MySQL执行建表脚本和测试数据脚本修改后端application.yml中的数据库连接信息运行mvn spring-boot:run启动后端验证接口可用进入 frontend 目录执行npm install安装依赖执行npm run dev启动前端开发服务器验证页面正常执行npm run build构建前端产物将 dist 目录内容上传到服务器后端的 Jar 包部署到服务器用java -jar启动配置 Nginx 静态托管和反向代理完整走完这套流程你对整个项目的掌控才算真正到位。6. 常见问题与排障方式笔记6.1 数据库连接失败与时区问题MySQL 连接报错时最常见的两个原因一是驱动版本不匹配二是时区配置报错。现在 MySQL 8.x 用的驱动类名是com.mysql.cj.jdbc.Driver而很多旧项目还是写com.mysql.jdbc.Driver启动直接报ClassNotFoundException或Loading class is disabled错误。时区问题一般报错为The server time zone value Öйú±ê׼ʱ¼ä is unrecognized解决办法就是在数据库连接 URL 后加上serverTimezoneAsia/Shanghai。遇到这类问题我习惯把配置文件的三要素driver、url、password逐个对照检查大部分数据库连不上问题都出在这三个地方之一。6.2 前端请求接口 404 的问题排查前端页面能打开但请求接口时 404最容易犯的错误是路径拼接不一致。比如前端请求/api/album/list经过 Vite proxy 的 rewrite 后变成/album/list但后端接口实际路径是/albumInfo/list那就 404 了。排查这种问题的最有效方式是打开浏览器的 Network 面板看实际的请求 URL。如果看到的请求路径不对把前端和 proxy 配置逐层检查如果路径对了但还是 404去后端控制台看有没有路径映射的报错。用这种方式定位通常三五分钟就能解决问题而不是靠猜。6.3 中文乱码与存储异常数据库中文乱码的现象非常典型出现这一问题的根本原因是字符集不一致。后端连接 URL 已经设置了characterEncodingutf8但数据库表本身的字符集如果还是 latin1一样乱码。建库的时候就要指定CREATE DATABASE album_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;另外还要注意 JVM 启动参数。如果使用的运行环境默认编码不是 UTF-8后端读取配置文件时也可能乱码。保险起见在启动脚本里加上-Dfile.encodingUTF-8。6.4 MyBatis 参数绑定和空值问题使用 MyBatis 时最容易踩的坑是动态 SQL 里的参数类型不符。比如#{offset}传了字符串类型而 SQL 里用在LIMIT后面MySQL 虽然会自动转换但在复杂 SQL 里可能会出现微妙的问题。所以 Mapper 接口参数建议显式标注ParamXML 中也尽量写清楚。还有一个典型问题if testkeyword ! null and keyword ! 这种判断对空字符串的判断是必须的否则前端传空字符串过去时SQL 会拼接一个AND album_name LIKE %%导致全表匹配。这不仅是性能问题还可能让搜索结果的语义不符合预期。7. 扩展优化与应用场景建议7.1 如何把项目改造成分布式基础如果你觉得这个项目做完后还有上升空间可以从单机架构向分布式架构靠拢。最常见的优化点是把 JWT 换成 Spring Security OAuth2引入 Redis 做缓存和分布式 Session 管理把文件上传专辑封面改造成对接 OSS 对象存储。这个项目本身的结构是留有扩展空间的Mapper 层和 Service 层分离得比较清楚改造起来不会伤筋动骨。7.2 业务场景的横向迁移专辑鉴赏网站的架构模型其实可以迁移到许多类似的场景。比如电子书收录、电影点评、摄影作品分享核心都还是内容主体 用户交互的模型。你只需要调整album_info里的字段定义把专辑名换成书名或电影名其他模块基本可以复用。我在做这个项目时最深的体会是不要急着写代码先把用户评论、专辑列表、登录这三大块的数据流画清楚。数据流理清了后端的接口粒度自然就明白前端页面怎么拆也不容易乱。项目做完之后把建表 SQL、接口文档、前端组件关系这三样整理出来整个项目就完全掌握在自己手里了。这个项目后续如果要做得更完善我建议优先补充用户管理后台和专辑数据导入功能这两个方向是最贴近实际使用的迭代点。