☰
基于Web的图书管理系统:期末大作业从技术选型到部署避坑全攻略
2026/10/6 20:59:28 网站建设 项目流程

简介:基于Web的图书管理系统期末大作业完整方案,包含前端页面、后端业务逻辑与数据库设计,覆盖用户登录、图书管理、借阅管理等典型功能,适合计算机专业学生完成课程设计或期末项目参考。压缩包共91个文件,大小8.05MB,文件类型以Java源码和JSP页面为主,另有CSS/JS前端资源、SQL数据库脚本、配置文件及项目说明文档;目录结构清晰,包含src源码目录与WebRoot静态资源目录,便于导入IDE运行调试。当前已有1316人学习下载,资源附带用户表、图书表、借阅表等SQL脚本,可快速还原数据库环境,配合项目说明文档梳理的模块划分与部署步骤,能帮助读者理解系统整体架构并快速上手二次开发。整体设计完整,模块划分合理,既可用于期末作业提交与答辩展示,也可作为功能扩展和实战练手的基础版本。

1. 基于Web的图书管理系统:为什么期末大作业都选它

每到期末,Web开发类课程的最后一个关卡,总有一批学生对着同一个题目挠头:基于Web的图书管理系统。它看起来不起眼,却是历届作业里出现频率最高的一个,几乎可以称得上是 Web 期末大作业的“守门员”。原因很直白:一个图书管理系统恰好把 Web 开发从数据库设计到前后端联调的所有环节都覆盖了——图书的新增、查询、删除,读者的登记与状态管理,借书和还书两条业务线,规模控制在两三周能完成,复杂度又足够撑起答辩。源码加数据库一起交付,老师能在本地直接跑起来看效果,你也能拿到一份能讲清楚每一行代码的完整工程。

2. 技术选型与项目架构:期末Web图书管理系统用什么做最稳

做基于Web的图书管理系统,第一件事不是找源码,是定技术路线。技术路线决定你后面所有的工作量,也决定老师打开项目时的第一印象。

2.1 图书管理系统的主流Web技术栈对比:JSP、PHP还是Spring Boot

期末大作业里最常见的三种路线:JSP + Servlet + MySQL,PHP + MySQL,以及 Spring Boot + Thymeleaf + MySQL。客观说,没有绝对的好坏,只有“和你的课程匹配度”的差别。

技术栈上手成本环境复杂度老师偏好适合场景
JSP + Servlet中需要 Tomcat,配置较繁琐教材用 Java Web 时首选计算机专业 Java 方向的经典作业
PHP + MySQL低装 phpStudy 或 XAMPP 即可能跑即可,验收偏展示非科班、快速交差
Spring Boot + Thymeleaf高Maven 依赖常出问题学过框架的课程更认可要求分层规范、想写进简历的
Python + Flask/Django中需配虚拟环境选修课的常见选择Python 课设

我见过太多学生拿着一个包装精美的 Spring Boot 项目去答辩,结果老师问一句“依赖怎么下的”就卡住了。期末大作业的选型原则应该是:你熟悉的优先,而不是看起来高级的优先。如果你的课程就是 Java Web,老老实实用 JSP + Servlet,这既是课程要求,也是最好解释的方案。往下讲,我都以 JSP + Servlet + MySQL 这条最经典的路线为例,因为这套组合的源码和数据库脚本最容易在本地完整跑起来。

2.2 数据库设计:图书、读者、借阅三张核心表的字段与关系

基于Web的图书管理系统本质上是一个“数据库增删改查”的壳,数据库设计决定了系统能撑起多少业务逻辑。最少需要三张表:图书表 book、读者表 reader、借阅表 borrow。借阅表是关联表,它同时引用图书主键和读者主键,形成多对多关系。

-- 创建图书管理系统的数据库,注意字符集选 utf8mb4,避免中文乱码 CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_db; -- 图书表:存书籍基础信息与库存 CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID,自增主键', book_name VARCHAR(100) NOT NULL COMMENT '书名,不能为空', author VARCHAR(50) DEFAULT NULL COMMENT '作者', publisher VARCHAR(50) DEFAULT NULL COMMENT '出版社', isbn VARCHAR(20) DEFAULT NULL COMMENT 'ISBN编号', total_count INT DEFAULT 1 COMMENT '馆藏总量', remaining_count INT DEFAULT 1 COMMENT '当前可借数量,每次借出后减一', publish_date DATE DEFAULT NULL COMMENT '出版日期' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 读者表:读者的账号信息与状态 CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '读者ID', reader_name VARCHAR(50) NOT NULL COMMENT '读者姓名', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录用户名,唯一约束', password VARCHAR(50) NOT NULL COMMENT '登录密码,期末作业可明文,真实项目必须加密', status TINYINT DEFAULT 1 COMMENT '状态:1正常 0禁用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 借阅表:记录每一笔借书、还书操作 CREATE TABLE borrow ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '借阅记录ID', book_id INT NOT NULL COMMENT '外键,关联book表的id', reader_id INT NOT NULL COMMENT '外键,关联reader表的id', borrow_date DATE NOT NULL COMMENT '借出日期', due_date DATE NOT NULL COMMENT '应还日期,通常借出日期加30天', return_date DATE DEFAULT NULL COMMENT '实际归还日期,未归还时为NULL', status TINYINT DEFAULT 0 COMMENT '状态:0借出中 1已归还 2已逾期', KEY idx_book_id (book_id), KEY idx_reader_id (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这套表结构我做了两个容易被忽略的设计,一个是图书表里的remaining_count,一个是借阅表里的due_date。remaining_count记录“还能不能借”,每次借书成功都要减一,还书加一,这是图书管理系统的核心业务状态。due_date是应还日期,有了它,“逾期”这个功能才能算出来,而不是靠手写死。

2.3 源码目录结构:Web项目的分层与文件清单

拿到一份图书管理系统的 Web 工程,先别急着运行,把目录结构读明白比什么都重要。一个标准的 JSP + Servlet 项目是 Maven 结构或传统 Dynamic Web Project 结构,前者有src/main/java,后者直接是src。无论哪种,核心分层都一致:

library-system/ ├── src/ │ ├── com/example/entity/ // 实体类:Book.java, Reader.java, Borrow.java │ ├── com/example/dao/ // 数据库操作层:BookDao.java, ReaderDao.java │ ├── com/example/servlet/ // 控制器:BookServlet.java, BorrowServlet.java │ └── com/example/util/ // 工具类:DBUtil.java 数据库连接 ├── webapp/ │ ├── admin/ // 后台管理页面:book_list.jsp, book_add.jsp │ ├── reader/ // 读者端页面:index.jsp, borrow.jsp │ ├── WEB-INF/ │ │ ├── web.xml // 部署描述符,配置欢迎页和Servlet映射 │ │ └── lib/ // 数据库驱动:mysql-connector-java.jar │ ├── login.jsp │ └── index.jsp └── database/ └── library.sql // 数据库脚本,含建库建表和初始数据

这里最关键的认知是:JSP 页面放在 webapp 下可以直接被访问,而 WEB-INF 下的东西浏览器永远访问不到。很多学生在期末翻车,就是因为把 JSP 放到了 WEB-INF 里面,然后访问页面时报 404,还以为是源码有问题。另外,数据库驱动 jar 包必须放在WEB-INF/lib下,Tomcat 启动后才会加载它,这是新手最容易漏的一步。理解了目录结构,后面的部署排查才能有条理地走。

3. 把源码跑起来的完整过程:环境准备与最小启动命令

“源码下载下来了,数据库文件也给了,但我跑不起来。”这是我听到最多的一句话。基于Web的图书管理系统跑不起来,九成是环境问题和路径问题,不是代码问题。这一章按顺序做,每一步都有对应的检查点。

3.1 环境准备:JDK、MySQL、Tomcat版本怎么配

图书管理系统是典型的 Java Web 项目,环境就三个组件:JDK 8、MySQL 5.7 或 8.0、Tomcat 8.5 或 9。版本不需要追求最新,反而要避开太新的组合,比如 JDK 17 配老 Tomcat 会出现模块访问报错。我一般建议:JDK 8 + Tomcat 8.5 + MySQL 5.7,这组合最稳,网上能找到的排错案例也最多。

在命令行验证环境是否就绪:

# 检查 JDK 版本,能看到 1.8 即可 java -version # 检查 MySQL 服务是否启动,Linux/Mac 下用这个 mysqladmin -u root -p status # Windows 下确认 8080 端口没被占用 netstat -ano | findstr :8080

三个命令分别对应 Java 运行环境、数据库状态、Web 端口三个关键检查点。findstr :8080如果没有任何输出,说明 8080 空闲,Tomcat 能正常启动;如果输出了 PID,说明端口被占,先处理端口问题再继续。

3.2 初始化数据库:用命令行导入数据库脚本

数据库脚本通常是一个.sql文件,里面已经写好了建库、建表和初始数据。拿到它之后,直接在 MySQL 命令行里执行即可,注意 Windows 下文件路径不要含中文,否则 MySQL 可能读不到。

# 进入 MySQL,输入密码后进入命令行 mysql -u root -p # 在 MySQL 命令行中执行脚本文件 # source 后面是 SQL 文件的绝对路径 source D:/work/library_db.sql; # 查看数据库是否创建成功 SHOW DATABASES; # 进入图书管理系统数据库 USE library_db; # 查看三张核心表是否都在 SHOW TABLES;

source是 MySQL 自带的命令,比图形化工具更可靠。执行后如果看到Query OK就说明建表成功。有些数据库脚本会带DROP TABLE IF EXISTS,重复执行不会报错,这是好事,说明作者考虑到了二次导入的场景。导入后建议顺手执行一次SELECT * FROM book;,看看初始数据里有没有中文乱码。如果显示正常中文,说明数据库字符集没问题,后面乱码排查就少一个环节。

3.3 修改数据库连接配置:用户名、密码、时区参数

源码里的数据库连接配置几乎不可能和你本机的 MySQL 账号完全一致,所以这一步是必改项。连接配置通常集中在一个DBUtil.java或db.properties文件里。如果是DBUtil.java,你会看到类似下面的代码:

// 数据库连接工具类,整个Web工程唯一的连接入口 public class DBUtil { // 数据库驱动类名,MySQL 5.7 和 8.0 通用 private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // 连接地址:localhost 是本机,3306 是 MySQL 默认端口 // 库名是 library_db,如果脚本里建的是别的库名,这里要改 private static final String URL = "jdbc:mysql://localhost:3306/library_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; // 本机 MySQL 的用户名,通常是 root private static final String USER = "root"; // 改成你自己 MySQL 的密码,别用原作者的 private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { try { // 注册驱动,只执行一次 Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new SQLException("MySQL驱动加载失败,请检查 WEB-INF/lib 下的 jar 包"); } // 每次调用返回一个新的数据库连接 return DriverManager.getConnection(URL, USER, PASSWORD); } }

三个参数按你的实际环境改:USER改成你的 MySQL 用户名,PASSWORD改成你的真实密码,URL里的library_db改成你导入的数据库名。serverTimezone=Asia/Shanghai这个参数不要删,MySQL 8.0 默认时区是 UTC,不指定的话,Java 连接时会直接抛时区异常,这是新版 MySQL 的著名坑。

3.4 部署到 Tomcat 并启动 Web 服务

部署这一步,新手最容易在 IDE 里绕晕。最常见的做法是把 Web 工程打包成 WAR 文件,或者直接把整个 webapp 目录扔到 Tomcat 的 webapps 下。如果源码给的是完整工程,用 IDEA 或 Eclipse 导入后,配置好 Tomcat 运行环境直接启动即可。

# 进入 Tomcat 的 bin 目录 cd D:/apache-tomcat-8.5.100/bin # 启动 Tomcat(Windows 下) startup.bat # 启动 Tomcat(Linux/Mac 下) ./startup.sh # 启动后检查日志,看到 Server startup 才是成功 tail -f D:/apache-tomcat-8.5.100/logs/catalina.out

启动成功的标志不是浏览器能打开页面,而是日志里出现Server startup in [xxxx] milliseconds。很多人 Tomcat 启动了,页面白屏,就是因为项目部署名不对。默认localhost:8080访问的是 ROOT 项目,你的图书管理系统如果部署名是library-system,访问路径应该是http://localhost:8080/library-system/login.jsp。

4. 前端页面与后端接口怎么对上:源码结构逐层拆解

跑起来之后,接下来的问题是:这份源码我能不能讲清楚?期末答辩最怕的就是“代码不是自己写的,一问三不知”。把前端到数据库这条链路走一遍,你就能讲出逻辑。

4.1 前端 JSP 页面:表单提交与页面跳转

图书管理系统的主页面通常是登录页 + 图书列表页 + 借阅页。JSP 页面本质上是一个嵌了 Java 代码的 HTML,但期末作业里最常见的写法是用 JSTL 和 EL 表达式来渲染数据,这比在页面里写一堆<% %>看起来规范得多。

<!-- 图书新增页面的核心表单:提交到 /book/add 这个路径 --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>新增图书</title> </head> <body> <h2>新增图书</h2> <!-- 表单的 action 决定了提交给哪个 Servlet --> <form action="${pageContext.request.contextPath}/book/add" method="post"> <div> <label>书名:</label> <!-- name 属性是后端取参数的依据,改名后后端也要同步改 --> <input type="text" name="bookName" required> </div> <div> <label>作者:</label> <input type="text" name="author"> </div> <div> <label>出版社:</label> <input type="text" name="publisher"> </div> <div> <label>馆藏总量:</label> <!-- 数字输入框,后端会做一次 parseInt --> <input type="number" name="totalCount" min="1" value="1"> </div> <button type="submit">保存</button> </form> </body> </html>

${pageContext.request.contextPath}是动态获取项目部署路径,这样无论项目叫什么名字,表单提交地址都不会因部署名变化而失效。表单里每个输入框的name属性就是后端request.getParameter()要用的键,前后端不一致是联调最常见的低级错误。

4.2 Servlet 控制器:请求接收与业务分发

Servlet 是前端和数据库之间的中间层,它负责三件事:接收请求参数、调用 DAO 层方法、决定跳转到哪个页面。

// 新增图书的Servlet,路径和前端表单的action一致 @WebServlet("/book/add") public class BookAddServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 必须放在最前面,处理POST请求的中文乱码 req.setCharacterEncoding("UTF-8"); // 1. 从请求中取出表单参数 String bookName = req.getParameter("bookName"); String author = req.getParameter("author"); String publisher = req.getParameter("publisher"); String isbn = req.getParameter("isbn"); // 前端传来的字符串,需要转换成整数 int totalCount = Integer.parseInt(req.getParameter("totalCount")); // 2. 组装实体对象 Book book = new Book(); book.setBookName(bookName); book.setAuthor(author); book.setPublisher(publisher); book.setIsbn(isbn); book.setTotalCount(totalCount); // 新书的可借数量等于馆藏总量 book.setRemainingCount(totalCount); // 3. 调用 DAO 层完成数据库插入 BookDao dao = new BookDao(); int rows = dao.insert(book); // 4. 根据结果做页面跳转 if (rows > 0) { // 用重定向,避免刷新时表单重复提交 resp.sendRedirect(req.getContextPath() + "/book/list"); } else { req.setAttribute("msg", "新增图书失败,请检查数据"); req.getRequestDispatcher("/book/add.jsp").forward(req, resp); } } }

这个 Servlet 的写法是标准的“接收—处理—跳转”三段式。注意最后的分支:新增成功用sendRedirect跳转到列表页,失败用forward回到表单页并携带错误提示。sendRedirect和forward的区别在于,前者浏览器地址栏会变化且刷新不会重复提交,后者地址栏不变,适合带错误信息返回原页面。

4.3 数据库操作层:JDBC 增删改查与预编译语句

DAO 层是访问数据库的唯一入口。图书管理系统里,BookDao 至少要有 insert、deleteById、findAll、findByName 四个方法,对应图书管理的增删查三个核心功能。

// 图书数据库操作层:封装所有sql语句 public class BookDao { // 按书名模糊查询图书,支持列表页的搜索功能 public List<Book> findByName(String keyword) { List<Book> list = new ArrayList<>(); // SQL语句用?占位符,防止SQL注入 String sql = "SELECT * FROM book WHERE book_name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // %keyword% 实现模糊匹配,注意%号是拼接进参数里的 ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPublisher(rs.getString("publisher")); book.setRemainingCount(rs.getInt("remaining_count")); list.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 新增图书,返回受影响的行数,1表示成功 public int insert(Book book) { String sql = "INSERT INTO book (book_name, author, publisher, isbn, total_count, remaining_count) VALUES (?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setString(3, book.getPublisher()); ps.setString(4, book.getIsbn()); ps.setInt(5, book.getTotalCount()); ps.setInt(6, book.getRemainingCount()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }

这里必须强调用PreparedStatement而不是直接拼接 SQL。原因有两个:一是?占位符会自动处理字符串中的单引号和特殊字符,不会破坏 SQL 结构;二是代码可读性强,别人接手你的项目时能一眼看懂哪个参数对应哪个字段。另外注意try-with-resources写法,连接、预编译语句、结果集三层的关闭顺序由 Java 自动处理,比手动finally里关闭干净得多,期末答辩时这个细节是加分项。

4.4 借阅与还书的业务逻辑:事务和数量校验

借书和还书是图书管理系统里最复杂的两个动作,因为它同时涉及 borrow 表和 book 表两张表的变动。借书成功 = 在 borrow 插入一条记录 + book 的 remaining_count 减一,两个操作必须同时成功,否则数据就不一致。这里就引出了事务的概念。

// 借书业务:包含插入借阅记录和扣减库存两个步骤 public boolean borrowBook(int bookId, int readerId) { // 借书日期和应还日期:应还日 = 借出日 + 30天 Date borrowDate = new Date(); Calendar cal = Calendar.getInstance(); cal.setTime(borrowDate); cal.add(Calendar.DAY_OF_YEAR, 30); Date dueDate = cal.getTime(); String insertSql = "INSERT INTO borrow (book_id, reader_id, borrow_date, due_date, status) VALUES (?,?,?,?,0)"; // 关键:WHERE 条件里带 remaining_count > 0,防止超借 String updateSql = "UPDATE book SET remaining_count = remaining_count - 1 WHERE id = ? AND remaining_count > 0"; Connection conn = null; try { conn = DBUtil.getConnection(); // 关闭自动提交,两步操作变成同一个事务 conn.setAutoCommit(false); // 第一步:插入借阅记录 try (PreparedStatement ps1 = conn.prepareStatement(insertSql)) { ps1.setInt(1, bookId); ps1.setInt(2, readerId); ps1.setDate(3, new java.sql.Date(borrowDate.getTime())); ps1.setDate(4, new java.sql.Date(dueDate.getTime())); ps1.executeUpdate(); } // 第二步:扣减库存,检查受影响行数是否为0 int rows; try (PreparedStatement ps2 = conn.prepareStatement(updateSql)) { ps2.setInt(1, bookId); rows = ps2.executeUpdate(); } // 减库存失败说明书已被借完,回滚并返回失败 if (rows == 0) { conn.rollback(); return false; } // 两步都成功才提交事务 conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); // 任何一步出错,回滚整个事务 try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { // 恢复自动提交,避免连接池里的连接状态被污染 if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这段代码有两个刻意设计:conn.setAutoCommit(false)开启事务,保证两步操作要么都成功要么都失败;UPDATE语句里带remaining_count > 0条件,从数据库层面杜绝库存变负数。如果你发现你拿到的源码没有这两段逻辑,那我建议你加上,因为“库存变成负数”是图书管理系统答辩时最容易被老师抓的问题。

5. 图书管理系统项目避坑与常见问题排查:交作业前必看

无论源码是自己写的还是拿来的,期末作业的验收现场总会出点幺蛾子。下面是图书管理系统上线前必须排查的五类高频问题,每一条都是血泪经验。

5.1 数据库连接失败:驱动、密码、时区三个坑

现象:启动项目后,访问查询图书列表的页面,直接报Communications link failure或者Access denied for user。

原因:这三个报错对应的原因完全不同。Communications link failure说明网络层面就连不上 MySQL,常见原因是 MySQL 服务没启动,或者连接 URL 里的端口号写错;Access denied则是用户名或密码不对;还有一个高频隐藏原因,MySQL 8.0 的驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,如果源码里写的还是老驱动名,会直接报ClassNotFoundException。

解决:先系统服务里确认 MySQL 在运行,再检查DBUtil.java里的用户名密码是否匹配,最后看连接 URL 里有没有serverTimezone=Asia/Shanghai。这三项按顺序查,两分钟内能定位绝大多数连接失败问题。

5.2 中文乱码的三级排查

现象:页面上书名显示成???,或者数据库里存的是中文,页面上却是一堆乱码。

原因:图书管理系统的中文乱码逃不出三个位置:数据库字符集、连接 URL 字符集、JSP/Servlet 请求响应字符集。数据库建库时没指定utf8mb4,连接时没带characterEncoding=utf8,Servlet 里request.setCharacterEncoding("UTF-8")没写在最前面,三处任一不对都会乱码。

解决:按“数据库 → 连接 → 代码”顺序排查。先看建表语句是不是DEFAULT CHARSET=utf8mb4,再看 JDBC URL 是否带characterEncoding=utf8,最后在所有 Servlet 的doPost第一行加req.setCharacterEncoding("UTF-8")。JSP 页面顶部记得写contentType="text/html;charset=UTF-8"。三个位置全对了,中文乱码基本绝迹。

5.3 Tomcat 启动失败:端口被占用与部署路径

现象:Tomcat 启动不到两秒就闪退,控制台报Address already in use: JVM_Bind :8080,或者启动成功但访问所有页面都是 404。

原因:Address already in use是 8080 端口被别的程序占了,常见的有其他 Tomcat 实例、IDEA 内置服务器、甚至某些软件的调试端口。404 则是部署名对不上,项目明明叫library-system,你却访问localhost:8080/。

解决:端口占用时,Windows 下用netstat -ano | findstr :8080查到占用 PID,再到任务管理器里结束对应的进程;不想杀进程的话,改 Tomcat 的conf/server.xml里端口号为 8081 也行。404 问题,看一眼 Tomcat 的webapps目录下文件夹名,按实际部署名拼接访问路径。

5.4 库存变成负数和还书日期异常

现象:同一本书被连续借出,remaining_count一路减到 -3;或者还书后日期显示成 1970 年。

原因:库存负数是因为借书逻辑只做了“INSERT 借阅记录”和“UPDATE 库存减一”,没判断库存是否已经为零。多个读者同时操作时,甚至会出现两个人同时借走最后一本的情况。日期变 1970 是 Java 中Date和java.sql.Date转换出问题,常见于直接把util.Date塞给setDate()方法或毫秒数丢失。

解决:按第 4 章的写法,UPDATE语句里加AND remaining_count > 0,再用受影响行数判断是否扣减成功。日期转换用new java.sql.Date(utilDate.getTime()),不要直接传对象,我在实际排错时见过太多次这个低级但隐蔽的坑了。顺带说一句,如果这些逻辑在你的源码里都没有,这恰恰是你答辩时能主动补强的地方。

5.5 表单重复提交与刷新后重复扣库存

现象:借书时手一抖点了两次按钮,系统里多了两条一模一样的借阅记录,库存也连着减了两次。

原因:前端没有防双击,后端又没有做重定向跳转。用户点按钮后页面没跳转,再点一次又发了一个 POST 请求;或者操作成功后停留在原页面,按 F5 刷新,浏览器重新提交上一次的 POST 请求。

解决:前端在表单提交后把按钮设置为disabled,只需要一行 JS;后端在成功操作后统一用resp.sendRedirect(),不要用forward()或直接返回视图。这就是经典的 PRG(Post-Redirect-Get)模式,面试时能说出这个名词,说明你真的理解了这个坑。

6. 交作业前的最后冲刺:演示录像与答辩讲解

项目跑通只完成了一半,期末作业的验收是“看演示 + 问原理”,所以最后一步是把演示流程和讲解逻辑打磨好。

6.1 功能演示脚本:录屏时按这个顺序操作

录屏演示时,不要手握鼠标乱点。按一个严谨的业务流程走一遍,比零散地展示十个页面更有说服力。推荐的演示顺序:

  1. 展示数据库,在 MySQL 里执行SELECT * FROM book,让老师看到三张表的数据,这是“数据库设计”的实证。
  2. 打开系统,输入管理员账号登录,展示登录跳转。
  3. 新增一本图书,填写表单,提交后到列表页确认新书出现。
  4. 模拟一次完整借阅:选择一个读者,借出刚新增的图书,回到详情页看库存减一。
  5. 执行还书,确认库存加一,借阅记录状态变为已归还。
  6. 演示一个边界场景:把图书库存借到 0,再尝试借阅,系统应提示“库存不足”。

6.2 答辩讲解的切入点

答辩被问得最多的三个问题是:表结构为什么这么设计?借阅逻辑怎么保证数据不错?项目如果上线还缺什么?对应准备三句话:表结构用“三张表,借阅表做关联,冗余了 remaining_count 和 due_date 两个关键字段”;借阅逻辑讲“事务 + 库存判断,两步操作要么都成功要么都失败”;上线缺什么讲“密码加密、分页查询、并发控制、日志记录”。

关于版权问题多说一句:如果是网上找的源码,务必把 README 里的作者信息、版权声明删掉或注明出处,把代码里的包名、类名改成自己的风格。老师看重的不是代码多高级,而是你能不能把每行逻辑讲明白。把源码从头到尾读一遍,标注出每段代码的作用,你就能把别人的工程变成自己的作品。这套方法我带过很多学生走通,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询