
前后端分离的练手项目我见过太多人选图书管理系统了。原因很简单业务够经典模块够完整但又不像电商、秒杀系统那样涉及复杂的分布式问题非常适合拿来完整走一遍“SpringBootVueMyBatisMySQL”这套前后端分离技术栈。最近我又完整做了一遍这个项目从数据库设计、后端接口、前端页面一路做到打包部署上线过程中踩了不少坑也总结了一些比源码本身更值钱的经验这里完整分享出来希望能给正在做毕设、或者准备上手前后端分离实站的朋友一点参考。这个项目说到底是解决一个很实际的问题图书馆里的书怎么管、借出去的书怎么跟踪、用户数据怎么维护。用前后端分离的方式来做就是把“管理员操作的界面”和“后端数据服务的逻辑”彻底拆开前端只负责交互展示后端只负责业务处理和存储。SpringBoot承担后端接口Vue负责前端页面MyBatis处理数据库访问MySQL负责数据落地。这套组合的好处是每个环节都有非常成熟的生态网上能找到极其丰富的问题答案对新手极其友好。1. 项目整体设计从需求到技术选型的拆解1.1 图书管理系统到底要管哪些事开始动手之前先把业务边界划清楚。一个标准的前后端分离图书管理系统核心模块至少包含这些用户登录与权限管理、图书分类管理、图书信息维护、图书借阅与归还、借阅记录查询、图书统计。听起来不多但每一个模块展开来都足够写一篇完整的博文。举个例子图书借阅这个动作表面上就是“点一下借书按钮”实际上后端要处理的事情包括检查图书是否存在、检查库存是否大于0、检查该用户是否有未归还的借阅记录、生成一条借阅记录、扣减图书库存。归还的时候要做对称操作更新借阅记录的归还时间、增加库存。这些业务逻辑如果全部堆在Controller里代码会变得非常难看。所以在设计阶段就得把数据表和接口规划好后面coding才能顺畅。我的建议是先在纸上画一遍流程图把“谁在什么条件下对什么数据做什么操作”理清楚。千万别上来就写代码前后端分离项目最怕的就是前端写完了发现后端接口对不上后端做完了发现前端要的数据结构不匹配。1.2 为什么偏偏是SpringBootVue这套组合选择技术栈不是越新越好而是越稳妥越好。SpringBoot之所以成为后端主流是因为它把Spring家族繁杂的XML配置彻底简化了内嵌Tomcat一个jar包就能跑起来。Vue作为前端框架核心优势是组件化和响应式一个图书列表页拆成表格组件、搜索组件、分页组件每个组件都能独立维护。MyBatis在持久层里讲究“SQL可控”图书管理的很多场景都有多表关联查询MyBatis可以写自己完全掌控的SQL而不需要像JPA那样通过方法名猜语义。做个简单对比可能更直观技术组件核心优势在本项目中承担的角色SpringBoot配置简洁、生态成熟、内嵌服务器提供RESTful接口处理业务逻辑Vue组件化开发、数据双向绑定管理端页面渲染与用户交互MyBatisSQL灵活可控、支持动态SQL数据持久化操作多表联查MySQL免费稳定、使用广泛图书、用户、借阅记录等数据存储另外提一嘴若依框架很多人搜“若依框架前后端分离”那是因为若依把权限体系、代码生成、部门管理都内置好了开箱即用。但如果你是为了学习前后端分离的原理我还是建议从零搭建一遍。自己搭一遍之后你会对“接口-服务-数据库”这条链路有更深的体感跳过了这个过程就算用了若依出了问题也不知道从哪里排查。1.3 数据库表设计与初始化数据我设计数据库时遵循一个原则能拆的表尽量拆能加约束的地方不要省。这个项目我用了四张核心表管理员用户表user、图书分类表category、图书信息表book、借阅记录表borrow_record。管理员用户表很简单字段包括id、username、password、create_time。密码一定不能存明文我用的是BCrypt加密后面鉴权部分细说。图书分类表也就id、name两个核心字段。图书信息表稍微复杂一点包含isbn、书名、作者、出版社、分类id、库存数量、总库存、简介、图片地址、上架状态。这里有个小细节isbn字段建议设置为唯一索引同一个ISBN号的图书在系统中视为同一本书在入库时如果重复录入可以直接更新库存数量。借阅记录表是关联关系的核心字段包括id、user_id关联用户表、book_id关联图书表、borrow_time、return_time、status。status字段我用0表示借出中1表示已归还。为什么不用直接删除记录呢因为借阅历史是有统计价值的后面做“最热图书”“读者借阅排行”都要靠这些历史记录。建表SQL我这里给出核心部分CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library; CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名称, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id int(11) NOT NULL AUTO_INCREMENT, isbn varchar(20) NOT NULL COMMENT ISBN编号, name varchar(200) NOT NULL COMMENT 书名, author varchar(100) DEFAULT NULL, publisher varchar(100) DEFAULT NULL, category_id int(11) DEFAULT NULL, stock int(11) DEFAULT 0 COMMENT 当前库存, total_stock int(11) DEFAULT 0 COMMENT 总库存, cover_image varchar(255) DEFAULT NULL COMMENT 封面图URL, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), UNIQUE KEY uk_isbn (isbn), KEY idx_category_id (category_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, book_id int(11) NOT NULL, borrow_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 借书时间, return_time datetime DEFAULT NULL COMMENT 还书时间, status tinyint(1) DEFAULT 0 COMMENT 0借出中 1已归还, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_book_id (book_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;注意一个细节所有表的字符集我都用了utf8mb4而不是utf8因为utf8mb4才是真正的四字节编码能完整支持生僻字和特殊字符。图书管理里可能会出现“”这类生僻字作者名用utf8可能直接入库失败。2. 后端实现SpringBoot整合MyBatis的完整链路2.1 版本选型与Maven依赖配置后端搭建第一步是选版本。这里我想重点强调一个最近很多人踩的问题springboot版本太高。Spring Boot 3.x发布之后确实很吸引人但随之而来的是JDK最低要求17、javax包全部迁移到jakarta、大量的starter也要对应升级。对于图书管理系统这种项目完全没必要追求最新我在实操中统一使用Spring Boot 2.7.x JDK 8/11的组合稳定、资料多、兼容性好。Maven依赖这里我贴出pom中核心的部分注意mybatis和mysql驱动的版本要跟Spring Boot版本匹配。如果不小心配了Spring Boot 3.x下面这段依赖里的javax.sql相关内容就要改成jakarta会多出不少工作量parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 整合 SpringBoot 的官方starter -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- BCrypt 密码加密 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependency !-- JWT 工具 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- Lombok 简化实体类注意IDEA要装插件 -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesMaven的构建方式这里多说一句很多新手在IDEA里搞了半天项目起不来问题大多出在Maven没有配置好。IDEA里设置Maven的User settings file指向你自己下载的settings.xml把本地仓库路径也配置一下。项目下载依赖的时候优先用阿里云镜像仓库不然某些依赖可能拉取极其缓慢甚至失败。2.2 application.yml配置文件里的“坑”配置文件是整个项目最容易装神弄鬼的地方我见过不少项目后端本身代码没问题纯粹因为配置不对起不来。图书管理系统这份application.yml我建议按下面的基础结构来加自己的内容server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的数据库密码 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.library.mapper: debug这里有三个关键点值得展开讲一下。第一url里的useSSLfalse和allowPublicKeyRetrievaltrue前者解决MySQL 8.0的SSL连接错误后者解决MySQL 8.0的Public Key Retrieval报错。这两行是无数人遇到“MySQL ssl连接错误”“Public Key Retrieval is not allowed”之后搜出来的解决方案建议直接写进去。第二serverTimezoneAsia/Shanghai不能省否则日期类型的字段会出现八小时时差。第三map-underscore-to-camel-case: true一开数据库字段user_id就能自动映射为Java实体里的userId少写一堆resultMap映射但复杂SQL我仍然建议用显式的resultMap后面会讲。关于“mybatis配置打印SQL”就是log-impl配置成StdOutImpl。在控制台就能看到每次请求执行的完整SQL和参数值。这个配置在联调阶段非常关键前端说数据不对你把后端的SQL日志一贴出来问题在哪儿一目了然。2.3 实体类、Mapper接口与XML映射后端代码的分层是我比较在意的部分。这个项目我用的是经典的五层结构Controller接收请求、Service写业务逻辑、Mapper接口定义数据方法、XML文件写SQL、Entity载体数据。另外加一个common包放统一返回结果、异常处理、JWT工具等。实体类我以Book为例用Lombok简化getter/setter代码量大大减少Data public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private Integer categoryId; private Integer stock; private Integer totalStock; private String coverImage; private Boolean status; }Mapper接口和XML的配合是MyBatis的核心玩法。接口只定义方法签名和注解参数真正执行的SQL全部写在XML文件里Mapper public interface BookMapper { ListBook selectBookList(BookQuery query); Book selectBookById(Integer id); int insertBook(Book book); int updateBook(Book book); int deleteBook(Integer id); int updateStock(Param(bookId) Integer bookId, Param(amount) Integer amount); }对应的XML文件里图书列表查询我用的是动态SQL。为什么因为列表页的搜索条件是可变的用户可能只按书名搜可能只按分类筛也可能两者一起上。每个条件都写一条SQL会疯掉用动态SQL组合条件才是正解select idselectBookList resultTypecom.example.library.entity.Book SELECT * FROM book where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select有人问过我数据量大了以后这里要不要加索引。关于分页这个项目数据量不大用limit做手动分页或者PageHelper分页插件都可以。如果笔试面试考到MyBatis动态SQL上面这个ifwhere的写法就是标准答案。我还想特别提一下typehandler。热词里有“mybatis中typehandler的工作流程图”这在MyBatis源码面试题里经常出现。简单理解typehandler就是负责Java类型和JDBC类型之间转换的适配器默认已经处理了String、Integer、Date这些常见类型。图书分类的id、图书的ISBN其实都用默认就够不建议在这个项目里自定义typehandler。但你要知道有这个机制面试官问起来的时候至少能说上来。2.4 登录鉴权与拦截器图书管理系统的后台必须要有登录功能。我的实现方案很标准登录接口校验用户名密码后签发一个JWT前端把token存到localStorage之后每次请求都在请求头里带上Authorization字段后端通过拦截器校验token是否合法。JWT工具类的核心代码不算复杂网上有大量模板但有几个关键点签名密钥要足够复杂不要把密钥写死在代码里而是放到配置文件里过期时间设定为24小时比较合理太短用户体验差太长有安全风险。密码存储用BCrypt加密就是之前引入spring-security-crypto的原因。BCrypt每次加密的盐都不同所以数据库中存储的密文即使两个用户明文密码相同密文也不同。拦截器实现上需要注意放行路径的配置。登录接口本身、静态资源路径、以及前端的入口路径default.html都要放行其他接口全部拦截未携带token或者token过期统一返回401状态码和提示信息。这里如果配置不对最容易出现的现象是前端页面能打开但所有数据请求都报“未授权”。遇到这个问题先检查拦截器的排除路径有没有写对。统一返回格式也很有必要。我定义了一个Result类包含code、message、data三个字段。成功返回Result.success(data)失败返回Result.error()。这样前端axios响应拦截器里只需要判断code是不是200如果是再取data不是则弹出错误提示。这个设计虽然简单但能省掉前端大量重复的错误处理代码。3. 前端Vue实现从环境搭建到页面联调3.1 Vue开发环境安装与项目初始化前端这一侧第一道坎就是环境配置。很多新手上来就安装最新版Node.js然后发现跟一些老项目冲突。我这次使用的是Node.js 16 LTS配合Vite 4.0创建Vue 3项目。这里说明一下Vue有2和3两个大版本新项目我强烈建议直接用Vue 3 Vite如果是毕设或者参考老教程也可以用Vue 2 Vue CLI但Vue 2生态已经进入维护尾声新写的代码不建议再迁就它了。环境安装的完整流程如下# 1. 检查node版本建议使用16.x或18.x node -v # 2. 使用Vite初始化Vue3项目 npm create vitelatest library-ui -- --template vue # 3. 进入项目目录并安装依赖 cd library-ui npm install # 4. 安装路由、状态管理、网络请求库和UI组件 npm install vue-router4 axios element-plus # 5. 启动开发服务器 npm run dev初始化的项目会在http://localhost:5173启动这就带来一个CORS跨域问题前端跑在5173端口后端跑在8080端口两个源不同直接请求会被浏览器拦下来。解决方案之一是在Vite的配置文件中配置代理把/api路径的请求转发到后端import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求中只要统一发起/api/xxx请求浏览器实际访问的是5173端口的/api/xxxVite再把请求转发到8080端口绕开了跨域问题。3.2 前后端接口规范与axios封装前后端联调最容易出现的问题就是“前端接口调通了后端却拿不到数据”。原因大多集中在参数传递格式不一致上。我做这个项目的时候前后端约定查询参数一律放在query里提交数据一律采用JSON格式放在request body里。这个约定在axios封装中直接落实。我封装了一个request.js统一处理请求前缀、token携带和响应拦截import axios from axios import { ElMessage } from element-plus // 创建axios实例 const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器每次请求自动携带token 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) { ElMessage.error(res.message || 请求失败) if (res.code 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request注意事项响应拦截器里我把res.data直接返回了也就是说在业务代码里调用getBookList()拿到的直接就是data字段的内容不用再层层解包。3.3 路由守卫与页面结构前端路由设计我用的是嵌套路由配合动态组件的方式。最外层是布局组件Layout包含侧边栏和顶部栏内部再嵌入每个业务页面。登录页和404页面不放在Layout里面是独立页面。vue-router4的写法与Vue2时略有不同尤其是createRouter/createWebHistory这些API的引入方式。路由守卫是登录功能的最后一环。页面刷新的时候前端不能直接认为用户已经登录需要校验localStorage里是否存在token。如果不存在一律重定向到/login页面。但这里有个小坑直接刷新某个深链接时前端并不知道用户是否登录如果token存在普通校验即可通过可如果后端登录态过期了后端接口返回401此时响应拦截器会删除本地token并跳回登录页。整体闭环是OK的。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (token) { next() } else { next(/login) } } })3.4 图书管理核心页面实现图书列表页是整个前端最典型的CRUD页面。我用Element Plus的el-table展示列表数据el-pagination实现分页el-dialog嵌套el-form实现新增和编辑。这里有一个值得说的细节表格里展示图书分类名称而不是分类ID。后端返回的数据里是categoryId而前端期望展示的是“文学”“科技”这样的名称。解决办法有几种第一种是后端SQLjoin联查把categoryName一起查出来第二种是前端拿到全部分类后做映射。在我的实际项目中图书列表接口直接做了表关联查询返回数据里自带categoryName。这就是为什么当初设计数据库时category_id字段单独拆一张表联查时方便得多。借书和还书按钮放在表格的“操作”列里点击借书时调用后端接口成功后刷新当前页数据即可。这些交互如果拆开来讲每一块都能展开不少内容但在项目里它们都是标准的弹窗和接口调用属于“写熟了就快”的部分。4. 部署发布后端打包与前端的两种集成方式4.1 后端Maven打包与运行参数项目开发完成后第一步是后端打包。在IDEA右侧Maven面板中执行clean然后双击package。如果你想在命令行操作可以这样mvn clean package -DskipTests打包成功后target目录下会生成一个library-backend-1.0.0.jar。这里有一个细节Spring Boot的Maven插件会生成一个可执行jar和原有的jar可执行jar通过java -jar命令直接运行这是SpringBoot内嵌Tomcat带来的便利。以前SSM项目部署要单独装Tomcat把war包丢进webapps目录现在一个jar全搞定。启动后端的命令我习惯写成这样java -jar library-backend-1.0.0.jar --server.port8080如果你在服务器上就需要配合nohup命令在后台运行nohup java -jar library-backend-1.0.0.jar app.log 21 日志输出重定向到app.log文件方便随时查看运行状态。4.2 前端打包后如何整合进SpringBoot前端开发模式下是通过Vite dev server跑在5173端口但到了线上你不能指望服务器再单独跑一个Node进程前端服务。生产环境下最省事的方式是执行前端打包把dist产物复制到SpringBoot的静态资源目录下与后端一起发布。SpringBoot会优先从classpath下的META-INF/resources、resources、static、public这几个目录寻找静态资源。具体操作过程# 第一步前端构建生产包 npm run build # 第二步把dist目录的内容复制到后端项目的static目录 cp -r dist/* ../library-backend/src/main/resources/static/ # 第三步重新打包后端项目 mvn clean package -DskipTests整合后的jar包自带前端静态页面访问http://服务器IP:8080/就能直接打开管理系统不用再启动任何额外服务。这个方案对图书管理系统这种中小型应用来说几乎是性价比最高的部署方式。前端打包对应的就是热词里“vue打包放进springboot中”这件事做熟练了之后你会在nginx配置一类问题上省下大量时间。注意如果前端使用了history路由模式比如访问/book-list这个前端路由直接刷新页面时SpringBoot会返回404因为它找不到对应的静态文件。解决办法有多种一是把registerWebServerComponents的静态资源映射调整另一种更简单的做法是在后端写一个控制器把非/api开头的请求都转发到index.html。后面第5节会详细说。4.3 服务器MySQL初始化服务器上必须有一个数据库实例。我用的MySQL 8.0初始化过程其实不难但有几个地方容易踩坑。安装完成后用mysql_secure_installation做一个基础的安全加固设置root密码、禁用匿名用户、删除test库。接着创建图书管理系统的专用数据库和用户CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER lib_user% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON library.* TO lib_user%; FLUSH PRIVILEGES;我建议不要直接用root账号跑业务。用专用账号的好处是权限可控如果应用被注入或信息泄露攻击者拿到的也不是数据库最高权限。导入表结构时把本地导出的library.sql上传到服务器执行即可。MySQL 5.7和8.0的安装过程细节上有差异特别是8.0默认的认证插件是caching_sha2_password如果你的JDBC驱动版本太老会出现认证方式不兼容的问题。所以驱动一定要用8.0.33这个较新的版本。4.4 防火墙、端口与进程管理服务器上有一个经验打不开页面时90%的情况不是程序问题而是防火墙没放行端口。CentOS系的服务器用firewalldUbuntu系用ufw。8080端口需要通过防火墙# CentOS / AlmaLinux sudo firewall-cmd --permanent --add-port8080/tcp sudo firewall-cmd --reload # Ubuntu sudo ufw allow 8080/tcp有些云服务器厂商还在安全组层面做了网络隔离单改服务器防火墙还不够还要去云控制台放行8080端口。这个坑我踩过多次每次换新服务器都要检查一遍。进程管理的几个常用操作附上我一直在用的命令# 查看jar进程是否在运行 ps -ef | grep library-backend # 杀掉旧进程PID根据上面查询结果替换 kill -9 PID # 方便起见把启动命令写成一个脚本 start.sh nohup java -jar library-backend-1.0.0.jar app.log 21 echo $! app.pid这个start.sh脚本里echo $!把进程号写入app.pid后面要停服务时直接cat app.pid就能找到进程号不用再去ps里翻了。5. 常见问题与排查技巧实录5.1 MySQL连接报错排查我把这次部署过程中切切实实遇到的报错整理成了一张速查表基本都是高频问题报错信息原因解决方案Public Key Retrieval is not allowedMySQL 8.0的caching_sha2_password认证机制JDBC url中加allowPublicKeyRetrievaltrueSSL connection error / SSL 握手失败客户端与服务器SSL协议协商失败JDBC url中加useSSLfalse或配置SSL证书Access denied for user用户密码错误或权限不足GRANT语句重新授权注意账号的host匹配The server time zone value XXX is unrecognized时区配置缺失url中加serverTimezoneAsia/ShanghaiCommunication link failure数据库连接被断开或防火墙拦截检查云安全组和服务器防火墙是否放行3306端口关于MySQL的SSL问题再多说一句。本地开发环境mysql-connector-java 8.0版本默认会尝试建立SSL连接如果你的MySQL服务器没有相应配置就会报错。开发环境直接useSSLfalse是最快的解决办法生产环境如果要安全通信正确做法是配置SSL证书而不是关掉SSL。但对于图书管理系统这种内网应用关闭SSL不影响实际使用。5.2 SpringBoot版本过高引发的兼容性问题热词里有一条“springboot版本太高”这确实是有道理的。Spring Boot 3.x和2.x之间不只是大版本号变了底层有几处非常硬核的改动。最直观的是javax到jakarta的迁移你原本的代码里import javax.servlet.到3.x全部要改成jakarta.servlet.。如果项目里写了一些传统的Servlet Filter或者拦截器这一波改动会波及很多文件。MyBatis官方starter也一样mybatis-spring-boot-starter在SpringBoot 3.x下需要新版本配合的mybatis-spring版本也要对应升级。数据库驱动mysql-connector-java在SpringBoot 3.x中默认变成com.mysql:mysql-connector-j这个新坐标如果沿用了旧坐标会有依赖冲突。我的建议如果只是用图书管理系统来实践或者做毕设老老实实停在SpringBoot 2.7.x。所有资料、教程、视频里给的代码你都直接能用不用花额外时间做迁移。如果是面试中聊到SpringBoot版本选型你也可以直接回答“稳定优先宁可保守不要激进”。5.3 MyBatis常见“坑”缓存、日志与多参数MyBatis的缓存机制是面试里被问烂了的点也是实际开发中最容易造成困惑的地方。一级缓存是SqlSession级别的默认开启。二级缓存是namespace级别的默认关闭。在图书管理系统中如果开启二级缓存并且对图书表做了更新操作旧缓存不会自动失效就可能出现“数据库已经改了但查询结果还是老数据”的情况。避免这个坑的方法是第一默认不要开启二级缓存MyBatis默认就是关闭的第二如果你确实要开启二级缓存那么增删改操作所在的mapper里一定要配置flushCachetrue。这个经验在实际项目中比理论知识值钱得多很多人一开始都会被缓存问题搞得云里雾里其实关掉是最省心的。SQL日志打印这块在2.2已经提到过。再补充一个细节点控制台打印出来的Preparing和Parameters两行前者是最终SQL骨架后者是参数值。排查“为什么查出来是空”这类问题时把SQL放到数据库客户端手动执行就能区分是SQL本身写错了还是传参传错了。5.4 前端显示PDF与路由刷新404热词里有“vue image能显示pdf吗”这个我在项目中其实遇到过。图书详情、论文附件这类场景前端最常见的错误是用img标签去加载一个PDF文件。img标签确实能打开部分浏览器内置的PDF插件但兼容性不可控尤其在Chrome和Edge上有差异。我的建议是图片用img标签显示PDF文件不要硬塞给img标签而是提供一个按钮或链接通过点击触发window.open(url)打开新页面展示PDF。如果要在页面内嵌预览用iframe或embed标签更为靠谱。具体来说!-- 图片正常显示用img -- img :srcbook.coverImage alt封面 / !-- PDF展示不再用img而是用iframe或window.open -- iframe :srcpdfUrl stylewidth:100%;height:600px/iframe另一个高频问题是路由刷新404。当前端使用history模式时浏览器访问/books这个路由请求会被发送到服务器但SpringBoot并不知道这个路径是什么就返回404。解决办法是在后端加一个转发规则把非API的请求全部转发到index.htmlController public class PageForwardController { RequestMapping(value {/, /books, /borrow, /users, /login, /404}) public String forward() { return forward:/index.html; } // 更严谨的做法是通过WebMvcConfigurer实现view controller }后端加完这一段之后刷新任意前端路由请求都会被转发到index.html由Vue Router接管并渲染正确页面。注意如果前端无法预知所有路由路径可以用一个兜底的WebMvcConfigurer来对非/api路径做转发但不建议把接口路径也纳入转发范围否则后端接口会直接被前端SPA吞掉。5.5 跨域问题排查完整思路跨域问题可能发生在三个层面开发环境、生产环境、浏览器缓存。开发环境我用Vite代理解决生产环境因为前端资源已经打包到jar里同源部署不存在跨域。但如果你选择了前后端分服务器部署前端在Nginx上后端在8080端口那么Nginx需要加一段proxy_pass反向代理配置让/api请求打到后端。这种方式相当于把Vite dev proxy搬到了Nginx上原理是一样的。排查跨域问题时先打开浏览器开发者工具看Network里失败请求的响应。如果响应头里没有Access-Control-Allow-Origin那就是后端没给CORS头如果连请求都没发出去那要检查前端代理配置是不是没生效或写错了target地址。最后分享一点个人体会做完这个项目我最深的感受是前后端分离的核心不是界面有多漂亮而是数据如何规范地流动。你定好了一个接口规范前后端各管一头后面所有页面都按这个规范来写效率会高很多。图书管理系统虽然业务不复杂但它把“增删改查登录鉴权部署上线”这条主流程完整覆盖了。顺着这个项目走一遍你会对SpringBoot和Vue是怎么协同工作的有真正具体的理解而不是停留在概念上。代码再多也会忘但排查问题的思路不会忘。如果大家在做这个项目的过程中卡在某个报错上记得先看后端日志再看控制台网络请求最后看数据库执行记录绝大多数问题都能定位出来。祝各位顺利跑起来也欢迎交流踩坑经历。