简介:这是一份基于JavaWeb的图书借阅管理系统项目源码,采用JSP+JavaBean+MySQL+Tomcat经典架构,适合JavaWeb初学者、课程设计或毕业设计学生参考。系统分为读者和管理员两端:读者可注册登录、查询借阅图书、查看借阅历史、归还图书及修改个人信息;管理员可对图书进行增删改查,管理读者信息并查询全部借阅记录,覆盖了图书借阅管理的核心业务流程。资源包共38个文件,以JSP页面、Java类、SQL脚本、CSS样式及XML配置文件为主,整体大小1.76MB,同时附有项目运行说明和系统介绍文档,可帮助快速导入部署并理解项目结构。目前已有531人学习下载,适合需要完整项目案例或二次开发基础的同学获取参考。 图书借阅管理系统,JavaWeb里被做烂了的题目,但凡课程设计、毕业设计,十个里面八个都是它。但恰恰是这种"老项目",最能检验流程细节:数据库怎么写才能不冗余?借书时多人同时操作怎么保证不超借?还书逾期罚款是跑批还是实时算?我在帮人改这类系统的过程中,发现大多数版本只做到了"能运行",离"可交付"还有很大距离。这篇文章就结合我重写一版图书借阅管理系统的完整思路,从表结构、VS Code 开发环境、登录权限到借还书核心流程,把关键代码和踩坑经验全部摊开讲,适合正在做 JavaWeb 课设或想系统过一遍 Servlet + JSP + MySQL 的同学参考。
1. 为什么图书借阅管理系统值得你亲手写一遍
1.1 这套系统到底在练什么
很多人觉得图书借阅管理就是简单的增删改查,不值得认真做。我的看法相反:它麻雀虽小,五脏俱全。图书信息管理是基础 CRUD,借书和还书是事务和并发问题,登录和权限是 Filter 和 Session 的典型场景,逾期罚款又是业务规则计算的代表。这些点串起来,几乎覆盖了 JavaWeb 后端开发所有基础知识点。
我见过不少同学把系统写成了"图书数据的维护页面",书表的增删改查做得很用心,但借书功能就写成了"把 book 表的总数减一"。一旦并发或者归还时没处理好,库存数据迟早对不上。真正完整的借阅闭环应该包括:读者登录、图书检索、借书扣减库存、还书恢复库存、逾期计算罚款、管理员对图书和读者的维护。每个环节都有值得抠细节的地方。
1.2 需求拆解:不只有 CRUD 的借阅闭环
先明确角色。这套系统至少要分清两类人:普通读者和管理员。读者登录后能查书、借书、还书、看自己的借阅记录;管理员负责录入图书、管理读者、处理借阅异常。很多人把管理员和读者混在一个表里,用一个 userType 字段区分,这没问题,但你必须想清楚权限控制怎么做。
业务闭环我建议这么拆:
- 读者查询图书,看到"可借数量"。
- 借书时,判断该读者未还数量是否达到上限,判断这本书当前是否有库存。
- 扣减库存,生成一条借阅记录,并登记应还日期。
- 还书时,恢复库存,标记记录为已还,并根据实际归还日期计算是否逾期。
- 管理员可以查看所有借阅记录,对逾期读者进行罚款操作。
这个流程看起来简单,编程时最容易出问题的就是第3步和第4步。后面我会专门讲。
2. 数据库设计:把库存和借阅记录拆在两张表里,少走弯路
2.1 三张核心表与字段设计
我把表设计成三张:book(图书表)、reader(读者表)、borrow_record(借阅记录表)。这是最直观、也是绝大多数成熟系统采用的结构。下面给出字段清单和设计说明。
CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) UNIQUE, name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), total_count INT DEFAULT 0, -- 总库存 available_count INT DEFAULT 0, -- 当前可借数量 create_time DATETIME ); CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) UNIQUE, password VARCHAR(64), real_name VARCHAR(30), phone VARCHAR(20), max_borrow INT DEFAULT 5, -- 最多可借本数 create_time DATETIME ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT, reader_id INT, borrow_date DATETIME, due_date DATETIME, return_date DATETIME, -- 实际归还时间,未还则为 NULL status TINYINT DEFAULT 0, -- 0: 借出中,1: 已归还 fine DECIMAL(10,2) DEFAULT 0, -- 罚款金额 FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) );实际项目里还会加索引,比如borrow_record(reader_id, status),方便查某个读者的在借记录;borrow_record(book_id, status)则方便查看某本书是否还有未还的旧记录。这几个索引按查询习惯建即可,不用太多。
2.2 为什么"可用数量"不能靠统计借阅记录得到
这是新手最容易犯的错。有人觉得 book 表里不需要available_count,每次要查询的时候,直接total_count - 未还借阅记录数不就行了?表面上可行,实际维护成本很高。
第一,计算会慢。每查一本书就要子查询一张借阅记录表,数据量大了以后性能很难看。第二,一致性难保证。如果借书和还书的更新先改借阅记录,再改 book 表,两步之间一旦程序崩溃,两边就会对不上。所以正确的做法是:在 book 表保存一个available_count,借书时减一、还书时加一,并且用数据库事务保证这两步要么都成功,要么都失败。
我把它类比成超市库存:货架上显示"剩余数量",而不是每次结账都去翻历史销售明细。
2.3 逾期罚款用计算而非定时任务
逾期罚款不建议做一个定时任务去天天扫描数据库。原因很简单:罚款额度取决于"实际归还时间"。在没还书之前,逾期天数每天都在变,但罚款金额只需要在还书那一刻算一次就够了。
在borrow_record表里存放fine字段,还书时根据due_date和当天日期计算,写入字段。这样历史记录可追溯,哪怕后来改罚款规则,也不会影响已还的记录。设计上只加一个return_date,通过是否为 NULL 判断借出状态,比单独维护一个状态位更直观。status字段建议保留,因为后续如果要支持"丢失""预约"等状态,光靠时间字段表达不了。
3. 用 VS Code 搭好 JavaWeb 开发环境:我的实际步骤
3.1 环境清单和插件
很多人做 JavaWeb 默认用 Eclipse 或 IDEA,但如果你只是想快速验证代码、而且平时习惯用 VS Code,完全可以用 VS Code 跑通 Servlet + JSP 项目。我实测过的组合是:
- JDK 1.8 或 11(我用的是 8)
- Tomcat 9
- VS Code
- 插件:
Extension Pack for Java(必装)、Tomcat for VS Code(必装) - MySQL 8.0 + Navicat 或命令行
装完插件后,VS Code 对 Java 项目的编译、运行、调试基本够用。唯一要注意的是,VS Code 对 Maven 项目的支持好于传统 Web 项目,所以建议直接用 Maven 结构创建,别手动建一堆 lib 目录。
3.2 工程目录与 Tomcat 配置
我习惯用 Maven 的 war 包结构:
book-manager ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/book │ │ │ ├── filter │ │ │ ├── servlet │ │ │ ├── dao │ │ │ └── util │ │ ├── resources │ │ └── webapp │ │ ├── WEB-INF │ │ │ ├── web.xml │ │ │ └── jsp │ │ └── index.jsp在pom.xml里加上javax.servlet-api和mysql-connector-java依赖,注意 servlet-api 要设置为providedscope,因为 Tomcat 自带该 jar,避免冲突。
VS Code 启动项目有个小坑:直接用Tomcat for VS Code插件时,它要求有.vscode/tomcat.code-workspace之类的配置,否则可能识别不到 war 包。切换标签页,找到 Tomcat Server 视图,把本地 Tomcat 路径添加进去即可。点击绿色启动按钮后,插件会部署并把浏览器打开,如果端口被占用,改一下server.xml里的端口,或者右键服务修改监听端口。
3.3 第一个 Servlet 怎么跑通
为了验证环境没问题,先写一个最简单的TestServlet走一遍流程:
@WebServlet("/hello") public class TestServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>JavaWeb 环境 OK</h1>"); } }只要浏览器访问http://localhost:8080/项目名/hello能看到这行字,说明 Tomcat + Maven + VS Code 的三方配合没问题。这一步能过滤掉 80% 的环境问题,再往下写业务逻辑才不慌。
4. 核心流程实现:登录过滤、借书并发、归还罚款
4.1 用 Filter 把未登录用户挡在门外
登录和权限校验不要在每个 Servlet 里重复写,用 Filter 统一拦截就行。我在web.xml或者用@WebFilter注解配置一个LoginFilter:
@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、静态资源和登录接口本身 if (uri.endsWith("/login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }如果你区分管理员,再写一个AdminFilter或者在一个 Filter 里判断,用户登录后在 session 里存一个role字段,管理员接口检查 role,不满足就跳转 403 页面。简单的角色系统不需要引入 Spring Security,这个方案够用且直观。
4.2 借书:数据库原子更新,防止可借数量出现负数
借书流程的关键在于并发。我举一个具体场景:某本书只剩最后 1 本,两个读者同时点借书。如果你的代码是先SELECT available_count判断大于 0,再UPDATE book SET available_count = available_count - 1,两个请求可能同时读到 1,然后都执行更新,库存最终变成 -1。这就是典型的"超卖"问题。
解决办法是用一条 SQL 原子更新:
Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 原子扣减库存,只有影响行数为 1 才说明抢到了这本书 String sql = "UPDATE book SET available_count = available_count - 1 " + "WHERE id = ? AND available_count > 0"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, bookId); int rows = ps.executeUpdate(); if (rows == 0) { conn.rollback(); // 提示读者:库存不足 return; } // 2. 检查该读者未还数量是否已达上限 // 3. 插入借阅记录 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn); }available_count > 0这个条件必须放在 WHERE 里,配合行锁,MySQL 会锁住这行记录直到事务结束,这样并发时后续请求只能等前一个事务提交后再执行。这里用事务保证"扣库存"和"插入借阅记录"的一致性,否则库存扣了但记录没插,数据就乱了。
判断读者是否超限的 SQL 也要注意:必须在同一个事务内查borrow_record,因为同一时间可能有多个借书请求,避免两边都发现没超限,最后超了。
4.3 还书:逾期天数和罚金的计算细节
还书逻辑相对简单,但计算日期时建议直接用LocalDate,别再用Date加减。
LocalDate dueDate = LocalDate.parse(record.getDueDate()); LocalDate returnDate = LocalDate.now(); long days = ChronoUnit.DAYS.between(dueDate, returnDate); if (days > 0) { double fine = days * 0.1; if (fine > 10) fine = 10; // 单本书罚款封顶 10 元 }SQL 更新的时候,别忘了同时更新book.available_count加一,并把borrow_record里的return_date和fine、status一起更新。这同样要放在一个事务里。我见过有人写还书时忘了恢复库存,结果没多久系统的可借书越来越少,最后只能手动改数据库。
另外,还要处理一个边界情况:图书被归还时,如果这本书已经删除了怎么办?建议外键设计时不要强制级联删除,而是给 book 表加一个is_deleted标记,或者借阅记录只保存book_id,就算图书下架也不影响历史记录查询。
5. 项目上线前,这些加分项和坑值得注意
5.1 事务与连接池,别把 SQL 执行错当业务结束
如果项目里到处都直接用DriverManager.getConnection(),每访问一次数据库就新建连接,性能会非常差。建议至少用Druid或HikariCP连接池。
配置连接池时,我建议在src/main/resources/druid.properties写:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=20再用一个静态工厂获取数据源,所有 DAO 都从这个工厂拿连接。这样能做到连接复用,并发能力比新手默认写法强一个量级。
5.2 对接在线支付的可行方案
如果你想把"罚款缴纳"做成在线支付,让系统显得更完整,可以引入支付平台的沙箱环境来做模拟对接。核心思路是:生成一个支付订单,前端展示付款二维码,用户支付后支付平台异步回调后端接口,后端更新订单状态和罚款记录。
不要把支付回调逻辑写在主流程里,而是单独建一个PayNotifyServlet,校验签名后更新数据库。因为回调可能重复推送,所以要做幂等处理,比如先查订单状态,已处理则直接返回成功。
这里要提醒一点:真正的线上支付需要商户资质,课程设计里用沙箱环境演示即可。别在公网项目里随便接入真实支付,安全风险和合规风险都很高。
5.3 部署时的常见错误清单
我整理过几个高频部署报错,几乎每次都能用上:
- 数据库中文乱码:连接串必须加
characterEncoding=utf8mb4,同时数据库、表、字段的字符集也要一致。 - Tomcat 启动端口冲突:在
conf/server.xml里改port,三个端口都要改,或者停掉占用端口的进程。 - 找不到 JSP 或 Servlet:检查项目是否打成 war 包,context path 是否正确。
- MySQL 驱动加载失败:确认
pom.xml里依赖没设置<scope>provided</scope>,那是给 servlet-api 用的。
另外,部署到云服务器时,记得开放 8080 端口,数据库不要用 root 账号加弱密码,至少建一个业务账号并限制 IP。
6. 我个人重写这套系统时的几点体会
如果让我再重写一遍,我会把日志从一开始就加上,而不是等出了问题再补。像借书、还书、罚款这些关键操作,分别记录操作时间、操作人、操作结果,排查数据不一致时能省非常多时间。代码层面尽量把 DAO 层、Service 层分开,别把 SQL 写在 Servlet 里。刚写的时候觉得 Servlet 直接查库很爽,改到后面你就会后悔,因为一旦业务逻辑复杂起来,Servlet 会被代码塞满,连调试都困难。
另一个建议是,务必保留一份完整的测试数据。课程设计最怕答辩前数据被改乱了,准备一个 SQL 初始脚本,包含几十本不同类型图书和几个测试账号,一键初始化到干净状态。这也是我后来养成的习惯,每次演示之前跑一遍脚本,比手工删数据可靠得多。
图书借阅管理系统听起来普通,但把每个细节都做到位,你对 JavaWeb 的理解会上一个台阶。希望这篇分享能帮你少踩一些我已经踩过的坑。
本文还有配套的精品资源,点击获取