ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+MySQL+MyBatis图书管理系统实战拆解

SpringBoot+Vue+MySQL+MyBatis图书管理系统实战拆解 做图书管理系统的人很多但真正把SpringBoot Vue MySQL MyBatis这套组合跑通、跑顺、还能交付成完整源码的其实没那么容易。花了一周时间把这个项目从零到一搭完从数据库建模、后端接口、前端页面到线上部署走了一遍全流程踩了不少坑也沉淀了一些经验。这篇东西就是把整个设计和实现过程完整拆开来讲适合正在做课设、毕设、或者想快速上手前后端分离项目的朋友参考。先说这个项目能干什么图书信息录入与维护、分类检索、库存管理、读者管理、借书还书、超期判断、借阅统计再加上管理员登录和权限控制日常图书大厦的管理场景基本都覆盖到了。技术侧就是SpringBoot做后端RESTful接口Vue做前端单页应用MySQL存数据MyBatis负责数据库交互。整体架构不算复杂但麻雀虽小五脏俱全从前端交互到后端事务处理都有涉及。1. 项目整体设计与需求拆解1.1 核心业务场景分析图书大厦的管理场景拆开来看就是几个核心动作书要能进系统人能借到书书要能还回来管理员要看得见整体情况库存不能对不上账。所以业务模型围绕四个基础对象展开——图书、读者、借阅记录、系统用户对应到数据库就是四张核心表。借阅业务流程是我在设计时最先定的。读者选书后管理员登记借出系统校验读者是否有未还的书、图书库存是否够借出后图书库存减一同时生成一条借阅记录并带上应还日期。还书时库存加回来借阅记录的状态改成已归还。如果超过应还日期还没还系统需要在列表和统计里给出超期标识。这套流程不复杂但状态流转和库存联动的逻辑必须提前想清楚否则写代码时很容易漏。功能模块我最终拆成了五个登录认证、图书管理、读者管理、借阅管理、数据统计。图书管理包含图书的增删改查、分类筛选、模糊搜索、库存展示读者管理是读者信息的维护和身份状态的查看借阅管理是整个系统的核心包含借书、还书、续借、超期标记和借阅历史数据统计则提供图表化的图书分类占比、借阅趋势、热门图书排行。1.2 功能模块划分与角色权限设计这个系统有系统管理员和图书管理员两类角色权限上做了区别。系统管理员拥有全部功能权限包括用户账号的创建与重置图书管理员的账号也在他手里。图书管理员能操作图书、读者、借阅这些日常业务但不能管理系统用户。实现上是登录后返回角色字段前端根据角色渲染不同菜单后端在拦截器里对关键接口做角色校验。这种设计不算高级但够用也比完全不做权限控制的课设项目更有说服力。登录认证我用JWT实现。用户登录成功后后端生成一个带过期时间的token返回给前端前端存到localStorage里之后的每次请求都在Authorization请求头里带上。后端写了一个拦截器统一解析token并校验登录状态拦截器放行了登录接口和静态资源路径其余接口全部走校验。这种方案在前后端分离项目里非常主流比用Session简单也不需要考虑分布式环境下的会话共享问题。1.3 为什么选择前后端分离架构做这个项目时很多人会纠结要不要直接用Thymeleaf做服务端渲染省事又简单。我选择前后端分离核心原因是这套架构更贴近目前企业级开发的真实形态。后端只负责提供JSON数据接口前端独立开发和部署两边并行推进互不阻塞。图书管理这种业务后端接口基本就是CRUD前端写好页面调接口就行分工非常清晰。前后端分离带来的另一个好处是调试效率高。前端可以启动在Vite开发服务器上通过proxy把接口请求转发到后端8080端口改前端代码会热更新不用重启后端服务。后端也可以直接用Postman或Apifox调试接口不用非得起前端页面。项目开发过程中我把接口文档整理在Apifox里前后端联调时直接对着文档测少了很多口口相传的歧义。2. 数据库设计与后端技术栈选型解析2.1 核心表结构设计与索引规划数据库表我设计了五张book图书表、reader读者表、borrow借阅表、sys_user系统用户表、book_category图书分类表。为什么不把分类直接塞在图书表里用字符串存而是单独建分类表因为分类会反复用于筛选和统计单独建表可以用分类ID做关联和分组数据一致性更好也方便后续扩展分类层级。book图书表核心字段字段名类型说明book_idbigint主键自增图书IDisbnvarchar(20)ISBN编号book_namevarchar(100)书名authorvarchar(50)作者category_idbigint分类ID关联分类表publishervarchar(100)出版社pricedecimal(10,2)定价stockint库存数量shelf_locationvarchar(50)架位号statustinyint状态 0-下架 1-在售create_timedatetime录入时间borrow借阅表是整个系统里最需要设计好的一张因为借阅记录的查询非常频繁——要查某读者当前借了哪些书、某本书被谁借走了、逾期记录有哪些。这张表的索引设计我在写完数据后做过一次调整先在isbn和borrow_date上建了联合索引又在reader_name和book_name上加了普通索引查询效率提升很明显。borrow核心字段字段名类型说明borrow_idbigint主键自增借阅IDbook_idbigint图书IDreader_idbigint读者IDborrow_datedatetime借出日期due_datedatetime应还日期return_datedatetime可空实际归还日期statustinyint0-借出中 1-已归还 2-已逾期operator_idbigint经办管理员ID2.2 MyBatis还是JPA选型背后的逻辑后端做持久层时很多人会在MyBatis和Spring Data JPA之间犹豫。我选择MyBatis而不是JPA一个重要原因是这个项目里有不少查询需要手写SQL来控制比如借阅统计里的分组聚合、多表关联查询、动态条件筛选。MyBatis允许你精确控制每个SQL语句结合动态SQL标签可以灵活拼装查询条件这在管理系统的统计报表场景下特别方便。JPA的Criteria API写复杂查询时确实绕一旦碰到多表关联加动态条件读起来非常痛苦。但这不意味着MyBatis没有缺点。最典型的问题是写了大量XML映射文件每个实体都要维护对应的Mapper接口和XML表结构一旦变更XML里的字段名都要手动同步。另外MyBatis默认的驼峰映射配置要自己开否则数据库的下划线字段名比如book_name和Java的驼峰属性bookName对不上查询结果全是null。我项目里一开始就漏了这件事排查了好久才发现是mapUnderscoreToCamelCase没开启。2.3 后端代码分层与统一返回结构后端包结构我用的是经典的controller/service/mapper/entity四层。Controller层只做参数接收和结果包装不写任何业务逻辑Service层承载业务判断和事务控制Mapper层负责SQL操作entity里是数据库对应的实体类。这个分层让代码职责非常清晰功能扩展时基本不需要大改。为了让前端处理接口返回值时不至于混乱我封装了一个统一的Result类数据结构是code、message、data三个字段。code为200表示成功400表示参数错误401表示未登录或token过期500表示服务端异常。前端axios拦截器里统一判断code非200统一弹提示不用每个页面各写一遍错误处理逻辑。前端只要看到接口返回值结构一致联调效率高出不少。Controller示例RestController RequestMapping(/api/book) public class BookController { Resource private BookService bookService; GetMapping(/list) public Result list(RequestParam(required false) String bookName, RequestParam(required false) Long categoryId, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageResultBookVO page bookService.queryBookPage(bookName, categoryId, pageNum, pageSize); return Result.success(page); } PostMapping(/add) public Result add(RequestBody BookDTO bookDTO) { bookService.addBook(bookDTO); return Result.success(); } }2.4 事务控制在借阅流程中的应用借书操作不是一条insert就能完事的。整个过程涉及三步插入借阅记录、扣减图书库存、校验读者借阅状态。这三步必须保证原子性——任何一步失败前面的操作都要回滚否则就会出现数据不一致的情况比如借阅记录已经生成但库存没减或者库存减了但记录没插入。我在Service方法上加了Transactional注解来实现事务控制。借书核心代码逻辑Override Transactional(rollbackFor Exception.class) public void borrowBook(BorrowDTO borrowDTO) { // 1.校验读者和图书是否存在 Reader reader readerMapper.selectById(borrowDTO.getReaderId()); Book book bookMapper.selectById(borrowDTO.getBookId()); if (reader null || book null) { throw new BusinessException(读者或图书不存在); } // 2.校验库存是否充足 if (book.getStock() 0) { throw new BusinessException(图书库存不足); } // 3.校验读者是否有未还的借阅记录 int unReturnCount borrowMapper.countUnReturnByReaderId(borrowDTO.getReaderId()); if (unReturnCount 5) { throw new BusinessException(该读者已有5本未归还图书无法继续借阅); } // 4.插入借阅记录默认借期30天 Borrow borrow new Borrow(); borrow.setBookId(book.getBookId()); borrow.setReaderId(reader.getReaderId()); borrow.setBorrowDate(new Date()); borrow.setDueDate(DateUtil.offsetDay(new Date(), 30)); borrow.setStatus(0); borrowMapper.insert(borrow); // 5.扣减库存 bookMapper.decreaseStock(book.getBookId()); }这段逻辑里我加了“最多同时借阅5本”的业务限制。这种限制在真实业务里很常见做毕设时加一个这样的校验答辩时会更有说服力从“能跑”变成了“有业务逻辑”。3. 后端核心功能实现与难点解析3.1 图书分页查询与动态条件组合图书列表页是前端最核心的页面要支持按书名、作者、分类、状态等条件组合筛选还需要分页。这里我用MyBatis的动态SQL来实现核心思路是传入的查询条件对象Query对象里哪个字段不为空就拼接对应的WHERE条件。MyBatis的if标签天然适合这个场景。Mapper XML示例select idqueryBookPage resultTypecom.book.vo.BookVO SELECT b.*, c.category_name FROM book b LEFT JOIN book_category c ON b.category_id c.category_id where if testbookName ! null and bookName ! AND b.book_name LIKE CONCAT(%, #{bookName}, %) /if if testauthor ! null and author ! AND b.author LIKE CONCAT(%, #{author}, %) /if if testcategoryId ! null AND b.category_id #{categoryId} /if /where ORDER BY b.create_time DESC LIMIT #{offset}, #{pageSize} /select分页逻辑我用的是手写LIMIT而不是PageHelper。原因是这个查询里有LEFT JOINPageHelper在某些复杂SQL场景下解析count语句会出错特别是带group by或子查询的时候会出现count统计结果不对的情况。手写LIMIT虽然要多写一条count查询但SQL完全可控排查问题也方便。3.2 借阅超期自动判定与列表展示逾期判定我每天通过一个定时任务扫描borrow表把status为0且due_date小于当前时间的记录标记为2已逾期。这里的关键是定时任务不能影响正常业务所以我用Spring的Scheduled注解给这个方法加了个每天凌晨两点执行的计划。还有一个实时判定逻辑前端页面展示借阅列表时如果一条记录的status是0但due_date已经过了后端在返回数据时会把status转成2返回前端保证用户刷新页面时看到的逾期状态是即时的。Scheduled(cron 0 0 2 * * ?) public void updateOverdueStatus() { Date now new Date(); // status为0且应还时间小于当前时间批量更新为逾期状态 int updated borrowMapper.updateOverdue(now); log.info(定时任务更新逾期记录 {} 条, updated); }超期的标识出来之后前端在借阅列表里把逾期记录的背景标红读者借新书时后端也会做限制只要有逾期未还的书借书功能会被拒绝。这套联动验证了整个流程是闭环的。3.3 统计模块的多表聚合查询Dashboard数据看板是我写SQL最多的地方。图书分类占比、近30天借阅趋势、热门图书Top 5、当前逾期数量这几个统计指标分别用不同的SQL实现。热门图书排行用GROUP BY加COUNT和LIMIT就能实现借阅趋势则是将borrow_date按天分组统计从业务数据里看出哪几天系统活跃。-- 热门图书Top5 SELECT b.book_id, b.book_name, COUNT(br.borrow_id) AS borrow_count FROM borrow br JOIN book b ON br.book_id b.book_id WHERE br.borrow_date DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY br.book_id, b.book_name ORDER BY borrow_count DESC LIMIT 5;这里有个容易犯的错直接SELECT b.book_id, b.book_name再GROUP BY br.book_id有些MySQL版本开了only_full_group_by模式会报错所以查询字段必须和分组字段保持一致或者用聚合函数包起来。我在写这个SQL的时候就踩了一版后来把SELECT里的字段调整成和GROUP BY一致才解决问题。3.4 MyBatis参数映射与主键回填的坑MyBatis在参数传递上有几个细节值得说。当Mapper方法传入多个参数时如果不加Param注解SQL里直接用#{bookName}会报错因为MyBatis无法识别参数名字。我项目的做法是凡是查询方法参数超过一个就统一封装成实体类或Query对象或者在方法签名处加Param注解避免在XML里写Arg0、Param1这种可读性极差的占位符。另外新增图书时需要拿到插入后的主键ID因为后面可能会用这个ID做关联操作。MyBatis支持通过useGeneratedKeys和keyProperty实现主键回填这个配置被很多人忽略以至于插入后还想用bookId时发现是null。我项目里没踩这个坑但刚学MyBatis的时候确实遇到过当时查了半天没查出原因。insert idinsertBook parameterTypecom.book.entity.Book useGeneratedKeystrue keyPropertybookId INSERT INTO book (isbn, book_name, author, category_id, publisher, price, stock, shelf_location, status) VALUES (#{isbn}, #{bookName}, #{author}, #{categoryId}, #{publisher}, #{price}, #{stock}, #{shelfLocation}, #{status}) /insert4. Vue前端实现与关键交互设计4.1 前端工程化搭建与依赖选型前端我用的是Vue3 Vite Element Plus这套组合。Vue3的Composition API配合setup语法糖代码组织方式更灵活特别是图书列表这类逻辑集中、状态较多的页面用ref和reactive管理响应式数据比Vue2的data选项清晰很多。Element Plus是全套的组件库表格、弹窗、表单、下拉菜单都有现成组件一个管理后台的UI基本不需要额外开发样式。前端项目结构src/ ├── api/ # 接口请求封装 │ ├── book.js │ ├── borrow.js │ └── login.js ├── router/ # 路由配置 ├── store/ # Pinia状态管理 ├── views/ │ ├── login/ # 登录页 │ ├── dashboard/ # 数据统计 │ ├── book/ # 图书管理 │ ├── reader/ # 读者管理 │ ├── borrow/ # 借阅管理 │ └── user/ # 系统用户 ├── utils/ │ └── request.js # axios二次封装 └── App.vue这里我提一下Vite的安装和配置。用npm create vuelatest初始化项目选择需要的功能模块创建完执行npm install安装依赖。Vite开发服务器的端口默认是5173配置proxy代理把/api开头的请求转发到后端的8080端口这样开发时完全不需要处理CORS跨域问题。4.2 登录交互与路由访问控制登录页的表单校验是本系统的重要交互之一。用户输入账号密码后前端调用登录接口后端校验通过返回token和用户信息。前端把token存储到localStorage调用接口时通过axios请求拦截器自动附加在请求头的Authorization字段中。路由守卫是前端权限控制的关键。我在router里配置了路由meta信息{ path: /book, name: BookList, component: () import(../views/book/index.vue), meta: { requiresAuth: true } }然后在router.beforeEach里判断如果访问的页面需要登录且本地没有token跳转到登录页。如果有token但当前访问的是登录页跳转回首页。这套逻辑写一次全局生效不需要在每个页面单独判断登录态。4.3 图书管理页面的增删改查与分页交互图书列表页是整个项目里最典型的交互场景表格展示、条件搜索、分页导航、新增和编辑弹窗、删除确认这套模式在管理后台中极具代表性。页面顶部的筛选区包含书名输入框、分类下拉框、状态下拉框和查询/重置按钮点击查询按钮时把筛选条件传给后端接口重新拉取数据。Element Plus的el-table自带loading属性和空数据展示配合后端返回的分页数据我能用较少的代码实现完整的分页表格。新增和编辑共用一个el-dialog弹窗根据editMode判断是新增还是编辑模式提交成功后刷新列表。删除操作使用el-popconfirm确认弹窗避免误删数据引发不可逆问题。Axios的响应拦截器里我统一做了错误提示不需要每个页面catch一遍。token过期时后端返回401拦截器里做登出处理并跳转登录页。这套封装在实际开发中非常实用任何页面出现权限问题都能自动跳转不需要用户手动清除缓存。4.4 前端打包与路由模式的处理前端开发完成后需要打包部署。执行npm run build会把代码打包到dist目录这个目录里是不需要额外配置的静态HTML、JS和CSS文件。前端路由我默认用history模式但history模式有个典型坑如果直接把dist扔进SpringBoot的static目录刷新一个深层页面比如直接访问/book/3时后端找不到这个路由对应的controller会报404错误。解决方式有两种。第一种是前端打包改成hash模式URL里会多一个#号刷新时不会发请求到后端简单可行但URL不太美观。第二种是后端写一个转发规则把非API且非静态资源的请求都转发到index.html。我项目里用的是第二种方案增加了一个路由控制器Controller public class ForwardController { RequestMapping(value {/, /login, /dashboard, /book, /reader, /borrow, /user}) public String forward() { return forward:/index.html; } }这样刷新任意前端页面都能正常显示。如果有条件部署到Nginx上用try_files指令也能实现同样的效果个人部署时直接把整个dist目录挂到服务器上更稳定。5. 环境搭建与前后端联调全过程5.1 数据库初始化与连接配置数据库初始化方式我用了两种一种是用Navicat可视化工具导入SQL脚本适合不熟悉命令行的初学者另一种是直接用MySQL命令行执行SQL文件适合在服务器上快速部署。核心要点是务必将编码设置为utf8mb4否则中文数据进去后查询出来是乱码。CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_manager; SOURCE /path/to/book_manager.sql;后端连接MySQL时我建议在url上明确指定以下参数spring: datasource: url: jdbc:mysql://localhost:3306/book_manager?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/Shanghai这个参数在MySQL 8.0版本下尤为重要如果不加系统时间和数据库时间会有时区差异日期字段的读写会出现8小时偏差。allowPublicKeyRetrievaltrue则用于解决MySQL 8.0连接时出现的公钥检索异常。5.2 后端启动与常见依赖冲突SpringBoot项目使用Maven管理依赖我在pom.xml中配置了web、mybatis、mysql驱动、jwt、lombok等依赖。这里有个非常容易踩的坑Lombok版本和JDK版本不兼容启动时会直接报错提示annotationProcessor相关的错误。SpringBoot版本我选的是2.7.x这个版本相对稳定兼容JDK8到JDK17绝大多数教程和依赖都适配。如果要选SpringBoot 3.x要注意JDK必须用17及以上而且部分老版本依赖需要升级网上很多旧教程里的写法会直接报错。还有一个版本问题是Maven依赖树中的冲突。如果项目里同时引入了不同版本的mybatis-spring-boot-starter和mysql-connector-j驱动可能出现无法创建SqlSessionFactory之类的异常。使用mvn dependency:tree命令可以清晰地查看依赖树定位冲突的来源。5.3 前端代理配置与跨域解决开发阶段前端和后端分别跑在5173和8080端口跨域是绕不开的问题。前端真正的跨域解决方案是使用Vite的server.proxy配置server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }配置完成后前端请求/api/login时Vite开发服务器会把它转发到http://localhost:8080/api/login浏览器里看到的始终是同源请求不会触发跨域限制。后端也要配合做两件事一是在后端配置跨域过滤器允许前端开发服务器的origin访问二是拦截器里要把OPTIONS预检请求直接放行否则跨域请求在正式发出前就会被拦截。5.4 前后端接口联调的关键节点联调是整个项目里最考验耐心的环节。我的做法是先用Apifox把后端所有接口全部测通并保存成接口文档再让前端对着文档编码。联调期间前端出现的常见问题可以整理成一个速查表后期排查问题时非常有用。接口联调中出现最多的错误是接口返回结构和前端预期不一致。例如后端Result类里的data字段是null前端直接通过res.data.xxx访问属性时会报错但这不一定是后端bug可能是前端漏传参数导致后端查询结果为空。联调时要养成先看Network面板中原始JSON返回的习惯不要直接根据控制台报错就假设是哪边的错。拿事实数据说话这是前后端协作的基础。6. 常见问题排查与避坑经验6.1 MyBatis常见坑位详解MyBatis的缓存机制是很多人容易忽视的知识点。MyBatis默认开启一级缓存作用范围是同一个SqlSession。如果在一个事务里先查询一条数据更新这条数据后再次查询第二次查询如果不做缓存清理可能会查到更新前的旧数据。我在图书编辑功能里就遇到过用户修改图书信息后页面仍然显示旧数据原因是一级缓存没有清理。解决方式是在更新操作后调用sqlSession.clearCache()或者确保每次操作都使用新的SqlSession。二级缓存需要显式配置默认关闭如果配置了二级缓存就要注意缓存粒度和失效时机特别是连表查询的结果不适合放进强缓存因为关联表更新时缓存不会自动失效。另一个高频坑位是数据库下划线字段到Java驼峰属性的映射。配置yml里加上mybatis.configuration.map-underscore-to-camel-casetrue后查询字段book_name才能自动映射到bookName属性。默认情况下MyBatis是不能自动做这个转换的很多人的项目里查询出来字段全是null就是因为少了这一步。6.2 前后端联调高频问题速查表问题现象可能原因解决方案POST请求后端收不到参数请求头Content-Type不是application/json前端提交时明确指定JSON格式后端用RequestBody接收查询结果中文乱码数据库连接URL缺少characterEncoding参数URL加上characterEncodingutf8数据库创建时用utf8mb4前端刷新页面404前端history路由模式未处理后端增加转发规则或前端打包改用hash模式接口返回401但token有效拦截器放行路径不完整检查拦截器配置确认登录接口和静态资源已放行MyBatis查询结果字段全为null未开启驼峰映射配置map-underscore-to-camel-casetrue新增数据后拿不到主键IDinsert语句未配置主键回填添加useGeneratedKeys和keyProperty属性6.3 项目部署上线与后续扩展方向项目部署时我采用了本地打包的流程后端使用mvn clean package打成可执行jar包用java -jar book-manager.jar启动服务。前端执行npm run build生成dist目录把dist下所有文件复制到后端resources/static目录下重新打包这样整个系统只需要运行一个jar包就能同时提供接口和页面访问是最简单的单机部署方式。有条件的可以配置Nginx反向代理这样后端api反向代理到8080端口静态资源由Nginx直接平趟性能和配置灵活度都更好。后续如果时间充裕可以做的扩展方向包括用Redis缓存热门图书数据和验证码降低数据库压力引入RabbitMQ处理借阅超期提醒的异步通知增加图书批量导入导出功能用EasyExcel把Excel数据直接导入系统引入WebSocket实现读者借阅状态变化时前端实时刷新。这几个方向无论从业务完整性还是技术深度角度都会让系统提升一个台阶。7. 我对这个项目的几点实际操作体会项目做到最后我最深的感受是管理系统的难点不在某个单点技术上而在把业务闭环打通。图书从录入到借出到归还到统计每一步都有前置校验和后置联动漏掉任何一个环节功能演示的时候就会出洋相。所以开发过程中我把每个业务流程都画了一张简单的状态流转图代码写完后再对着图逐一验证场景是否覆盖完整这个习惯帮我减少了很多返工。再分享一个小技巧开发前后端分离项目时接口文档一定不要最后再补而是每写完一个模块就更新一次文档。联调阶段前端拿着文档做开发后端对着文档自测能在很大程度上减少沟通成本。我项目里用的是Apifox管理接口文档前后端共享一个项目问题反馈直接评论在接口上效率比微信群传话高了不知道多少。这套源码后续如果你要做成自己的项目建议至少做两件事一是把图书分类管理从固定表改成支持多级分类这样更贴近真实图书大厦的图书归类需求二是把借阅流程改成支持预约和续借操作业务丰富度会明显提高。希望这份完整拆解能帮你少走一些弯路有问题随时交流。
返回列表