
简介本资源是一套面向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 报 404MySQL导入失败提示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 ArchiveWAR结构雏形。解压后你会看到hospital_drug_system/ ├── WebContent/ ← 等价于 webapp/JSP、CSS、JS、图片全在这 │ ├── index.jsp ← 登录入口页注意不是首页 │ ├── admin/ ← 管理员功能页药品管理、用户管理等 │ ├── user/ ← 普通药剂师功能页入库、出库、查询 │ └── WEB-INF/ │ ├── web.xml ← 核心配置Servlet 映射、过滤器、欢迎页 │ └── lib/ ← 所有依赖 JARmysql-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 默认 WebRootIDEA 默认识别为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 usernameadmin\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-INFProject 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 找不到 ServletJSP 也找不到 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?useUnicodetruecharacterEncodingutf8; // 【改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.Driver8.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-fileindex.jsp/welcome-file !-- 确保这里写的是 index.jsp不是 login.jsp -- /welcome-file-list参数说明useUnicodetruecharacterEncodingutf8是 JDBC 连接参数告诉驱动用 UTF-8 解析数据com.mysql.jdbc.Driver是 MySQL 5.x 的驱动类若你强行换成 8.x 驱动会报ClassNotFoundException127.0.0.1比localhost更可靠避免 DNS 解析延迟导致连接超时。3. 登录后进不去“药品管理”页三个典型权限与路径陷阱排查清单3.1 现象输入 admin/123456 登录成功跳转到admin/index.jsp但点击“药品管理”链接报 404原因admin/index.jsp里的a hrefdrug/list.jsp是相对路径而实际 JSP 文件在WebContent/admin/drug/list.jsp但服务器解析时认为drug/是 Web 根目录下的子目录即/drug/list.jsp而非/admin/drug/list.jsp。解决打开WebContent/admin/index.jsp把所有hrefxxx.jsp改成hrefadmin/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标签有methodpost打开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()中查询后的代码段ListDrug 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 prefixc urihttp://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 tdinput typetext namebatchNo required/td /tr tr td有效期至/td tdinput typedate nameexpiryDate 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格式而 HTMLinput typedate正好输出此格式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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % // 直接在 JSP 里写 DAO 查询仅用于实训生产环境严禁 java.util.Listcom.hospital.entity.Drug expiringDrugs new com.hospital.dao.DrugDao().findExpiringDrugs(30); // 查 30 天内到期的 request.setAttribute(expiringDrugs, expiringDrugs); % html headtitle近效期药品预警/title/head body h2距有效期不足30天的药品共 ${fn:length(expiringDrugs)} 种/h2 table border1 trth药品名/thth规格/thth批号/thth有效期至/thth剩余天数/th/tr c:forEach items${expiringDrugs} vard 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 ListDrug 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(sessionnull)原始代码里每个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-nameLoginFilter/filter-name filter-classcom.hospital.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mapping技巧价值Filter 是 JavaWeb 最被低估的组件。它让权限控制从“每个 Servlet 自查”变成“一次配置全局生效”且可轻松扩展为角色校验如admin/*拦截普通用户。5.2 用 JavaScript 实现入库数量实时校验防手抖输错add.jsp里药品数量输入框加一行 HTML5 属性即可input typenumber namequantity min1 max99999 required oninputif(this.value 1){this.value1;} if(this.value 99999){this.value99999;}但更进一步联动药品总库存做校验比如不能入库超过仓库最大容量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 typenumber namequantity oninputcheckInventory() ...后端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.rootLoggerINFO, FILE log4j.appender.FILEorg.apache.log4j.RollingFileAppender log4j.appender.FILE.Filelogs/operate.log log4j.appender.FILE.MaxFileSize10MB log4j.appender.FILE.MaxBackupIndex5 log4j.appender.FILE.layoutorg.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 relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap4.6.2/dist/css/bootstrap.min.css script srchttps://cdn.jsdelivr.net/npm/jquery3.5.1/dist/jquery.slim.min.js/script script srchttps://cdn.jsdelivr.net/npm/bootstrap4.6.2/dist/js/bootstrap.bundle.min.js/script然后把list.jsp的表格换成div classtable-responsive table classtable table-hover table-striped thead classthead-dark trth药品ID/thth名称/thth规格/thth单位/thth操作/th/tr /thead tbody c:forEach items${drugList} vard tr td${d.id}/td td${d.name}/td td${d.specification}/td td${d.unit}/td td a hrefedit.jsp?id${d.id} classbtn btn-sm btn-primary编辑/a a hrefdelete?id${d.id} classbtn btn-sm btn-danger onclickreturn 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 { ListDrug drugs drugService.findAll(); // 设置响应头 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment;filenamedrug_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都能帮你绕开那些没人告诉你、但会让你熬通宵的坑。希望帮到你。本文还有配套的精品资源点击获取