简介这是一套面向Java初学者与课程设计学习者的餐厅点餐管理系统完整源码基于Java Swing桌面界面与MySQL数据库实现适合作为毕业设计、课程大作业或SwingJDBC综合练习的参考方案。系统区分管理员与顾客两种角色管理员可新增与管理套餐、处理订单、修改密码顾客可注册登录、点餐、查看订单及修改个人信息功能闭环较为完整。压缩包共170个文件约2.19MB其中18个java源文件承载核心业务逻辑66个class为编译产物62个png与16个jpg提供界面图标与图片素材另含3个jar依赖、1个sql建库脚本及project、classpath等工程配置导入Eclipse或IDEA并配置MySQL 8.0以上环境即可运行。目前已有1532人学习下载读者可借此理解Swing窗体布局、事件监听、JDBC增删改查与订单状态流转的实现思路并在此基础上二次开发或撰写设计文档。1. JavaSwingMySQL 餐厅点餐管理系统从课程设计到能跑起来的桌面应用很多 Java 课程设计选题里「餐厅点餐管理系统」出现的频率高得离谱但真正能拿出手的没几个。大部分情况是界面用 Swing 拖几个按钮数据往 MySQL 一塞查询用Statement拼字符串最后交上去能跑就行。可一旦答辩老师问一句「两个人同时点同一道菜库存怎么扣」或者「MySQL 连接断了程序为什么直接卡死」就露馅了。这个标题对应的东西其实很明确用 Java 做桌面端Swing 负责界面MySQL 存菜品、订单、桌台这些数据最终交付一个能点菜、能下单、能结账、能查流水的单机或局域网小系统。它适合两类人一是正在做课程设计、需要一套完整可复现方案的学生二是刚学完 Java 基础、想找一个能把面向对象、JDBC、集合、异常处理串起来的练手项目的人。下面我按实际做过的路径把选型、建表、界面、数据一致性、打包这几件事讲透中间会给出可直接抄的代码和参数。2. 技术选型与数据库设计为什么不用 JavaFX表该怎么建2.1 Swing 在 2025 年还值不值得用先说结论做课程设计和内部小工具Swing 依然是最省事的选择。原因有三点。第一JDK 自带不需要额外配 Maven 依赖或 Scene Builderjavac加java两条命令就能跑这对「java环境变量配置」还没搞利索的人来说很关键。第二Swing 的组件模型足够简单JFrameJPanelJTable这套组合能覆盖点餐系统 90% 的界面需求。第三网上「java课程设计案例源码」里 Swing 占大头遇到问题容易搜到答案。JavaFX 当然更现代但它需要单独引入模块打包时还要处理module-path对只想交个课程设计的人来说是额外负担。所以我的建议是除非老师明确要求 JavaFX否则就用 Swing。选型定了接下来是数据库。MySQL 用 8.x 版本字符集统一utf8mb4排序规则utf8mb4_0900_ai_ci。这里有个高频踩坑点很多人装完 MySQL 用mysql -u root -p连不上报error 2002 (hy000): cant connect to local mysql server through socket /tmp/mysql.sock这通常是服务没启动或者 socket 路径不对Windows 下用net start mysql80启动服务即可Linux 下用systemctl start mysqld。2.2 四张核心表与建表语句点餐系统的数据模型不复杂抓住「菜品—订单—订单明细—桌台」这条主线就够了。下面是我一般会用的建表 SQL直接可以在 Navicat 或 MySQL Workbench 里执行。-- 菜品表存菜名、价格、分类、库存、是否上架 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 菜名, price DECIMAL(8,2) NOT NULL DEFAULT 0.00 COMMENT 单价, category VARCHAR(20) NOT NULL DEFAULT 热菜 COMMENT 分类, stock INT NOT NULL DEFAULT 0 COMMENT 库存份数, on_sale TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 桌台表桌号、座位数、状态 CREATE TABLE dining_table ( id INT PRIMARY KEY AUTO_INCREMENT, table_no VARCHAR(10) NOT NULL UNIQUE COMMENT 桌号, seats INT NOT NULL DEFAULT 4, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1占用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表一桌一次下单对应一条 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, table_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未结账 1已结账, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_table (table_id), CONSTRAINT fk_order_table FOREIGN KEY (table_id) REFERENCES dining_table(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表一条订单里每道菜的数量和小计 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00, INDEX idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT fk_item_dish FOREIGN KEY (dish_id) REFERENCES dish(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表逻辑说明dish表用stock字段控制库存on_sale控制上下架这两个字段是后面做并发扣减的关键。orders和order_item拆成两张表是标准做法主表存总额和状态明细表存每道菜的数量这样查流水和统计销量都方便。外键约束建议保留虽然有人为了性能会去掉但在课程设计阶段外键能帮你提前发现数据插入顺序错误。参数说明DECIMAL(8,2)表示总共 8 位、小数 2 位最大 999999.99对餐厅单价足够。utf8mb4是为了支持菜名里的生僻字和 emoji有些餐厅菜单确实有。ENGINEInnoDB必须写因为要用事务MyISAM 不支持。2.3 JDBC 连接与连接池的最小配置数据库建好后Java 这边第一件事是拿到Connection。新手常犯的错是每次查询都DriverManager.getConnection这样在并发稍微高一点时性能很差而且连接泄漏后程序会卡死。常见做法是引入 HikariCP 或 Druid但课程设计里如果不想加额外 jar 包可以自己写一个简单的连接池或者至少把连接参数抽到配置文件。下面是一个不依赖第三方库的DBUtil用静态块加载驱动并提供获取和关闭连接的方法。import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/restaurant?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动未找到请检查mysql-connector-java是否在classpath, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } 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) {} } }逻辑说明Class.forName加载驱动MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver写错会报ClassNotFoundException。URL 里的serverTimezone必须加否则可能报时区错误useSSLfalse在本地开发时避免 SSL 连接警告。close方法按ResultSet → Statement → Connection顺序关闭这是 JDBC 的标准关闭顺序。参数说明3306是 MySQL 默认端口如果你改过要对应修改。restaurant是数据库名建库时用CREATE DATABASE restaurant DEFAULT CHARSET utf8mb4;。密码不要硬编码在代码里正式一点可以放到db.properties文件用Properties类读取。3. Swing 界面搭建从登录到点餐主界面的可复现步骤3.1 界面分层与卡片布局Swing 做点餐系统界面不要全塞在一个JFrame里否则代码会膨胀到几千行没法维护。我一般会分成三层登录窗口LoginFrame、主窗口MainFrame、以及各个功能面板DishPanel、OrderPanel、TablePanel。主窗口用CardLayout管理面板切换这样点「点餐」「订单」「桌台」按钮时只切换卡片不用反复创建窗口。下面是一个主窗口的骨架代码展示CardLayout的用法。import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { private final CardLayout cardLayout new CardLayout(); private final JPanel cardPanel new JPanel(cardLayout); public MainFrame() { setTitle(餐厅点餐管理系统); setSize(1000, 680); setLocationRelativeTo(null); setDefaultCloseOperation(EXIT_ON_CLOSE); // 左侧导航 JPanel nav new JPanel(new GridLayout(6, 1, 8, 8)); nav.setPreferredSize(new Dimension(140, 0)); String[] names {点餐, 订单查询, 桌台管理, 菜品管理, 营业统计}; String[] cards {order, query, table, dish, stat}; for (int i 0; i names.length; i) { final String card cards[i]; JButton btn new JButton(names[i]); btn.addActionListener(e - cardLayout.show(cardPanel, card)); nav.add(btn); } cardPanel.add(new OrderPanel(), order); cardPanel.add(new QueryPanel(), query); cardPanel.add(new TablePanel(), table); cardPanel.add(new DishPanel(), dish); cardPanel.add(new StatPanel(), stat); add(nav, BorderLayout.WEST); add(cardPanel, BorderLayout.CENTER); } public static void main(String[] args) { SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }逻辑说明CardLayout允许在同一个容器里叠放多个面板通过show(container, name)切换。左侧导航用GridLayout竖排按钮每个按钮的监听器里调用cardLayout.show。SwingUtilities.invokeLater是 Swing 的线程规范所有界面创建都应在事件调度线程里做否则可能出现界面闪烁或组件不显示。参数说明setLocationRelativeTo(null)让窗口居中。GridLayout(6, 1, 8, 8)表示 6 行 1 列水平和垂直间距都是 8 像素。setPreferredSize固定导航栏宽度避免窗口拉伸时按钮变形。3.2 点餐面板JTable 展示菜品与下单逻辑点餐面板的核心是一个JTable显示菜品列表加上数量输入框和「加入订单」按钮。JTable要用DefaultTableModel这样增删行方便。下面给出关键代码。import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; import java.util.Vector; public class OrderPanel extends JPanel { private final DefaultTableModel dishModel; private final JTable dishTable; private final DefaultTableModel cartModel; private final JTable cartTable; public OrderPanel() { setLayout(new BorderLayout(10, 10)); // 菜品表格 dishModel new DefaultTableModel(new String[]{ID, 菜名, 单价, 库存}, 0) { Override public boolean isCellEditable(int r, int c) { return false; } }; dishTable new JTable(dishModel); loadDishes(); // 购物车表格 cartModel new DefaultTableModel(new String[]{菜名, 数量, 小计}, 0); cartTable new JTable(cartModel); JButton addBtn new JButton(加入订单); addBtn.addActionListener(e - addToCart()); JPanel center new JPanel(new GridLayout(1, 2, 10, 10)); center.add(new JScrollPane(dishTable)); center.add(new JScrollPane(cartTable)); add(center, BorderLayout.CENTER); add(addBtn, BorderLayout.SOUTH); } private void loadDishes() { dishModel.setRowCount(0); String sql SELECT id, name, price, stock FROM dish WHERE on_sale 1; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { VectorObject row new Vector(); row.add(rs.getInt(id)); row.add(rs.getString(name)); row.add(rs.getBigDecimal(price)); row.add(rs.getInt(stock)); dishModel.addRow(row); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 加载菜品失败 ex.getMessage()); } } private void addToCart() { int row dishTable.getSelectedRow(); if (row 0) { JOptionPane.showMessageDialog(this, 请先选择一道菜); return; } String name dishModel.getValueAt(row, 1).toString(); java.math.BigDecimal price new java.math.BigDecimal(dishModel.getValueAt(row, 2).toString()); String qtyStr JOptionPane.showInputDialog(this, 请输入数量, 1); if (qtyStr null || qtyStr.trim().isEmpty()) return; int qty; try { qty Integer.parseInt(qtyStr.trim()); if (qty 0) throw new NumberFormatException(); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, 数量必须是正整数); return; } java.math.BigDecimal subtotal price.multiply(java.math.BigDecimal.valueOf(qty)); cartModel.addRow(new Object[]{name, qty, subtotal}); } }逻辑说明loadDishes从dish表查上架菜品填充到dishModel。addToCart先检查是否选中行再弹输入框问数量用BigDecimal计算小计避免浮点误差。这里没有直接扣库存因为购物车只是临时数据真正扣库存在「提交订单」时做这样用户改数量不会反复操作数据库。参数说明isCellEditable返回false让表格只读防止用户直接改 ID。JOptionPane.showInputDialog的第二个参数是默认值这里给1减少输入。BigDecimal.multiply做金额乘法比double精确。3.3 提交订单与事务扣库存提交订单是整个系统最需要小心的地方。用户点完菜点「提交订单」要做三件事插入orders主表、插入order_item明细、扣减dish库存。这三步必须在一个事务里否则可能出现订单插入了但库存没扣或者库存扣了订单没插入。public boolean submitOrder(int tableId, ListCartItem items) { String insertOrder INSERT INTO orders(table_id, total_price, status) VALUES(?, ?, 0); String insertItem INSERT INTO order_item(order_id, dish_id, quantity, subtotal) VALUES(?, ?, ?, ?); String deductStock UPDATE dish SET stock stock - ? WHERE id ? AND stock ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总额 java.math.BigDecimal total java.math.BigDecimal.ZERO; for (CartItem it : items) { total total.add(it.getSubtotal()); } // 2. 插入订单主表拿回自增ID int orderId; try (PreparedStatement ps conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, tableId); ps.setBigDecimal(2, total); ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { rs.next(); orderId rs.getInt(1); } } // 3. 逐条插入明细并扣库存 try (PreparedStatement psItem conn.prepareStatement(insertItem); PreparedStatement psStock conn.prepareStatement(deductStock)) { for (CartItem it : items) { psItem.setInt(1, orderId); psItem.setInt(2, it.getDishId()); psItem.setInt(3, it.getQuantity()); psItem.setBigDecimal(4, it.getSubtotal()); psItem.executeUpdate(); psStock.setInt(1, it.getQuantity()); psStock.setInt(2, it.getDishId()); psStock.setInt(3, it.getQuantity()); int affected psStock.executeUpdate(); if (affected 0) { throw new SQLException(菜品[ it.getDishName() ]库存不足); } } } conn.commit(); return true; } catch (SQLException ex) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } JOptionPane.showMessageDialog(null, 下单失败 ex.getMessage()); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } } }逻辑说明setAutoCommit(false)开启事务所有操作成功后commit任何一步抛异常就rollback。扣库存的 SQL 用了stock ?条件如果库存不够executeUpdate返回 0此时主动抛异常触发回滚。RETURN_GENERATED_KEYS用来拿刚插入订单的自增 ID供明细表使用。参数说明PreparedStatement的占位符从 1 开始编号。setBigDecimal对应DECIMAL字段。扣库存的第三个参数是数量和第一个参数相同这是为了在WHERE里做库存充足判断。4. 避坑与排查连接、并发、中文乱码的 5 个血泪经验4.1 现象程序启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver原因mysql-connector-java的 jar 包没有加到 classpath或者版本太老5.x 的驱动类名是com.mysql.jdbc.Driver。解决去 MySQL 官网下载mysql-connector-j-8.x.x.jar在 IDEA 里通过File → Project Structure → Libraries添加命令行运行时用java -cp .;mysql-connector-j-8.0.33.jar MainWindows 用分号Linux 用冒号。4.2 现象两个收银员同时给最后一桌点同一道菜库存变成 -1原因扣库存时没有加行锁或条件判断两个事务同时读到stock1都执行stock stock - 1。解决用上面submitOrder里的UPDATE dish SET stock stock - ? WHERE id ? AND stock ?利用 InnoDB 的行锁和WHERE条件保证不会超卖。更严格的做法是在SELECT时加FOR UPDATE但简单场景用条件更新就够了。4.3 现象JTable 里菜名显示成问号或方块原因数据库连接 URL 没指定字符集或者 MySQL 服务端字符集不是utf8mb4。解决URL 加characterEncodingutf8mb4建库时用CREATE DATABASE restaurant DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;建表也指定DEFAULT CHARSETutf8mb4。如果已经建错用ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4;修复。4.4 现象程序运行一段时间后界面卡死点任何按钮都没反应原因在事件调度线程EDT里执行了耗时数据库查询Swing 的界面刷新和事件处理都在 EDT一旦阻塞整个界面就假死。解决把数据库操作放到SwingWorker的doInBackground里查询完在done方法里更新表格。简单场景也可以用new Thread(() - {...}).start()但更新界面时要用SwingUtilities.invokeLater。4.5 现象MySQL 8 连接报 Public Key Retrieval is not allowed原因MySQL 8 默认用caching_sha2_password认证插件JDBC 连接时如果不允许公钥检索会报这个错。解决URL 加allowPublicKeyRetrievaltrue或者把用户认证方式改成mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。本地开发推荐前者改 URL 就行。5. 打包与进阶把系统变成能双击运行的 jar以及营业统计的 SQL 技巧5.1 用 Maven 打可执行 jar课程设计交作业时老师通常希望拿到一个能双击运行的 jar。用 Maven 的maven-shade-plugin可以把依赖一起打进去。build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.0/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.restaurant.MainFrame/mainClass /transformer /transformers /configuration /execution /executions /plugin /plugins /build执行mvn clean package后target目录下会生成一个xxx-jar-with-dependencies.jar双击或java -jar都能运行。注意mainClass要写全限定类名且该类必须有main方法。5.2 营业统计用 SQL 聚合代替 Java 循环很多同学做「今日营业额」时先把订单全查出来再在 Java 里for循环累加。数据量一大就慢而且代码冗长。正确做法是让 MySQL 做聚合。-- 今日营业额与订单数 SELECT COUNT(*) AS order_count, IFNULL(SUM(total_price), 0) AS revenue FROM orders WHERE status 1 AND DATE(create_time) CURDATE(); -- 菜品销量排行前 10 SELECT d.name, SUM(oi.quantity) AS sold FROM order_item oi JOIN dish d ON d.id oi.dish_id JOIN orders o ON o.id oi.order_id WHERE o.status 1 GROUP BY d.id, d.name ORDER BY sold DESC LIMIT 10;IFNULL处理没有订单时SUM返回NULL的情况避免 Java 端空指针。DATE(create_time) CURDATE()只取日期部分比较适合按天统计。如果数据量大建议在create_time上建索引。5.3 一个我踩过的坑别在 EDT 里弹模态对话框等数据库最后说一个我实际翻车过的场景。早期版本里我在「提交订单」按钮的监听器里直接调用了submitOrder而submitOrder里又用JOptionPane弹成功提示。结果在库存不足触发回滚时JOptionPane在事务未结束时就弹出来用户点确定后事务才回滚界面显示的顺序完全乱了。后来我把所有数据库操作和提示都拆开先在后台线程执行事务拿到结果后再用SwingUtilities.invokeLater弹提示。这个习惯让我后面做任何 Swing 数据库的项目都少了很多玄学问题。如果你也在做这个系统我的建议是先把dish、orders、order_item三张表和事务扣库存跑通再回头美化界面。界面丑一点没关系数据错一次答辩就得多解释十分钟。希望帮到你。本文还有配套的精品资源点击获取