
前后端分离的SpringBootVue3MyBatis图书管理系统配上一套MySQL数据库这个组合这几年几乎成了Java全栈学习路径里绕不开的经典项目。不管你是准备毕业设计还是想给自己简历上增加一个有完整链路的全栈项目这套技术栈都值得认真敲一遍。图书管理系统看起来很简单但它把登录、权限、分页查询、关联业务、事务控制、跨域联调这些后端日常高频场景全串起来了前端部分又能把Vue3的组合式API、路由守卫、状态管理、Axios封装这些实战技能练到位。这篇文章我从业务拆解讲起一直讲到后端接口实现、前端页面联调、MySQL环境搭建、部署打包最后把我在实际开发和调试中踩过的坑一并整理出来。文中涉及的代码都是可以直接复用的核心片段不是那种只有结构没有内容的空架子。1. 项目怎么拆搞懂图书管理系统的边界与选型逻辑1.1 先把业务逻辑谈清楚书、人、借还三条主线图书管理系统的核心业务并不复杂归根到底是三条线书的管理、人的管理、书和人的关系管理。书的管理就是图书的增删改查、库存维护、分类管理人的管理就是用户的注册、登录、角色区分书和人的关系管理就是借书、还书、借阅记录、逾期判断。把这三条线理清楚了整个系统的功能模块也就自然浮现出来了。我习惯用一张表来整理模块边界开发的时候照着这个表去建页面、写接口基本不会乱业务模块核心数据关键操作常见页面/接口图书管理图书编号、书名、作者、出版社、分类、库存新增、编辑、删除、分页查询图书列表、图书表单读者管理用户名、姓名、角色、状态注册、登录、禁用/启用登录页、读者列表借阅管理借阅记录、借出时间、应还时间、归还时间借书、还书、续借、记录查询借书页、借阅记录列表统计报表借阅次数、热门图书、库存余量统计查询首页看板、排行榜之所以把“借阅管理”放在核心位置是因为它是这个系统里唯一涉及“多表关联 事务 状态流转”的模块。借一本书要同时更新借阅记录表和图书库存表这个操作如果不用事务包起来就会出现“借阅记录已经生成、但库存没减”这种脏数据。对于学习项目来说这个场景比单纯写CRUD有价值得多。1.2 为什么非要用前后端分离而不是传统的单体页面很多初学者会问一个图书管理系统用SpringBoot模板引擎直接渲染页面不行吗当然可以但“前后端分离”本身就是这个项目要重点练的能力。分离架构的核心收益在于前端和后端可以并行开发后端把接口定义清楚前端用Mock数据先行联调部署的时候前端静态文件可以扔到Nginx后端只负责提供API两边互不干扰。如果是在校学生做毕业设计分离架构还有一个现实的好处论文和工作汇报里能多写一章“前后端联调与部署方案”而且面试时关于跨域、接口设计、打包部署的问题全都覆盖到了。当然代价也很实在——多一套工程多一次跨域配置打包部署的链路变长。所以我的建议是如果你只是想快速跑通一个页面那就别分离如果你想通过这个项目把全栈能力练扎实分离是必须的。1.3 技术选型的理由SpringBoot、MyBatis、Vue3、MySQL8各自解决什么问题SpringBoot选它是因为“约定大于配置”这套理念太适合中小型项目了。内嵌Tomcat、自动装配、起步依赖省掉了大量XML配置和Web容器搭建的琐碎工作。相比早期的SSH架构SpringBoot让开发者能把精力集中在业务代码上这也是为什么现在绝大部分Java岗位要求里都写着SpringBoot。MyBatis和JPA之争每个项目群都能吵半天。我的观点是图书管理系统这种业务SQL逻辑比较直观用MyBatis更合适。MyBatis把SQL控制权完全交给开发者复杂查询、多表联查、SQL调优时可操作空间大而JPA在简单CRUD上虽然快但一旦涉及动态查询和复杂关联反而要花大量精力去处理派生查询方法和Entity关系映射。简单说MyBatis是“你能看见SQL、能控制SQL”这对学习阶段特别重要。Vue3选它是因为组合式API带来的工程化优势。图书管理系统的图书列表、借阅记录、统计看板这些页面内部逻辑都包含“数据请求、筛选条件、分页状态、加载状态”几块内容用Options API写会散落在data、methods、computed三个区域而组合式API能把这些逻辑聚合在同一个setup作用域里。加上Vite开发服务器的热更新速度确实快开发体验比Webpack时代舒服太多。MySQL8.0就不多说了稳定、免费、资料多在本地和服务器上都能跑。需要注意的点是安装时选对字符集utf8mb4后面中文乱码能省掉很多麻烦。2. 后端实现SpringBoot MyBatis的关键细节2.1 工程结构怎么组织接口应该放在哪一层后端工程我建议按“实体层—映射层—服务层—控制层”四层来组织不要图省事把业务逻辑塞Controller里。这个习惯越早养成越好因为后面做权限校验、事务控制、单元测试的时候分层的优势会非常明显。一个标准的包结构大致是这样com.example.library ├── controller # 接收请求、参数校验、返回结果 ├── service # 业务逻辑、事务控制 ├── mapper # MyBatis数据访问接口 ├── entity # 数据库表对应的实体类 ├── vo # 给前端返回的视图对象 ├── common # 统一结果封装、异常处理、工具类 └── config # 跨域配置、拦截器配置Controller只做三件事接收参数、调用Service、返回Result对象。Service负责业务规则比如借书时检查库存、还书时更新状态。Mapper只写SQL和映射不掺业务判断。这样的结构虽然每个文件都很薄但整体逻辑链路清晰出了问题能快速定位是哪个环节。2.2 数据库表设计图书表、用户表、借阅记录表数据库设计是整个项目的基石表字段没定好后面写接口会处处难受。图书管理系统最低限度需要三张核心表图书表book、用户表user、借阅记录表borrow_record。图书表的核心字段是字段名类型说明idbigint主键自增isbnvarchar(32)ISBN编号可加唯一索引titlevarchar(128)书名authorvarchar(64)作者publishervarchar(128)出版社categoryvarchar(64)分类stockint当前可借库存total_stockint总库存create_timedatetime上架时间这里的stock和total_stock是两个概念total_stock表示图书的总量stock表示当前还能借的数量。每借出一本stock减1归还时再加回来。用一个冗余字段而不是每次动态计算查询效率更高这也是实际开发里常见的空间换时间思路。借阅记录表是整个系统里关联关系最核心的表字段名类型说明idbigint主键user_idbigint借阅人IDbook_idbigint图书IDborrow_timedatetime借出时间due_timedatetime应还时间return_timedatetime实际归还时间statustinyint0-借出中 1-已归还 2-逾期status字段是状态流转的关键。每次还书操作都会先查当前记录的状态只有“借出中”的记录才能执行归还避免重复还书造成库存虚增。user_id和book_id上各建一个普通索引就够了这个数据量级别的系统不需要过度优化复合索引在这个场景下收益不大。2.3 核心接口实现分页查询和借还书的事务控制图书列表的分页查询是最常见的接口我用MyBatis手写limit分页。为什么不用PageHelper插件因为手写一次能让你彻底搞明白分页的原理用插件反而像黑盒。分页查询的Mapper XML核心部分如下select idselectPage resultTypecom.example.library.entity.Book select id, isbn, title, author, publisher, category, stock, total_stock from book where if testtitle ! null and title ! and title like concat(%, #{title}, %) /if if testcategory ! null and category ! and category #{category} /if /where order by id desc limit #{offset}, #{pageSize} /select注意这里的分页参数用#{}而不是${}前者是预编译占位符能有效防止SQL注入后者是字符串拼接虽然有时方便但绝不建议用在用户可传的参数上。还有一个细节是concat(%, #{title}, %)不要直接写%${title}%同样是SQL注入的考虑。借书接口是系统里最体现事务控制能力的地方。整个借书流程包含三步查图书库存是否大于0、插入一条借阅记录、把图书库存减1。这三步必须放在同一个事务里任何一个环节失败前面已经做的操作都要回滚。代码实现如下Transactional(rollbackFor Exception.class) public boolean borrowBook(Long userId, Long bookId) { // 1. 查询图书检查库存是否足够 Book book bookMapper.selectById(bookId); if (book null || book.getStock() 0) { throw new BusinessException(图书不存在或库存不足); } // 2. 校验用户是否存在且状态正常 User user userMapper.selectById(userId); if (user null || user.getStatus() ! 1) { throw new BusinessException(用户不存在或已被禁用); } // 3. 生成借阅记录默认借期30天 BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setDueTime(DateUtils.addDays(new Date(), 30)); record.setStatus(0); borrowRecordMapper.insert(record); // 4. 扣减库存 bookMapper.decreaseStock(bookId); return true; }Transactional(rollbackFor Exception.class)这个注解一定要带上rollbackFor参数因为Spring默认只对RuntimeException回滚而业务异常如果是受检异常就会导致事务不生效这个坑我在早期项目里踩过印象特别深。库存扣减这条SQL值得单独说一下update book set stock stock - 1 where id #{bookId} and stock 0这个写法是关键——把“检查库存 0”和“扣减库存”合并到一条SQL里完成通过数据库的行锁和条件更新来防超借。如果在代码里先select再update两个请求同时进来时会读到同一个库存值导致超卖。这个知识点面试时经常被问到能讲明白很加分。2.4 MyBatis配置的几个易错点Mapper扫描、驼峰映射、缓存机制MyBatis在使用中有几个高频坑点。第一个是Mapper接口扫描。要么在启动类上写MapperScan(com.example.library.mapper)要么在Mapper接口上逐个加Mapper注解。漏掉的典型表现是启动报错Consider defining a bean of type ...UserMapper。第二个是驼峰映射。popertiesmap-underscore-to-camel-case: true否则数据库的create_time无法自动映射到实体的createTime字段查出来全为null。我在配置文件里加上mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl注意后面那个log-impl开发阶段建议开启这样控制台会直接打印SQL语句和参数值排查问题效率翻倍。生产环境记得关掉否则日志量太大。第三个是MyBatis的缓存机制。我们平时说的“MyBatis一级缓存”是SqlSession级别的同一个SqlSession内执行相同的SQL第二次会直接从缓存里拿结果不再查数据库。但在Spring集成环境下如果缓存没有及时清空可能出现查询结果不刷新的诡异问题。二级缓存是Mapper级别的需要显式开启在分布式环境下容易踩脏数据的坑。对于图书管理系统这个体量我的建议是不要开二级缓存。数据库只有本地访问缓存带来的性能提升可以忽略不计但引入的脏数据风险却实实在在地增加了排错难度。我还想提一下MyBatis的启动流程这不仅是理解框架的关键也是面试常问。MyBatis启动时会先通过XMLConfigBuilder解析全局配置文件把mappers标签中的Mapper XML路径收集起来再对每个XML进行解析生成MappedStatement对象最后通过MapperRegistry把Mapper接口与对应的XML Statement绑定。映射接口的全限定名必须和XML的namespace一致接口方法名必须和statement的id一致这就是为什么常见报错Invalid bound statement (not found)本质就是绑定环节的对应关系没对上。另外当Spring Boot和MyBatis整合时MybatisAutoConfiguration会帮你完成SqlSessionFactory的创建和Mapper扫描这也是SpringBoot跟MyBatis配合那么顺畅的原因。3. 前端实现Vue3 组合式API怎么落地3.1 项目初始化Vite脚手架和目录规划前端工程我直接用Vite的官方脚手架create-vue创建。相比老一代Vue CLIVite在冷启动速度和热更新速度上的提升非常明显现在和面试官聊前端工程化Vite基本是默认选项。npm create vuelatest创建过程中按需选择Router、Pinia、ESLint即可。创建完成后目录结构按功能整理src ├── api # 接口请求模块 ├── router # 路由配置 ├── stores # Pinia状态管理 ├── views # 页面组件 ├── components # 公共组件 └── utils # Axios封装、工具函数一个很容易忽视的细节是api目录的分层。我习惯每个业务模块一个文件book.js里放图书相关的接口borrow.js里放借阅相关的接口。这样做的好处是接口路径管理集中后端接口一变只需要在一个文件里修改。3.2 组合式APIsetup、ref、reactive到底怎么选Vue3最核心的变化就是组合式API。图书管理系统的图书列表页是一个特别能说明问题的例子——这个页面同时有图书列表数据、搜索关键词、分页信息、加载状态、选中行数据。用Options API写data里一大坨methods里一大堆computed还要单独写改一个功能要来回切换多个区块。用组合式API写代码是按“功能”聚合的script setup import { ref, reactive, computed } from vue import { getBookPage } from /api/book const loading ref(false) const searchForm reactive({ title: , category: }) const pageInfo reactive({ pageNum: 1, pageSize: 10, total: 0 }) const tableData ref([]) const loadData async () { loading.value true try { const res await getBookPage({ ...searchForm, pageNum: pageInfo.pageNum, pageSize: pageInfo.pageSize }) tableData.value res.rows pageInfo.total res.total } finally { loading.value false } } /script这里用到ref和reactive各有各的适用场景。我的经验是基础类型和数组用ref对象用reactive。搜索条件和分页信息这种结构化的对象用reactive很自然而tableData这种需要被整体替换的数据用ref更方便因为tableData.value res.rows这行代码直接替换了引用响应式不会断。很多教程会说组合式API比Options API“高级”其实本质是解决逻辑复用和组织的痛点。图书管理系统的借阅记录查询页面和图书列表页面它们的“数据加载 条件筛选 分页翻页”逻辑几乎一样如果用Options API每个页面都要复制一遍data和methods用组合式API可以把这段逻辑抽成一个useBookList.jscomposable两个页面各自引用代码量直接减半。这才是组合式API真正的价值。3.3 路由配置与登录拦截图书管理系统需要区分管理员和普通读者两种角色前端路由要做权限控制。Vue Router的路由守卫是这里的关键router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(localStorage.getItem(role))) { next(/403) } else { next() } })路由守卫的逻辑其实很简单没有token只能去登录页登录了也不能访问没有权限的页面。但需要注意后端也要做同样的校验前端的路由守卫只是用户体验层面的拦截真正的安全必须依赖后端接口鉴权。路由建议采用history模式还是hash模式开发环境都无所谓但如果前端要打进SpringBoot的static目录里部署history模式刷新页面会404因为后端没有对前端路由做回退处理。如果不打算单独配Nginx直接用hash模式最省心。3.4 Axios封装拦截器里统一处理登录态和错误提示Axios封装是每个Vue3项目都绕不开的事。图书管理系统里的接口数量不多但如果不封装每个页面单独写一遍请求逻辑和错误处理代码会重复得让人头疼。我一般封装两层实例层和拦截器层。实例层指定baseURL和超时时间const service axios.create({ baseURL: /api, timeout: 10000 })开发环境通过Vite的代理把/api转发到后端端口这个配置放在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })拦截器层做两件事。请求拦截器在请求头上加入token这样后端登录拦截器就能在接口层面做认证响应拦截器统一处理后端返回的结果结构如果后端返回401就跳登录页如果业务code不是0就弹出错误提示。这样每个页面调用接口时只需要关心正常返回的数据错误处理全部收口到拦截器里。4. 环境搭建与部署从空环境到能访问的完整步骤4.1 MySQL8安装与建库字符集和时区是两个最容易被忽视的细节MySQL8的安装方式有很多本地环境可以直接下载安装包。开发环境我更喜欢用Docker干净、便于删除重来docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASElibrary \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0无论哪种方式有两个参数一定要检查。第一是字符集my.cnf里设置character-set-serverutf8mb4第二是时区连接URL里带上serverTimezoneAsia/Shanghai。这两个问题不解决后面大概率会出现中文乱码或者时间字段差8小时的现象。数据库和账号准备完成后需要执行建库脚本。我在项目里会准备一个schema.sql包含建库、建表、插入少量测试数据保证任何人拿到项目都能一键初始化。测试数据很重要没有数据的图书管理系统跑起来后空空荡荡连SQL有没有问题都看不出来。SpringBoot的数据库连接配置spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver4.2 前端打包进SpringBoot静态目录的实现前后端联调完成后为了能直接通过SpringBoot的8080端口访问整个项目可以把前端的构建产物交给后端来托管。具体操作用两步先执行前端打包命令npm run build打包完成后把前端工程dist目录下的所有内容复制到后端工程的src/main/resources/static目录下。然后重新启动SpringBoot浏览器直接访问http://localhost:8080就不需要再单独启动Vite开发服务器了。这个方案的优点是一个服务搞定全部对演示和部署都方便。缺点也很明显前端和后端不再独立部署前后端分离的意义打了一半折扣。所以我把这个方案定位为“演示模式”在开发环境用Vite代理联调在交付演示或简化部署时用静态目录托管。4.3 跨域问题的三种解决方案对比前后端分离开发时跨域问题一定会遇到。前端跑在5173端口后端跑在8080端口没有处理时浏览器会报CORS错误。解决方式有三种适用场景各不相同方案实现位置优点缺点适用场景后端CorsFilterSpringBoot配置类配置简单、全局生效放开了所有域开发阶段快速解决前端Vite代理vite.config.js不依赖后端改动仅开发环境生效日常开发联调Nginx反向代理Nginx配置符合生产规范需要额外部署Nginx正式环境部署我在开发阶段推荐用Vite代理原因很简单后端不用加任何跨域代码改动最小。生产环境推荐用Nginx让前端静态资源请求和后端API请求走同一个域名从根源上消灭跨域问题。如果只在后端加CorsFilter解决一切生产环境接口还是会暴露跨域风险不是一个长久的方案。5. 我踩过的坑前后端联调与运行环境的排查实录5.1 后端高频报错排查表这个项目的技术栈非常主流踩坑概率也相对集中。我把遇到过的典型问题整理成表方便你对照排查错误现象根本原因解决方法Consider defining a bean of type BookMapperMapper没被扫描启动类加MapperScan或Mapper接口加MapperInvalid bound statement (not found)XML文件没打包进classes目录确认src/main/resources/mapper/*.xml路径正确Access denied for user rootlocalhostMySQL密码或主机授权不对执行ALTER USER rootlocalhost IDENTIFIED BY 新密码Table library.book doesnt exist建表脚本没执行先执行初始化建表SQL接口返回时间差8小时连接URL时区配置错误在serverTimezoneAsia/Shanghai后面加时区参数SQL查询结果中文乱码数据库字符集不是utf8mb4改成utf8mb4并检查连接参数characterEncodingutf8其中Invalid bound statement (not found)这个报错我需要单独强调一下因为它特别容易迷惑新手。接口方法、ServiceImpl都写对了但运行时就报“找不到绑定语句”。原因往往是SpringBoot打包时没有把src/main/resources/mapper目录下的XML文件复制到target/classes。排查时先打开target/classes/mapper目录看看有没有对应的XML文件没有的话检查一下pom.xml里的资源目录配置是不是漏掉了xml类型。5.2 前端联调常见问题跨域、白屏、路径错误前端开发调试时最容易遇到的是三个问题。第一个是控制台报跨域错误这对应我们上面说的Vite代理配置确认/api前缀是否在axios请求里正确拼接。第二个是页面白屏没有任何报错。这种情况十有八九是Vue3的创建实例代码有问题或者路由守卫里出现了死循环。比如beforeEach里做token检查时如果token不存在但路由又重定向到了/login而/login的守卫又做了同样判断就会无限循环跳转浏览器最后因为栈溢出停止执行。解决办法是在守卫里加上if (to.path /login) return next()。第三个是路由切换后页面正常但一刷新就404。这是history路由模式在静态部署时的问题排查方法就是看浏览器地址栏如果URL里带#就是hash模式不带#就是history模式。如果不想为这个项目额外配Nginx回退规则直接改用hash模式是最快的解法。5.3 调试经验从三层日志定位问题的高效思路整个项目跑起来之后真正调试时的效率取决于你是否有一套清晰的排查思路。我个人的习惯是“由前到后、逐层剥离”先看浏览器Network面板确认请求发出去了没有、返回状态码是多少再看后端控制台日志重点看SQL日志有没有执行、参数是什么最后看前端解析确认数据结构是否符合页面绑定要求。图书管理系统里那一行log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置就特别有用。SQL日志一开MyBatis生成的SQL语句、传入参数、返回条数全部一目了然。很多你觉得“数据查不到”的问题看了SQL日志就能立刻明白是SQL条件配错了还是参数传了null。比如查询条件里书名传了空串如果XML里写的是title #{title}而不是title like concat(%, #{title}, %)空串也能正常匹配到数据换大写或去掉空格就不行了——这类问题只有日志能最快暴露。6. 项目复盘与扩展方向图书管理系统还能变成什么6.1 面试官最容易追问的六个问题如果你把这个项目写进简历面试官大概率会围绕几个关键点深挖。提前想明白面试时就能有的放矢MyBatis和JPA你选哪个为什么分页查询是怎么实现的limit参数为什么不直接拼字符串借书操作的事务是如何控制的如果库存扣减失败借阅记录会不会残留#{}和${}有什么区别什么场景能用${}前后端分离是怎么解决跨域的生产环境和开发环境分别怎么做Vue3的ref和reactive有什么区别什么场景用哪个这几个问题都能在这个项目里找到具体的答案。我的建议是每个问题都回到自己的代码里去解释而不是背八股文。比如事务控制那题直接说“我在借书Service方法上加Transactional(rollbackFor Exception.class)一旦库存扣减失败会抛异常整个事务就会回滚借阅记录不会落库”——这样的回答明显比罗列概念更有说服力。6.2 这个项目往后还能怎么扩展图书管理系统跑通之后扩展路径很丰富。最简单的升级是引入JWT替换Session模型登录成功后签发token前端请求头带上Authorization后端用拦截器校验。这个改动能让你把SpringMVC拦截器机制和JWT的原理都过一遍。其次是引入Redis做热门图书排行榜的缓存。借还书时更新借阅计数定时把计数同步到Redis的ZSet前端排行榜接口直接从Redis读。这个扩展能接触到缓存设计、定时任务、数据一致性等话题项目技术含量直接上一个台阶。再往后可以加Docker部署后端打镜像、前端打包后由Nginx承载、MySQL用容器跑再写个docker-compose文件一键拉起整个系统。这套方案会把项目交付这个维度补上而且写进简历在“项目管理/部署运维”章节也有东西可写。无论选哪个方向都要克制加功能的欲望先把核心流程跑稳了再谈扩展。写在最后的一点个人体会图书管理系统在别人眼里可能很“老套”但我每次回看从这个项目里学到的东西都觉得很值。它让我把SpringBoot的自动装配、MyBatis的映射原理、事务的生效机制、分页SQL的写法、Vue3的响应式原理、跨域的来龙去脉这些零散的知识点通过一个完整需求串成了一条线。后来我接手公司项目时发现那些线上系统拆开来看无非就是在这些基础能力上叠了更多业务规则和并发场景而已。有个习惯我很推荐大家也试试动手编码之前先花半小时把一个接口的请求方式、路径、入参、出参写在一个Markdown文件里。图书管理系统体量不大这个步骤可能显得多余但等前端、后端、测试三方同时介入时这份最原始的接口契约文档会比群聊记录靠谱得多。很多联调时的扯皮说到底就是一开始参数格式没约定清楚。项目可以简单做事的习惯不妨从第一天就开始认真养。