简介:本资源是一套基于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 加微信小程序重做一遍
很多同学拿到「基于JAVA+SpringBoot+MySQL+微信小程序的图书管理系统」这类毕设题目时,第一反应是去搜现成源码,解压、改个名字、跑起来截图交差。但真正做过一轮的人会发现,能跑起来和能讲清楚之间隔着一条鸿沟:答辩老师随口问一句「借阅超期怎么算的」「库存并发扣减怎么保证不超卖」,代码里没写过就答不上来。这套技术栈之所以在毕设和中小型项目里反复出现,是因为它把三个真实工程问题压缩进了一个可控的规模: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 ) ENGINE=InnoDB; -- 库存表,与 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) ) ENGINE=InnoDB; -- 读者表,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 ) ENGINE=InnoDB; -- 借阅流水表,一条记录代表一次借阅行为 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) ) ENGINE=InnoDB;字段里有两个地方值得单独说。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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 id="decreaseAvailable"> UPDATE book_stock SET available = available - 1, version = version + 1 WHERE book_id = #{bookId} AND available > 0 AND version = #{version} </update>@Transactional保证库存更新和流水写入要么都成功要么都回滚。参数上,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 List<BookVO> listBooks(String category, String keyword, int page, int size) { int offset = (page - 1) * size; return bookMapper.selectByCondition(category, keyword, offset, size); }对应的 SQL 用动态标签拼条件:
<select id="selectByCondition" resultType="com.example.vo.BookVO"> SELECT b.*, s.available FROM book b LEFT JOIN book_stock s ON b.id = s.book_id <where> <if test="category != null and category != ''"> AND b.category = #{category} </if> <if test="keyword != 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} </select>LEFT 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_type=authorization_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 里加serverTimezone=Asia/Shanghai,或者升级驱动版本。这个错误信息很明确,照着改就行,但新手容易忽略 URL 里的参数。
5.4 小程序登录 code 重复使用报错
现象是第二次调登录接口时报code been used。原因是wx.login拿到的 code 只能用一次,前端如果在多个地方调登录或者重复提交就会失败。解决方式是每次登录前重新调wx.login拿新 code,不要缓存 code,只缓存后端返回的 token。
5.5 中文书名插入数据库变问号
现象是插入中文后查询显示???。原因是数据库或连接字符集不是 utf8mb4。解决方式是建库时指定CHARACTER SET utf8mb4,JDBC URL 里加characterEncoding=utf8,两边都对齐。如果已经建了库,用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 这两处卡一下,后来养成先写测试用例再写接口的习惯,省了很多返工。希望帮到你。
本文还有配套的精品资源,点击获取