简介:这是一份基于Java+MySQL的医药销售管理系统完整源码,适合JavaWeb课程设计或毕业设计参考。系统围绕员工与经理两类角色设计权限:员工可管理会员、供应商、药品查询、销售退货与盘点;经理可管理员工和全部基础数据,但不参与销售业务,并设置了供应商与顾客无访问权限的边界。包内共26个文件,以19个JSP页面为核心,配合XML配置、MF清单、SQL数据库脚本及JDBC驱动JAR,另有README与LICENSE说明,整体压缩包仅1.06MB。目前已有263人学习,适合快速导入IDE后结合SQL脚本部署调试,能直观理解权限控制、库存盘点等典型业务模块的编码实现。
1. 医药销售管理系统:先定权限边界,再做 Java+MySQL 的增删改查
医药销售管理系统这类 Java + MySQL 课程设计,表面看是员工、会员、供应商、药品的增删改查,真正拉开差距的其实是权限边界和库存联动。这套项目最值得看的一点:经理角色没有销售、退货权限,只能做会员、供应商、员工管理和报表查看;员工能卖货、能退货,却进不了删除员工、删除供应商的页面,职责分得开,答辩时才有的聊。系统走 JSP + Servlet + JDBC 的传统架构,配 mysql-connector-java-5.1.39-bin.jar 和随包 SQL 脚本就能跑,适合正在做 Java Web 课程设计、毕业设计,或者想看一套完整进销存闭环怎么处理会话、事务和外键约束的新手。
2. 数据库设计与权限模型:五张核心表怎么撑起销售、退货和盘点
拿到压缩包先别急着把「医药销售管理系统.sql」导入就去点登录,我习惯先把表关系理一遍。项目正文的页面清单已经把功能暴露得很清楚:AddDrugs.jsp 对应药品与采购入库,MemberManage.jsp、AddSupplier.jsp 对应会员和供应商管理,Sell.jsp、Refunds.jsp 对应销售和退货,WareHouse.jsp 对应盘点,Records.jsp 和 Financial_Statistics.jsp 对应流水和财务统计。页面背后至少要五类核心表支撑:员工表、会员表、供应商表、药品表、单据表——其中单据又拆成销售、退货、入库三张。谁能对哪张表做增删改,就是权限模型要回答的问题。
2.1 角色权限:为什么经理反而不能卖药
从页面命名能直接归纳出两套角色的边界。员工:查看和添加会员、查看和添加供应商、查询药品(支持按编号、名称、类别查)、添加药品采购记录、销售药品、处理退货、盘点仓库、查看销售/退货/入库记录、修改个人信息。经理:查看/添加/删除会员、供应商、员工,盘点仓库,查看销售/退货/入库记录,修改个人信息,但没有销售和退货入口。
乍一看反直觉:权限更高的经理反而不能卖货?实际业务里销售和退货是收银动作,必须留痕到具体员工;经理管的是人员、档案和报表,如果经理也能直接改销售数据,财务核对时根本分不清是系统真实产生还是人为干预。门店通常的做法就是"管账的和收银的分开",这套系统把规则直接做进了路由:Login.jsp 登录后按 role 决定进入哪套主页,Sell.jsp 和 Refunds.jsp 对经理不可见。
实现层面,这类项目是典型的"登录后把 role 放进 session,每个页面再校验"。我一般在类似系统里还会加一层:前端按角色渲染入口只解决"看不见",解决不了"手动输 URL 直接访问 Sell.jsp",所以更稳的做法是在目标页面顶部用 Java 代码判断 session 里的 role,不符合就重定向回主页。课程设计做到这一步,就能跟老师聊"为什么经理不能卖药"而不只是"我把按钮藏了"。
2.2 表结构拆解:员工、药品、会员、供应商、单据
包里没有单独的表结构文档,但根据页面反推,一套合格的建表脚本至少包含下面这些核心表。参考 SQL 如下,实际字段名以「医药销售管理系统.sql」导出的为准:
CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL, emp_name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '1-员工 2-经理', phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT, member_no VARCHAR(20) UNIQUE NOT NULL, member_name VARCHAR(50) NOT NULL, phone VARCHAR(20), points INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ); CREATE TABLE drug ( drug_id INT PRIMARY KEY AUTO_INCREMENT, drug_no VARCHAR(30) UNIQUE NOT NULL, drug_name VARCHAR(100) NOT NULL, category VARCHAR(50), specification VARCHAR(100), unit_price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, safety_stock INT DEFAULT 50, supplier_id INT, FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ); CREATE TABLE sale_record ( sale_id INT PRIMARY KEY AUTO_INCREMENT, sale_no VARCHAR(30) UNIQUE NOT NULL, drug_id INT NOT NULL, emp_id INT NOT NULL, member_id INT, quantity INT NOT NULL, amount DECIMAL(10,2) NOT NULL, sale_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (drug_id) REFERENCES drug(drug_id), FOREIGN KEY (emp_id) REFERENCES employee(emp_id), FOREIGN KEY (member_id) REFERENCES member(member_id) );employee 表用 emp_no 做登录账号而不是 emp_id,是因为工号比自增主键更适合记忆和输入;role 用 TINYINT 存 1、2,比字符串"admin"省空间,JSP 里判断 role == 2 也直接。member 表带 points 积分字段,对应 AddVIP.jsp 的开通入口,积分能在后续做折扣、会员等级,属于"埋得下扩展点"的设计。drug 表加了 safety_stock 安全库存字段,WareHouse.jsp 盘点时靠它判断哪些药需要补货,这个字段在第三章查询和第六章预警里都会用到。
几个 SQL 细节值得抄作业:unit_price 用 DECIMAL(10,2) 而不是 FLOAT,药品价格做累计求和时浮点会出 0.1+0.2 不等于 0.3 的精度问题,DECIMAL 是精确小数;stock INT DEFAULT 0 保证了新增药品默认不超卖;sale_record 同时外键关联 drug、employee、member,查报表时一次 JOIN 就能把"哪个员工把什么药卖给了哪个会员"拉出来,Financial_Statistics.jsp 的统计全部依赖这张表。sale_no 用 VARCHAR 存业务单号而不是直接用自增 id,是为了以后能和纸质单据、对账单对上号。
退货和入库两张表在原脚本里通常长这样:refund_record 至少要带原 sale_id 外键、退款数量、退款原因和操作员工;inbound_record 带 drug_id、数量、进价、采购时间和操作员工。单号生成常见做法是"前缀 + 时间戳",比如 XS20250101001,因为自增 id 在并发和跨库迁移时容易撞。
2.3 初始账号与默认数据:导入 SQL 后先别急着登录
SQL 导入完成后,第一件事不是开 Tomcat,而是先执行一条 SELECT,看清楚 employee 表里到底存了什么。下面几条语句是我每次拿到类似系统都会跑的:
-- 查看所有账号和角色,确认默认管理员是谁 SELECT emp_id, emp_no, emp_name, role FROM employee; -- 如果密码是明文,且想改成自己的测试密码 UPDATE employee SET password = '123456' WHERE emp_no = 'admin'; -- 如果密码是 MD5 密文,别直接 UPDATE 明文,否则永远登不进去 -- 走 MySetting.jsp 的修改密码功能最稳这里有个高频翻车点:原脚本 password 字段如果是 MD5 加密存储,你直接 UPDATE 成明文,登录时 Java 端拿明文去比密文,必然失败。先 SELECT 看数据形态,再决定是查 Java 代码里的加密逻辑还是走系统自带的修改密码页面。
提示:改初始密码之前先备份原数据一眼,至少确认原来的 password 是明文还是密文,再做修改。
MySQL 8.0 用户还要注意:包里带的驱动是 mysql-connector-java-5.1.39-bin.jar,5.1.39 对 8.0 默认的 caching_sha2_password 认证插件支持不好,导入 SQL 或启动时可能报认证插件错误。我一般建议课程设计直接用 MySQL 5.7 和驱动配对,省一晚上的调试时间;如果非要用 8.0,方案见第五章 5.5 节。
3. JDBC + JSP 实现链路:登录路由、药品查询与库存联动
这套项目没用 MyBatis、Spring 那一套,就是最直接的 JSP + Servlet + JDBC。对课程设计来说这反而是好事:能从 Login.jsp 到 Sell.jsp 把一条请求链路从头看到尾,理解会话、防注入、事务这些基础概念是怎么落到代码里的。下面按"登录路由 → 查询盘点 → 销售退货"三条链路拆。
3.1 登录会话与角色路由:session 里存什么,页面权限才能立住
登录页 Login.jsp 收集工号和密码,提交到登录 Servlet。核心逻辑是先按 emp_no 和 password 查 employee 表,能查到就把员工身份写进 session,然后进主页;查不到就回登录页带错误提示。参考实现:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String empNo = request.getParameter("empNo"); String password = request.getParameter("password"); String sql = "SELECT emp_id, emp_name, role FROM employee WHERE emp_no = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empNo); ps.setString(2, password); ResultSet rs = ps.executeQuery(); if (rs.next()) { // 身份信息放进 session,后续每个页面都靠它判断权限 HttpSession session = request.getSession(); session.setAttribute("empId", rs.getInt("emp_id")); session.setAttribute("empName", rs.getString("emp_name")); session.setAttribute("role", rs.getInt("role")); // 员工和经理进同一个主页,菜单根据 role 动态渲染 response.sendRedirect("HomePage.jsp"); } else { request.setAttribute("errorMsg", "工号或密码错误"); request.getRequestDispatcher("Login.jsp").forward(request, response); } } catch (Exception e) { e.printStackTrace(); request.setAttribute("errorMsg", "系统异常,请查看日志"); request.getRequestDispatcher("Login.jsp").forward(request, response); } } }从文件清单看只有 HomePage.jsp 一个主页,所以更可能的做法是主页内按 role 渲染菜单——员工看到销售、退货入口,经理看到员工管理、供应商删除入口。这样权限路由其实分两段:主页内做菜单级控制,目标页面再做动作级控制。如果后面答辩被问"员工直接输 Sell.jsp 怎么办",答案就在 3.3 和第六章的 Filter。
这里用 PreparedStatement 而不是 Statement,是因为参数都是页面传进来的字符串,直接拼接 SQL 等于把登录接口变成注入点。PreparedStatement 的预编译机制把参数当成数据而不是 SQL 片段,这是 Java 基础面试里常考、也是这套系统里真实存在的一层保护。
3.2 药品查询与库存盘点:模糊匹配和补货状态一条 SQL 搞定
Sell.jsp 的查询区域通常接收一个 keyword 参数,后端按药品编号、名称、类别三个维度做 LIKE 匹配:
SELECT drug_id, drug_no, drug_name, category, specification, unit_price, stock FROM drug WHERE drug_no LIKE ? OR drug_name LIKE ? OR category LIKE ? ORDER BY drug_id DESC;对应 Java 端就是查出来之后设置三个一样的参数值(keyword 前后拼 %),比如 setString(1, "%阿莫%")。三个 OR 条件在数据量大时可以换 UNIQUE 索引或加药品类别下拉框缩小范围,课程设计阶段不用过度设计,字段上 drug_no 有 UNIQUE 索引已经够。
仓库盘点 WareHouse.jsp 在查列表的同时往往要显示"是否缺货",我一般会直接加一个 CASE WHEN 判断:
SELECT drug_id, drug_no, drug_name, stock, safety_stock, CASE WHEN stock <= safety_stock THEN '需补货' ELSE '正常' END AS stock_status FROM drug ORDER BY stock_status DESC, drug_id;ORDER BY stock_status DESC 会把"需补货"排在前面,盘点的人不用在整页列表里找。safety_stock 这个字段建表时有默认值 50,盘点页里也能顺手把默认值改掉——这就是前面强调"初始数据要改"的原因之一,默认安全库存不等于你药房的实际周转水平。
3.3 销售与退货的库存联动:一个事务里完成写单和扣减
销售的核心不是 INSERT 销售单,而是"插入销售单 + 扣减库存"必须同时成功或同时失败。如果先插单再扣库存,扣库存失败就会留下一个凭空的销售记录;如果先扣库存再插单,插单失败库存就莫名其妙少了。正确写法是放在同一个数据库事务里:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,手动控制事务边界 // 1. 锁定药品行,防止两个窗口同时卖最后一盒 String selectSql = "SELECT stock FROM drug WHERE drug_id = ? FOR UPDATE"; PreparedStatement psSelect = conn.prepareStatement(selectSql); psSelect.setInt(1, drugId); ResultSet rs = psSelect.executeQuery(); if (!rs.next() || rs.getInt("stock") < quantity) { throw new RuntimeException("库存不足"); } // 2. 生成业务单号并插入销售记录 String saleNo = "XS" + System.currentTimeMillis(); String insertSql = "INSERT INTO sale_record(sale_no, drug_id, emp_id, member_id, quantity, amount) " + "VALUES (?, ?, ?, ?, ?, ?)"; PreparedStatement psInsert = conn.prepareStatement(insertSql); psInsert.setString(1, saleNo); psInsert.setInt(2, drugId); psInsert.setInt(3, empId); psInsert.setInt(4, memberId); psInsert.setInt(5, quantity); psInsert.setBigDecimal(6, price.multiply(BigDecimal.valueOf(quantity))); psInsert.executeUpdate(); // 3. 扣库存,WHERE 条件里带 stock >= ? 做二次兜底 String updateSql = "UPDATE drug SET stock = stock - ? WHERE drug_id = ? AND stock >= ?"; PreparedStatement psUpdate = conn.prepareStatement(updateSql); psUpdate.setInt(1, quantity); psUpdate.setInt(2, drugId); psUpdate.setInt(3, quantity); int rows = psUpdate.executeUpdate(); if (rows == 0) { // 走到这里说明 SELECT 之后、UPDATE 之前库存被并发改掉了 throw new RuntimeException("库存不足,本次销售已回滚"); } conn.commit(); // 提交事务 } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } throw e; } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (Exception e) { } } DBUtil.close(conn, null, null); }逻辑说明:FOR UPDATE 是行级锁,把当前事务和这条药品库存绑定,别人要 UPDATE 同一行必须等提交;不带锁的话,两个窗口同时卖最后一盒药,各自 SELECT 都看到 stock=1,各自扣减,就超卖了。步骤 3 的 UPDATE 又加了一道 WHERE stock >= ?,即使锁失效也能用受影响行数判断是否超卖——这就是"双保险"。
amount 金额我建议用后端 unit_price 乘 quantity 计算,不要轻信前端传过来的值,否则前端改个 hidden 字段就能把药价改了。退货就是反向操作:INSERT 一条退款记录 + UPDATE drug SET stock = stock + ?,同样要包事务,并且退货数量要小于等于原销售单的可退数量,这部分在 RefundsDetail.jsp 里通常按原销售单展开明细。
4. 部署复现:MySQL 5.7 + Tomcat 8.5 把这套系统跑起来
这套系统是传统的 Java Web 项目,部署路径很固定,但每一步都有对应的坑。按照下面的顺序做,基本半小时内能见到登录页。
4.1 环境准备:JDK、MySQL、Tomcat 和驱动的配对关系
| 组件 | 推荐版本 | 原因 |
|---|---|---|
| JDK | 1.8 | 传统 JSP/Servlet 项目最稳,和 Tomcat 8.5 兼容性好 |
| MySQL | 5.7 | 和 5.1.39 驱动认证方式匹配,免去插件兼容问题 |
| Tomcat | 8.5 | 支持 JSP 2.3,Java EE 7 标准 |
| JDBC 驱动 | 压缩包自带 mysql-connector-java-5.1.39-bin.jar | 版本是项目验证过的,不用另下 |
如果你的电脑装的是 JDK 17,编译时大概率看到"警告: 源发行版 17 需要目标发行版 17"或者编译报错,那是因为 Eclipse 的 Compiler compliance level 跟着 JDK 走了,而项目是老的 Java 8 写法。解决方式:Eclipse 里 Window → Preferences → Java → Compiler 把编译级别改成 1.8,再把项目的 Java Build Path 里的 JRE 指到 JDK 8。MySQL 同理,8.0 不是不能用,但要用它就得同步换驱动和改 JDBC 参数,新手先按 5.7 来省事。
4.2 导入 SQL 与修改连接配置:URL 里三个参数决定生死
把压缩包解压后,用命令行导入 SQL:
# 方式一:重定向导入,注意文件路径 mysql -uroot -p < /path/to/医药销售管理系统.sql # 方式二:进入 mysql 后用 source,Windows 下路径用正斜杠 mysql -uroot -p mysql> source D:/projects/医药销售管理系统.sql;Linux 下如果报 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock',说明 MySQL 服务没启动,先 systemctl start mysqld 或者 service mysql start,再重新执行。Windows 下如果命令行直接粘贴中文路径乱码,就用 --default-character-set=utf8 指定客户端编码:
mysql --default-character-set=utf8 -uroot -p < 医药销售管理系统.sql注意:如果导入 SQL 时不是用命令行而是用 Navicat 这类图形工具,工具默认字符集有时候会覆盖 SQL 文件里的 utf8 声明,导入前先确认连接编码是 UTF-8。
导入以后,连接配置集中在 DBUtil 类里,改成你自己的数据库账号密码:
private static final String URL = "jdbc:mysql://localhost:3306/drug_sales_management" + "?useUnicode=true&characterEncoding=utf8&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456";URL 里三个参数值得记一下:useUnicode=true 配合 characterEncoding=utf8 决定中文能不能正常读写,缺了它页面查出来全是问号;useSSL=false 是因为本机开发没有证书,5.1.39 驱动在 MySQL 5.7 上默认会尝试 SSL 握手,本地跑没必要,关掉能少一堆 SSL 警告。如果你换成了 MySQL 8.0 的 8.x 驱动,驱动类名要改成 com.mysql.cj.jdbc.Driver,URL 里把 useSSL=false 换成 sslMode=DISABLED 更合适。
4.3 从 webcontent 目录到浏览器可访问:部署步骤和 404 排查
Eclipse 导入步骤:File → Import → General → Existing Projects into Workspace,选择解压目录,让 Eclipse 识别成 Dynamic Web Project。注意项目目录结构里是 webcontent 而不是通常的 WebContent,如果 Eclipse 没自动识别 Web 根目录,进 Project Properties → Project Facets 勾选 Dynamic Web Module,再到 Deployment Assembly 里把 webcontent 映射到 /。
然后把 mysql-connector-java-5.1.39-bin.jar 复制到 webcontent/WEB-INF/lib 下——这一步经常被忘,忘了就是下章第一个报错。启动 Tomcat 后访问 http://localhost:8080/项目名/Login.jsp,登录用的默认账号在 2.3 节查过,直接输进去测。
如果访问 404,按这个顺序排查:第一看 Tomcat 日志有没有报 Servlet 编译失败;第二确认 webcontent 下 JSP 路径大小写,Linux 服务器上区分大小写;第三确认项目有没有成功部署到 webapps,Eclipse 里看 Servers 视图的状态是不是 Started;最后才是怀疑代码问题。这套排查顺序对老项目特别管用,别一上来就翻业务代码。
5. 避坑记录:驱动版本、中文乱码与重复扣库存
这一章全是血泪经验。JSP + JDBC 老项目的问题特征很明显:报错集中在编译、编码、数据库连接三块。下面五条是我在类似项目里反复遇到的,每一条按"现象 → 原因 → 解决"写。
5.1 现象:启动后报 ClassNotFoundException: com.mysql.jdbc.Driver
启动 Tomcat 一访问就抛 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,或者在 DBUtil 的静态块里直接打异常。原因很简单:驱动 jar 没有进入 WEB-INF/lib,或者 Eclipse 里 jar 只加了 Build Path 但没进部署清单。解决:把 mysql-connector-java-5.1.39-bin.jar 复制到 webcontent/WEB-INF/lib 下;Eclipse 里还要右键项目 → Properties → Deployment Assembly → Add → Java Build Path Entries,把该 jar 勾上。判断标准很简单:部署目录 WEB-INF/lib 里能看到这个 jar 才算完。
5.2 现象:数据库中文正常,页面上全是问号
加了药品"阿莫西林胶囊",页面上显示"??????"。原因是连接 URL 没带 characterEncoding=utf8,或者 JSP 页面缺 pageEncoding 声明,又或者建库时字符集就是 latin1。解决要三层一起改:MySQL 建库时用 utf8mb4(CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4);JDBC URL 带上 useUnicode=true&characterEncoding=utf8;JSP 头部写全<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。注意如果库里已有乱码数据,改完 URL 后要重新插入才能验证,旧数据已经坏掉了。
5.3 现象:销售成功后刷新页面,库存被重复扣减
卖完一盒阿莫西林,按 F5 一刷新,销售单多出一条,库存多扣了一盒。原因是提交表单后服务端用 forward 转发回原页面,浏览器觉得"刚才的 POST 还能再发一次",刷新就重复提交。解决用 Post/Redirect/Get 模式:销售接口处理成功后,直接 response.sendRedirect 到销售列表或首页,而不是 request.getRequestDispatcher().forward()。这样刷新时触发的是 GET 请求,不会重复执行写库逻辑。如果场景要求更高的严谨度(比如订单提交),再加一次性 token,课程设计做到 PRG 就够。
5.4 现象:删除供应商时被外键约束拦截,DELETE 失败
DeleteSupplier.jsp 里点删除,直接抛外键约束异常,说 drug 表里有记录引用这个 supplier_id。原因:drug 表的外键指向 supplier 表,只要还有药品挂在这个供应商名下,数据库就不允许直接删父表记录——这是 InnoDB 外键的默认行为,反而是在保护数据。解决:删除前先做关联检查,把该供应商名下的药品数查出来:
SELECT COUNT(*) FROM drug WHERE supplier_id = 2;查出有药就先在页面上提示"该供应商下有 N 种药品,请先转移或下架",或者让用户选择把这些药品的 supplier_id 置空再删。不要为了图方便把外键约束 DROP 掉,课程设计里外键是要讲的点,删了反而没得讲。
5.5 现象:MySQL 8.0 下驱动报 Authentication plugin 错误
用 8.0 的 MySQL 跑这套系统,启动时报 Communications link failure,或者直接提示 Authentication plugin 'caching_sha2_password' cannot be loaded。原因是 MySQL 8.0 默认认证插件换成了 caching_sha2_password,而 mysql-connector-java-5.1.39 是 2015 年的驱动,只认 mysql_native_password。解决有三条路:最省事是把 MySQL 退到 5.7;不想换数据库就把登录用户改回老认证插件——ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';;再或者换 8.0.x 驱动并把驱动类名改成 com.mysql.cj.jdbc.Driver。三个方案我推荐第一个,课程设计别在环境上花太多时间。
6. 进阶:用 Filter 统一权限,把库存预警和日报表补上
这套系统跑通之后,我建议做三个很小但很显功力的加强,答辩时能讲的内容立刻不一样。
6.1 用 Filter 把权限规则收敛到一处
前面 3.1 说过,员工输 URL 直接访问 Sell.jsp 的问题,只靠页面跳转是挡不住的。用 Filter 统一拦截,规则集中在一处:
@WebFilter("/*") public class AuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 登录页、登录接口和静态资源直接放行 if (uri.endsWith("Login.jsp") || uri.endsWith("/login") || uri.contains("/img/")) { chain.doFilter(request, response); return; } // 未登录一律回登录页 HttpSession session = req.getSession(false); if (session == null || session.getAttribute("empId") == null) { resp.sendRedirect(req.getContextPath() + "/Login.jsp"); return; } // 经理不允许访问销售和退货页 Integer role = (Integer) session.getAttribute("role"); if (role != null && role == 2 && (uri.endsWith("Sell.jsp") || uri.endsWith("Refunds.jsp"))) { resp.sendRedirect(req.getContextPath() + "/HomePage.jsp"); return; } chain.doFilter(request, response); } }这个 Filter 的价值在于:权限规则只写一份,以后加页面只需在拦截列表里加一行;面试聊到权限控制时,能说出"前端隐藏菜单只是体验,Filter 才是边界",比背八股文管用得多。再往下想上连接池,就把 DBUtil 的 getConnection 换成从 Druid 或 HikariCP 数据源里拿,其余调用点不用动。
6.2 库存预警:把 safety_stock 用起来
建表时留了 safety_stock,盘点页可以直接加一段高亮区:
SELECT drug_no, drug_name, stock, safety_stock, (safety_stock - stock) AS shortage FROM drug WHERE stock < safety_stock ORDER BY shortage DESC;缺货药品按缺口排序,仓库的人一眼看到最急的。再往前一步,把这个查询结果做成定时任务发邮件,就是一套还没上消息队列的简易补货提醒。
6.3 日报表:一条 GROUP BY 吃下财务统计
Financial_Statistics.jsp 的单日销售统计用一条 GROUP BY 就能出:
SELECT DATE_FORMAT(sale_time, '%Y-%m-%d') AS sale_date, COUNT(*) AS order_count, SUM(quantity) AS total_quantity, SUM(amount) AS total_amount FROM sale_record WHERE sale_time >= CURDATE() GROUP BY sale_date ORDER BY sale_date DESC;amount 是 DECIMAL 类型,SUM 结果就是准确小数,这就是建表时不用 FLOAT 的红利。以后想扩展月度、季度,只改 DATE_FORMAT 的格式串和 WHERE 条件。
我从那以后每次拿到别人的课程设计,第一件事不再点开 README 看介绍,而是先执行 SELECT 查员工表、再看 JDBC URL 三个参数、最后用 Filter 把所有受限页面扫一遍——这三步做完,项目能不能跑、边界在哪基本心里有数。这套系统从项目编号 100013136 就能定位到原包,按第四章的顺序导入 SQL、部署 Tomcat,跑通后再把第六章的 Filter 和预警 SQL 加进去,就能拿出去当课程设计交了。希望帮到你。
本文还有配套的精品资源,点击获取