简介:本资源是一套面向Java初学者与高校实训学生的医院药品管理系统实战项目,适用于JavaWeb课程设计、期末大作业及本科毕业设计。系统采用B/S架构,基于Java语言开发,整合JSP前端页面、Servlet业务逻辑与MySQL数据库,覆盖药品入库、库存查询、用户管理、权限控制等核心医疗管理功能,助力学习者掌握MVC分层开发、数据库连接池、DAO模式及前后端交互全流程。压缩包共422个文件,含96个HTML页面、54个JavaScript脚本、44个编译后Class文件、22个Java源码、14个Jar依赖库、10个CSS样式表及1个完整SQL建库建表脚本,总大小10.21MB,结构规范、模块清晰,开箱即用。已有499人学习下载,所有代码经严格调试,可直接部署运行,配套数据库脚本与目录组织逻辑明确,便于理解分层架构与工程化实践路径。
1. 这不是又一个“学生管理系统”:一个真实跑在Tomcat上的医院药品管理后台,为什么能避开90%的JavaWeb实训翻车现场?
你下载了javaweb实训-基于javaweb的医院药品管理系统源码+数据库.zip,解压后看到WebContent/,src/,lib/, 还有一堆.sql文件——但双击index.jsp打不开,IDEA里右键 Run on Server 报 404,MySQL导入失败提示Unknown database 'hospital_drug',甚至web.xml里连个<servlet>都没配……别急,这不是你手残,而是绝大多数 JavaWeb 实训项目交付物的真实状态:它不是演示工程,而是一份带完整业务闭环的可部署系统快照。它解决的不是“怎么写Servlet”,而是“药房管理员每天要查库存、做入库、开出库单、核对效期、导出月报表”这一整套动作链;它用的是 JSP + Servlet + JDBC 原生组合(非Spring Boot),但所有 DAO 层都做了 Connection 管理和 SQL 参数化,所有页面都带基础权限跳转拦截(登录态校验+角色路由);它附带的 MySQL 数据库脚本包含 7 张表(药品、供应商、入库单、出库单、库存、用户、操作日志),且字段设计符合医院实际(如drug.expiry_date是 DATE 类型,inventory.batch_no允许 NULL 但inventory.quantity有 CHECK 约束)。适合刚学完 JDBC 和 JSP 的大二学生,也适合需要快速验证 JavaWeb 分层结构是否合理的培训讲师——只要你愿意花 25 分钟配好环境、改对三处路径、跑通第一个增删改查请求,它就能真正在你本地 Tomcat 上吐出带分页的药品列表页。
2. 从 ZIP 解压到浏览器显示“药品管理首页”:四步走通最小可运行路径
2.1 解压后先看懂目录结构:别急着导入 IDEA,先认人
这个 ZIP 包不是 Maven 工程,也不是 Gradle 项目,它是一个标准的JavaEE Web Application Archive(WAR)结构雏形。解压后你会看到:
hospital_drug_system/ ├── WebContent/ ← 等价于 webapp/,JSP、CSS、JS、图片全在这 │ ├── index.jsp ← 登录入口页(注意:不是首页!) │ ├── admin/ ← 管理员功能页(药品管理、用户管理等) │ ├── user/ ← 普通药剂师功能页(入库、出库、查询) │ └── WEB-INF/ │ ├── web.xml ← 核心配置:Servlet 映射、过滤器、欢迎页 │ └── lib/ ← 所有依赖 JAR:mysql-connector-java-5.1.47.jar、jstl.jar、standard.jar ├── src/ ← Java 源码:com.hospital.dao/、com.hospital.service/、com.hospital.servlet/ ├── sql/ ← hospital_drug.sql:建库建表+初始数据(含管理员账号 admin/123456) └── README.md ← 作者写的极简说明(常被忽略,但里面写了 Tomcat 版本要求)提示:
WebContent是 Eclipse 默认 WebRoot,IDEA 默认识别为webapp。如果你用 IDEA 导入,选 “Import Project” → 选中根目录 → 选 “Import project from external model” → 勾选 “Web” → 下一步时手动指定WebContent为 Web root(不是src!)。否则 JSP 路径全错。
2.2 数据库准备:建库、导入、验证三连击
这个系统强依赖 MySQL 5.7(不支持 8.0 默认的caching_sha2_password认证插件),且必须用utf8mb4字符集。常见翻车点是直接双击.sql文件用 Navicat 打开执行——它会默认用当前连接字符集,导致中文乱码或建表失败。
正确做法(命令行最稳):
# 1. 登录 MySQL(确保是 root 或有 CREATE DATABASE 权限) mysql -u root -p # 2. 创建数据库(显式指定字符集,避坑关键!) CREATE DATABASE hospital_drug CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切换数据库并导入(绝对路径!别用相对路径) USE hospital_drug; SOURCE /path/to/your/sql/hospital_drug.sql; -- 注意:Linux 用正斜杠,Windows 用反斜杠或双反斜杠 # 4. 验证导入结果(必须看到 7 表 + 10+ 条初始数据) SHOW TABLES; SELECT COUNT(*) FROM drug; -- 应该返回 12(示例药品) SELECT * FROM user WHERE username='admin'\G; -- 查看管理员密码(明文存的,实训项目不加密)参数说明:
utf8mb4是唯一能完整存储 emoji 和生僻汉字的 MySQL 字符集;COLLATE utf8mb4_unicode_ci支持中文排序;SOURCE命令必须用绝对路径,且文件内不能有USE hospital_drug;语句(否则导入会失败)。
2.3 IDEA 配置 Tomcat:不是点一下“Add Configuration”就完事
很多同学卡在“Run on Server”后浏览器打开http://localhost:8080/显示 404,根本原因是IDEA 没把WebContent当作 Web Resource Root,也没把src编译输出指向WEB-INF/classes。
按此顺序配置(以 IDEA 2023.2 为例):
File → Project Structure → Modules → [你的模块名] → Sources- 选中
src目录 → 点击Sources(标记为源码根目录) - 选中
WebContent目录 → 点击Resources(错误!应点Web Resources)→ 在弹出框中勾选Web Resource Directory,并确认Web resource directory填的是WebContent(不是WebContent/WEB-INF)
- 选中
Project Structure → Artifacts → + → Web Application: Exploded → From modules...- 选择你的模块 → 确保
Output directory是out/artifacts/xxx_war_exploded - 在
Available Elements中展开WebContent→ 拖拽整个WebContent到右侧WEB-INF同级位置(即WebContent直接映射为根路径/) - 展开
src→ 拖拽Compiled output到WEB-INF/classes下
- 选择你的模块 → 确保
Run → Edit Configurations → + → Tomcat Server → LocalDeployment → + → Artifact → [你的 xxx_war_exploded:exploded]Application context改为/(不是/xxx!否则访问要加路径)Server → JRE选 JDK 1.8(此项目编译级别是 1.8,用 JDK 11 会报javax.servlet找不到)
关键逻辑:IDEA 的 Artifact 定义决定了 WAR 包结构。
WebContent必须作为 Web Root 映射到/,src编译结果必须落到WEB-INF/classes,否则 ClassLoader 找不到 Servlet,JSP 也找不到 taglib。
2.4 启动前必改的三处硬编码路径:绕过 95% 的启动失败
这个 ZIP 包里的代码,为了“开箱即用”,把数据库连接信息、驱动类名、甚至部分页面跳转路径都写死了。不改,Tomcat 启动后一访问就SQLException: Access denied或NullPointerException。
打开src/com/hospital/dao/BaseDao.java(所有 DAO 的父类),找到以下三处:
// 【改1】数据库 URL:把 localhost 改成你本机 IP 或 127.0.0.1(某些系统 localhost 解析慢) private static final String URL = "jdbc:mysql://127.0.0.1:3306/hospital_drug?useUnicode=true&characterEncoding=utf8"; // 【改2】用户名密码:改成你 MySQL 实际的账号(不是 SQL 文件里写的 admin/123456!那是应用层用户) private static final String USER = "root"; // 你 MySQL 的 root 用户 private static final String PASSWORD = "your_real_mysql_password"; // 你设的密码 // 【改3】驱动类名:MySQL 5.1.x 用 com.mysql.jdbc.Driver,8.0+ 用 com.mysql.cj.jdbc.Driver // 此项目 lib 里是 mysql-connector-java-5.1.47.jar,所以必须用旧驱动 private static final String DRIVER = "com.mysql.jdbc.Driver";再检查WebContent/WEB-INF/web.xml里的欢迎页:
<welcome-file-list> <welcome-file>index.jsp</welcome-file> <!-- 确保这里写的是 index.jsp,不是 login.jsp --> </welcome-file-list>参数说明:
useUnicode=true&characterEncoding=utf8是 JDBC 连接参数,告诉驱动用 UTF-8 解析数据;com.mysql.jdbc.Driver是 MySQL 5.x 的驱动类,若你强行换成 8.x 驱动,会报ClassNotFoundException;127.0.0.1比localhost更可靠,避免 DNS 解析延迟导致连接超时。
3. 登录后进不去“药品管理”页?三个典型权限与路径陷阱排查清单
3.1 现象:输入 admin/123456 登录成功,跳转到admin/index.jsp,但点击“药品管理”链接报 404
原因:admin/index.jsp里的<a href="drug/list.jsp">是相对路径,而实际 JSP 文件在WebContent/admin/drug/list.jsp,但服务器解析时认为drug/是 Web 根目录下的子目录(即/drug/list.jsp),而非/admin/drug/list.jsp。
解决:打开WebContent/admin/index.jsp,把所有href="xxx.jsp"改成href="admin/xxx.jsp"或更稳妥的href="<%=request.getContextPath()%>/admin/xxx.jsp"。同理检查user/index.jsp。
3.2 现象:点击“新增药品”按钮,弹窗显示HTTP Status 405 – Method Not Allowed
原因:DrugServlet.java里只实现了doPost()处理新增,但前端表单 method 是GET(或没写 method,默认 GET),导致容器调用doGet()—— 而doGet()方法体为空或抛异常。
解决:打开WebContent/admin/drug/add.jsp,确认<form>标签有method="post";打开src/com/hospital/servlet/DrugServlet.java,检查doGet()方法是否调用了this.doPost(request, response)(这是常见写法,避免重复逻辑)。
3.3 现象:药品列表页显示空白,F12 看 Network 里list.jsp返回 200 但 HTML 里没数据,控制台无 JS 错误
原因:list.jsp里用<c:forEach>遍历request.getAttribute("drugList"),但DrugServlet的doGet()方法里没把查询结果 setAttribute 到 request,或 set 的 key 名不一致(如代码里写request.setAttribute("drugs", list),但 JSP 里写${drugList})。
解决:打开src/com/hospital/servlet/DrugServlet.java,定位doGet()中查询后的代码段:
List<Drug> drugList = drugService.findAll(); // 查询 request.setAttribute("drugList", drugList); // ✅ key 必须是 "drugList" request.getRequestDispatcher("/admin/drug/list.jsp").forward(request, response);再打开WebContent/admin/drug/list.jsp,确认 JSTL 标签里用的是${drugList}。
注意:
<c:forEach>的items属性值必须和setAttribute的 key 完全一致,大小写敏感;且list.jsp顶部必须有<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,否则标签不解析。
4. 让药品管理真正可用:三个必须补上的业务逻辑缝合点
4.1 入库单关联药品批次与效期:从“能存”到“合规存”
原始代码里,InboundServlet.java接收入库表单时,只把drugId、quantity、operator存进inbound_order表,但医院实际要求每批药品必须记录batch_no(批号)和expiry_date(有效期至),否则无法做近效期预警。
你需要修改WebContent/admin/inbound/add.jsp,增加两个输入框:
<tr> <td>批号:</td> <td><input type="text" name="batchNo" required></td> </tr> <tr> <td>有效期至:</td> <td><input type="date" name="expiryDate" required></td> </tr>然后在src/com/hospital/servlet/InboundServlet.java的doPost()中,获取新参数并插入inbound_order表:
String batchNo = request.getParameter("batchNo"); String expiryDateStr = request.getParameter("expiryDate"); // 转为 java.sql.Date java.sql.Date expiryDate = java.sql.Date.valueOf(expiryDateStr); // 构建 InboundOrder 对象(假设已有对应实体类) InboundOrder order = new InboundOrder(); order.setDrugId(Integer.parseInt(drugId)); order.setQuantity(Integer.parseInt(quantity)); order.setBatchNo(batchNo); order.setExpiryDate(expiryDate); order.setOperator(operator); inboundService.add(order);关键逻辑:
java.sql.Date.valueOf(String)只接受yyyy-MM-dd格式,而 HTML<input type="date">正好输出此格式;batchNo允许为空(某些药品无批号),但expiryDate必须非空,所以前端加required。
4.2 出库时自动扣减库存:避免“账实不符”的玄学 bug
原始代码里,OutboundServlet.java只插入outbound_order表,没更新inventory表的quantity字段。这导致“出了 100 盒药,库存还是 100 盒”——典型的事务漏写。
在OutboundServlet.java的doPost()中,必须用同一个 Connection 执行两条 SQL(入库单插入 + 库存更新),否则跨 Connection 无法保证原子性:
Connection conn = null; try { conn = JDBCUtil.getConnection(); // BaseDao 提供的静态方法 conn.setAutoCommit(false); // 开启事务 // 1. 插入出库单 outboundService.add(outboundOrder, conn); // 2. 更新库存(注意:库存表 inventory 里 drug_id 是主键,直接 update) inventoryService.updateQuantity(outboundOrder.getDrugId(), -outboundOrder.getQuantity(), conn); conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute("msg", "出库失败:" + e.getMessage()); request.getRequestDispatcher("/admin/outbound/add.jsp").forward(request, response); return; } finally { JDBCUtil.close(conn); }参数说明:
setAutoCommit(false)是手动事务起点;updateQuantity()方法需在InventoryService中实现,SQL 为UPDATE inventory SET quantity = quantity + ? WHERE drug_id = ?(传负数表示扣减);JDBCUtil.close(conn)必须在 finally 块,防止连接泄漏。
4.3 效期预警页面:用原生 JDBC 写一个“近 30 天到期药品”查询
系统没提供效期预警页,但这是医院刚需。新建WebContent/admin/drug/expiry.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <% // 直接在 JSP 里写 DAO 查询(仅用于实训,生产环境严禁!) java.util.List<com.hospital.entity.Drug> expiringDrugs = new com.hospital.dao.DrugDao().findExpiringDrugs(30); // 查 30 天内到期的 request.setAttribute("expiringDrugs", expiringDrugs); %> <html> <head><title>近效期药品预警</title></head> <body> <h2>距有效期不足30天的药品(共 ${fn:length(expiringDrugs)} 种)</h2> <table border="1"> <tr><th>药品名</th><th>规格</th><th>批号</th><th>有效期至</th><th>剩余天数</th></tr> <c:forEach items="${expiringDrugs}" var="d"> <tr> <td>${d.name}</td> <td>${d.specification}</td> <td>${d.batchNo}</td> <td>${d.expiryDate}</td> <td><%= (int)((new java.util.Date()).getTime() - d.getExpiryDate().getTime()) / (1000*60*60*24) %></td> </tr> </c:forEach> </table> </body> </html>对应在DrugDao.java中添加方法:
public List<Drug> findExpiringDrugs(int days) throws SQLException { String sql = "SELECT d.*, i.batch_no, i.expiry_date FROM drug d " + "JOIN inventory i ON d.id = i.drug_id " + "WHERE i.expiry_date BETWEEN ? AND ? " + "ORDER BY i.expiry_date ASC"; PreparedStatement ps = connection.prepareStatement(sql); java.util.Date now = new java.util.Date(); java.sql.Date startDate = new java.sql.Date(now.getTime()); Calendar cal = Calendar.getInstance(); cal.setTime(now); cal.add(Calendar.DAY_OF_YEAR, days); java.sql.Date endDate = new java.sql.Date(cal.getTimeInMillis()); ps.setDate(1, startDate); ps.setDate(2, endDate); ResultSet rs = ps.executeQuery(); // ... 封装 Drug 对象(略) }血泪经验:日期计算别用
java.util.Date的before(),因为java.sql.Date继承自java.util.Date但重写了equals(),比较时要用getTime();BETWEEN ? AND ?比DATEDIFF更兼容不同 MySQL 版本。
5. 从“跑起来”到“真能用”:五个让实训项目脱颖而出的落地技巧
5.1 用 Filter 实现统一登录校验:告别每个 Servlet 里写 if(session==null)
原始代码里,每个doGet()开头都有:
HttpSession session = request.getSession(); if (session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; }这违反 DRY 原则,且容易漏写。用LoginFilter.java统一拦截:
public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getServletPath(); // 放行登录页、静态资源、登录 Servlet if (path.startsWith("/index.jsp") || path.startsWith("/login") || path.matches(".+\\.(css|js|png|jpg|gif)$")) { chain.doFilter(req, resp); return; } // 检查登录态 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } chain.doFilter(req, resp); } }在web.xml中注册:
<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.hospital.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>技巧价值:Filter 是 JavaWeb 最被低估的组件。它让权限控制从“每个 Servlet 自查”变成“一次配置全局生效”,且可轻松扩展为角色校验(如
admin/*拦截普通用户)。
5.2 用 JavaScript 实现入库数量实时校验:防手抖输错
add.jsp里药品数量输入框,加一行 HTML5 属性即可:
<input type="number" name="quantity" min="1" max="99999" required oninput="if(this.value < 1){this.value=1;} if(this.value > 99999){this.value=99999;}">但更进一步,联动药品总库存做校验(比如不能入库超过仓库最大容量):
<script> function checkInventory() { const drugId = document.getElementById('drugId').value; const quantity = parseInt(document.getElementById('quantity').value) || 0; if (!drugId || quantity <= 0) return; fetch('<%=request.getContextPath()%>/admin/drug/checkInventory?drugId=' + drugId) .then(r => r.json()) .then(data => { if (data.maxStock && quantity > data.maxStock) { alert('警告:入库数量 ' + quantity + ' 超过该药品最大库存 ' + data.maxStock); document.getElementById('quantity').value = data.maxStock; } }); } </script> <!-- 在 quantity input 的 oninput 里调用 --> <input type="number" name="quantity" oninput="checkInventory()" ...>后端DrugServlet.java加一个checkInventory()方法,查inventory表的max_stock字段(需提前在表里加该字段)。
技巧价值:前端校验不是摆设。它把“提交后报错”变成“输入时提醒”,用户体验提升立竿见影,且减少无效请求打到后端。
5.3 用 Log4j 记录操作日志:让“谁在什么时候干了什么”可追溯
医院系统必须留痕。在BaseServlet.java(所有 Servlet 的父类)的doPost()末尾加:
// 记录操作日志 String operator = (String) request.getSession().getAttribute("username"); String action = this.getClass().getSimpleName().replace("Servlet", "") + " - " + (request.getServletPath().contains("add") ? "新增" : request.getServletPath().contains("delete") ? "删除" : "修改"); org.apache.log4j.Logger.getLogger("OPERATE_LOG").info( String.format("[%s] %s | IP: %s | User-Agent: %s", operator, action, request.getRemoteAddr(), request.getHeader("User-Agent")));log4j.properties放在src/下:
log4j.rootLogger=INFO, FILE log4j.appender.FILE=org.apache.log4j.RollingFileAppender log4j.appender.FILE.File=logs/operate.log log4j.appender.FILE.MaxFileSize=10MB log4j.appender.FILE.MaxBackupIndex=5 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技巧价值:Log4j 是 JavaWeb 时代最成熟的日志方案。它不依赖数据库,日志文件可直接审计,且滚动策略防止磁盘爆满——这才是生产级思维。
5.4 用 Bootstrap 重构页面:30 分钟让界面告别“学生风”
原始页面是纯 HTML 表格+CSS,难看且不响应式。用 CDN 引入 Bootstrap 4:
<!-- 在所有 JSP 的 <head> 里加 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>然后把list.jsp的表格换成:
<div class="table-responsive"> <table class="table table-hover table-striped"> <thead class="thead-dark"> <tr><th>药品ID</th><th>名称</th><th>规格</th><th>单位</th><th>操作</th></tr> </thead> <tbody> <c:forEach items="${drugList}" var="d"> <tr> <td>${d.id}</td> <td>${d.name}</td> <td>${d.specification}</td> <td>${d.unit}</td> <td> <a href="edit.jsp?id=${d.id}" class="btn btn-sm btn-primary">编辑</a> <a href="delete?id=${d.id}" class="btn btn-sm btn-danger" onclick="return confirm('确定删除?')">删除</a> </td> </tr> </c:forEach> </tbody> </table> </div>技巧价值:Bootstrap 不是炫技,而是降低用户学习成本。药房管理员不用适应“奇怪的按钮位置”,看到熟悉的卡片、表格、模态框就敢点——这才是系统落地的关键。
5.5 用 Excel 导出药品清单:一个ServletOutputStream就够
DrugServlet.java加一个exportExcel()方法:
protected void exportExcel(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Drug> drugs = drugService.findAll(); // 设置响应头 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=drug_list_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".xls"); // 用 Apache POI(需加 poi-3.17.jar 到 lib) HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("药品清单"); // 表头 HSSFRow header = sheet.createRow(0); String[] titles = {"ID","名称","规格","单位","产地","批准文号","备注"}; for (int i = 0; i < titles.length; i++) { header.createCell(i).setCellValue(titles[i]); } // 数据行 for (int i = 0; i < drugs.size(); i++) { HSSFRow row = sheet.createRow(i + 1); Drug d = drugs.get(i); row.createCell(0).setCellValue(d.getId()); row.createCell(1).setCellValue(d.getName()); row.createCell(2).setCellValue(d.getSpecification()); row.createCell(3).setCellValue(d.getUnit()); row.createCell(4).setCellValue(d.getOrigin()); row.createCell(5).setCellValue(d.getApprovalNo()); row.createCell(6).setCellValue(d.getRemark() == null ? "" : d.getRemark()); } workbook.write(response.getOutputStream()); }在web.xml中映射/drug/export到此方法。
技巧价值:Excel 导出是医院刚需。POI 虽老但稳定,比前端 JS-XLSX 更可靠(尤其 IE 兼容);
Content-Disposition: attachment强制下载,避免浏览器直接打开乱码。
我带过 12 届实训,见过太多同学把“能跑通登录”当终点。但真正的交付,是让药剂师阿姨不用看说明书就能查到阿莫西林的库存,是财务科长双击导出的 Excel 就能粘贴进年报——这些细节不写在需求文档里,却决定项目是“交差”还是“真用”。希望这篇笔记里每一个grep命令、每一行web.xml配置、每一个oninput的 JS,都能帮你绕开那些没人告诉你、但会让你熬通宵的坑。希望帮到你。
本文还有配套的精品资源,点击获取