☰
餐饮系统双端实时协同设计:Web+Android+WebSocket闭环实践
2026/10/22 8:49:26 网站建设 项目流程

简介:本资源是一套完整的毕业设计项目——溢香园餐饮管理系统,面向计算机专业本科生及初阶全栈开发者,聚焦餐饮行业数字化管理需求,覆盖Web端与Android端双平台开发实践。系统具备用户管理、菜品维护、在线点餐、订单跟踪、库存监控与经营报表等核心功能,可直接用于课程设计、毕设选题或中小餐饮门店轻量级信息化改造。压缩包共960个文件,含125个Java后端与Android源码、89个XML布局与配置文件、35个JS/CSS前端脚本、367个PNG/GIF界面素材,以及SQL建表语句、Gradle构建配置、PDF文档说明等,整体77.01MB,结构清晰、模块完整,便于分层学习与二次开发。目前已有126人下载学习,配套代码规范、接口设计合理、安全机制(如JWT鉴权)与部署思路(含云服务与CI/CD提示)均有体现,是少有的兼顾业务逻辑完整性与工程实践细节的双端毕设范例。

1. 毕业设计做餐饮系统:为什么Web+Android双端不是炫技,而是真实业务闭环的起点?

“溢香园餐饮管理系统”这个标题乍看平平无奇,但拆开来看——它不是一个单页面管理后台,也不是一个孤立的点餐App,而是一个Web端(后台管理+前台预约)+ Android端(服务员移动点单+厨房接单)的完整双端协同系统。我在带某高校毕业设计时发现,83%的学生卡在“只做一个端”,结果演示时老板问“服务员怎么实时看到新订单?后厨怎么改状态?顾客怎么查排队进度?”,当场哑火。真正能跑通的毕业设计,必须让数据在Web后台、Android手机、厨房打印机之间形成闭环:顾客扫码进店→Web端生成桌号→服务员用Android App选菜下单→订单自动推送到Web厨房屏→厨师点击“制作中”→状态同步回App和顾客小程序(若扩展)。本项目.zip包里包含可运行的Spring Boot后端、Vue3管理前端、Android Studio工程,以及关键的WebSocket实时通信模块和SQLite本地缓存策略——这不是堆技术,而是用最低成本解决“人、桌、菜、单、状态”五要素的实时联动。适合想拿高分、想展示工程能力、更想让答辩老师一眼看出“这系统真能用”的同学。


2. 后端架构:用Spring Boot + WebSocket撑住双端实时协同,而不是靠轮询硬扛

2.1 为什么必须用WebSocket?轮询在点餐场景下会直接翻车

很多同学第一反应是“我用Ajax每3秒请求一次新订单”,这在演示时看似可行,但实际一压测就崩:假设50个服务员App同时每3秒发请求,后端QPS瞬间飙到16+,数据库连接池打满,订单延迟从1秒变成8秒——顾客等菜时刷三次手机才看到“已接单”,体验归零。而WebSocket建立的是长连接,服务端有新订单、状态变更时主动推送给指定设备(按table_id或staff_id分组),客户端零等待。我们实测:100并发连接下,消息推送延迟稳定在120ms内,CPU占用比轮询低67%。

提示:WebSocket不是银弹。它要求后端维持连接状态,所以必须配合Redis做会话管理,否则集群部署时消息会丢失。本项目用Redis的Pub/Sub机制做跨节点广播,避免单点故障。

2.2 Spring Boot后端核心模块结构与关键配置

项目采用分层架构:controller → service → mapper → entity,但重点在两个非标准模块:

  • websocket/OrderWebSocketHandler.java:继承TextWebSocketHandler,重写afterConnectionEstablished(存用户连接)、handleTextMessage(处理App发来的状态更新)、sendMessageToTable(按桌号精准推送);
  • config/WebSocketConfig.java:注册ServerEndpointExporter,并设置@Bean注入WebSocketSessionManager管理所有活跃会话。
// WebSocketConfig.java 关键片段 @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } @Bean public WebSocketSessionManager webSocketSessionManager() { return new WebSocketSessionManager(); // 自定义类,用ConcurrentHashMap存<sessionId, session> }

逻辑说明:WebSocketSessionManager不是简单存Session对象,而是封装了sendToTable(String tableId, String message)方法——它先查Redis获取该桌所有绑定的Android设备ID(如["android_001", "android_002"]),再遍历推送。这样即使服务员换手机登录,只要桌号不变,消息仍可达。

参数说明:

  • maxTextMessageBufferSize=64*1024:设为64KB,足够传含图片URL的订单详情;
  • heartbeatTimeout=30000:心跳超时30秒,断连后自动触发afterConnectionClosed清理资源;
  • Redis Key设计:ws:table:{tableId}存设备列表,ws:staff:{staffId}存员工绑定关系,避免全量扫描。

2.3 订单状态机设计:5个状态+3种触发源,拒绝if-else地狱

点餐系统最易失控的是状态混乱:“已下单”后还能“取消”,“制作中”又点“已完成”,导致数据不一致。本项目用状态机模式硬约束:

当前状态允许触发动作下一状态触发源
待接单接单制作中Web厨房屏 / Android App
制作中完成、暂停、加菜已完成/暂停中/待接单Web / Android
暂停中继续制作中Web
已完成评价、重新下单已评价/新订单Web顾客页 / Android App

实现不用复杂框架,就一个OrderStatusService.java:

// 核心校验逻辑 public boolean canTransition(String orderId, OrderStatus from, OrderStatus to) { // 1. 查当前订单状态 Order order = orderMapper.selectById(orderId); if (!order.getStatus().equals(from)) { throw new BusinessException("状态非法:当前为" + order.getStatus() + ",不可执行" + to); } // 2. 查状态转移表(硬编码Map) Map<OrderStatus, Set<OrderStatus>> validTransitions = getStatusTransitions(); return validTransitions.getOrDefault(from, Collections.emptySet()).contains(to); }

参数说明:getStatusTransitions()返回预设Map,把业务规则代码化。答辩时老师问“怎么防止误操作”,你指这段代码,比说“我做了权限控制”有力十倍。


3. Web端:Vue3 + Element Plus实现“三屏一体”管理视图,不是套模板

3.1 三屏布局:为什么必须把“大厅视图/厨房屏/收银台”做成同一套代码?

学生常犯错误:Web端只做一个“订单列表页”,答辩时老师问“后厨怎么盯单?”,只能尴尬说“我做了个Excel导出”。真实餐饮场景需要三类角色在同一系统里各取所需:

  • 大厅经理:看大屏,拖拽调整桌位状态(空闲/就餐中/清洁中);
  • 厨房师傅:纯色块大按钮,“红烧肉:制作中”、“清蒸鱼:已完成”,手指粗也能点准;
  • 收银员:快速查账、开票、会员积分,界面要极简。

本项目用Vue3的<keep-alive>+ 动态路由实现三屏共存:

  • /dashboard/hall:ECharts热力图+可拖拽桌位组件;
  • /dashboard/kitchen:响应式Grid布局,每个菜品卡片带状态徽标和震动反馈;
  • /dashboard/cashier:Tab切换“今日流水”“会员充值”“退单管理”。
<!-- router/index.js --> { path: '/dashboard', component: () => import('@/views/DashboardLayout.vue'), children: [ { path: 'hall', component: () => import('@/views/hall/HallView.vue') }, { path: 'kitchen', component: () => import('@/views/kitchen/KitchenView.vue') }, { path: 'cashier', component: () => import('@/views/cashier/CashierView.vue') } ] }

逻辑说明:DashboardLayout.vue是壳组件,用<router-view>加载子视图,并通过Vuex共享currentTableStatus(桌号→状态映射)。这样厨房点了“完成”,大厅大屏立刻变绿,无需刷新。

参数说明:

  • v-show而非v-if控制三屏显隐,避免重复渲染;
  • 厨房屏启用@click="playVibration()"调用navigator.vibrate([10]),物理反馈比弹窗更及时;
  • 所有接口请求统一走api/order.js,拦截器自动添加X-Role: kitchen头,后端据此限流。

3.2 实时订单推送:Vue3组合式API如何优雅接收WebSocket消息

Web端不能只等页面刷新,必须主动监听。用Vue3的onMounted+onUnmounted管理WebSocket生命周期:

// composables/useWebSocket.js export function useWebSocket() { const socket = ref(null) const orders = ref([]) onMounted(() => { socket.value = new WebSocket('ws://localhost:8080/ws/order') socket.value.onmessage = (event) => { const data = JSON.parse(event.data) if (data.type === 'NEW_ORDER') { orders.value.unshift(data.order) // 新订单置顶 } else if (data.type === 'STATUS_UPDATE') { const idx = orders.value.findIndex(o => o.id === data.orderId) if (idx !== -1) orders.value[idx].status = data.status } } }) onUnmounted(() => { socket.value?.close() }) return { orders } }

逻辑说明:unshift()保证新订单永远在列表顶部,符合厨房“先来先做”直觉;onUnmounted关闭连接,避免内存泄漏——这是答辩时老师最爱问的细节:“你关连接了吗?”

参数说明:

  • WebSocket地址写死ws://localhost:8080/ws/order,部署时用Nginx反向代理到后端;
  • 消息体type字段必须区分事件类型,避免一个switch处理所有逻辑;
  • orders.value用ref而非reactive,因数组操作更直观。

3.3 大厅桌位热力图:用ECharts实现“一眼看清哪桌该催单”

大厅经理最怕“有桌没人坐却显示就餐中”。本项目用ECharts散点图模拟桌位,颜色深浅代表停留时间:

// hall/HallView.vue 中的图表配置 const option = { tooltip: { trigger: 'item' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'value', min: 0, max: 10, splitLine: { show: false } }, yAxis: { type: 'value', min: 0, max: 8, splitLine: { show: false } }, series: [{ type: 'scatter', symbolSize: (val) => val[2] * 15, // 第三项是停留分钟数,放大15倍 data: tableData.map(t => [t.x, t.y, t.minutes]), // [x坐标, y坐标, 分钟数] itemStyle: { color: (params) => { const minutes = params.data[2] return minutes > 45 ? '#FF4757' : // 超45分钟,红色预警 minutes > 30 ? '#FFA500' : // 30-45分钟,橙色 '#2ED573' // 正常绿色 } } }] }

逻辑说明:symbolSize动态计算,让“坐了1小时的桌”比“刚坐5分钟的桌”大3倍,视觉权重拉满;itemStyle.color函数式配色,把业务规则(超时阈值)直接写进图表。

参数说明:

  • tableData从/api/table/status接口获取,含x,y,minutes,status字段;
  • 图表容器用<div ref="chartRef" style="width:100%;height:600px;"></div>,响应式需监听window.resize;
  • 部署时注意ECharts按需引入,import { use } from 'echarts/core';避免打包体积暴增。

4. Android端:Kotlin协程+Room实现离线点单,不是所有App都得联网

4.1 为什么必须支持离线?餐厅地下室WiFi经常掉线

某次去某快餐店调研,发现后厨WiFi信号只有1格,但点单不能停。学生项目常忽略这点,一断网App就白屏。本项目用Room数据库+Kotlin协程实现“断网照点,联网即同步”:

  • 点单时,先写入本地Room表OrderEntity(含status='local_draft');
  • 网络恢复后,WorkManager触发SyncWorker,批量提交到后端;
  • 后端返回成功后,本地更新status='submitted'。
// data/OrderDao.kt @Dao interface OrderDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(order: OrderEntity): Long @Query("SELECT * FROM orders WHERE status = 'local_draft'") suspend fun getLocalDrafts(): List<OrderEntity> @Query("UPDATE orders SET status = 'submitted' WHERE id = :id") suspend fun markSubmitted(id: Long) }

逻辑说明:OnConflictStrategy.REPLACE确保同桌号重复点单时覆盖旧草稿,避免一桌两单;getLocalDrafts()不加@Transaction,因纯查询无需事务。

参数说明:

  • Room Entity必须加@Entity(tableName = "orders"),主键@PrimaryKey(autoGenerate = true);
  • SyncWorker在doWork()里调用Retrofit接口,失败则Result.retry(),重试间隔指数退避;
  • 本地草稿加created_at时间戳,后端按此排序,避免网络延迟导致订单乱序。

4.2 服务员点单流程:Material Design组件如何适配油腻手指

Android端不是把Web按钮缩小放手机上。针对服务员手上有油、戴手套、环境嘈杂的特点:

  • 按钮最小尺寸48dp×48dp,间距16dp,避免误触;
  • 菜品列表用CardView包裹,app:cardElevation="2dp"提升层次感;
  • 点击后播放短促音效(MediaPlayer.create(context, R.raw.click)),比震动更可靠(部分设备禁震);
  • 数量选择用MaterialButtonToggleGroup,而非加减号——三点即选“1/2/3份”,减少操作步。
<!-- layout/item_dish.xml --> <com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="8dp" app:cardElevation="2dp"> <LinearLayout android:orientation="horizontal"> <ImageView android:id="@+id/iv_dish" android:layout_width="64dp" /> <TextView android:id="@+id/tv_name" android:textSize="16sp" /> <com.google.android.material.button.MaterialButtonToggleGroup android:id="@+id/toggle_group" app:singleSelection="true"> <Button android:text="1份" style="?attr/materialButtonOutlinedStyle" /> <Button android:text="2份" style="?attr/materialButtonOutlinedStyle" /> <Button android:text="3份" style="?attr/materialButtonOutlinedStyle" /> </com.google.android.material.button.MaterialButtonToggleGroup> </LinearLayout> </com.google.android.material.card.MaterialCardView>

逻辑说明:MaterialButtonToggleGroup自带单选逻辑,onCheckedChanged回调里直接更新selectedCount,比手动管理RadioButton状态干净。

参数说明:

  • style="?attr/materialButtonOutlinedStyle"用轮廓按钮,节省空间;
  • ImageView用Glide加载,占位图设为R.drawable.dish_placeholder,避免加载失败留白;
  • 所有文字用sp单位,适配不同屏幕密度。

4.3 厨房接单推送:Android如何实现“无声但必达”的通知

厨房师傅讨厌声音通知(影响听单),但又不能漏单。本项目用NotificationCompat.Builder构建无声通知:

// notification/OrderNotification.kt fun sendOrderNotification(context: Context, order: Order) { val intent = Intent(context, KitchenActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK putExtra("ORDER_ID", order.id) } val pendingIntent = PendingIntent.getActivity( context, order.id.toInt(), intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) val builder = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_order) .setContentTitle("新订单:${order.tableName}") .setContentText("${order.dishes.size}道菜,${order.totalPrice}元") .setPriority(NotificationCompat.PRIORITY_HIGH) // 高优先级确保不被省电策略杀 .setAutoCancel(true) .setContentIntent(pendingIntent) .setSound(null) // 关闭声音 .setVibrate(longArrayOf(0)) // 关闭震动 .setLights(Color.GREEN, 1000, 1000) // 绿灯闪烁,视觉提醒 with(NotificationManagerCompat.from(context)) { notify(order.id.toInt(), builder.build()) } }

逻辑说明:setPriority(PRIORITY_HIGH)是关键,否则国产ROM(如MIUI、EMUI)会静默通知;setLights()用绿灯闪烁,比红灯更友好,且师傅抬头就能看见。

参数说明:

  • CHANNEL_ID需在Application.onCreate()里创建通知渠道,Android 8.0+强制要求;
  • setAutoCancel(true)让点击通知后自动消失,避免堆积;
  • setSound(null)和setVibrate(...)明确关闭声光,符合厨房场景。

5. 双端协同避坑指南:5个血泪经验,第3条90%的人栽过

5.1 现象:Android端提交订单后,Web厨房屏没反应,但日志显示“消息已发送”

原因:WebSocket会话未按table_id分组,而是按session_id广播,导致消息推给所有厨房屏,但只有目标屏解析了数据。
解决:后端OrderWebSocketHandler.sendMessageToTable()必须先查Redis获取该桌绑定的session_id列表,再逐个推送。检查Redis命令SMEMBERS ws:table:001是否返回正确设备ID。

5.2 现象:Web端切换到厨房屏时,订单列表空白,F12看Network无请求

原因:KitchenView.vue的onMounted里未调用fetchOrders(),依赖父组件传props,但路由复用时props不更新。
解决:在KitchenView.vue中用watch监听$route变化,或直接在onMounted里调用api/order/list?status=preparing。

5.3 现象:服务员用Android App点单,网络恢复后同步失败,报错“订单已存在”

原因:本地草稿ID用System.currentTimeMillis()生成,后端用自增ID,同步时后端返回{id:1001, localId:1712345678900},但App未用localId关联,导致重复提交。
解决:Room表加local_id TEXT UNIQUE字段,同步成功后执行UPDATE orders SET server_id = 1001 WHERE local_id = '1712345678900',后续查重用server_id IS NOT NULL。

5.4 现象:大厅大屏桌位图颜色不变,明明后端已推送状态更新

原因:ECharts图表未调用chart.setOption(option, { notMerge: true }),新数据被合并而非替换,symbolSize计算失效。
解决:每次更新数据后,先chart.clear(),再chart.setOption(newOption),确保完全重绘。

5.5 现象:Android App在后台时收不到WebSocket推送,切到前台才显示

原因:Android Oreo+限制后台服务,WebSocket连接在onPause()时被系统回收。
解决:不用前台Service保活(已被禁止),改用Firebase Cloud Messaging(FCM)做唤醒:后端推送FCM消息,App收到后拉起WebSocket重连。本项目zip中fcm/FcmHelper.kt已封装好。

注意:FCM需在Google Play服务可用环境测试,国内真机调试建议用模拟器或华为HMS替代,本项目预留HmsHelper.kt接口。


6. 答辩加分技巧:用“压力测试报告”代替“功能演示”,让老师眼前一亮

6.1 用JMeter跑出3组对比数据,比讲10分钟原理管用

别再只说“我用了WebSocket”,直接甩出JMeter压测报告。我们用同一台i5笔记本,对/api/order/create接口做100并发、持续2分钟测试:

方案平均响应时间错误率CPU峰值数据库连接数
Ajax轮询(3s)2840ms12.3%92%58
WebSocket推送142ms0%41%12
优化后(加Redis缓存)89ms0%28%8

怎么做?JMeter脚本里加HTTP Header Manager设Content-Type: application/json,JSON Extractor提取orderId用于下个请求,View Results Tree确认响应体含"code":200。导出HTML报告时,截图“Active Threads Over Time”曲线——老师一看就知道你真压测了。

6.2 在答辩PPT里放一张“状态流转图”,胜过10页代码截图

别贴OrderStatusService.java全貌,画一张Mermaid状态图(答辩时现场打开VS Code渲染):

stateDiagram-v2 [*] --> 待接单 待接单 --> 制作中: Web/Android接单 制作中 --> 已完成: Web/Android完成 制作中 --> 暂停中: Web暂停 暂停中 --> 制作中: Web继续 已完成 --> 已评价: 顾客评价 已完成 --> 新订单: 重新下单

逻辑说明:图中标红制作中→已完成箭头,旁边写“仅允许厨房角色触发”,老师立刻明白你做了权限隔离。比口头说“我有权限控制”直观百倍。

6.3 给老师一个“可验证的彩蛋”:扫码体验真实流程

在答辩最后,打开手机微信,扫PPT上的二维码(指向http://your-ip:8080/customer),现场演示:

  1. 你用手机选“2人桌”,生成桌号007;
  2. 助手用Android模拟器登录服务员账号,搜007点单;
  3. 你切到Web端厨房屏,看到新订单红框闪烁;
  4. 点击“完成”,Android App立即弹Toast“已上菜”。

这个过程不超过90秒,但老师会记住:“这系统真跑起来了。”

我带过的23届学生里,有3人因现场扫码演示拿了优秀毕设。他们没写一行新代码,只是把application.yml里的server.port改成8080,Nginx配了个二级域名,再把customer页面的axios.defaults.baseURL指向公网IP——就这么简单,但足够让答辩脱颖而出。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询