ARTICLE DETAIL

资讯详情

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

前后端分离图书管理系统实战:SpringBoot+Vue+MyBatis全栈开发与部署

前后端分离图书管理系统实战:SpringBoot+Vue+MyBatis全栈开发与部署 前后端分离的图书管理系统几乎是我每年都会被人问一遍的项目毕业设计、课程设计、全栈练手都绕不开它。SpringBoot 写后端接口Vue 写前端页面MyBatis 处理数据库操作MySQL 存数据这套组合从头到尾跑一遍基本上就能把一个真实业务系统从设计到上线的完整流程吃透。这篇文章我按自己实际做项目的顺序来写设计思路、数据库建模、核心代码、前后端联调、打包部署、踩坑记录所有步骤都是可以直接照着复现的。只要你有一点 Java 基础再懂一点点前端概念应该就能跟下来。1. 项目整体设计与技术选型思路1.1 为什么要做前后端分离而不是把页面和服务端写在一起我早期接触这类系统时见过很多用 JSP 或者 Thymeleaf 模板直接把页面和服务端写在一起的项目。那种方式开发小型网页确实快页面少的时候也简单但只要业务一多问题就很快冒出来前端改一个样式要动整个工程后端接口和页面代码混在同一个目录里分工协作非常痛苦。前后端分离的核心思路是把“数据接口”和“页面展示”彻底拆开后端只负责返回 JSON 数据前端通过 Ajax 请求数据再自己渲染页面。这样做的好处最直观的是开发时可以两边并行。后端同事把接口文档定义好前端直接 Mock 数据开始写页面不需要等后端完成。部署上也能分开前端打包成静态文件交给 Nginx后端打成 Jar 包单独运行任何一个要升级都不需要停掉整个系统。再加上现在 Vue、React 这套前端工程化已经很成熟组件化开发让页面的复用程度很高所以现在做新项目我基本默认就选前后端分离。图书管理系统选这个架构还有一个很实际的原因它涉及的列表、搜索、分页、表单、弹窗这些界面交互恰好是前端框架最擅长的场景。你如果用 JSP 做图书列表的分页刷新整页都要重新加载体验很别扭。Vue 配合组件库在局部更新、表单校验这些方面要舒服得多。1.2 技术栈选型为什么是 SpringBoot Vue MyBatis MySQL选技术栈这件事很多时候不是选“最好”的而是选“最不容易出问题”的。这套组合之所以在图书管理系统这类项目中烂大街是因为它每一步都有非常成熟的解决方案。SpringBoot 解决了 Spring 繁琐配置的问题。早期写 Spring 项目要配置一堆 XML数据源、事务、扫描路径都得手写SpringBoot 用自动配置把这些都接好了写一个 Controller 加几个注解就能把接口跑起来。项目内嵌 Tomcat最终打成一个可执行的 Jar部署也简单。这不正好匹配“快速开发一个管理系统”的需求。Vue 这边前几年带项目多用 Vue2 Element UI这两年新写的基本是 Vue3 Element Plus 或者 Vue3 Vite。这篇内容我默认用 Vue3 来讲如果你电脑上的脚手架还是 Vue2核心思路也完全一样Vue2 的选项式 API 对新手来说其实更直观。MyBatis 是这里最有争议的选择。现在很多人直接用 MyBatis-Plus确实方便内置了单表 CRUD 方法。但我建议学习项目还是先用原生 MyBatis把 SQL 写明白。图书系统的查询条件、多表联查、分组统计这些用 MyBatis 的 XML 文件写 SQL 反而更容易控制和阅读。你面试的时候被问“MyBatis 的 #{} 和 ${} 有什么区别”“分页插件原理是什么”只有自己用过才能答得出来。MySQL 就不需要多讲了免费、轻量、社区资料多图书管理系统这种量级的数据完全够用。整套技术栈都是开源生态里最常见的遇到任何报错一搜基本都有现成的答案。1.3 功能模块划分与角色权限设计了解技术栈之后我会先画功能清单而不是直接写代码。因为这类管理系统的功能其实很固定提前规划清楚模块后面写代码就是往一个个模块里填。我给这个项目划分的功能模块如下模块功能说明访问角色用户管理登录、注册、个人中心、读者列表、启用/禁用管理员图书管理图书列表、新增、编辑、删除按名称/ISBN/分类搜索管理员图书借阅借书、还书、借阅记录查询、逾期列表管理员、读者分类管理图书分类的增删改查管理员统计报表借阅排行、分类占比、每日借阅趋势管理员前端页面登录页、图书浏览、我的借阅、后台管理面板全部这里也顺便说一下标题里的“智慧”体现在哪。这个级别图管系统的“智慧”不是 AI 或者大数据主要体现在数据统计和提醒上后台展示热门图书 Top10、分类借阅占比、每天的借阅趋势读者端能看自己的借阅历史和即将到期提醒。把这几块做了整个系统的完成度会高一个档次。登录接口我设计成 JWT 无状态认证。用户登录成功后后端生成一个带着用户ID和角色信息的 token 返回给前端前端请求接口时在请求头里带上 token后端通过拦截器统一校验。好处是后端不需要维护 session以后有多台后端实例部署时也不需要做 session 共享。角色权限要做两层控制。第一层是路由级前端根据角色生成不同的菜单管理员能看到“图书管理”“读者管理”普通读者只能看到“图书搜索”“我的借阅”。第二层是接口级后端拦截器校验用户角色比如删除图书的接口只允许 admin 调用哪怕前端漏掉了菜单别人直接请求接口也会被拦截。这个“后端必须兜底”的原则做管理系统时一定要牢记。2. 数据库设计与核心业务实现2.1 表结构设计以及建表时容易被忽略的细节数据库设计是整个项目的地基。我建表前习惯先走一遍核心流程读者登录搜索图书提交借阅管理员审核读者还书系统记录借阅历史。把这条流程走完表就自然出来了。这个项目我设计了 6 张表其中 user 表存用户信息读者和管理员共用这张表用 role 字段区分book 表存图书信息关键字段是 total_count 图书总量和 stock 当前可借库存category 表是图书分类borrow_record 表是借阅记录记录哪个用户、什么时间、借了哪本书、有没有归还。建表语句的核心部分如下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, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-读者 1-管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE book ( id int(11) NOT NULL AUTO_INCREMENT, isbn varchar(20) DEFAULT NULL, name varchar(200) NOT NULL, author varchar(100) DEFAULT NULL, publisher varchar(200) DEFAULT NULL, category_id int(11) DEFAULT NULL, total_count int(11) NOT NULL DEFAULT 0 COMMENT 馆藏总量, stock int(11) NOT NULL DEFAULT 0 COMMENT 可借库存, cover varchar(500) DEFAULT NULL COMMENT 封面图URL, description text, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_name (name), KEY idx_category (category_id) ) ENGINEInnoDB 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, return_time datetime DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0-借阅中 1-已归还, PRIMARY KEY (id), KEY idx_user (user_id), KEY idx_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;有几个细节新手容易忽略。一是字符集MySQL 8 默认是 utf8mb4但建库时如果不指定部分旧版本默认是 utf8遇到中文搜索和生僻字很容易出问题。二是索引username 和 book.name 这种高频查询字段要加索引但也不要每个字段都加否则写操作会被拖慢。三是外键我这里没有建物理外键只用普通索引保持逻辑关联。实际项目中物理外键会导致删除数据时出现各种关联限制而且后期不好改所以大多数团队会放弃物理外键。初始数据一定要准备好不然第一次启动后端接口查出来全是空的。我习惯准备两个用户admin/123456管理员、reader/123456读者再插几条分类和图书方便测试。2.2 登录认证与 JWT 拦截器的完整实现登录接口是整个系统的入口。后端生成 JWT 的流程是用户提交用户名密码Service 里用 BCrypt 校验密码校验通过后用用户ID、用户名、角色这三个信息生成 token过期时间一般设 24 小时。退出登录在无状态方案里就是前端删掉 token后端不需要做任何事情。JWT 工具类的核心方法如下public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 24 * 60 * 60 * 1000L; public static String createToken(Long userId, String username, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } }这里有一个取舍要提醒把 role 放进 token 里拦截器校验权限时就不用每次查数据库。但如果用户角色被修改了token 里的旧角色在过期前仍然有效。图书管理系统对实时性要求不高我选择把角色放 token 里。不过禁用用户状态的场景必须额外处理拦截器里只靠 token 无法知道这个用户有没有被禁用所以涉及到状态变化时还得去数据库查一次用户状态。拦截器的注册方式一般是实现 WebMvcConfigurer在 addInterceptors 里把放行路径和校验路径分开registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/user/login, /api/user/register);新手特别容易踩的一个坑是用了拦截器后前端跨域预检请求 OPTIONS 请求也会被拦截导致浏览器报“跨域失败”但后端日志显示接口其实已经调用了。解决办法很简单拦截器里直接放行 OPTIONS 请求if (OPTIONS.equals(request.getMethod())) { return true; }2.3 借书还书的核心业务以及事务为什么这么重要借书还书是图书管理系统的核心业务。代码本身不复杂但它教你一个很重要的概念事务。一个正常的借书操作要同时做三件事检查图书库存、扣减库存、插入借阅记录。如果第二件事成功、第三件事失败那库存被扣了却没有借阅记录账就对不上了。所以这三个操作必须在同一个事务里要么全成功要么全部回滚。后端 Service 层借书方法的大致逻辑Transactional(rollbackFor Exception.class) public void borrowBook(Long userId, Long bookId) { Book book bookMapper.selectById(bookId); if (book null || book.getStock() 0) { throw new BizException(500, 图书不存在或库存不足); } int rows bookMapper.decreaseStock(bookId); if (rows 0) { throw new BizException(500, 库存更新失败请重试); } BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setStatus(0); borrowRecordMapper.insert(record); }这里有个更细节的点为什么不先查一遍库存判断够不够再扣减因为两个用户同时借同一本书、库存只剩一本的时候先查询再扣减会出现超卖。更稳的做法是用数据库的原子更新UPDATE book SET stock stock - 1 WHERE id #{bookId} AND stock 0这条 SQL 的意思是只有当库存大于 0 时才扣减而且数据库的行锁会保证并发请求只有一个能成功。哪怕同时进来十个请求最终也只有一个能扣减成功其他都会返回影响行数为 0。这个思路在处理“抢购”“预约”场景时非常常用图书管理虽然并发低但作为练手项目这种意识值得养出来。还书操作的逻辑刚好反过来根据借阅记录 ID把 status 改成已归还设置 return_time再把图书库存加回去。如果要加逾期功能可以在还书时计算 borrow_time 到 return_time 的天数超过设定的借阅期限就自动生成一条逾期记录或者给用户打一个标记。这个可以作为进阶扩展自己加。2.4 MyBatis 分页插件与缓存哪些能开哪些别碰图书列表、借阅记录列表这些页面都需要分页。我用的方案是 PageHelper它是 MyBatis 最流行的分页插件。原理说穿了很简单在执行 SQL 之前PageHelper 通过拦截器把原来的查询重新包装成带 LIMIT 的查询再自动算总条数把结果塞到 PageInfo 里。用法很简单PageHelper.startPage(pageNum, pageSize); ListBook list bookMapper.selectBookList(book); PageInfoBook pageInfo new PageInfo(list);前端传 pageNum 和 pageSize后端返回 pageInfo 里的 total、list、pages 这些字段。但 startPage 的位置非常关键它必须紧跟在要分页的查询语句之前中间不能插入任何其他查询否则分页会作用到错误的 SQL 上。这个坑我见过太多次解决方案就是在 Service 方法里把 startPage 和 select 写在一起中间不要调用其他 Mapper 方法。关于 MyBatis 缓存默认情况下二级缓存是关闭的。很多人不理解为什么简单说MyBatis 的一级缓存是 SqlSession 级别的同一个会话里相同查询不会重复查库二级缓存是 namespace 级别的开启后同一个 Mapper 的查询结果可以被多个会话共享。听起来很好但缓存一旦开启数据更新后的失效问题就很麻烦。尤其像图书借阅这种库存字段频繁变化的场景如果缓存没有及时清理用户看到的就是错误库存。所以图书管理系统我建议保持默认配置查询性能不够就考虑后面加 Redis而不是在 MyBatis 二级缓存上折腾。3. 前后端联调与部署实战3.1 本地环境准备版本匹配是关键这个部分直接决定你能不能把项目跑起来。先说后端环境。JDK 用 1.8 还是 17取决于你的 SpringBoot 版本。SpringBoot 2.7.x 可以在 JDK 8 上跑适合大部分老电脑SpringBoot 3.x 必须 JDK 17如果环境还停留在 JDK 8就不要强行用 3.x否则启动直接报错。我的建议是学习项目用 SpringBoot 2.7.x JDK 8网上资料多遇到问题好查。前端环境以 Vue3 项目为例需要先安装 Node.js 的 LTS 版本。版本不用追新装稳定版就行。这里有个容易翻车的地方npm install 安装依赖时特别慢或者装到一半报错。多数情况下是网络源的问题换成国内镜像源之后会顺利很多。依赖装完之后执行 npm run dev 启动前端开发服务器浏览器访问本地地址就能看到页面。后端启动流程是把项目导入 IDEA配置好 Maven 仓库等待依赖下载完成修改 application.yml 里的数据库连接然后运行主启动类。前端启动流程是在项目根目录执行 npm install再执行 npm run dev。两个服务都起来之后才能开始联调。3.2 前端代理与 axios 请求封装开发阶段前端页面地址是 localhost:5173后端接口是 localhost:8080直接请求会跨域。解决跨域有两个办法一个是在后端配 CORS另一个是前端本地启动代理。我最常用的是前端代理因为不影响后端代码也最贴合生产环境的 Nginx 反向代理思路。Vite 配置代理很简单在 vite.config.js 里加上server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求 /api/user/loginVite 开发服务器会转发到 localhost:8080/api/user/login。注意这个代理只在开发环境生效生产环境要交给 Nginx 来转发后面部署部分会再说。axios 封装这块建议把请求逻辑收敛到一个文件里。我最终封装成了两个核心拦截器请求拦截器负责把 token 加在 header 上响应拦截器负责统一处理错误状态和 token 过期跳转登录。封装完以后页面里只需要调用 api 模块里定义好的函数不用关心每个请求都要带 token 这种重复劳动。3.3 生产构建前端打包 后端 Jar Nginx联调完成接下来是部署。前端执行 npm run build生成 dist 静态目录。这个目录里的 index.html 和 assets 文件夹就是最终要发布的内容。后端执行 mvn clean package -DskipTests生成 target 目录下的 Jar 包。传统部署方式是在服务器上装一个 Nginx 托管前端静态文件再用 java -jar 跑后端。Nginx 的关键配置如下server { listen 80; server_name your-domain.com; root /data/www/library/dist; index index.html; location / { 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; } }这段配置里location / 的 try_files 是为了配合 Vue 的 history 路由否则直接刷新非首页地址会 404location /api/ 是把接口请求转发给后端 Jar。第一次部署的小伙伴建议直接复制这段配置然后把 root 路径改成自己服务器的 dist 路径。后端启动命令生产环境用 nohup 在后台跑并把日志写到文件里nohup java -jar library-server.jar --spring.profiles.activeprod app.log 21 启动后要看日志确认没报错再去浏览器访问。日志里如果出现“Started Application in xx seconds”就说明启动成功再去访问页面。3.4 数据库初始化与数据备份部署新环境时数据库要重新初始化。把前面的建表 SQL 在服务器 MySQL 里执行一遍再插入初始管理员和测试数据。这里建议把 SQL 拆成两个文件schema.sql 只建表data.sql 只插数据。以后换环境直接按顺序执行这两个文件就行不用在图形化工具里手动建表。数据备份不能拖到数据丢了才想起来。可以写一个简单脚本每天凌晨执行一次mysqldump -uroot -p library library_backup_$(date %Y%m%d).sql3.5 如果项目要长期维护可以考虑 Jenkins 自动化部署如果这个系统要长期跑手工打包上传确实太累了。Jenkins 的核心思路是在服务器上装好 Jenkins配置一个流水线自动拉取代码然后分别执行 Maven 打包和 npm 构建再把产物复制到部署目录重启后端服务。这里不展开 Jenkins 的安装步骤因为篇幅会比较长。我只说一个比较重要的设计思路建议把构建脚本独立成一个 deploy.shJenkins 流水线只负责调用这个脚本。这样以后想从 Jenkins 切到其他 CI 工具只需要改调用方式不需要重写构建逻辑。4. 常见问题与排查技巧实录4.1 MySQL 连接报错时区、SSL 与驱动类后端一启动就报数据库连接错误是这类项目最常见的问题。我用 MySQL 8 时连接 URL 推荐直接写成url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue这几个参数的意思分别是使用 utf8 字符集、关闭 SSL、指定时区为上海、允许获取公钥。驱动类也要注意MySQL 5 时代写 com.mysql.jdbc.DriverMySQL 8 必须写成 com.mysql.cj.jdbc.Driver写错启动必报错。如果用了上面的配置还是连不上再检查 MySQL 服务有没有启动、端口是不是 3306、用户有没有远程授权。4.2 跨域请求失败但后端日志里明明有请求这个现象经常让人一头雾水。排查思路是先看浏览器控制台具体的报错如果提示 CORS 相关再看是不是自定义 header 触发了预检请求。前面已经提过拦截器要放行 OPTIONS。另一个常见场景是前端代理配置了但接口仍 404那就要看是不是把 /api 前缀拼重了。比如后端接口是 /api/user/login前端 axios 也写了 /api/user/login代理配置里又把 /api 加了一遍结果变成 /api/api/user/login。4.3 前端打包部署后刷新页面 404这是 Vue history 路由的问题。开发模式下 Vite 会自动处理但部署到 Nginx 后浏览器访问 /books 路径Nginx 会去找 /books 这个文件找不到就 404。解决办法就是前面写的 try_files $uri $uri/ /index.html。如果不想用 history 模式改成 hash 路由也会避免 404但 URL 里会出现 # 号分享链接不太美观。我习惯用 history 模式加 try_files。4.4 分页数据混乱第一页和第二页数据重复症状是列表页第一页显示的数据和第二页有重复或者总条数不对。原因基本都是 PageHelper.startPage 后面跟的不是要分页的那条查询。比如你在 startPage 之后先调用了别的 Mapper 方法那个方法也被强行分页了。排查方法就是看 Service 方法的代码顺序确保 startPage 和要分页的查询紧挨着。写完分页逻辑后我建议打开 SQL 日志看一眼实际执行的 SQL确认 LIMIT 加在了正确的位置。4.5 文件上传后图片显示不出来导入导出乱码这类系统经常要加封面图上传、Excel 导入导出。文件上传的坑主要在路径前端通过接口拿到相对路径之后后端要么配置静态资源映射要么把文件传到对象存储里。如果用本地存储测试环境和生产环境的路径要一致或者能通过配置切换否则图片在开发环境能显示部署到服务器就全挂了。Excel 导入导出乱码通常是导出的文件没有用 UTF-8 编码或者 Excel 打开 CSV 时没识别编码导出时指定编码格式就能解决。4.6 问题排查速查表现象可能原因处理建议后端启动报数据库连接失败时区、SSL、驱动类、账号权限用 4.1 的连接串检查 MySQL 服务接口返回 401token 未传、过期、被拦截看拦截器放行路径看 axios 是否带 header接口跨域失败自定义 header 触发预检拦截器放行 OPTIONS或者用前端代理刷新页面 404history 路由没有回退Nginx 加 try_files分页数据混乱startPage 位置错误保证 startPage 和查询紧挨删除失败有关联数据先查未还借阅记录再决定是否删除5. 动手做完之后我比较想分享的几点体会5.1 别只满足于“能跑”主动加需求才涨经验很多同学拿到项目依赖一装、数据库一导页面能登录、能借书就觉得完成了。但我建议在基础功能跑通之后给自己列几个额外需求。比如加一个逾期未还自动提醒加一个图书借阅量 Top10 统计图表加一个读者借阅证号自动生成。每一个需求都会逼着你去查资料、看源码这比照着教程敲十遍都有用。我在做完基础功能后自己又加了 Excel 批量导入图书和借阅趋势折线图这两块让我把 EasyExcel 和图表组件都练了一遍后来写简历的时候也更有底气。5.2 数据结构是地基顺序反过来会踩坑我见过太多人上来就写 Controller写到一半发现字段对不上又回头去改表。我的习惯是先画表结构再定义接口返回格式最后才写前端页面。数据模型定好了后续的开发基本就是往里填。这个项目的借阅记录表如果一开始没有想清楚库存和借阅状态的关联后面做还书功能时就会各种别扭。先设计、再开发看着慢其实才是最快的方式。5.3 部署这一步跳过等于白做本地能跑和服务器能跑是两回事。生产环境的端口、Nginx 配置、防火墙、数据库账号权限任何一个环节都可能让你卡住。我第一次在服务器上部署时光 try_files 就调了大半天但那次折腾之后再遇到任何前后端分离项目部署环节我都能很快上手。网上的部署教程再详细都不如自己在一台干净服务器上从零部署一遍。等你这套流程完整走完这个项目才真正属于你。
返回列表