
简介这是一套面向高校计算机专业学生与Java初学者的超市订单管理系统课程设计源码基于MySQL数据库与原生JDBC实现采用ServletJSP的Web工程结构适合作为大学实训、课程设计或毕业设计的参考方案。压缩包共231个文件约1.57MB其中包含31个Java源文件承载核心业务逻辑40个JSP页面负责前端展示46个JavaScript脚本与10个CSS样式文件完成交互与界面美化另有12个XML配置、3个properties属性文件及若干图片资源整体结构清晰、模块划分明确。项目完整覆盖商品管理、订单处理、用户权限等典型超市业务场景读者可直接导入jar包运行调试快速理解JDBC连接池、Servlet请求分发与JSP动态渲染的协作流程并在此基础上进行功能扩展或二次开发。目前已有1817人学习下载适合需要一份可运行、易上手的Java Web实战项目来巩固知识体系的学习者。1. 超市订单管理系统课设从能跑到能讲清楚的完整落地路径很多同学拿到“java课设超市订单管理系统源代码.zip”这类资源时第一反应是解压、导入 IDE、点运行看到控制台输出菜单就觉得万事大吉。但真正到了答辩现场老师问一句“你的订单状态流转是怎么设计的”“库存扣减和订单生成在同一个事务里吗”立刻就卡住了。这个标题背后真正要解决的问题不是“有没有代码”而是“这套代码能不能支撑你讲清楚一个完整的业务闭环”。超市订单管理系统听起来简单实际上它覆盖了商品管理、库存扣减、订单创建、订单状态流转、收银结算这几个核心模块是一个非常适合练手的 Java 课程设计题目。它适合正在做课设的在校生也适合想用一个小项目把 Java 基础、JDBC、面向对象编程串起来的初学者。接下来我会按实际做项目的顺序把选型、建表、核心代码、避坑点一层层拆开讲。2. 技术选型与项目骨架为什么课设阶段不建议上 Spring Boot2.1 课设场景下的技术栈取舍拿到一个课设题目第一个要做的决定不是写代码而是选技术栈。很多同学一上来就想用 Spring Boot MyBatis Vue 搞前后端分离觉得这样“显得高级”。但课设的评分标准通常看的是业务逻辑是否完整、代码结构是否清晰、你能不能讲明白每一层在做什么而不是你用了多新的框架。我一般会建议课设阶段用Java SE JDBC MySQL Swing 或控制台菜单这套组合原因很直接依赖少、环境好配、调试链路短出问题了你能定位到具体哪一行 SQL 或哪一个对象。具体来说项目骨架按经典三层结构组织就够了。实体层放Product、Order、OrderItem、User这些类DAO 层负责和数据库打交道每个实体对应一个 DAO 类Service 层写业务逻辑比如创建订单时要同时扣库存、算总价、写订单明细UI 层用控制台菜单或者简单的 Swing 窗口做交互。这个结构不复杂但它能让你在答辩时清楚地说出“这一层负责什么、那一层负责什么”这比堆框架有用得多。注意如果你的学校明确要求用 SSM 或 Spring Boot那就按学校要求来。这里说的是没有框架强制要求时的推荐方案。2.2 数据库表设计与建表 SQL超市订单管理系统的核心表不多但每张表的字段设计直接影响后面写代码顺不顺手。下面是我常用的五张表结构直接可以在 MySQL 里执行。-- 商品表超市里的每一件商品 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, category VARCHAR(50) COMMENT 分类, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单主表一次收银对应一条订单 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单编号, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细表一个订单里买了哪些商品 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL COMMENT 购买数量, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (product_id) REFERENCES product(id) ); -- 用户表收银员和管理员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0收银员 1管理员 ); -- 库存流水表记录每次库存变动方便排查 CREATE TABLE stock_log ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, change_amount INT NOT NULL COMMENT 正数入库 负数出库, order_no VARCHAR(32) COMMENT 关联订单号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这几张表的设计逻辑是这样的product存商品基本信息和当前库存orders和order_item是一对多的关系一个订单对应多条明细。stock_log这张表很多同学会忽略但它在你排查“库存对不上”的时候非常有用相当于一个黑匣子每次扣减都留痕。字段类型上金额统一用DECIMAL(10,2)而不是FLOAT或DOUBLE因为浮点数算钱会出现精度问题这是血泪经验。2.3 JDBC 工具类与数据库连接配置选 JDBC 就意味着你要自己管理连接。最基础的做法是写一个DBUtil类把驱动加载、连接获取、资源关闭封装起来。import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/supermarket?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (ps ! null) ps.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这段代码的关键点有三个。第一Class.forName加载驱动只需要一次放在静态块里最合适。第二URL 里的serverTimezone参数必须加否则 MySQL 8.x 会报时区错误这是新手最容易翻车的地方之一。第三close方法按 ResultSet、Statement、Connection 的顺序关闭和打开顺序相反。参数方面characterEncodingutf8保证中文商品名不乱码useSSLfalse在本地开发时避免证书警告。3. 核心业务代码订单创建与库存扣减怎么写到不出错3.1 商品管理模块的增删改查实现商品管理是整个系统的入口先把这块写扎实后面的订单模块才有数据可用。以查询和新增为例DAO 层的写法如下。public class ProductDAO { // 查询所有商品 public ListProduct findAll() { ListProduct list new ArrayList(); String sql SELECT id, name, price, stock, category FROM product ORDER BY id; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); p.setCategory(rs.getString(category)); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 新增商品 public boolean insert(Product p) { String sql INSERT INTO product(name, price, stock, category) VALUES(?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, p.getName()); ps.setBigDecimal(2, p.getPrice()); ps.setInt(3, p.getStock()); ps.setString(4, p.getCategory()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }这里用了 try-with-resources 语法连接和语句会自动关闭比手动在 finally 里关更简洁。findAll返回ListProductService 层拿到之后可以直接传给 UI 层展示。insert返回布尔值表示是否成功调用方根据返回值决定提示“添加成功”还是“添加失败”。参数设置用PreparedStatement的占位符不要用字符串拼接否则商品名里带单引号就会报 SQL 错误更严重的是会有 SQL 注入风险。3.2 创建订单的完整事务流程订单创建是整个系统最核心也最容易出问题的环节。它涉及三个操作插入订单主表、插入订单明细、扣减商品库存。这三个操作必须在一个事务里完成要么全成功要么全回滚。public class OrderService { public String createOrder(ListOrderItem items) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总金额并校验库存 BigDecimal total BigDecimal.ZERO; for (OrderItem item : items) { Product p queryProductForUpdate(conn, item.getProductId()); if (p.getStock() item.getQuantity()) { throw new RuntimeException(商品[ p.getName() ]库存不足当前库存 p.getStock()); } item.setSubtotal(p.getPrice().multiply(new BigDecimal(item.getQuantity()))); total total.add(item.getSubtotal()); } // 2. 插入订单主表 String orderNo generateOrderNo(); int orderId insertOrder(conn, orderNo, total); // 3. 插入订单明细并扣减库存 for (OrderItem item : items) { insertOrderItem(conn, orderId, item); deductStock(conn, item.getProductId(), item.getQuantity(), orderNo); } conn.commit(); // 全部成功才提交 return orderNo; } catch (Exception e) { if (conn ! null) conn.rollback(); // 任何一步失败都回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } // 用 FOR UPDATE 锁住行防止并发扣减导致超卖 private Product queryProductForUpdate(Connection conn, int productId) throws SQLException { String sql SELECT id, name, price, stock FROM product WHERE id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, productId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); return p; } throw new RuntimeException(商品不存在ID productId); } } } private String generateOrderNo() { return ORD System.currentTimeMillis() (int)(Math.random() * 900 100); } }这段代码有几个关键设计需要说清楚。第一conn.setAutoCommit(false)开启事务后所有 SQL 都在同一个连接上执行最后统一commit。第二查询商品时用了FOR UPDATE这是行级锁防止两个收银员同时卖同一件商品时出现超卖。第三订单号用时间戳加随机数生成保证唯一性的同时不需要额外查数据库。第四任何异常都会触发rollback保证不会出现“订单建了但库存没扣”或者“库存扣了但订单没建”的情况。参数方面items是购物车里的商品列表每个OrderItem包含productId和quantity。total用BigDecimal累加不要用double。generateOrderNo里的随机数范围是 100 到 999加上时间戳后重复概率极低课设场景完全够用。3.3 订单状态流转与查询订单创建后状态是“待支付”收银完成后要改成“已支付”取消订单则改成“已取消”。状态流转的代码不复杂但要注意只能从合法状态转换。public boolean updateOrderStatus(int orderId, int newStatus) { // 只允许 0-1支付或 0-2取消 String checkSql SELECT status FROM orders WHERE id ?; String updateSql UPDATE orders SET status ? WHERE id ? AND status 0; try (Connection conn DBUtil.getConnection(); PreparedStatement check conn.prepareStatement(checkSql); PreparedStatement update conn.prepareStatement(updateSql)) { check.setInt(1, orderId); try (ResultSet rs check.executeQuery()) { if (!rs.next()) return false; int current rs.getInt(status); if (current ! 0) { System.out.println(订单当前状态不允许此操作); return false; } } update.setInt(1, newStatus); update.setInt(2, orderId); return update.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }UPDATE语句里带了AND status 0条件这是一个乐观锁的思路只有当前状态确实是待支付时才会更新成功。如果两个人同时操作同一订单只有一个能成功另一个会返回 0 行受影响。这种写法比先查再改更安全因为查和改之间可能有其他操作插入。4. 避坑与排查课设答辩前必须检查的五个问题4.1 中文乱码从数据库到控制台一路排查现象商品名称在数据库里看是正常的但 Java 程序查出来变成问号或乱码。原因乱码可能出现在三个环节——数据库字符集、JDBC 连接字符集、控制台输出编码。任何一个环节不是 UTF-8 都会出问题。解决先确认数据库和表的字符集是utf8mb4然后检查 JDBC URL 里有没有characterEncodingutf8最后在 IDEA 的 Run Configuration 里加上-Dfile.encodingUTF-8虚拟机参数。三步都对齐之后乱码基本消失。4.2 库存扣成负数却没报错现象某个商品库存只剩 3 件但订单里买了 5 件系统居然创建成功了库存变成 -2。原因扣减库存的 SQL 写成了UPDATE product SET stock stock - ? WHERE id ?没有加库存判断条件。解决把 SQL 改成UPDATE product SET stock stock - ? WHERE id ? AND stock ?然后检查executeUpdate()的返回值。如果返回 0说明库存不足抛异常触发事务回滚。这样即使并发场景下也不会扣成负数。4.3 事务回滚了但库存还是被扣了现象订单创建过程中抛了异常订单表里没有新记录但商品库存已经减少了。原因扣库存用的 Connection 和订单插入用的不是同一个连接或者扣库存的操作在事务开启之前就执行了。解决确保所有数据库操作共用同一个Connection对象并且这个连接在操作开始前就调用了setAutoCommit(false)。检查代码里有没有在 Service 方法内部又调用了DBUtil.getConnection()获取新连接的情况这是最常见的翻车点。4.4 订单号重复导致插入失败现象偶尔报Duplicate entry ORD... for key order_no。原因订单号生成规则太简单比如只用时间戳同一毫秒内创建两个订单就会重复。解决在时间戳基础上加随机数或序列号或者直接用UUID.randomUUID().toString().replace(-, )作为订单号。如果已经建了唯一索引插入失败时捕获异常重新生成即可。4.5 答辩时被问“你的系统支持并发吗”怎么回答现象老师问并发场景下库存会不会超卖你心里没底。原因课设代码通常没考虑并发但老师可能就是想看你有没有这个意识。解决如实回答“当前版本用FOR UPDATE行锁保证了单机并发下不会超卖但在分布式环境下需要改用乐观锁或 Redis 预扣减”。然后可以补充一句“我在stock_log表里留了每次变动的记录方便对账”。这样既展示了你知道边界在哪也说明你留了排查手段。5. 从课设到面试把订单系统讲成你的项目亮点5.1 用订单状态机把业务逻辑串起来课设做完之后如果你想让这个项目在面试里也能用最好的办法是把它讲成一个“订单状态机”的故事。订单从创建到完成状态流转是待支付 → 已支付 → 已完成或者待支付 → 已取消。每个状态转换都有对应的业务动作和权限校验。你可以画一张状态转换表把每个转换的触发条件、涉及的表操作、失败处理都列清楚。面试官听到你用状态机来组织业务逻辑会觉得你是有设计意识的而不是随便堆代码。当前状态触发动作目标状态涉及操作待支付收银结算已支付更新订单状态、记录支付时间待支付取消订单已取消更新订单状态、回滚库存已支付申请退款已取消更新状态、回滚库存、记录退款流水已支付完成交易已完成更新状态、记录完成时间这张表可以直接放在你的课设报告里也可以在面试时口述。关键是要能说清楚每个转换背后的数据一致性怎么保证。5.2 用 stock_log 做库存对账的验证方法很多人写完扣库存就结束了但你可以多做一步写一个对账方法把product表的当前库存和stock_log表里的流水汇总做比对。public void checkStockConsistency() { String sql SELECT p.id, p.name, p.stock, COALESCE(SUM(s.change_amount), 0) AS log_total FROM product p LEFT JOIN stock_log s ON p.id s.product_id GROUP BY p.id, p.name, p.stock HAVING p.stock COALESCE(SUM(s.change_amount), 0); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { boolean found false; while (rs.next()) { found true; System.out.println(库存不一致商品 rs.getString(name) 当前库存 rs.getInt(stock) 流水汇总 rs.getInt(log_total)); } if (!found) System.out.println(库存对账通过无差异); } catch (SQLException e) { e.printStackTrace(); } }这个方法在答辩时是一个很好的加分项。你可以当场演示先正常下单然后手动改一下库存再跑对账方法立刻就能发现差异。面试官或老师看到你有主动验证数据一致性的意识印象分会高很多。5.3 我踩过的那些坑和后来养成的习惯做第一个课设版本的时候我把所有 SQL 都写在 Service 里一个方法几百行改一个字段要翻半天。后来我强迫自己把 DAO 层抽干净每个方法只做一件事代码立刻好维护了。还有一个坑是事务我一开始不知道setAutoCommit(false)之后必须手动commit结果数据一直不落库查了半天以为是 SQL 写错了。现在我的习惯是任何涉及两张以上表的写操作先画一遍事务边界确认所有操作在同一个 Connection 上再动手写代码。另外stock_log这张表是我后来加的加完之后排查库存问题从“靠猜”变成了“查流水”省了很多时间。希望这些经验帮到你少走几个我走过的弯路。本文还有配套的精品资源点击获取