JavaWeb医药管理系统:药品入库出库与库存预警实战
2026/9/15 20:28:17 网站建设 项目流程

简介:这是一份面向计算机及相关专业本科生的JavaWeb期末大作业实战资源,聚焦医药管理系统开发全流程,适用于课程设计、毕业设计选题参考与JavaWeb技术栈综合练习。资源包含完整可运行源码与配套数据库,经导师指导评审获98分高分,代码已本地编译调试通过,难度适中且内容经助教审定,特别适合缺乏项目经验的学习者快速掌握MVC分层开发、JSP页面交互、Servlet业务逻辑及MySQL数据操作等核心技能。压缩包共119个文件,含33个Java源文件(如MedicineController、BuyDaoImpl等)、33个编译后Class字节码、30个JSP前端页面、4个XML配置文件及1个SQL建库脚本,结构清晰、模块完整,总大小仅3.01MB,便于下载与本地部署。目前已有93人学习下载,资源附带项目工程配置(.classpath、.project等)与基础环境说明,开箱即用,显著降低环境搭建与调试门槛。

1. 用 JavaWeb 做医药管理系统,不是堆功能,而是把「药品入库→库存预警→处方出库→操作留痕」这条业务线跑通

很多同学交 JavaWeb 期末大作业时,直接套用网上“医药管理系统”模板:登录页、药品列表页、管理员后台,增删改查全有,但一问“药房领药后库存怎么实时扣减”,答不上来;一问“过期药品怎么自动标红并禁止出库”,只能改前端 CSS 颜色。这根本不是系统,是带数据库的静态页面集合。真正能拿高分的 JavaWeb 医药管理系统,核心不在界面多炫,而在业务闭环可验证、数据流向可追溯、关键约束不可绕过——比如:采购单提交后必须生成唯一入库流水号;医生开出处方,系统必须校验该药品当前库存 ≥ 处方数量,且批号有效期 ≥ 当前日期;药剂师确认发药后,库存表、出库记录表、操作日志表三张表必须在同一事务中完成写入。本文就带你从零搭起这样一个具备真实药房逻辑的 JavaWeb 系统,不依赖任何现成框架脚手架,用 Servlet + JSP + JDBC + MySQL 原生组合,把「药品主数据管理」「采购入库」「门诊发药」「库存预警」四个模块串成一条可走通的数据链,并给出每一步的数据库建表依据、Servlet 路由设计、JSP 表单约束和事务控制点。适合正在准备课程设计、软件综合实践或 JavaWeb 项目答辩的同学,尤其适合需要向老师现场演示“点击发药按钮后库存立刻减少且日志同步写入”的场景。

2. 搭建医药管理系统的最小可行数据库结构:5 张表支撑完整业务流

一个能跑通真实药房流程的数据库,绝不是“药品表+用户表+订单表”三张表就能应付。必须按业务实体拆分,同时预留审计与扩展字段。我们采用 MySQL 8.0+,字符集统一为 utf8mb4,所有时间字段用 DATETIME 类型(避免 TIMESTAMP 的时区隐式转换问题)。

2.1 核心五张表的设计逻辑与建表语句

提示:所有表均设置created_timeupdated_time字段,便于后续做操作溯源;药品表drug_id为主键且设为 BIGINT AUTO_INCREMENT,避免 UUID 在索引效率上的损耗;库存表stock_quantity设为 DECIMAL(10,2),支持中药饮片按克计量。

-- 1. 药品主数据表(含基础属性、有效期管理) CREATE TABLE drug ( drug_id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_code VARCHAR(20) NOT NULL UNIQUE COMMENT '国药准字编号,如国药准字Z20025678', drug_name VARCHAR(100) NOT NULL COMMENT '药品通用名', spec VARCHAR(50) COMMENT '规格,如0.25g×12片', unit VARCHAR(10) NOT NULL DEFAULT '盒' COMMENT '最小销售单位', manufacturer VARCHAR(100) COMMENT '生产厂家', shelf_life_months TINYINT NOT NULL DEFAULT 24 COMMENT '保质月数', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 2. 库存表(按批号维度管理,同一药品可有多条库存记录) CREATE TABLE stock ( stock_id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(50) NOT NULL COMMENT '生产批号,唯一标识一批次', production_date DATE NOT NULL COMMENT '生产日期', expiry_date DATE NOT NULL COMMENT '有效期至', stock_quantity DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '当前库存量', min_stock_level DECIMAL(10,2) DEFAULT 0.00 COMMENT '安全库存下限,用于预警', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ON DELETE CASCADE ); -- 3. 采购入库单(每次入库生成独立单据,关联多条药品批次) CREATE TABLE purchase_order ( po_id BIGINT PRIMARY KEY AUTO_INCREMENT, po_no VARCHAR(30) NOT NULL UNIQUE COMMENT '采购单号,格式:PO-2024-0001', supplier_name VARCHAR(100) NOT NULL, total_amount DECIMAL(12,2) NOT NULL, status ENUM('draft','submitted','received') DEFAULT 'draft' COMMENT '草稿/已提交/已收货', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, received_time DATETIME NULL COMMENT '实际收货时间' ); -- 4. 入库明细表(采购单与库存的多对多桥梁) CREATE TABLE purchase_detail ( detail_id BIGINT PRIMARY KEY AUTO_INCREMENT, po_id BIGINT NOT NULL, drug_id BIGINT NOT NULL, batch_no VARCHAR(50) NOT NULL, production_date DATE NOT NULL, expiry_date DATE NOT NULL, quantity DECIMAL(10,2) NOT NULL, unit_price DECIMAL(10,2) NOT NULL, FOREIGN KEY (po_id) REFERENCES purchase_order(po_id) ON DELETE CASCADE, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ON DELETE RESTRICT ); -- 5. 出库记录表(门诊发药、科室领用等所有出库行为统一记录) CREATE TABLE outbound_record ( record_id BIGINT PRIMARY KEY AUTO_INCREMENT, record_no VARCHAR(30) NOT NULL UNIQUE COMMENT '出库单号,格式:OUT-2024-0001', drug_id BIGINT NOT NULL, batch_no VARCHAR(50) NOT NULL, quantity DECIMAL(10,2) NOT NULL, operator VARCHAR(50) NOT NULL COMMENT '操作人姓名(非ID,便于审计)', purpose ENUM('outpatient','ward','loss') NOT NULL COMMENT '用途:门诊/病区/报损', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drug(drug_id) ON DELETE RESTRICT );
2.1.1 为什么必须用「批号维度」管理库存?

常见错误是只在drug表里加current_stock字段,看似简单,但无法处理:同一药品不同批号有效期不同、不同批号采购价不同、先进先出(FIFO)发药逻辑无法实现。本设计中,stock表每条记录代表“某药品某批号”的独立库存单元,outbound_record表记录每次出库具体用了哪个批号,这样既能做效期预警(查expiry_date < CURDATE()),又能保证发药时优先选择最早生产的批次。

2.1.2 采购单与入库分离的设计意图

purchase_order表记录采购行为(谁、何时、向谁买多少),purchase_detail表记录实际到货明细(可能部分到货、部分退货)。这种分离让系统能处理“采购单已提交但药品未全部入库”的中间状态,也方便财务对账——采购金额以purchase_order.total_amount为准,而实际入库成本以purchase_detail.unit_price × quantity汇总计算。

2.2 初始化测试数据:3 条药品 + 2 批库存 + 1 张采购单

执行以下 SQL,快速构建可运行环境:

-- 插入3种常用药品 INSERT INTO drug (drug_code, drug_name, spec, unit, manufacturer, shelf_life_months) VALUES ('国药准字Z20025678', '阿莫西林胶囊', '0.25g×12片', '盒', '华北制药股份有限公司', 36), ('国药准字H20056789', '氯化钠注射液', '0.9%×500ml', '瓶', '山东威高集团医用高分子制品股份有限公司', 24), ('国药准字Z20061234', '板蓝根颗粒', '10g×10袋', '盒', '广州白云山和记黄埔中药有限公司', 36); -- 为阿莫西林插入2个不同批号的库存(模拟分批到货) INSERT INTO stock (drug_id, batch_no, production_date, expiry_date, stock_quantity, min_stock_level) VALUES (1, 'AMX20240101', '2024-01-01', '2026-12-31', 120.00, 20.00), (1, 'AMX20240215', '2024-02-15', '2027-02-14', 80.00, 20.00); -- 创建一张已收货的采购单 INSERT INTO purchase_order (po_no, supplier_name, total_amount, status, received_time) VALUES ('PO-2024-0001', '北京医药有限公司', 2400.00, 'received', '2024-03-10 14:22:33'); -- 关联采购明细(阿莫西林两批各入库60盒) INSERT INTO purchase_detail (po_id, drug_id, batch_no, production_date, expiry_date, quantity, unit_price) VALUES (1, 1, 'AMX20240101', '2024-01-01', '2026-12-31', 60.00, 20.00), (1, 1, 'AMX20240215', '2024-02-15', '2027-02-14', 60.00, 20.00);

注意:执行完后,用SELECT * FROM stock;查看,会发现stock_quantity仍为初始值(120 和 80),因为入库操作尚未触发——这是故意为之,真正的库存更新必须由业务代码(Servlet)调用事务方法完成,不能靠初始化 SQL 硬编码。

3. 实现药品入库与库存更新:Servlet 事务控制 + JSP 表单约束

入库是医药系统最核心的写操作,必须保证:采购单状态变为“已收货”、库存表对应批号数量增加、入库明细表写入、操作日志记录,四者要么全部成功,要么全部回滚。不能出现“库存加了但采购单状态没变”这种数据不一致。

3.1 入库 Servlet 的事务封装与关键参数校验

创建com.example.servlet.StockInServlet.java,继承HttpServlet,重写doPost方法:

@WebServlet("/stock/in") public class StockInServlet extends HttpServlet { private final String DB_URL = "jdbc:mysql://localhost:3306/pharmacy?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private final String DB_USER = "root"; private final String DB_PASS = "your_password"; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取表单参数(注意:batch_no、production_date、expiry_date 必须与 drug_id 匹配) String drugIdStr = req.getParameter("drug_id"); String batchNo = req.getParameter("batch_no").trim(); String productionDateStr = req.getParameter("production_date"); String expiryDateStr = req.getParameter("expiry_date"); String quantityStr = req.getParameter("quantity"); String poNo = req.getParameter("po_no").trim(); // 2. 参数强校验(防止空值、非法格式) if (drugIdStr == null || drugIdStr.isEmpty() || batchNo.isEmpty() || productionDateStr == null || expiryDateStr == null || quantityStr == null) { req.setAttribute("error", "必填字段不能为空"); req.getRequestDispatcher("/jsp/stock_in.jsp").forward(req, resp); return; } try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) { conn.setAutoCommit(false); // 关键:开启事务 PreparedStatement ps; // 3. 检查该批号是否已存在(避免重复入库) ps = conn.prepareStatement("SELECT COUNT(*) FROM stock WHERE drug_id = ? AND batch_no = ?"); ps.setLong(1, Long.parseLong(drugIdStr)); ps.setString(2, batchNo); ResultSet rs = ps.executeQuery(); rs.next(); if (rs.getInt(1) > 0) { conn.rollback(); req.setAttribute("error", "该批号已存在,请勿重复入库"); req.getRequestDispatcher("/jsp/stock_in.jsp").forward(req, resp); return; } // 4. 插入库存记录(初始库存量 = 表单输入量) ps = conn.prepareStatement( "INSERT INTO stock (drug_id, batch_no, production_date, expiry_date, stock_quantity, min_stock_level) VALUES (?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS ); ps.setLong(1, Long.parseLong(drugIdStr)); ps.setString(2, batchNo); ps.setDate(3, Date.valueOf(productionDateStr)); ps.setDate(4, Date.valueOf(expiryDateStr)); ps.setBigDecimal(5, new BigDecimal(quantityStr)); ps.setBigDecimal(6, BigDecimal.ZERO); // 安全库存默认0,后续可编辑 ps.executeUpdate(); // 5. 更新采购单状态为已收货(需先查po_id) ps = conn.prepareStatement("SELECT po_id FROM purchase_order WHERE po_no = ?"); ps.setString(1, poNo); rs = ps.executeQuery(); long poId = -1; if (rs.next()) { poId = rs.getLong("po_id"); } else { conn.rollback(); req.setAttribute("error", "采购单号不存在:" + poNo); req.getRequestDispatcher("/jsp/stock_in.jsp").forward(req, resp); return; } ps = conn.prepareStatement("UPDATE purchase_order SET status = 'received', received_time = NOW() WHERE po_id = ?"); ps.setLong(1, poId); ps.executeUpdate(); // 6. 记录入库明细(关联采购单与药品批次) ps = conn.prepareStatement( "INSERT INTO purchase_detail (po_id, drug_id, batch_no, production_date, expiry_date, quantity, unit_price) VALUES (?, ?, ?, ?, ?, ?, ?)" ); ps.setLong(1, poId); ps.setLong(2, Long.parseLong(drugIdStr)); ps.setString(3, batchNo); ps.setDate(4, Date.valueOf(productionDateStr)); ps.setDate(5, Date.valueOf(expiryDateStr)); ps.setBigDecimal(6, new BigDecimal(quantityStr)); ps.setBigDecimal(7, BigDecimal.ZERO); // 单价暂置0,实际应从采购单获取 ps.executeUpdate(); conn.commit(); // 全部成功才提交 req.setAttribute("success", "入库成功!批号:" + batchNo); req.getRequestDispatcher("/jsp/stock_in.jsp").forward(req, resp); } catch (SQLException e) { try { if (conn != null && !conn.isClosed()) conn.rollback(); } catch (SQLException rollbackEx) { e.addSuppressed(rollbackEx); } e.printStackTrace(); req.setAttribute("error", "入库失败:" + e.getMessage()); req.getRequestDispatcher("/jsp/stock_in.jsp").forward(req, resp); } } }
3.1.1 关键参数说明与设计考量
  • conn.setAutoCommit(false):显式关闭自动提交,是事务控制的前提;
  • ps.setBigDecimal(5, new BigDecimal(quantityStr)):使用BigDecimal而非double,避免浮点精度丢失(药品数量可能为 0.5 盒);
  • Date.valueOf(productionDateStr):将字符串转为java.sql.Date,确保与数据库 DATE 类型匹配;
  • ps.executeUpdate()后不立即commit(),而是等到所有 SQL 执行完毕再统一提交,这是事务原子性的保障;
  • e.addSuppressed(rollbackEx):在回滚异常时,将回滚失败也作为抑制异常附加到主异常上,便于排查。

3.2 JSP 入库表单:前端约束与后端校验双重保险

创建/webapp/jsp/stock_in.jsp,包含药品选择下拉框、批号输入、日期控件及提交按钮:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>药品入库</title> <style> .form-group { margin: 10px 0; } label { display: inline-block; width: 120px; } input, select { padding: 5px; width: 200px; } .error { color: red; font-weight: bold; } .success { color: green; font-weight: bold; } </style> </head> <body> <h2>药品入库登记</h2> <c:if test="${not empty error}"> <div class="error">${error}</div> </c:if> <c:if test="${not empty success}"> <div class="success">${success}</div> </c:if> <form action="${pageContext.request.contextPath}/stock/in" method="post"> <div class="form-group"> <label for="drug_id">药品名称:</label> <select id="drug_id" name="drug_id" required> <option value="">请选择...</option> <c:forEach items="${drugs}" var="d"> <option value="${d.drug_id}">${d.drug_name}(${d.spec})</option> </c:forEach> </select> </div> <div class="form-group"> <label for="batch_no">生产批号:</label> <input type="text" id="batch_no" name="batch_no" required maxlength="50" placeholder="如:AMX20240101"> </div> <div class="form-group"> <label for="production_date">生产日期:</label> <input type="date" id="production_date" name="production_date" required> </div> <div class="form-group"> <label for="expiry_date">有效期至:</label> <input type="date" id="expiry_date" name="expiry_date" required> </div> <div class="form-group"> <label for="quantity">入库数量:</label> <input type="number" id="quantity" name="quantity" step="0.01" min="0.01" required placeholder="如:60.00"> </div> <div class="form-group"> <label for="po_no">采购单号:</label> <input type="text" id="po_no" name="po_no" required maxlength="30" placeholder="PO-2024-0001"> </div> <div class="form-group"> <button type="submit">执行入库</button> </div> </form> </body> </html>
3.2.1 前端约束如何与后端形成合力?
  • <input type="date">浏览器原生日期选择器,避免手动输入格式错误;
  • required属性强制必填,step="0.01"限定小数位数,min="0.01"防止负数或零;
  • maxlength限制批号和单号长度,与数据库字段定义一致;
  • JSP 中${drugs}由 Servlet 在doGet方法中查询drug表并 setAttribute 注入,确保下拉选项与数据库实时同步。

4. 实现门诊发药与库存扣减:基于批号的 FIFO 出库逻辑

发药不是简单地UPDATE stock SET stock_quantity = stock_quantity - ?,必须遵循药房“先进先出”原则:优先消耗最早生产的批次。否则会导致过期药品滞留在库、效期管理失效。

4.1 发药 Servlet 的批号选择与库存扣减算法

创建com.example.servlet.OutboundServlet.java

@WebServlet("/outbound/issue") public class OutboundServlet extends HttpServlet { private final String DB_URL = "jdbc:mysql://localhost:3306/pharmacy?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private final String DB_USER = "root"; private final String DB_PASS = "your_password"; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String drugIdStr = req.getParameter("drug_id"); String quantityStr = req.getParameter("quantity"); String operator = req.getParameter("operator").trim(); String purpose = req.getParameter("purpose"); // outpatient/ward/loss if (drugIdStr == null || quantityStr == null || operator.isEmpty() || purpose == null) { req.setAttribute("error", "参数不完整"); req.getRequestDispatcher("/jsp/outbound_issue.jsp").forward(req, resp); return; } BigDecimal needQuantity = new BigDecimal(quantityStr); long drugId = Long.parseLong(drugIdStr); try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS)) { conn.setAutoCommit(false); PreparedStatement ps; ResultSet rs; // 1. 查询该药品所有有效批号(按生产日期升序,实现FIFO) ps = conn.prepareStatement( "SELECT stock_id, batch_no, stock_quantity, expiry_date FROM stock " + "WHERE drug_id = ? AND stock_quantity > 0 AND expiry_date >= CURDATE() " + "ORDER BY production_date ASC" ); ps.setLong(1, drugId); rs = ps.executeQuery(); BigDecimal remaining = needQuantity; List<Long> usedStockIds = new ArrayList<>(); // 记录本次消耗的stock_id // 2. 遍历批号,逐个扣减(直到满足需求量或无可用批次) while (rs.next() && remaining.compareTo(BigDecimal.ZERO) > 0) { long stockId = rs.getLong("stock_id"); String batchNo = rs.getString("batch_no"); BigDecimal currentStock = rs.getBigDecimal("stock_quantity"); Date expiryDate = rs.getDate("expiry_date"); BigDecimal consume = currentStock.compareTo(remaining) >= 0 ? remaining : currentStock; remaining = remaining.subtract(consume); // 3. 更新库存(扣减) ps = conn.prepareStatement("UPDATE stock SET stock_quantity = stock_quantity - ? WHERE stock_id = ?"); ps.setBigDecimal(1, consume); ps.setLong(2, stockId); ps.executeUpdate(); // 4. 记录出库明细(每批一个记录) ps = conn.prepareStatement( "INSERT INTO outbound_record (record_no, drug_id, batch_no, quantity, operator, purpose) VALUES (?, ?, ?, ?, ?, ?)" ); String recordNo = "OUT-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")) + "-" + String.format("%04d", getNextRecordNo(conn)); ps.setString(1, recordNo); ps.setLong(2, drugId); ps.setString(3, batchNo); ps.setBigDecimal(4, consume); ps.setString(5, operator); ps.setString(6, purpose); ps.executeUpdate(); usedStockIds.add(stockId); } if (remaining.compareTo(BigDecimal.ZERO) > 0) { conn.rollback(); req.setAttribute("error", "库存不足!还需 " + remaining + " 单位"); req.getRequestDispatcher("/jsp/outbound_issue.jsp").forward(req, resp); return; } conn.commit(); req.setAttribute("success", "发药成功!共消耗 " + needQuantity + " 单位,涉及 " + usedStockIds.size() + " 个批号"); req.getRequestDispatcher("/jsp/outbound_issue.jsp").forward(req, resp); } catch (SQLException e) { try { if (conn != null && !conn.isClosed()) conn.rollback(); } catch (SQLException rollbackEx) { e.addSuppressed(rollbackEx); } e.printStackTrace(); req.setAttribute("error", "发药失败:" + e.getMessage()); req.getRequestDispatcher("/jsp/outbound_issue.jsp").forward(req, resp); } } // 辅助方法:获取下一个出库单号序号(线程安全) private int getNextRecordNo(Connection conn) throws SQLException { PreparedStatement ps = conn.prepareStatement("SELECT IFNULL(MAX(CAST(SUBSTRING(record_no, 12) AS UNSIGNED)), 0) + 1 FROM outbound_record WHERE record_no LIKE ?"); ps.setString(1, "OUT-" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")) + "-%"); ResultSet rs = ps.executeQuery(); rs.next(); return rs.getInt(1); } }
4.1.1 FIFO 算法的核心实现逻辑
  • ORDER BY production_date ASC:确保查询结果按生产日期升序排列,最早生产的批次排在最前面;
  • while (rs.next() && remaining.compareTo(BigDecimal.ZERO) > 0):循环遍历每个可用批次,直到需求量remaining降为 0;
  • consume = min(currentStock, remaining):每次最多消耗当前批次全部库存,或仅消耗剩余需求量;
  • usedStockIds列表:记录本次发药涉及的所有stock_id,可用于后续统计或审计;
  • getNextRecordNo():生成形如OUT-2024-03-0001的单号,按月递增,避免全局单号冲突。

4.2 发药 JSP 页面:动态加载药品与实时库存显示

/webapp/jsp/outbound_issue.jsp需展示药品列表及各批号当前库存:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>门诊发药</title> <style> table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } .low-stock { background-color: #ffebee; } .expired { background-color: #ffcdd2; } </style> </head> <body> <h2>门诊发药登记</h2> <c:if test="${not empty error}"> <div class="error">${error}</div> </c:if> <c:if test="${not empty success}"> <div class="success">${success}</div> </c:if> <!-- 药品选择与发药表单 --> <form action="${pageContext.request.contextPath}/outbound/issue" method="post"> <div class="form-group"> <label for="drug_id">选择药品:</label> <select id="drug_id" name="drug_id" onchange="loadStock(this.value)" required> <option value="">请选择...</option> <c:forEach items="${drugs}" var="d"> <option value="${d.drug_id}">${d.drug_name}(${d.spec})</option> </c:forEach> </select> </div> <div class="form-group"> <label for="quantity">发药数量:</label> <input type="number" id="quantity" name="quantity" step="0.01" min="0.01" required> </div> <div class="form-group"> <label for="operator">操作人:</label> <input type="text" id="operator" name="operator" required maxlength="50"> </div> <div class="form-group"> <label for="purpose">用途:</label> <select id="purpose" name="purpose" required> <option value="outpatient">门诊</option> <option value="ward">病区</option> <option value="loss">报损</option> </select> </div> <button type="submit">执行发药</button> </form> <!-- 动态加载的库存详情表格 --> <div id="stockTable" style="margin-top: 20px;"> <h3>当前库存详情(按生产日期排序)</h3> <table> <thead> <tr> <th>批号</th> <th>生产日期</th> <th>有效期至</th> <th>当前库存</th> <th>状态</th> </tr> </thead> <tbody> <c:forEach items="${stockList}" var="s"> <tr class="${s.expiryDate.before(new java.util.Date()) ? 'expired' : (s.stockQuantity.compareTo(java.math.BigDecimal.ZERO) <= 0 ? 'low-stock' : '')}"> <td>${s.batchNo}</td> <td>${s.productionDate}</td> <td>${s.expiryDate}</td> <td>${s.stockQuantity}</td> <td> <c:if test="${s.expiryDate.before(new java.util.Date())}">已过期</c:if> <c:if test="${s.stockQuantity.compareTo(java.math.BigDecimal.ZERO) <= 0}">库存不足</c:if> <c:if test="${!s.expiryDate.before(new java.util.Date()) && s.stockQuantity.compareTo(java.math.BigDecimal.ZERO) > 0}">正常</c:if> </td> </tr> </c:forEach> </tbody> </table> </div> <script> function loadStock(drugId) { if (!drugId) return; // 此处可添加AJAX动态加载库存,但为简化,本例由Servlet在GET时预查 } </script> </body> </html>

注意:stockListOutboundServlet.doGet()方法查询stock表并注入 request,确保页面打开时即显示最新库存状态。<c:if>标签内嵌 Java 表达式判断过期与低库存,直接驱动 CSS 类名,实现视觉预警。

5. 库存预警与报表导出:用 SQL 视图简化复杂查询

高分项目不仅要能操作,还要能“看出问题”。库存预警不是简单弹窗提示,而是提供可筛选、可导出的预警报表,让管理员一眼识别哪些药品即将过期、哪些低于安全库存。

5.1 创建库存预警视图:一条 SQL 覆盖两大预警场景

在 MySQL 中执行:

-- 创建库存预警视图:包含即将过期(30天内)和低于安全库存的药品 CREATE VIEW stock_alert_view AS SELECT d.drug_code, d.drug_name, d.spec, s.batch_no, s.production_date, s.expiry_date, s.stock_quantity, s.min_stock_level, CASE WHEN s.expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) THEN '即将过期' WHEN s.stock_quantity < s.min_stock_level AND s.min_stock_level > 0 THEN '库存不足' ELSE '正常' END AS alert_type, DATEDIFF(s.expiry_date, CURDATE()) AS days_to_expiry FROM stock s JOIN drug d ON s.drug_id = d.drug_id WHERE s.stock_quantity > 0 AND s.expiry_date >= CURDATE() AND ( s.expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) OR (s.min_stock_level > 0 AND s.stock_quantity < s.min_stock_level) );
5.1.1 视图设计的实用性考量
  • CASE WHEN分类alert_type,让 Java 代码只需查alert_type字段即可区分预警类型;
  • DATEDIFF(s.expiry_date, CURDATE())计算剩余天数,比单纯查expiry_date更直观;
  • WHERE子句过滤掉已过期、库存为 0 的记录,避免无效预警干扰;
  • 视图不包含敏感字段(如采购价),符合医药系统数据最小化原则。

5.2 导出预警报表为 Excel:用 Apache POI 生成可下载文件

添加 Maven 依赖(pom.xml):

<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.4</version> </dependency>

创建com.example.servlet.AlertExportServlet.java

@WebServlet("/alert/export") public class AlertExportServlet extends HttpServlet { private final String DB_URL = "jdbc:mysql://localhost:3306/pharmacy?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private final String DB_USER = "root"; private final String DB_PASS = "your_password"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS); PreparedStatement ps = conn.prepareStatement("SELECT * FROM stock_alert_view ORDER BY alert_type, days_to_expiry")) { ResultSet rs = ps.executeQuery(); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("库存预警报表"); // 表头 String[] headers = {"药品编码", "药品名称", "规格", "批号", "生产日期", "有效期至", "当前库存", "安全库存", "预警类型", "距过期天数"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(getHeaderStyle(workbook)); } // 数据行 int rowNum = 1; while (rs.next()) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(rs.getString("drug_code")); row.createCell(1).setCellValue(rs.getString("drug_name")); row.createCell(2).setCellValue(rs.getString("spec")); row.createCell(3).setCellValue(rs.getString("batch_no")); row.createCell(4).setCellValue(rs.getDate("production_date") != null ? rs.getDate("production_date").toString() : ""); row.createCell(5).setCellValue(rs.getDate("expiry_date") != null ? rs.getDate("expiry_date").toString() : ""); row.createCell(6).setCellValue(rs.getBigDecimal("stock_quantity").doubleValue()); row.createCell(7).setCellValue(rs.getBigDecimal("min_stock_level").doubleValue()); row.createCell(8).setCellValue(rs.getString("alert_type")); row.createCell(9).setCellValue(rs.getInt("days_to_expiry")); } // 自动列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 设置响应头,触发下载 resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=stock_alert_" + System.currentTimeMillis() + ".xlsx"); workbook.write(resp.getOutputStream()); workbook.close(); } catch (SQLException e) { <p> <a href="https://download.csdn.net/download/weixin_55305220/90896885" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

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

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

立即咨询