☰
Java+MySQL+JDBC+Swing实现超市管理系统:数据库课设完整实战指南
2026/9/26 11:27:36 网站建设 项目流程

简介:面向期末数据库课程设计,提供基于Java+MySQL+JDBC+JavaSwing的超市管理系统完整源码与数据库脚本,适合高校学生完成课程设计或学习桌面数据库应用开发。项目采用JDK12与MySQL8.0环境,覆盖商品管理、销售记录、库存维护等典型业务模块,可帮助理解JavaSwing界面搭建与JDBC数据访问的完整流程。压缩包共24个文件,以17个Java源文件为主,辅以SQL初始化脚本、Maven配置(pom.xml、mvn-settings.xml)、flatlaf主题依赖JAR及README说明,整体仅1.1MB,结构清晰,便于直接导入IDEA或Eclipse运行。资源中附带了基于flatlaf的系统外观美化代码片段,可快速统一界面风格,提升演示效果。已有217人学习下载,适合需要快速搭建超市管理演示项目、补全数据库课程设计材料或参考分层编码的读者。

1. 期末数据库课设撞上这个组合:先弄懂它值不值得做

期末数据库课程设计跳出「基于 Java+MySQL+JDBC+JavaSwing 的超市管理系统源码+数据库」这类题,基本等于抽到一个标准剧本:Java 写业务逻辑,MySQL 存数据,JDBC 管连接和增删改查,Swing 画界面,最后交一份能编译运行的源码加一份能从头建库的数据库脚本。这套组合不新,但把「界面层-逻辑层-存储层」这条链完整铺开,适合正在赶课程设计的学生,也适合想把手写 SQL 和 JDBC 流程彻底搞清楚的初学者。它最大的价值不是功能多炫,而是每一层都能被答辩老师问到,你也能真答出来。把你手上的这道题当一条训练链路来做,别当成拼装品来凑。

2. 建表先于写码:把超市数据模型和 Java 分层选型定下来

2.1 为什么这套技术栈是课设的「标准答案」:JDBC 直连的好处

很多同学一看到 Java 就往 Spring Boot 想,但数据库课程设计用纯 JDBC 其实是出题人刻意留的考核点。课程设计的评分点从来不是「用了几个框架」,而是:SQL 写规不规范、表结构设计合不合理、连接有没有释放、你能不能讲清楚一次查询从按钮到内存再到硬盘的完整路径。JDBC 直连把这条路径赤裸裸暴露在你面前,而 MyBatis 这类框架会把连接管理、参数绑定全部包起来,反而让你答不上来。

Swing 同理。它虽然老,但 JFrame、JTable、JOptionPane 这几个组件的学习成本很低,两天就能把登录窗体和主界面搭出来。JavaFX 界面更现代,可光是配置模块、处理 CSS 样式就能耗掉你大半个期末周,在课程设计的时间约束里不划算。我一般会做这样的取舍:

| 对比维度 | Swing + JDBC 直连 | JavaFX + MyBatis | | 学习门槛 | 低,JDK 自带 Swing | 中,需要额外引库和配置 | | 课程考核点 | SQL、连接、事务一眼看穿 | 框架配置容易被追问 | | 答辩风险 | 表结构和 SQL 被直接提问 | 环境缺依赖容易现场翻车 | | 课设适用度 | 推荐 | 不推荐用于数据库课 |

这套选型还有一个隐性好处:你交上去的源码里几乎每一行都是「自己的代码」,老师让解释某个方法时你能讲出设计意图。换成框架后,很多配置是从网上复制来的,答不上来反而扣分。

2.2 六张核心表:把超市进销存的最小闭环建出来

超市管理系统不管题目里写了多少功能,进销存三条线是主骨架:供应商给店供商品、店里收银卖商品、销售记录形成流水。围绕这条线,最少的表结构是六张:员工表、类别表、供应商表、商品表、销售单主表、销售明细表。明细表必须单独存在,因为一笔销售单可能包含多种商品,主表和明细是一对多关系,这是数据库课最常考的设计点。

下面是一份可直接导入的建表脚本,字段做了裁剪,但足够撑起登录、商品管理、收银、查流水这四类核心功能:

-- 建库:utf8mb4 能让中文不乱码 CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4; USE supermarket; -- 员工/收银员表:登录和操作留痕用 CREATE TABLE staff ( staff_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT '课设可用明文,生产必须加密', real_name VARCHAR(50) NULL, role VARCHAR(20) DEFAULT 'CASHIER', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 商品类别表和供应商表是商品表的两个引用源 CREATE TABLE category ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(50) NOT NULL ) ENGINE=InnoDB; CREATE TABLE supplier ( supplier_id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(100) NOT NULL, contact_name VARCHAR(50), phone VARCHAR(20) ) ENGINE=InnoDB; -- 商品表:库存默认 0,避免新商品没设库存时出现 NULL CREATE TABLE goods ( goods_id INT AUTO_INCREMENT PRIMARY KEY, goods_name VARCHAR(100) NOT NULL, category_id INT, supplier_id INT, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES category(category_id), CONSTRAINT fk_goods_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(supplier_id) ) ENGINE=InnoDB; -- 销售单主表和明细表:一单多品 CREATE TABLE sale_order ( order_id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, staff_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, sale_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_staff FOREIGN KEY (staff_id) REFERENCES staff(staff_id) ) ENGINE=InnoDB; CREATE TABLE sale_order_detail ( detail_id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, CONSTRAINT fk_detail_order FOREIGN KEY (order_id) REFERENCES sale_order(order_id), CONSTRAINT fk_detail_goods FOREIGN KEY (goods_id) REFERENCES goods(goods_id) ) ENGINE=InnoDB;

字段上有几个点值得较真。金额列一律用 DECIMAL(10,2),不要用 float 或 double,二进制浮点数表达 0.1 不精确,累计对账时会出现 0.0000000001 的偏差,答辩老师很爱拿这个追问。库存 stock 设成 INT NOT NULL DEFAULT 0,新商品上架时没填库存也能落库,不会因为 NULL 参与运算而出错。销售明细里的 price 是冗余字段,它存的是成交那一刻的单价,不能去 join 商品表拿实时价格,否则后来商品改价,历史账单金额就会跟着变。所有表都用 InnoDB,外键约束才能生效,MyISAM 不支持外键。

2.3 数据库脚本的交付姿势:一个 .sql 要从头跑到尾

课程设计交「数据库」时,交的不是 MySQL 数据目录里的文件,而是一份能从头建库的 .sql 脚本。老师拿到后会新建一个环境,用 Navicat 或命令行 source 整体导入,跑通才算数据库交付合格。所以脚本要按「删库→建库→建表→插入测试数据」四段来组织,插入数据的顺序还要额外注意外键约束。

-- 先删后建,方便老师重复导入,也避免你本地反复改结构时留垃圾数据 DROP DATABASE IF EXISTS supermarket; -- 接下来的 CREATE DATABASE / CREATE TABLE 见 2.2,导入时整段连续执行 -- 插入顺序:先基础表,再引用基础表的表 INSERT INTO staff(username, password, real_name, role) VALUES ('admin', '123456', '管理员', 'ADMIN'); INSERT INTO category(category_name) VALUES ('饮料'), ('零食'), ('日用品'); INSERT INTO supplier(supplier_name, contact_name, phone) VALUES ('本地食品供应商', '王老板', '13800000000'); -- 有了 category 和 supplier 的 id,商品才能引外键 INSERT INTO goods(goods_name, category_id, supplier_id, price, stock) VALUES ('可乐', 1, 1, 3.50, 100);

提示:测试数据每张表 3 到 5 条就够,重点是能支撑演示流程。订单和明细可以留在程序里现场生成,脚本里保留两条历史数据即可。

交付时我习惯按「源码工程 + init.sql + 说明文档」三件套准备。说明文档写清楚四样东西:MySQL 版本、JDK 版本、数据库账号密码、运行步骤。这一步看着简单,却能避免答辩时老师因为环境对不上而迟迟跑不起来。

3. 用 JDBC 把增删改查跑通:驱动、URL 和一套能抄到作业里的封装

3.1 环境准备:先装 MySQL 再决定驱动 jar 与 JDBC URL 三参数

如果本机还没装 MySQL,先把 8.x 按安装教程装好,记住 root 密码。安装过程中最值得留意的两个选项是端口号和字符集:端口保持默认的 3306,字符集选 utf8mb4,后面 JDBC URL 和建表语句都要和它对齐。然后去 Maven 仓库下载与 MySQL 大版本匹配的 mysql-connector-java 驱动 jar,放进工程的 lib 目录并 Add to Build Path。老驱动对应类名是 com.mysql.jdbc.Driver,8.x 新驱动变成了 com.mysql.cj.jdbc.Driver,名字记错会在运行时直接报 ClassNotFoundException。

连接串写成下面这个模板,三个参数缺一不可:

String url = "jdbc:mysql://localhost:3306/supermarket" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8";

三个参数各自的作用要能讲明白。useSSL=false 是为了关掉本地开发环境不需要的 TLS 握手,不关掉的话控制台会刷一大堆 SSL 警告,虽然不是报错,但答辩时看着很吓人。serverTimezone=Asia/Shanghai 是 MySQL 8 驱动要求的显式时区,不写会抛 The server time zone value 异常。characterEncoding=utf8 是让中文安全过 JDBC 这道桥,底层 MySQL 建库用 utf8mb4,两边对齐以后才能彻底解决乱码。

3.2 连接工具类 DBUtil:一次写好,全项目复用

用 DriverManager 直连时,最烦的是每个 DAO 类都要重复写「拿连接、关连接」。常见做法是抽一个 DBUtil,在静态代码块里注册驱动,getConnection 对外提供连接,close 统一关闭三个资源。课程设计的并发量很低,DriverManager 完全够用,没必要上连接池;如果以后真要扩展成多收银台同时操作的系统,再换成 HikariCP 这类连接池不迟,但课设阶段别为了炫技引入额外依赖。

import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { // 8.x 驱动类名;老项目用 com.mysql.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }

静态块在类第一次被加载时执行一次,Class.forName 的作用是触发驱动注册,新驱动下这段其实可以省略,但保留能兼容老环境,也不会带来副作用。close 方法的关闭顺序必须是 ResultSet 先、Statement 中间、Connection 最后,反了会出现「连接已关闭」的伪报错,让排查绕远路。账号密码写死在常量里是课设的妥协,但在 README 里注明一句「生产环境密钥不得入库」,能体现你懂工程规范。

3.3 商品表增删改查:PreparedStatement 和 ResultSet 的标准动作

JDBC 的增删改查本质上就四种动作:增删改用 executeUpdate,查询用 executeQuery。这里的关键是从头到尾使用 PreparedStatement,而不是直接拼字符串的 Statement。占位符 ? 把参数和 SQL 模板分离,既免去了拼接字符串时引号转义的麻烦,也顺带防住了 SQL 注入,答辩时这是必问题目。

public class GoodsDAO { // 新增商品:executeUpdate 返回受影响行数 public int insert(Goods g) { String sql = "INSERT INTO goods(goods_name, category_id, supplier_id, price, stock) " + "VALUES(?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, g.getGoodsName()); ps.setInt(2, g.getCategoryId()); ps.setInt(3, g.getSupplierId()); ps.setBigDecimal(4, g.getPrice()); ps.setInt(5, g.getStock()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } // 修改商品:UPDATE 语法里 where 一定带主键,别漏 public int update(Goods g) { String sql = "UPDATE goods SET goods_name = ?, price = ?, stock = ? WHERE goods_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, g.getGoodsName()); ps.setBigDecimal(2, g.getPrice()); ps.setInt(3, g.getStock()); ps.setInt(4, g.getGoodsId()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } // 模糊查询:LIKE 的 % 拼在参数侧,SQL 模板保持干净 public List<Goods> searchByName(String keyword) { List<Goods> list = new ArrayList<>(); String sql = "SELECT goods_id, goods_name, price, stock FROM goods WHERE goods_name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setGoodsId(rs.getInt("goods_id")); g.setGoodsName(rs.getString("goods_name")); g.setPrice(rs.getBigDecimal("price")); g.setStock(rs.getInt("stock")); list.add(g); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }

PreparedStatement 的参数索引从 1 开始,顺序对应 SQL 里 ? 出现的顺序,和表字段顺序无关。setBigDecimal 对应 DECIMAL 列,setInt 对应 INT 列,类型不匹配会在运行期抛异常,而且这种错很容易被误判成 SQL 语法问题。UPDATE 语句里 WHERE 没带主键是最危险的翻车现场,轻则改掉一批不该改的数据,重则把整张表的价格和库存覆盖掉。模糊查询的 % 写在参数侧,而不是拼进 SQL 模板,这样 SQL 模板始终是干净的,查询条件被当成数据传入。

这里还涉及一个资源释放选择:代码里用的是 try-with-resources,离开 try 块时 PreparedStatement 和 Connection 会自动关闭。如果你更习惯手动关闭,就用上一节 DBUtil.close 在 finally 里收口。两种方式都行,但全项目要保持一致,别一半 try-with-resources、一半手动关。

3.4 从 DAO 到 Service:先给源码装一个不会腐烂的骨架

如果所有业务逻辑都写在 Swing 窗口的 ActionListener 里,代码跑起来没问题,但答辩老师问「加一个商品名非空校验要改哪里」,你会发现自己被困在一大坨事件代码里。常见做法是补一个 Service 层:窗口层只负责拿界面数据、把数据传给 Service、再处理返回结果;Service 做业务校验;DAO 只做 SQL 读写。这样三层各管一摊,出问题时能分清是「数据不对」「SQL 不对」还是「界面没刷新」。

public class GoodsService { private final GoodsDAO goodsDAO = new GoodsDAO(); // 新增商品:先做业务校验,校验过不了不进 DAO public boolean addGoods(Goods g) { if (g.getGoodsName() == null || g.getGoodsName().trim().isEmpty()) { return false; } if (g.getPrice() == null || g.getPrice().compareTo(BigDecimal.ZERO) <= 0) { return false; } return goodsDAO.insert(g) > 0; } // 删除商品前检查是否被销售明细引用 public boolean deleteGoods(int goodsId) { if (new SaleDetailDAO().countByGoods(goodsId) > 0) { return false; // 有历史销售记录的商品不允许直接删 } return goodsDAO.delete(goodsId) > 0; } }

注意:课程设计规模不需要硬套 MVC,但 DAO 和 Service 两层隔离是最低要求。比「代码能跑」更重要的是「你能讲清楚每层在干什么」。

4. 把 Swing 界面接上数据:登录窗体到商品管理面板的完整链路

4.1 登录窗体的最短实现:一个按钮触发一次 JDBC 校验

系统里第一个用到 Swing 和 JDBC 配合的地方是登录。实现思路很直白:JTextField 输入用户名,JPasswordField 输入密码,登录按钮的事件里调 StaffDAO 按用户名查记录,再把密码比对。课设阶段密码可以明文存储,但代码注释里要写明生产环境必须加盐哈希,这是安全意识的体现。

public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } Staff staff = new StaffDAO().findByUsername(username); if (staff != null && password.equals(staff.getPassword())) { JOptionPane.showMessageDialog(this, "登录成功"); new MainFrame(staff).setVisible(true); dispose(); // 登录窗体释放 } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } }

JPasswordField.getPassword() 返回 char[] 而不是 String,这是 API 刻意做的设计,为了避免密码字符串残留在 JVM 常量池里。课设为了方便转成 String 可以接受,但你要知道这一点。getText().trim() 处理用户手滑输入的空格,能省掉一堆「明明密码没错却登不进去」的玄学问题。登录成功之后要先把主窗口 setVisible(true) 再 dispose() 当前窗口,顺序反了会出现主界面一闪而过或根本显示不出来的情况。

4.2 JTable 显示商品列表:DefaultTableModel 的刷新套路

Swing 里展示多条查询结果最常用 JTable,但 JTable 不直接接收 List,它认的是 TableModel。常见做法是维护一个 DefaultTableModel,把每一行数据转成 Object[] 数组,addRow 进去。这个环节最常见的坑是刷新前没清空旧数据,表格越点越长,演示到第三遍时全是重复行。

public class GoodsPanel extends JPanel { private JTable table; private DefaultTableModel model; private JTextField keywordField; public GoodsPanel() { String[] columns = {"商品编号", "商品名称", "单价", "库存"}; model = new DefaultTableModel(columns, 0); // 初始 0 行 table = new JTable(model); // 实际代码要把 table 放进 JScrollPane 再 add(this) } // 每次点“查询/刷新”都重新拉数据 public void refreshTable() { model.setRowCount(0); // 先清空,否则上一次的数据会残留 List<Goods> list = new GoodsDAO().selectAll(); for (Goods g : list) { model.addRow(new Object[]{ g.getGoodsId(), g.getGoodsName(), g.getPrice().toString(), // BigDecimal 转字符串,避免表格显示科学计数法 g.getStock() }); } } }

DefaultTableModel 的构造参数是「列名数组 + 初始行数」,columns 决定表头,第二参数写 0 表示还没有数据行。每次刷新先调用 setRowCount(0) 清空,再循环 addRow,界面才能保持和数据库一致。单价列用 BigDecimal.toString(),直接塞对象也能显示,但转成字符串最稳,不会在表格里冒出 3.5E0 这种不友好的计数法形态。关键词搜索的入口可以和查询按钮共用,输入框有值就调 searchByName,没有就调 selectAll。

4.3 耗时的查询不能霸占界面:SwingWorker 的加分用法

Swing 的事件监听默认跑在事件分发线程上,也就是界面线程。如果一次查询要两秒,用户看到的不是等待而是窗口转圈假死。课程设计的数据量通常很小,直接查询问题不大,但答辩演示时如果 MySQL 服务没启动,窗口会假死好几秒才弹错,给老师的体验很不好。

一个加分做法是把查询扔给 SwingWorker:doInBackground 里做耗时 JDBC 操作,done 里切回界面线程刷新表格。代码量不多,但对稳定性的提升立竿见影。

new SwingWorker<List<Goods>, Void>() { @Override protected List<Goods> doInBackground() throws Exception { return new GoodsDAO().selectAll(); // 耗时查询放后台线程 } @Override protected void done() { try { List<Goods> list = get(); model.setRowCount(0); for (Goods g : list) { model.addRow(new Object[]{g.getGoodsId(), g.getGoodsName(), g.getPrice().toString(), g.getStock()}); } } catch (Exception e) { JOptionPane.showMessageDialog(GoodsPanel.this, "数据加载失败"); } } }.execute();

doInBackground 里做 JDBC 查询,返回的 List 会缓存起来,done 里通过 get() 取回数据,期间界面不会出现无响应状态。如果查询抛异常,get() 会把异常重新抛出来,在 done 里统一弹窗提示即可。这个优化不是课设必需品,但写上之后老师演示时窗口始终流畅,属于「几行代码就能加分的优化」。

5. 课设避坑指南:跑不起来的前 5 个现场,逐个排掉

5.1 驱动与 MySQL 版本错位:时区报错和 SSL 刷屏

现象:程序一启动,控制台抛 The server time zone value 或 Public Key Retrieval is not allowed;或者 MySQL 能连,但日志被 SSL 警告刷屏。

原因:MySQL 8 的连接驱动对时区要求很严格;新版 MySQL 默认使用 caching_sha2_password 认证插件,在没有配置公钥获取时不允许直连;本地开发不需要 TLS 加密,默认配置却会尝试走 SSL 握手。

解决:JDBC URL 统一写成下面这行,三个参数补齐后再跑,绝大部分版本错位问题都会消失:

jdbc:mysql://localhost:3306/supermarket?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8

同时确认驱动 jar 和 MySQL 大版本对齐。老驱动连 MySQL 8.x 时在 SSL 握手上很容易出问题,换新驱动后连抛错都可能一起消失。这是课设现场出现频率最高的一条,报错截图先对着这里看。

5.2 2002 错误:MySQL 服务压根没起来

现象:控制台报 Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2002)。

原因:JDBC URL 写得没问题,但本机 MySQL 服务没启动,或者装的是 MariaDB,套接字路径不在默认位置。这个错误和用户名、密码、SQL 都没关系,是服务层面的连接失败。

解决:Windows 在服务管理器里找到 MySQL 服务并手动启动;macOS 用 brew services start mysql;Linux 用 systemctl start mysqld 或 service mysql start。启动后先打开命令行执行 mysql -u root -p 验证能进,再回程序里重新跑。如果安装时改过端口,JDBC URL 里的 3306 也要同步改。

5.3 中文乱码:从建库到控制台一整条链路上的编码对齐

现象:控制台输出中文变成问号;界面上看得正常,但写入数据库后再查出来是乱码。

原因:程序、数据库、驱动三处的字符集不一致。最常见的是建库时没指定 utf8mb4,或者 JDBC URL 少了 characterEncoding,或者 Windows 控制台默认用 GBK 显示 UTF-8 输出。

解决:三层统一。建库建表用 utf8mb4,JDBC URL 带 characterEncoding=utf8,IDE 的项目编码和控制台编码都设成 UTF-8。改完重启程序和 MySQL 客户端,再插入一条中文测试数据验证。如果用的是 Navicat 导入 SQL,连接属性里的编码也要选 UTF-8,不能只靠文件头声明。

5.4 连接泄漏:程序越跑越卡,最终报 too many connections

现象:演示收银功能时,点几次查询程序变慢,最后 MySQL 报 Too many connections。

原因:资源释放链路断了。最常见的是只关了 Connection,Statement 和 ResultSet 没关;其次是查询抛出异常时直接跳出方法,连接永远占着。DriverManager 创建的连接池由 MySQL 端控制数量,积累一段时间后必然触顶。

解决:定一条铁律——拿到 Connection 的方法要么用 try-with-resources,要么在 finally 里调 DBUtil.close(conn, stmt, rs),异常路径也一样要关。写完数一遍项目中 new Connection 和 close 的地方是否一一对应,不要靠猜。这条排错没有黑匣子,逐方法检查很快能定位。

5.5 外键插入失败:先插谁、后插谁,顺序比 SQL 长错更隐蔽

现象:向 goods 表插入商品时报 Cannot add or update a child row: a foreign key constraint fails。

原因:goods 表的外键指向 category 和 supplier,插入时这个 category_id 或 supplier_id 在对应表里不存在;或者在初始化脚本里顺序写反,先插 goods 后插 category。

解决:初始化脚本严格按「先基础表,后引用表」的顺序执行:staff、category、supplier 先插,goods 后插,最后才是 sale_order 和 sale_order_detail。手动新增商品时,category_id 和 supplier_id 要从下拉框里选数据库已有的值,不要直接在文本框里随手填一个 99。这个坑我在帮人调作业时见过太多回,报错一看是外键,往前查插入顺序基本就能破案。

6. 进阶与验收:把课设做成能讲 20 分钟、能当场重跑的作品

基本功能做完之后,我建议把「收银结账」这个方法用事务包起来。业务链路是:插入一条销售主表、插入若干条销售明细、扣减商品库存。这三步要么全成功,要么全回滚,否则会出现库存扣了、明细没写、钱也算不清的中间状态。事务在 JDBC 里实现很简单,但有一个关键点:事务里的多个 DAO 操作必须使用同一个 Connection,不能每个 DAO 内部各开各的连接,否则 commit 和 rollback 根本控制不到对方。

conn.setAutoCommit(false); try { int orderId = orderDao.insert(conn, order); for (CartItem item : cart) { orderDetailDao.insert(conn, orderId, item); goodsDao.decreaseStock(conn, item.getGoodsId(), item.getQuantity()); } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn, null, null); }

老师验收一般走三步:看表结构设计是否合理、看核心代码能不能讲、看现场导入数据库跑不跑得起来。交付文档里写明 MySQL 版本、JDK 版本、数据库账号密码、导入 .sql 的方法和运行入口类。演示流程固定成一条线:登录、新增商品、进货加库存、收银下单、看库存减少和销售流水,这套链路能覆盖全部六张核心表。答辩讲稿也围绕这条线准备,重点讲清楚三件事:为什么金额用 DECIMAL,为什么查询用 PreparedStatement,为什么订单和库存要放同一个事务。这三问答顺了,课设分基本就稳了。

我当年吃过一次亏:建库脚本里字符集没有前后统一,老师现场导入后商品名全是问号,答辩节奏完全被打乱。后来我养成了一个习惯——打包前从 DROP DATABASE 开始把整个导入流程重跑一遍,跑通再交。课程设计很容易让人把时间花在堆积功能上,但最后真正挣到分的,往往是「能稳定复现、能讲清楚」这八个字。希望帮到你。

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

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

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

立即咨询