多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java+MySQL+JDBC+Swing超市管理系统课设实战:从建表到事务与界面刷新

Java+MySQL+JDBC+Swing超市管理系统课设实战:从建表到事务与界面刷新 简介这是一套面向高校计算机相关专业学生的期末数据库课程设计完整方案以JavaMySQLJDBCJavaSwing技术栈实现超市管理系统适合正在准备课程设计、需要可运行参考项目的初学者与进阶学习者。资源包共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. 从一份超市管理系统课设说起JavaMySQLJDBCSwing 到底能跑出什么每年期末数据库课程设计这个环节总能把人逼到墙角。题目发下来一看——“超市管理系统”要求用 Java 连 MySQL做个带界面的增删改查。听起来不难真动手才发现数据库表怎么设计才不被导师打回JDBC 连接池要不要上Swing 的表格刷新为什么总是慢半拍更别提商品库存和销售记录之间的联动稍微一改就报外键约束错误。这套JavaMySQLJDBCJavaSwing 超市管理系统本质上是一个典型的 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 ) ENGINEInnoDB; -- 商品表外键关联供应商 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) ) ENGINEInnoDB; -- 库存表与商品一对一 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) ) ENGINEInnoDB; -- 用户表 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 ) ENGINEInnoDB; -- 销售记录表 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) ) ENGINEInnoDB;参数说明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?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); } // 统一关闭资源顺序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 会报时区错误characterEncodingutf8配合数据库的 utf8mb4 使用。提示课设阶段用 DriverManager 足够但如果要模拟并发收银建议了解连接池如 HikariCP否则频繁开关连接会让界面卡顿。3. 用 JDBC 把增删改查写扎实DAO 层与事务边界3.1 商品 DAO 的四个方法从插入到模糊查询DAOData 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 ListProduct searchByName(String keyword) throws SQLException { String sql SELECT * FROM product WHERE product_name LIKE ?; ListProduct 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 ListProduct data; public ProductTableModel(ListProduct data) { this.data data; } // 刷新数据通知表格重绘 public void refresh(ListProduct 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 SwingWorkerListProduct, Void() { Override protected ListProduct doInBackground() throws Exception { // 后台线程执行查询 return new ProductDAO().searchByName(keyword); } Override protected void done() { try { ListProduct 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 没指定characterEncodingutf8或者数据库/表字符集不是 utf8mb4。解决URL 加characterEncodingutf8建库时指定DEFAULT CHARACTER SET utf8mb4Swing 组件用默认字体即可不要手动设成不支持中文的字体。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 ListObject[] getLowStock() throws SQLException { ListObject[] 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 这三个点各准备一个演示场景——因为导师最爱问“并发怎么办”“界面卡不卡”“数据一致性怎么保证”。把这三点讲清楚分数不会低。希望帮到你。本文还有配套的精品资源点击获取
返回列表