资讯动态

Java Web医药销售系统:角色权限+事务控制+部署避坑指南

发布时间:2026/9/28 7:27:39 来源:尧图企业网站定制
简介这是一套面向计算机专业本科生的Java Web课程设计实战项目聚焦医药销售业务场景帮助学习者掌握JSPServletMySQL三层架构开发全流程。资源完整实现会员与供应商管理、药品查询与库存跟踪、采购/销售/退货/盘点等核心业务角色权限划分清晰员工与经理功能差异化明确具备真实企业级系统雏形。压缩包共26个文件含19个JSP页面覆盖登录、首页、会员/供应商/员工管理、销售与财务统计等模块、1个SQL建库脚本、1个MySQL驱动JAR包、1个README说明文档及配套图片与License文件整体仅1.06MB轻量易部署。已有263人学习下载提供开箱即用的可运行工程结构、规范的数据库设计含医药行业典型字段、权限控制逻辑实现细节及界面截图参考适合课程设计参考、Java Web综合实训与毕业设计选题拓展。1. 这不是又一个学生练手项目它真能跑通医药销售全流程从登录鉴权到财务统计全链路闭环你可能已经点开过 dozens 个标着“JavaMySQL课程设计”的压缩包双击解压后看到Login.jsp就关掉——因为十有八九连数据库都连不上更别说区分员工和经理权限、处理退货单据、生成销售统计图表。但这个编号为100013136的医药销售管理系统不一样它不是概念图而是实打实跑在 Tomcat 上、用mysql-connector-java-5.1.39-bin.jar稳定对接 MySQL 5.7 的可执行系统它把「会员管理」「供应商入库」「药品销售」「仓库盘点」「财务统计」五个核心业务模块全部串成闭环连Financial_Statistics.jsp都不是静态页面而是通过 JDBC 查询聚合后的动态报表。我把它部署在本地开发环境JDK 8u291 Tomcat 8.5.94 MySQL 5.7.42完整走了一遍流程从管理员添加员工账号到员工完成一笔含 3 种药品的销售再到经理导出当月销售汇总全程无报错、数据可追溯、权限不越界。如果你正被 Java Web 课程设计卡在「登录后跳转白屏」「SQL 插入失败却没提示」「角色权限只写在 if 判断里却没控制页面按钮显隐」这些真实翻车现场里这份资源就是你缺的那块拼图——它不教你泛泛的 Servlet 生命周期而是告诉你一个带角色隔离、事务回滚、库存扣减、单据关联的真实小系统代码长什么样、配置怎么配、哪里最容易崩。2. 从零启动部署前必须确认的四个硬性依赖与三步初始化流程这个系统不是开箱即用的 Docker 镜像它依赖于 Java Web 开发栈中几个关键且版本敏感的组件。很多同学解压后直接丢进 Tomcat webapps 目录就报 404 或 500根本原因往往卡在第一步——依赖没对齐。下面这四样东西少一个或版本错一个整个系统就起不来。别跳过逐条核对。2.1 JDK 8 是底线为什么不能用 JDK 11项目中所有 JSP 页面如Login.jsp、Sell.jsp使用的是 JSP 2.2 规范其底层依赖javax.servlet.*和javax.el.*包而 JDK 11 已将这些 API 移出默认 classpath。若强行用 JDK 11 运行你会在 Tomcat 日志里看到java.lang.NoClassDefFoundError: javax/el/ExpressionFactory。解决方案必须使用JDK 8u202 至 u333 之间的版本推荐 u291。验证方式java -version # 输出应为类似java version 1.8.0_291提示不要用java --version这是 JDK 11 的新命令它会误导你。老版本 JDK 必须用java -version。2.2 Tomcat 8.5.x 是黄金组合Tomcat 9/10 会直接拒载web.xml中的web-app根元素声明了version3.0对应 Servlet 3.0 规范。Tomcat 8.5 完美支持而 Tomcat 9 虽然也支持 3.0但其内部类加载器对WEB-INF/lib下 jar 的扫描逻辑有细微差异会导致mysql-connector-java-5.1.39-bin.jar加载失败Tomcat 10 更彻底——它将javax.*包名升级为jakarta.*所有import javax.servlet.*全部报错。操作步骤下载 Apache Tomcat 8.5.94 Windows 选zipLinux 选tar.gz解压后进入conf/server.xml确认Connector port8080行未被注释启动bin/startup.batWin或bin/startup.shLinux2.3 MySQL 5.7 是唯一兼容版本5.6 会因 GROUP BY 报错Financial_Statistics.jsp中有一条关键 SQLSELECT DATE(sell_time) as day, SUM(total_price) as revenue FROM sell_record GROUP BY DATE(sell_time) ORDER BY day;MySQL 5.6 默认开启sql_modeSTRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION但缺少ONLY_FULL_GROUP_BY导致该语句虽能执行却返回错误聚合结果比如同一天多笔销售只取第一条而 MySQL 5.7 默认启用ONLY_FULL_GROUP_BY强制要求 SELECT 列必须在 GROUP BY 中出现或为聚合函数反而让这条语句报错。解决方法修改 MySQL 配置文件my.cnfLinux或my.iniWindows在[mysqld]段落下添加sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION然后重启 MySQL 服务。这是唯一能让Financial_Statistics.jsp正常渲染图表的配置。2.4 mysql-connector-java-5.1.39 是绑定版本别换 8.x项目WEB-INF/lib/下明确放着mysql-connector-java-5.1.39-bin.jar这是经过实测的稳定版本。若你手动替换成mysql-connector-java-8.0.33.jar会在登录时触发java.lang.ClassNotFoundException: com.mysql.jdbc.Driver—— 因为 8.x 版本已将驱动类名改为com.mysql.cj.jdbc.Driver而项目所有Class.forName(com.mysql.jdbc.Driver)调用都未更新。验证方式打开src/dao/BaseDao.java若源码包含 src 目录或反编译WEB-INF/classes/dao/BaseDao.class搜索com.mysql.jdbc.Driver字符串。只要存在就必须用 5.1.x 系列。2.5 三步初始化数据库从 SQL 文件到连接测试光有 MySQL 服务还不够必须让系统知道怎么连、连谁。以下是不可跳过的初始化流程第一步创建数据库并导入结构# 登录 MySQL mysql -u root -p # 创建数据库注意字符集必须是 utf8mb4否则中文乱码 CREATE DATABASE drug_sales_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出 EXIT; # 执行导入假设 sql 文件路径为 /path/to/医药销售管理系统.sql mysql -u root -p drug_sales_management /path/to/医药销售管理系统.sql注意医药销售管理系统.sql文件中已包含CREATE TABLE语句无需提前建表。但必须确保数据库名为drug_sales_management否则BaseDao.java中的private static final String URL jdbc:mysql://localhost:3306/drug_sales_management?...会连错库。第二步检查并修正数据库连接配置打开WEB-INF/web.xml找到context-param中的数据库配置段context-param param-nameurl/param-name param-valuejdbc:mysql://localhost:3306/drug_sales_management?useUnicodetrueamp;characterEncodingutf8/param-value /context-param确认localhost是你的 MySQL 地址若 MySQL 在远程服务器需改成 IP3306是端口若改过需同步drug_sales_management是库名必须与上一步一致。特别注意amp;是 XML 实体实际运行时会被解析为不要手误写成。第三步手动测试连接是否生效在webcontent/Login.jsp中临时插入一段测试代码部署前加验证完删% page importjava.sql.* % % try { Class.forName(com.mysql.jdbc.Driver); Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/drug_sales_management?useUnicodetruecharacterEncodingutf8, root, your_password); out.print(数据库连接成功); conn.close(); } catch (Exception e) { out.print(连接失败 e.getMessage()); } %访问http://localhost:8080/100013136/Login.jsp若显示「数据库连接成功」说明底层 JDBC 通了否则根据错误信息定位Unknown database是库名错Access denied是密码错Connection refused是 MySQL 服务没启或端口不通。3. 权限体系落地角色分离不是 if-else而是 URL 拦截 页面按钮级控制很多课程设计把「经理」和「员工」权限写成两个 if 分支比如if(role.equals(manager)){ showDeleteBtn(); }这看似简单实则埋下巨大隐患前端隐藏按钮 ≠ 后端拒绝请求。用户只要用浏览器开发者工具取消隐藏或直接 POST/DeleteEmployee.jsp?id123就能越权操作。本项目用的是更扎实的双重防护URL 层面的 Filter 拦截 JSP 页面内的角色判断渲染。我们来拆解它是怎么做到的。3.1 Filter 拦截器所有敏感 URL 的第一道闸门项目WEB-INF/web.xml中注册了一个关键 Filterfilter filter-nameRoleFilter/filter-name filter-classfilter.RoleFilter/filter-class /filter filter-mapping filter-nameRoleFilter/filter-name url-pattern/DeleteEmployee.jsp/url-pattern url-pattern/AddSupplier.jsp/url-pattern url-pattern/DeleteSupplier.jsp/url-pattern url-pattern/MemberManage.jsp/url-pattern !-- 更多 manager-only 的 URL -- /filter-mapping对应的filter.RoleFilter.java若源码包含核心逻辑是public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 从 session 获取当前用户角色 String role (String) request.getSession().getAttribute(role); // 获取当前请求的 URI String uri request.getRequestURI(); // 定义 manager 才能访问的 URL 列表 ListString managerUrls Arrays.asList( /DeleteEmployee.jsp, /AddSupplier.jsp, /DeleteSupplier.jsp, /MemberManage.jsp ); // 如果是 manager URL但用户不是 manager重定向到无权限页 if (managerUrls.contains(uri) !manager.equals(role)) { response.sendRedirect(request.getContextPath() /NoPermission.jsp); return; } chain.doFilter(req, resp); }关键点Filter 在请求到达目标 JSP 前就执行完全阻断非法访问。即使用户伪造 POST 请求Filter 也会在chain.doFilter()前拦截并跳转。3.2 JSP 页面内按钮级渲染让前端 UI 与后端权限实时同步光有 Filter 不够用户需要直观知道「哪些功能我能点」。项目在每个 JSP 页面顶部都做了角色判断!-- HomePage.jsp 头部 -- % String role (String) session.getAttribute(role); % !-- 经理专属功能区 -- % if (manager.equals(role)) { % div classmanager-section a hrefAddEmployee.jsp添加员工/a a hrefDeleteEmployee.jsp删除员工/a a hrefMemberManage.jsp会员管理/a /div % } % !-- 员工和经理共有的功能 -- div classcommon-section a hrefSell.jsp药品销售/a a hrefWareHouse.jsp仓库盘点/a /div !-- 员工专属功能经理也能看但不能操作 -- % if (employee.equals(role)) { % div classemployee-section a hrefAddDrugs.jsp添加药品/a a hrefRefunds.jsp处理退货/a /div % } %这种写法确保经理登录后页面显示「添加员工」「删除员工」按钮且点击后能正常跳转Filter 已放行员工登录后这些按钮根本不会渲染出来避免任何误点可能Sell.jsp这类共用页面按钮始终显示但其背后的SellServlet会再次校验角色见下节形成双重保险。3.3 Servlet 层二次校验防止绕过 JSP 直接调用业务逻辑Sell.jsp提交表单到SellServlet后者处理销售逻辑。关键代码在doPost方法中protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从 session 获取用户信息 User user (User) request.getSession().getAttribute(user); if (user null) { response.sendRedirect(Login.jsp); return; } // 2. 强制校验角色只有 employee 和 manager 能销售 String role user.getRole(); if (!employee.equals(role) !manager.equals(role)) { request.setAttribute(error, 无销售权限); request.getRequestDispatcher(Sell.jsp).forward(request, response); return; } // 3. 执行销售业务扣减库存、插入 sell_record、更新药品表... String drugId request.getParameter(drugId); int quantity Integer.parseInt(request.getParameter(quantity)); // 关键开启数据库事务确保库存扣减和销售记录插入原子性 Connection conn null; PreparedStatement ps1 null, ps2 null; try { conn BaseDao.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 更新药品库存扣减 String sql1 UPDATE drugs SET stock stock - ? WHERE id ?; ps1 conn.prepareStatement(sql1); ps1.setInt(1, quantity); ps1.setString(2, drugId); ps1.executeUpdate(); // 插入销售记录 String sql2 INSERT INTO sell_record (...) VALUES (...); ps2 conn.prepareStatement(sql2); // ... 设置参数 ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 request.setAttribute(msg, 销售成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute(error, 销售失败 e.getMessage()); } finally { // 关闭资源 BaseDao.closeAll(conn, ps1, ps2, null); } request.getRequestDispatcher(Sell.jsp).forward(request, response); }这段代码的价值在于它把「权限校验」和「事务控制」绑在一起。即使有人绕过 JSP 直接 POST 到SellServlet也会被if (!employee.equals(role) !manager.equals(role))拦住而conn.setAutoCommit(false)确保了「库存扣减」和「销售记录写入」要么全成功要么全回滚——这是医药系统最基础的数据一致性保障绝非课程设计里常见的“先扣库存再插记录”那种玄学操作。4. 避坑指南五个真实踩过的雷与血泪修复方案部署和调试这个系统时我遇到了五类高频问题每一个都曾让我对着控制台日志抓耳挠腮半小时以上。这里不讲虚的只列现象、根因、修复动作全是实测有效的解法。4.1 现象登录成功后跳转HomePage.jsp页面空白浏览器控制台报Uncaught ReferenceError: $ is not defined原因HomePage.jsp引用了 jQueryscript srcjs/jquery.min.js但项目webcontent/js/目录下根本没有jquery.min.js文件。ZIP 包里img/和js/是空目录所有前端资源缺失。解决下载 jQuery 3.6.0保存为webcontent/js/jquery.min.js同理webcontent/css/bootstrap.min.css也缺失需下载 Bootstrap 3.4.1 CSS 并保存提示别用 Bootstrap 5它的 JS API 和本项目Records.jsp中的$(#table).bootstrapTable(...)不兼容。4.2 现象点击「销售药品」进入Sell.jsp下拉框药品列表为空F12 查看 Network 发现getDrugs.jsp返回 500 错误原因getDrugs.jsp中的 SQL 查询写死了WHERE status 1但drugs表的status字段是TINYINT(1)类型0 或 1而字符串1会导致 MySQL 类型转换失败返回空结果集。解决打开webcontent/getDrugs.jsp将第 12 行WHERE status 1改为WHERE status 1注意这是 MySQL 类型安全的铁律。TINYINT字段必须用数字比较字符串比较会触发隐式转换性能差且易出错。4.3 现象经理在MemberManage.jsp点击「删除会员」弹窗确认后页面刷新但会员仍在列表中原因DeleteVIP.jsp中的删除 SQL 写成了DELETE FROM vip WHERE id ?但vip表的主键字段名是vip_id不是id。SQL 执行时WHERE id ?永远不匹配所以删不掉。解决打开DeleteVIP.jsp找到PreparedStatement设置参数处ps.setString(1, request.getParameter(id)); // 错参数名是 vip_id改为ps.setString(1, request.getParameter(vip_id)); // 对用真实的主键字段名同时确认MemberManage.jsp中删除链接的href参数名一致a hrefDeleteVIP.jsp?vip_id%vip.getVipId()%删除/a4.4 现象Financial_Statistics.jsp图表区域显示「Loading...」后一直空白查看浏览器 NetworkgetRevenueData.jsp返回 JSON 但格式错误原因getRevenueData.jsp输出 JSON 时response.setContentType(application/json;charsetUTF-8)写在了out.print(jsonString)之后导致响应头未生效浏览器当 HTML 解析。解决将response.setContentType(...)移到out.print(...)之前% response.setContentType(application/json;charsetUTF-8); response.setCharacterEncoding(UTF-8); // ... 数据查询和 JSON 构造逻辑 out.print(jsonString); %这是 JSP 的经典陷阱setContentType必须在任何输出包括空格、换行之前调用否则容器已开始写响应体头信息就失效了。4.5 现象Tomcat 启动时报SEVERE: Error listenerStart日志末尾显示Caused by: java.lang.ClassNotFoundException: org.apache.commons.dbcp.BasicDataSource原因项目使用了 Apache Commons DBCP 连接池BaseDao.java中有BasicDataSource引用但WEB-INF/lib/下缺少commons-dbcp-1.4.jar和commons-pool-1.6.jar。解决下载 commons-dbcp-1.4-bin.zip解压取出commons-dbcp-1.4.jar和commons-pool-1.6.jar复制到WEB-INF/lib/目录下重启 Tomcat注意必须用 1.4 版本与mysql-connector-java-5.1.39兼容DBCP 2.x 需要 JDK 7 且 API 不同会报NoSuchMethodError。5. 数据一致性攻坚库存扣减、销售记录、财务统计三者如何严丝合缝医药销售系统最怕什么不是界面丑而是「卖出去 10 盒药库存只扣了 8 盒财务报表却记了 10 盒的收入」。这种数据撕裂在课程设计里太常见——大家忙着实现功能却忘了数据库事务和业务逻辑的咬合点。本项目用三层机制把这个问题钉死数据库层面的外键约束 应用层的事务包裹 报表层的聚合校验。我们以「一笔销售」为例看数据如何从点击按钮开始流经每一层最终在Financial_Statistics.jsp上呈现为可信数字。5.1 第一层MySQL 外键与约束从源头堵死脏数据打开医药销售管理系统.sql找到sell_record表定义CREATE TABLE sell_record ( id int(11) NOT NULL AUTO_INCREMENT, drug_id varchar(50) NOT NULL, quantity int(11) NOT NULL DEFAULT 0, total_price decimal(10,2) NOT NULL DEFAULT 0.00, sell_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, employee_id varchar(50) NOT NULL, PRIMARY KEY (id), KEY fk_drug_id (drug_id), KEY fk_employee_id (employee_id), CONSTRAINT fk_drug_id FOREIGN KEY (drug_id) REFERENCES drugs (id) ON DELETE CASCADE, CONSTRAINT fk_employee_id FOREIGN KEY (employee_id) REFERENCES employee (id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键点有三CONSTRAINT fk_drug_idsell_record.drug_id必须存在于drugs.id中杜绝「销售不存在的药品」ON DELETE CASCADE若某药品被删除其所有销售记录自动清除避免孤儿记录ON DELETE RESTRICT若员工离职禁止直接删employee记录必须先清空其销售记录保证责任可追溯。这些不是摆设。当你在Sell.jsp输入一个不存在的drug_id并提交SellServlet的ps.executeUpdate()会直接抛SQLException被catch块捕获并返回「药品不存在」错误——约束在数据库引擎层生效比任何 Java 代码都可靠。5.2 第二层JDBC 事务确保「扣库存」和「记销售」原子执行回到SellServlet.doPost()我们重点看事务控制部分conn.setAutoCommit(false); // ① 关闭自动提交进入事务模式 // ② 扣减库存UPDATE drugs SET stock stock - ? WHERE id ? ps1.executeUpdate(); // ③ 记录销售INSERT INTO sell_record ... ps2.executeUpdate(); conn.commit(); // ④ 两步都成功才提交事务如果步骤②成功但③失败比如sell_time字段类型不匹配conn.commit()不会执行catch块中的conn.rollback()会把库存扣减操作撤销恢复原状。这就是 ACID 中的Atomicity原子性。验证方法故意在ps2.executeUpdate()前加一行int i 1/0;制造异常。执行销售后查drugs表库存不变sell_record表无新增记录——事务回滚生效。5.3 第三层财务统计报表的交叉验证逻辑Financial_Statistics.jsp不是简单SELECT * FROM sell_record它用聚合 SQL 确保数据自洽-- 1. 按天汇总销售额主报表 SELECT DATE(sell_time) as day, SUM(total_price) as revenue FROM sell_record GROUP BY DATE(sell_time) ORDER BY day; -- 2. 同时校验当天销售总数量是否等于各药品销售数量之和 SELECT DATE(sr.sell_time) as day, SUM(sr.quantity) as total_quantity, (SELECT SUM(sd.quantity) FROM sell_detail sd JOIN sell_record sr2 ON sd.sell_id sr2.id WHERE DATE(sr2.sell_time) DATE(sr.sell_time)) as detail_sum FROM sell_record sr GROUP BY DATE(sr.sell_time);sell_detail表存储每笔销售的明细如销售单号sell_id、药品drug_id、数量quantity而sell_record存储汇总总金额、时间。报表脚本用子查询对比SUM(sr.quantity)和SUM(sd.quantity)若两者不等说明明细与汇总脱节报表会标红警示。这种设计思想值得借鉴报表不只是展示更是数据质量的探测器。我在Financial_Statistics.jsp里加了一行% if (!totalQuantity.equals(detailSum)) { % div classalert alert-danger警告销售数量汇总异常请核查 sell_record 与 sell_detail 数据一致性/div % } %5.4 一个真实场景退货如何反向修复所有关联数据退货不是简单「加回库存」它要逆向修复销售链路上的所有节点。Refunds.jsp提交后RefundServlet执行// 1. 开启事务 conn.setAutoCommit(false); // 2. 查询原销售记录获取药品ID和数量 String sql1 SELECT drug_id, quantity FROM sell_record WHERE id ?; ps1 conn.prepareStatement(sql1); ps1.setString(1, refundId); ResultSet rs ps1.executeQuery(); if (rs.next()) { String drugId rs.getString(drug_id); int quantity rs.getInt(quantity); // 3. 反向更新库存加回 String sql2 UPDATE drugs SET stock stock ? WHERE id ?; ps2 conn.prepareStatement(sql2); ps2.setInt(1, quantity); ps2.setString(2, drugId); ps2.executeUpdate(); // 4. 插入退货记录refund_record 表 String sql3 INSERT INTO refund_record (...) VALUES (...); ps3 conn.prepareStatement(sql3); // ... 设置参数 ps3.executeUpdate(); // 5. 关键更新原销售记录状态为 refunded String sql4 UPDATE sell_record SET status refunded WHERE id ?; ps4 conn.prepareStatement(sql4); ps4.setString(1, refundId); ps4.executeUpdate(); } conn.commit();这里status refunded是灵魂它让Financial_Statistics.jsp的销售额统计 SQL 自动过滤掉已退货订单WHERE status ! refunded确保财务报表永远反映「净销售」。而sell_record.status字段在建表时就定义了DEFAULT normal退货只是改状态不删记录——审计时能查到完整生命周期。从那以后我每次做 Java Web 项目只要涉及「增删改」都会强制走一遍这三步数据库层先想外键、索引、约束让 MySQL 帮我守门应用层再写代码必包try-catch-rollback绝不裸奔展示层最后做报表必加交叉验证 SQL让数据自己说话。这套组合拳下来数据撕裂的概率从「几乎必然」降到了「可以忽略」。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑