简介这是一套基于Java Web的超市管理系统完整项目资料面向计算机相关专业的在校学生、课程设计或毕业设计需求者以及希望以真实项目练手的学习者。项目已通过导师指导与答辩评审功能经测试可正常运行适合直接用于数据库课程设计、作业提交或项目初期立项演示。压缩包共125个文件约3.05MB以30个Java源文件、20个JSP页面、23个JavaScript脚本为核心配合XML配置、CSS样式、properties配置及图片资源并附带war包与说明文档覆盖从后端逻辑到前端交互的完整实现。目前已有40人学习关注。读者可从中获得可运行的源码工程、清晰的目录结构、数据库设计思路与前后端整合范例既能直接参考完成课设也可在现有代码基础上修改扩展实现商品管理、订单处理等更多功能是兼顾学习与实战的优质参考。1. 从一份超市管理系统课设说起Java web 数据库到底要交付什么每年一到期末总有人被「基于 Java web 的超市管理系统数据库课程设计」这个题目卡住。表面看它只是个增删改查练习真动手才发现商品、库存、订单、会员、供应商五张表一关联事务、外键、并发扣减全冒出来写错一行库存就对不上账。我带过几届课设翻车最多的不是 Java 语法而是数据库设计——表建好了业务跑不通。这个方向适合两类人一是要交数据库课程设计、需要一份能跑通、能答辩、文档齐全的项目二是刚学完 Servlet/JSP 或 SSM想找一个业务闭环完整、表关系够复杂的练手项目。它解决的核心问题是把「数据库原理」课本里的范式、事务、索引、视图落到一个真实的超市进销存场景里。下面我按自己带课设的路径把选型、建库、写接口、避坑、加分项一层层拆开你照着做就能复现一套能拿高分的系统。2. 技术选型与数据库设计先定架构再动手2.1 为什么课设首选 SSM 或 ServletJSP而不是 Spring Boot 全家桶课设的评分点通常压在「数据库设计」和「业务逻辑」上不是框架新旧。我一般推荐两条路线纯 ServletJSPJDBC或者 SSMSpringSpringMVCMyBatis。前者代码量大但每一步都看得见答辩时老师问「这个连接怎么来的」你能答后者开发快、分层清晰适合想顺便练框架的人。Spring Boot 不是不能用但它把太多东西自动化了课设答辩容易被追问「你数据库连接池怎么配的」时答不上来。数据库统一选 MySQL 8.x理由是课程设计环境普遍装的就是它Navicat、DataGrip 都能连mysql-connector-java驱动成熟。热搜里常出现的「mysql 的数据库连接池」「数据库增删改查」正是这个项目的核心考点连接池用 Druid 或 HikariCP 都行课设里手写一个简易连接池反而更能体现对原理的理解。选型定下来后先画 ER 图再建表别急着写代码。超市系统的实体关系不复杂但容易漏一个订单对应多个订单明细一个商品属于一个分类、可能来自多个供应商会员和订单是一对多。漏掉中间表后面库存和订单就对不上。2.2 核心表结构7 张表撑起进销存闭环下面是我常用的建表脚本覆盖商品、分类、供应商、库存、订单、订单明细、会员。字段类型和约束都按课设答辩能讲清楚的原则来定。-- 商品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 供应商表 CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表库存直接挂在商品上简化课设模型 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, supplier_id INT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, unit VARCHAR(10) DEFAULT 件, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, FOREIGN KEY (category_id) REFERENCES category(id), FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 会员表 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, points INT DEFAULT 0, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, member_id INT, total_amount DECIMAL(10,2) NOT NULL, pay_type TINYINT COMMENT 1现金 2扫码 3会员卡, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES member(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑说明product.stock直接存库存是为了课设阶段避免再拆库存流水表答辩时能讲清「简化模型」即可。orders.order_no加唯一索引防止重复提交生成同号订单。所有金额字段用DECIMAL而不是FLOAT这是数据库课设的经典考点——浮点精度问题在收银场景会直接导致对账差几分钱。外键约束建议保留虽然有些生产环境会去掉但课设里它是体现「参照完整性」的加分项。参数上注意两点字符集统一utf8mb4否则商品名带 emoji 或生僻字会乱码存储引擎用InnoDB因为要事务支持MyISAM 不支持回滚扣库存失败就麻烦了。2.3 连接池配置别再用 DriverManager 裸连课设里最常见的翻车是每个 Servlet 里DriverManager.getConnection()请求一多就卡死。正确做法是配一个连接池。下面用 Druid 举例druid.properties放src下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password你的密码 initialSize5 maxActive20 maxWait3000public class DruidUtil { private static DataSource ds; static { try { Properties p new Properties(); p.load(DruidUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); ds DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败); } } public static Connection getConn() throws SQLException { return ds.getConnection(); } }initialSize5是启动时建 5 个连接maxActive20是并发上限maxWait3000表示拿不到连接最多等 3 秒就抛异常。这三个参数答辩常被问记住含义即可。用连接池后每次getConn()拿到的连接用完必须close()但这里的 close 是归还而不是真关闭这是 Druid 的机制别被名字骗了。3. 核心业务实现收银下单与库存扣减的事务处理3.1 下单接口一个事务里完成插订单、插明细、扣库存超市系统最核心的业务就是收银选商品、算总价、生成订单、扣库存、加会员积分。这五步必须在一个事务里任何一步失败全部回滚否则会出现「订单生成了但库存没扣」的脏数据。下面用 JDBC 手写事务SSM 用户把这段逻辑搬到 Service 层加Transactional即可。public boolean createOrder(Order order, ListOrderItem items) { Connection conn null; PreparedStatement psOrder null, psItem null, psStock null; try { conn DruidUtil.getConn(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表拿回自增主键 String sqlOrder INSERT INTO orders(order_no,member_id,total_amount,pay_type) VALUES(?,?,?,?); psOrder conn.prepareStatement(sqlOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, order.getOrderNo()); psOrder.setInt(2, order.getMemberId()); psOrder.setBigDecimal(3, order.getTotalAmount()); psOrder.setInt(4, order.getPayType()); psOrder.executeUpdate(); ResultSet rs psOrder.getGeneratedKeys(); int orderId 0; if (rs.next()) orderId rs.getInt(1); // 2. 循环插入明细同时扣库存 String sqlItem INSERT INTO order_item(order_id,product_id,quantity,price) VALUES(?,?,?,?); String sqlStock UPDATE product SET stock stock - ? WHERE id ? AND stock ?; psItem conn.prepareStatement(sqlItem); psStock conn.prepareStatement(sqlStock); for (OrderItem it : items) { psItem.setInt(1, orderId); psItem.setInt(2, it.getProductId()); psItem.setInt(3, it.getQuantity()); psItem.setBigDecimal(4, it.getPrice()); psItem.addBatch(); // 带 stock ? 条件防止超卖 psStock.setInt(1, it.getQuantity()); psStock.setInt(2, it.getProductId()); psStock.setInt(3, it.getQuantity()); if (psStock.executeUpdate() 0) { throw new RuntimeException(库存不足商品ID it.getProductId()); } } psItem.executeBatch(); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { // 归还连接连接池会自动重置 autoCommit if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) {} } }逻辑说明setAutoCommit(false)是事务起点commit()是终点catch里rollback()是后悔药。扣库存的 SQL 用了stock ?作为更新条件这是防超卖的关键——如果库存只剩 2 件你买 3 件executeUpdate()返回 0直接抛异常回滚不会出现负库存。这个写法比「先查再扣」安全因为查和扣之间可能有并发。参数说明Statement.RETURN_GENERATED_KEYS让插入后能拿到自增订单 ID明细表要用它做外键。addBatch()和executeBatch()是批量插入明细多的时候比逐条快。注意finally里要把autoCommit改回true再归还否则连接池里这个连接下次被拿到还是手动提交模式会出玄学问题。3.2 商品分页查询用 LIMIT 和索引把响应压下来后台商品列表动辄几百条一次全查出来页面卡死。分页是课设必做项也是「数据库增删改查」里最能体现 SQL 功底的地方。-- 第 page 页每页 size 条page 从 1 开始 SELECT p.id, p.name, c.name AS category, p.price, p.stock, p.status FROM product p LEFT JOIN category c ON p.category_id c.id WHERE p.status 1 ORDER BY p.id DESC LIMIT ?, ?;int offset (page - 1) * size; ps.setInt(1, offset); ps.setInt(2, size);LIMIT offset, size里 offset 是跳过的行数。深分页时LIMIT 10000, 10会扫描前 10000 行再丢弃性能差课设数据量小无所谓但答辩时能说出「可以用游标或子查询优化」就是加分项。给product.category_id和product.status建联合索引查询会走索引而不是全表扫。CREATE INDEX idx_category_status ON product(category_id, status);索引不是越多越好每个索引都会拖慢插入和更新。课设里给外键列和常用查询条件列建索引就够了别给name这种模糊查询字段建因为LIKE %xx%用不上索引。3.3 会员积分与订单联动一个容易漏掉的更新下单成功后给会员加积分规则一般是消费 1 元积 1 分。这一步很多人忘了放进事务导致订单成功但积分没加。正确做法是在createOrder的commit()之前追加一条更新String sqlPoints UPDATE member SET points points ? WHERE id ?; psPoints conn.prepareStatement(sqlPoints); psPoints.setInt(1, order.getTotalAmount().intValue()); psPoints.setInt(2, order.getMemberId()); psPoints.executeUpdate();注意total_amount是BigDecimal转int会丢小数课设里可以按「满 1 元积 1 分」取整或者用FLOOR函数在 SQL 里算。会员 ID 为空散客时要跳过这步否则WHERE id null更新 0 行不报错但也没积分属于静默失败调试时容易懵。4. 避坑与排查课设答辩前必须过的 5 道坎4.1 中文乱码从数据库到 JSP 一条链都要查现象商品名存进去是???或者页面显示问号。原因通常有三层数据库字符集不是utf8mb4、JDBC URL 没带characterEncodingutf8、JSP 页面没设pageEncoding。解决顺序是先从数据库查起SHOW VARIABLES LIKE character%;确认character_set_server是utf8mb4不是就改my.ini重启。然后检查连接 URL最后在 JSP 顶部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。三层都对了才不会乱码。4.2 外键约束导致删不掉数据现象删除分类时报Cannot delete or update a parent row。原因是这个分类下还有商品外键挡住了。解决有两种一是先删子表数据再删父表二是建表时加ON DELETE CASCADE级联删除。课设里我建议用第一种手动控制删除顺序答辩时能讲清「参照完整性」的意义。级联删除虽然方便但误删分类会连带删掉所有商品风险大。4.3 事务没生效Service 层方法不是 public现象SSM 项目里加了Transactional但回滚不生效。原因常见两个方法不是public或者同类内部方法直接调用this.xxx()绕过了代理。Spring 的事务靠 AOP 代理实现私有方法和自调用都不走代理。解决是把事务方法改成public自调用场景通过注入自身或拆到另一个 Service 里调用。4.4 连接池耗尽连接忘了归还现象系统跑一会儿就报GetConnectionTimeoutException。原因是某处getConn()后没close()连接被占满。排查方法是看 Druid 监控页配了stat-view-servlet的话访问/druid能看到活跃连接数。解决是把所有数据库操作放进try-finallyfinally里close()。用 MyBatis 的话它自动管理但手写 JDBC 必须自己管。4.5 订单号重复并发下的唯一索引冲突现象快速点两次提交报Duplicate entry for key order_no。原因是订单号用时间戳生成同一毫秒内两次请求撞号。解决是订单号加随机后缀或用户 ID比如时间戳 用户ID 3位随机数同时数据库order_no的唯一索引保留作为最后一道防线。前端也要做按钮防重复点击但后端唯一约束才是根本。5. 让课设拿高分的三个进阶技巧第一个技巧是用视图简化复杂查询。后台常要展示「订单 会员名 商品明细」三表关联写起来长建一个视图v_order_detail把常用字段拼好查询时直接SELECT * FROM v_order_detail WHERE order_id ?。视图在数据库课设里是明确考点用上就是加分项但注意视图不存数据性能取决于底层查询。CREATE VIEW v_order_detail AS SELECT o.order_no, o.create_time, m.name AS member_name, p.name AS product_name, oi.quantity, oi.price FROM orders o LEFT JOIN member m ON o.member_id m.id JOIN order_item oi ON oi.order_id o.id JOIN product p ON p.id oi.product_id;第二个技巧是加一个库存预警查询用HAVING或子查询找出库存低于阈值的商品这是超市系统的真实需求答辩时能体现业务理解SELECT name, stock FROM product WHERE stock 10 AND status 1 ORDER BY stock ASC;第三个技巧是准备一份数据字典文档。课设评分里「详细文档」占分不低把每张表的字段、类型、约束、说明整理成表格比只交代码强得多。下面是我常用的字段说明格式字段名类型约束说明idINT主键自增商品唯一标识nameVARCHAR(100)非空商品名称priceDECIMAL(10,2)非空默认0单价两位小数stockINT非空默认0当前库存最后说个血泪经验答辩前一定把数据库导出成.sql文件连同项目一起打包。我见过太多人换台电脑演示数据库没导现场建表建到一半老师就喊停了。还有root密码别写死在代码里用配置文件这是老师最爱问的安全点。这套东西我从第一次带课设翻车到现在能稳定交付靠的就是把事务和约束这两块抠死。希望帮到你。本文还有配套的精品资源点击获取