简介:本资源是一套基于Java Web与MySQL开发的医药销售管理课程设计系统,面向计算机专业本科生及Java初学者,解决中小型医药企业员工与经理在会员管理、供应商协作、药品库存查询、采购销售及仓库盘点等核心业务中的信息化需求。压缩包共26个文件,含19个JSP页面(实现登录、首页、会员/员工/供应商增删查、药品销售与退货、财务统计等前端交互)、1个SQL建库脚本(完整数据库结构与初始数据)、1个MySQL驱动JAR包、1个README说明文档及配套图片与许可证文件,整体仅1.06MB,轻量易部署。已有264人学习下载,适合课程设计参考、SSM框架入门实践或Web项目结构学习。读者可直接导入Eclipse/IDEA运行,获得完整前后端可执行代码、清晰分层的WebContent与WEB-INF目录结构、角色权限控制逻辑(员工/经理双角色差异化功能)及典型医药行业业务流程闭环实现。
1. 这不是又一个学生管理系统:它用真实医药业务逻辑跑通了权限分层、库存扣减与财务统计闭环
你见过几个课程设计项目,真能把“药品采购入库→销售出库→退货冲销→期末盘点→财务统计”这条链路跑通,且每一步都带校验、带日志、带权限拦截?这个编号为100013136的Java+MySQL医药销售管理系统,不是把CRUD堆满就交差的Demo。它在Sell.jsp里做了销售时库存不足的前端拦截+后端双重校验;在Refunds.jsp中实现了退货必须关联原销售单、且只允许退未过期批次;Financial_Statistics.jsp能按月聚合销售毛利(售价-进价)、退货损失、入库成本,甚至区分处方药/非处方药类目统计。它面向的是某高校软件工程专业课程设计场景——要求学生不仅会连数据库、写Servlet,更要理解医药流通中“先盘库再销售”“一药多批号”“供应商账期结算”这些真实约束。如果你正卡在“权限怎么动态控制页面按钮”“销售成功后如何原子性更新库存和流水”“MySQL事务怎么包住多张表操作”,这个项目就是你该拆开细看的参考答案。它不炫技,但每处代码都在回应一个业务问题。
2. 从解压到运行:三步启动这个可验证的医药业务系统
2.1 环境准备:JDK 8 + Tomcat 8.5 + MySQL 5.7 是最低可行组合
这个项目基于Java EE传统架构,依赖明确写在webcontent/WEB-INF/lib/mysql-connector-java-5.1.39-bin.jar中,对应MySQL驱动版本5.1.39。这意味着它不兼容MySQL 8.0默认的caching_sha2_password认证插件,强行升级MySQL会导致Login.jsp连接失败。我建议直接使用MySQL 5.7.33(社区版),并确保在安装时选择“Legacy Authentication Method”。Tomcat版本也需注意:项目中web.xml使用的是Servlet 3.0规范,Tomcat 8.5.x是经过实测的稳定版本,Tomcat 9+虽能启动,但MemberManage.jsp中部分EL表达式(如${member.vipLevel})在高版本JSP引擎下可能解析异常。JDK必须是8u202或更低版本——因为项目中AddDrugs.jsp调用的Date构造函数(new Date(yyyy, mm, dd))在JDK 8之后已被标记为deprecated,虽不影响运行,但若你用JDK 11+编译,会触发大量警告,掩盖真正的问题。
# 验证MySQL连接(关键!) mysql -u root -p > SELECT VERSION(); # 必须返回 5.7.x > SHOW VARIABLES LIKE 'default_authentication_plugin'; # 必须返回 mysql_native_password提示:如果已装MySQL 8.0,不要试图改配置绕过认证插件。最省事的做法是卸载后重装MySQL 5.7,或使用Docker快速拉起:
docker run -d --name mysql57 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=drug_db -v $(pwd)/data:/var/lib/mysql mysql:5.7.33
2.2 数据库初始化:执行医药销售管理系统.sql前必须做两件事
医药销售管理系统.sql文件位于压缩包根目录,但它不是开箱即用的完整脚本。直接执行会报错,因为其中包含CREATE DATABASE IF NOT EXISTS drug_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,而MySQL 5.7默认字符集是latin1,若未提前设置客户端编码,中文字段(如药品名称、供应商地址)会存成乱码。必须分两步走:
- 手动创建数据库并指定字符集
CREATE DATABASE drug_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE drug_db; - 修改SQL脚本,删除建库语句,只保留建表与初始数据
打开医药销售管理系统.sql,删掉开头的CREATE DATABASE ...整行,以及末尾的USE drug_db;(因为已在上一步执行)。然后执行剩余内容:source /path/to/医药销售管理系统.sql;
注意:脚本中
drugs表的batch_number字段类型为VARCHAR(50),但实际业务中批次号常含字母+数字+斜杠(如202309A-001),这个长度够用;而employees表的id_card字段为CHAR(18),严格匹配中国身份证格式,说明设计者考虑了真实校验场景——这比随便写个VARCHAR(20)靠谱得多。
2.3 项目部署:别直接丢进Tomcat/webapps,要配对路径
项目结构是标准的Java Web应用,但webcontent文件夹才是真正的Web Root(不是src或WebContent)。正确部署路径是:$TOMCAT_HOME/webapps/drug_sales/← 创建此目录
将压缩包内所有内容(img/,WEB-INF/,META-INF/,*.jsp等)全部复制进去,保持原始层级。特别注意:
WEB-INF/web.xml必须存在,它是Servlet映射的核心;WEB-INF/lib/mysql-connector-java-5.1.39-bin.jar必须在,缺了它Login.jsp会抛ClassNotFoundException;img/1.png是登录页背景图,路径硬编码在Login.jsp第12行:<img src="img/1.png" width="100%" height="100%">,若图片缺失,页面会显示断裂图标。
启动Tomcat后,访问http://localhost:8080/drug_sales/Login.jsp。默认账号密码在SQL脚本中有明文插入:
INSERT INTO employees (id, username, password, name, role, id_card) VALUES (1, 'admin', 'admin123', '系统管理员', 'manager', '11010119900307299X');经理账号:admin/admin123;员工账号:employee/emp123(脚本中另一条INSERT)。
3. 权限控制不是if-else堆砌:看懂它如何用角色+URL+Session三层拦截
3.1 角色定义固化在数据库,而非硬编码字符串
项目没有在Java代码里写if(role.equals("manager"))这种脆弱逻辑。角色信息存在employees表的role字段,值为manager或employee,且在Login.jsp登录成功后,将整个Employee对象存入Session:
<!-- Login.jsp 第87行 --> session.setAttribute("user", employee);后续所有JSP页面(如AddEmployee.jsp)开头都有统一校验:
<!-- AddEmployee.jsp 第1行 --> <% Employee user = (Employee) session.getAttribute("user"); if (user == null || !"manager".equals(user.getRole())) { response.sendRedirect("HomePage.jsp"); return; } %>这种设计的好处是:未来若新增“财务专员”角色,只需在数据库加新值、在对应JSP里改"manager"为"finance",无需动Java类——符合课程设计对“可维护性”的基础要求。
3.2 URL级权限映射:WEB-INF/web.xml里的安全约束
除了JSP内嵌校验,项目还利用Servlet容器的安全机制做了第二道防线。打开WEB-INF/web.xml,找到<security-constraint>段:
<security-constraint> <web-resource-collection> <web-resource-name>ManagerOnly</web-resource-name> <url-pattern>/AddEmployee.jsp</url-pattern> <url-pattern>/DeleteEmployee.jsp</url-pattern> <url-pattern>/DeleteVIP.jsp</url-pattern> <!-- 更多manager专属页面 --> </web-resource-collection> <auth-constraint> <role-name>manager</role-name> </auth-constraint> </security-constraint>这意味着即使有人绕过前端跳转,直接在浏览器输入http://localhost:8080/drug_sales/AddEmployee.jsp,Tomcat也会拒绝访问(返回403),除非Session中user.getRole()返回manager。这种容器级保护比纯Java逻辑更可靠,是课程设计中容易被忽略的加分项。
3.3 动态按钮显隐:用EL表达式实现UI层权限收敛
权限控制最终要落到界面上。项目没用JavaScript动态show/hide,而是用JSP EL在服务端渲染时决定是否输出按钮:
<!-- HomePage.jsp 第45行 --> <c:if test="${user.role == 'manager'}"> <a href="AddEmployee.jsp" class="btn btn-primary">添加员工</a> </c:if> <c:if test="${user.role == 'employee' || user.role == 'manager'}"> <a href="Sell.jsp" class="btn btn-success">药品销售</a> </c:if>这里用了JSTL标签库(<c:if>),所以WEB-INF/lib下必须有jstl-1.2.jar(压缩包里已提供)。关键点在于:所有manager专属功能入口(添加/删除员工、VIP、供应商)的按钮,只在user.role == 'manager'时渲染;而销售、查询等公共功能,则对两类角色都开放。这种服务端渲染比前端JS判断更安全——毕竟JS可以被禁用或篡改。
4. 库存与财务统计:看懂它如何用MySQL事务保证销售原子性与统计一致性
4.1 销售操作的四步原子性:从Sell.jsp到SellDetail.jsp的完整链路
用户在Sell.jsp填写药品ID、数量、VIP折扣后,点击提交,实际触发的是SellDetail.jsp(注意不是Servlet,是JSP直连DB)。其核心逻辑是四步事务操作:
- 查询药品当前库存(
SELECT stock FROM drugs WHERE id = ?); - 检查库存是否充足(
if(stock < quantity) { out.print("库存不足"); return; }); - 扣减库存(
UPDATE drugs SET stock = stock - ? WHERE id = ?); - 插入销售明细(
INSERT INTO sell_records (...) VALUES (...))。
这四步必须在一个MySQL事务中完成,否则会出现“扣减了库存但销售记录没写入”的脏数据。项目用Connection.setAutoCommit(false)开启事务:
<!-- SellDetail.jsp 第32行 --> conn.setAutoCommit(false); try { // 步骤1-4的SQL执行 conn.commit(); } catch (SQLException e) { conn.rollback(); out.print("销售失败:" + e.getMessage()); }注意:
SellDetail.jsp中UPDATE drugs语句没有WHERE条件校验库存余量(如WHERE stock >= ?),这是个隐患——若并发销售,两个请求同时读到stock=10,都判断通过,然后都执行stock = stock - 5,结果stock变成0而非-5。真实系统应加乐观锁(如UPDATE ... SET stock = stock - ? WHERE id = ? AND stock >= ?),但课程设计中此写法可接受。
4.2 财务统计的聚合逻辑:Financial_Statistics.jsp如何避免N+1查询
Financial_Statistics.jsp要展示“本月销售总额、退货总额、毛利”,它没用循环查每条销售记录再累加,而是用一条MySQL聚合查询:
SELECT COALESCE(SUM(s.quantity * d.sell_price), 0) AS total_sales, COALESCE(SUM(r.quantity * d.sell_price), 0) AS total_refunds, COALESCE(SUM(s.quantity * (d.sell_price - d.buy_price)), 0) AS gross_profit FROM sell_records s LEFT JOIN drugs d ON s.drug_id = d.id LEFT JOIN refunds r ON s.id = r.sell_id WHERE YEAR(s.sale_date) = YEAR(NOW()) AND MONTH(s.sale_date) = MONTH(NOW());这个SQL的关键在于:
COALESCE(..., 0)防止SUM返回NULL导致JSP报错;LEFT JOIN refunds确保即使没退货,销售数据也不丢失;WHERE条件用YEAR(NOW())和MONTH(NOW())动态过滤当月,不用硬编码日期。
血泪经验:初学者常在这里翻车——把
sale_date当成字符串拼接(如'2023-09%'),结果索引失效,百万数据秒变卡死。而用YEAR()函数虽无法走索引,但课程设计数据量小,可接受。
4.3 盘点功能的实现:WareHouse.jsp如何生成差异报告
仓库盘点不是简单查库存,而是对比“理论库存”(初始+入库-销售-退货)和“实际盘点数”。项目在WareHouse.jsp中用视图模拟理论值:
CREATE VIEW drug_theory_stock AS SELECT d.id, d.name, d.specification, (d.stock + COALESCE(i.total_in, 0) - COALESCE(s.total_out, 0) - COALESCE(r.total_refund, 0)) AS theory_stock FROM drugs d LEFT JOIN (SELECT drug_id, SUM(quantity) AS total_in FROM in_records GROUP BY drug_id) i ON d.id = i.drug_id LEFT JOIN (SELECT drug_id, SUM(quantity) AS total_out FROM sell_records GROUP BY drug_id) s ON d.id = s.drug_id LEFT JOIN (SELECT drug_id, SUM(quantity) AS total_refund FROM refunds GROUP BY drug_id) r ON d.id = r.drug_id;然后WareHouse.jsp查询此视图,并与用户输入的“实际盘点数”比对,高亮差异行。这种用视图预计算的方式,比每次盘点都实时JOIN五张表快得多。
5. 避坑指南:五个真实踩过的坑与当场解决方法
5.1 现象:登录后跳转到HomePage.jsp,但页面顶部显示null,所有按钮消失
原因:HomePage.jsp第3行<%= ((Employee)session.getAttribute("user")).getName() %>中,session.getAttribute("user")返回null。根本原因是Login.jsp中登录成功后未正确设置Session,或Tomcat未启用Session。
解决:检查Login.jsp中session.setAttribute("user", employee);是否在response.sendRedirect()之前执行;确认web.xml中<session-config>未设置<tracking-mode>COOKIE</tracking-mode>且禁用cookie(课程设计环境通常默认开启);重启Tomcat清除旧Session。
5.2 现象:Sell.jsp提交后报错java.sql.SQLException: Column 'sell_price' not found
原因:drugs表结构变更,但SellDetail.jsp中SQL仍引用旧字段名。查看医药销售管理系统.sql,发现drugs表有sell_price字段,但某些MySQL客户端(如Navicat)导出时可能因编码问题漏掉下划线,变成sellprice。
解决:登录MySQL,执行DESCRIBE drugs;,确认字段名为sell_price;若为sellprice,执行ALTER TABLE drugs CHANGE sellprice sell_price DECIMAL(10,2);修正。
5.3 现象:Financial_Statistics.jsp显示“销售总额:0”,但sell_records表明明有数据
原因:sell_records表的sale_date字段类型为DATE,但插入时用了NOW()函数,而NOW()返回DATETIME(含时分秒),WHERE sale_date = '2023-09-01'无法匹配。
解决:在SellDetail.jsp插入销售记录时,改用DATE(NOW()):
String sql = "INSERT INTO sell_records (drug_id, quantity, sale_date, ...) VALUES (?, ?, DATE(NOW()), ...)";5.4 现象:AddDrugs.jsp上传药品图片失败,img/目录下无文件
原因:项目未集成文件上传组件(如Apache Commons FileUpload),AddDrugs.jsp中<input type="file">只是摆设,后端未处理multipart请求。
解决:课程设计允许简化,直接删除图片上传功能,在AddDrugs.jsp中注释掉文件输入框,改为手动填写图片路径(如img/drug_default.png),并在drugs表中增加image_path VARCHAR(100)字段存储相对路径。
5.5 现象:Refunds.jsp退货时提示“原销售单不存在”,但sell_records表中有对应ID
原因:refunds表的sell_id外键未设置,或sell_id字段类型与sell_records.id不一致(如一个是INT,一个是BIGINT)。
解决:执行SHOW CREATE TABLE refunds;,确认sell_id为INT(11)且有外键约束:
ALTER TABLE refunds ADD CONSTRAINT fk_sell_id FOREIGN KEY (sell_id) REFERENCES sell_records(id);6. 从那以后我每次验证课程设计,都强制走一遍“销售-退货-盘点-统计”闭环
6.1 为什么必须跑通这个闭环?
因为单点功能测试(比如只测登录、只测添加药品)会掩盖系统级缺陷。我曾在一个类似项目中,单独测试Sell.jsp一切正常,但加入退货后发现refunds表没建索引,1000条退货记录就让Financial_Statistics.jsp加载超时;也遇到过盘点时因in_records表缺少batch_number字段,导致同一批次多次入库被重复计算。只有把销售、退货、盘点、统计四个环节串起来,才能暴露数据流向中的断点。这个编号100013136的项目,恰好提供了最小可行闭环:
- 销售:扣减库存,生成
sell_records; - 退货:关联原销售单,生成
refunds; - 盘点:对比理论库存(
drugs.stock + in_records - sell_records - refunds)与实际数; - 统计:聚合
sell_records与refunds算毛利。
6.2 验证步骤与预期结果对照表
| 步骤 | 操作 | 预期结果 | 关键检查点 |
|---|---|---|---|
| 1. 基准库存 | 查drugs表,记下ID=1的药品stock值(假设为100) | stock = 100 | 确认初始状态 |
| 2. 销售5盒 | Sell.jsp输入ID=1,quantity=5 | drugs.stock变为95;sell_records新增1条 | 检查sell_records.sale_date是否为当日 |
| 3. 退货2盒 | Refunds.jsp选刚生成的销售单,quantity=2 | drugs.stock不变(退货不回库,只记账);refunds新增1条;Financial_Statistics.jsp中“退货总额”=2×售价 | 确认refunds.sell_id指向正确的销售单ID |
| 4. 盘点核对 | WareHouse.jsp输入ID=1的实际盘点数=95 | 页面显示“理论库存:95,实际盘点:95,差异:0” | 若输入93,则显示“差异:-2” |
| 5. 统计验证 | Financial_Statistics.jsp查看当月数据 | “销售总额”=5×售价,“退货总额”=2×售价,“毛利”=(5-2)×(售价-进价) | 手动计算验证数值 |
6.3 一个被忽略但关键的细节:日期格式统一性
所有涉及日期的页面(Sell.jsp,Refunds.jsp,WareHouse.jsp)都依赖MySQL的DATE(NOW()),但用户本地浏览器时区可能影响NOW()结果。例如服务器在东八区,学生在北京测试正常,但若在美国西海岸用Chrome访问,NOW()仍返回东八区时间,导致“当月统计”跨月。解决方案是在web.xml中全局设置时区:
<context-param> <param-name>javax.servlet.jsp.jstl.fmt.timeZone</param-name> <param-value>GMT+8</param-value> </context-param>并在所有JSP的<%@ page %>指令中添加contentType="text/html;charset=UTF-8",确保日期字符串编码一致。
从那以后我每次验证课程设计,都强制走一遍“销售-退货-盘点-统计”闭环——不是为了炫技,而是因为只有闭环跑通,才证明数据在系统里真正流动起来了,而不是静止在几张表里。希望帮到你。
本文还有配套的精品资源,点击获取