简介:这份资源是面向高校计算机专业学生与Java Web初学者的一套完整学校教材管理系统源码,基于Java、JSP与MySQL技术栈构建,运行于Tomcat环境,适合用作课程设计、毕业设计或Web开发练手项目。压缩包共81个文件,约3.91MB,包含26个JSP页面、22个class与11个Java源文件、10个CSS样式、8个XML配置及1个SQL建库脚本,覆盖前端展示、后端逻辑与数据库脚本,结构完整可直接部署调试。系统围绕管理员登录认证与教材增删改查两大模块展开,采用MVC思路分离模型、视图与控制器,并涉及输入校验、SQL注入防护、权限控制与数据备份等实践要点。目前已有1229人学习下载,读者可借此掌握JSP动态页面开发、MySQL数据操作与Web项目分层设计,快速理解教材管理类系统的实现流程与排错思路。
1. 从一份教材订购单说起:Java+JSP+MySQL 的学校教材管理系统到底在管什么
每学期开学前两周,教务处最怕听到的一句话就是「老师,我的书还没到」。教材征订这件事看着简单,真跑起来是一团乱麻:二十几个院系各自报书目,版本号、ISBN、出版社、单价、教师用书还是学生用书,全靠 Excel 来回传;仓库那边又不知道到底订了多少、到了多少、发下去多少。我见过最离谱的一所学校,教材科用三张表加一个微信群撑了四年,直到某次把「高等数学(上)」订成了「高等数学(下)」,两百多本砸在库里。
这套 Java+JSP+MySQL 的 Web 学校教材管理系统,要解决的就是这条链路上的信息断层:把教材基础信息、院系征订、库存入库、领用发放、退书结算这几件事收进一个浏览器就能打开的 Web 项目里。它属于典型的 JavaWeb 课程设计 / 毕设选题,技术栈是 JSP + Servlet + JDBC + MySQL,跑在 Tomcat 上,前端用 JSP 页面加一点 jQuery 或原生 JS。适合谁?适合正在做 JavaWeb 课程设计、毕设,或者单位里想给教材科搭个内部小系统的人。它不追求高并发,追求的是「一个人两周能跑起来、能演示、能答辩」。下面我按自己带学生做过几轮的经验,把选型、建库、核心功能、踩坑和进阶一条条讲清楚。
2. 技术选型与工程骨架:为什么这套老技术栈还在被反复用
2.1 JSP + Servlet + JDBC 的分工边界
先把三层说清楚,不然后面写代码会乱。JSP 只负责展示和收集表单,Servlet 负责接收请求、调业务、转发页面,JDBC 负责和 MySQL 打交道。很多人一上来就把 SQL 写进 JSP 的<% %>脚本片段里,页面能跑,但改一个字段要翻十几个文件,这就是典型的翻车起点。
我一般会这样分层:
com.textbook.entity:实体类,和数据库表一一对应,比如Textbook、Order、Stock。com.textbook.dao:数据访问层,一个类管一张表的增删改查,SQL 全在这里。com.textbook.service:业务层,处理「征订数量不能为负」「退书不能超过已领数量」这类规则。com.textbook.servlet:控制层,一个 Servlet 对应一组相关操作,比如TextbookServlet管教材的增删改查。web/:JSP 页面,按模块分目录,textbook/、order/、stock/。
这样分的好处是,答辩时老师问「你的业务逻辑写在哪」,你能直接指到 service 层,而不是含糊地说「在页面里」。
2.2 用 IDEA 新建 JSP 项目并配好 Tomcat
热词里「idea新建jsp项目」被搜了很多次,说明这一步就卡住不少人。我一般用 Maven 的 webapp 骨架,比手动建目录省事。
# 用 Maven 原型生成 webapp 骨架 mvn archetype:generate -DgroupId=com.textbook \ -DartifactId=textbook-system \ -DarchetypeArtifactId=maven-archetype-webapp \ -DinteractiveMode=false生成后目录里只有src/main/webapp,需要自己补src/main/java。在 IDEA 里右键java目录 Mark as Sources Root,再在 Project Structure 里把 Tomcat 配成 Artifact 的部署目标。pom.xml里至少要有 servlet-api 和 mysql-connector-java 两个依赖,servlet-api 的 scope 要写provided,因为 Tomcat 自带,打进 war 会冲突。
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>参数说明:scope=provided表示编译和测试时用,打包时不带,避免和容器里的实现打架。MySQL 驱动版本要和你的 MySQL 服务端大版本对齐,8.x 的服务端配 8.x 驱动,5.7 的服务端用 5.1.x 驱动更稳。
2.3 数据库连接池:别每次请求都 DriverManager
新手最常见的写法是每个 Servlet 里DriverManager.getConnection(),一个页面刷三次就开三次连接,演示时看不出来,真放到教材科十几个人同时用,MySQL 的max_connections很快被打满,报Too many connections。正确做法是上连接池,Druid 或 HikariCP 都行,我一般用 Druid,配置直观。
# druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/textbook_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000initialSize是启动时建几个连接,maxActive是上限,maxWait是拿不到连接时最多等多少毫秒。serverTimezone一定要写,不然 8.x 驱动连 5.7 服务端会报时区错误,这是血泪经验。连接池用一个静态工具类初始化,全项目共用一份DataSource。
3. 建库建表与教材主数据:字段设计决定后面改不改得动
3.1 五张核心表的最小可用设计
教材管理系统的表不用多,五张就能撑起完整流程:教材信息表、院系表、征订表、库存表、领用记录表。字段设计上最容易后悔的是「教材版本」和「库存数量」这两块。
CREATE TABLE textbook ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(80), edition VARCHAR(20), -- 版次,如"第3版" price DECIMAL(8,2) DEFAULT 0.00, category VARCHAR(30), -- 类别:公共课/专业课 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, contact VARCHAR(30) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, textbook_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, semester VARCHAR(20), -- 学期,如"2024-2025-1" status TINYINT DEFAULT 0, -- 0待审 1已审 2已入库 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (dept_id) REFERENCES department(id), FOREIGN KEY (textbook_id) REFERENCES textbook(id) ); CREATE TABLE stock ( id INT PRIMARY KEY AUTO_INCREMENT, textbook_id INT NOT NULL UNIQUE, total_in INT DEFAULT 0, -- 累计入库 total_out INT DEFAULT 0, -- 累计发放 FOREIGN KEY (textbook_id) REFERENCES textbook(id) ); CREATE TABLE issue_record ( id INT PRIMARY KEY AUTO_INCREMENT, textbook_id INT NOT NULL, dept_id INT NOT NULL, quantity INT NOT NULL, issue_time DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(30) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数说明:isbn加 UNIQUE,防止同一本书重复录入;price用 DECIMAL 不用 FLOAT,金额算错是财务级事故;orders.status用 TINYINT 做状态机,比字符串好索引;stock表用total_in和total_out两个累计字段,实时库存 = total_in - total_out,避免每次去 SUM 领用记录。
3.2 教材信息的增删改查 Servlet 怎么写
以教材录入为例,一个 Servlet 里用action参数区分操作,是这套老技术栈里最省事的做法。
@WebServlet("/textbook") public class TextbookServlet extends HttpServlet { private TextbookService service = new TextbookService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("add".equals(action)) { Textbook t = new Textbook(); t.setIsbn(req.getParameter("isbn")); t.setName(req.getParameter("name")); t.setPublisher(req.getParameter("publisher")); t.setEdition(req.getParameter("edition")); t.setPrice(new BigDecimal(req.getParameter("price"))); boolean ok = service.add(t); resp.sendRedirect("textbook?action=list&msg=" + (ok ? "success" : "fail")); } } }逻辑说明:req.setCharacterEncoding("UTF-8")必须在取参数之前调用,否则中文书名会变乱码,这是 JSP 项目里出现频率最高的 bug。action用隐藏域在表单里传,比如<input type="hidden" name="action" value="add">。重定向带msg参数回列表页,页面顶部根据 msg 显示提示,比弹窗干净。
参数说明:BigDecimal接价格,别用Double.parseDouble,浮点误差会让 39.9 变成 39.899999。service.add里先查 ISBN 是否存在,存在就返回 false,把唯一约束的异常挡在业务层。
3.3 分页查询:教材列表超过一屏就得做
教材表几百条以后,一次性SELECT *全查出来渲染,页面会明显卡。分页用LIMIT加COUNT两条 SQL 就够。
-- 查总数 SELECT COUNT(*) FROM textbook WHERE name LIKE CONCAT('%', ?, '%'); -- 查当前页 SELECT * FROM textbook WHERE name LIKE CONCAT('%', ?, '%') ORDER BY id DESC LIMIT ?, ?;LIMIT的两个参数是偏移量和条数,偏移量 = (当前页 - 1) * 每页条数。注意 MySQL 的LIMIT偏移量在数据量大时性能会下降,但教材表撑死几千条,不用优化。JSP 页面用 JSTL 的<c:forEach>渲染列表,页码用循环生成链接,别用 JS 拼字符串,维护起来更清楚。
4. 征订、入库、发放的闭环:状态流转和库存一致性怎么保证
4.1 征订单从提交到入库的状态机
征订流程是这套系统的核心,也是最容易写乱的地方。我一般把它定义成三个状态:待审核(0)、已审核(1)、已入库(2)。院系提交后是 0,教材科审核通过变 1,仓库确认到货后变 2,同时往stock表累加total_in。
状态流转必须放在 service 层,并且用事务包住「改状态」和「加库存」两个操作,否则会出现状态改了但库存没加的脏数据。
public boolean confirmStock(int orderId) { Connection conn = null; try { conn = DruidUtil.getConnection(); conn.setAutoCommit(false); OrderDao orderDao = new OrderDao(conn); StockDao stockDao = new StockDao(conn); Order o = orderDao.findById(orderId); if (o == null || o.getStatus() != 1) return false; orderDao.updateStatus(orderId, 2); stockDao.addIn(o.getTextbookId(), o.getQuantity()); conn.commit(); return true; } catch (Exception e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} return false; } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) {} } }逻辑说明:DAO 层的方法接收外部传入的Connection,这样多个 DAO 能共用同一个事务。如果每个 DAO 自己开连接,事务就失效了,这是 JDBC 事务最经典的坑。conn.setAutoCommit(false)之后所有操作要么一起提交,要么一起回滚。
参数说明:o.getStatus() != 1是幂等保护,防止重复点「确认入库」把库存加两次。stockDao.addIn用UPDATE stock SET total_in = total_in + ? WHERE textbook_id = ?,用数据库的原子加法,别在 Java 里先查再算再写,并发下会丢更新。
4.2 发放登记与退书:数量校验放在哪一层
发放登记要校验「发放数量不能超过当前库存」,退书要校验「退书数量不能超过该院系该教材的已领数量」。这两个校验都放 service 层,别放 JSP 的 JS 里,JS 能被绕过。
public String issue(int textbookId, int deptId, int qty, String operator) { int stock = stockDao.getCurrentStock(textbookId); // total_in - total_out if (qty <= 0) return "数量必须大于0"; if (qty > stock) return "库存不足,当前库存:" + stock; issueDao.insert(textbookId, deptId, qty, operator); stockDao.addOut(textbookId, qty); return "ok"; }逻辑说明:先查库存再插入记录再扣减,这三步最好也包在事务里。返回字符串而不是布尔值,是为了把具体原因带回页面显示,比统一报「操作失败」友好得多。
参数说明:getCurrentStock用SELECT total_in - total_out FROM stock WHERE textbook_id = ?,如果这行不存在返回 0。addOut同样用total_out = total_out + ?的原子写法。
4.3 用 JSTL 渲染征订列表并做权限区分
JSP 页面里尽量别写 Java 代码,用 JSTL 和 EL 表达式。征订列表要按角色显示不同按钮:院系用户只能看自己的单子,教材科能看全部并能审核。
<c:forEach items="${orderList}" var="o"> <tr> <td>${o.textbookName}</td> <td>${o.deptName}</td> <td>${o.quantity}</td> <td> <c:choose> <c:when test="${o.status == 0}">待审核</c:when> <c:when test="${o.status == 1}">已审核</c:when> <c:otherwise>已入库</c:otherwise> </c:choose> </td> <td> <c:if test="${sessionScope.role == 'admin' && o.status == 1}"> <a href="order?action=confirm&id=${o.id}">确认入库</a> </c:if> </td> </tr> </c:forEach>逻辑说明:sessionScope.role在登录时写入 session,JSP 里直接判断。按钮的显示条件同时判断角色和状态,避免已入库的单子还显示「确认入库」。
参数说明:${o.textbookName}这种字段来自关联查询的 VO 对象,不是实体类,所以 DAO 里要用 JOIN 把教材名和院系名一起查出来,别在 JSP 里再查一次数据库。
5. 避坑与排查:这套老技术栈最容易翻车的五个地方
5.1 中文乱码:POST 和 GET 要分开处理
现象:表单提交的中文书名在数据库里变成????,或者页面显示成乱码。
原因:POST 请求的编码由request.setCharacterEncoding控制,GET 请求的参数编码由 Tomcat 的server.xml里 Connector 的URIEncoding控制,两者不是一回事。
解决:POST 在每个 Servlet 取参数前调req.setCharacterEncoding("UTF-8");GET 在 Tomcat 的server.xml的 Connector 上加URIEncoding="UTF-8"。数据库连接串里也要带characterEncoding=utf8,表用utf8mb4。三处都对齐才不会乱。
5.2 连接池报 Too many connections
现象:演示时好好的,几个人同时点就报Too many connections或连接超时。
原因:要么没用连接池,要么用了但 DAO 里拿了连接忘记关,连接被耗尽。
解决:统一用 Druid 工具类拿连接,所有 DAO 在 finally 里close()。如果用了事务,注意setAutoCommit(true)之后再关,否则连接回到池里还是事务状态。排查时可以在 MySQL 里执行SHOW PROCESSLIST看有多少连接挂着。
5.3 外键约束导致删教材失败
现象:删除一本没人订的教材也报Cannot delete or update a parent row。
原因:orders和stock表对textbook有外键,只要有过征订记录就删不掉。
解决:教材不做物理删除,加一个is_deleted字段做逻辑删除,列表查询带WHERE is_deleted = 0。这是业务系统里的常规做法,物理删除会破坏历史单据的可追溯性。
5.4 金额用 Double 算出一堆小数
现象:单价 39.9 乘数量 3,页面显示 119.69999999999999。
原因:Double和Float是二进制浮点,无法精确表示十进制小数。
解决:实体类里金额字段用BigDecimal,数据库用DECIMAL(8,2),运算用BigDecimal.multiply并指定setScale(2, RoundingMode.HALF_UP)。JSP 显示时用<fmt:formatNumber value="${o.price}" pattern="0.00"/>。
5.5 打包 war 部署到 Tomcat 后 404
现象:IDEA 里跑得好好的,打成 war 丢进 Tomcat 的 webapps 就 404。
原因:多半是web.xml的metadata-complete="true"导致@WebServlet注解不生效,或者 war 包名和访问路径对不上。
解决:检查web.xml的 version 是不是 4.0,metadata-complete是不是 false。war 包名就是访问路径的第一段,比如textbook-system.war访问就是http://localhost:8080/textbook-system/。传统 JSP 项目打包 war 时,pom.xml的packaging要写war,servlet-api 的 scope 保持provided。
6. 从能跑到好用:导出 Excel 和登录拦截的两个进阶技巧
系统能跑通增删改查之后,教材科最想要的功能其实是「把征订汇总导成 Excel 发给出版社」。用 Apache POI 就能做,热词里「java poi word能生成图表吗」问的是 Word,但教材系统里更常用的是 Excel 导出。
public void exportOrders(HttpServletResponse resp, List<OrderVO> list) throws IOException { resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment;filename=orders.xlsx"); try (XSSFWorkbook wb = new XSSFWorkbook(); ServletOutputStream out = resp.getOutputStream()) { XSSFSheet sheet = wb.createSheet("征订汇总"); String[] headers = {"教材名称", "ISBN", "院系", "数量", "单价", "状态"}; XSSFRow head = sheet.createRow(0); for (int i = 0; i < headers.length; i++) head.createCell(i).setCellValue(headers[i]); int rowIdx = 1; for (OrderVO o : list) { XSSFRow row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(o.getTextbookName()); row.createCell(1).setCellValue(o.getIsbn()); row.createCell(2).setCellValue(o.getDeptName()); row.createCell(3).setCellValue(o.getQuantity()); row.createCell(4).setCellValue(o.getPrice().doubleValue()); row.createCell(5).setCellValue(o.getStatusText()); } wb.write(out); } }逻辑说明:Content-Disposition的attachment让浏览器直接下载而不是打开。用 try-with-resources 保证 workbook 和输出流都关闭。表头和数据行分开写,方便以后加合计行。
参数说明:XSSFWorkbook对应 xlsx 格式,数据量大时用SXSSFWorkbook省内存。o.getPrice().doubleValue()这里转 double 只是为了写进 Excel,业务计算仍然用 BigDecimal。导出接口要放在登录拦截之后,别让未登录的人也能下载。
登录拦截用 Filter 实现,比在每个 Servlet 里判断 session 干净得多。
@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.contains("/login")) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }逻辑说明:/*拦截所有请求,白名单放行登录页和登录接口,其余检查 session 里有没有 user。request.getContextPath()拼在重定向前面,避免部署路径不是根路径时跳错。
参数说明:白名单判断用endsWith和contains组合,注意别把/login写成能匹配到/loginRecord这种前缀,必要时用正则精确匹配。Filter 的@WebFilter注解同样依赖web.xml的metadata-complete=false。
最后说个我自己的习惯:这套系统我从来不在答辩前一天才打包部署,而是从第一天起就坚持「IDEA 里跑通 → 立刻打 war 丢进独立 Tomcat 验证一遍」。因为 IDEA 的内置 Tomcat 和独立 Tomcat 在类加载、路径、编码上都有细微差别,早发现早改,别等到最后一天才发现 404 却不知道从哪查。教材管理系统这种项目,功能不难,难的是把每个环节的边界都想到,把状态和库存的一致性守住。希望帮到你。
本文还有配套的精品资源,点击获取