Spring Boot+MySQL图书馆管理系统:核心表设计与事务实战
简介这是一套基于Spring Boot和MySQL的图书馆管理系统项目资源面向需要完成毕业设计或课程设计的学生同样适合想快速搭建后台管理Web应用的开发者。内含项目全套源码与完整文档源码经过测试校正可稳定运行。压缩包共385个文件、约6.24MB主要包含Java后端代码与编译后的class文件、前端页面及样式脚本、数据库建表脚本、Maven相关配置与依赖以及丰富的图片和动图展示素材目录按前台、后台、安全设计等模块划分并附带可执行启动脚本便于对照学习和二次开发。系统在前台实现登录界面、读者编辑个人信息、馆藏书籍查询、个人借阅记录管理后台则提供用户管理、书籍管理、借阅记录管理并覆盖安全设计配套文档对各模块进行说明有助于理解控制层、业务层、数据访问层的分工可快速部署演示也可作为毕业设计答辩的参考资料。已有1590人学习下载可以用作管理系统类项目的实践资料。1. 基于Springbootmysql的图书馆管理系统这东西到底解决什么问题一家高校分馆或者街道图书馆靠 Excel 记录借还书。每天开馆第一件事就是对账看谁超期、谁续借时写错了条码馆长一边翻台账一边叹气——这大概是很多小馆的常态。而这个标题里的方案基于 Spring Boot MySQL 的图书馆管理系统就是把这个常态打破的标准姿势一个能跑起来的管理端一套完整的图书信息表和借阅流水再加上一份能照着部署的文档落地周期按天算。这套系统适合三类人正在做毕业设计想少走弯路的学生需要给中小型场馆做信息化改造的开发者以及想用一个完整 CRUD 项目作为作品集的初级研发。它的技术范围不算大也正因为不大正好把“借书、还书、续借、超期罚款”这类高频小事务如何做到不丢数据、能对上账这件事讲透彻了。接下来我按最常规的工程形态来讲实现路径包括数据模型、关键接口、事务边界和排错你可以照着改。2. 一上手就要定的事技术栈理由、数据模型与六张核心表2.1 为什么是 Spring Boot MySQL而不是别的组合很多人的第一反应是图书馆管理系统没什么技术含量。确实从纯 CRUD 的角度看它不复杂但真正接需求时有几个点决定了技术选型。第一个是事务边界。借书动作要同时写图书表库存减一、写借阅记录、改读者表借阅数加一三个操作必须一起成功或一起失败。MySQL 的 InnoDB 引擎配合 Spring 的Transactional注解在单库场景下能把这件事做得很稳。如果换成 NoSQL 或者没有事务能力的框架你需要自己写补偿逻辑成本立刻上去了。第二个是生态和文档。图书馆管理系统大概率要运行在不那么新的服务器上MySQL 5.7 和 8.0 都是被反复验证过的版本Spring Boot 的 Starter JDBC、MyBatis 或 JPA 都有大量生产案例。说得直白一点出了问题你能在网上找到答案这本身就是中小型项目最需要的特性。第三个是交付形态。标题里带了“源码文档”说明它面向学习或二次开发资料齐全比极端性能更重要。Spring Boot MySQL 的工程结构约定俗成新接手的人看项目不需要花太多时间这对毕设或团队传承都是加分项。2.2 六张核心表怎么拆把业务对象映射成关系我一般会先把业务对象捋一遍图书、读者、借阅流水、图书分类、管理员用户、罚款单。常见做法是做成六张表其中书和分类是多对一读者和借阅流水是一对多借阅流水和图书是借出时的快照关系。这里有个容易被想错的地方借阅记录要不要冗余图书信息我建议冗余。因为图书价格、出版社这些信息可能变动而借阅记录需要保留“当时借的是什么版本”的事实。同理借阅流水里要带due_time应还时间而不是等归还时现算。下面是建表 SQL 的骨架完全兼容 MySQL 8.0也能在 5.7 上跑CREATE TABLE book ( id bigint NOT NULL AUTO_INCREMENT, isbn varchar(32) NOT NULL COMMENT ISBN编号, title varchar(128) NOT NULL COMMENT 书名, author varchar(64) DEFAULT NULL COMMENT 作者, category_id bigint DEFAULT NULL COMMENT 分类ID, stock int NOT NULL DEFAULT 0 COMMENT 可借库存, total_stock int NOT NULL DEFAULT 0 COMMENT 馆藏总量, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_title (title), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;建表时先把时间字段的默认值写好后面代码里就少写很多setCreateTime(new Date())这种无意义的行。title加了索引因为图书检索的常态是按书名模糊搜索如果未来要做全文检索或者拼音首字母匹配那是后话索引要从查询频率倒推出来。借阅记录表borrow_record需要记录四个关键时间借出时间borrow_time、应还时间due_time、实际归还时间return_time以及超期产生的罚款字段fine_amount。这里最核心的设计点due_time不能靠查询时动态算必须在借出那一刻落库否则后续修改借期规则时老记录怎么算都说不清。CREATE TABLE borrow_record ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL, book_id bigint NOT NULL, book_title varchar(128) NOT NULL COMMENT 借出时的快照, borrow_time datetime NOT NULL, due_time datetime NOT NULL COMMENT 应还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, status tinyint NOT NULL DEFAULT 0 COMMENT 0借出中 1已还 2续借, fine_amount decimal(10,2) DEFAULT 0.00 COMMENT 罚款金额, PRIMARY KEY (id), KEY idx_user_status (user_id, status), KEY idx_due_time (due_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;注意idx_user_status是联合索引查“某读者当前借了什么书”是最常见的查询联合索引可以避免回表。表里冗余book_title之后查流水列表时不需要每次 JOIN 图书表对性能友好对代码也友好。2.3 字段类型、字符集与排序规则三个一开始就要注意的参数很多工程做了一半才回来改字符集那时候数据已经进去了改起来非常痛苦。我建议建库直接用utf8mb4而不是utf8因为utf8mb4才能真正存下 emoji 字符MySQL 8.0 的默认字符集也是它。如果你的系统里有读者留言、书名备注里含有生僻字或 emojiutf8会直接报错Incorrect string value这种问题在线上出现一次就够你记一辈子。排序规则collate我一般选utf8mb4_unicode_ci它比较符合中文场景对排序和大小写不敏感的预期。另一个容易踩的点是金额字段罚款金额用decimal(10,2)别用float。float在做财务加法时会慢慢出现精度偏移虽然单条记录看不出问题一旦累计就和手工台账对不上。库表命名也值得提前统一表名全小写加下划线字段名带上业务前缀比如borrow_time而不是time。MySQL 在 Linux 下区分表名大小写Windows 不区分如果你在 Windows 开发、Linux 部署大小写不一致的表名是经典的部署翻车现场这个坑几乎每个项目都会有人踩一遍。3. 把借还书流程跑通Controller、Service、Mapper 三层的核心代码3.1 为什么用三层结构每层只干一件事图书馆管理系统的规模没有必要上 DDD 或 CQRS经典三层 Controller-Service-Mapper 是最稳的方案。Controller 只做参数接收和响应包装Service 处理业务规则Mapper 只跟 SQL 打交道。这样做的好处是当你要加“管理员审核读者”这种规则时只改 Service不动接口和数据访问层。我见过太多毕设项目把 SQL 直接写在 Controller 里当时很快后续要加权限、加日志时全乱了。这个项目规模不大但分层带来的维护收益依然成立——尤其是接手人不是你自己的时候。3.2 图书检索接口分页、模糊查询、状态过滤图书检索是最常被问“怎么优化”的接口。核心 SQL 是title LIKE ? AND status ?加上 MySQL 的LIMIT分页。这里给出一个直接能用的 Mapper 实现用的是 MyBatis-Plus 风格// BookMapper.java Select(script SELECT id, isbn, title, author, stock, total_stock, status FROM book WHERE status 1 if testkeyword ! null and keyword ! \\ AND (title LIKE CONCAT(%, #{keyword}, %) OR isbn LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY id DESC LIMIT #{offset}, #{pageSize} /script) ListBook searchBooks(Param(keyword) String keyword, Param(offset) Integer offset, Param(pageSize) Integer pageSize);重点说两个参数。offset (pageNo - 1) * pageSize这个换算放在 Service 层而不是 SQL 里pageSize一定要在入口做上限控制公共接口如果不限 size一次拉出几万条记录数据库和前端都扛不住。常见做法是最大 50 条超出直接拒绝。上面这段是动态脚本MyBatis-Plus 也有QueryWrapper写法效果一样。如果你用原生 JDBC 或 JPA逻辑也是同一套。这个接口真正的坑点在于LIKE %keyword%这种前模糊匹配即使title建了索引也用不上数据量到几十万级别之后检索会明显变慢到时候可以再加全文索引或改成前缀匹配。3.3 借书接口扣库存、写流水、更新读者借阅数必须在同一事务里借书是最能体现“为什么要有事务”的业务。一次借书请求背后有三件事检查库存是否充足book表stock减一borrow_record插入一条新记录。如果中途某个写操作失败库存已经扣了但流水没记上这本书就凭空消失了。解决这个问题的标准姿势是Transactional注解加在 Service 方法上。下面这段借书逻辑可以直接套用// BorrowService.java Transactional(rollbackFor Exception.class) public Long borrow(Long userId, Long bookId, Integer borrowDays) { // 1. 查书带行锁 Book book bookMapper.selectByIdForUpdate(bookId); if (book null || book.getStock() 0) { throw new BizException(图书库存不足或已下架); } // 2. 扣库存用 SET stock stock - 1 而不是先查后改 int updated bookMapper.deductStock(bookId); if (updated ! 1) { throw new BizException(并发借阅库存扣减失败); } // 3. 写借阅流水 BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBookTitle(book.getTitle()); record.setBorrowTime(new Date()); record.setDueTime(DateUtils.addDays(new Date(), borrowDays)); record.setStatus(0); borrowRecordMapper.insert(record); return record.getId(); }重点在三个细节。第一处是selectByIdForUpdateMySQL InnoDB 对同一行数据的 UPDATE 会排他但当两个并发请求同时SELECT到stock1然后都通过判断、都执行扣减最终库存会变成-1。用SELECT ... FOR UPDATE加锁读或者直接靠UPDATE的受影响行数来判断都是对的解法。第二处是扣库存的 SQL 写成UPDATE book SET stock stock - 1 WHERE id ? AND stock 0。如果返回影响行数为 0说明并发下库存已经没了直接抛异常这样最稳。这里不要写UPDATE book SET stock #{stock} - 1因为那需要先把 stock 查出来再放回去多一步就多一个并发窗口。第三处注意rollbackFor Exception.class。Spring 的Transactional默认只在RuntimeException时回滚如果你的BizException是受检异常继承Exception不加rollbackFor会导致事务不提交也不回滚数据卡在中间状态。这个是教科书里反复讲、现场反复犯的坑后面排错章节会单列一条。3.4 还书接口更新状态、计算超期罚款、释放库存还书和借书正好相反更新borrow_record的return_time和status判断是否超期并计算罚款最后把book的stock加一。流程的难点不在 SQL而在“超期金额怎么算才公平”。超期罚款 超期天数 × 每日罚款单价。罚款单价建议做成系统参数不要写死在代码里因为不同图书馆对不同类型的书有不同的规则——小说类可能每天 0.5 元工具书每天 2 元。另一个边界是还书当天如果恰好是到期日不算超期判断用dueTime.before(now)而不是dueTime.before(now) || dueTime.equals(now)日期比较用LocalDate或毫秒值不能直接用字符串比。还书逻辑里还有一个容易被漏掉的地方已超期未还书的读者能不能再借新书这个属于业务规则建议放在借书 Service 的开头检查而不是放在还书里。借书方法开头加一条查询即可SELECT COUNT(*) FROM borrow_record WHERE user_id ? AND status 0 AND due_time NOW()有记录就直接拒绝新借书。4. 数据库连接、连接池与时区Spring Boot 集成 MySQL 的配置细节4.1 application.yml 里的关键参数只列真正影响运行的Spring Boot 集成 MySQL 的套路固定但每次新项目我都要重新查一遍参数。下面是一个实际能用的 yml 配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000driver-class-name值得单独说明。如果你用 MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver如果是老驱动mysql-connector-java5.x要写com.mysql.jdbc.Driver。版本不匹配的典型现象是启动报Loading class com.mysql.jdbc.Driver is deprecated项目还能跑但这个警告不解决早晚变成坑。URL 里的参数大多是标配有两个容易被忽略。第一个是serverTimezoneMySQL 8.0 里如果不指定时区驱动会用 JVM 的默认时区如果服务器是 UTC 而业务在东八区存进数据库的时间会差 8 小时超期判断会乱套。第二个是allowPublicKeyRetrievaltrue它只在 MySQL 8.0 的caching_sha2_password认证插件下需要报错Public Key Retrieval is not allowed时加上就能解决。4.2 连接池的四个参数为什么这么设HikariCP 是 Spring Boot 2.x 之后的默认连接池很多人全部用默认值中小型并发场景下确实不用怎么调。但如果你把maximum-pool-size设得过大反而可能坏事MySQL 默认的max_connections是 151如果 20 个应用实例每个开 50 条连接直接把数据库打满新连接全被拒绝表现为应用偶发性报Too many connections。我一般遵循的原则是minimum-idle和maximum-pool-size不要相差太大避免连接频繁创建销毁idle-timeout要小于数据库的wait_timeout。MySQL 服务端默认wait_timeout是 28800 秒8 小时如果连接池里的空闲连接被服务端回收而应用不知道下一次请求就会报Communications link failure。HikariCP 的max-lifetime默认 30 分钟比服务端的 8 小时小通常没问题但如果你改过数据库的wait_timeout连接池参数就要跟着改。4.3 时区问题的三种表现和定位方法时区问题在图书馆管理系统里比想象中更常见因为借阅场景对时间敏感。三种典型表现数据库时间比本地晚 8 小时某段时间查不到数据超期罚款金额对不上。定位时先弄清楚三个时区MySQL 服务端的time_zone、连接 URL 里的serverTimezone、Java 应用里 JVM 默认时区。命令行执行SELECT global.time_zone, session.time_zone;能直接看到数据库时区。如果数据库是SYSTEM最终取的是操作系统时区这时候即使连接串写了serverTimezoneAsia/Shanghai也可能不一致。我建议固定做两件事MySQL 的配置文件my.cnf里写default-time-zone 08:00连接 URL 里写serverTimezoneAsia/Shanghai两处对齐。4.4 慢查询和索引图书馆系统的查询瓶颈在哪图书检索、借阅记录查询、超期提醒这三个接口最容易被数据量放大。借阅记录会随时间无限增长——长期使用的情况下它是这个项目里唯一必然超过百万行的表。所以从一开始就要把索引设计到位并留好查询条件的组合。定期用EXPLAIN观察关键的 SELECT重点关注type列是ALL全表扫描还是ref/range。常见问题状态过滤字段和排序字段组合不当导致 MySQL 排序时使用filesort两个单列索引不如一个联合索引效果好。我之前处理过一个案例查询某读者全部历史借阅记录只建了user_id单列索引没建(user_id, borrow_time)联合索引导致每次查流水都全表扫数据到二十万行后接口直接超时——加联合索引的 ALTER 语句执行完问题从秒级降到毫秒级。5. 图书馆管理系统的常见翻车地点从环境安装到上线排错5.1 MySQL 驱动版本不匹配导致项目起不来现象Spring Boot 项目启动时抛ClassNotFoundException: com.mysql.jdbc.Driver或者提示 driver-class-name 配置了但找不到类。原因pom 里引入的mysql-connector-java版本和 yml 里写的类名不一致。5.x 驱动类名是com.mysql.jdbc.Driver8.x 必须是com.mysql.cj.jdbc.Driver。另外Spring Boot 2.x 的 BOM 会自动管理这个依赖的版本如果你没有显式声明它可能引入 8.x。解决先跑mvn dependency:tree查实际生效的连接器版本再按版本写类名。不要凭记忆选类名这两个驱动类名的差异是这类问题最容易的解法。5.2 MySQL 8.0 认证插件导致的连接失败现象应用启动后第一次访问数据库报Public Key Retrieval is not allowed。原因MySQL 8.0 默认使用caching_sha2_password认证非 SSL 连接下首次握手需要获取公钥JDBC 驱动默认不自动获取。解决在 JDBC URL 上追加allowPublicKeyRetrievaltrueuseSSLfalse。注意这个参数只解决握手问题如果数据库要求 SSL还需要配置证书不过绝大多数局域网开发环境不需要。把这条加到 URL 后重启即可不需要改数据库端。5.3 事务没回滚受检异常导致的库存错乱现象借书接口在插入流水时报了业务异常比如读者已被禁用但日志显示库存已经扣了排查时数据不一致。原因Transactional默认只回滚RuntimeException自定义的BizException如果继承的是ExceptionSpring 不会自动回滚。解决事务方法上加rollbackFor Exception.class或者让所有自定义异常继承RuntimeException。团队内部要约定统一业务异常统一继承RuntimeException这样之后每个方法不用再关注rollbackFor。我早期做这类系统时一本借不出去的书库存变成 -1就是这个原因。5.4 物理删除图书导致历史记录消失现象删除一本下架图书后借阅历史里查到的是空书名或者 JOIN 失败。原因borrow_record只存了book_id没存冗余快照图书删除后关联断裂。这类问题在文档未强调“物理删除还是逻辑删除”时特别容易发生。解决book表加status字段做逻辑删除管理员关闭这本书的借出状态而不是DELETE记录。同时借阅流水表写库时把book_title等关键信息冗余进去让历史查询不依赖 JOIN。这个设计在第 2 章建表时已经埋了伏笔实际项目中强烈建议执行。5.5 Docker 装 MySQL 失败目录权限与初始化顺序现象docker run启动 MySQL 容器后马上退出docker logs里看到Cant open file (errno: 24)或者权限相关错误。原因大多数是容器内数据目录权限问题或者对宿主机挂载目录没有做正确处理。常见的是容器启动时宿主机挂载目录的所有权不是容器内 mysql 用户uid 999 或 1000的。解决挂载本地目录时先让容器自动初始化目录结构不要提前在宿主机创建一堆文件。用一个干净目录启动一次让它生成数据文件如果日志提到chown或permission denied对挂载目录执行chown -R 1000:1000或者改用 Docker 具名卷-v mysql-data:/var/lib/mysql。不要直接chmod 777后期会引入不少安全风险。补充一个开发环境建议Docker 跑 MySQL 做本地开发很舒服但如果你打算部署到生产要先把数据备份方案想清楚。容器重建时数据会保留在卷里但如果没有做恢复演练等真正丢数据那天再补课就迟了。6. 进阶把日志、权限和备份补上系统才算真正能上线到这一步一个能跑通的图书馆管理系统已经具备了。但能用和能上线之间还有一段路。我建议在交付前做三件成本最低、收益最高的事。第一把管理员和读者的权限真正分开。很多人会把所有接口都放开功能少的时候确实图省事但要上线就绕不开读者只能查书、借书、查自己的流水管理员才具备上架、下架、罚款豁免、查所有人流水的能力。最轻量的做法是加一个拦截器校验角色码不用引入 Spring Security 全家桶——一个没有外部登录集成的内网系统用拦截器就够了。第二把借阅超期提醒做成定时任务。用Scheduled(cron 0 8 * * *)每天早上扫一遍due_time在未来三天内且status 0的借阅记录生成待提醒列表。这个功能不复杂但它是这个系统最容易让使用方感知价值的点——“系统提醒我该还书了”比任何后台功能都直观。第三把数据库备份脚本准备好。图书馆管理系统的数据量不可能大到要分库分表但备份可能因为“觉得简单”而一直没做。我会写一个最简单的 cron 脚本每天凌晨用mysqldump导出一份 SQL 文件保留最近 7 天。它不是高级技巧但能在出问题时救回一条命。最后分享一个小教训我早期接这类项目时总想着把功能堆得多、页面做得炫但真正上线后馆长最在意的反而是“借出去的书记得回来超期的书要能提醒”。从那以后我每次做这种管理系统第一个验证的就是“借书-还书-超期-补交罚款”这条闭环跑得稳不稳。参数可以调页面可以改但数据一致性和备份这两条红线一定不能省。希望这篇笔记能帮你少踩几个我踩过的坑。本文还有配套的精品资源点击获取