尧图精选

Java+Swing+MySQL餐厅点餐系统源码解析:从环境搭建到业务闭环

🕒 发布时间:2026/10/2 18:00:27 📁 来源:尧图网络
简介这是一套面向Java初学者与课程设计学习者的餐厅点餐管理系统完整源码基于Java Swing桌面界面与MySQL数据库开发适合用于毕业设计、课程作业或SwingJDBC综合练习。系统区分管理员与顾客两种角色覆盖登录注册、套餐新增与管理、订单处理、顾客点餐与订单查询、个人信息修改等核心业务功能闭环完整便于理解桌面端分层结构与数据库交互流程。压缩包共170个文件约2.19MB包含18个java源文件、66个class编译文件、62个png与16个jpg界面素材、3个jar依赖、1个sql建表脚本及工程配置文件源码与素材齐备导入Eclipse或IDEA并配置MySQL 8.0以上环境即可运行调试。目前已有1532人学习下载可帮助读者快速掌握Swing窗体设计、事件处理与数据库增删改查的整合思路并在此基础上进行二次开发与功能扩展。1. 从一份只有十个 class 文件的源码包说起JavaSwingMySQL 餐厅点餐系统能跑出什么如果你手头正好有一份 Java 课程设计或者实训作业要求做一个带图形界面的餐厅点餐管理系统那这份基于 JavaSwingMySQL 的源码包大概率能让你少熬两个通宵。它没有用 Spring Boot、没有前后端分离、没有 Vue 和 Element UI就是最朴素的 JavaSE 桌面应用Swing 画窗口JDBC 连 MySQL十个核心 class 文件把管理员和顾客两条业务线全部串起来。GoodsManageInterFrm 管套餐DealOrderInterFrm 处理订单CheckOrderInterFrm 查订单OrderGoodsInterFrm 负责点餐LogOnFrm 和 RegisterFrm 管登录注册AccountModifyInterFrm 改密码AdminFrm 和 UserOrderFrm 分别是管理员和顾客的主界面。这套东西适合谁适合正在做 Java 课程设计、需要快速交出一个能演示、能答辩、能写进论文的系统原型的同学也适合想拿一个完整 Swing 项目练手 JDBC 和面向对象编程的初学者。它不炫技但业务闭环是完整的注册、登录、点餐、下单、处理订单、管理套餐、改密码一条龙全通。下面我按实际拆包和跑通的顺序把这份资源从环境搭建到业务逻辑再到踩坑排查完整过一遍。2. 环境搭建与数据库初始化MySQL 8.0 建库建表和 JDBC 连接配置2.1 为什么是 MySQL 8.0 而不是 5.7项目正文明确写了运行环境是 Eclipse 或 IDEA 加 MySQL 8.0 以上。这个版本要求不是随便写的。MySQL 8.0 默认的认证插件从mysql_native_password换成了caching_sha2_password而老版本的 JDBC 驱动比如mysql-connector-java-5.1.x根本不认识这个插件连接时会直接抛Unable to load authentication plugin caching_sha2_password。所以如果你本地装的是 MySQL 5.7要么升级到 8.0要么在 JDBC URL 里显式指定useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue否则时区报错和公钥检索报错会轮番上阵。常见做法是直接上 MySQL 8.0驱动用mysql-connector-java-8.0.28或更高省掉一堆兼容性玄学。2.2 建库建表的 SQL 脚本这份源码包一般会附带一个.sql文件但如果你拿到的版本没有或者你想自己重建一遍下面这套建表语句可以直接抄。核心表就四张用户表、套餐表、订单表、订单明细表。-- 创建数据库字符集用 utf8mb4 防止中文乱码 CREATE DATABASE IF NOT EXISTS restaurant_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE restaurant_db; -- 用户表同时存管理员和顾客用 user_type 区分 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL, user_type INT NOT NULL DEFAULT 0, -- 0 顾客1 管理员 phone VARCHAR(20), address VARCHAR(200) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 套餐表 CREATE TABLE t_goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, description VARCHAR(500), stock INT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, total_price DECIMAL(10,2) DEFAULT 0.00, status INT DEFAULT 0, -- 0 待处理1 已完成 FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE t_order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (goods_id) REFERENCES t_goods(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入一个默认管理员账号密码 123456 INSERT INTO t_user (username, password, user_type) VALUES (admin, 123456, 1);这段 SQL 的逻辑很直白t_user用user_type字段把管理员和顾客揉在一张表里登录时根据这个字段决定跳转到AdminFrm还是UserOrderFrm。t_order和t_order_detail拆成两张表是标准做法一个订单对应多条明细避免在订单表里塞 JSON 或者逗号分隔的字符串。t_goods里的stock字段在点餐时会做扣减虽然源码里可能只是简单更新但表结构先留好。注意utf8mb4和utf8在 MySQL 里不是一回事用utf8存中文没问题但存 emoji 会炸。课程设计一般用不到 emoji但养成utf8mb4的习惯没坏处。2.3 JDBC 连接配置与驱动引入在 Eclipse 或 IDEA 里新建 Java 项目后第一件事是把mysql-connector-java-8.0.x.jar加到 Build Path 或者 Maven 依赖里。如果你用 Mavenpom.xml里加这一段dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency然后写一个DBUtil工具类把连接参数集中管理。源码包里可能直接在每个InterFrm里写DriverManager.getConnection但那样改数据库密码的时候你会想砸键盘。我一般会抽一个工具类出来import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // MySQL 8.0 驱动类名 private static final String DRIVER com.mysql.cj.jdbc.Driver; // 关键参数useSSLfalse 关掉 SSL 警告serverTimezone 解决时区差 8 小时问题 private static final String URL jdbc:mysql://localhost:3306/restaurant_db ?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue characterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn) { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }Class.forName在 JDBC 4.0 之后其实可以省略但很多老教程和源码包都保留着留着也不碍事。URL 里三个参数各有用途useSSLfalse是关掉 SSL 连接警告本地开发用不上加密serverTimezoneAsia/Shanghai是必须的不写的话 MySQL 8.0 驱动会按 UTC 时区解析时间你存进去的2024-01-01 12:00:00读出来可能变成2024-01-01 04:00:00allowPublicKeyRetrievaltrue是配合caching_sha2_password插件用的不写的话第一次连接可能被拒绝。characterEncodingutf8保证中文不乱码。2.4 在 IDE 里跑起来的完整步骤把源码导入 Eclipse 或 IDEA 之后按这个顺序操作确认项目 JDK 版本在 1.8 以上Swing 在 JDK 11 之后被移出了标准库但 JDK 8 和 JDK 11 都还能用JDK 17 以上需要额外加--add-modules java.desktop。把mysql-connector-java的 jar 包加到项目的 Libraries 里或者用 Maven 依赖。修改DBUtil里的数据库密码为你本地的密码。在 MySQL 里执行上面的建表 SQL。找到LogOnFrm.java右键 Run As Java Application。用admin / 123456登录管理员或者先注册一个顾客账号再登录。如果启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver说明驱动没加对如果报Access denied for user rootlocalhost说明密码错了如果报Unknown database restaurant_db说明建库那步没执行。这三个错误覆盖了 90% 的启动失败场景。3. 管理员模块拆解套餐管理、订单处理与 GoodsManageInterFrm 的实现逻辑3.1 管理员主界面 AdminFrm 的导航结构AdminFrm是整个管理员端的入口登录成功后从LogOnFrm跳过来。它一般是一个JFrame上面挂几个按钮或者一个JTabbedPane分别对应套餐管理、订单处理、修改密码。源码包里GoodsManageInterFrm、DealOrderInterFrm、CheckOrderInterFrm、AccountModifyInterFrm这几个类都是独立的JFrame或JInternalFrame由AdminFrm里的按钮事件new出来再setVisible(true)。这种写法在 Swing 里很常见好处是每个功能模块互不干扰坏处是窗口开多了任务栏会爆炸。如果你想让界面看起来稍微专业一点可以把JFrame换成JDesktopPane加JInternalFrame让子窗口在主窗口内部浮动但源码包大概率没做这层封装直接弹新窗口。3.2 GoodsManageInterFrm套餐的增删改查GoodsManageInterFrm负责套餐管理核心是一个JTable加几个JButton。表格的数据来自t_goods表的SELECT *查询用DefaultTableModel填充。新增套餐的按钮会弹出一个GoodsAddInterFrm里面是几个JTextField对应套餐名称、价格、描述、库存点保存时执行INSERT INTO t_goods。修改和删除则是先选中表格某一行拿到id再执行UPDATE或DELETE。这里有一个新手很容易翻车的地方JTable的getSelectedRow()返回的是视图行号如果你对表格做了排序RowSorter视图行号和模型行号会对不上。源码包里一般没做排序所以直接table.getSelectedRow()再table.getValueAt(row, 0)拿 id 是没问题的。但如果你自己加了排序功能就得用table.convertRowIndexToModel(row)转换一下否则删错数据是分分钟的事。// 删除套餐的典型写法 int row goodsTable.getSelectedRow(); if (row -1) { JOptionPane.showMessageDialog(this, 请先选中一行); return; } // 如果加了 RowSorter这行必须加 // row goodsTable.convertRowIndexToModel(row); int goodsId Integer.parseInt(goodsTable.getValueAt(row, 0).toString()); String sql DELETE FROM t_goods WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, goodsId); int affected ps.executeUpdate(); if (affected 0) { JOptionPane.showMessageDialog(this, 删除成功); refreshTable(); // 重新查询并刷新表格 } } catch (SQLException e) { e.printStackTrace(); JOptionPane.showMessageDialog(this, 删除失败 e.getMessage()); }用PreparedStatement而不是Statement拼接字符串除了防 SQL 注入还能避免套餐名称里带单引号导致语法错误。refreshTable()方法一般是重新执行SELECT然后用tableModel.setRowCount(0)清空再逐行addRow不要直接new一个新的DefaultTableModel塞给JTable那样会丢失已有的监听器。3.3 DealOrderInterFrm 与 CheckOrderInterFrm订单处理与查询DealOrderInterFrm是管理员处理订单的界面通常左边一个订单列表右边一个明细列表选中某个订单后右边显示这个订单点了哪些套餐、数量多少、小计多少。数据来自t_order和t_order_detail的联表查询SELECT o.id, u.username, o.order_time, o.total_price, o.status FROM t_order o JOIN t_user u ON o.user_id u.id WHERE o.status 0 ORDER BY o.order_time DESC;status 0表示待处理管理员点“处理完成”按钮后执行UPDATE t_order SET status 1 WHERE id ?。CheckOrderInterFrm则是查所有订单不加status条件可能还带一个按用户名或日期筛选的查询框。这两个界面的技术难点不在 SQL而在 Swing 的事件监听和表格刷新。常见做法是给JTable加一个ListSelectionListener当选中行变化时从t_order_detail里查对应order_id的明细填充右边的表格。注意ListSelectionListener会触发两次一次是旧行取消选中一次是新行选中如果你在监听器里直接查数据库会多跑一次无用查询。可以在监听器开头加if (!e.getValueIsAdjusting())判断只在最终选中时执行。3.4 AccountModifyInterFrm改密码的边界条件AccountModifyInterFrm的逻辑很简单三个密码框旧密码、新密码、确认新密码点确认后先查t_user表验证旧密码是否正确再UPDATE新密码。这里有两个边界条件源码包可能没处理一是新密码和确认密码不一致时要拦截二是新密码不能为空。如果没做这两层校验用户点确认后要么报 SQL 错误要么把密码改成空字符串下次登录直接进不去。我一般会在按钮事件里先做前端校验通过了再走数据库。String oldPwd new String(oldPasswordField.getPassword()); String newPwd new String(newPasswordField.getPassword()); String confirmPwd new String(confirmPasswordField.getPassword()); if (newPwd.isEmpty()) { JOptionPane.showMessageDialog(this, 新密码不能为空); return; } if (!newPwd.equals(confirmPwd)) { JOptionPane.showMessageDialog(this, 两次输入的新密码不一致); return; } // 再走数据库校验旧密码和更新JPasswordField.getPassword()返回的是char[]用new String()转一下再用不要用getText()那个方法在JPasswordField里已经废弃了虽然还能用但会报警告。4. 顾客模块拆解点餐流程、订单查看与 UserOrderFrm 的数据流转4.1 UserOrderFrm 的界面布局与点餐逻辑UserOrderFrm是顾客登录后的主界面一般分三块左边是套餐列表JTable或JList中间是数量选择JSpinner或JTextField右边是已选购物车另一个JTable。顾客选中一个套餐输入数量点“加入购物车”数据先存在内存里比如一个ListOrderItem点“提交订单”时才批量写入t_order和t_order_detail。这个“先内存后数据库”的设计是有道理的如果每加一个菜就写一次数据库顾客改主意想删掉就得再写一次删除事务管理会很麻烦。放在内存里最后一次性提交用Connection.setAutoCommit(false)开事务要么全成功要么全回滚。// 提交订单的核心逻辑 Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入订单主表 String orderSql INSERT INTO t_order (user_id, total_price, status) VALUES (?, ?, 0); PreparedStatement orderPs conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); orderPs.setInt(1, currentUserId); orderPs.setBigDecimal(2, totalPrice); orderPs.executeUpdate(); // 2. 拿到刚插入的订单 id ResultSet rs orderPs.getGeneratedKeys(); int orderId 0; if (rs.next()) { orderId rs.getInt(1); } // 3. 批量插入订单明细 String detailSql INSERT INTO t_order_detail (order_id, goods_id, quantity, subtotal) VALUES (?, ?, ?, ?); PreparedStatement detailPs conn.prepareStatement(detailSql); for (OrderItem item : cartList) { detailPs.setInt(1, orderId); detailPs.setInt(2, item.getGoodsId()); detailPs.setInt(3, item.getQuantity()); detailPs.setBigDecimal(4, item.getSubtotal()); detailPs.addBatch(); } detailPs.executeBatch(); conn.commit(); // 提交事务 JOptionPane.showMessageDialog(this, 下单成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); JOptionPane.showMessageDialog(this, 下单失败 e.getMessage()); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }Statement.RETURN_GENERATED_KEYS是 JDBC 里拿自增主键的标准做法比再查一次SELECT MAX(id)靠谱得多并发场景下不会拿错。addBatch()和executeBatch()是批量插入的优化购物车里东西多的时候比一条条executeUpdate快一个数量级。4.2 订单查看与状态流转顾客查看订单的界面一般叫CheckOrderInterFrm或者直接在UserOrderFrm里加一个标签页。查询语句按user_id过滤SELECT o.id, o.order_time, o.total_price, o.status FROM t_order o WHERE o.user_id ? ORDER BY o.order_time DESC;status字段在顾客端显示为“待处理”或“已完成”管理员在DealOrderInterFrm里改的就是这个字段。这里有一个数据一致性的小坑如果管理员正在处理订单的同时顾客又下了一单两边看到的列表可能不同步。Swing 桌面应用没有 WebSocket 那种实时推送常见做法是顾客端加一个“刷新”按钮手动重新查询。课程设计层面这样够用了不用上定时器轮询。4.3 注册与登录的数据校验RegisterFrm负责顾客注册核心是往t_user表插一条user_type 0的记录。这里必须做用户名唯一性校验否则两个顾客注册同一个用户名登录时SELECT会返回两条记录ResultSet.next()只取第一条另一个人的密码就永远对不上。校验方式有两种一是先SELECT COUNT(*) FROM t_user WHERE username ?返回 0 才允许插入二是直接依赖数据库的UNIQUE约束插入时捕获SQLIntegrityConstraintViolationException。我一般两种都做前端查一次给用户友好提示数据库约束兜底防并发。LogOnFrm登录时根据user_type决定跳转String sql SELECT id, user_type FROM t_user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { int userId rs.getInt(id); int userType rs.getInt(user_type); if (userType 1) { new AdminFrm().setVisible(true); } else { new UserOrderFrm(userId).setVisible(true); } this.dispose(); // 关掉登录窗口 } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); }this.dispose()只释放当前窗口的资源不会退出整个 JVM因为新开的AdminFrm或UserOrderFrm还在事件调度线程里跑着。如果你用System.exit(0)整个程序直接退出新窗口也活不了。5. 避坑与排查从驱动报错到中文乱码的五个血泪现场5.1 现象启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因项目没有引入 MySQL 8.0 的 JDBC 驱动 jar 包或者引入的是老版本的com.mysql.jdbc.Driver。MySQL 8.0 的驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver老驱动连 8.0 的库会直接报认证插件错误。解决在项目 Libraries 里添加mysql-connector-java-8.0.28.jar或者 Maven 里加对应依赖。如果已经加了还报错检查 Build Path 里 jar 包是不是只加到了 Modulepath 而不是 ClasspathEclipse 里这两个路径的类加载器不一样。5.2 现象连接数据库时报The server time zone value ???ú±ê×??± is unrecognized原因JDBC URL 里没指定serverTimezoneMySQL 8.0 驱动尝试从服务器读取时区但服务器时区是中文乱码或者不认识的格式驱动解析失败。解决在 URL 里显式加serverTimezoneAsia/Shanghai。如果加了还报错检查 MySQL 的my.ini或my.cnf里default-time-zone配置或者直接在 MySQL 命令行执行SET GLOBAL time_zone 8:00;。5.3 现象插入中文数据后在 MySQL 里查出来是问号???原因数据库、表、连接三层的字符集不一致。常见情况是数据库建的时候用了latin1或者 JDBC URL 里没加characterEncodingutf8。解决建库时指定DEFAULT CHARACTER SET utf8mb4JDBC URL 里加characterEncodingutf8MySQL 配置文件里character-set-serverutf8mb4。三层都对齐之后中文乱码基本绝迹。5.4 现象点餐提交订单后t_order里有记录但t_order_detail是空的原因插入订单主表后没有正确获取自增主键orderId是 0 或者默认值导致明细插入时外键关联失败但代码里没开事务主表插入成功了明细插入报错被吞掉。解决用Statement.RETURN_GENERATED_KEYS拿自增 id并且把主表和明细的插入放在同一个事务里任何一步失败都rollback。检查t_order_detail的外键约束是否生效如果外键没建明细插入不会报错但数据是脏的。5.5 现象管理员处理订单后顾客端刷新还是显示“待处理”原因顾客端的JTable数据是登录时查一次就缓存住了没有重新查询数据库。Swing 的JTable不会自动感知数据库变化。解决在顾客端加一个“刷新”按钮点击时重新执行SELECT并fireTableDataChanged()。如果不想加按钮可以在窗口获得焦点时WindowFocusListener自动刷新但课程设计层面手动刷新就够了。6. 进阶技巧用 DBUtil 单例加连接池把 JDBC 代码从重复劳动里捞出来源码包里最常见的写法是每个InterFrm里都写一遍DriverManager.getConnection改数据库密码的时候要全局搜索替换漏一个就报错。我一般会做两层封装第一层是前面写的DBUtil工具类第二层是引入一个轻量连接池比如 HikariCP 或者 Druid。课程设计不强制用连接池但用了之后代码会干净很多而且面试的时候被问到“MySQL 的数据库连接池”也有东西可讲。HikariCP 的引入很简单Maven 加依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency然后把DBUtil改成从连接池取连接import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static final DataSource DATA_SOURCE; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/restaurant_db ?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue characterEncodingutf8); config.setUsername(root); config.setPassword(你的密码); config.setMaximumPoolSize(10); // 最大连接数 config.setMinimumIdle(2); // 最小空闲连接 config.setConnectionTimeout(30000); // 连接超时 30 秒 config.setIdleTimeout(600000); // 空闲回收 10 分钟 DATA_SOURCE new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } }maximumPoolSize设 10 对桌面应用来说绰绰有余Swing 是单线程事件调度模型同一时刻不会有几十个并发查询。connectionTimeout设 30 秒是防止数据库挂了之后界面卡死超时后抛异常JOptionPane弹个提示框比无限等待友好得多。idleTimeout控制空闲连接多久被回收桌面应用访问频率低设 10 分钟比较合理。用上连接池之后每个InterFrm里的DBUtil.getConnection()调用方式不变但底层从每次新建物理连接变成了从池里借用完close()是归还而不是真正关闭。这里有一个血泪经验如果你在代码里手动conn.close()之后又拿同一个conn去执行查询连接池会报Connection is closed因为归还后的连接可能已经被分配给别的线程了。所以close()之后不要再碰那个Connection对象这是铁律。验证连接池是否生效可以在 MySQL 命令行执行SHOW STATUS LIKE Threads_connected;程序启动后这个值应该稳定在minimumIdle附近而不是每次查询都涨。如果每次操作都涨说明连接池没配好可能还在用DriverManager。从那以后我每次拿到一个 JDBC 项目第一件事就是把DBUtil抽出来第二件事就是看有没有连接池。没有的话哪怕课程设计不要求我也会顺手加上因为后面调试的时候少受很多罪。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →