1. 餐厅点餐系统设计与实现
十年前我第一次在快餐店看到触屏点餐机时,就被这种自助化操作吸引。如今随着移动互联网普及,餐厅点餐系统已经成为餐饮行业数字化转型的标配。本文将分享基于SpringBoot+Vue技术栈的餐厅点餐系统完整实现方案,这套系统目前已在三家连锁餐厅稳定运行两年,高峰期并发处理能力达到300+订单/分钟。
1.1 系统核心价值分析
传统纸质菜单点餐存在三大痛点:服务员工作强度大(平均每桌需往返3-5次)、菜品信息更新滞后(每次换季需重新印刷)、经营数据分析困难(依赖人工统计)。我们设计的点餐系统主要解决以下问题:
- 降本增效:顾客自助扫码点餐,减少服务员30%工作量
- 动态管理:实时更新菜品库存(特别对于限量供应菜品)
- 数据驱动:自动生成销售报表(包括时段销量、菜品关联分析等)
实际运营数据显示,采用该系统后餐厅翻台率提升22%,顾客平均等待时间减少8分钟。
1.2 技术选型考量
后端选择SpringBoot的三大理由:
- 嵌入式Tomcat简化部署(对比传统SSH框架)
- 自动配置机制快速集成Redis缓存(解决高并发下单问题)
- Actuator监控端点保障系统稳定性
前端选择Vue.js的关键优势:
- 组件化开发便于功能扩展(如加购动画、优惠券模块)
- Vuex状态管理完美应对多端数据同步(Pad点餐端+厨房显示屏)
- 相比React更轻量(gzip后仅20KB)
2. 系统架构与核心模块
2.1 整体架构设计
[移动端] ←WebSocket→ [SpringBoot] ←MySQL ↑ ↓ | [Redis] ↓ | [厨房打印机] [管理后台]系统采用经典前后端分离架构,关键设计点包括:
- 双通道通信:HTTP API处理常规请求 + WebSocket实时推送订单状态
- 二级缓存策略:Redis缓存热门菜品 + Caffeine本地缓存分类信息
- 分布式ID生成:美团Leaf方案解决集群环境ID冲突
2.2 核心数据库表设计
CREATE TABLE `dish` ( `id` BIGINT PRIMARY KEY COMMENT '菜品ID', `name` VARCHAR(32) NOT NULL COMMENT '菜品名称', `price` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '单价', `status` TINYINT DEFAULT 1 COMMENT '1在售 0停售', `category_id` INT NOT NULL COMMENT '分类ID', `stock` INT UNSIGNED DEFAULT 999 COMMENT '库存', `sales` INT UNSIGNED DEFAULT 0 COMMENT '月销量' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order` ( `id` VARCHAR(32) PRIMARY KEY COMMENT '订单号', `table_id` INT NOT NULL COMMENT '桌号', `total_amount` DECIMAL(10,2) UNSIGNED NOT NULL COMMENT '总金额', `status` TINYINT NOT NULL COMMENT '1待支付 2已支付 3已接单 4已完成', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;特别注意:金额字段必须使用DECIMAL而非FLOAT,避免浮点计算精度问题
3. 关键功能实现细节
3.1 高并发下单解决方案
当特价菜品上线时,可能出现瞬时高并发下单。我们采用三级防护策略:
- 前端限流:提交按钮添加loading状态防止重复点击
- Redis原子操作:
// 使用Lua脚本保证原子性 String script = "if tonumber(redis.call('get', KEYS[1])) >= tonumber(ARGV[1]) then " + "return redis.call('decrby', KEYS[1], ARGV[1]) " + "else return -1 end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList("dish:stock:" + dishId), String.valueOf(quantity));- 数据库乐观锁:
UPDATE dish SET stock = stock - 1 WHERE id = 1001 AND stock >= 13.2 订单状态机设计
使用枚举实现订单状态流转控制:
public enum OrderStatus { UNPAID(1, "待支付") { @Override public boolean canChangeTo(OrderStatus status) { return status == PAID || status == CANCELLED; } }, PAID(2, "已支付") { @Override public boolean canChangeTo(OrderStatus status) { return status == ACCEPTED || status == REFUNDED; } }; // 其他状态省略... }在状态变更时进行校验:
if (!currentStatus.canChangeTo(targetStatus)) { throw new IllegalStateException("非法状态变更"); }4. 典型问题排查实录
4.1 微信支付回调丢失
现象:顾客已完成支付但系统未更新订单状态
排查过程:
- 检查Nginx日志发现回调接口返回404
- 发现是SpringBoot的XSS过滤器拦截了微信回调参数
- 添加白名单路径解决:
xss: exclude-urls: - /api/payment/callback4.2 内存泄漏问题
现象:系统运行一周后出现OOM
MAT分析结果:
- 发现未释放的WebSocket Session对象
- 原因是未正确处理断开事件
解决方案:
@OnClose public void onClose(Session session) { sessionMap.remove(session.getId()); // 手动移除引用 try { session.close(); } catch (IOException e) { log.error("关闭session异常", e); } }5. 性能优化实践
5.1 接口响应优化
通过Arthas工具分析发现,菜品分类查询耗时主要来自:
- 重复查询(相同分类被多次请求)
- 全字段查询(实际只需id和name)
优化方案:
- 添加Caffeine本地缓存:
@Cacheable(value = "category", key = "#root.methodName") public List<Category> listAllSimple() { return categoryMapper.selectSimpleList(); }- 创建专用DTO投影查询:
<select id="selectSimpleList" resultType="CategorySimpleDTO"> SELECT id, name FROM category </select>优化后接口耗时从120ms降至15ms。
5.2 打印服务可靠性保障
厨房打印机常因网络问题离线,我们设计了三重保障机制:
- 本地缓存待打印订单:使用SQLite存储未成功打印的订单
- 断线自动重试:通过指数退避算法(1s, 2s, 4s...)重连
- 人工兜底方案:前台备用平板可手动重打
实现核心代码:
// 前端打印状态检查 function checkPrintStatus(orderId) { return new Promise((resolve) => { let retries = 0; const maxRetries = 5; function attempt() { api.getPrintStatus(orderId).then(res => { if (res.success) { resolve(true); } else if (retries < maxRetries) { retries++; setTimeout(attempt, 1000 * Math.pow(2, retries)); } else { resolve(false); } }); } attempt(); }); }6. 安全防护措施
6.1 防薅羊毛策略
针对优惠券滥用问题,我们实现以下防护:
- 设备指纹:通过canvas指纹+UA生成设备ID
- 行为验证:下单前进行滑块验证(超过3次/小时触发)
- 规则引擎:使用Drools实现复杂规则判断
示例规则:
rule "新用户首单限制" when $order : Order(isNewUser == true) $coupon : Coupon(type == "NEW_USER") $count : Number(intValue > 0) from accumulate( $o : Order(userId == $order.userId), count($o) ) then throw new RuntimeException("新用户只能使用一次优惠"); end6.2 SQL注入防护
除了常规的MyBatis参数绑定,我们还添加了:
- 敏感词过滤:拦截包含
drop、union select等关键词的请求 - 权限最小化:应用数据库账号仅具有必要表的CRUD权限
- 定期扫描:使用SQLMap对接口进行自动化检测
7. 部署与监控方案
7.1 容器化部署
使用Docker Compose编排服务:
version: '3' services: app: image: restaurant-order:1.0 ports: - "8080:8080" depends_on: - redis - mysql redis: image: redis:6-alpine volumes: - redis_data:/data mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql volumes: redis_data: mysql_data:7.2 监控指标配置
Prometheus监控关键指标:
- job_name: 'order-service' metrics_path: '/actuator/prometheus' scrape_interval: 15s static_configs: - targets: ['host.docker.internal:8080']Grafana监控看板包含:
- 实时订单量(按状态分类)
- 接口成功率(近1小时)
- 系统资源占用(CPU/内存)
- 菜品销售TOP10
8. 项目演进方向
当前系统已在以下方面进行迭代:
- 智能推荐:基于历史订单实现协同过滤推荐
- 语音交互:集成ASR技术支持语音点餐
- 供应链对接:库存自动同步至供应商系统
特别在推荐算法方面,我们采用改进的ItemCF算法:
def item_similarity(train): # 计算共现矩阵 cooccur = defaultdict(int) item_user_count = defaultdict(int) for user, items in train.items(): for i in items: item_user_count[i] += 1 for j in items: if i == j: continue cooccur[(i,j)] += 1 # 计算相似度矩阵 sim = {} for (i,j), cnt in cooccur.items(): sim.setdefault(i, {}) sim[i][j] = cnt / math.sqrt(item_user_count[i] * item_user_count[j]) return sim这个点餐系统从最初的单店版发展到现在的连锁版,最大的体会是:技术方案必须预留扩展性。比如早期没考虑分店ID导致后期改造困难,现在所有表都强制包含shop_id字段。另外打印服务一定要有降级方案,我们曾因打印机故障导致整个餐厅停摆半小时,这个教训价值百万。