简介:这是一份基于JavaWeb的图书馆管理系统完整项目源码,适合正在做课程设计、毕业设计或想系统梳理服务端开发流程的学习者。项目以MVC分层为核心,后端使用Servlet处理业务逻辑,JSP完成页面展示,通过JDBC操作MySQL数据库,覆盖图书信息增删改查、图书分类检索、用户注册登录与权限控制、借阅归还及超期罚款等常见业务模块。压缩包共包含199个文件,核心为64个Java源码与51个JSP页面,附带32个class编译文件、SQL建库脚本、properties配置及少量前端样式脚本,整包仅766KB,结构清晰,便于导入Eclipse/IDEA后快速运行调试。目前已吸引5167人学习下载,适合用于理解JavaWeb项目从数据库设计、后端逻辑到界面交互的完整链路;同时可参考其中的用户管理、借阅流程与数据库表设计,直接作为课设或毕设的基础工程与二次开发蓝本。
1. 基于javaweb图书馆管理系统项目源码:这套经典项目到底解决什么问题
“基于javaweb图书馆管理系统项目源码”这个标题,在搜索栏里出现的频率一直很高。它对应的是JavaWeb最常见的一类教学级项目——用 JSP、Servlet 和 MySQL 搭建完整的图书借还系统,覆盖管理员登录、图书分类管理、图书录入修改、借书还书、逾期记录这些核心流程。我最早接触这个方向是在帮某高校的课程设计做辅导时,后来又帮某公司图书室做过内部版本。这类系统适合谁?想跑通第一个完整Web项目的初学者、需要交课程设计的学生、手头有几百本书需要简单管理的小型图书室。它不炫技,但胜在链路完整,能把 JavaWeb 的骨架真正撑起来。
2. 用 MySQL + Servlet + JSP 把系统跑起来:技术选型与最小启动步骤
这个标题看起来是“源码”,但源码到手第一步不是读代码,而是把环境搭对。JavaWeb 图书管理系统在技术选型上基本都是同一套老组合:Servlet 做控制层,JSP 做展示层,JDBC 操作 MySQL。下面拆开讲。
2.1 为什么还是 Servlet + JSP:这套技术栈的边界在哪里
很多刚接触的人会问:都什么年代了,怎么不用 Spring Boot?这个问题要分场景回答。图书管理系统是典型的业务型 CRUD 项目,数据量小、并发低、逻辑不复杂,Servlet + JSP 完全够用。更重要的是,这个标题面向的人群是刚学完 JavaWeb 基础的学习者,如果直接上 Spring Boot,反而把请求生命周期、会话管理、JDBC 这些底层概念包在了框架里,学完只知道自己写了几个接口,说不清一个请求从浏览器到数据库到底经过了几层。
我一般建议这类项目保持三层结构:Servlet 接收请求并调用业务方法,DAO 负责数据库增删改查,JavaBean 承载实体数据。JSP 只做页面渲染。这样的好处是,每一层都能独立测试,任何一个地方出问题,日志一打就能定位到层。坏处也很明显:没有事务注解、没有依赖注入,代码里会出现不少样板代码,事务要手动管,连接要手动关。但恰恰是这些“麻烦”,才是这个项目真正的学习价值。
选型上我常用的组合如下表:
| 层次 | 选型 | 选型理由 |
|---|---|---|
| 展示层 | JSP + EL + JSTL | Servlet 输出 HTML 太痛苦,JSP 天生适合做页面模板,EL 表达式取数据简洁 |
| 控制层 | Servlet 3.0 注解版 | 不写 web.xml 里的 Servlet 映射,用 @WebServlet 注解,减少配置 |
| 数据访问 | JDBC + DAO | 不引入 MyBatis,SQL 手写,更直观理解数据库操作 |
| 数据库 | MySQL 5.7 或 8.0 | 资料多、安装简单,适合教学项目 |
| 服务器 | Tomcat 8.5 或 9 | 兼容 javax.servlet 命名空间,教程资料最齐全 |
| 构建工具 | Maven 或手动导 jar | 如果源码带了 pom.xml 就用 Maven,没有就直接拷 jar 到 WEB-INF/lib |
2.2 项目目录结构与三层分包:拿到源码先看这四层
拿到一份源码后,不要急着启动,先看目录结构。一份规范的 JavaWeb 图书管理系统源码,目录结构应该是这样的:
src/ com/library/filter/ AuthFilter.java 登录拦截过滤器 com/library/servlet/ LoginServlet.java 登录与退出 BookServlet.java 图书增删改查与分页 BorrowServlet.java 借书还书处理 CategoryServlet.java 分类管理 com/library/dao/ BookDao.java 图书表操作 BorrowDao.java 借阅记录表操作 ReaderDao.java 读者表操作 AdminDao.java 管理员表操作 com/library/entity/ Book.java 图书实体 BorrowRecord.java 借阅记录实体 Reader.java 读者实体 com/library/util/ DBUtil.java 数据库连接工具 web/ login.jsp 登录页 book_list.jsp 图书列表页 book_edit.jsp 图书新增/编辑页 borrow_list.jsp 借阅记录页 index.jsp 首页跳转 WEB-INF/lib/ mysql-connector jar 或其它依赖这里有个关键点:源码中 util 包下的 DBUtil.java 是所有 DAO 的依赖入口。我见过不少新手改代码时,第一件事就是把数据库密码改成自己的,结果发现改了不生效——因为 DBUtil 里可能读的是 classpath 下的 jdbc.properties,而不是写死在 Java 文件里的字符串。看源码时先找到 DBUtil,看它到底是读配置文件还是硬编码,这一步决定了后续所有数据库操作能不能跑通。
2.3 用 IDEA 在本地跑通的最小步骤:从导入到看见登录页
如果是带 Maven 的源码,导入非常简单;如果是直接放 jar 的老式 Web 项目,需要手动配置。最常见的做法是按下面这几步走:
# 1. 确保本地已安装 JDK 8 或 11,Tomcat 8.5/9,MySQL 5.7+ java -version mvn -version # 如果是 Maven 项目,用 mvn install 先构建用 IDEA 打开源码目录(如果是 Maven 项目,选择 pom.xml 导入)。 在 Project Structure -> Libraries 里确认是否已引入 WEB-INF/lib 下的 jar。 编辑运行配置,添加 Tomcat Server -> Local,Deployment 里选择 Artifact -> 以 war exploded 方式部署。 设置 Application context 为 /library,然后启动 Tomcat。 浏览器访问 http://localhost:8080/library/login.jsp这里 Application context 的值很关键。代码里跳转会经常用到req.getContextPath(),如果你的 context 不是 /library,登录成功后的跳转地址就会 404。这个值要和源码里 DBUtil 或 web.xml 里注释提示的路径保持一致。如果源码没注释,就统一用 /library。
参数说明:编码方面,IDEA 里要确保 File Encoding 是 UTF-8,Tomcat 的 server.xml 里<Connector>标签建议加上URIEncoding="UTF-8"。数据库连接串如果是 MySQL 8.0,必须在 url 后面追加useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8,否则连上后中文乱码、SSL 报错会一起找上来。
3. 图书管理系统数据库设计:E-R 梳理与五张核心表的建表 SQL
做管理系统类的开发,数据库设计永远是第一步。很多源码跑不起来,不是代码问题,而是数据库脚本没执行对,或者表结构缺字段。这章把表结构和核心 SQL 写清楚,你拿到任何同类源码都能对照着检查。
3.1 数据模型怎么定:先画关系再写 SQL
图书管理系统最少需要五张表:管理员表、图书分类表、图书表、读者表、借阅记录表。这五张表之间的核心关系是:借阅记录表是中间表,同时关联图书表和读者表;图书表通过 category_id 关联分类表。用户角色上,管理员和读者分开登录。
借阅记录表是这个项目的灵魂。它不能只记录“谁借了什么书”,还要记录借出时间、应还时间、实际归还时间、当前状态。状态字段我一般用整数:0 表示在借、1 表示已还、2 表示逾期未还。为什么不用字符串?因为整数在 SQL 里比较和统计都方便,WHERE status = 0一眼就能看出所有未还记录,而且后期做逾期提醒时可以直接用due_time < NOW() AND status = 0作为筛选条件。
图书表的库存字段要注意区分“总库存”和“当前可借数量”。这个差别在借书逻辑里非常重要:总库存是采购时录入的数量,可借数量会随着借出、归还动态变化。如果设计时只留一个数字,还书时就要做加减判断,很容易出 bug。正确做法是设计total(总册数)和stock(当前可借数)两个字段,借书时 stock 减 1,还书时 stock 加 1,total 保持不变。
3.2 建表 SQL 与字段说明:五张表一次到位
下面是一套可以直接执行的建表 SQL,字符集统一用 utf8mb4,排序规则 utf8mb4_unicode_ci。用 utf8mb4 而不是 utf8,是为了能存生僻字和特殊符号,图书书名里偶尔会出现这类字符。
-- 管理员表 CREATE TABLE admin_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '存 MD5 或 SHA-256 摘要,不存明文', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 图书表 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(30) UNIQUE COMMENT 'ISBN 号,尽量加唯一索引', name VARCHAR(200) NOT NULL, author VARCHAR(100), publisher VARCHAR(100), category_id INT, total INT DEFAULT 1 COMMENT '总册数', stock INT DEFAULT 1 COMMENT '当前可借数量', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 读者表 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅记录表 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', due_time DATETIME COMMENT '应还时间', return_time DATETIME COMMENT '实际归还时间', status TINYINT DEFAULT 0 COMMENT '0在借 1已还 2逾期', CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), INDEX idx_borrow_status (status), INDEX idx_borrow_reader (reader_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:ON UPDATE CURRENT_TIMESTAMP是 MySQL 5.6.5 以后才支持的写法,老版本数据库执行会报语法错误,如果用的是 MySQL 5.5,要去掉这一句。due_time应还时间没有用默认值,是因为它要在借书成功时由代码计算生成,一般是borrow_time + 30 天,这个天数可以做成管理员可配置的参数,但起步阶段写死为 30 即可。
外键约束在这个项目里建议加上。很多老源码为了省事会去掉外键,由代码层面维护关联关系。我个人的习惯是表结构里保留外键,因为图书管理系统数据量小,外键带来的性能影响可以忽略,但数据完整性有保障,不会出现借阅记录指向一张不存在的图书的情况。
3.3 测试数据准备:三条 SQL 让页面不白屏
建完表后第一件事是插入测试数据。很多源码跑起来后页面空白,不是代码问题,而是数据库里一张空表,列表页查不到任何记录。插入数据时要注意一个细节:管理员密码不要用明文。
-- 分类 INSERT INTO category (name) VALUES ('计算机'), ('文学'), ('历史'); -- 图书 INSERT INTO book (isbn, name, author, publisher, category_id, total, stock) VALUES ('978-7-111-00000-0', 'Java Web 开发实战', 'A同学', '某出版社', 1, 5, 5), ('978-7-111-00000-1', '数据库系统概论', 'B老师', '某出版社', 1, 3, 3); -- 管理员和读者,密码用 MD5 摘要(测试环境可用) INSERT INTO admin_user (username, password) VALUES ('admin', MD5('123456')); INSERT INTO reader (username, password, real_name, phone) VALUES ('reader01', MD5('123456'), '测试读者', '13800000000');密码用MD5('123456')是测试环境下的偷懒做法。正式一点的做法是在 Java 代码里用 MessageDigest 生成 SHA-256 摘要后再入库,这样数据库里永远不会出现明文密码。源码里如果登录时是对密码做同样的摘要算法再比对,就说明它的设计是密文存储;如果源码里直接拿用户输入去查库,那说明这套源码本身就不安全,拿到后要自己改掉。判断方法很简单:找到登录代码,看它拼 SQL 时 password 字段是怎么处理的。
4. 核心业务代码落地:登录会话、图书维护与借还事务
环境跑通、表结构建好之后,就到了最核心的部分——业务代码怎么把整个流程串起来。这章不打算逐行贴完整源码,而是把这类系统里最关键的三个模块的写法讲透:登录会话控制、图书分页查询、借还书事务。这三个模块写懂了,剩下分类管理、读者管理都是同一套模式的重复。
4.1 登录与会话控制:用 Filter 拦住未登录请求
登录模块很多源码都写得简单——验证用户名密码对了就跳转,错了就提示。但一个合格的图书管理系统,登录后必须解决“会话保持”的问题:用户关掉浏览器再打开,或者直接访问内页 URL,系统都应当识别出未登录状态并踢回登录页。这个逻辑靠 Servlet 的 Session 和 Filter 配合完成。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 对用户输入做 MD5 摘要后再比对 String hashedPwd = MD5Util.md5(password); AdminUser user = adminDao.findByUsernameAndPwd(username, hashedPwd); if (user != null) { req.getSession().setAttribute("loginUser", user); req.getSession().setAttribute("role", "admin"); resp.sendRedirect(req.getContextPath() + "/book/list?pageNum=1"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }登录写完后,过滤器负责拦截:
@WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 放行登录接口、登录页面和静态资源 if (uri.endsWith("/login") || uri.endsWith("/login.jsp") || uri.contains("/static/") || uri.endsWith(".css") || uri.endsWith(".js")) { chain.doFilter(request, response); return; } Object loginUser = req.getSession().getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }逻辑说明:LoginServlet 里查询成功后把用户对象放进 Session,跳转到图书列表页。AuthFilter 配置成拦截所有请求,然后按 URL 后缀放行登录页和静态资源,其余请求一律检查 Session 里有没有 loginUser,没有就重定向到登录页。这个设计的好处是新增页面不需要改动过滤器,天然受保护。
参数说明:MD5Util.md5()是源码里常见的工具类方法,没有的话自己写一段 MessageDigest 的封装即可。req.getContextPath()必须用,它会让重定向地址自动带上部署上下文名,不会因为 Tomcat 部署名变化而 404。Session 过期时间默认是 30 分钟,如果业务需要更长时间,可以在 web.xml 里配置 session-timeout。
4.2 图书管理:分页查询与新增编辑的后端写法
图书列表是系统里最常被访问的页面,如果一次性查询全部数据,表里上千条记录就会让页面卡顿。分页是必须的。Servlet 接收 pageNum 参数,DAO 用 LIMIT 做分页查询,同时 COUNT 计算总页数回传页面。
@WebServlet("/book/list") public class BookListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNum = 1; int pageSize = 10; String keyword = req.getParameter("keyword"); if (keyword == null) { keyword = ""; } try { pageNum = Integer.parseInt(req.getParameter("pageNum")); } catch (NumberFormatException e) { pageNum = 1; } // 调用 DAO 查询 int total = bookDao.count(keyword); int totalPage = (total + pageSize - 1) / pageSize; if (pageNum > totalPage) { pageNum = totalPage; } List<Book> list = bookDao.findPage(keyword, (pageNum - 1) * pageSize, pageSize); req.setAttribute("list", list); req.setAttribute("totalPage", totalPage); req.setAttribute("pageNum", pageNum); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/book_list.jsp").forward(req, resp); } }对应的 DAO 方法:
public List<Book> findPage(String keyword, int offset, int limit) { String sql = "SELECT b.*, c.name AS category_name FROM book b " + "LEFT JOIN category c ON b.category_id = c.id " + "WHERE b.name LIKE ? OR b.author LIKE ? " + "ORDER BY b.id DESC LIMIT ?, ?"; // 使用 PrepareStatement 拼接参数,防止 SQL 注入 }逻辑说明:分页参数 pageNum 从请求里解析,pageSize 固定为 10。计算总页数用的公式(total + pageSize - 1) / pageSize是整数向上取整的经典写法,避免最后一页丢失数据。keyword 的模糊查询用 LIKE 拼接,但一定要用 PreparedStatement 的占位符传参,不要直接拼接字符串,否则用户输入%或单引号会破坏 SQL 结构。
新增和编辑是同一个 JSP 页面,根据 book 对象是否为空决定是新增还是回显编辑。这个模式叫“表单复用”,图书管理系统里很常见。判断依据是 URL 里带不带 id:带 id 就是编辑,先根据 id 查出旧数据回填 form;不带就是新增,表单留空。提交动作统一走/book/save,Servlet 里判断 id 是否为空再决定执行 insert 还是 update。分类下拉框的数据在进入 book_edit.jsp 前加载,用 request 属性传给 JSP。
4.3 借书还书:一个事务里完成的两步写库
借书是整个系统里最容易出 bug 的地方,因为表面上看只是“插入一条借阅记录”,实际上涉及两次数据库写操作:插入借阅记录 + 图书库存减 1。两次操作必须作为一个整体成功或失败,否则会出现记录了借出但库存没减,或者库存减了但没有借阅记录的情况。这个整体性就是事务。
public void borrowBook(int bookId, int readerId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 检查库存是否充足 int stock = bookDao.getStock(conn, bookId); if (stock <= 0) { throw new RuntimeException("库存不足"); } // 2. 插入借阅记录,应还时间为当前时间 + 30 天 borrowDao.insert(conn, bookId, readerId, new Date(), new Date(System.currentTimeMillis() + 30L * 24 * 3600 * 1000)); // 3. 扣减库存 bookDao.decreaseStock(conn, bookId); conn.commit(); // 全部成功才提交 } catch (Exception e) { conn.rollback(); // 任何一步失败都回滚 throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明:关键点在于三个 DAO 方法都接收同一个 Connection 参数。很多新手写的源码是每个 DAO 方法内部自己 getConnection、自己 close,这样即使写了 try-catch 也无法回滚,因为每次 getConnection 拿到的是不同连接,事务根本不在同一个连接上。正确做法是事务由 Service 层控制,获取一个连接后一路传到底层 DAO,全部执行完统一次 commit 或 rollback,最后在 finally 里把连接放回连接池或关闭。
还书是相反的流程:更新借阅记录状态 + 图书库存加 1。这里有一个容易被忽略的细节:还书时要判断是否逾期,更新 status 为 1(已还)还是 2(逾期)。判断标准是实际归还时间晚于 due_time。这个判断放在 Service 层做,不要放进 SQL 里,因为逾期状态可能需要后续再做短信、邮件等提醒动作,代码层面处理更灵活。
5. JavaWeb 图书管理系统最常见的 5 个坑:现象、原因与解决办法
这类项目的代码结构大同小异,出问题的地方也高度一致。下面的踩坑记录来自我给几个同学改代码时的真实经历,基本覆盖了从“跑不起来”到“跑起来但结果不对”的所有典型故障。
5.1 启动后访问一直 404:部署名和访问路径对不上
现象:Tomcat 启动日志显示部署成功,但浏览器访问登录页始终 404,控制台无任何异常。原因:IDEA 部署时的 Application context 和代码里重定向用的路径不一致。比如代码里跳转写的是/library/login.jsp,但部署名默认是/项目名_war_exploded,路径就完全对不上。解决:设置 Deployment 的 Application context 为/library,或者统一修改代码把/library全部替换为req.getContextPath()。
5.2 页面中文全变问号或乱码:三层编码只改了一层
现象:数据库里中文正常,但页面显示???,或者反过来页面正常但入库后变成问号。原因:连接串没加characterEncoding=UTF-8,或 JSP 的 pageEncoding 写的是 ISO-8859-1,或 Tomcat 接收 POST 请求参数时没设置字符集。这是三个独立的位置,只改一处没用。解决:JSP 顶部统一<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>,数据库连接串追加characterEncoding=UTF-8,Servlet 里接收参数前调用req.setCharacterEncoding("UTF-8")。
5.3 借书成功率不稳定,偶尔借阅记录和库存对不上:事务边界不清
现象:借书操作有时提示成功但库存没减,有时库存减了但查不到借阅记录。原因:借阅记录的 insert 和库存的 update 分别在两个连接上执行,没有包在同一个事务里,中间任何一步抛异常就出现数据不一致。解决:参考上一章的写法,所有数据库操作共用一个 Connection,设置setAutoCommit(false),全部成功后再 commit,异常时 rollback。检查源码时重点看 DAO 方法是否接收 Connection 参数,如果每个方法自己建连接,说明事务是断裂的。
5.4 页面卡顿越来越严重,报 Too many connections:连接泄漏
现象:系统刚启动正常,用一段时间后所有页面响应变慢,数据库日志报连接数超限。原因:代码里获取了 Connection 但没有在 finally 里关闭,或者 close 的是包装类而不是真实连接。Servlet 每次请求都新建连接,用完不还,连接池的活跃连接数一路涨到上限。解决:在 DAO 和 Service 的 finally 块里关闭连接,或者用 try-with-resources 语法让连接自动关闭。写完代码后打开 MySQL 的SHOW PROCESSLIST,看看有没有大量 Sleep 状态连接堆积,有就说明泄漏点还没找完。
5.5 ClassNotFoundException: com.mysql.jdbc.Driver:JDBC 驱动没加载进来
现象:启动 Tomcat 后第一次访问数据库相关页面,控制台直接抛ClassNotFoundException。原因:mysql-connector-java 的 jar 没有放到 WEB-INF/lib 目录,或者放到了但 IDEA 的 Artifact 没同步。常见误区是把 jar 放在 Project Structure 的 Libraries 里,但没确认它被打包进输出目录。解决:把 jar 拷贝到web/WEB-INF/lib/下,然后在 Artifact 配置里勾选 Include in project build。MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,5.x 才是com.mysql.jdbc.Driver,类名写错同样会报这个异常。
6. 从能用走向好用:批量导入、逾期提醒与统计报表的轻量加装
核心流程跑通之后,这个系统已经能应付日常借还管理了。但回过头看,图书录入全靠手动一条条填、有没有逾期图书全靠人工翻记录,这两件事在真实使用场景里最浪费体力。这一章分享我一般会给这类系统加的三个实用功能,都是在现有结构上小改动就能完成的。
第一个是 Excel 批量导入图书。图书室采购后数据往往在 Excel 里,手动录入几十上百条太痛苦。常见做法是用 Apache POI 的WorkbookFactory读取上传的 xlsx 文件,循环遍历每一行,把单元格值映射到 Book 实体的属性。代码结构大致是:
Workbook wb = WorkbookFactory.create(new FileInputStream(uploadFile)); Sheet sheet = wb.getSheetAt(0); for (int i = 1; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); if (row == null) { continue; } Book book = new Book(); book.setIsbn(row.getCell(0).getStringCellValue()); book.setName(row.getCell(1).getStringCellValue()); book.setAuthor(row.getCell(2).getStringCellValue()); // 逐条插入前先查 ISBN 是否已存在,避免重复入库 bookDao.insert(book); }这里要注意,POI 读取单元格时,数字列会返回 numeric 类型,直接调getStringCellValue()会抛异常,所以读取前要根据CellType做类型判断。批量导入的重点是去重校验,否则同一本书导入两次,库存就翻倍了。
第二个是逾期未还清单。这个功能一条 SQL 就能查出来,不需要额外建表:
SELECT r.real_name AS reader_name, b.name AS book_name, br.due_time, DATEDIFF(NOW(), br.due_time) AS overdue_days FROM borrow_record br JOIN reader r ON br.reader_id = r.id JOIN book b ON br.book_id = b.id WHERE br.status = 0 AND br.due_time < NOW() ORDER BY overdue_days DESC;按 overdue_days 排序,最严重的逾期排在最前面。把这个查询做成一个单独的 Servlet 映射,页面上多一个菜单入口,管理员每天打开就能看到全部逾期情况,比翻纸质记录高效得多。
第三个是借阅排行,用于了解哪些书流通量高。同样是一条聚合 SQL:
SELECT b.name, b.author, COUNT(*) AS borrow_count FROM borrow_record br JOIN book b ON br.book_id = b.id GROUP BY br.book_id ORDER BY borrow_count DESC LIMIT 10;三个功能加完,这套系统才真正从“课程设计”走到了“能日常用”。我自己的习惯是每改完一个功能,先备份一次数据库再继续下一个,防止改崩了没有后悔药。图书管理系统这种项目,难点从来不在代码量,而在事务边界和会话控制这两个地方想没想清楚。把这两块琢磨透了,以后做任何 JavaWeb 项目都能少走弯路,希望帮到你。
本文还有配套的精品资源,点击获取