☰
Java web超市管理系统课设:数据库设计与事务处理实战
2026/9/28 1:03:21 网站建设 项目流程

简介:这是一套基于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 或 Servlet+JSP,而不是 Spring Boot 全家桶

课设的评分点通常压在「数据库设计」和「业务逻辑」上,不是框架新旧。我一般推荐两条路线:纯 Servlet+JSP+JDBC,或者 SSM(Spring+SpringMVC+MyBatis)。前者代码量大但每一步都看得见,答辩时老师问「这个连接怎么来的」你能答;后者开发快、分层清晰,适合想顺便练框架的人。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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 供应商表 CREATE TABLE supplier ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表:库存直接挂在商品上,简化课设模型 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会员表 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表逻辑说明:product.stock直接存库存,是为了课设阶段避免再拆库存流水表,答辩时能讲清「简化模型」即可。orders.order_no加唯一索引,防止重复提交生成同号订单。所有金额字段用DECIMAL而不是FLOAT,这是数据库课设的经典考点——浮点精度问题在收银场景会直接导致对账差几分钱。外键约束建议保留,虽然有些生产环境会去掉,但课设里它是体现「参照完整性」的加分项。

参数上注意两点:字符集统一utf8mb4,否则商品名带 emoji 或生僻字会乱码;存储引擎用InnoDB,因为要事务支持,MyISAM 不支持回滚,扣库存失败就麻烦了。

2.3 连接池配置:别再用 DriverManager 裸连

课设里最常见的翻车是每个 Servlet 里DriverManager.getConnection(),请求一多就卡死。正确做法是配一个连接池。下面用 Druid 举例,druid.properties放src下:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000
public 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(); } }

initialSize=5是启动时建 5 个连接,maxActive=20是并发上限,maxWait=3000表示拿不到连接最多等 3 秒就抛异常。这三个参数答辩常被问,记住含义即可。用连接池后,每次getConn()拿到的连接用完必须close(),但这里的 close 是归还而不是真关闭,这是 Druid 的机制,别被名字骗了。

3. 核心业务实现:收银下单与库存扣减的事务处理

3.1 下单接口:一个事务里完成插订单、插明细、扣库存

超市系统最核心的业务就是收银:选商品、算总价、生成订单、扣库存、加会员积分。这五步必须在一个事务里,任何一步失败全部回滚,否则会出现「订单生成了但库存没扣」的脏数据。下面用 JDBC 手写事务,SSM 用户把这段逻辑搬到 Service 层加@Transactional即可。

public boolean createOrder(Order order, List<OrderItem> 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 没带characterEncoding=utf8、JSP 页面没设pageEncoding。解决顺序是先从数据库查起:SHOW VARIABLES LIKE 'character%';确认character_set_server是utf8mb4,不是就改my.ini重启。然后检查连接 URL,最后在 JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-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-finally,finally里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密码别写死在代码里,用配置文件,这是老师最爱问的安全点。这套东西我从第一次带课设翻车到现在能稳定交付,靠的就是把事务和约束这两块抠死。希望帮到你。

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

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

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

立即咨询