资讯动态

SpringBoot+MyBatis+JSP图书借阅系统库存事务实践

发布时间:2026/10/27 13:40:39 来源:尧图企业网站定制
简介面向Java Web入门与课程设计场景的图书管理系统完整源码采用SpringBoot整合MyBatis持久层并以JSP负责视图渲染覆盖图书增删改查、分类管理、借阅归还与分页展示等核心业务适合高校学生完成课程设计、毕业设计参考也适合开发者熟悉MVC分层与数据库映射。压缩包共1192个文件约95.3MB包含648个class、173个jsp、148个xml、65个jar及51个java源文件另有properties配置、js/css静态资源与图片素材兼顾可运行产物与二次开发所需源码。目前已有2208人学习下载。项目体现SpringBoot自动配置、MyBatis Mapper映射、PageHelper分页、Spring Security权限控制与jar包部署等实践点可帮助读者对照理解控制器、服务层、DAO与JSP页面之间的调用关系并借鉴表结构设计与前后端数据传递方式。1. 从一次借书卡住说起这套图书管理系统要解决什么学期末整理借阅台账管理员要在纸质登记本和 Excel 之间来回抄一本书被谁借走、还剩几本、谁逾期没还全靠人工核对一处对不上就得从头翻。这套基于 springboot mybatis jsp 的图书管理系统就是把这条线下流程搬进浏览器——读者查书、借书、还书管理员上架、下架、盯逾期。它适合两类人要交毕设或课程设计、需要一个结构完整还能跑起来的学生以及想用轻量技术栈快速搭一套后台的从业者jsp 管视图、mybatis 写 SQL改起来不必翻几层封装。2. 需求拆解与数据模型三张主表撑起的借阅链路做这类系统最常见的翻车不是代码写不出来而是表结构一开始就没想清楚。图书表里塞一个“借阅人”字段看着省事等要查借阅历史、算逾期天数、支持一人借多本的时候就得整表返工。我一般的做法是先把角色和用例定下来再让表结构去匹配用例最后用状态机把借阅记录的生命周期卡死。2.1 角色与用例管理员和读者各自看到什么角色只有两个但权限边界要在设计阶段就画清楚否则写到一半发现读者能进后台就得回头补拦截器。管理员侧的用例包括登录与注销、图书的增删改查、分类维护、读者账号管理、借书与还书的柜台办理、逾期列表、简单的借阅统计。读者侧的用例包括注册登录、按书名/作者/分类检索、查看图书是否可借、查看自己的借阅记录与应还日期。用例角色关键约束图书上架/下架管理员下架前校验该书无未归还记录借书办理管理员库存大于 0同一读者同一本书不可重复借还书办理管理员记录状态必须为“借阅中”或“逾期”图书检索读者只展示上架状态的书我的借阅读者只能看到 reader_id 等于自己的记录这张表不是走形式它直接决定了后面接口的入参和校验点。比如“同一读者同一本书不可重复借”这条如果不在需求阶段写下来代码里十有八九会漏最后演示时被问一句“我借两本同样的书会怎样”就答不上来。2.2 表结构设计图书、读者、借阅记录与状态字段三张主表加一张分类表就够了字段不要贪多。图书表里放库存和总藏书量借阅记录表里放状态和时间两者通过 book_id 关联读者通过 reader_id 关联。-- 图书表stock 是当前可借数total 是馆藏总数 CREATE TABLE book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT 国际标准书号做唯一约束防重复录入, title VARCHAR(120) NOT NULL COMMENT 书名, author VARCHAR(60) NOT NULL, category_id BIGINT NOT NULL COMMENT 分类外键, total INT NOT NULL DEFAULT 1 COMMENT 馆藏总数, stock INT NOT NULL DEFAULT 1 COMMENT 当前可借数量借出减一、归还加一, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 借阅记录表状态和时间字段是逾期统计的依据 CREATE TABLE borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, book_id BIGINT NOT NULL, reader_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL COMMENT 借出时间, due_time DATETIME NOT NULL COMMENT 应还时间借出时按 30 天算好落库, return_time DATETIME NULL COMMENT 实际归还时间未还为 NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 借阅中 1 已归还 2 逾期 3 遗失, KEY idx_reader (reader_id), KEY idx_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;due_time一定要在借出时就落库不要在查询时用borrow_time 30 天现算。现算看起来省一个字段但一旦管理员手动延期或者要按应还日期排序就得改 SQL落库之后逾期判断退化成一次due_time NOW()的比较简单且不会算错。status用数字而不是中文字符串是为了索引效率和后续加状态时不用改字段长度代价是前端要维护一份映射这个成本可以接受。2.3 借阅状态机从借出到归还的四种状态流转状态字段最怕变成“想写什么写什么”所以要把合法流转写死借出时写 0归还时把 0 或 2 改成 1定时任务把到期未还的 0 刷成 2确认丢书时改成 3。除这四条路径外任何状态变更都视为非法。0 → 1正常归还同时stock 10 → 2到期未还由每日定时任务扫描due_time NOW() AND status 02 → 1逾期后归还库存照加记录保留逾期痕迹0/2 → 3遗失登记库存不加回作为馆藏损耗提示状态更新的 SQL 一律带上原状态作为条件例如WHERE id ? AND status IN (0,2)靠影响行数判断是否成功避免两个管理员同时点归还导致库存加两次。这套状态机在后面写归还接口时是直接照着翻译成 SQL 的需求阶段多想十分钟编码阶段能省一小时。3. 技术选型与工程骨架springboot mybatis jsp 怎么搭才不别扭SpringBoot 配 JSP 是这套组合里最容易让人怀疑人生的部分因为官方默认推荐模板引擎JSP 需要额外的依赖和目录约定。但只要把打包方式和视图前缀配好它跑起来很稳而且对课程设计这类需要“看得见页面、改得动页面”的场景反而比前后端分离省事。3.1 为什么是 JSP与模板引擎、前后端分离方案的取舍三种方案我都用过差别主要在调试链路和答辩时的解释成本上。方案页面改动调试链路适合场景JSP JSTL改完刷新即可请求直达 Controller断点好打课程设计、后台管理、需要快速演示Thymeleaf需重启或热部署同上语法更现代新建项目、HTML 原型复用前后端分离需跑两个服务跨域、代理、双端联调团队协作、有前端人力选 JSP 的核心理由不是它先进而是它把“数据怎么到页面”这件事摊得很平Controller 往 Model 里塞一个 listJSP 用c:forEach循环出来中间没有序列化、没有跨域、没有前端构建。对需要在一周内交出可运行系统的场景这条链路最短。代价是页面逻辑容易和 Java 代码混在一起所以后面要立一条规矩JSP 里只做展示和简单判断取数一律放 Controller。3.2 目录结构与依赖webapp 目录、jasper 依赖与打包方式JSP 要能被解析必须满足两个条件依赖里有 jasper编译产物里能找得到 webapp 目录。前者靠 pom后者靠打包方式和目录位置。!-- 视图解析 JSP 必需缺了它访问页面会 404 -- dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId scopeprovided/scope /dependency !-- JSTL 标签库用于 c:forEach、c:if -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId /dependencyspring: mvc: view: prefix: /WEB-INF/jsp/ # JSP 放在 webapp/WEB-INF/jsp 下外部无法直接访问 suffix: .jsp datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true # 下划线字段自动映射驼峰属性prefix指向/WEB-INF/jsp/有安全意义放在 WEB-INF 下的资源不能被浏览器直接请求只能由 Controller 转发避免有人绕过权限直接访问页面。map-underscore-to-camel-case打开后borrow_time会自动映射到borrowTime能省掉大量 resultMap 手写。打包建议用 war 或者引入 jasper 后打成可执行 jar两种都行但目录必须是src/main/webapp放到resources下一定 404。3.3 分层约定controller、service、mapper 各自不该做什么分层不是形式主义它决定了出问题时你能不能快速定位。我一般立三条硬规矩Controller 不写业务判断Service 不碰 HttpServletRequestMapper 里不出现业务分支。Controller RequestMapping(/book) public class BookController { Resource private BookService bookService; // 只做参数接收和视图跳转业务交给 service GetMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, String keyword, Model model) { PageInfoBook page bookService.page(pageNum, pageSize, keyword); model.addAttribute(page, page); model.addAttribute(keyword, keyword); return book/list; // 对应 /WEB-INF/jsp/book/list.jsp } }pageNum和pageSize给了默认值是为了让不带参数的访问也能出结果避免演示时一个空链接报 500。keyword原样回填到 model是为了翻页时搜索条件不丢——这个细节不加翻到第二页搜索就失效了属于典型的“自己测不出来、别人一用就发现”的问题。Service 层负责分页和条件拼装Mapper 只负责 SQL职责清楚之后改一个查询条件不需要动页面。4. 核心模块落地借阅、归还与库存扣减的代码骨架整套系统真正有技术含量的地方就两处库存不能扣成负数借阅记录和库存必须同时成功或同时失败。其余都是增删改查的重复劳动。4.1 库存扣减一条带条件的 update 比加锁更省事库存扣减有三种写法先查再改、synchronized加锁、带条件的原子更新。前两种在单机小流量下也能跑但只要有两个管理员同时点借书就会扣成负数或者互相覆盖。-- 扣减库存stock 0 作为条件影响行数为 0 说明已被借空 UPDATE book SET stock stock - 1 WHERE id #{bookId} AND stock 0 AND status 1;// 返回受影响行数0 表示库存不足或书已下架 int rows bookMapper.decreaseStock(bookId); if (rows 0) { throw new BizException(该书已无可借库存); }这条 SQL 把“判断”和“修改”合并成一次数据库操作InnoDB 的行锁保证同一行不会并发扣减不需要应用层加锁。status 1一起带上是为了防止已下架的书被借走。业务异常抛出去由统一异常处理器接管转成页面提示而不是让它变成 500 白页。4.2 借阅事务service 层如何保证记录与库存同时成功借书是典型的多表写操作插一条借阅记录、扣一次库存。任何一步失败都必须回滚否则会出现“有记录没扣库存”或“扣了库存没记录”的脏数据。Service public class BorrowServiceImpl implements BorrowService { Resource private BookMapper bookMapper; Resource private BorrowRecordMapper recordMapper; // rollbackFor 显式声明 Exception避免受检异常不回滚 Override Transactional(rollbackFor Exception.class) public void borrow(Long bookId, Long readerId) { // 1. 先扣库存条件更新失败直接抛异常中断 if (bookMapper.decreaseStock(bookId) 0) { throw new BizException(库存不足借阅失败); } // 2. 再写借阅记录应还日期在落库时就算好 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); recordMapper.insert(record); } }顺序很关键先扣库存再插记录。反过来的话插入成功但扣减失败回滚同样能保证一致但日志里会先出现一条记录再消失排查时容易看花眼。rollbackFor Exception.class不能省Spring 默认只对运行时异常回滚一旦你在方法里抛了受检异常事务会静默提交这就是很多人遇到的“明明报错了数据还是写进去了”。业务异常继承 RuntimeException 也可以但显式声明更保险。4.3 登录与权限拦截session HandlerInterceptor 的最小实现JSP 项目用 session 存登录态最直接拦截器负责把未登录请求挡在门外。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { // 未登录跳登录页注意要带上项目上下文路径 response.sendRedirect(request.getContextPath() /login); return false; } return true; } }Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /doLogin, /css/**, /js/**, /images/**); } }excludePathPatterns里必须放静态资源否则登录页的样式会被拦掉页面裸奔。重定向用getContextPath()拼前缀是因为项目如果部署在非根路径下写死/login会跳到错误地址。拦截器只管“有没有登录”至于“是不是管理员”我在拦截器里再读一次 session 中的角色做二次判断比在每個 Controller 里写 if 干净得多。4.4 分页与查询PageHelper 加动态 SQL 的组合图书列表要支持按书名模糊查、按分类筛选、按是否可借过滤条件组合起来有七八种全部写死 SQL 会很难维护。select idselectByCondition resultTypecom.demo.entity.Book SELECT * FROM book where if testkeyword ! null and keyword ! AND (title LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND category_id #{categoryId} /if if testonlyAvailable ! null and onlyAvailable AND stock gt; 0 /if /where ORDER BY id DESC /selectpublic PageInfoBook page(Integer pageNum, Integer pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListBook list bookMapper.selectByCondition(keyword); return new PageInfo(list); }PageHelper.startPage必须紧挨着查询方法调用中间插任何一条别的查询分页参数就会被那条 SQL 消费掉这是最常见的翻车点之一。where标签会自动去掉多余的 AND不用手写WHERE 11。gt;是 XML 里的转义写法直接写在部分解析器下会报错。5. 避坑与排查这类项目最容易翻车的五个地方下面五条都是我在这类项目里真实踩过或帮别人排查过的现象和原因写清楚照着对一遍能省下大半天。5.1 访问页面 404Controller 明明返回了视图名现象接口日志显示方法执行完了返回book/list浏览器却是 404 或 Whitelabel 页面。原因基本只有三个JSP 文件没放在src/main/webapp/WEB-INF/jsp/下pom 里缺tomcat-embed-jasperprefix配成了/WEB-INF/jsp少了结尾斜杠拼出来变成/WEB-INF/jspbook/list.jsp。解决先看编译产物 target 目录里有没有 jsp 文件没有就是目录放错了有的话检查依赖和前缀前缀建议固定写成/WEB-INF/jsp/带斜杠省得记。5.2 表单提交中文变成问号或者存进库是乱码现象页面上输入“数据结构”列表里显示 ???或者数据库里存的是乱码。原因通常有两层数据库和表的字符集不是 utf8mb4或者 JDBC URL 少了characterEncodingutf8。还有一种少见情况是 JSP 页面顶部没写% page contentTypetext/html;charsetUTF-8 %。解决建库建表统一CHARSETutf8mb4URL 里带上useUnicodetruecharacterEncodingutf8JSP 首行声明编码。三处都对齐基本不会再出问题。5.3 事务加了注解却不回滚现象借书接口抛了异常页面也报错了回头一查借阅记录插进去了库存却没扣。原因Transactional加在了 Controller 上或者加在 Service 的私有方法上或者同类内部方法直接调用——这三种情况 Spring 的代理都拦不到注解等于没写。解决注解只加在 public 的 Service 方法上且必须从外部调用注入 Bean 后调不要this.xxx()。排查时在方法里打一行日志看事务是否真的开启。5.4 日期字段存取差 8 小时现象借出时间存进去是 14:00查出来变成 06:00。原因JDBC URL 没指定serverTimezone驱动用了 UTC或者实体类用LocalDateTime但 MyBatis 版本较老走了默认的 Date 转换。解决URL 里补serverTimezoneAsia/Shanghai实体统一用LocalDateTime并在 MyBatis 3.4.5 以上版本使用基本能对齐。涉及统计按天分组时最好在 SQL 里用DATE()明确截断别依赖应用层。5.5 登录页样式丢失静态资源全被拦截现象登录页能打开但没有样式F12 里 css 请求返回的是登录页的 HTML。原因拦截器addPathPatterns(/**)把静态资源也包进去了未登录状态下被重定向。解决excludePathPatterns里显式放行/css/**、/js/**、/images/**并且确认静态资源放在src/main/resources/static下而不是 webapp 里。6. 验收与答辩用一份可复现的检查清单过一遍答辩最怕的不是功能少而是演示到一半系统崩了或者数据对不上。所以我在交之前一定按下面这张表自己走一遍每一条都点开、点完、改完再打勾。检查项操作通过标准库存边界把某本书 stock 改成 1连续借两次第二次提示库存不足库存停在 0不出现负数事务完整性借书时手动在插入记录后抛异常借阅记录不落库库存回到借出前重复借阅同一读者同一本书借两次第二次被拦截并给出提示权限边界未登录直接访问/book/list跳转登录页不泄露数据逾期统计把某条记录 due_time 改成昨天逾期列表能查到状态显示正确分页与搜索搜索关键词后翻到第二页关键词保持页码正确不出现重复数据演示顺序也有讲究先跑通“查书—借书—还书”这条主链路让评委看到完整闭环再展示逾期列表和权限拦截这些加分项。库存和事务这类看不见的地方用语言补一句“这里做了条件更新和事务控制防止超借”比现场制造并发更有效。被问到“为什么用 JSP 不用 Vue”时别硬吹技术先进就说清楚取舍项目需要的是短时间内交付一个能跑、能改、部署简单的系统JSP 把请求到页面的链路压到最短代价是页面逻辑要克制。能讲清代价的选型才是站得住的选型。注意答辩前把数据库脚本单独导出一份放在项目根目录评委让你现场重建环境时不至于手忙脚乱。从那以后我每次交这类项目都会在演示前一天按清单完整走一遍并且故意制造两次失败——借空库存、未登录访问确认提示友好、数据没脏。这套习惯救过我至少两次。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑