简介:这是一套面向高校计算机相关专业学生的期末数据库课程设计完整方案,以Java+MySQL+JDBC+JavaSwing技术栈实现超市管理系统,适合正在准备课程设计、需要可运行参考项目的初学者与进阶学习者。资源包共24个文件,包含17个Java源码文件、1个SQL建库脚本、2个依赖jar包及pom.xml、mvn-settings.xml等配置文档,压缩包约1.1MB,采用Maven结构组织,源码与数据库脚本配套齐全,导入后即可对照学习。项目基于JDK12与MySQL8.0开发,编码为GBK,涵盖商品管理、库存、销售等典型业务模块,并附有UI美化方案,通过UIManager设置系统外观风格,便于理解Swing界面统一美化的实现思路。目前已有217人学习下载,读者可从中获取完整的数据库表设计、JDBC连接与增删改查实现、Swing事件处理与界面布局等实战经验,适合作为课程设计参考或二次开发基础。
1. 从一份超市管理系统课设说起:Java+MySQL+JDBC+Swing 到底能跑出什么
每年期末,数据库课程设计这个环节总能把人逼到墙角。题目发下来一看——“超市管理系统”,要求用 Java 连 MySQL,做个带界面的增删改查。听起来不难,真动手才发现:数据库表怎么设计才不被导师打回?JDBC 连接池要不要上?Swing 的表格刷新为什么总是慢半拍?更别提商品库存和销售记录之间的联动,稍微一改就报外键约束错误。
这套Java+MySQL+JDBC+JavaSwing 超市管理系统,本质上是一个典型的 C/S 桌面应用:Swing 负责前端交互,JDBC 做数据通道,MySQL 存业务数据。它解决的核心问题是——让你在一个学期内,把“数据库设计 + Java 后端逻辑 + 桌面 UI”这三件事串起来,而不是各学各的。适合谁?正在做数据库课设的本科生、想补一个完整 CRUD 项目练手的 Java 新手,以及需要快速搭一个进销存 Demo 的开发者。
我见过太多人卡在“能连上数据库但界面不刷新”或者“删了商品但库存没回滚”这种坑里。下面按实际落地顺序,从建库建表一路讲到 Swing 事件绑定和事务处理,中间穿插我踩过的血泪教训。
2. 数据库设计先定骨架:五张表撑起超市进销存
2.1 商品、库存、销售、用户、供应商——最小可用表结构
超市管理系统的核心实体就五个:商品、库存、销售记录、用户、供应商。别一上来就搞十几张表,课设阶段五张足够跑通全流程。我一般会这样设计:
- 商品表(product):商品 ID、名称、条码、分类、进价、售价、供应商 ID
- 库存表(inventory):库存 ID、商品 ID、当前数量、预警阈值、最后更新时间
- 销售表(sale):销售 ID、商品 ID、数量、单价、总价、销售时间、操作员 ID
- 用户表(user):用户 ID、用户名、密码哈希、角色(管理员/收银员)
- 供应商表(supplier):供应商 ID、名称、联系人、电话
这里有个关键决策:库存是单独一张表还是直接放在商品表里?我建议单独拆出来。原因很简单——商品信息相对静态,库存变动频繁,拆开后更新库存不会锁住商品表的读操作。而且库存预警阈值这种字段,放在商品表里会让表越来越臃肿。
建表 SQL 如下,注意字符集用 utf8mb4,否则商品名里的生僻字会变问号:
-- 创建数据库,指定字符集 CREATE DATABASE supermarket_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE supermarket_db; -- 供应商表 CREATE TABLE supplier ( supplier_id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(100) NOT NULL, contact_person VARCHAR(50), phone VARCHAR(20), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 商品表,外键关联供应商 CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, barcode VARCHAR(50) UNIQUE, category VARCHAR(50), purchase_price DECIMAL(10,2) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, supplier_id INT, FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ) ENGINE=InnoDB; -- 库存表,与商品一对一 CREATE TABLE inventory ( inventory_id INT PRIMARY KEY AUTO_INCREMENT, product_id INT UNIQUE NOT NULL, quantity INT NOT NULL DEFAULT 0, warn_threshold INT DEFAULT 10, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES product(product_id) ) ENGINE=InnoDB; -- 用户表 CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(128) NOT NULL, role ENUM('admin','cashier') DEFAULT 'cashier' ) ENGINE=InnoDB; -- 销售记录表 CREATE TABLE sale ( sale_id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL, total_price DECIMAL(10,2) NOT NULL, sale_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, operator_id INT, FOREIGN KEY (product_id) REFERENCES product(product_id), FOREIGN KEY (operator_id) REFERENCES user(user_id) ) ENGINE=InnoDB;参数说明:DECIMAL(10,2)用于金额,避免 float 精度丢失;ON UPDATE CURRENT_TIMESTAMP让库存表自动记录最后变动时间;ENUM限制角色只有两种,防止脏数据。外键约束在课设阶段建议保留,虽然插入顺序有要求,但能帮你理解参照完整性。
2.2 用 JDBC 封装数据库连接:别把 DriverManager 散落各处
很多课设代码里,每个 DAO 方法都写一遍DriverManager.getConnection(),结果就是改个数据库密码要翻遍所有文件。正确做法是抽一个DBUtil类,把连接获取和释放统一管起来。
import java.sql.*; public class DBUtil { // 数据库配置,实际项目建议放配置文件 private static final String URL = "jdbc:mysql://localhost:3306/supermarket_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; 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); } // 统一关闭资源,顺序:ResultSet -> Statement -> Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明:静态块保证驱动只加载一次;close方法按“后开先关”顺序释放,避免资源泄漏。参数上,serverTimezone必须指定,否则 MySQL 8 会报时区错误;characterEncoding=utf8配合数据库的 utf8mb4 使用。
提示:课设阶段用 DriverManager 足够,但如果要模拟并发收银,建议了解连接池(如 HikariCP),否则频繁开关连接会让界面卡顿。
3. 用 JDBC 把增删改查写扎实:DAO 层与事务边界
3.1 商品 DAO 的四个方法:从插入到模糊查询
DAO(Data Access Object)层的职责很纯粹——把 SQL 执行结果映射成 Java 对象。以商品为例,我一般写四个方法:插入、按 ID 删除、更新价格、按名称模糊查询。
import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDAO { // 插入商品,返回自增主键 public int insert(Product p) throws SQLException { String sql = "INSERT INTO product(product_name, barcode, category, purchase_price, sale_price, supplier_id) VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, p.getProductName()); ps.setString(2, p.getBarcode()); ps.setString(3, p.getCategory()); ps.setBigDecimal(4, p.getPurchasePrice()); ps.setBigDecimal(5, p.getSalePrice()); ps.setInt(6, p.getSupplierId()); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) return rs.getInt(1); } } return -1; } // 按名称模糊查询 public List<Product> searchByName(String keyword) throws SQLException { String sql = "SELECT * FROM product WHERE product_name LIKE ?"; List<Product> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setProductId(rs.getInt("product_id")); p.setProductName(rs.getString("product_name")); p.setSalePrice(rs.getBigDecimal("sale_price")); list.add(p); } } } return list; } }参数说明:Statement.RETURN_GENERATED_KEYS让插入后能拿到自增 ID,方便后续给库存表插记录;LIKE ?配合%keyword%实现模糊匹配,注意不要用字符串拼接,否则会有 SQL 注入风险。try-with-resources自动关闭资源,比手动 close 更安全。
3.2 销售扣库存必须用事务:一个 Connection 贯穿到底
销售操作涉及两张表:往sale插一条记录,同时更新inventory的 quantity。这两步必须原子执行——要么都成功,要么都回滚。我见过有人用两个独立连接分别操作,结果销售记录插入了但库存没扣,盘点时对不上账。
public boolean sellProduct(int productId, int qty, int operatorId) throws SQLException { String insertSale = "INSERT INTO sale(product_id, quantity, unit_price, total_price, operator_id) VALUES(?,?,?,?,?)"; String updateStock = "UPDATE inventory SET quantity = quantity - ? WHERE product_id = ? AND quantity >= ?"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 查售价 BigDecimal price; try (PreparedStatement ps = conn.prepareStatement("SELECT sale_price FROM product WHERE product_id=?")) { ps.setInt(1, productId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) throw new SQLException("商品不存在"); price = rs.getBigDecimal("sale_price"); } } // 扣库存,带条件防止超卖 try (PreparedStatement ps = conn.prepareStatement(updateStock)) { ps.setInt(1, qty); ps.setInt(2, productId); ps.setInt(3, qty); int rows = ps.executeUpdate(); if (rows == 0) throw new SQLException("库存不足"); } // 插销售记录 try (PreparedStatement ps = conn.prepareStatement(insertSale)) { ps.setInt(1, productId); ps.setInt(2, qty); ps.setBigDecimal(3, price); ps.setBigDecimal(4, price.multiply(BigDecimal.valueOf(qty))); ps.setInt(5, operatorId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明:setAutoCommit(false)开启事务后,所有操作在同一个连接上执行;UPDATE ... WHERE quantity >= ?这个条件很关键,它在数据库层面防止了超卖——如果库存不够,更新影响行数为 0,直接抛异常回滚。参数上,price.multiply(BigDecimal.valueOf(qty))用 BigDecimal 做乘法,避免精度问题。
注意:事务里不要做耗时操作(比如弹窗让用户确认),否则会长时间持有锁。Swing 的事件线程和数据库操作要分开,后面会讲。
4. Swing 界面与 JDBC 的配合:表格刷新、事件绑定与线程
4.1 用 JTable 展示商品列表:TableModel 比直接塞数据靠谱
Swing 的JTable是课设里用得最多的组件,但很多人直接new JTable(Object[][], String[]),结果数据一多就乱,排序和刷新也麻烦。正确做法是自定义AbstractTableModel,把数据源和视图解耦。
import javax.swing.table.AbstractTableModel; import java.util.List; public class ProductTableModel extends AbstractTableModel { private final String[] columns = {"ID", "名称", "条码", "售价", "库存"}; private List<Product> data; public ProductTableModel(List<Product> data) { this.data = data; } // 刷新数据,通知表格重绘 public void refresh(List<Product> newData) { this.data = newData; fireTableDataChanged(); } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columns.length; } @Override public String getColumnName(int col) { return columns[col]; } @Override public Object getValueAt(int row, int col) { Product p = data.get(row); switch (col) { case 0: return p.getProductId(); case 1: return p.getProductName(); case 2: return p.getBarcode(); case 3: return p.getSalePrice(); case 4: return p.getStock(); // 库存字段 default: return null; } } }逻辑说明:fireTableDataChanged()会通知 JTable 整个数据变了,触发重绘。参数上,getValueAt里按列索引返回对应字段,注意类型要匹配,否则表格显示会异常。用 TableModel 的好处是,查询结果直接替换data再刷新,不用手动清空表格行。
4.2 按钮点击到数据库更新:SwingWorker 防止界面假死
Swing 的事件分发线程(EDT)负责界面绘制和事件响应。如果你在按钮的actionPerformed里直接调 JDBC,数据库一慢,整个界面就卡住不动。我踩过这个坑——查询几千条销售记录时,窗口直接“无响应”,用户以为死机了。
正确做法是用SwingWorker把数据库操作放到后台线程,完成后在 EDT 里更新 UI。
searchButton.addActionListener(e -> { String keyword = keywordField.getText().trim(); new SwingWorker<List<Product>, Void>() { @Override protected List<Product> doInBackground() throws Exception { // 后台线程执行查询 return new ProductDAO().searchByName(keyword); } @Override protected void done() { try { List<Product> result = get(); tableModel.refresh(result); } catch (Exception ex) { JOptionPane.showMessageDialog(null, "查询失败:" + ex.getMessage()); } } }.execute(); });逻辑说明:doInBackground在后台线程跑,返回结果;done在 EDT 里执行,可以安全更新表格。参数上,get()会阻塞直到后台完成,但因为在done里调用,不会卡界面。异常通过get()抛出,统一弹窗提示。
提示:如果只是简单查询,数据量小,直接调也行。但养成 SwingWorker 的习惯,后期加导出 Excel 或批量导入时不会翻车。
5. 避坑与排查:课设里最容易翻车的五个地方
5.1 中文乱码:从数据库到 Swing 的完整链路
现象:商品名在数据库里正常,但 Swing 表格显示问号。原因:JDBC URL 没指定characterEncoding=utf8,或者数据库/表字符集不是 utf8mb4。解决:URL 加characterEncoding=utf8;建库时指定DEFAULT CHARACTER SET utf8mb4;Swing 组件用默认字体即可,不要手动设成不支持中文的字体。
5.2 外键约束报错:插入顺序和删除顺序
现象:插入商品时报Cannot add or update a child row。原因:商品表的外键supplier_id引用了不存在的供应商。解决:先插供应商,再插商品;删除时先删销售记录和库存,再删商品,最后删供应商。或者建表时加ON DELETE CASCADE,但课设阶段建议手动控制顺序,理解参照完整性。
5.3 库存扣成负数:并发下的超卖
现象:两个收银员同时卖同一商品,库存扣到 -1。原因:先查库存再更新,两步之间没有锁。解决:用UPDATE inventory SET quantity = quantity - ? WHERE product_id = ? AND quantity >= ?,把判断和更新合并成一条原子 SQL,影响行数为 0 就回滚。
5.4 表格数据不刷新:忘了 fireTableDataChanged
现象:数据库里新增了商品,但 JTable 还是旧数据。原因:直接改了List但没通知 TableModel。解决:调用tableModel.refresh(newList),内部触发fireTableDataChanged()。不要用table.repaint(),那只重绘不重读数据。
5.5 连接泄漏:Connection 没关导致 MySQL 连接数满
现象:程序运行一段时间后报Too many connections。原因:异常路径下Connection没关闭。解决:统一用try-with-resources,或者在finally里关闭。DBUtil.close方法要确保每个资源都单独 try-catch,一个关闭失败不影响其他。
6. 进阶技巧:用存储过程做库存预警和销售统计
课设如果只做增删改查,分数很难拔高。我一般会加一个存储过程,把库存预警和销售统计放到数据库层,既展示 SQL 能力,又减少 Java 代码量。
-- 存储过程:查询低于预警阈值的商品 DELIMITER // CREATE PROCEDURE check_low_stock() BEGIN SELECT p.product_name, i.quantity, i.warn_threshold FROM inventory i JOIN product p ON i.product_id = p.product_id WHERE i.quantity < i.warn_threshold; END // DELIMITER ; -- 存储过程:按日期统计销售额 DELIMITER // CREATE PROCEDURE daily_sales(IN start_date DATE, IN end_date DATE) BEGIN SELECT DATE(sale_time) AS sale_date, COUNT(*) AS order_count, SUM(total_price) AS total_amount FROM sale WHERE DATE(sale_time) BETWEEN start_date AND end_date GROUP BY DATE(sale_time) ORDER BY sale_date; END // DELIMITER ;Java 侧用CallableStatement调用:
public List<Object[]> getLowStock() throws SQLException { List<Object[]> list = new ArrayList<>(); String sql = "{call check_low_stock()}"; try (Connection conn = DBUtil.getConnection(); CallableStatement cs = conn.prepareCall(sql); ResultSet rs = cs.executeQuery()) { while (rs.next()) { list.add(new Object[]{ rs.getString("product_name"), rs.getInt("quantity"), rs.getInt("warn_threshold") }); } } return list; }参数说明:CallableStatement的 SQL 用{call 过程名(参数)}格式;daily_sales接收两个日期参数,调用时用cs.setDate(1, java.sql.Date.valueOf("2025-01-01"))传参。存储过程的好处是统计逻辑在数据库端执行,网络传输量小,但调试不如 Java 方便,建议先在 MySQL 客户端里跑通再集成。
验证方法:插入几条低于阈值的库存记录,调用check_low_stock看是否返回;销售几笔后调daily_sales核对金额。如果结果不对,先单独在 MySQL 里执行存储过程,排除 Java 调用问题。
我自己的习惯是,课设答辩前一定把存储过程、事务、SwingWorker 这三个点各准备一个演示场景——因为导师最爱问“并发怎么办”“界面卡不卡”“数据一致性怎么保证”。把这三点讲清楚,分数不会低。希望帮到你。
本文还有配套的精品资源,点击获取