简介:这是一套面向Java初学者与课程设计学习者的餐厅点餐管理系统完整源码,基于Java Swing桌面界面与MySQL数据库实现,适合作为毕业设计、课程大作业或Swing+JDBC综合练习的参考方案。系统区分管理员与顾客两种角色:管理员可新增与管理套餐、处理订单、修改密码,顾客可注册登录、点餐、查看订单及修改个人信息,功能闭环较为完整。压缩包共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. Java+Swing+MySQL 餐厅点餐管理系统:从课程设计到能跑起来的桌面应用
很多 Java 课程设计选题里,「餐厅点餐管理系统」出现的频率高得离谱,但真正能拿出手的没几个。大部分情况是:界面用 Swing 拖几个按钮,数据往 MySQL 一塞,查询用Statement拼字符串,最后交上去能跑就行。可一旦答辩老师问一句「两个人同时点同一道菜,库存怎么扣」,或者「MySQL 连接断了程序为什么直接卡死」,就露馅了。
这个标题对应的东西其实很明确:用 Java 做桌面端,Swing 负责界面,MySQL 存菜品、订单、桌台这些数据,最终交付一个能点菜、能下单、能结账、能查流水的单机或局域网小系统。它适合两类人:一是正在做课程设计、需要一套完整可复现方案的学生;二是刚学完 Java 基础、想找一个能把面向对象、JDBC、集合、异常处理串起来的练手项目的人。下面我按实际做过的路径,把选型、建表、界面、数据一致性、打包这几件事讲透,中间会给出可直接抄的代码和参数。
2. 技术选型与数据库设计:为什么不用 JavaFX,表该怎么建
2.1 Swing 在 2025 年还值不值得用
先说结论:做课程设计和内部小工具,Swing 依然是最省事的选择。原因有三点。第一,JDK 自带,不需要额外配 Maven 依赖或 Scene Builder,javac加java两条命令就能跑,这对「java环境变量配置」还没搞利索的人来说很关键。第二,Swing 的组件模型足够简单,JFrame+JPanel+JTable这套组合能覆盖点餐系统 90% 的界面需求。第三,网上「java课程设计案例源码」里 Swing 占大头,遇到问题容易搜到答案。
JavaFX 当然更现代,但它需要单独引入模块,打包时还要处理module-path,对只想交个课程设计的人来说是额外负担。所以我的建议是:除非老师明确要求 JavaFX,否则就用 Swing。
选型定了,接下来是数据库。MySQL 用 8.x 版本,字符集统一utf8mb4,排序规则utf8mb4_0900_ai_ci。这里有个高频踩坑点:很多人装完 MySQL 用mysql -u root -p连不上,报error 2002 (hy000): can't 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 桌台表:桌号、座位数、状态 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占用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表:一桌一次下单对应一条 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:一条订单里每道菜的数量和小计 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表逻辑说明:dish表用stock字段控制库存,on_sale控制上下架,这两个字段是后面做并发扣减的关键。orders和order_item拆成两张表是标准做法,主表存总额和状态,明细表存每道菜的数量,这样查流水和统计销量都方便。外键约束建议保留,虽然有人为了性能会去掉,但在课程设计阶段,外键能帮你提前发现数据插入顺序错误。
参数说明:DECIMAL(8,2)表示总共 8 位、小数 2 位,最大 999999.99,对餐厅单价足够。utf8mb4是为了支持菜名里的生僻字和 emoji(有些餐厅菜单确实有)。ENGINE=InnoDB必须写,因为要用事务,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?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4"; 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必须加,否则可能报时区错误;useSSL=false在本地开发时避免 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()) { Vector<Object> 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, List<CartItem> 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" Main(Windows 用分号,Linux 用冒号)。
4.2 现象:两个收银员同时给最后一桌点同一道菜,库存变成 -1
原因:扣库存时没有加行锁或条件判断,两个事务同时读到stock=1,都执行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 加characterEncoding=utf8mb4,建库时用CREATE DATABASE restaurant DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;,建表也指定DEFAULT CHARSET=utf8mb4。如果已经建错,用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 加allowPublicKeyRetrieval=true,或者把用户认证方式改成mysql_native_password:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';。本地开发推荐前者,改 URL 就行。
5. 打包与进阶:把系统变成能双击运行的 jar,以及营业统计的 SQL 技巧
5.1 用 Maven 打可执行 jar
课程设计交作业时,老师通常希望拿到一个能双击运行的 jar。用 Maven 的maven-shade-plugin可以把依赖一起打进去。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <phase>package</phase> <goals><goal>shade</goal></goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.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三张表和事务扣库存跑通,再回头美化界面。界面丑一点没关系,数据错一次,答辩就得多解释十分钟。希望帮到你。
本文还有配套的精品资源,点击获取