资讯动态

JavaGUI订餐系统课设:Swing、状态机与数据库事务全解析

发布时间:2026/10/26 22:56:53 来源:尧图企业网站定制
简介这是一份基于JavaGUI实现的订餐管理系统完整源码与配套数据库主要面向Java课程期末大作业、毕业设计以及希望上手桌面应用开发的初学者能够解决课程设计中缺少完整可运行项目的问题。工程代码结构清晰既可直接运行也可作为理解Swing界面与JDBC数据库交互的入门范本。压缩包共126个文件含34个Java业务与界面类、16个JSP页面、15个XML配置文件、15张界面预览图、3个CSS样式表、1个SQL数据库脚本以及字体图标等静态资源整体11.62MB解压后目录分工明确便于定位核心模块其中Java类覆盖界面事件响应、商品更新、订单数据访问等逻辑SQL脚本可导入MySQL初始化表结构JSP则提供辅助管理页面整体形成从数据层到展示层的闭环。资源已有360人学习下载。读者可参考UpdateGoods、OrderDao等具体实现直接运行体验订餐流程并借鉴其分层写法快速改造成符合个人选题的毕设或期末项目。1. 这门JavaGUI课设正在吃掉你的周末从订单状态机到数据库事务的完整拆解如果你是计算机专业的学生或者刚转行Java后端、正在为课设发愁那“基于JavaGUI实现的订餐管理系统源码数据库”这类标题你一定在搜索引擎里翻到过很多次。先说一个反直觉的结论这类系统真正的难点从来不在GUI界面上而在于订单模块的状态流转和数据库端的并发一致性。Swing拖几个控件、拉个表格半天就能做完但要让“用户点餐→后厨接单→支付完成”这条链路在数据上不丢、不乱、不错才是课设拿高分、或者项目能真正跑起来的关键。这篇笔记我会顺着一条完整的设计路径来讲从表结构怎么建、JDBC还是ORM怎么选到订单状态机、SQL注入防护再到最后怎么验证你写的东西真的可用。中间穿插的是我这些年看过的学生项目里最容易翻车的几个坑——这些坑几乎每个初写者都会踩一遍。读完你不仅能交付一套能跑的源码还能在答辩时把设计理由讲明白。2. 不只“怼个界面”订餐管理系统的实体关系与表结构设计2.1 先画清楚业务边界一个系统最少需要几张表很多人拿到这类题目直接开写代码结果写到一半发现“订单不知道挂在哪个用户下面”“菜品分类改不了”……然后开始推倒重来。这类系统的核心业务其实很固定用户/会员管理、菜品与分类、订单与订单明细、以及可选但强烈推荐支付记录或餐桌状态。如果你做的是食堂/快餐场景那餐桌号必有一个字段如果做外卖场景则是配送地址。我一般会先画一张非常简单的ER图不需要工具纸笔即可。订餐系统的核心实体是“订单头”和“订单明细”它们是一对多的关系一个订单包含多个菜品项。用户表与订单头是一对多菜品表与订单明细是一对多。分类表与菜品是一对多。这些关系定了表结构基本就定了。用MySQL建表的DLL我贴在下面这是最小可运行版本CREATE DATABASE IF NOT EXISTS order_sys DEFAULT CHARSET utf8mb4; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(64) NOT NULL, -- 存SHA-256或加盐哈希不存明文 role TINYINT NOT NULL DEFAULT 1, -- 0管理员 1普通用户 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, sort_order INT DEFAULT 0 ); CREATE TABLE t_dish ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1, -- 1在售 0下架 FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, table_no VARCHAR(20), status TINYINT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2制作中 3已完成 4已取消 total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME NULL, FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, -- 冗余菜名防止菜品改名影响历史订单 price DECIMAL(10,2) NOT NULL, -- 冗余下单时价格 quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (dish_id) REFERENCES t_dish(id) );2.2 为什么订单明细要冗余菜名和价格这是我在评审课设时一定会问的问题。很多初学者只存了dish_id觉得关联查询就能拿到菜名和价格不冗余更“规范”。但业务上这个设计是危险的菜品改价了历史订单的金额就跟着变了。虽然课程设计里不一定会追溯历史账单但“下单当时的快照”才是账单该有的语义。订单表里的order_no我建议用程序生成而不是数据库自增格式比如“yyyyMMddHHmmss 三位随机数”。自增ID会暴露订单量而且在做订单状态推送、多端同步时字符串订单号比数字更不容易撞。t_order_item表里冗余了dish_name和price查询时就不需要再连着菜品表做JOINGUI列表渲染性能也会好一点。这个表结构踩过的坑还有两点提前给你打预防针金额字段别用FLOAT。FLOAT是近似值累计订单金额超过一定数值会出现0.10.2不等于0.3的问题。课设数据量小可能不暴露但如果你后续嵌入支付逻辑金额不对就是大事故。用DECIMAL(10,2)精确到分。外键别省。有人为了“图方便”不建外键删除菜品时订单明细里的dish_id就成了悬空引用。Swing端查订单详情时拿到一个不存在的菜品列表直接报NPE或空指针。外键确实影响一点写入性能但管理系统的写入量远没到那个量级建上更稳。2.3 数据库连接参数java.sql.SQLException是简历上最丑的一行字表结构建好后就要连数据库。很多同学的入门姿势是DriverManager.getConnection但项目一旦多开几个窗口就频繁爆“Too many connections”。原因其实很简单每次getConnection都是新连接用完不关连接数被耗尽了。课程设计阶段我建议至少用一个简单的连接池比如HikariCP或DBCP。不过有些学校不允许引入太多外部依赖那也要用一个静态工具类管理连接至少做到统一获取、统一关闭。下面是我常用的写法public class DBUtil { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/order_sys?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明maximumPoolSize10对小系统足够线程里如果并发跑到十几个窗口10个连接已经能撑住connectionTimeout3000是拿连接的最大等待时间超过就报错避免界面假死serverTimezoneAsia/Shanghai必填否则MySQL 8.x会报时区错误。用连接池之后有个新麻烦连接池的连接生命周期比DriverManager长长事务用完连接不还回去还是会把池子占满。所以每段数据库操作都要在finally或try-with-resources里关闭Connection、PreparedStatement、ResultSet三件套。别嫌麻烦这是血泪经验。3. Swing响应式界面的关键EDT线程与TableModel的联动3.1 确定技术选型为什么是Swing而不是JavaFX课程设计如果用“JavaGUI”做关键词多数老师默认接受Swing。它的学习曲线平缓资料多出了问题在搜索引擎里能直接找到答案。JavaFX虽然界面效果更现代但很多学校的环境还是JDK 8跑Swing没配JavaFX插件。所以我的建议很直接课设求稳就Swing想炫技再考虑JavaFX。Swing的界面布局我用的是BorderLayout做主框架北边放工具栏、中间放选项卡面板。第一个选项卡是菜品列表用JTable第二个选项卡是订单管理还是JTable第三个才是用户管理。JTable的数据模型不要直接往里塞ArrayList要封装成TableModel。3.2 封装一个自动刷新的TableModel我见过有人把JTable的单元格数据直接放在二维数组里每次增删改就重新setValueAt刷新不及时而且极其容易下标越界。更规范的做法是继承AbstractTableModel让界面和业务数据解耦。下面是菜品类的一个例子public class DishTableModel extends AbstractTableModel { private final String[] columns {ID, 菜名, 分类, 价格, 状态}; private ListDish data; public DishTableModel(ListDish data) { this.data data; } public void setData(ListDish newData) { this.data newData; fireTableDataChanged(); // 通知JTable整体刷新 } Override public int getRowCount() { return data null ? 0 : data.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Dish d data.get(rowIndex); switch (columnIndex) { case 0: return d.getId(); case 1: return d.getName(); case 2: return d.getCategoryName(); case 3: return d.getPrice(); case 4: return d.getStatus() 1 ? 在售 : 下架; default: return null; } } Override public String getColumnName(int column) { return columns[column]; } }这个模型的关键不是getValueAt写得多漂亮而是fireTableDataChanged()。每次从数据库查出最新列表后调用setData()更新数据源并触发重绘。JTable只负责显示不持有业务数据。这样后续做搜索、筛选、分页都只需要替换data指向的List即可。3.3 不要在子线程里碰JComponentSwing EDT的规矩Swing不是线程安全的。所有界面的构建和更新必须在事件调度线程EDT上执行。很多同学在按钮事件里直接调用dao.query()数据量小的时候没事可一旦数据库响应慢了界面就会卡住不动表现为“白屏无响应”。正确做法是点击“加载订单”按钮后先禁用按钮再开子线程查数据库查完通过SwingUtilities.invokeLater把结果传回EDT更新表格。核心代码逻辑如下btnLoadOrders.addActionListener(e - { btnLoadOrders.setEnabled(false); new Thread(() - { ListOrder orders; try { orders orderDao.listRecentOrders(); } catch (SQLException ex) { SwingUtilities.invokeLater(() - showErrorDialog(ex.getMessage())); return; } ListOrder finalOrders orders; SwingUtilities.invokeLater(() - { orderTableModel.setData(finalOrders); btnLoadOrders.setEnabled(true); }); }).start(); });这段代码的逻辑拆解子线程里查数据库如果异常通过invokeLater切回EDT弹错误框正常的话同样切回EDT更新表格和按钮状态。注意不允许在子线程里直接调用JTable的任何方法这是Swing并发模型的红线。这个坑几乎每个初写者都踩过。如果验证时看到界面卡顿甚至光标变沙漏基本可以断定数据库查询跑在EDT上了。还有个治标不治本的办法是把查询超时设短但这掩盖了问题不是长久之计。3.4 下拉框联动刷新分类时避免吃掉用户正在选的值菜品筛选的UI一般长这样分类下拉框点“全部”或某个分类表格就只显示对应菜品。实现时有个细节很容易被忽略——JComboBox的选中项在数据刷新后可能被重置。如果用户刚选了“热菜”分类你重新加载分类列表并调用setModel它会默认选中第一项“全部”用户以为没选对实际是被程序重置了。解决办法是先记录当前选中项再重新填充模型最后恢复选中String selected (String) categoryBox.getSelectedItem(); categoryBox.setModel(new DefaultComboBoxModel(categoryList.toArray(new String[0]))); if (selected ! null) { categoryBox.setSelectedItem(selected); }这种做法看着简单但能避免用户在点选时被“回退”的糟糕体验。另外分类列表本身变动频率很低我建议做成启动时加载一次、在“新增分类”成功后手动刷新而不是每次下拉都查库减少数据库压力。4. 订单状态机的实现增删改查只是表面状态流转才是灵魂4.1 为什么订单状态不能随便UPDATE“数据库增删改查”是这类系统的基础但订餐管理的核心是订单状态。仔细看订单状态机的业务规则待支付订单可以取消一旦支付就不能直接跳到已完成必须先制作中。如果代码里写一句UPDATE t_order SET status 3 WHERE id ?就把订单改成“已完成”那支付环节就形同虚设。我的做法是写一个OrderStatus枚举枚举里带上所有合法流转public enum OrderStatus { PENDING_PAY(0, 待支付), PAID(1, 已支付), PREPARING(2, 制作中), COMPLETED(3, 已完成), CANCELED(4, 已取消); private final int code; private final String label; OrderStatus(int code, String label) { this.code code; this.label label; } public boolean canTransitTo(OrderStatus target) { switch (this) { case PENDING_PAY: return target PAID || target CANCELED; case PAID: return target PREPARING || target CANCELED; case PREPARING: return target COMPLETED; default: return false; } } }这个canTransitTo方法就是状态机的核心。在Service层更新状态时先查出当前订单状态再判断目标状态是否合法合法才执行UPDATE。注意顺序必须先查再改如果跳过了查询直接UPDATE业务规则形同虚设。还有一处坑支付操作会同时触发“订单状态改为已支付”和“记录支付时间”。这两件事必须在同一个事务里完成否则出现状态变了但paid_at为空的脏数据。下面是一个标准写法public void payOrder(int orderId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查询当前订单 Order current getOrderById(conn, orderId); if (current null) { throw new IllegalStateException(订单不存在); } // 2. 状态校验 OrderStatus from OrderStatus.of(current.getStatus()); if (!from.canTransitTo(OrderStatus.PAID)) { throw new IllegalStateException(当前状态不能支付); } // 3. 更新状态和支付时间 updateOrderStatusInTx(conn, orderId, OrderStatus.PAID, new Date()); // 4. 只有全部成功才提交 conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }这段代码体现的是数据库事务的原子性。如果你把这两步拆成两个DAO调用中间进程崩了订单状态和支付时间就永久不一致了。用连接池时尤其注意conn.setAutoCommit(false)之后如果没复位连接归还池子后再被借出下一个业务会莫名开启事务这是很隐晦的坑。4.2 数据库端的状态字段设计加一列version比加一把悲观锁更简单多个窗口同时操作同一个订单时会出现经典的更新丢失问题。比如收银台和后厨同时打开同一个订单后厨把状态从“已支付”改成“制作中”收银台此时把“已支付”改成“已完成”后厨的修改就被覆盖了。一个人做课设时很难触发这个并发场景但答辩老师可能会问“多用户同时操作怎么办”。最常用的稳妥手段是乐观锁在t_order表增加一列version INT DEFAULT 0更新时把version当成条件UPDATE t_order SET status 2, version version 1 WHERE id ? AND version ?如果影响行数为0说明在你读出订单之后、提交修改之前别人已经改了。此时程序应该提示“订单已被其他窗口修改请刷新后再试”。这比用SELECT FOR UPDATE加悲观锁简单也不会长时间占用连接。4.3 写操作的四条守则先查后改、多步操作利用事务、增删改返回影响行数、删除先查外键学生项目里最常见的翻车现场点击“删除菜品”后系统提示删除成功但菜品列表里它还在。原因通常是没看executeUpdate()的返回值。executeUpdate返回的是受影响行数如果删了个不存在的ID返回0界面却照旧提示成功。所以我在使用DAO层时增删改的返回值绝不省略public boolean deleteDish(int dishId) throws SQLException { String sql DELETE FROM t_dish WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, dishId); return ps.executeUpdate() 1; } }Service层拿到false时弹窗提示“删除失败菜品不存在”而不是无脑提示成功。类似的编辑菜品时如果数据没变化update返回0也算正常情况不能当异常处理。删除菜品还有一个外键坑如果某个菜品已经被订单明细引用直接DELETE会抛外键约束异常。行业里常见做法是先判断t_order_item里有没有引用SELECT COUNT(*) FROM t_order_item WHERE dish_id ?如果大于0要么禁止删除并提示“该菜品已有历史订单不能删除”要么做软删除也就是把status字段置为0下架。我一般推荐软删除因为订餐系统的菜品不允许物理删除否则账单明细里引用断掉查询历史订单时会出现空白行。5. 别忽略的那部分登录认证与SQL注入防护的落地写法5.1 登录接口为什么不能用拼接SQL实现因为“用户管理”是这类系统最容易被打分的模块很多同学在登录验证时直接用字符串拼SQLString sql SELECT * FROM t_user WHERE username username AND password password ;只写这一行你的系统就等着被“万能密码”打穿。比如用户名输入admin --注释符后面全部失效直接绕过密码登录。这就是教科书级的SQL注入。所有数据库访问都应该用PreparedStatement占位符public User findByUsernameAndPassword(String username, String passwordHash) throws SQLException { String sql SELECT * FROM t_user WHERE username ? AND password_hash ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, passwordHash); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapUser(rs); } } } return null; }PreparedStatement的参数会被MySQL驱动转义不会把单引号当成SQL语法的一部分。这条规矩不只适用于登录所有SQL都要走占位符。5.2 密码存储不能是明文MD5不够要加盐哈希课程设计里密码写成明文很常见但这属于答辩时容易被老师直接打低分的点。如果做的是演示系统用一个SHA-256工具类也能说过去。但如果想解释得专业一点就提“加盐哈希”这个概念每个用户注册时随机生成一个盐值盐值和密码拼接后做哈希存储。校验时用输入的密码加上存储的盐再算一次哈希比对结果。我用Java的标准库就能实现不需要额外依赖public static String hashPassword(String rawPassword, String salt) { String salted salt rawPassword; return DigestUtils.sha256Hex(salted); // Apache Commons Codec } public static String generateSalt() { byte[] bytes new byte[8]; new SecureRandom().nextBytes(bytes); return HexFormat.of().formatHex(bytes); }登录时不直接查用户表而是先用用户名查出用户和盐值再用输入的密码加盐做哈希和库里的password_hash比对。Swing端回车键可以绑定登录按钮不要把“密码错误”“用户不存在”区分得太细防止被遍历用户名。这点虽然对课设不重要但养成习惯比较好。5.3 逃避SQL注入的另一个方案MyBatis和JdbcTemplate如果界面上要大量使用动态查询、条件多字段组合纯JDBC的PreparedStatement写起来会很啰嗦。很多人到这一步就想上ORM框架了。对于SwingMySQL这类纯本地课程设计我的建议是不上MyBatis用JdbcTemplate即可。原因很简单MyBatis的XML映射文件和Mapper接口会把这门课设引向“JavaWebSSM”的知识体系和Swing的代码风格差异太大。JdbcTemplate比原生JDBC代码少一大截且SQL同样走占位符。但注意JdbcTemplate需要Spring依赖你有两条路可走一是保持原生JDBC逻辑更透明答辩容易讲清楚二是引入Spring-core和spring-jdbc代码量降低但架构变重。我给出的建议是以原生JDBC为主把PreparedStatement写熟练毕竟答辩老师问得最多的就是框架原理而非API熟练度。6. 验收测试与进阶技巧把正确性焊死在交付之前6.1 不靠“点一遍”来验收写一个DataChecker工具类交付前最后一天通宵点界面点着点着发现订单金额算错了——这是课设期间的经典剧情。与其人工反复点不如写一个不依赖GUI的控制台校验器。核心验证两件事第一订单总金额是否等于明细行累计金额。用SQL比SELECT o.id, o.total_amount, COALESCE(SUM(oi.price * oi.quantity),0) AS calc_amount FROM t_order o LEFT JOIN t_order_item oi ON oi.order_id o.id GROUP BY o.id HAVING o.total_amount calc_amount;这条SQL能查出所有手填金额和明细不一致的订单。凡是查到数据就是订单生成逻辑里有bug。第二状态机流转是否合法。可以用一个简单的Java main方法遍历所有订单把状态变化日志打出来人工瞄一眼有没有跳状态。常见异常是“待支付直接变成已完成”这种订单数据基本是UPDATE写得太粗暴了。6.2 进阶技巧给订单报表加“按天聚合”统计答辩时间有限但老师看到报表里能按“今日营收”“本周销量Top菜品”这类分组统计通过率会明显提升。在Swing里实现方式不复杂JTable的模型数据源改成查询SalesSummary对象列表SQL用GROUP BYSELECT DATE(created_at) AS stat_date, COUNT(*) AS order_count, SUM(total_amount) AS revenue FROM t_order WHERE status IN (1,2,3) GROUP BY DATE(created_at) ORDER BY stat_date DESC LIMIT 30;这一步不需要在GUI里引入图表库表格展示即可。如果要更直观一点可以用JFreeChart但它引入的依赖体积比较大课设时间紧张的话不必要。另外统计订购量的时候注意状态过滤条件不要把已取消的订单算进营收这是明显逻辑错误老师一眼能看出来。6.3 几个收尾经验数据库连接字符串、时区、字符编码最后说几个我每次接手类似项目都要检查的点它们对功能没有直接影响但会决定你会不会在演示现场翻车数据库连接字符串务必带characterEncodingutf8。否则Insert中文时乱码菜单里出现“鍟嗗搧”这种不认识的字符非常掉品。MySQL 8.x时区必须显式设置serverTimezoneAsia/Shanghai否则会报The server time zone value错误更隐蔽的是日期字段存储和读取相差8小时。Swing界面字体在Windows下用默认字体就很好如果放到Mac上中文可能发虚。要统一可以在启动时设置UIManager.put(Table.font, new Font(Microsoft YaHei, Font.PLAIN, 14));不要在生产环境用root账户连数据库课设里无所谓但答辩时老师如果问“既然能跑安全吗”你可以至少解释出连接池和最小权限的概念。虽然不要求真做但能说出来是加分项。最后分享一个我自己的习惯每次写完这种系统我会故意把数据库服务停掉再启动程序看Swing界面能不能弹出友好的“连接失败”提示而不是堆栈刷屏。如果一个系统在数据库离线时能优雅报错、而不是白屏卡死它的健壮性就已经超过大多数课设了。希望这个方向的实践笔记帮到你少熬夜多留点时间做测试。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑