☰
JavaWeb医药管理系统实战:从数据库设计到高分答辩全指南
2026/10/4 21:18:02 网站建设 项目流程

简介:面向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 ) ENGINE=InnoDB; CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, contact VARCHAR(32), phone VARCHAR(20), address VARCHAR(128) ) ENGINE=InnoDB; 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 ) ENGINE=InnoDB;

这份脚本里最关键的两个细节是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 设为 false:

public 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 驱动 jar;web.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 10,Servlet 注解和 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的好处是改参数不用重新编译,典型内容长这样:

driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/drug_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=123456

这里的三个参数十分容易踩雷。serverTimezone=Asia/Shanghai不加,MySQL 8.0 的驱动会在连接时报时区错误,报错信息里会提示serverTimezone,但很多人看到英文就会慌。useSSL=false是为了跳过本机测试时的 SSL 警告。allowPublicKeyRetrieval=true是 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. 代码从哪里看起:按请求流程读懂Servlet+JSP+DAO

4.1 从登录页到底层SQL:一条请求链路上的分层写法

运行起来后的第一件事不是乱点页面,而是把登录这个流程读通,因为它是几乎所有期末项目的“主心骨”。一条请求在 JavaWeb 工程里大致经过:JSP 表单提交到 Servlet,Servlet 调用 DAO 查数据库,DAO 返回结果给 Servlet,Servlet 再决定跳转登录成功页面还是错误页。这个链路答上来,老师第一印象就稳了。

以登录为例,页面里的表单大概长这样:

<form action="login" method="post"> 用户名:<input type="text" name="username"> 密码:<input type="password" name="password"> <input type="submit" value="登录"> </form>

重点是action="login",这个值要和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,访问任何页面先检查 session:

@WebFilter("/*") 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 List<Drug> listDrugs(String keyword, String category) throws SQLException { List<Drug> list = new ArrayList<>(); // 用 StringBuilder 拼 SQL,理由是不让条件越来越长时阅读困难 StringBuilder sql = new StringBuilder( "SELECT id, name, spec, unit, price, stock, supplier_id FROM drug WHERE 1=1"); List<Object> 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 1=1是个技巧,它让后面的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")对应表单action="login",成功后跳转应写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 补充完整,加上?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true,并且确认DBUtil里的驱动类名是com.mysql.cj.jdbc.Driver。改了这两处还连不上,就换个mysql-connector-javajar,旧项目里放 8.0 的 jar 代码写 5.x 类名,会一直卡在同一个位置。

5.3 中文乱码,原因:请求编码和数据库字符集不一致

现象:页面显示 “??”,或者存入数据库之后变成乱码,有的网页源码是 UTF-8,但浏览器按 GBK 解析。查起来费时间,但根因基本就两类:数据库字符集不统一,或者请求被解码时用了错误编码。

原因:JSP 页面声明了 UTF-8,但数据库连接 URL 没有带characterEncoding=utf8;或者请求处理时没有在第一次读取参数前调用setCharacterEncoding,导致 Tomcat 默认按 ISO-8859-1 解码。

解决:三步走。第一步是数据库连接 URL 里带上characterEncoding=utf8,确保 JDBC 传输层是 UTF-8。第二步是在所有doGet/doPost开头加req.setCharacterEncoding("UTF-8"),而且要放在第一次读参数之前。第三步是检查 MySQL 建库语句,用DEFAULT CHARACTER SET utf8mb4重建数据库。如果你用的是旧 MySQL 5.5 环境,utf8mb4可能不支持,那就退回utf8,但页面 meta 也要写charset=UTF-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 期末项目这几年,最深的教训是:跑通只是起点,能在面对提问时把每条链路、每个配置讲清楚,才是高分的关键。希望帮到你。

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

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

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

立即咨询