
简介面向JavaWeb期末大作业与课程设计场景这套医药管理系统源码整合了前后端实现与数据库脚本适合计算机相关专业学生用于项目参考、功能扩展或毕业设计借鉴。项目经导师指导并评审为98分源码均在本地编译调试通过可直接运行且配套SQL数据库脚本导入即可还原数据环境。压缩包共119个文件主要包含33个Java源文件与对应class编译文件、30个JSP页面、SQL数据库脚本以及XML配置、JAR依赖库和项目工程描述文件等整体约3.01MB结构清晰便于导入IDE后按模块查看。内容覆盖客户、医药、采购等业务模块的控制器、DAO实现与页面展示能帮助读者快速理解JavaWeb分层开发思路并可作为功能改造与界面调整的基础。目前已有94人学习下载适合需要完整可运行样例作为高分参考的实践学习者。1. 期末大作业选它的理由JavaWeb医药管理系统到底能拿多少分如果你正在找一份能直接交付的期末大作业JavaWeb医药管理系统的源代码与数据库几乎是最省心的选择之一。它不像博客系统那样只有单表增删改查也不像电商系统那样多到做不完而是刚好踩在课程评分点上登录、权限拦截、药品管理、供应商关联、入库出库、库存统计、报表查询每一处都能对上一个知识点。这个标题里“源代码与数据库”的意思很直白交付物是完整工程和一份初始化的SQL脚本你拿到后导入IDEA、建库导数据、配好Tomcat就能跑起来。适合哪些人第一类是时间不够想稳妥交作业的学生第二类是已经跑通但担心答辩被问住、想往项目里加亮点的人。你只需要在别人做过的血肉上看懂它、讲清楚它再补上几个加分细节。2. 动手前先看清架构医药管理系统该有的模块与数据库设计2.1 从登录权限到库存预警功能模块怎么拆才算“完整”拿到一份 JavaWeb 医药管理系统的源代码第一件事不是打开代码编辑器而是把模块清单列出来。大多数人交上去的项目不是代码质量问题而是功能太少老师想打分都找不到给分点。一份能拿高分的结课项目至少要覆盖登录认证、药品管理、库存变动和统计报表四块。我在帮人看作业时常用一张表来问对方你的系统里有哪几张表、哪几个页面如果说不出来那他多半还没把别人的源码吃透。模块关联表要覆盖的知识点管理员登录user/accountServlet处理请求、Session管理、Filter权限拦截药品信息管理drug分页查询、模糊搜索、增删改查、表单验证药品分类category外键关联、下拉框联动供应商管理supplier多表查询、删除时外键约束处理入库出库stock_in/stock_out事务处理、库存联动更新库存预警drug.stock条件查询、数字阈值比较销售与统计sale_orderGROUP BY 聚合、日期查询以最常见的 Servlet JSP MySQL 组合为例药品管理页面承载的是增删改查入库出库页面承载的是跨表事务库存预警页面承载的是条件查询报表页面承载的是 GROUP BY 聚合。这四个页面只要齐了课堂知识点也基本齐了。很多现成源码会把“库存预警”做得特别简陋只写死一个stock 10条件其实更好的方案是在 drug 表里加一个warn_stock字段让预警阈值可配置这个差异在答辩时一讲就有人眼前一亮。除了功能清单还要看工程结构。一个相对规范的 JavaWeb 工程会把 Java 源代码按包分层看到com.example.medicine.servlet就能猜到这是控制层看到dao就是数据访问层看到filter就是权限拦截。顺序上我一般建议先看 filter 包有登录拦截说明系统不是“裸奔”的再看 dao 包看有没有 PreparedStatement 和外键维护最后看 jsp看菜单能不能返回到正确的页面。如果标题里写的是 JavaWeb大多数情况下指的是 Servlet JSP JDBC 这种经典三层而不是 Spring Boot。但市面上也有把 Spring Boot 版包装成 JavaWeb 项目的它的运行方式会变成 Maven 打包、内置 Tomcat。这两者的导入步骤完全不同。拿到压缩包先看有没有pom.xml有pom.xml就走 Maven 导入没有就走传统 Web 工程导入不要先入为主。我见过有人把 Spring Boot 工程当成 Servlet 项目配置折腾半天也起不来。2.2 药品表、供应商表与订单表MySQL表结构设计的关键约束常见的交付文件里SQL 脚本名字很随意可能是medicine.sql、drug.sql甚至是db.sql。先别执行打开看它是否有CREATE DATABASE和USE语句。如果脚本只在没有选中库的情况下执行CREATE TABLE会默认建进 test 库后续 JDBC 连接天然找不到表。下面这套表结构是典型的医药管理基座我用精简版说明CREATE DATABASE IF NOT EXISTS drug_db DEFAULT CHARACTER SET utf8mb4; USE drug_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(16) DEFAULT USER, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, contact VARCHAR(32), phone VARCHAR(20), address VARCHAR(128) ) ENGINEInnoDB; CREATE TABLE drug ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, category VARCHAR(32), spec VARCHAR(64), unit VARCHAR(16), supplier_id INT, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, warn_stock INT DEFAULT 10, create_time DATETIME, CONSTRAINT fk_drug_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ON DELETE SET NULL ) ENGINEInnoDB;这份脚本里最关键的两个细节是fk_drug_supplier外键和warn_stock预警阈值。外键保证药品里录入的supplier_id必须是供应商表里真实存在的值做不到这一点页面上的“按供应商筛选”就会查出孤儿数据。warn_stock给了库存预警一个明确写法WHERE stock warn_stock而不是写死一个数字。写死数字的代码虽然也能跑但答辩时一句“为什么不建预警字段”就能问住。字符集部分要用utf8mb4不要用utf8。MySQL 中utf8最大只支持三个字节的编码遇到“药”这类汉字没问题可一旦你之后插入特殊符号就会报Incorrect string value。这是数据库初始化的血泪经验先省一句第5章还会展开。不少源码里的药品表没有有效期字段但医药管理系统几乎躲不开“批次、有效期、库存数量”这三个词。如果你拿到的表结构缺了有效期可以用一条 ALTER TABLE 补上ALTER TABLE drug ADD COLUMN expiry_date DATE DEFAULT NULL COMMENT 有效期;注意给已有表加字段时已有数据行会填 NULL。页面显示时要对空值做处理不要直接输出null让老师看到。另一个经常要修改的字段是价格精度如果原来用DECIMAL(10,2)没问题但个别源码会用FLOAT导致价格累加出现0.00000001的尾差金额统计就会对不上。这类修改表结构的需求放在答辩前做一次比反复重跑建库脚本更稳妥。2.3 外键与索引为什么说医药系统比学生管理系统更看重数据完整性学生管理系统可以把学生和班级的关联写散因为班级删除后补一个“暂无班级”就行。医药系统不同药品进出有成本金额库存不能出现负数供应商删除后药品不能变成无主记录。所以医药系统的数据库设计里外键、约束和事务的使用频率明显更高。很多现成源码为了省事把外键删掉只靠 SQL JOIN 去查这样页面显示没问题但删除供应商时药品表里的残留 id 会造成关联断裂。索引的选择同样影响系统评分。你不需要让user.username有索引因为登录查询量小但drug.name和sale_order.create_time值得加索引因为报表页会频繁按名称和日期筛选。下面的 DDL 可以单独执行ALTER TABLE drug ADD INDEX idx_drug_name (name); ALTER TABLE sale_order ADD INDEX idx_sale_time (create_time);加了索引后执行WHERE name LIKE 阿莫西林%这类前缀匹配才会走索引加速。注意如果写成LIKE %阿莫西林%一般不会命中索引这也能顺带解释为什么模糊搜索不像等值查询那么快。库存扣减要比这更严格。药品售出一个要同时更新sale_order和drug.stock两个步骤必须在一个事务里完成。常见做法是在 DAO 层写一个包含了两个 update 的方法并把 Connection 的 autoCommit 设为 falsepublic boolean saleOut(int drugId, int count) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 先检查库存是否足够 PreparedStatement check conn.prepareStatement( SELECT stock FROM drug WHERE id? FOR UPDATE); check.setInt(1, drugId); ResultSet rs check.executeQuery(); int stock 0; if (rs.next()) stock rs.getInt(stock); if (stock count) return false; // 扣减药品库存 PreparedStatement updateDrug conn.prepareStatement( UPDATE drug SET stock stock - ? WHERE id?); updateDrug.setInt(1, count); updateDrug.setInt(2, drugId); updateDrug.executeUpdate(); // 写入销售单 PreparedStatement insertSale conn.prepareStatement( INSERT INTO sale_order (drug_id, count, total_price, sale_time) VALUES (?,?,?,NOW())); insertSale.setInt(1, drugId); insertSale.setInt(2, count); insertSale.setBigDecimal(3, new BigDecimal(0)); // 价格代码里需要再按实际价格计算 insertSale.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn); } }这段代码里有两个值得在答辩时讲清楚的点。第一SELECT ... FOR UPDATE会把这一行锁住防止两个线程同时卖出最后一个库存这是数据库行级锁。第二执行失败后rollback()会把前面所有操作撤销不会出现“订单生成了但库存没扣”或者“库存扣了但订单没生成”的问题。如果你的源码 DAO 层没有类似写法我一般会在答辩前补一段并用纸笔画一下“正常、异常、回滚”三条分支。能把这个讲清楚比多做两个页面更管用。3. 把项目跑起来源码导入IDEA、配置Tomcat与MySQL的完整步骤3.1 用IDEA导入现有工程常见目录结构说明与运行前检查拿到压缩包后先解压别急着双击打开。先看根目录里有什么文件这决定你该用 IDEA 的哪种导入方式。如果有pom.xml这是 Maven 工程IDEA 会提示你安装依赖如果只有.project、.classpath这是 Eclipse 工程IDEA 需要把它当作普通项目导入如果连这些都没有那就看src文件夹和web文件夹还在不在缺了任何一部分都跑不起来。很多翻车现场就是解压后以为全了实际缺了web/WEB-INF/web.xml导致 Tomcat 无法识别。我一般建议用 IDEA 的File - Open直接选择解压目录IDEA 会自动识别项目结构。导入后展开的目录大概长这样src ├─ com/example/medicine │ ├─ entity // User, Drug, Supplier 等实体类 │ ├─ dao // 和数据库交互的 DAO 层 │ ├─ servlet // 处理请求的 Servlet │ ├─ filter // 登录拦截、编码过滤器 │ └─ util // DBUtil 数据库连接工具 web ├─ WEB-INF │ ├─ web.xml // Servlet 映射、欢迎页配置 │ └─ lib // mysql-connector-java.jar 等依赖 ├─ jsp 或根目录资源运行前检查四件事src与web是否齐全lib下有没有 MySQL 驱动 jarweb.xml里有没有被注释掉的配置src/util/DBUtil.java里的用户名密码和你本机是否一致。只要这四项没问题剩下的就是部署到 Tomcat。IDEA 配置 JavaWeb 项目的具体入口是Run - Edit Configurations - Tomcat Server - Local先选本地 Tomcat 路径再在Deployment标签页把war exploded加进去。还有一个常被忽略的版本匹配问题。老项目可能是用 JDK 8 和 Tomcat 8 搭的你新机器装的是 JDK 17 和 Tomcat 10Servlet 注解和 JSP 版本可能不兼容。传统 Servlet 项目的web.xml头部如果是 3.1 版本Tomcat 10 的 Jakarta Servlet 命名空间和旧javax.servlet不兼容第一次启动就会报各种 ClassNotFoundException。我的习惯是尽量使用和原项目一致的运行环境如果代码里import javax.servlet.*就用 Tomcat 9 或 8.5如果代码里import jakarta.servlet.*则必须用 Tomcat 10。IDEA 配置 Tomcat 的界面里可以选择本地 Tomcat 安装目录选错版本会把一个能跑的项目变得满页红。这里有一个最容易栽跟头的细节Application context默认会带上项目名比如/drug_war_exploded如果代码里的跳转都写死成/drug/login.jsp一旦上下文对不上就会 404。最简单的做法是把它改成/让所有路径从根开始页面跳转和 Servlet 映射才不容易乱。3.2 数据库初始化的两种方式直接执行SQL脚本还是手动建表带“数据库”的交付项目SQL 脚本一般放在压缩包根目录文件名常见的是medicine.sql或drug_db.sql。怎么把它导入本地 MySQL两种方式任选。第一种方式用 MySQL 命令行mysql -u root -p medicine.sql如果脚本里有建库语句执行后会自动创建数据库。如果不放心可以在 MySQL 里先登进去确认库和表都建好了USE drug_db; SHOW TABLES;第二种方式用 Navicat 或 IDEA 自带的 Database 面板。新建连接后右键数据库选择“运行 SQL 文件”然后定位到脚本。执行完后刷新表列表正常会看到user、drug、supplier、sale_order等表。如果一张表都没有大概率是脚本开头标了DROP TABLE IF EXISTS但前面的USE语句被注释掉了导致语句执行在默认的 test 库里。source命令的前提是 SQL 文件路径不能有中文。我在帮别人处理项目时见过这种报错ERROR 1049 (42000): Unknown database drug折腾半天发现是 Windows 下命令行编码把中文路径变成了乱码把脚本复制到纯英文目录下执行立刻就好了。执行 SQL 脚本前建议先看一遍里面有没有DROP TABLE IF EXISTS。很多交付脚本为了便于重复执行会在建表前把旧表删掉。如果你是在老师给的数据库上执行会把已经存在的数据清空。虽然期末项目一般是本地新库但谨慎一点不吃亏。执行完 SQL 后重跑一遍SELECT COUNT(*) FROM drug;至少有几条测试数据才说明导入成功。如果脚本只有建表语句没有任何INSERT INTO页面会空得让老师以为功能有问题。常见做法是手动补测试数据药品和供应商至少各十条销售单要跨至少两个日期这样报表页才能刷出有意义的图表。3.3 Tomcat部署与JDBC连接参数最容易出错的三处配置数据库和代码都齐了最后接线的就是 JDBC 连接参数。大多数非 Maven 工程的驱动都在web/WEB-INF/lib里连接信息写在DBUtil.java或db.properties。用db.properties的好处是改参数不用重新编译典型内容长这样drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/drug_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot password123456这里的三个参数十分容易踩雷。serverTimezoneAsia/Shanghai不加MySQL 8.0 的驱动会在连接时报时区错误报错信息里会提示serverTimezone但很多人看到英文就会慌。useSSLfalse是为了跳过本机测试时的 SSL 警告。allowPublicKeyRetrievaltrue是 MySQL 8.0 驱动连接时的常见要求不加会报Public Key Retrieval is not allowed。如果是 IDEA 内嵌的 Tomcat只需要保证DBUtil的加载路径正确。如果DBUtil.java里用的是Class.forName(com.mysql.jdbc.Driver)而 lib 里放的是 MySQL 8.0 的驱动 jar就会报ClassNotFoundException。旧驱动类名是com.mysql.jdbc.Driver新驱动是com.mysql.cj.jdbc.Driver两者不能混用。检查 jar 包版本最简单的方式是把 lib 里文件名看一眼如果是mysql-connector-java-8.0.31.jar这类代码里就要写成新类名。配置完以上三项常见的启动顺序是先启动 MySQL 服务再在 IDEA 里点击 Tomcat 的运行按钮看到控制台输出Connected to database之类的日志或页面能正常打开整个运行环境就算闭环了。这里提醒一句如果 Tomcat 启动时报端口占用 8080在配置里的HTTP port改成 8081页面上访问时也要跟着改很多人改完端口忘了 URL于是又多了一个 404 问题。如果改了db.properties不生效还要检查是不是classes目录里的旧配置没被覆盖用Build - Rebuild Project清掉重来。4. 代码从哪里看起按请求流程读懂ServletJSPDAO4.1 从登录页到底层SQL一条请求链路上的分层写法运行起来后的第一件事不是乱点页面而是把登录这个流程读通因为它是几乎所有期末项目的“主心骨”。一条请求在 JavaWeb 工程里大致经过JSP 表单提交到 ServletServlet 调用 DAO 查数据库DAO 返回结果给 ServletServlet 再决定跳转登录成功页面还是错误页。这个链路答上来老师第一印象就稳了。以登录为例页面里的表单大概长这样form actionlogin methodpost 用户名input typetext nameusername 密码input typepassword namepassword input typesubmit value登录 /form重点是actionlogin这个值要和web.xml或WebServlet里的映射一致。如果这里写的是loginServlet而 Servlet 里WebServlet(/login)运行时必然 404。拿到源码后先把这个映射对应关系查一遍我可以负责任地说我见过的期末项目里三分之一都是栽在这条路径上。Servlet 里取出参数的典型代码WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { req.getSession().setAttribute(user, user); resp.sendRedirect(index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }这里有个考点为什么用sendRedirect而不是forward因为登录成功后的页面需要考虑刷新问题用sendRedirect可以避免表单重复提交也能让浏览器地址栏变成最终页面。失败时用forward方便把error带回去回显两种跳转的语义不同。很多旧源码两种跳转混用答辩时你如果能把这条讲清楚比背十个概念都有说服力。登录请求还需要一个重要角色过滤器。很多源码里有个LoginFilter访问任何页面先检查 sessionWebFilter(/*) public class LoginFilter implements Filter { 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.jsp) || uri.endsWith(/login)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { resp.sendRedirect(login.jsp); return; } chain.doFilter(request, response); } }这段代码的边界条件在于放行规则。如果过滤器拦截了login.jsp和/login之外的资源用户不登录就只能停在登录页但如果页面里有静态 CSS 或图片也要加到放行列表否则登录页会变成没有样式的裸 HTML。答辩时老师问“怎么控制权限”你把这个过滤器指出来比说一句“我用了过滤器”要可信得多。4.2 药品核心操作增删改查的代码模板与参数绑定医药管理系统的核心说白了就是药品的增删改查。老师不会要求你把系统做出商业软件的水平但会要求你把“查”做出点名堂。普通查是SELECT * FROM drug高分查询是带分页、带模糊、带分类筛选。下面这段 DAO 代码基本概括了药品管理页常写的三种查法public ListDrug listDrugs(String keyword, String category) throws SQLException { ListDrug list new ArrayList(); // 用 StringBuilder 拼 SQL理由是不让条件越来越长时阅读困难 StringBuilder sql new StringBuilder( SELECT id, name, spec, unit, price, stock, supplier_id FROM drug WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND name LIKE ?); params.add(% keyword.trim() %); } if (category ! null !category.isEmpty()) { sql.append( AND category ?); params.add(category); } sql.append( ORDER BY id DESC); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Drug d new Drug(); d.setId(rs.getInt(id)); d.setName(rs.getString(name)); d.setPrice(rs.getBigDecimal(price)); d.setStock(rs.getInt(stock)); list.add(d); } } } return list; }参数说明WHERE 11是个技巧它让后面的AND无论加不加条件都成立代码就不用区分“有没有条件”两种情况。但这也意味着你必须在外面用keyword变量把%和查询词拼好而不能直接在 SQL 字符串里预设条件否则会把用户输入直接拼进语句留下 SQL 注入风险。PreparedStatement 相对于 Statement 的好处不只是写法规范而是参数由驱动完成转义。如果你手头的源码里是stmt.executeQuery(SELECT * FROM drug WHERE name name )我建议至少把那几个查询方法改成 PreparedStatement。这不仅是代码整洁问题也是期末项目答辩时“防注入”这个最常见追问的答案。如果你的系统数据量大还要考虑分页查询。典型写法是在 SQL 末尾加LIMIT ? OFFSET ?第一个参数是每页条数第二个参数是偏移量。偏移量计算成(currentPage - 1) * pageSize。不少源码把这一步省了导致药品一多页面就卡补上分页能多一个“性能优化”的说法。4.3 报表与统计用SQL聚合函数给项目加分当增删改查写顺了一定会在某个页面遇到“统计”。医药管理系统常见的统计有两类按药品分类统计库存总值按月份统计销售总额。这两类都能用聚合函数一行 SQL 解决这也是从“管理系统”走向“分析系统”的拐点。按分类统计的 SQL 大概是这样SELECT d.category AS 药品分类, COUNT(d.id) AS 药品品种数, SUM(d.stock * d.price) AS 库存总金额 FROM drug d GROUP BY d.category ORDER BY 库存总金额 DESC;如果把GROUP BY d.category后面加上WITH ROLLUP最后一行还会多出一条总计要在 JSP 里处理最后一个汇总行。这个细节很容易被忽略不加的时候页面反而更干净。我一般会让报表页只展示明细总计用另一个查询单独算让代码可读性更好。按月份统计销售额的写法SELECT DATE_FORMAT(sale_time, %Y-%m) AS 月份, SUM(total_price) AS 销售额 FROM sale_order WHERE sale_time BETWEEN ? AND ? GROUP BY DATE_FORMAT(sale_time, %Y-%m) ORDER BY 月份;这里?占位符传入的是日期参数。前端页面如果给了2024-01-01到2024-12-31这样整天的时间BETWEEN会把结束日期的零点作为边界漏掉当天剩余时间。为了避免这种令人窝火的统计差异常见做法是结束日期直接传2024-12-31 23:59:59或者把 WHERE 写成sale_time ? AND sale_time DATE_ADD(?, INTERVAL 1 DAY)。报表模块还有一层加分在 JSP 页面上把统计结果画成表格再用一个简单的柱状图用原生 CSS 的height比例就能表现不需要额外引入库。老师看到页面不是只有单调表格通常会在印象分上加一点。这里要注意别在期末前临时引入一个不熟悉的图表库把项目搞崩简单 CSS 柱图反而更稳。无论报表还是普通列表不要直接在 JSP 里写冗长的% ... %拼 SQL那会被提问对象。把 SQL 放在 DAO把结果集封装成 JavaBean让 JSP 只管显示才是 JavaWeb 的分层本意。5. 避坑指南我帮别人改期末项目时踩过的5个坑以下几个坑是我接手现成代码时最常遇到也是期末作业里出现频率最高的五类。每一条都不是理论推演而是实实在在影响运行和答辩的翻车现场。5.1 页面404的连环坑URL和Servlet映射没对上现象在 IDEA 里点击运行后首页能打开但点任何一个菜单都 404浏览器地址栏里的路径看起来很有道理可就是不出页面。原因绝大多数是form的action、WebServlet注解、sendRedirect的路径三者不一致。还有可能是Application context带了项目名所有写死的路径都缺了这段。解决在 IDEA 里全局搜索WebServlet和.jsp中的action把两边的路径粘贴到文本里对比。比如WebServlet(/login)对应表单actionlogin成功后跳转应写resp.sendRedirect(index.jsp)而不是resp.sendRedirect(/index.jsp)——多一个斜杠就是另一个绝对路径。我一般会把Application context改成/这样浏览器里的路径最直观也不容易被工程名干扰。5.2 数据库连接失败“Communications link failure”原因时区与驱动版本问题现象启动 Tomcat 后第一次访问数据页面控制台或页面报Communications link failure有时还会跟着The last packet sent successfully to the server was 0 milliseconds ago。第一次遇到会下意识以为是 MySQL 服务没开检查后却一切正常。原因这是 MySQL 8.x 与连接参数不匹配的典型表现。MySQL 8.0 驱动com.mysql.cj.jdbc.Driver要求 URL 里带serverTimezone不带就报时区错而Communications link failure是它在握手阶段失败的另一种表现。解决把jdbc:mysql://localhost:3306/数据库名的 URL 补充完整加上?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue并且确认DBUtil里的驱动类名是com.mysql.cj.jdbc.Driver。改了这两处还连不上就换个mysql-connector-javajar旧项目里放 8.0 的 jar 代码写 5.x 类名会一直卡在同一个位置。5.3 中文乱码原因请求编码和数据库字符集不一致现象页面显示 “??”或者存入数据库之后变成乱码有的网页源码是 UTF-8但浏览器按 GBK 解析。查起来费时间但根因基本就两类数据库字符集不统一或者请求被解码时用了错误编码。原因JSP 页面声明了 UTF-8但数据库连接 URL 没有带characterEncodingutf8或者请求处理时没有在第一次读取参数前调用setCharacterEncoding导致 Tomcat 默认按 ISO-8859-1 解码。解决三步走。第一步是数据库连接 URL 里带上characterEncodingutf8确保 JDBC 传输层是 UTF-8。第二步是在所有doGet/doPost开头加req.setCharacterEncoding(UTF-8)而且要放在第一次读参数之前。第三步是检查 MySQL 建库语句用DEFAULT CHARACTER SET utf8mb4重建数据库。如果你用的是旧 MySQL 5.5 环境utf8mb4可能不支持那就退回utf8但页面 meta 也要写charsetUTF-8两头对齐。说起来都是老生常谈但我见过太多人只改 JSP 页面编码忽略了数据库端白忙一下午。5.4 JSP页面报ClassNotFoundException/找不到jar原因缺少lib目录依赖现象启动页能打开但一访问某个功能页面就报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者直接看到一大串NoClassDefFoundError。原因导入工程后web/WEB-INF/lib目录没有被 IDEA 识别成依赖库。有些压缩包为了减小体积会把 jar 删掉只留一个“下载驱动”的说明还有的工程是 Maven 项目但你在 IDEA 里没导入 Maven 依赖导致所有第三方类都找不到。解决到File - Project Structure - Libraries里手动把web/WEB-INF/lib添加进依赖。如果是 Maven 工程就等右下角进度条下载完毕再在 Maven 面板里执行reimport。检查lib里有没有驱动 jar、commons-fileupload、JSTL等如果缺失从本地 Maven 仓库或网络镜像补。这里有个值得养成的习惯交作业的压缩包里必须把 jar 带上并写一个README说明驱动版本否则换台电脑就是一个新的 404。5.5 导出项目给老师后别人跑不起来原因路径和配置文件绝对路径现象项目在自己机器上一切正常复制到老师电脑后Tomcat 启动成功数据库也能连但页面图片加载不出来或者 Excel 导出功能找不到路径点击某个菜单直接报文件不存在。原因代码里写了本地绝对路径比如图片上传保存到了C:/Users/xxx/upload或配置里写了D:/mysql/data。这类路径只在你电脑上存在换机器就失效。解决把所有外部资源路径改成相对路径或配置项。图片上传保存位置可以写到upload/目录下然后用 Servlet 映射/upload/*去读取数据库连接信息写进db.properties而不是散落在 DAO 类里。这样别人拿到项目后只需要改一个配置文件的连接参数。我一般还会在压缩包里放一个运行说明.txt把 JDK、Tomcat、MySQL 版本和初始账号密码都列上这比多写十行注释都有用。别嫌麻烦期末项目最重要的是让人能复现复现不了分再满也有风险。6. 从“能跑”到“高分”答辩前值得补的三个验证细节6.1 给系统加一个操作日志表答辩时老师问“你怎么证明系统做过”一个很有效的回答是从操作日志开始。加一张oper_log表在用户登录、删除药品、修改库存时插入一条记录。做法不复杂用 Filter 或直接在 Servlet 里写一行 JDBC 代码即可。登录成功后insert into oper_log(user_id, event, create_time) values(?, 登录系统, now())然后新增一个“操作日志”页面按时间倒序展示。这会把“系统”从静态的 CRUD 拉高成有行为记录的系统也给了老师一个可以在 UI 上看到的验证入口。6.2 用事务把入库出库包起来第2章已经写过库存扣减的伪代码这里说什么时候补如果现有源码里入库和更新库存是两条独立 SQL没有try-catch-rollback建议在答辩前把它改成事务。改完后自己试验一次把updateDrug后面的语句故意写成错误 SQL看事务会不会回滚再试验正常流程确认库存减少和单据新增同时生效。把这两个试验结果写在笔记里老师问“有什么健壮性保障”时你直接说出“我测过回滚”就很有说服力。6.3 用统一异常页兜底最后一个小技巧是给项目加一个error.jsp在web.xml里配置 500 错误跳转到它。这样即便是你代码里的空指针也不会把一屏异常抛给老师而是看到友好的“系统繁忙请稍后再试”。这虽然不能真正消除 bug但在答辩演示的关键时刻能避免当场社死。我自己的习惯是答辩前把所有功能按顺序走一遍遇到异常先看是不是环境问题再排查代码最后再准备“这个问题我清楚是因为什么”的回答。做 JavaWeb 期末项目这几年最深的教训是跑通只是起点能在面对提问时把每条链路、每个配置讲清楚才是高分的关键。希望帮到你。本文还有配套的精品资源点击获取