简介:这份毕业设计资源聚焦无线点餐系统,面向高校计算机相关专业毕业生及需要完成课程设计的学生,帮助解决餐饮场景下从传统人工点菜向移动端点餐转型的完整实现问题。压缩包共365个文件,约9.99MB,以109个class与75个java源码为核心,配合30个xml布局、18个jar依赖、11个jsp页面及61张jpg与18个gif界面素材,另含6份doc说明文档、4个db数据库文件,并附带apk安装包与dex文件,覆盖客户端与服务端两端代码。资源已有238人学习下载,内容包含登录、主菜单、点餐、更新等模块的Activity与Provider实现,以及PayServlet等支付相关逻辑,适合作为无线点餐系统从需求分析到编码落地的参考范例,便于读者理解Android客户端与后台数据交互的整体结构,并在此基础上进行功能扩展与二次开发。
1. 无线点餐系统毕设:从选题到跑通,一个能写进简历的落地路径
每年毕业季,后台总有人问同一个问题:毕业设计选无线点餐系统到底行不行?会不会太简单、太烂大街,答辩时被老师一句“这不就是个增删改查”怼回来?说实话,这个判断本身就跑偏了。无线点餐系统这个题目之所以年年有人做,恰恰因为它是一个“麻雀虽小五脏俱全”的完整业务闭环——它逼着你同时处理多端通信、实时状态同步、订单并发和权限隔离,这些才是答辩老师真正想看的东西。烂大街的不是题目,是那种只做了个静态菜单页、下单还得刷新页面的半成品。这篇笔记就按一线做项目的思路,把无线点餐系统从技术选型、数据库设计、核心下单链路到联调排错完整走一遍,目标很明确:让你做出来的东西能演示、能讲清、能写进简历,而不是答辩前三天拼凑出来的黑匣子。适合正在定题或已经开题、但不知道从哪下手把系统做扎实的毕业生。
2. 无线点餐系统的技术选型:为什么我劝你别一上来就上微服务
选型这一步翻车的人最多。很多同学一听到“系统”两个字,脑子里立刻蹦出 Spring Cloud、消息队列、Redis 集群,觉得架构越复杂越显得有水平。结果光是环境搭建就耗掉两周,最后业务逻辑一行没写,答辩时讲的全是中间件配置,老师一问“你这个下单接口怎么保证不超卖”就哑火了。无线点餐系统的本质是一个中小规模的实时业务系统,它的技术难点集中在“多端状态一致”和“订单链路可靠”,而不是高并发架构。所以选型的第一原则是:用你完全掌控得了的技术栈,把业务闭环做扎实。
2.1 前后端分离还是服务端渲染,先看你的演示场景
无线点餐系统通常涉及三个角色:顾客端(手机扫码点餐)、后厨端(接单出餐)、管理端(菜品和订单管理)。顾客端天然是移动端场景,所以前端大概率要用到 H5 或小程序。这时候就面临一个选择:是前后端彻底分离,前端用 Vue/React 调后端 API,还是用服务端渲染模板一把梭。
我的建议是,如果你的前端基础一般,优先选前后端分离但用最简单的方案——后端只提供 JSON 接口,前端用 Vue3 + Vite 起一个单页应用,顾客端和后厨端共用一套接口,靠路由和权限区分。这样做的好处是接口边界清晰,答辩时你能明确说出“顾客端调的是哪个接口、后厨端调的是哪个接口”,而不是一团模板代码里找逻辑。
如果你时间非常紧,比如只剩三周,那就退一步用服务端渲染,后端直接渲染页面,用 WebSocket 做局部刷新。但要注意,服务端渲染做移动端适配会比较痛苦,顾客扫码后的页面体验容易显得粗糙。
2.2 数据库选型:MySQL 够用,但表结构设计才是分水岭
数据库这块,MySQL 是绝大多数毕设的默认选择,也确实够用。无线点餐系统的数据量级,一个食堂档口或小型餐厅,日均订单几百到几千,MySQL 单机完全扛得住。真正拉开差距的不是数据库品牌,而是表结构设计。
我见过太多毕设的表结构是这样的:一张 orders 表里塞了顾客信息、菜品信息、订单状态、支付状态,字段几十个,还全是字符串。这种设计在演示时看不出问题,但一旦你要做“后厨端只显示未出餐订单”或者“统计某个菜品销量”这种功能,就得写一堆字符串匹配,代码又臭又长。
正确的做法是按业务实体拆表,至少要有:用户表、菜品表、订单主表、订单明细表、桌台表。订单主表只存订单级别的状态(待支付、已支付、制作中、已完成),订单明细表存每一道菜的快照信息(菜名、单价、数量、备注)。这样后厨端查询就是一句SELECT * FROM order_items WHERE status = 'pending',干净利落。
2.3 实时通信方案:WebSocket 不是必须,但用了会加分
无线点餐系统有一个核心体验点:顾客下单后,后厨端要能立刻看到新订单,不需要手动刷新。这个“立刻”就是实时通信要解决的问题。
常见方案有三种:轮询、长轮询、WebSocket。轮询最简单,前端每隔几秒调一次接口查新订单,但实时性差、请求量大,演示时后厨端会有明显延迟。长轮询稍微好一点,但实现起来比 WebSocket 还绕。WebSocket 是真正的双向通信,后厨端和顾客端可以保持长连接,订单状态一变就推送。
如果你的技术栈是 Spring Boot,集成 WebSocket 非常简单,加一个spring-boot-starter-websocket依赖,写一个配置类和一个消息处理器就行。Node.js 的话用ws库或者 Socket.IO 也很顺手。但要注意,WebSocket 在部署时会遇到反向代理配置问题,如果你用 Nginx 做转发,记得加Upgrade和Connection头,否则连接会一直失败。
提示:如果答辩环境网络不稳定,WebSocket 频繁断连会很尴尬。建议加一个降级方案,WebSocket 连不上时自动切回轮询,保证演示不翻车。
3. 从扫码到出餐:把下单链路拆成可复现的代码
这一章是整篇笔记的核心,我会把无线点餐系统最关键的下单链路拆开,从数据库建表到接口实现到前端调用,每一步都给出可抄作业的代码和参数说明。你照着做,至少能跑通一个完整的“顾客扫码点餐 → 后厨接单 → 订单状态流转”闭环。
3.1 建表:五张表撑起整个点餐业务
先看表结构。这里用 MySQL 8.0,字符集统一utf8mb4,排序规则utf8mb4_unicode_ci。建表语句如下:
-- 桌台表:每个二维码对应一个桌台 CREATE TABLE `dining_table` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `table_no` VARCHAR(16) NOT NULL COMMENT '桌号,如 A01', `qr_token` VARCHAR(64) NOT NULL COMMENT '二维码令牌,用于扫码识别', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1占用', PRIMARY KEY (`id`), UNIQUE KEY `uk_qr_token` (`qr_token`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 菜品表 CREATE TABLE `dish` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `category` VARCHAR(32) NOT NULL COMMENT '分类:热菜/凉菜/主食/饮品', `image_url` VARCHAR(255) DEFAULT NULL, `is_available` TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', PRIMARY KEY (`id`), KEY `idx_category` (`category`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 订单主表 CREATE TABLE `orders` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '业务订单号', `table_id` INT UNSIGNED NOT NULL, `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3已完成 4已取消', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_table_status` (`table_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 订单明细表:存菜品快照,避免菜品改价影响历史订单 CREATE TABLE `order_item` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_id` BIGINT UNSIGNED NOT NULL, `dish_id` INT UNSIGNED NOT NULL, `dish_name` VARCHAR(64) NOT NULL COMMENT '下单时的菜名快照', `unit_price` DECIMAL(10,2) NOT NULL COMMENT '下单时的单价快照', `quantity` INT NOT NULL DEFAULT 1, `remark` VARCHAR(128) DEFAULT NULL COMMENT '口味备注', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待制作 1制作中 2已出餐', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 用户表(简化版,只存顾客标识) CREATE TABLE `customer` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `open_id` VARCHAR(64) NOT NULL COMMENT '微信openid或设备指纹', `nickname` VARCHAR(32) DEFAULT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_open_id` (`open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;这五张表的设计逻辑:dining_table通过qr_token和二维码绑定,顾客扫码后前端拿到 token,后端反查桌台;dish表用is_available控制上下架,避免直接删数据;orders和order_item拆开,主表管状态流转,明细表管菜品快照;customer表用open_id做唯一标识,不存敏感信息。
参数说明:order_no建议用“日期 + 随机数”生成,比如20250612 + 6位随机,方便排查问题时按日期检索。status字段用 TINYINT 而不是 ENUM,因为 ENUM 在后续加状态时要改表结构,TINYINT 更灵活。
3.2 下单接口:一个事务里完成订单创建和库存校验
下单是核心链路,必须保证原子性。这里用 Spring Boot + MyBatis 演示,核心逻辑是:校验菜品是否上架 → 计算总价 → 插入订单主表 → 批量插入订单明细 → 返回订单号。
@Service public class OrderService { @Autowired private DishMapper dishMapper; @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper; @Transactional(rollbackFor = Exception.class) public String createOrder(Long tableId, List<OrderItemDTO> items) { // 1. 校验菜品可用性并计算总价 BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : items) { Dish dish = dishMapper.selectById(item.getDishId()); if (dish == null || dish.getIsAvailable() == 0) { throw new BizException("菜品已下架:" + item.getDishId()); } total = total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 生成订单号 String orderNo = "ORD" + System.currentTimeMillis() + (int)(Math.random() * 900 + 100); // 3. 插入订单主表 Orders order = new Orders(); order.setOrderNo(orderNo); order.setTableId(tableId); order.setTotalAmount(total); order.setStatus(0); // 待支付 orderMapper.insert(order); // 4. 批量插入明细,存快照 for (OrderItemDTO item : items) { Dish dish = dishMapper.selectById(item.getDishId()); OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(dish.getId()); oi.setDishName(dish.getName()); oi.setUnitPrice(dish.getPrice()); oi.setQuantity(item.getQuantity()); oi.setRemark(item.getRemark()); oi.setStatus(0); orderItemMapper.insert(oi); } // 5. 更新桌台状态为占用 // ... 省略 return orderNo; } }逻辑说明:整个方法加了@Transactional,任何一步失败都会回滚,避免出现“订单主表插入了但明细没插入”的脏数据。菜品校验放在最前面,防止下架菜品被下单。订单号用时间戳加随机数,保证唯一性且可读。
参数说明:tableId来自扫码后前端传回的桌台 ID,不要直接信任前端传的桌号,要用 ID 反查。items是前端传来的菜品列表,每个元素包含dishId、quantity、remark。注意quantity要做上限校验,比如单次下单不超过 50 份,防止恶意刷单。
3.3 后厨端实时接单:WebSocket 推送与状态流转
后厨端需要实时看到新订单。这里用 Spring Boot 的 WebSocket 实现,核心是维护一个会话池,订单创建成功后主动推送消息。
@Component @ServerEndpoint("/ws/kitchen") public class KitchenWebSocket { // 静态会话池,存储后厨端连接 private static final CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>(); @OnOpen public void onOpen(Session session) { sessions.add(session); System.out.println("后厨端连接:" + session.getId()); } @OnClose public void onClose(Session session) { sessions.remove(session); } @OnError public void onError(Session session, Throwable error) { sessions.remove(session); error.printStackTrace(); } // 供订单服务调用的推送方法 public static void broadcast(String message) { for (Session session : sessions) { if (session.isOpen()) { session.getAsyncRemote().sendText(message); } } } }订单创建成功后,在createOrder方法末尾调用KitchenWebSocket.broadcast(orderNo),后厨端收到消息后刷新订单列表。
前端连接代码:
// 后厨端连接 WebSocket const ws = new WebSocket('ws://localhost:8080/ws/kitchen'); ws.onmessage = function(event) { const orderNo = event.data; console.log('新订单:', orderNo); // 触发订单列表刷新 fetchOrderList(); }; ws.onclose = function() { // 断线重连,5秒后重试 setTimeout(() => { window.location.reload(); }, 5000); };逻辑说明:CopyOnWriteArraySet保证多线程下会话池的安全,broadcast方法遍历所有连接推送消息。前端收到消息后调接口刷新列表,而不是直接把消息内容渲染出来,这样保证数据一致性。
参数说明:WebSocket 路径/ws/kitchen要和后端配置一致。如果部署时用了 Nginx,需要在配置里加:
location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }少了Upgrade和Connection头,WebSocket 握手会失败,浏览器控制台会报WebSocket connection failed。
3.4 订单状态流转:用状态机管住后厨和顾客两端
订单状态不能随便改,要有明确的状态机。我一般用一张状态流转表来约束:
| 当前状态 | 允许的下一状态 | 触发方 |
|---|---|---|
| 0 待支付 | 1 已支付 / 4 已取消 | 顾客 |
| 1 已支付 | 2 制作中 / 4 已取消 | 后厨 / 顾客 |
| 2 制作中 | 3 已完成 | 后厨 |
| 3 已完成 | 无 | — |
| 4 已取消 | 无 | — |
后端更新状态时,先查当前状态,判断是否在允许的流转范围内,不在就抛异常。这样能防止后厨端误操作把“已完成”的订单又改回“制作中”。
public void updateOrderStatus(Long orderId, int targetStatus) { Orders order = orderMapper.selectById(orderId); int current = order.getStatus(); // 状态流转校验 if (!isValidTransition(current, targetStatus)) { throw new BizException("非法状态流转:" + current + " -> " + targetStatus); } order.setStatus(targetStatus); orderMapper.updateById(order); // 推送状态变更给顾客端 CustomerWebSocket.broadcast(orderId + ":" + targetStatus); }4. 联调阶段最容易翻车的四个坑:扫码、并发、跨域、断连
代码写完了不代表能跑通。联调阶段才是血泪经验的集中爆发区。下面这四个坑,我几乎在每个无线点餐系统项目里都见过,提前知道能省你至少三天。
4.1 扫码后桌台识别失败:token 生成和解析的坑
现象:顾客扫码后页面提示“桌台不存在”,但数据库里明明有这条记录。
原因通常有两个:一是二维码里的 token 带了 URL 编码,前端解析时没解码;二是 token 生成时用了特殊字符,比如+、/,在 URL 传输中被转义了。
解决:token 用纯字母数字生成,比如 UUID 去掉横线,或者用SecureRandom生成 Base62 字符串。前端解析时先decodeURIComponent再传给后端。
// 从 URL 参数中获取 token const urlParams = new URLSearchParams(window.location.search); const token = decodeURIComponent(urlParams.get('token'));4.2 同一桌台并发下单:订单号重复和桌台状态错乱
现象:两个顾客同时扫同一桌的码下单,偶尔出现订单号重复,或者桌台状态被覆盖。
原因:订单号生成用了System.currentTimeMillis(),高并发下同一毫秒会生成相同订单号。桌台状态更新没有加锁,后一个请求覆盖了前一个。
解决:订单号加随机后缀,或者用数据库自增 ID 拼接。桌台状态更新用乐观锁,加version字段。
UPDATE dining_table SET status = 1, version = version + 1 WHERE id = ? AND version = ?;4.3 前端调接口报 CORS:开发环境和生产环境两套配置
现象:本地开发时前端调后端接口正常,部署到服务器后浏览器控制台报Access-Control-Allow-Origin。
原因:开发时可能用了代理,生产环境直接跨域。或者后端 CORS 配置只允许了localhost。
解决:后端统一配置 CORS,允许前端域名。Spring Boot 里加一个配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别:前者支持通配符,后者在allowCredentials=true时不能用*。
4.4 WebSocket 频繁断连:心跳机制和重连策略
现象:后厨端页面挂了一段时间后,新订单不再推送,刷新页面才恢复。
原因:WebSocket 长连接被中间网络设备断开,前端没有重连机制。
解决:前端加心跳,每隔 30 秒发一次 ping,后端回 pong。断连后自动重连,重连间隔递增,避免频繁请求。
let ws; let reconnectDelay = 1000; function connect() { ws = new WebSocket('ws://localhost:8080/ws/kitchen'); ws.onopen = () => { reconnectDelay = 1000; setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send('ping'); } }, 30000); }; ws.onclose = () => { setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 2, 30000); }; } connect();5. 答辩前怎么验证系统真的跑通了:一份可执行的检查清单
最后一章落到验证方法上。很多同学做完系统自己觉得没问题,一答辩就露馅,因为演示环境和开发环境不一样,或者老师问的某个边界情况根本没测过。我一般会在答辩前跑一遍下面这份检查清单,每项都手动过一遍,确保不翻车。
5.1 用 curl 和浏览器双端验证接口
先确保后端接口单独可用。用 curl 测下单接口:
curl -X POST http://localhost:8080/api/order/create \ -H "Content-Type: application/json" \ -d '{"tableId":1,"items":[{"dishId":1,"quantity":2,"remark":"不要辣"}]}'返回订单号说明接口正常。然后用浏览器打开前端页面,走一遍完整流程:扫码 → 选菜 → 下单 → 后厨端收到推送 → 后厨点出餐 → 顾客端看到状态变更。每一步都截图留存,答辩时可以直接展示。
5.2 边界情况测试:空订单、超量、重复提交
老师最爱问的边界情况,提前测一遍:
| 测试场景 | 预期结果 | 实际结果 |
|---|---|---|
| 不选菜品直接下单 | 提示“请选择菜品” | |
| 单次下单 100 份 | 提示“数量超出限制” | |
| 连续点击下单按钮 | 只生成一个订单 | |
| 下单后菜品被下架 | 订单仍能正常出餐 | |
| 后厨端断网后恢复 | 自动重连并同步订单 |
重复提交的防护,前端用按钮禁用,后端用幂等 token。简单做法是前端下单时生成一个requestId,后端用 Redis 存 5 分钟,重复的requestId直接返回已有订单号。
5.3 性能压测:用 JMeter 模拟 50 人同时点餐
毕设不需要做大规模压测,但至少要证明系统在并发下不崩。用 JMeter 建一个线程组,50 个线程,循环 10 次,调下单接口。观察响应时间和错误率。如果错误率超过 1%,检查数据库连接池配置,把maximumPoolSize调到 20 以上。
# application.yml 连接池配置 spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 300005.4 答辩演示的兜底方案
演示环境最怕网络抽风。我一般会准备两个方案:一是本地跑一套完整环境,用localhost演示,不依赖外网;二是提前录一段完整流程的视频,万一现场环境出问题,直接放视频。另外,数据库提前准备好演示数据,菜品图片用本地图片,不要用外链,避免图片加载不出来。
最后说个我自己的习惯:答辩前一晚,把整个流程从头到尾走三遍,每遍都换一个角色——第一遍当顾客,第二遍当后厨,第三遍当管理员。每遍都会发现一两个之前没注意的小问题,比如某个按钮点不了、某个状态显示不对。这些问题在答辩时被老师发现,远比你自己提前发现要尴尬得多。希望帮到你。
本文还有配套的精品资源,点击获取