☰
Java Web医药销售系统:角色权限+事务控制+部署避坑指南
2026/9/28 7:27:39 网站建设 项目流程

简介:这是一套面向计算机专业本科生的Java Web课程设计实战项目,聚焦医药销售业务场景,帮助学习者掌握JSP+Servlet+MySQL三层架构开发全流程。资源完整实现会员与供应商管理、药品查询与库存跟踪、采购/销售/退货/盘点等核心业务,角色权限划分清晰(员工与经理功能差异化明确),具备真实企业级系统雏形。压缩包共26个文件,含19个JSP页面(覆盖登录、首页、会员/供应商/员工管理、销售与财务统计等模块)、1个SQL建库脚本、1个MySQL驱动JAR包、1个README说明文档及配套图片与License文件,整体仅1.06MB,轻量易部署。已有263人学习下载,提供开箱即用的可运行工程结构、规范的数据库设计(含医药行业典型字段)、权限控制逻辑实现细节及界面截图参考,适合课程设计参考、Java Web综合实训与毕业设计选题拓展。

1. 这不是又一个学生练手项目:它真能跑通医药销售全流程,从登录鉴权到财务统计全链路闭环

你可能已经点开过 dozens 个标着“Java+MySQL课程设计”的压缩包,双击解压后看到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>根元素声明了version="3.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.*全部报错。
操作步骤:

  1. 下载 Apache Tomcat 8.5.94 (Windows 选zip,Linux 选tar.gz)
  2. 解压后进入conf/server.xml,确认<Connector port="8080"行未被注释
  3. 启动:bin/startup.bat(Win)或bin/startup.sh(Linux)

2.3 MySQL 5.7+ 是唯一兼容版本:5.6 会因 GROUP BY 报错

Financial_Statistics.jsp中有一条关键 SQL:

SELECT 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_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,但缺少ONLY_FULL_GROUP_BY,导致该语句虽能执行,却返回错误聚合结果(比如同一天多笔销售只取第一条);而 MySQL 5.7 默认启用ONLY_FULL_GROUP_BY,强制要求 SELECT 列必须在 GROUP BY 中出现或为聚合函数,反而让这条语句报错。
解决方法:修改 MySQL 配置文件my.cnf(Linux)或my.ini(Windows),在[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-name>url</param-name> <param-value>jdbc:mysql://localhost:3306/drug_sales_management?useUnicode=true&amp;characterEncoding=utf8</param-value> </context-param>

确认localhost是你的 MySQL 地址(若 MySQL 在远程服务器,需改成 IP);3306是端口(若改过需同步);drug_sales_management是库名(必须与上一步一致)。特别注意&amp;是 XML 实体,实际运行时会被解析为&,不要手误写成&。

第三步:手动测试连接是否生效
在webcontent/Login.jsp中临时插入一段测试代码(部署前加,验证完删):

<%@ page import="java.sql.*" %> <% try { Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/drug_sales_management?useUnicode=true&characterEncoding=utf8", "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?id=123,就能越权操作。本项目用的是更扎实的双重防护:URL 层面的 Filter 拦截 + JSP 页面内的角色判断渲染。我们来拆解它是怎么做到的。

3.1 Filter 拦截器:所有敏感 URL 的第一道闸门

项目WEB-INF/web.xml中注册了一个关键 Filter:

<filter> <filter-name>RoleFilter</filter-name> <filter-class>filter.RoleFilter</filter-class> </filter> <filter-mapping> <filter-name>RoleFilter</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 列表 List<String> 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 class="manager-section"> <a href="AddEmployee.jsp">添加员工</a> <a href="DeleteEmployee.jsp">删除员工</a> <a href="MemberManage.jsp">会员管理</a> </div> <% } %> <!-- 员工和经理共有的功能 --> <div class="common-section"> <a href="Sell.jsp">药品销售</a> <a href="WareHouse.jsp">仓库盘点</a> </div> <!-- 员工专属功能(经理也能看,但不能操作) --> <% if ("employee".equals(role)) { %> <div class="employee-section"> <a href="AddDrugs.jsp">添加药品</a> <a href="Refunds.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引用了 jQuery(<script src="js/jquery.min.js">),但项目webcontent/js/目录下根本没有jquery.min.js文件。ZIP 包里img/和js/是空目录,所有前端资源缺失。
解决:

  1. 下载 jQuery 3.6.0
  2. 保存为webcontent/js/jquery.min.js
  3. 同理,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 href="DeleteVIP.jsp?vip_id=<%=vip.getVipId()%>">删除</a>

4.4 现象:Financial_Statistics.jsp图表区域显示「Loading...」后一直空白,查看浏览器 Network,getRevenueData.jsp返回 JSON 但格式错误

原因:getRevenueData.jsp输出 JSON 时,response.setContentType("application/json;charset=UTF-8")写在了out.print(jsonString)之后,导致响应头未生效,浏览器当 HTML 解析。
解决:将response.setContentType(...)移到out.print(...)之前:

<% response.setContentType("application/json;charset=UTF-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。
解决:

  1. 下载 commons-dbcp-1.4-bin.zip
  2. 解压,取出commons-dbcp-1.4.jar和commons-pool-1.6.jar
  3. 复制到WEB-INF/lib/目录下
  4. 重启 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键点有三:

  • CONSTRAINT fk_drug_id:sell_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 class="alert 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 项目,只要涉及「增删改」,都会强制走一遍这三步:

  1. 数据库层:先想外键、索引、约束,让 MySQL 帮我守门;
  2. 应用层:再写代码,必包try-catch-rollback,绝不裸奔;
  3. 展示层:最后做报表,必加交叉验证 SQL,让数据自己说话。
    这套组合拳下来,数据撕裂的概率从「几乎必然」降到了「可以忽略」。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询