无线点餐系统毕设实战:从技术选型到下单链路完整落地指南
简介这份毕业设计资源聚焦无线点餐系统面向高校计算机相关专业毕业生及需要完成课程设计的学生帮助解决餐饮场景下从传统人工点菜向移动端点餐转型的完整实现问题。压缩包共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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_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, ListOrderItemDTO 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 CopyOnWriteArraySetSession 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的区别前者支持通配符后者在allowCredentialstrue时不能用*。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演示不依赖外网二是提前录一段完整流程的视频万一现场环境出问题直接放视频。另外数据库提前准备好演示数据菜品图片用本地图片不要用外链避免图片加载不出来。最后说个我自己的习惯答辩前一晚把整个流程从头到尾走三遍每遍都换一个角色——第一遍当顾客第二遍当后厨第三遍当管理员。每遍都会发现一两个之前没注意的小问题比如某个按钮点不了、某个状态显示不对。这些问题在答辩时被老师发现远比你自己提前发现要尴尬得多。希望帮到你。本文还有配套的精品资源点击获取