☰
Java+JSP+MySQL学校教材管理系统实战:从建库到事务扣库存
2026/10/8 7:34:19 网站建设 项目流程

简介:这份资源是面向计算机专业学生与Java Web初学者的一套完整学校教材管理系统源码,基于Java、JSP与MySQL技术栈构建,可直接用于课程设计、毕业设计或Web开发练手。压缩包共81个文件,约3.91MB,包含26个JSP页面、22个class编译文件、11个Java源文件,以及CSS样式、XML配置、JAR依赖和一份SQL建表脚本,覆盖前端展示、后端逻辑与数据库脚本的完整链路。系统围绕管理员登录与教材管理两大模块展开,实现教材的增删改查、库存查询与权限校验,并采用MVC思路分离模型、视图与控制器,便于理解请求处理与数据访问流程。目前已有1229人学习下载,读者可借此掌握JSP动态页面开发、MySQL数据操作及异常处理、输入校验等实用技巧,快速搭建可运行的教材管理平台。

1. 从零搭一套学校教材管理系统:Java+JSP+MySQL 到底能解决什么

每到学期初,教务处最头疼的不是排课,而是教材。班级数、学生人数、教材版本、库存余量、领用记录,全靠几张 Excel 来回传,改一处就全乱。用 Java+JSP+MySQL 做一套 Web 学校教材管理系统,本质就是把这套线下流程搬到浏览器里:管理员维护教材信息和库存,教师按班级提交领用申请,系统自动扣减库存并留下可追溯的记录。它适合两类人:一是正在找 JavaWeb 完整案例练手的在校生,二是想给中小学校做一套轻量内部工具的一线开发者。技术栈不新,但胜在依赖少、部署简单、逻辑闭环清晰,一台普通服务器甚至一台办公电脑就能跑起来。下面我按真实落地顺序,把选型、建库、编码、排错和进阶一条条讲透。

2. 技术选型与数据库设计:为什么这套老组合反而更适合教材管理

2.1 选型理由:JSP 不是过时,是场景匹配

很多人一听 JSP 就皱眉,觉得 Spring Boot 才是正道。但教材管理系统的真实使用场景是:用户量小(几百人)、并发低、功能固定、部署环境往往是学校内网的老服务器。这种场景下,JSP 的优势反而明显——它把 Java 代码和 HTML 写在一个文件里,页面跳转直观,不需要前后端分离那一套构建工具链,一个 Tomcat 加一个 MySQL 就能跑。对于要交课程设计或者快速交付内部工具的团队,省下来的环境配置时间就是实打实的收益。

当然,这不意味着可以乱写。JSP 里塞业务逻辑是新手最容易翻车的地方,正确做法是 JSP 只负责展示,业务逻辑全部放到 Servlet 或 JavaBean 里,数据库操作统一走 DAO 层。这个分层习惯养成了,后面想迁移到 Spring Boot 也不难。

环境版本上,我一般会选 JDK 8 或 JDK 11、Tomcat 8.5 或 9、MySQL 5.7 或 8.0。MySQL 8.0 的安装配置在 Windows 10 上已经比较顺,注意选对认证方式即可,后面避坑章节会细说。

2.2 数据库表设计:五张表撑起整个系统

教材管理的核心实体其实就几个:用户、教材、库存、领用记录、班级。表不用多,但字段要想清楚。下面是我常用的建表脚本,直接可以在 MySQL 里执行。

-- 用户表:区分管理员和教师 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '存储MD5后的密码', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT DEFAULT 2 COMMENT '1管理员 2教师', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 教材表:教材基础信息 CREATE TABLE textbook ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT '书号,唯一', name VARCHAR(100) NOT NULL COMMENT '教材名称', publisher VARCHAR(100) COMMENT '出版社', price DECIMAL(8,2) DEFAULT 0.00 COMMENT '单价', stock INT DEFAULT 0 COMMENT '当前库存', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 领用记录表:谁在什么时候领了什么 CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '领用人', textbook_id INT NOT NULL COMMENT '教材ID', class_name VARCHAR(50) COMMENT '使用班级', quantity INT NOT NULL DEFAULT 1 COMMENT '领用数量', borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT '1已领用 2已归还', INDEX idx_user (user_id), INDEX idx_textbook (textbook_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个设计要点值得说。第一,stock字段直接放在教材表里,而不是单独做库存表,因为教材管理的库存粒度就是「某本书还剩几本」,没必要过度设计。第二,borrow_record上建了两个索引,因为查询「某教师领了哪些书」和「某本书被谁领了」是两个高频操作,没索引数据一多就明显变慢。

提示:字符集统一用 utf8mb4,教材名称里出现生僻字或特殊符号时不会乱码,这个坑我踩过不止一次。

2.3 连接配置:一个 DBUtil 类解决所有数据库访问

JSP 项目里最容易写乱的就是数据库连接。我的习惯是写一个工具类,统一管理连接、关闭资源,避免每个 Servlet 里都重复写 DriverManager。

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/textbook_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 8.0 驱动 } catch (ClassNotFoundException e) { throw new RuntimeException("驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一关闭,顺序不能反 public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (ps != null) ps.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }

参数说明:serverTimezone=Asia/Shanghai是 MySQL 8.0 必须加的,否则时间字段会差 8 小时;useSSL=false在本地和内网环境可以关掉,省去证书配置的麻烦。驱动类名从 MySQL 5.x 的com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,写错会直接报找不到驱动。

3. 核心功能实现:登录、教材增删改查与领用扣库存

3.1 登录与权限拦截:一个 Filter 挡住未登录请求

登录本身不难,难的是别让用户绕过登录直接访问后台页面。做法是写一个 Filter,拦截所有后台路径,检查 session 里有没有用户信息。

public class AuthFilter implements Filter { @Override 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.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

逻辑说明:先判断请求路径,登录页和静态资源直接放行,否则检查 session。这里有个细节,getContextPath()不能省,否则项目部署名不是 ROOT 时跳转会 404。web.xml 里配置这个 Filter 时,url-pattern用/*拦截全部,再在代码里做白名单,比配多个 pattern 更省心。

3.2 教材列表与分页:别一次性把整张表查出来

教材数量少的时候SELECT *没问题,但只要有几百条,页面就会明显卡。分页是必须的,MySQL 用LIMIT实现最简单。

public List<Textbook> queryByPage(int page, int pageSize, String keyword) { List<Textbook> list = new ArrayList<>(); String sql = "SELECT * FROM textbook WHERE name LIKE ? ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + (keyword == null ? "" : keyword) + "%"); ps.setInt(2, (page - 1) * pageSize); // 起始偏移量 ps.setInt(3, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { Textbook t = new Textbook(); t.setId(rs.getInt("id")); t.setName(rs.getString("name")); t.setStock(rs.getInt("stock")); list.add(t); } } catch (SQLException e) { e.printStackTrace(); } return list; }

参数说明:page从 1 开始,(page-1)*pageSize就是 SQL 的偏移量,这是分页最容易算错的地方,写成page*pageSize会漏掉第一页数据。keyword用LIKE模糊匹配,注意%要拼在 Java 里而不是 SQL 里,用PreparedStatement能防注入。

3.3 领用扣库存:事务是这里唯一的后悔药

领用教材必须同时做两件事:插入一条领用记录,扣减教材库存。这两步要么都成功,要么都失败,否则就会出现「记录有了但库存没扣」的脏数据。必须用事务。

public boolean borrow(int userId, int textbookId, String className, int qty) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 先查库存,带行锁防止并发超领 String checkSql = "SELECT stock FROM textbook WHERE id = ? FOR UPDATE"; PreparedStatement check = conn.prepareStatement(checkSql); check.setInt(1, textbookId); ResultSet rs = check.executeQuery(); if (!rs.next() || rs.getInt("stock") < qty) { conn.rollback(); return false; // 库存不足 } // 扣库存 PreparedStatement update = conn.prepareStatement( "UPDATE textbook SET stock = stock - ? WHERE id = ?"); update.setInt(1, qty); update.setInt(2, textbookId); update.executeUpdate(); // 插记录 PreparedStatement insert = conn.prepareStatement( "INSERT INTO borrow_record(user_id, textbook_id, class_name, quantity) VALUES(?,?,?,?)"); insert.setInt(1, userId); insert.setInt(2, textbookId); insert.setString(3, className); insert.setInt(4, qty); insert.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { try { if (conn != null) conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }

逻辑说明:FOR UPDATE是关键,它给这一行加排他锁,防止两个教师同时领同一本书时都读到「库存够」然后各扣一次,导致库存变负。这个并发问题在测试时很难复现,但上线后一定会遇到,属于典型的血泪经验。事务提交后记得把autoCommit恢复,否则连接池复用时会出问题。

4. 避坑与排查:教材管理系统上线前必须过的五道坎

4.1 中文乱码:现象是页面显示问号,原因是编码链没统一

现象:教材名称在数据库里正常,但页面上显示成???或者乱码方块。原因通常出在三处:JSP 页面没声明pageEncoding、请求参数没设setCharacterEncoding、数据库连接串没带characterEncoding=utf8。解决方法是三处一起改:JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,Servlet 里第一行写request.setCharacterEncoding("UTF-8"),连接串加上characterEncoding=utf8。三处缺一处都可能乱码,别只改一个就以为好了。

4.2 MySQL 8.0 连接报时区错误:现象是启动就抛异常

现象:The server time zone value '?D1ú±ê×?ê±??' is unrecognized。原因是 MySQL 8.0 驱动要求明确指定时区,而系统默认时区名带中文。解决方法是在 JDBC URL 里加serverTimezone=Asia/Shanghai,或者干脆用serverTimezone=GMT%2B8。这个错误在 MySQL 5.7 上不会出现,所以从 5.7 升级到 8.0 的项目特别容易中招。

4.3 库存扣成负数:现象是并发领用后库存异常

现象:两个教师同时领同一本只剩 1 本的教材,结果库存变成 -1。原因是查询和更新之间没有锁,两个请求都读到了「库存 1」。解决方法就是 3.3 里说的SELECT ... FOR UPDATE加事务。如果不想用行锁,也可以在 UPDATE 语句里加条件WHERE id = ? AND stock >= ?,根据executeUpdate()返回的影响行数判断是否成功,这是更轻量的做法。

4.4 JSP 里写业务逻辑导致页面报 500:现象是改一处崩一片

现象:JSP 页面里直接写数据库查询,某次改字段后多个页面同时 500。原因是业务逻辑散落在各个 JSP 里,没有统一入口。解决方法是把数据库操作全部抽到 DAO 类,JSP 只负责遍历 Servlet 传过来的 list。这个重构越早做越好,等到十几个页面都写了 SQL 再改,工作量翻倍。

4.5 Tomcat 部署后 404:现象是本地能跑,部署就找不到页面

现象:IDE 里运行正常,打成 war 包丢到 Tomcat 的 webapps 下访问就 404。原因通常是访问路径没带项目名,比如实际路径是/textbook/login.jsp,却直接访问/login.jsp。解决方法是访问时带上 war 包名,或者把 war 包改名为ROOT.war部署成根应用。另外检查web.xml的welcome-file配置,别把默认首页写错。

5. 进阶技巧:用导出和校验把系统从「能用」做到「敢用」

系统能跑起来只是第一步,真正让教务老师愿意天天用的,往往是几个细节功能。这里分享两个我每次都会加的进阶点。

第一个是教材领用记录的 Excel 导出。教务每到期末都要对账,让他们一条条复制粘贴不现实。用 Apache POI 生成 xls 文件,一个 Servlet 就能搞定。核心代码不长:

// 在 Servlet 中,response 设置好 content-type 和 header 后 HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("领用记录"); HSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("教材名称"); header.createCell(1).setCellValue("领用班级"); header.createCell(2).setCellValue("数量"); header.createCell(3).setCellValue("领用时间"); List<BorrowRecord> records = recordDao.queryAll(); int rowIdx = 1; for (BorrowRecord r : records) { HSSFRow row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(r.getTextbookName()); row.createCell(1).setCellValue(r.getClassName()); row.createCell(2).setCellValue(r.getQuantity()); row.createCell(3).setCellValue(r.getBorrowTime()); } workbook.write(response.getOutputStream()); workbook.close();

参数说明:HSSFWorkbook对应 xls 格式,数据量大时换成XSSFWorkbook生成 xlsx。注意response的 header 要设成application/vnd.ms-excel并指定文件名,否则浏览器会直接打开而不是下载。文件名带中文时要用URLEncoder.encode处理,不然会乱码。

第二个是库存预警。在教材列表页,对stock < 10的教材标红,并在首页显示一个待补货清单。实现上就是查询时加一个WHERE stock < 10的统计,前端用 CSS 类控制颜色。这个功能不复杂,但能让管理员提前发现库存不足,避免开学时才发现书不够。

最后一个习惯:每次改完 DAO 层的 SQL,我都会手动在 MySQL 客户端里把这条 SQL 单独跑一遍,确认结果集正确再放回代码。因为 Java 里 SQL 报错往往被 try-catch 吞掉,只打印堆栈,不主动验证很容易带着错误上线。这套系统不大,但每个字段、每个事务都值得较真,毕竟教务数据出错,补起来比写代码累多了。希望帮到你。

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

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

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

立即咨询