简介面向期末数据库课程设计提供基于JavaMySQLJDBCJavaSwing的超市管理系统完整源码与数据库脚本适合高校学生完成课程设计或学习桌面数据库应用开发。项目采用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. 期末数据库课设撞上这个组合先弄懂它值不值得做期末数据库课程设计跳出「基于 JavaMySQLJDBCJavaSwing 的超市管理系统源码数据库」这类题基本等于抽到一个标准剧本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 ) ENGINEInnoDB; -- 商品类别表和供应商表是商品表的两个引用源 CREATE TABLE category ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(50) NOT NULL ) ENGINEInnoDB; CREATE TABLE supplier ( supplier_id INT AUTO_INCREMENT PRIMARY KEY, supplier_name VARCHAR(100) NOT NULL, contact_name VARCHAR(50), phone VARCHAR(20) ) ENGINEInnoDB; -- 商品表库存默认 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) ) ENGINEInnoDB; -- 销售单主表和明细表一单多品 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB;字段上有几个点值得较真。金额列一律用 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.Driver8.x 新驱动变成了 com.mysql.cj.jdbc.Driver名字记错会在运行时直接报 ClassNotFoundException。连接串写成下面这个模板三个参数缺一不可String url jdbc:mysql://localhost:3306/supermarket ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8;三个参数各自的作用要能讲明白。useSSLfalse 是为了关掉本地开发环境不需要的 TLS 握手不关掉的话控制台会刷一大堆 SSL 警告虽然不是报错但答辩时看着很吓人。serverTimezoneAsia/Shanghai 是 MySQL 8 驱动要求的显式时区不写会抛 The server time zone value 异常。characterEncodingutf8 是让中文安全过 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 ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; 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 ListGoods searchByName(String keyword) { ListGoods 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); // 先清空否则上一次的数据会残留 ListGoods 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 服务没启动窗口会假死好几秒才弹错给老师的体验很不好。一个加分做法是把查询扔给 SwingWorkerdoInBackground 里做耗时 JDBC 操作done 里切回界面线程刷新表格。代码量不多但对稳定性的提升立竿见影。new SwingWorkerListGoods, Void() { Override protected ListGoods doInBackground() throws Exception { return new GoodsDAO().selectAll(); // 耗时查询放后台线程 } Override protected void done() { try { ListGoods 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?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingutf8同时确认驱动 jar 和 MySQL 大版本对齐。老驱动连 MySQL 8.x 时在 SSL 握手上很容易出问题换新驱动后连抛错都可能一起消失。这是课设现场出现频率最高的一条报错截图先对着这里看。5.2 2002 错误MySQL 服务压根没起来现象控制台报 Cant connect to local MySQL server through socket /tmp/mysql.sock (2002)。原因JDBC URL 写得没问题但本机 MySQL 服务没启动或者装的是 MariaDB套接字路径不在默认位置。这个错误和用户名、密码、SQL 都没关系是服务层面的连接失败。解决Windows 在服务管理器里找到 MySQL 服务并手动启动macOS 用 brew services start mysqlLinux 用 systemctl start mysqld 或 service mysql start。启动后先打开命令行执行 mysql -u root -p 验证能进再回程序里重新跑。如果安装时改过端口JDBC URL 里的 3306 也要同步改。5.3 中文乱码从建库到控制台一整条链路上的编码对齐现象控制台输出中文变成问号界面上看得正常但写入数据库后再查出来是乱码。原因程序、数据库、驱动三处的字符集不一致。最常见的是建库时没指定 utf8mb4或者 JDBC URL 少了 characterEncoding或者 Windows 控制台默认用 GBK 显示 UTF-8 输出。解决三层统一。建库建表用 utf8mb4JDBC URL 带 characterEncodingutf8IDE 的项目编码和控制台编码都设成 UTF-8。改完重启程序和 MySQL 客户端再插入一条中文测试数据验证。如果用的是 Navicat 导入 SQL连接属性里的编码也要选 UTF-8不能只靠文件头声明。5.4 连接泄漏程序越跑越卡最终报 too many connections现象演示收银功能时点几次查询程序变慢最后 MySQL 报 Too many connections。原因资源释放链路断了。最常见的是只关了 ConnectionStatement 和 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 开始把整个导入流程重跑一遍跑通再交。课程设计很容易让人把时间花在堆积功能上但最后真正挣到分的往往是「能稳定复现、能讲清楚」这八个字。希望帮到你。本文还有配套的精品资源点击获取
