ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3图书管理系统全栈实战:架构、数据库与部署

SpringBoot+Vue3图书管理系统全栈实战:架构、数据库与部署 开头部分一个完整的图书管理系统是Java程序员成长路上绕不开的经典项目。这次我打算把 SpringBoot Vue3 MyBatis MySQL 这套前后端分离的“智慧图书管理系统”源码从技术选型到落地部署全部拆开讲清楚。不管你是准备做毕业设计、课程设计还是想系统梳理一下全栈开发的核心技能点这篇文章都能给你一条可以直接照抄的路径。项目本身不复杂但涉及的知识面很广后端有 SpringBoot 的分层架构、MyBatis 的映射与分页、JWT 的登录鉴权前端有 Vue3 的组件化、路由守卫、Axios 封装再加上 MySQL 的表设计与 SQL 优化。把它们串起来的过程其实就是一次很完整的全栈实战训练。1. 整体架构设计与技术选型思路1.1 为什么选前后端分离架构技术选型这块我在设计之初就确定要走前后端分离。传统 JSP Servlet 那套页面和后端代码揉在一起改个按钮样式都得重启服务维护成本太高了。前后端分离之后后端只负责提供 JSON 数据接口前端只管渲染页面和交互两边可以并行开发——前端不用等后端写完接口后端也不用等前端画完页面各干各的最后联调对接即可。这个项目里我把前端放在 8080 端口跑 Vite 开发服务器后端在 8081 端口跑 SpringBoot开发阶段通过 Vite 的代理配置把/api开头的请求转发到后端生产环境则用 Nginx 托管前端静态文件并做反向代理。这样既解决了开发时的跨域问题也模拟了真实的生产部署形态。1.2 技术栈选型的理由与取舍后端选 SpringBoot 2.7.x 而不是 SSM 手写配置核心原因只有四个字开发效率。SpringBoot 的自动配置机制省掉了一大堆 XML 配置内嵌 Tomcat 也让部署变成一条java -jar命令。Spring Boot 不是把 SSM 抛弃了而是把 SSM 从“需要你手动拼装”变成了“开箱即用”的组合。而 MyBatis 作为持久层框架我特意选了它而不是 JPA原因是图书管理系统这种场景里有大量多表关联查询和动态条件查询MyBatis 的 SQL 完全可控调优起来心里有数。JPA 那种自动生成 SQL 的方式确实快但碰到复杂查询时 SQL 往往不是你想要的还得去拼接 Specification反而绕远路。前端这块Vue3 Vite Pinia Vue Router 是当前 Vue 生态的标准组合。Vue3 的组合式 API 比 Vue2 的选项式 API 在逻辑复用上强太多一个 useBookList 函数就能把图书列表的加载、搜索、分页逻辑全部封装起来多个组件共用起来非常舒服。Vite 的开发服务器冷启动快到飞起热更新也是毫秒级的比 Vue CLI 时代的 Webpack 体验好太多。MySQL 作为数据库是顺理成章的选择图书管理系统这种读多写少的业务场景MySQL 的 InnoDB 引擎在并发读性能和事务支持之间取得了一个很不错的平衡。配合 MyBatis 的二级缓存热点数据查询的性能还能再上一个台阶。2. 数据库设计与MyBatis核心实践2.1 图书管理系统的表结构设计数据库设计是一切的根基表结构没设计好后面写多少代码都是白搭。图书管理系统最核心的表有四张用户表sys_user、图书表book、图书分类表book_category和借阅记录表borrow_record。用户表里我特意加了一个role字段区分管理员和普通读者。这里有个小建议角色字段用 int 类型存储0 表示管理员1 表示普通用户不要直接存字符串。一方面省存储空间另一方面程序里判断逻辑也更直观而且后续如果要扩展角色加数字枚举值就行不用动表结构。图书表是系统的主角字段设计要放长远看。除了常规的book_name、author、publisher、isbn、publish_date之外我还加了total_count和borrowed_count两个字段。很多人会把“可借数量”直接在页面上用total_count - borrowed_count计算这个做法在数据量小的时候没问题但一旦借阅记录多了每次查询都要 COUNT 一遍 borrow_record 表性能就会下降。我一开始就是这么设计的后来压测发现全表扫描太慢才改成了用冗余字段实时维护两个计数在借阅和归还的事务里同时更新查询时直接读字段性能问题就消失了。借阅记录表 borrow_record 是核心业务表包含user_id、book_id、borrow_time、due_time、return_time、status几个字段。status 字段用 0 表示借阅中1 表示已归还2 表示逾期未还。逾期状态我用了定时任务每天扫描一次当due_time小于当前时间且return_time为空时自动把 status 更新为 2。外键约束我在建表时没有加物理外键只在逻辑层用索引来保证查询效率。原因很实际物理外键在删除和插入时需要额外校验影响写入性能而且在分库分表的场景下物理外键基本就是摆设。生产级别的系统更倾向于用应用层逻辑保证数据一致性而不是把压力推给数据库。2.2 MyBatis映射文件与动态SQL实战MyBatis 的强大之处在于写 SQL 的灵活性。这个项目里图书列表查询是最典型的动态 SQL 场景——搜索条件可能有书名模糊查询、分类筛选、出版社精确匹配用户不会每次都填全所有筛选条件所以不能写死 SQL。select idselectBookPage resultTypecom.example.entity.Book SELECT b.*, c.category_name FROM book b LEFT JOIN book_category c ON b.category_id c.id where if testbookName ! null and bookName ! AND b.book_name LIKE CONCAT(%, #{bookName}, %) /if if testcategoryId ! null AND b.category_id #{categoryId} /if if testpublisher ! null and publisher ! AND b.publisher #{publisher} /if /where ORDER BY b.create_time DESC /selectwhere标签的好处是它自动处理了第一个条件前的 AND 关键字——如果你第一个条件没传它不会在 WHERE 后面留下一个裸奔的 AND。很多人第一次写动态 SQL 会踩这个坑直接写WHERE 11再加条件性能上虽然影响不大但看着实在不够优雅。项目里我把所有 SQL 都写在 XML 映射文件里而不是用注解。注解写 SQL 确实快但一旦 SQL 长了字符串拼接在 Java 代码里既难看又难调优XML 里可以有注释、有格式化的缩进还能在测试工具里直接捞出来执行排查问题。2.3 MyBatis分页插件与缓存实战分页这个功能我要重点讲一下 MyBatis 里 PageHelper 的用法因为这是面试八股文里必然会出现的知识点。PageHelper 是一个基于 MyBatis 拦截器机制实现的分页插件原理是在 Executor 执行 SQL 前拦截自动把原始 SQL 改写成带 LIMIT 的分页 SQL然后再查询一条 COUNT 语句统计总条数。使用方式非常简单在查询前调用一行代码PageHelper.startPage(pageNum, pageSize); PageBook page bookMapper.selectBookPage(bookQuery); long total page.getTotal(); ListBook books page.getResult();这里有个致命细节我必须强调PageHelper.startPage()只对紧接着的下一条 SQL 生效。如果你在 startPage 和 mapper 调用之间插入了其他业务逻辑或者其他查询语句分页就会作用到错误的 SQL 上。我早期就犯过这个错在 startPage 之后调了一个统计方法获取某个数据结果分页跑到统计 SQL 上去了查出来的数据完全错乱。排查了好久才定位到原因。另外还要提醒一点PageHelper 返回的 Page 对象本身继承自 ArrayList所以你不要手动再去 new 一个 ArrayList 来接收结果否则total就丢失了。MyBatis 的缓存机制也是面试高频考点。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 中执行两次完全相同的 SQL第二次会直接走缓存。但注意如果你在同一个 SqlSession 中先查询再做了更新操作一级缓存会被清空。二级缓存是 namespace 级别的默认关闭需要手动开启。图书列表这种热点数据非常适合开二级缓存——多个用户查询同一份图书数据时第二次起就不再查数据库了直接命中缓存。不过二级缓存有个使用禁忌如果同一个 namespace 下有更新操作缓存会全部失效。所以我把图书分类这类几乎不变的数据单独放在一个 mapper 里开启二级缓存借阅记录这类高频写的数据坚决不开。3. 后端核心模块实现3.1 SpringBoot分层架构与控制层设计后端代码组织上我严格遵循 Controller - Service - Mapper 三层架构。Controller 只负责接收参数和返回结果不写业务逻辑Service 层处理业务规则比如借书时要判断库存是否充足、用户是否已有逾期未还的书Mapper 层只做数据库交互不掺业务判断。这样分层最大的好处是测试和排查问题方便。举个实际场景用户反馈“借书接口报错”如果是传统写成一坨的代码你得从头到尾捋一遍分层之后你可以先看 Controller 是否正常接收参数再看 Service 层的业务规则哪个校验没通过最后才轮到排查 SQL 问题定位效率高出一大截。Controller 层我全部返回统一响应体ResultT结构是 code、message、data 三个字段。code 为 0 表示成功非 0 表示各种异常码。这样前端 Axios 拦截器统一判断 code 是否为 0 即可决定走成功还是失败回调避免每写一个接口都重复做异常判断。PostMapping(/borrow) public ResultVoid borrowBook(RequestBody BorrowRequest request) { borrowService.borrowBook(request.getUserId(), request.getBookId()); return Result.success(); }3.2 JWT登录鉴权与密码安全登录鉴权这块我选了 JWTJSON Web Token方案。JWT 本质上是一个经过签名的 JSON 字符串服务端无需存储 session用户拿着 token 来请求服务端验签通过即信任该用户身份。这在前后端分离架构下天然的友好——不需要处理跨域情况下的 Cookie 传递问题也方便后续做移动端接入。JWT 的 header 里存放签名算法payload 里存放用户的 id、用户名、角色最后用密钥做 HMACSHA256 签名。我生成 token 时设置了 24 小时的过期时间前端在 Axios 请求拦截器里自动从 localStorage 取出 token 放到 Authorization 请求头后端用拦截器统一校验。密码存储必须强调一定不要用明文也不要用简单的 MD5。MD5 虽然不可逆但彩虹表攻击太容易破解。我用了 BCrypt 加密它内部自带随机盐每次加密同一个密码得到的结果都不一样安全性比 MD5 高一个量级。用户注册时用BCrypt.hashpw()加密存储登录校验时用BCrypt.checkpw()比对Spring Security 里自带这个工具类单独抽出来用也很方便。拦截器里放行白名单也值得说下登录接口、注册接口、静态资源路径需要放行其余请求全都要校验 token。校验逻辑很简单——判断请求头里有没有 Authorization、token 能否解析成功、解析出的用户是否存在于数据库。注意要捕获 JWT 解析时的异常防止伪造 token 直接穿透拦截器。4. Vue3前端构建与前后端联调4.1 Vite项目搭建与Axios请求封装前端部分我用 Vite 脚手架初始化了 Vue3 项目。这里要说个经验Vite 很挑 Node 版本建议用 Node 16 以上的 LTS 版本否则启动时会报一堆莫名其妙的错误。项目装好之后第一件事就是配置路径别名把指向src目录这样组件里引用文件路径就不用一长串相对路径了。Axios 封装是前端工程化的关键一步。我单独写了一个request.js工具文件创建 Axios 实例时统一配置 baseURL 和超时时间然后在请求拦截器里加上 token在响应拦截器里统一处理错误码。响应的 code 如果非 0直接弹出提示HTTP 401 状态码则清除本地 token 并跳回登录页。这样一来业务组件里只需要关心成功数据异常处理全部集中收敛。const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config })4.2 Vue3组件化设计与路由守卫图书列表页是前端最核心的页面。我用组合式 API 把逻辑抽成了一个useBookList函数里面封装了列表加载、分页切换、搜索条件重置等逻辑。组件模板只负责展示数据逻辑全在 composable 里其他页面如果也要展示图书数据直接复用这个函数就行。这就是组合式 API 对比选项式 API 最大优势的实际体验。页面结构上图书列表用了 Element Plus 的表格组件配合分页组件 Pagination。搜索区域是三个条件书名输入框、分类下拉框、出版社输入框点击搜索按钮重新从第一页加载数据点击重置按钮清空条件并重新加载。路由守卫是 Vue3 前端安全的第一道防线。全局前置守卫里判断目标路由是否需要登录权限如果用户没登录直接重定向到登录页并带上 redirect 参数方便登录后跳回原页面。管理员专属页面还要额外校验用户的角色字段角色不对直接跳到 403 页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })4.3 开发环境下联调的代理配置与跨域处理前后端联调是必经之路。开发环境下前端在 8080 端口后端在 8081 端口想让前端请求直接打到后端跨域问题必须先解决。Vite 的server.proxy配置就能优雅地处理server: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }配置好之后前端请求/api/user/login会被 Vite 转发到http://localhost:8081/api/user/login浏览器层面没有跨域这回事了。这里要强调一下changeOrigin必须设为 true否则后端接收到的 Host 头还是前端地址部分严格校验的框架会拒绝请求。生产环境就不靠 Vite 了用 Nginx 做反向代理。前端构建产物dist目录丢到 Nginx 的 html 目录location 块里配置/api/前缀的请求转发到后端服务地址。Nginx 的配置我觉得是每个全栈开发者都应该掌握的技能它不复杂但能解决部署落地的大问题。5. 常见问题与排查技巧5.1 MyBatis常见报错与分页失效场景我建这个项目时踩了无数坑挑几个典型的说一下。第一个是Invalid bound statement (not found)错误。这个报错几乎每个用 MyBatis 的人都会碰到原因通常是 Mapper 接口和 XML 映射文件没有正确关联。排查顺序可以这样来先确认 XML 文件在resources/mapper目录下再确认 XML 文件的 namespace 和接口全限定名完全一致最后确认 application.yml 里配了mapper-locations: classpath:mapper/*.xml。我最早犯的错误就是把 XML 文件放到了 Java 源码目录里编译时没被复制到 classpathMapper 接口根本找不到对应语句。第二个是分页失效问题。PageHelper 分页不生效的高频原因是不满足“startPage 紧接着是查询语句”这个前提。另外还有个隐蔽情况就是查询时 Mapper 方法内部做了多表关联查询返回结果嵌了一个集合属性PageHelper 对这种情况的分页统计不准确。我的做法是分页查询的 SQL 单独写一个不做嵌套集合映射需要详情时再按主键查一次。第三个是数据库连接相关的报错。项目启动时如果报Access denied for user rootlocalhost先检查用户名密码配没配错再检查数据库有没有建出来。如果报Public Key Retrieval is not allowed在 JDBC URL 后面加上allowPublicKeyRetrievaltrueuseSSLfalse这个问题尤其常见于 MySQL 8.0 以上版本。5.2 MySQL 8.0的时区与连接参数问题MySQL 8.0 和 5.7 有几个明显的坑。第一个就是时区问题——你连接的 JDBC URL 会看到这样一长串参数jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezone不配的话高版本 MySQL 驱动会直接报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这个报错第一次见到确实容易慌实际原因就是 JDBC 驱动和数据库服务器的时区信息不匹配指定成Asia/Shanghai就解决了。第二个坑是 MySQL 8.0 默认字符集是 utf8mb4但很多老项目建库时依然用 utf8。utf8 在 MySQL 里最多存 3 字节而 emoji 表情是 4 字节一旦用户昵称里带个 emoji插库直接报Incorrect string value错误。我的处理方式是建库时直接指定CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;写作时这里特别提醒utf8mb4_unicode_ci和utf8mb4_general_ci的区别在于排序和比较规则精度。中文场景推荐utf8mb4_general_ci就行排序更快除非你有德语、法语这类特殊字符需求才需要unicode_ci。5.3 配置文件和依赖冲突排查SpringBoot 项目明明启动成功但访问接口 404这种问题也让人头大。优先查 Controller 类有没有被 Spring 容器扫描到——如果你的启动类在com.example包而 Controller 在com.example.api包它会被扫描到但如果你把 Controller 放到了com.other包那 SpringBoot 默认的包扫描机制就扫不到它。解决方案通常是把启动类移动到项目包的根路径或者用ComponentScan显式指定扫描范围。依赖冲突这块我用 Maven 的mvn dependency:tree命令检查过多次。最典型的是多个 MyBatis 相关依赖版本不一致导致启动报NoSuchMethodError。比如mybatis-spring-boot-starter的版本和pagehelper-spring-boot-starter的版本不兼容运行时后者的拦截器调用前者的某个类方法方法不存在就崩了。遇到这种情况不要瞎猜先把依赖树拉出来看版本号再去官方文档查兼容对照表。5.4 前端控制台常见问题的排查思路前端项目启动时报error:0308010C:digital envelope routines::unsupported这个错通常是因为 Node 版本太高和 Webpack 4 的哈希算法不兼容。但 Vite 项目一般不受影响如果真碰到了用 Node 16 LTS 版本基本能解决。前端页面空白且控制台报路由相关错误大概率是路由模式问题。Vue Router 的 history 模式在生产环境下没有做 Nginx 配置的话刷新非首页会 404。因为 Nginx 默认只找真实文件路径你直接访问/books这个前端路由服务器去找 books 这个文件肯定找不到。解决方法是 Nginx 里配置 try_fileslocation / { try_files $uri $uri/ /index.html; }所有前端路由全部回退到 index.html由 Vue Router 接管后续路由渲染。5.5 数据库查询性能排查清单图书管理系统的数据量虽然不算大但性能优化的习惯要从一开始就养成。排查慢查询的第一件事是打开 MySQL 慢查询日志。在 my.ini 配置文件里加上两行slow_query_log ON long_query_time 1这样执行时间超过 1 秒的 SQL 都会被记录到日志里定位问题就有依据了。我依此发现过borrow_record表在user_id上没有加索引导致用户查询借阅记录时走了全表扫描。加索引时也要有度。不是越多越好索引会占磁盘空间还会拖慢写入速度。优先给 WHERE 条件里高频使用的字段加索引比如sys_user表的username字段、book表的isbn字段、borrow_record表的user_id和book_id字段。组合索引体验也很明显——比如查询条件是“状态 到期时间”建立(status, due_time)联合索引后逾期任务的扫描效率能提高很多。最后分享一个我总结的排查顺序先看代码里有没有循环查询数据库再看索引有没有命中最后才考虑要不要引入缓存。很多时候性能瓶颈不是数据库不够快而是代码写法有问题——比如在循环里一条条查数据这种问题加再多索引都救不回来改成批量查询就好了。
返回列表