超市订单管理系统Eclipse版:从环境搭建到并发库存扣减的完整实战
简介这是一套面向JavaWeb初学者与课程设计学习者的超市订单管理系统完整源码包基于Eclipse开发环境搭建适合用于毕业设计参考、课程实训或SSM/JavaWeb入门练手。压缩包共10个文件约3.5MB包含7张png界面截图、1份md说明文档、1个源码压缩包以及1份完整SQL数据库脚本截图可直观预览系统运行效果SQL文件便于快速还原数据库结构源码包则涵盖订单、商品、供应商等核心业务模块的实现逻辑。目前已有5133人学习下载说明该案例在同类教学资源中具有较高的参考价值。读者可借助源码与数据库脚本快速搭建可运行环境对照截图理解页面跳转与数据交互流程并结合说明文档梳理项目目录结构与功能划分从而掌握JavaWeb项目从建库、编码到部署的完整思路为后续独立开发或二次扩展打下基础。1. 超市订单管理系统(Eclipse版).zip一个老派但极其实用的练手项目如果你正在搜「超市订单管理系统 Eclipse版」大概率是三种人之一刚学完 Java SE 想找个完整项目练手的在校生、需要交课程设计但不想从零搭框架的开发者、或者想用最朴素的技术栈快速验证一个进销存想法的独立开发者。这个标题里的关键词其实已经暴露了它的技术底色——Eclipse 而不是 IDEA说明它大概率是一个偏传统的 Java Web 项目用的是 Servlet/JSP 或者 SSM 那一套而不是现在流行的 SpringBoot Vue 前后端分离。别急着嫌弃它老超市订单管理这个业务场景本身非常经典商品、库存、订单、会员、收银、退货几乎涵盖了 CRUD 的所有变体还天然带事务和并发扣减库存的问题。把它跑通一遍你对 Java Web 的理解会比看十篇教程扎实得多。这一章先把这个项目是什么、能解决什么、适合谁讲清楚后面几章直接上手拆解。2. 环境准备与项目导入别让 JDK 和 Tomcat 版本把你拦在门外2.1 为什么 Eclipse 版项目最容易死在环境上Eclipse 版项目通常不会带 Maven 或 Gradle 构建脚本而是直接把.jar包丢在WebContent/WEB-INF/lib目录下靠 Eclipse 的 Build Path 手动关联。这种做法的好处是脱离 IDE 也能看懂目录结构坏处是版本冲突全靠肉眼排查。我见过太多人卡在第一步项目导入后满屏红叉控制台报ClassNotFoundException或者NoClassDefFoundError然后就开始怀疑人生。其实核心就三件事——JDK 版本、Tomcat 版本、Servlet API 版本这三者必须对齐。常见做法是JDK 用 1.8这个年代的项目基本都锁在 8Tomcat 用 8.5 或 9.0Servlet API 用 3.1 或 4.0。如果你用 JDK 17 去跑大概率会遇到Unsupported class file major version或者反射相关的InaccessibleObjectException。这不是项目的问题是版本跨度太大。2.2 导入步骤与 Build Path 修正假设你已经装好了 JDK 8 和 Tomcat 8.5Eclipse 也配置好了 Server Runtime。接下来按这个流程走# 第一步解压项目到工作空间注意路径不要有中文和空格 unzip 超市订单管理系统\(Eclipse版\).zip -d ~/workspace/supermarket-order # 第二步查看目录结构确认是 Dynamic Web Project 还是普通 Java Project cd ~/workspace/supermarket-order find . -maxdepth 2 -type d | head -20 # 典型结构长这样 # ./src - Java 源码 # ./WebContent - 网页资源 # ./WebContent/WEB-INF/lib - 第三方 jar 包 # ./WebContent/WEB-INF/web.xml - 部署描述符导入 Eclipse 时选File - Import - Existing Projects into Workspace指向解压后的根目录。如果 Eclipse 没识别成 Web 项目右键项目 -Properties - Project Facets勾上Dynamic Web Module和Java版本分别选 3.1 和 1.8。# 第三步检查 lib 目录下的 jar 包是否齐全 ls WebContent/WEB-INF/lib/ # 常见应该有mysql-connector-java-x.x.x.jar、jstl.jar、servlet-api.jar可选、 # 以及可能的 json 解析库如 gson 或 fastjson如果servlet-api.jar不在 lib 里别慌它应该由 Tomcat 提供。在Properties - Java Build Path - Libraries里添加Server Runtime - Apache Tomcat v8.5。这一步做完红叉应该少一大半。2.3 数据库配置改对一处跑通全局超市订单管理系统离不开数据库。这类项目 99% 用 MySQL配置文件通常是src/db.properties或者src/c3p0-config.xml也有直接硬编码在DBUtil.java里的。先找到连接配置# db.properties 典型内容 jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.passwordyour_password注意useSSLfalse在 MySQL 8 里必须加否则控制台会刷一堆警告。驱动类名如果是com.mysql.jdbc.Driver而你的 jar 是 MySQL 8 的mysql-connector-java-8.0.x.jar要改成com.mysql.cj.jdbc.Driver否则报Loading class com.mysql.jdbc.Driver. This is deprecated。建库建表通常项目里会带一个.sql文件用 Navicat 或命令行导入mysql -uroot -p -e CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4; mysql -uroot -p supermarket db/supermarket.sql导入后检查表结构重点看orders、order_item、product、stock这几张表的主外键关系。如果外键约束导致插入失败可以先SET FOREIGN_KEY_CHECKS0;再导入完事再打开。3. 核心业务链路拆解从下单到扣库存的完整代码路径3.1 订单创建一个 Servlet 里藏着多少坑超市订单管理系统的核心链路是「选商品 - 生成订单 - 扣减库存 - 记录流水」。在 Eclipse 版项目里这条链路通常由一个OrderServlet串联用doPost接收表单调用 Service 层再跳转 JSP 展示结果。先看一个典型的订单创建代码// OrderServlet.java 核心片段 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取当前登录用户 User user (User) request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(login.jsp); return; } // 2. 解析购物车参数格式通常是 productId:quantity,productId:quantity String cartStr request.getParameter(cart); String[] items cartStr.split(,); // 3. 开启事务注意 Connection 必须从同一个数据源获取 Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交手动控制事务 OrderService orderService new OrderService(conn); int orderId orderService.createOrder(user.getId(), items); conn.commit(); // 全部成功才提交 response.sendRedirect(order_detail.jsp?orderId orderId); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute(error, 下单失败 e.getMessage()); request.getRequestDispatcher(error.jsp).forward(request, response); } finally { DBUtil.close(conn); } }这段代码的逻辑说明先做登录校验防止未登录用户直接调接口然后把购物车字符串解析成商品项最关键的是事务控制——setAutoCommit(false)之后创建订单、插入订单明细、扣减库存这三个操作要么全成功要么全回滚。参数方面cart的格式是前端 JSP 拼好的如果你要改成 JSON 格式记得同步改解析逻辑。3.2 库存扣减并发下的超卖是怎么发生的库存扣减是这类项目最容易翻车的地方。很多教程版代码直接写UPDATE product SET stock stock - ? WHERE id ?在单用户测试时没问题一旦并发上来就超卖。原因很简单两个线程同时读到库存为 1都认为可以扣结果扣成 -1。正确的做法有两种。第一种是乐观锁加版本号-- 乐观锁扣减每次更新带 version 条件 UPDATE product SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND stock #{quantity} AND version #{version};第二种是悲观锁在查询时就锁行-- 悲观锁SELECT ... FOR UPDATE SELECT stock FROM product WHERE id ? FOR UPDATE; -- 然后在同一事务内判断并更新 UPDATE product SET stock stock - ? WHERE id ?;我一般会推荐乐观锁因为超市订单系统的并发量通常不会特别高乐观锁重试成本低而且不会长时间持有行锁。但要注意乐观锁需要在 Service 层做重试通常重试 3 次还失败就返回「库存不足」。// 乐观锁重试逻辑 int retry 3; while (retry-- 0) { Product p productDao.findById(productId); if (p.getStock() quantity) { throw new RuntimeException(库存不足); } int affected productDao.reduceStock(productId, quantity, p.getVersion()); if (affected 0) { return; // 扣减成功 } // affected 0 说明版本号被改过重试 } throw new RuntimeException(系统繁忙请重试);3.3 订单状态流转别把状态机写成 if-else 泥潭订单状态一般有待支付、已支付、已发货、已完成、已取消。很多项目用一堆if (status 1) ... else if (status 2) ...来写后期加一个「退款中」状态就要改十几个地方。更好的做法是用状态模式或者至少用一个枚举加允许的迁移表。// OrderStatus.java 枚举定义 public enum OrderStatus { PENDING(0, 待支付), PAID(1, 已支付), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; // 构造和 getter 省略 // 定义允许的状态迁移 public static boolean canTransfer(int from, int to) { if (from PENDING.code (to PAID.code || to CANCELLED.code)) return true; if (from PAID.code (to SHIPPED.code || to CANCELLED.code)) return true; if (from SHIPPED.code to COMPLETED.code) return true; return false; } }在 Service 层更新状态前先调canTransfer不合法就抛异常。这样加新状态时只需要改枚举和迁移表业务代码不动。4. 避坑与排查Eclipse 版项目最常见的 5 个翻车现场4.1 中文乱码从 JSP 到数据库的全链路排查现象页面输入中文商品名保存后变成???或者乱码。原因通常有三层JSP 页面编码、Servlet 请求编码、数据库连接编码。解决要一层层查。JSP 头部加% page contentTypetext/html;charsetUTF-8 %Servlet 里在request.getParameter之前调request.setCharacterEncoding(UTF-8)数据库 URL 加characterEncodingutf8。如果是 Tomcat 8 以上GET 请求的中文一般没问题但 POST 必须设编码。4.2 ClassNotFoundExceptionjar 包在 lib 里却找不到现象明明WEB-INF/lib下有mysql-connector-java.jar启动就报ClassNotFoundException: com.mysql.jdbc.Driver。原因通常是 Eclipse 的 Deployment Assembly 没把 lib 目录映射到部署路径。右键项目 -Properties - Deployment Assembly确认/WebContent/WEB-INF/lib映射到了/WEB-INF/lib。如果没有点 Add - Folder 加上。另一个可能是 jar 包损坏重新下载一个替换。4.3 事务不回滚try-catch 把异常吞了现象下单时库存扣了但订单没生成数据不一致。原因多半是 Service 层把异常 catch 住然后没往外抛Servlet 层以为成功了就 commit。解决原则事务边界内只做业务异常必须往上抛由最外层统一 rollback。如果用了 Spring 的Transactional注意默认只对RuntimeException回滚受检异常要显式配rollbackFor。4.4 Tomcat 启动报端口占用现象启动 Server 时提示Port 8080 already in use。原因可能是上次 Tomcat 没关干净或者别的程序占了 8080。Windows 下netstat -ano | findstr 8080找到 PID任务管理器结束Linux 下lsof -i:8080然后kill -9。改端口也行在 Server 配置里把 HTTP/1.1 的端口改成 8081。4.5 JSP 页面报「无法解析导入的类」现象JSP 里% page importcom.xxx.entity.Product %报红。原因通常是项目 Build Path 里缺少源码目录或者 JSP 编译时找不到类。检查Properties - Java Build Path - Source确认src在列表里。如果用了外部 jar确认 jar 在 Libraries 里且被导出。5. 进阶技巧把老项目改造成能写进简历的样子5.1 用 Filter 统一处理编码和登录校验原始项目里每个 Servlet 都写一遍setCharacterEncoding和登录判断代码重复且容易漏。加一个 Filter 一次性解决// EncodingFilter.java public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); // 排除登录页和静态资源 String uri request.getRequestURI(); if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/static/)) { chain.doFilter(req, resp); return; } // 登录校验 if (request.getSession().getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }在web.xml里配置这个 Filter 拦截/*。这样业务 Servlet 里就不用再写重复代码了。5.2 把硬编码 SQL 换成 DAO 层 连接池原始项目可能直接在 Servlet 里写 JDBC改成 DAO 模式后代码清晰很多。再引入 Druid 或 HikariCP 连接池性能提升明显。以 Druid 为例加一个druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalse usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000然后写一个DruidUtil工具类加载配置。连接池的好处是避免频繁创建销毁连接在订单高峰期差别很大。5.3 加一个简单的库存预警定时任务超市场景里库存低于阈值要提醒补货。用ScheduledExecutorService或Timer就能做不需要引入 Quartz// StockWarningTask.java public class StockWarningTask implements Runnable { Override public void run() { ListProduct lowStockList productDao.findLowStock(10); // 阈值 10 if (!lowStockList.isEmpty()) { // 写日志或发邮件这里简单打印 System.out.println(库存预警 lowStockList.size() 个商品需要补货); for (Product p : lowStockList) { System.out.println(p.getName() 剩余 p.getStock()); } } } } // 启动时注册每 30 分钟跑一次 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(new StockWarningTask(), 0, 30, TimeUnit.MINUTES);这个任务可以放在ServletContextListener的contextInitialized里启动contextDestroyed里关闭避免内存泄漏。5.4 验证改造是否成功的三个检查点改完之后别急着说「跑通了」按这三个点验证第一并发测试。用 JMeter 或写个多线程测试类模拟 50 个线程同时下单同一商品看库存是否扣成负数。第二事务测试。在下单过程中手动抛异常看订单表和库存表是否都回滚。第三编码测试。输入中文、emoji、特殊字符看数据库存储和页面展示是否正常。我自己的习惯是每次改完核心链路先跑一遍「下单 - 支付 - 发货 - 完成」全流程再跑一遍「下单 - 取消」逆向流程最后用SELECT核对每张表的数据一致性。这套动作做熟了基本能挡住 90% 的低级错误。希望帮到你。本文还有配套的精品资源点击获取