资讯详情

资讯详情

基于JavaWeb+Servlet+JSP+MySQL的药店管理系统实战:从环境搭建到部署避坑

简介这是一套基于JavaWeb技术栈的药店管理系统完整源码采用servlet与jsp实现业务逻辑搭配MySQL完成数据存储适合正在学习JavaWeb课程设计或需要实战项目练手的在校学生与初级开发者。系统覆盖客户信息、药品类别、药品信息、出入库记录及用户权限等核心模块可实现药品的新增、修改、删除、出库入库与记录查询帮助理解MVC分层与数据库交互流程。资源包共112个文件约2.19MB包含32个java源文件与32个class编译文件、7个jsp页面、8个css与6个js前端资源以及6个jar依赖、1个sql数据库脚本和Eclipse项目配置结构完整可直接导入运行。目前已有574人学习下载可作为课程设计、毕业设计或JavaWeb入门练手的参考方案便于快速搭建药店管理场景并对照源码梳理功能实现思路。1. 药店管理系统为什么还在用 ServletJSP一次把技术选型说透如果你最近在搜「javaweb项目完整案例mysql」大概率会刷到一堆药店管理系统、药品管理系统的源码包技术栈清一色写着 ServletJSPMySQL。很多人第一反应是都 2025 年了SpringBoot 满天飞为什么还要看这种「老古董」我一开始也这么想直到帮一个开连锁药店的朋友做内部库存工具才发现这套组合在特定场景下真不是落后而是刚好够用。药店管理系统的核心诉求其实很朴素药品入库、出库、库存预警、销售记录、用户权限数据量不大并发不高部署环境往往是店里一台 Windows 收银机或者一台便宜的云服务器。ServletJSP 没有 Spring 那一堆自动配置和依赖注入启动快、调试直观、部署就是一个 war 包丢进 Tomcat对新手和中小项目来说学习成本和维护成本都低得离谱。这篇文章就围绕「基于 javaWebservletjspmysql 的药店管理系统」这个标题把环境搭建、数据库设计、核心功能实现、常见翻车点全部拆开讲清楚让你拿到源码后能真正跑起来而不是对着报错发呆。适合谁看一是课程设计或毕设要做药店管理系统的学生二是想接私活做小型进销存工具的自由开发者三是想从 SpringBoot 回头补一补 Web 底层原理的工程师。下面从环境到代码一步步来。2. 环境搭建与项目骨架IDEA 跑通 JavaWeb 的第一道坎2.1 JDK、Tomcat、MySQL 的版本搭配与安装要点先说版本这是最容易翻车的地方。ServletJSP 项目对 JDK 版本敏感JDK 8 是兼容性最好的选择JDK 11 以上在 JSP 编译和 Tomcat 反射调用上偶尔会出玄学问题。Tomcat 用 8.5 或 9.0 都行别上 Tomcat 10因为 Tomcat 10 把 javax.servlet 包名改成了 jakarta.servlet老项目直接编译不过。MySQL 用 5.7 或 8.0 都可以8.0 的驱动类名和连接串参数有变化下面会单独说。安装 MySQL 时Windows 用户搜「mysql安装教程8.0」会看到一堆图形化安装向导我建议直接用 ZIP 解压版配置少、卸载干净。解压后在根目录建 my.ini写入 basedir 和 datadir然后执行初始化命令# 以管理员身份进入 MySQL 的 bin 目录 mysqld --initialize-insecure --console # 安装服务 mysqld --install mysql # 启动服务 net start mysql # 登录并修改 root 密码 mysql -u root -p--initialize-insecure表示初始化时不生成随机密码root 密码为空方便本地开发。生产环境千万别这么干。--console把日志打到控制台初始化失败时能直接看到是端口占用还是目录权限问题。启动服务如果报「服务正在启动 . mysql 服务无法启动」九成是 my.ini 路径写错或者 datadir 目录不存在去 MySQL 安装目录的 data 文件夹下看错误日志。Tomcat 解压后重点改 conf/server.xml 里的端口默认 8080如果被占用改成 8081。IDEA 里配置 Tomcat 的步骤是Run → Edit Configurations → 加号 → Tomcat Server → Local在 Deployment 标签页把项目的 war exploded 加进去Application context 设成/drug之类的短路径。2.2 用 IDEA 建项目目录结构与 web.xml 最小配置IDEA 新建项目选 Java Enterprise勾选 Web Application不要选 Maven新手用普通 Web 项目更容易理解目录结构。建好后目录长这样drug-system/ ├── src/ │ └── com/drug/ │ ├── dao/ │ ├── entity/ │ ├── servlet/ │ └── util/ ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ │ ├── css/ │ ├── js/ │ └── index.jspweb.xml 是 Servlet 的注册中心3.0 以后可以用注解替代但药店管理系统这种多模块项目我建议还是保留 web.xml 做统一配置方便排查。最小配置如下?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-appversion3.1对应 Tomcat 8.5/9.0写 4.0 在 Tomcat 9 上也能跑但没必要。welcome-file设成 login.jsp访问根路径直接进登录页。注意 web.xml 的 xsd 地址如果写错IDEA 会标红但不影响运行只是没有代码提示。2.3 MySQL 驱动引入与数据库连接工具类驱动包 mysql-connector-java 放到 web/WEB-INF/lib 下IDEA 里右键 Add as Library。8.0 版本的驱动类名是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver连接串也要加时区和 SSL 参数package com.drug.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DBUtil { // 8.0 驱动写法5.7 去掉 cj 和后面参数 private static final String URL jdbc:mysql://localhost:3306/drug_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (Exception e) {} try { if (stmt ! null) stmt.close(); } catch (Exception e) {} try { if (conn ! null) conn.close(); } catch (Exception e) {} } }serverTimezoneAsia/Shanghai不加会报「The server time zone value is unrecognized」这是 MySQL 8.0 最经典的坑。useSSLfalse关掉 SSL 警告本地开发不需要加密连接。静态代码块加载驱动只执行一次getConnection每次新建连接简单项目够用高并发场景要换成连接池后面章节会提。提示如果启动报No suitable driver found先确认 lib 目录下有没有 mysql-connector 的 jar再看 IDEA 的 Project Structure → Modules → Dependencies 里有没有这个库。3. 数据库设计与核心表药品、库存、销售三张表怎么定3.1 表结构设计与字段类型选择药店管理系统的数据模型不复杂核心就四张表用户表、药品表、库存表、销售记录表。设计时最容易犯的错是把库存直接塞进药品表导致每次出入库都要改药品主数据查询和并发都难受。正确做法是药品表存静态信息库存表存动态数量用 drug_id 关联。CREATE DATABASE drug_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE drug_db; -- 用户表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT DEFAULT 1 COMMENT 1管理员 2店员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 药品表 CREATE TABLE drug ( id INT PRIMARY KEY AUTO_INCREMENT, drug_name VARCHAR(100) NOT NULL, drug_code VARCHAR(50) UNIQUE, category VARCHAR(50), spec VARCHAR(50) COMMENT 规格, unit VARCHAR(20) COMMENT 单位, price DECIMAL(10,2) DEFAULT 0.00, manufacturer VARCHAR(100), status TINYINT DEFAULT 1 COMMENT 1在售 0下架 ); -- 库存表 CREATE TABLE stock ( id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, quantity INT DEFAULT 0, warn_line INT DEFAULT 10 COMMENT 预警线, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_drug (drug_id) ); -- 销售记录表 CREATE TABLE sale_record ( id INT PRIMARY KEY AUTO_INCREMENT, drug_id INT NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2), sale_time DATETIME DEFAULT CURRENT_TIMESTAMP, operator_id INT );utf8mb4而不是utf8因为药品名里可能有生僻字或特殊符号。DECIMAL(10,2)存金额别用 FLOAT浮点误差在财务数据上是致命的。stock表给 drug_id 加了唯一索引保证一个药品只有一条库存记录用INSERT ... ON DUPLICATE KEY UPDATE可以一条 SQL 完成入库。3.2 库存预警与出入库的 SQL 实现库存预警是药店系统的刚需查询逻辑就是quantity warn_line。但要注意预警线应该按药品设置不是全局统一因为处方药和 OTC 的周转速度完全不同。-- 查询需要补货的药品 SELECT d.drug_name, d.spec, s.quantity, s.warn_line FROM stock s JOIN drug d ON s.drug_id d.id WHERE s.quantity s.warn_line AND d.status 1 ORDER BY (s.warn_line - s.quantity) DESC; -- 入库存在则累加不存在则插入 INSERT INTO stock (drug_id, quantity, warn_line) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity); -- 出库先判断库存够不够再扣减 UPDATE stock SET quantity quantity - ? WHERE drug_id ? AND quantity ?;出库那条 SQL 的AND quantity ?是关键它把库存判断和扣减合并成原子操作避免先查后改导致的超卖。executeUpdate返回 0 就说明库存不足业务层直接提示。这个技巧在「mysql高并发解决方案」里经常被提到小项目用这一条就够了不用上锁。3.3 分页查询与模糊搜索的 SQL 写法药品列表页需要分页和按名称搜索MySQL 用 LIMIT 实现SELECT d.*, s.quantity FROM drug d LEFT JOIN stock s ON d.id s.drug_id WHERE d.drug_name LIKE CONCAT(%, ?, %) AND d.status 1 ORDER BY d.id DESC LIMIT ?, ?;LIMIT ?, ?第一个参数是偏移量(pageNum-1)*pageSize第二个是每页条数。深分页时LIMIT 100000, 10会扫描大量行优化方式是记住上一页最大 id 用WHERE id ? LIMIT 10药店系统数据量小暂时不用管。LEFT JOIN保证没有库存记录的药品也能显示出来quantity 为 null 时前端显示 0。注意LIKE 的%放两边会导致索引失效数据量上万后搜索变慢。如果药品名搜索频繁考虑加全文索引或者用前缀匹配LIKE CONCAT(?, %)。4. ServletJSP 实现药品增删改查从请求到页面的完整链路4.1 Servlet 接收参数与中文乱码处理Servlet 是控制器负责收参数、调 DAO、跳页面。药店系统里药品新增的 Servlet 大概长这样package com.drug.servlet; import com.drug.dao.DrugDao; import com.drug.entity.Drug; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(/drug/add) public class DrugAddServlet extends HttpServlet { private DrugDao drugDao new DrugDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // POST 中文乱码必须放在取参数之前 req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String drugName req.getParameter(drugName); String drugCode req.getParameter(drugCode); String spec req.getParameter(spec); String priceStr req.getParameter(price); Drug drug new Drug(); drug.setDrugName(drugName); drug.setDrugCode(drugCode); drug.setSpec(spec); drug.setPrice(priceStr null || priceStr.isEmpty() ? 0.0 : Double.parseDouble(priceStr)); int result drugDao.insert(drug); if (result 0) { resp.sendRedirect(req.getContextPath() /drug/list); } else { req.setAttribute(msg, 新增失败药品编码可能重复); req.getRequestDispatcher(/drug_add.jsp).forward(req, resp); } } }req.setCharacterEncoding(UTF-8)只对 POST 有效GET 请求的中文乱码要在 Tomcat 的 server.xml 里给 Connector 加URIEncodingUTF-8。resp.setContentType管响应编码。sendRedirect是重定向地址栏会变刷新不会重复提交forward是转发地址栏不变适合带错误信息回显。这两个别用混新增成功后用重定向是标准做法。4.2 JSP 页面用 JSTL 遍历药品列表JSP 里写 Java 代码脚本片段是十年前的做法现在用 JSTL 和 EL 表达式页面干净也好维护。药品列表页核心代码% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle药品列表/title/head body table border1 cellspacing0 width80% tr th药品名称/thth编码/thth规格/th th单价/thth库存/thth操作/th /tr c:forEach items${page.list} vard tr td${d.drugName}/td td${d.drugCode}/td td${d.spec}/td tdfmt:formatNumber value${d.price} pattern0.00//td td c:choose c:when test${d.quantity d.warnLine} span stylecolor:red${d.quantity} 需补货/span /c:when c:otherwise${d.quantity}/c:otherwise /c:choose /td td a href${pageContext.request.contextPath}/drug/edit?id${d.id}编辑/a a hrefjavascript:del(${d.id})删除/a /td /tr /c:forEach /table /body /html${page.list}是后端 request 域里放的 PageBean 对象c:forEach遍历。fmt:formatNumber把价格格式化成两位小数比在 Java 里拼字符串优雅。库存低于预警线时标红这是药店系统最实用的视觉反馈。${pageContext.request.contextPath}拿项目上下文路径避免部署路径变化后链接失效。JSTL 的 jar 包jstl.jar 和 standard.jar要放进 WEB-INF/lib否则 taglib 报错。Tomcat 9 自带 EL 实现不用额外加。4.3 DAO 层封装与事务处理DAO 层负责所有 SQL 操作一个方法对应一个业务动作。药品新增和库存初始化应该在一个事务里否则药品插入了库存没建列表页就显示 null。public int insertWithStock(Drug drug, int initQty, int warnLine) { Connection conn null; PreparedStatement ps1 null, ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 String sql1 INSERT INTO drug (drug_name, drug_code, spec, price) VALUES (?,?,?,?); ps1 conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, drug.getDrugName()); ps1.setString(2, drug.getDrugCode()); ps1.setString(3, drug.getSpec()); ps1.setDouble(4, drug.getPrice()); ps1.executeUpdate(); ResultSet rs ps1.getGeneratedKeys(); int drugId 0; if (rs.next()) drugId rs.getInt(1); String sql2 INSERT INTO stock (drug_id, quantity, warn_line) VALUES (?,?,?); ps2 conn.prepareStatement(sql2); ps2.setInt(1, drugId); ps2.setInt(2, initQty); ps2.setInt(3, warnLine); ps2.executeUpdate(); conn.commit(); return 1; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (Exception ex) {} e.printStackTrace(); return 0; } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (Exception e) {} DBUtil.close(conn, ps2, null); } }Statement.RETURN_GENERATED_KEYS拿自增主键这是插入主从表时的标准操作。setAutoCommit(false)开启事务任何一步异常都 rollback保证药品和库存同时成功或同时失败。finally 里恢复自动提交再关连接避免连接池回收后状态污染。这个模式在「javaweb连接mysql数据库」的教程里很少讲全但实际项目里必须这么写。提示如果用的是连接池Druid、HikariCPclose 方法会把连接还给池而不是真正关闭事务逻辑不变但别忘了在 finally 里恢复 autoCommit。5. 部署与联调避坑那些让项目跑不起来的常见问题5.1 启动报 404路径、注解扫描与 web.xml 冲突现象Tomcat 启动没报错访问http://localhost:8080/drug/login.jsp显示 404。原因通常有三个。一是 Application context 配错IDEA 里 Deployment 页的 Application context 如果是/那访问路径就是http://localhost:8080/login.jsp多了/drug反而找不到。二是WebServlet注解没生效检查 web.xml 的metadata-complete是不是设成了 true设成 true 会忽略所有注解。三是 JSP 放在 WEB-INF 下这个目录浏览器不能直接访问必须通过 Servlet 转发。解决先看 IDEA 控制台启动日志里Deployed application at context path后面跟的是什么以那个为准拼 URL。注解和 web.xml 同时配了同一个 Servlet 会冲突二选一。5.2 中文乱码POST 正常 GET 乱码的根因现象表单提交的药品名正常但地址栏传的中文参数在后台变成??????。原因req.setCharacterEncoding对 GET 无效GET 参数由 Tomcat 的 Connector 在解析 URL 时解码默认 ISO-8859-1。解决改 Tomcat 的 conf/server.xml在 Connector 标签加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8/改完重启 Tomcat。如果不想动服务器配置也可以在代码里手动转new String(name.getBytes(ISO-8859-1), UTF-8)但这是后悔药不推荐。5.3 数据库连接报错时区、SSL 与驱动版本现象启动后第一次查数据库就抛异常堆栈里有Communications link failure或Unknown system variable。原因一MySQL 8.0 连接串没加serverTimezone服务器时区识别不了。原因二驱动版本和数据库版本不匹配5.7 的驱动连 8.0 的库会报Unknown system variable query_cache_size。原因三useSSL没关MySQL 8.0 默认要求 SSL本地没配证书就连接失败。解决按 2.3 节的连接串模板改驱动用 mysql-connector-java 8.0.x 配 MySQL 8.05.1.x 配 MySQL 5.7。不确定版本就去 MySQL 命令行执行SELECT VERSION();。5.4 JSP 编译报错缺少 jar 与 Tomcat 版本不兼容现象访问 JSP 页面报 500错误信息是org.apache.jasper.JasperException或者cannot resolve taglib。原因JSTL 的 jar 没放全或者 Tomcat 10 用了 javax 的 taglib。Tomcat 10 的包名是 jakarta老项目的http://java.sun.com/jsp/jstl/core直接失效。解决确认 WEB-INF/lib 下有 jstl.jar 和 standard.jarJSTL 1.2 只需要 jstl-1.2.jar 一个。Tomcat 换成 9.0.x别用 10。如果非要用 Tomcat 10所有 javax.servlet 的 import 都要改成 jakarta.servlet工作量不小药店系统这种项目没必要。5.5 事务不生效autoCommit 与异常吞掉的坑现象药品插入成功了但库存没插进去数据库里出现脏数据。原因DAO 方法里 catch 了异常但没有 rollback或者setAutoCommit(false)写在了获取连接之后但被其他方法重置了。还有一种情况是用了两个不同的 Connection事务根本不在一个会话里。解决事务代码严格按 4.3 节的模板写catch 里必须 rollbackfinally 里恢复 autoCommit。一个业务方法只获取一次 Connection所有 SQL 共用。如果用了连接池确认池配置没有强制 autoCommittrue。6. 从能跑到好用三个让药店系统更稳的进阶技巧第一个技巧是给库存扣减加乐观锁版本号。前面用的UPDATE ... WHERE quantity ?在并发极高时仍可能出问题虽然药店场景并发低但加上 version 字段是零成本的保险ALTER TABLE stock ADD COLUMN version INT DEFAULT 0; UPDATE stock SET quantity quantity - ?, version version 1 WHERE drug_id ? AND quantity ? AND version ?;执行前先查出 version更新时带上返回 0 就重试一次。这个模式在「mysql锁的分类」里属于乐观锁比悲观锁轻量适合读多写少的库存场景。第二个技巧是用过滤器统一处理编码和登录校验。每个 Servlet 都写setCharacterEncoding太啰嗦建一个 EncodingFilterWebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }再建一个 LoginFilter 拦截/drug/*和/sale/*检查 session 里有没有用户没有就跳登录页。过滤器的顺序按 web.xml 里 filter-mapping 的先后或者注解的默认顺序编码过滤器必须放最前面。第三个技巧是给销售记录加统计查询用 MySQL 的日期函数做日报。药店老板最关心今天卖了多少钱、哪些药卖得好-- 今日销售额 SELECT IFNULL(SUM(total_price), 0) AS today_amount FROM sale_record WHERE DATE(sale_time) CURDATE(); -- 近7天销量Top10 SELECT d.drug_name, SUM(s.quantity) AS total_qty FROM sale_record s JOIN drug d ON s.drug_id d.id WHERE s.sale_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY d.id, d.drug_name ORDER BY total_qty DESC LIMIT 10;IFNULL防止没有销售记录时返回 null前端不用做空判断。DATE_SUB算日期区间比在 Java 里拼字符串可靠。这两个查询直接放 DAO 里JSP 用 c:forEach 渲染成表格就是一个简易的经营看板。我自己做这类项目最大的教训是别一上来就想着上框架、上缓存、上微服务。药店管理系统这种体量ServletJSPMySQL 跑得好好的先把业务逻辑写对、把事务和编码这些基础打牢比什么都强。我见过太多人花两周搭 SpringBoot 环境结果连一个库存扣减的并发问题都没解决。源码拿到手先按第 2 章把环境跑通再按第 4 章把增删改查调通最后用第 6 章的技巧优化这个顺序别乱。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →