ARTICLE DETAIL

资讯详情

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

SpringBoot+MySQL+微信小程序图书管理系统:从建库到借书并发实战

SpringBoot+MySQL+微信小程序图书管理系统:从建库到借书并发实战 简介本资源是一套基于Java、SpringBoot、MySQL与微信小程序开发的图书管理系统完整毕业设计包面向高校计算机相关专业学生及需要课程设计、期末大作业参考的开发者。系统涵盖用户管理、图书管理、借阅管理、搜索查询等核心模块前后端代码齐全可直接运行。压缩包共1231个文件约20.76MB包含128个Java后端源码、145个Vue前端组件、229个JavaScript脚本、48个WXML与48个WXSS小程序页面文件以及PNG、SVG、JPG等界面素材另附SQL数据库脚本、论文文档与Maven构建配置结构完整便于按模块查阅。已有38人学习下载。项目经严格调试下载即用无需修改读者可据此掌握SpringBoot接口设计、MySQL数据建模与微信小程序页面开发流程也可作为二次开发与功能扩展的实践基础。1. 从一份毕设压缩包说起图书管理系统为什么值得用 SpringBoot 加微信小程序重做一遍很多同学拿到「基于JAVASpringBootMySQL微信小程序的图书管理系统」这类毕设题目时第一反应是去搜现成源码解压、改个名字、跑起来截图交差。但真正做过一轮的人会发现能跑起来和能讲清楚之间隔着一条鸿沟答辩老师随口问一句「借阅超期怎么算的」「库存并发扣减怎么保证不超卖」代码里没写过就答不上来。这套技术栈之所以在毕设和中小型项目里反复出现是因为它把三个真实工程问题压缩进了一个可控的规模SpringBoot 负责把后端接口和业务逻辑组织清楚MySQL 负责把图书、用户、借阅记录这些强关系数据管住微信小程序负责给读者一个不用装 App 就能查书、借书的入口。它适合两类人一类是需要一个完整闭环项目来打通前后端和数据库的在校生另一类是刚转 Java 后端、想找一个业务不复杂但环节齐全的练手项目的初级工程师。这一篇不讲空泛的架构图而是按我实际搭过一遍的顺序把建库、写接口、接小程序、避坑这几件事拆开说清楚让你看完能自己复现而不是只会改别人的包名。2. 先把数据模型定死图书管理系统的 MySQL 建库与四张核心表2.1 为什么图书管理系统的表设计不能照抄网上的模板网上流传的图书管理系统 SQL 脚本很多只有 book、user、borrow 三张表字段也随意比如把「在馆数量」和「总数量」合成一个字段借书时直接减一。这种设计在单机演示时没问题一旦你要讲清楚「同一本书被两个人同时借」的场景就会露馅。我一般会把核心表拆成四张book图书基础信息、book_stock库存与在馆状态、user读者含小程序 openid、borrow_record借阅流水。把库存从图书信息里拆出来是因为图书的元数据书名、作者、ISBN基本不变而库存是高频变动的拆开后更新库存不会锁住整行图书信息也方便后面做乐观锁。另一个必须提前定的是借阅状态机一条借阅记录从「借出」到「归还」中间可能经过「超期」状态字段用 tinyint 枚举而不是字符串查询和统计都省事。2.2 建库建表的完整 SQL 与字段说明下面这段 SQL 可以直接在 MySQL 5.7 或 8.0 里执行字符集统一用 utf8mb4避免书名里出现生僻字或 emoji 时插入失败。-- 创建数据库字符集用 utf8mb4 兼容 emoji 和生僻字 CREATE DATABASE library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 图书基础信息表 CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT ISBN 唯一防重复录入, title VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), category VARCHAR(50) COMMENT 分类用于小程序筛选, cover_url VARCHAR(255) COMMENT 封面图存相对路径, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 库存表与 book 一对一单独拆出便于高频更新 CREATE TABLE book_stock ( book_id BIGINT PRIMARY KEY, total INT NOT NULL DEFAULT 0 COMMENT 馆藏总数, available INT NOT NULL DEFAULT 0 COMMENT 当前可借数, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINEInnoDB; -- 读者表openid 是小程序登录后拿到的唯一标识 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE, nickname VARCHAR(50), phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 借阅流水表一条记录代表一次借阅行为 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL COMMENT 应还时间借出时按规则算好, return_time DATETIME COMMENT 实际归还时间未还为 NULL, status TINYINT DEFAULT 0 COMMENT 0借出 1已还 2超期, INDEX idx_user (user_id), INDEX idx_book (book_id) ) ENGINEInnoDB;字段里有两个地方值得单独说。book_stock的version字段是给乐观锁用的后面借书接口会用到borrow_record的due_time在借出那一刻就算好写进去而不是每次查询时用borrow_time加天数现算因为借阅规则可能中途调整写死能保证历史记录不受新规则影响。available和total分开存是为了支持「部分在馆」的情况比如馆藏 5 本借出 3 本available就是 2。2.3 初始化数据与索引检查建完表先插几条测试数据确认外键和唯一约束都生效。执行下面这段如果 ISBN 重复会直接报错说明唯一索引起作用了。INSERT INTO book (isbn, title, author, publisher, category) VALUES (9787111213826, 深入理解Java虚拟机, 周志明, 机械工业出版社, 计算机); INSERT INTO book_stock (book_id, total, available, version) VALUES (1, 5, 5, 0); INSERT INTO user (openid, nickname, phone) VALUES (test_openid_001, 测试读者, 13800000000);插完用EXPLAIN看一眼按用户查借阅记录的语句走没走索引EXPLAIN SELECT * FROM borrow_record WHERE user_id 1;如果 type 是 ref 且 key 是 idx_user说明索引正常。这一步很多人跳过等到数据量上来查询变慢才回头补索引属于典型的血泪经验。3. SpringBoot 后端接口借书、还书、查书三个核心链路怎么写3.1 项目依赖与配置里最容易翻车的地方SpringBoot 版本选择上我一般用 2.7.x 而不是最新的 3.x原因是 3.x 要求 JDK 17 起步而很多学校的实验环境还停在 JDK 8用 3.x 会在启动时直接报版本不兼容。依赖只需要四个spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok。application.yml里数据库连接串要显式指定时区和字符集否则插入中文可能变问号。spring: datasource: url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueserverTimezone不写的话MySQL 8.0 驱动会报时区错误这是新手最常卡住的一步。map-underscore-to-camel-case打开后数据库的borrow_time能自动映射到 Java 的borrowTime省掉一堆手动映射。3.2 借书接口用乐观锁防止库存超卖借书是整个系统里唯一有并发风险的操作。两个人同时点借同一本书如果先查库存再更新中间有时间窗口可能把available减成负数。我用的是乐观锁更新时带上版本号更新影响行数为 0 就说明被别人抢先了直接返回失败让前端重试。Service public class BorrowService { Autowired private BookStockMapper stockMapper; Autowired private BorrowRecordMapper recordMapper; // 借书核心逻辑返回 true 表示借阅成功 Transactional public boolean borrow(Long userId, Long bookId) { // 1. 查出当前库存和版本号 BookStock stock stockMapper.selectByBookId(bookId); if (stock null || stock.getAvailable() 0) { return false; // 没有可借库存 } // 2. 乐观锁更新available 减一version 加一条件带上旧 version int rows stockMapper.decreaseAvailable(bookId, stock.getVersion()); if (rows 0) { return false; // 版本号被改过说明有并发本次失败 } // 3. 写入借阅流水应还时间设为 30 天后 BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setDueTime(DateUtil.addDays(new Date(), 30)); record.setStatus(0); recordMapper.insert(record); return true; } }对应的 Mapper XML 里更新语句必须带version #{version}这个条件update iddecreaseAvailable UPDATE book_stock SET available available - 1, version version 1 WHERE book_id #{bookId} AND available 0 AND version #{version} /updateTransactional保证库存更新和流水写入要么都成功要么都回滚。参数上due_time用 30 天是常见做法实际项目里应该从配置表读方便运营调整。如果rows 0前端应该提示「手慢了请重试」而不是直接报系统错误。3.3 还书接口与超期判断还书比借书简单但要处理超期状态。我的做法是还书时对比当前时间和due_time如果已过就先把状态改成超期再改成已还或者直接标记为已还但保留超期标记供统计用。Transactional public boolean returnBook(Long recordId) { BorrowRecord record recordMapper.selectById(recordId); if (record null || record.getStatus() 1) { return false; // 记录不存在或已归还 } // 更新归还时间和状态 record.setReturnTime(new Date()); record.setStatus(1); recordMapper.updateById(record); // 库存加回同样用乐观锁 stockMapper.increaseAvailable(record.getBookId()); return true; }库存加回的 SQL 是UPDATE book_stock SET available available 1 WHERE book_id #{bookId} AND available total加available total是防止异常情况下加超。还书接口不需要版本号因为加库存不会导致超卖但加上界判断能兜底。3.4 查书接口分页加分类筛选小程序首页要展示图书列表支持按分类筛选和关键词搜索。用 MyBatis 的分页插件或者手写 limit 都行我一般手写少引一个依赖。public ListBookVO listBooks(String category, String keyword, int page, int size) { int offset (page - 1) * size; return bookMapper.selectByCondition(category, keyword, offset, size); }对应的 SQL 用动态标签拼条件select idselectByCondition resultTypecom.example.vo.BookVO SELECT b.*, s.available FROM book b LEFT JOIN book_stock s ON b.id s.book_id where if testcategory ! null and category ! AND b.category #{category} /if if testkeyword ! null and keyword ! AND (b.title LIKE CONCAT(%, #{keyword}, %) OR b.author LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY b.id DESC LIMIT #{offset}, #{size} /selectLEFT JOIN保证即使某本书还没建库存记录也能查出来available为 null 时前端显示「暂不可借」。关键词搜索用LIKE在数据量大时会慢但毕设规模完全够用真要优化就上全文索引那是另一个话题了。4. 微信小程序端登录、图书列表、借阅记录三个页面的接法4.1 小程序登录换 openid 的正确姿势微信小程序的登录不能直接拿用户信息必须先调wx.login拿到临时 code传给后端后端用 code 加 appid、secret 去换 openid。这里有个常见误解很多人以为前端能直接拿到 openid其实拿不到必须走服务端。后端换 openid 的接口大致这样PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 用 code 换取 openid实际请求微信接口 String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code dto.getCode() grant_typeauthorization_code; String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); // 查库没有就自动注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userMapper.insert(user); } // 生成 token 返回给小程序后续请求带上 String token JwtUtil.generate(user.getId()); return Result.success(token); }appid和secret放配置文件里不要硬编码在代码里。返回的 token 用 JWT 生成小程序存到 storage后续每次请求放在 header 里。注意jscode2session这个接口的 code 只能用一次且五分钟内有效前端不要缓存 code。4.2 图书列表页与借阅按钮小程序页面用wx.request调后端接口列表用scroll-view或普通view加wx:for都行。借阅按钮点击后调借书接口成功就刷新列表。// 借书操作 borrowBook(e) { const bookId e.currentTarget.dataset.id; wx.request({ url: http://localhost:8080/api/borrow, method: POST, header: { token: wx.getStorageSync(token) }, data: { bookId: bookId }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 借阅成功 }); this.loadBooks(); // 刷新列表更新可借数量 } else { wx.showToast({ title: res.data.msg || 借阅失败, icon: none }); } } }); }header里带 token 是后端鉴权的依据后端用拦截器解析 token 拿到 userId。localhost在真机调试时不通要用局域网 IP 或者内网穿透这是调试阶段必踩的坑。4.3 借阅记录页与状态展示借阅记录页展示当前用户的借阅流水状态用不同颜色区分借出中、已归还、已超期。后端返回列表时把due_time和当前时间对比前端只负责展示。// 状态映射后端返回 status 数字前端转文字 const statusMap { 0: 借出中, 1: 已归还, 2: 已超期 };页面加载时调/api/borrow/list后端根据 token 里的 userId 查记录。这里要注意分页记录多了不能一次全返回加page和size参数。5. 避坑与排查这套技术栈里最容易翻车的五个地方5.1 小程序请求后端报「不在以下 request 合法域名列表」现象是小程序开发者工具里请求能通真机预览时报域名不合法。原因是微信要求所有请求域名必须在后台配置且备案。开发阶段可以在开发者工具里勾选「不校验合法域名」但真机调试必须配。解决方式是本地开发用内网 IP 加端口真机调试时在微信公众平台配置服务器域名或者用开发者工具的「真机调试」模式绕过。5.2 借书时库存扣成负数现象是并发测试时available出现负值。原因是更新语句没加available 0条件或者没用乐观锁。解决方式是更新 SQL 里同时带available 0和version #{version}更新影响行数为 0 就返回失败。这个坑我在第一次写借书接口时就踩过压测一跑库存直接乱掉。5.3 MySQL 8.0 连接报时区错误现象是启动 SpringBoot 时报The server time zone value xxx is unrecognized。原因是 MySQL 8.0 驱动要求显式指定时区。解决方式是在 JDBC URL 里加serverTimezoneAsia/Shanghai或者升级驱动版本。这个错误信息很明确照着改就行但新手容易忽略 URL 里的参数。5.4 小程序登录 code 重复使用报错现象是第二次调登录接口时报code been used。原因是wx.login拿到的 code 只能用一次前端如果在多个地方调登录或者重复提交就会失败。解决方式是每次登录前重新调wx.login拿新 code不要缓存 code只缓存后端返回的 token。5.5 中文书名插入数据库变问号现象是插入中文后查询显示???。原因是数据库或连接字符集不是 utf8mb4。解决方式是建库时指定CHARACTER SET utf8mb4JDBC URL 里加characterEncodingutf8两边都对齐。如果已经建了库用ALTER DATABASE library_db CHARACTER SET utf8mb4;改。6. 让这套系统在答辩和面试里站得住三个能加分的进阶点第一个进阶点是给借阅记录加一个定时任务每天凌晨扫描所有未归还且已过due_time的记录把状态批量改成超期。用 SpringBoot 的Scheduled注解就能做cron 表达式写0 0 1 * * ?表示每天凌晨一点执行。这个功能代码量不大但能体现你对「状态流转」的理解答辩时被问到超期怎么处理你有实际实现而不是口头描述。Scheduled(cron 0 0 1 * * ?) public void markOverdue() { // 把所有未归还且应还时间已过的记录标记为超期 recordMapper.updateOverdueStatus(new Date()); }对应的 SQL 是UPDATE borrow_record SET status 2 WHERE status 0 AND due_time #{now}。注意这个任务要幂等重复执行不会出错因为条件里限定了status 0。第二个进阶点是把图书封面图存到本地磁盘而不是数据库数据库只存相对路径。小程序加载图片时用image组件的src拼上后端静态资源地址。SpringBoot 里配一个静态资源映射把/upload/**映射到本地目录。这样数据库不会因为存二进制图片而膨胀备份也快。第三个进阶点是给接口加统一的返回格式和全局异常处理。定义一个Result类包含code、msg、data三个字段所有接口都返回这个结构。再用RestControllerAdvice捕获异常统一返回错误码。这样小程序端处理响应时逻辑统一不用每个接口单独判断。我一般会在Result里定义几个常用码200 成功、401 未登录、500 系统错误前端根据 code 做不同处理。最后一个习惯每次改完接口用 Postman 或者 curl 先单独测一遍确认返回结构对了再接小程序。直接在小程序里调后端出错时你分不清是前端传参问题还是后端逻辑问题排查成本翻倍。这套系统我前后搭过三遍每次都会在借书并发和登录换 openid 这两处卡一下后来养成先写测试用例再写接口的习惯省了很多返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表