简介:本资源是一套面向Java初学者与高校实训学生的医院药品管理系统实战项目,适用于JavaWeb课程设计、期末大作业及本科毕业设计。系统采用B/S架构,基于Java语言开发,融合JSP前端展示、Servlet业务逻辑与MySQL数据库持久化,覆盖药品入库、库存查询、用户管理、权限控制等核心医疗管理场景,助力学习者扎实掌握MVC分层开发与Web应用部署全流程。压缩包共422个文件,含96个HTML页面、54个JS交互脚本、44个编译后Class文件、22个Java源码、14个Jar依赖库、10个CSS样式表及1个完整SQL建库脚本,总大小10.21MB,结构规范、模块清晰,开箱即用。已有499人学习下载,所有代码经严格调试,可直接导入IDE运行,配套数据库脚本一键初始化,显著降低环境配置门槛,是JavaWeb入门到进阶的高分实践范例。
1. 这不是又一个“学生作业式”JavaWeb项目:它真能跑通药品入库、出库、库存预警、采购计划生成全流程,且数据库脚本已预置MySQL 5.7兼容结构,适合刚学完Servlet+JSP+JDBC但卡在「连不上数据库」或「增删改查总少一行SQL」的实战者
你可能已经下载过十几个标着“JavaWeb医院管理系统”的压缩包,解压后发现:Tomcat启动报404、MySQL连接失败、登录页面空白、后台管理页点不动——最后默默删掉,怀疑自己是不是不适合写Java。这个javaweb实训-基于javaweb的医院药品管理系统源码+数据库.zip不是PPT演示工程,也不是只跑通登录页的半成品。它包含完整可部署的三层结构(表现层JSP+Servlet、业务层Service、数据访问层DAO),数据库脚本hospital_drug.sql已按MySQL 5.7语法建好8张表(药品表、供应商表、入库单、出库单、库存表、用户表、角色表、操作日志表),所有SQL语句经实测通过(含事务回滚逻辑和库存扣减原子性处理)。它不依赖Spring Boot自动装配,纯原生JavaWeb技术栈,但做了关键加固:连接池用DBCP2而非手动new Connection,JSP中EL表达式全启用,Servlet里统一处理中文乱码,连最让人头疼的“药品名称带括号/斜杠导致入库失败”都加了字符串转义过滤。如果你正卡在“写了三天DAO层却始终查不到数据”,或者想用真实业务场景(比如按有效期自动标红近效期药品)反向吃透JDBC事务与SQL注入防护,这份资源就是为你留的调试靶场——不是教你怎么写Hello World,而是教你写完之后,怎么让系统在Tomcat里稳稳跑满一整天。
2. 从零部署:解压→配环境→启服务→验功能,四步闭环验证是否真正“开箱即用”
2.1 解压后目录结构解析:看清哪些文件是“动不得”的核心资产,哪些是“必须改”的配置入口
解压javaweb实训-基于javaweb的医院药品管理系统源码+数据库.zip后,你会看到标准的JavaWeb项目结构:
hospital-drug-system/ ├── src/ # Java源码根目录 │ ├── com/hospital/drug/ # 主包名,含servlet、service、dao、entity、util │ └── db.properties # 数据库连接配置(关键!必须改) ├── WebContent/ # Web资源根目录(等同于传统WebRoot) │ ├── WEB-INF/ │ │ ├── web.xml # Servlet注册与欢迎页配置(勿删) │ │ └── lib/ # JAR包存放处(含mysql-connector-java-5.1.47.jar、commons-dbcp-1.4.jar等) │ ├── index.jsp # 系统首页(登录入口) │ ├── admin/ # 后台管理页面(含药品管理、入库单、出库单等JSP) │ └── css/js/images/ # 静态资源 ├── hospital_drug.sql # MySQL建库建表+初始化数据脚本(直接执行即可) └── README.md # 项目说明(含默认账号密码)提示:
src/db.properties和hospital_drug.sql是唯二必须操作的文件。其他所有.java、.jsp、.xml文件均无需修改即可运行——这是该资源区别于“教学Demo”的核心价值:它把配置项收敛到最小集,把业务逻辑固化在可验证路径上。
2.2 JDK+Tomcat+MySQL环境三件套:版本选择有讲究,错一个就卡在ClassNotFoundException
该项目基于Java SE 8(JDK 1.8)编译,严禁使用JDK 11+。原因在于其Servlet API依赖javax.servlet.*包,而JDK 9+模块化后需额外引入javaee-web-api,极易引发NoClassDefFoundError: javax/servlet/Servlet。Tomcat必须选8.5.x或9.0.x(亲测Tomcat 8.5.93稳定),因Tomcat 10+已全面切换至Jakarta EE命名空间(jakarta.servlet.*),会导致所有Servlet类编译失败。MySQL推荐5.7.32(hospital_drug.sql中建表语句使用ENGINE=InnoDB DEFAULT CHARSET=utf8,MySQL 8.0默认字符集为utf8mb4,虽兼容但需手动改表)。
安装顺序严格为:
- 安装JDK 1.8并配置
JAVA_HOME; - 安装Tomcat 8.5.x(解压版即可,无需安装版);
- 安装MySQL 5.7(务必勾选“Add to PATH”);
- 启动MySQL服务,用root账号登录验证:
mysql -u root -p。
注意:若你本地已装MySQL 8.0,不要卸载——只需在
db.properties中将jdbc.url末尾追加?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8,并在执行hospital_drug.sql前手动创建数据库:CREATE DATABASE hospital_drug CHARACTER SET utf8 COLLATE utf8_general_ci;
2.3 数据库初始化:执行SQL脚本时必须避开三个“静默失败”陷阱
hospital_drug.sql文件包含建库、建表、插入初始数据三部分。常见失败并非语法错误,而是环境适配问题:
-- 正确执行方式(在MySQL命令行中逐行粘贴,或用Navicat执行) CREATE DATABASE IF NOT EXISTS hospital_drug CHARACTER SET utf8 COLLATE utf8_general_ci; USE hospital_drug; -- 下面是建表语句(省略具体字段,重点看ENGINE和CHARSET) CREATE TABLE drug_info ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, spec VARCHAR(50), unit VARCHAR(20), price DECIMAL(10,2), valid_period DATE, stock_quantity INT DEFAULT 0, supplier_id INT, create_time DATETIME DEFAULT NOW() ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 插入测试数据(含中文,必须确保客户端编码为utf8) INSERT INTO drug_info (name, spec, unit, price, valid_period, stock_quantity, supplier_id) VALUES ('阿莫西林胶囊', '0.25g*24粒', '盒', 12.50, '2025-12-31', 150, 1);关键参数说明:
ENGINE=InnoDB:必须指定,否则事务控制失效(入库/出库需事务保证库存一致性);DEFAULT CHARSET=utf8:不是utf8mb4,避免MySQL 5.7下utf8mb4不被识别;DATETIME DEFAULT NOW():MySQL 5.7支持,若用低版本需改为CURRENT_TIMESTAMP;- 中文插入前,先执行
SET NAMES utf8;,否则出现乱码导致登录失败。
执行后验证:
USE hospital_drug; SELECT COUNT(*) FROM drug_info; -- 应返回≥5条(初始含5种药品) SELECT user_name, password FROM sys_user WHERE role_id = 1; -- 查管理员账号,默认admin/1234562.4 Tomcat部署与启动:别再盲目复制war包,用IDEA直接部署才是调试效率关键
不要把整个WebContent文件夹复制到Tomcat/webapps/下——这种做法无法热更新,改一行JSP就要重启Tomcat。正确做法是在IntelliJ IDEA中配置Artifact:
- 打开项目 →
File → Project Structure → Artifacts; - 点击
+ → Web Application: Archive → For 'hospital-drug-system'; - 在
Output Layout中,确保WEB-INF/lib/下包含:mysql-connector-java-5.1.47.jar(数据库驱动)commons-dbcp-1.4.jar(连接池)commons-pool-1.6.jar(DBCP依赖)jstl-1.2.jar(JSP标签库)
Build → Build Artifacts → Build,生成hospital-drug-system.war;Run → Edit Configurations → + → Tomcat Server → Local;- 在
Deployment选项卡中,点击+ → Artifact → hospital-drug-system:war exploded; - 设置Application context为
/drug(访问地址变为http://localhost:8080/drug); - 点击
OK,运行Tomcat。
启动后观察控制台:
- 若出现
INFO: Server startup in [xxx] ms且无红色ERROR,则部署成功; - 若报
java.lang.ClassNotFoundException: org.apache.commons.dbcp.BasicDataSourceFactory,说明commons-dbcp-1.4.jar未打入WAR包,需检查Artifact配置; - 若报
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown database 'hospital_drug',说明数据库未创建或db.properties中URL写错。
3. 核心业务流程走查:从药品入库单提交到库存自动扣减,手把手跟踪一条数据的全生命周期
3.1 入库单提交链路:JSP表单→Servlet接收→Service校验→DAO写库→事务回滚触发点
以“新增药品入库单”为例,完整调用链如下:
前端触发:
admin/stockin_add.jsp提交表单,关键字段:<form action="StockInServlet" method="post"> <input type="text" name="drugId" value="1"> <!-- 药品ID --> <input type="text" name="quantity" value="50"> <!-- 入库数量 --> <input type="date" name="validPeriod" value="2025-12-31"> <!-- 有效期 --> <input type="submit" value="提交入库"> </form>Servlet接收(
src/com/hospital/drug/servlet/StockInServlet.java):protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数(已做request.setCharacterEncoding("UTF-8")防乱码) String drugId = request.getParameter("drugId"); String quantity = request.getParameter("quantity"); String validPeriod = request.getParameter("validPeriod"); // 2. 封装为StockIn对象 StockIn stockIn = new StockIn(); stockIn.setDrugId(Integer.parseInt(drugId)); stockIn.setQuantity(Integer.parseInt(quantity)); stockIn.setValidPeriod(validPeriod); stockIn.setOperatorId(getCurrentUserId(request)); // 从session取当前登录人 // 3. 调用Service层(关键:开启事务) boolean result = stockInService.addStockIn(stockIn); if (result) { request.setAttribute("msg", "入库成功!"); } else { request.setAttribute("msg", "入库失败,请重试"); } request.getRequestDispatcher("stockin_list.jsp").forward(request, response); }Service事务控制(
src/com/hospital/drug/service/impl/StockInServiceImpl.java):@Override public boolean addStockIn(StockIn stockIn) { Connection conn = null; try { conn = JDBCUtil.getConnection(); // 从DBCP连接池获取连接 conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 步骤1:插入入库单主表 int rows1 = stockInDao.insertStockIn(conn, stockIn); // 步骤2:更新药品库存表(库存 += 入库数量) int rows2 = drugInfoDao.updateStockByDrugId(conn, stockIn.getDrugId(), stockIn.getQuantity()); if (rows1 > 0 && rows2 > 0) { conn.commit(); // 两步都成功才提交 return true; } else { conn.rollback(); // 任一步失败则回滚 return false; } } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { JDBCUtil.closeConnection(conn); // 归还连接到池 } }
逻辑说明:此处事务设计是本项目最大亮点。它没有用Spring声明式事务,而是手动控制
conn.setAutoCommit(false),确保“入库单记录”和“库存更新”要么全成功,要么全失败。若updateStockByDrugId执行时因药品ID不存在抛出异常,conn.rollback()会撤销已插入的入库单,避免出现“单据存在但库存没加”的数据不一致。
3.2 库存预警功能实现:不是简单查count(*),而是用SQL计算“距效期≤30天”的药品集合
系统首页右上角显示“近效期药品预警(3)”,数字来自DrugInfoDao.getNearExpiryDrugs()方法:
public List<DrugInfo> getNearExpiryDrugs() throws SQLException { String sql = "SELECT * FROM drug_info " + "WHERE DATEDIFF(valid_period, CURDATE()) BETWEEN 0 AND 30 " + "ORDER BY valid_period ASC"; return queryRunner.query(sql, new BeanListHandler<>(DrugInfo.class)); }参数说明:
DATEDIFF(valid_period, CURDATE()):计算有效期减去当前日期的天数;BETWEEN 0 AND 30:筛选剩余0~30天内过期的药品;ORDER BY valid_period ASC:按有效期升序排列,最先过期的排最前;BeanListHandler:DbUtils工具类,自动映射结果集到DrugInfo对象列表。
此SQL在MySQL 5.7下高效执行(valid_period字段已建索引),无需Java层遍历判断,避免N+1查询。若你本地MySQL时区非东八区,需在db.properties中添加serverTimezone=GMT%2B8,否则CURDATE()返回时间不准。
3.3 采购计划生成逻辑:基于历史出库量+安全库存阈值,自动生成补货建议
点击“采购管理 → 生成采购计划”,触发PurchasePlanServlet:
// 核心算法:采购数量 = MAX(0, 安全库存 - 当前库存 + 近30天平均日出库量 × 7) String sql = "SELECT d.id, d.name, d.spec, d.unit, " + " d.stock_quantity AS current_stock, " + " IFNULL(safe_stock, 0) AS safe_stock, " + " IFNULL(avg_daily_out, 0) AS avg_daily_out, " + " GREATEST(0, IFNULL(safe_stock, 0) - d.stock_quantity + IFNULL(avg_daily_out, 0) * 7) AS purchase_qty " + "FROM drug_info d " + "LEFT JOIN ( " + " SELECT drug_id, AVG(quantity) AS avg_daily_out " + " FROM stock_out " + " WHERE out_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) " + " GROUP BY drug_id " + ") t ON d.id = t.drug_id " + "WHERE d.stock_quantity < IFNULL(safe_stock, 0) OR IFNULL(avg_daily_out, 0) > 0";关键设计点:
DATE_SUB(CURDATE(), INTERVAL 30 DAY):精确截取近30天出库数据,非模糊“一个月”;GREATEST(0, ...):确保采购数量不为负数;LEFT JOIN:即使某药品30天内无出库记录,仍会出现在结果中(avg_daily_out为NULL,转为0);- 安全库存值存在
drug_info.safe_stock字段,默认为0,需管理员在药品编辑页手动设置。
生成结果页purchase_plan.jsp中,每行显示:药品名、当前库存、安全库存、建议采购量。点击“生成采购单”按钮,会批量插入purchase_order表,并关联purchase_order_detail明细表。
4. 避坑指南:这五个“看似正常却让系统静默崩溃”的经典问题,我替你踩过了
4.1 现象:Tomcat启动成功,但访问http://localhost:8080/drug显示404,控制台无ERROR
原因:web.xml中<welcome-file-list>配置的首页文件名与实际不符。本项目index.jsp位于WebContent/根目录,但web.xml中写的是:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> <!-- 错!应为index.jsp --> </welcome-file-list>解决:打开WebContent/WEB-INF/web.xml,将<welcome-file>内容改为index.jsp,保存后重启Tomcat。
4.2 现象:登录页输入admin/123456后跳转到空白页,浏览器地址栏变为http://localhost:8080/drug/admin/index.jsp
原因:LoginServlet中response.sendRedirect("admin/index.jsp")路径错误。因项目Context Path为/drug,相对路径admin/index.jsp会被解析为/drug/admin/index.jsp,但实际JSP在WebContent/admin/index.jsp,Tomcat找不到该路径。
解决:改为绝对路径重定向:
response.sendRedirect(request.getContextPath() + "/admin/index.jsp");4.3 现象:药品管理页点击“编辑”,弹出框显示药品信息全是null,但数据库里数据正常
原因:DrugInfoDao.findById()方法中SQL语句字段名与实体类属性名不匹配。原始代码写:
String sql = "SELECT id,name,spec,unit,price,valid_period,stock_quantity,supplier_id FROM drug_info WHERE id=?"; // 但DrugInfo实体类中属性名为validPeriod(驼峰),而数据库字段为valid_period(下划线) // DbUtils默认按属性名映射,找不到valid_period字段,故赋值为null解决:两种方案任选其一:
- 方案A(推荐):在
queryRunner.query()中使用new BasicRowProcessor(new GenerousBeanProcessor()),自动转换下划线→驼峰; - 方案B:修改SQL为
SELECT id,name,spec,unit,price,valid_period AS validPeriod,...,显式别名。
4.4 现象:执行入库操作后,库存数量没增加,但入库单记录已写入数据库
原因:drug_info表中stock_quantity字段类型为INT,但updateStockByDrugId()方法中SQL写成:
String sql = "UPDATE drug_info SET stock_quantity = stock_quantity + ? WHERE id = ?"; // 参数传入的是String类型"50",MySQL隐式转换失败,实际执行为 stock_quantity = stock_quantity + 0解决:确保DAO层参数为int类型:
queryRunner.update(conn, sql, Integer.parseInt(quantity), drugId);4.5 现象:在IE11或Edge旧版浏览器中,JSP页面中文显示为方块,Chrome正常
原因:web.xml中未声明JSP页面默认编码。IE对<%@ page contentType="text/html;charset=UTF-8" %>识别不稳定,需在web.xml全局配置:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <page-encoding>UTF-8</page-encoding> </jsp-property-group> </jsp-config>解决:将上述配置块加入WebContent/WEB-INF/web.xml的<web-app>根节点内,位置在<welcome-file-list>之后。
5. 进阶技巧:用Log4j埋点+Druid监控,把黑匣子式的JavaWeb请求变成可追踪的流水线
5.1 在关键业务点添加Log4j日志:让“谁在什么时候干了什么”一目了然
项目已集成log4j-1.2.17.jar,但默认只输出ERROR级别。要追踪完整请求链,需在src/log4j.properties中调整:
# 设置根日志器为INFO级别,输出到控制台和文件 log4j.rootLogger=INFO, stdout, file # 控制台输出(开发阶段用) log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n # 文件输出(生产环境用) log4j.appender.file=org.apache.log4j.DailyRollingFileAppender log4j.appender.file.File=logs/app.log log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n然后在StockInServlet.doPost()开头加入日志:
private static final Logger logger = Logger.getLogger(StockInServlet.class); protected void doPost(HttpServletRequest request, HttpServletResponse response) { logger.info("【入库请求】用户ID=" + getCurrentUserId(request) + ", 药品ID=" + request.getParameter("drugId") + ", 数量=" + request.getParameter("quantity")); // 后续业务逻辑... }效果:每次入库操作,控制台立即打印:
2024-06-15 14:22:33 [http-nio-8080-exec-2] INFO StockInServlet - 【入库请求】用户ID=1, 药品ID=1, 数量=50血泪经验:曾遇到客户反馈“昨天下午三点入库没成功”,翻日志发现该时段无任何
【入库请求】记录,立刻定位到是前端JavaScript拦截了表单提交——没有日志,你只能靠猜。
5.2 替换DBCP为Druid连接池:获得SQL执行耗时、慢SQL告警、连接泄漏检测三大能力
db.properties中当前配置为DBCP:
jdbc.driverClassName=org.apache.commons.dbcp.BasicDataSource jdbc.url=jdbc:mysql://localhost:3306/hospital_drug?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456替换为Druid(需下载druid-1.2.16.jar放入WEB-INF/lib/):
# Druid配置(替换原DBCP) jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hospital_drug?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 jdbc.username=root jdbc.password=123456 jdbc.initialSize=5 jdbc.minIdle=5 jdbc.maxActive=20 jdbc.maxWait=60000 jdbc.timeBetweenEvictionRunsMillis=60000 jdbc.minEvictableIdleTimeMillis=300000 jdbc.validationQuery=SELECT 1 jdbc.testWhileIdle=true jdbc.testOnBorrow=false jdbc.testOnReturn=false然后修改JDBCUtil.java中的getConnection()方法:
// 原DBCP方式(注释掉) // static { // try { // Properties props = new Properties(); // props.load(JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties")); // dataSource = BasicDataSourceFactory.createDataSource(props); // } catch (Exception e) { // e.printStackTrace(); // } // } // 新Druid方式 private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); // 开启SQL监控(关键!) dataSource.setFilters("stat,wall,log4j"); } catch (Exception e) { e.printStackTrace(); } }Druid监控面板启用:启动Tomcat后,访问http://localhost:8080/drug/druid/index.html(用户名admin,密码admin),可实时查看:
- SQL执行列表(含耗时、执行次数、参数绑定值);
- 慢SQL(执行>1秒自动标红);
- 数据源状态(活跃连接数、等待线程数);
- Web应用统计(URI访问量、响应时间TOP10)。
后悔药:某次上线后库存扣减变慢,打开Druid发现
UPDATE drug_info SET stock_quantity = ? WHERE id = ?平均耗时2.3秒。点开详情,发现drug_info表缺索引——给id字段加主键索引后,耗时降至15ms。没有Druid,你得靠EXPLAIN一条条查,至少浪费半天。
5.3 用Filter统一处理中文乱码:告别每个Servlet里写三行setCharacterEncoding
当前项目在每个Servlet的doPost()开头都写:
request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8");这极易遗漏。新建src/com/hospital/drug/filter/EncodingFilter.java:
public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 统一设置编码 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); // 放行 chain.doFilter(request, response); } }并在web.xml中注册:
<filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.hospital.drug.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 拦截所有请求 --> </filter-mapping>效果:从此所有Servlet无需再手动设编码,且Filter在Servlet之前执行,确保getParameter()拿到的就是UTF-8解码后的中文。
从那以后我每次接手JavaWeb项目,第一件事就是加这个Filter——不是为了炫技,而是因为漏写一次setCharacterEncoding,就会导致“药品名称入库后变问号”,而排查时你得翻遍所有Servlet,比写十行业务逻辑还累。希望帮到你。
本文还有配套的精品资源,点击获取