2026/10/28 2:34:21

酒吧点餐小程序:多人共享、实时出单与灵活结算设计

酒吧点餐小程序:多人共享、实时出单与灵活结算设计 1. 为什么我要做一套酒吧点餐小程序先说结论酒吧这个场景用通用的餐饮点餐系统去做八成会做残。我在三年前接过一个单子客户是一家做精酿的小酒馆老板一开始想得很简单——扫码点单、线上支付、后厨出单跟快餐店没什么两样。结果上线第一周就出问题了客人一桌八个人各自扫码各自下单最后结账时谁付了哪杯酒对不上调酒师那边收到五条重复的“莫吉托”还有客人半夜两点在卡座里加单服务员完全没收到提示因为手机锁屏了。这就是酒吧场景和普通餐饮场景最本质的区别它不是“点菜-出餐”的线性流程而是“多人共享、高频加单、时段集中、结算复杂”的网状流程。你如果只是把快餐点餐的逻辑套过来那些细节会一个接一个地咬你。我后来把这套系统重做了一遍从数据模型到交互都按酒吧的实际动线重新设计前后跑了两年多中间经历过大促爆单、跨年夜的洪峰、也见过凌晨三点客人喝多了乱点单的情况。所以这篇我想把整个从技术选型到真正落地的过程完整拆一遍包括我踩过的坑、做错的判断、以及后来验证有效的方案。1.1 这套系统到底解决什么问题先把需求边界说清楚不然做着做着就跑偏了。一个酒吧点餐系统核心要解决的是四件事第一桌台与人的解耦。传统点餐系统里一个订单绑一个桌号就够了。酒吧不行一桌可能来三拨人A先到点了两杯B后到点了一扎C中途走了。所以数据模型上必须是“桌台 - 会话Session - 订单项”三层结构桌台是物理位置会话是一批人的消费周期订单项挂在会话下面。第二加单的实时性与防漏。酒吧的加单频率远高于正餐一晚上同一桌加五六次很正常。这就要求从下单到调酒台/吧台出单的链路必须稳而且要有明确的“已接单/制作中/已完成”状态回传不能像很多简易方案那样只做单向推送。第三结算的灵活性。谁买单AA按人分按杯分还是最后一个人全结这块如果不在设计初期考虑后期改动成本极高。我的做法是把“支付”和“订单”彻底分开订单只管记录消费支付是独立的结算动作可以一次结清也可以拆成多笔。第四运营侧的实时看板。老板最关心的不是技术多优雅是“现在几桌在消费、客单价多少、哪些酒卖得快、库存还够不够”。这些数据必须实时不能等第二天导出报表。1.2 适合谁来读这篇如果你正在给一家小酒馆、清吧、Live House 做数字化或者你自己是店主想搞清楚这套东西该怎么搭这篇能直接用。技术侧我会写清楚选型依据和关键代码结构产品侧我会讲清楚每个交互为什么这么设计。即使你不写代码前面关于业务拆解和避坑的部分也值得看一遍因为你大概率会遇到跟你对接的人没想清楚这些事。我不打算写一篇“技术栈罗列”那种文章网上一抓一大把看完你还是不知道怎么下手。我想讲的是在酒吧这个特定场景下每个决策背后的真实权衡是什么。2. 技术选型不是选最强的是选最不容易出事的技术选型这块我见过太多人一上来就堆砌微服务、消息队列、分布式缓存、K8s 编排一套下来服务器成本一个月几千块结果店里就二十张桌子日订单量峰值不到三百。这不是技术能力问题是判断力问题。酒吧点餐系统的技术选型核心约束只有三个低运维成本、高实时性、能扛住短时洪峰。围绕这三条来选答案会清晰很多。2.1 前后端技术栈的真实取舍前端我最终选的是微信小程序原生 部分页面用 uni-app。原因很实在客人不会为了点单去下载 App扫码即用是刚需小程序是唯一解。用原生而不是全量 uni-app是因为酒吧场景对首屏速度和支付调起速度极敏感——客人喝到一半想加单你让他等三秒加载转圈他可能就懒得点了直接喊服务员那这套系统的价值就掉了一半。后端我用的是Node.jsNestJS 框架 MySQL Redis。这里解释一下为什么不用 Java 或者 GoNode.js 的 I/O 模型天然适合这个场景。酒吧系统的请求特点是“大量小请求、长连接推送、并发不高但延迟要求低”。Node 的事件循环处理这种模式非常顺手而且前后端同语言我一个人维护两套代码切换成本几乎为零。Java 不是不行是重了。如果你团队本来就有 Java 积累用 Spring Boot 完全没问题。但如果是一个人或者两三个人的小团队Spring 那套配置和启动时间会让你在小项目里付出不成比例的维护成本。Go 性能确实好但生态在小程序对接这块不如 Node 顺手。微信生态的 SDK、支付回调的示例、各种坑的解决方案Node 的社区资料密度明显更高。数据库选 MySQL 而不是 PostgreSQL理由更朴素这个系统的数据关系非常简单就是桌台、会话、订单、商品、用户这几张表没有复杂的地理查询、没有 JSON 深嵌套、没有全文检索需求。MySQL 在这个量级下足够稳而且云厂商的托管服务更成熟出问题好排查。Redis 在这里的作用不是“为了用而用”它承担了两个关键职责一是桌台会话的实时状态缓存客人扫码进来先查 Redis 拿桌台状态避免每次打数据库二是下单的幂等锁防止客人手抖连点两下生成两笔订单。这两个用途都是刚需不是为了炫技。选型项我的选择备选方案关键判断依据前端载体微信小程序原生App / H5扫码即用无下载门槛后端框架NestJS (Node.js)Spring Boot / Go Gin小团队维护成本低生态贴合主数据库MySQLPostgreSQL数据关系简单托管成熟缓存Redis无会话状态 幂等锁刚需实时通信WebSocket轮询出单延迟要求低于 1 秒部署单台云服务器 DockerK8s量级不匹配运维成本优先2.2 实时通信方案为什么我放弃轮询改用 WebSocket这是整个系统里最关键的一个技术决策。一开始我用的是前端定时轮询每 5 秒请求一次订单状态。上线第一天就发现了问题一家店二十桌每桌平均三个人在线就是六十个客户端每 5 秒六十次请求一小时四万多次。听起来不多但问题是这些请求大部分是空响应浪费资源不说最要命的是延迟不可控。客人下单后出单提示最快 5 秒、最慢 10 秒才能到调酒师的屏幕上。在酒吧这种节奏下客人点完一杯酒等了十秒没反应就会开始喊人。这个体验是不能接受的。换成 WebSocket 之后链路变成小程序下单 → 后端写入数据库 → 通过 WebSocket 主动推送到吧台端和客人端延迟压到 200ms 以内。这里有个实操细节要说明WebSocket 连接要按“店铺 ID 角色”分组管理。吧台端订阅的是“本店所有新订单”客人端订阅的是“本会话的订单状态变化”。如果不做分组一条订单广播给所有连接客户端还要自己过滤白白消耗流量和性能。连接管理我用一个简单的内存 Map 加上 Redis 的发布订阅来兜底。单机部署时内存 Map 就够如果以后要多实例部署靠 Redis 的 pub/sub 把消息广播到各个实例。这样切换成本很低不用改业务代码。提示WebSocket 一定要做心跳保活和断线重连。酒吧里客人手机切后台、锁屏、信号弱的情况太常见了没有重连机制客人回来看到的还是旧状态。我的做法是客户端每 30 秒发一次 ping服务端 60 秒没收到就断开客户端监听到断开后自动重连并拉取一次全量状态。2.3 部署架构小步快跑不要一步到位部署这块我的建议可能跟很多人不一样先单机别上集群。一台 4 核 8G 的云服务器跑 Docker 容器MySQL 和 Redis 用云厂商的托管服务这点钱不能省数据丢了是真完蛋应用容器单实例起步。为什么因为酒吧的流量特征太明确了——集中在晚上 8 点到凌晨 2 点工作日和周末差距巨大节假日翻好几倍。平时的资源利用率很低你搞一套 K8s 集群百分之七十的时间在空转。真正需要的是“能弹性应对峰值”而不是“常态高可用”。我的实际做法是单实例常态运行节假日之前手动扩容到两三个实例用 Nginx 做负载均衡配合 Redis 的 pub/sub 保证 WebSocket 消息跨实例可达。这套方案简陋但可靠两年来没出过因为架构问题导致的事故。3. 数据模型设计这套系统的地基如果数据模型一开始设计错了后面所有的功能都是在补丁上打补丁。我第一版就是吃了这个亏——把订单和桌号直接绑定结果一桌多人分单的时候改了两周才把数据迁移干净。3.1 核心五张表的职责划分我把核心数据拆成五张表每张表的职责非常明确table桌台表只存物理信息——桌号、区域、容纳人数、二维码标识、当前状态空闲/使用中/待清台。这张表不存任何消费数据状态只表示物理占用。session会话表这是整个系统的核心。一个 session 代表一批客人在一张桌上的一次消费周期。字段包括 session_id、table_id、开台时间、关台时间、状态进行中/已结算/已关闭、总金额。order_item订单项表每一杯酒、每一份小吃就是一条记录。字段有 item_id、session_id、商品 ID、数量、单价、下单人可以是匿名的设备标识、状态待接单/制作中/已完成/已取消、下单时间。product商品表酒水信息包括名称、分类、价格、图片、库存、是否上架、规格比如“大杯/小杯”“加冰/不加冰”。payment支付表独立于订单项记录每一笔支付——支付金额、支付方式、关联的 session_id、支付时间、支付状态。一个 session 可以对应多笔 payment。这样设计的直接好处是一桌多人各自加单所有订单项都挂在同一个 session 下谁点的记录在下单人字段里但消费总额归集在 session 上。结账时按 session 算总价支付时允许拆成多笔AA 分摊就是生成多条 payment 记录逻辑非常干净。3.2 会话生命周期状态机比 if-else 靠谱session 的状态流转必须用状态机来管不能散落在一堆 if-else 里。我的定义是四个状态待开台 → 进行中 → 已结算 → 已关闭流转规则很明确客人扫码如果桌台空闲就创建 session 进入“进行中”所有订单项都完成且支付完成进入“已结算”服务员确认清台后进入“已关闭”。这里有个容易踩的坑客人扫码但没下单就走了session 会一直挂着。如果不处理第二天这张桌子就显示“使用中”新客人扫不进来。我的解法是加一个定时任务每小时扫一次把“进行中”且超过四小时没有任何订单项的 session 自动关闭同时把桌台状态复位。这个兜底逻辑看起来不起眼但上线后至少避免了几十次“桌子被占死”的客诉。注意session 的自动关闭不能直接删数据要标记为“已关闭”并保留记录。酒吧偶尔会有客人第二天回来补开发票或者对账数据删了就说不清了。3.3 商品规格的设计别把组合爆炸写进代码酒水的规格比菜品复杂得多。一杯威士忌可能有容量单份/双份、冰加冰/不加冰/冰球、配饮苏打/纯净水/无。如果按笛卡尔积去建 SKU很快就爆炸了。我的做法是规格与商品分离商品表只存基础信息另建一张 spec 表存可选的规格项订单项里用一个 JSON 字段存本次选择的规格组合。这样做的好处是加新规格不用改表结构坏处是无法用 SQL 直接统计“加冰的比例”。权衡下来我选了前者因为运营侧对后者的需求频率极低真要看的话从 JSON 里解析也做得到。4. 核心功能实现从扫码到出单的完整链路这一节我把最核心的三条链路拆开讲扫码进店、下单到出单、结算支付。每条链路我都会写清楚关键逻辑和踩过的坑。4.1 扫码进店一秒之内要完成的事客人扫码后的体验目标只有一个一秒内看到菜单。这背后要做几件事第一步二维码里带的是桌台的唯一标识不是店铺 ID。客人扫的是桌上的码系统通过这个码定位到具体桌台再关联到店铺。第二步请求到达后端后先查 Redis 里的桌台状态如果桌台“空闲”创建一个新 session返回 session_id 给前端同时把桌台标记为“使用中”如果桌台“使用中”直接把当前 session_id 返回客人加入已有会话如果桌台“待清台”提示“本桌正在清理请稍候或联系服务员”。这一条很重要不然客人扫进来看到上一个客人的账单就麻烦了。第三步前端拿到 session_id 后缓存到本地后续所有请求都带上它。这里有个细节同一个客人重复扫码不应该创建新 session也不应该重复加入。我的做法是用设备标识小程序的 openid 加一个本地生成的随机标识去重已经在这个 session 里的直接返回原数据。提示扫码接口一定要做防重放和节流。我遇到过有人拿二维码图片疯狂请求一秒钟几百次。简单的做法是按 IP 加桌台 ID 做令牌桶限流超过阈值直接返回 429不影响正常客人。4.2 下单链路幂等、校验、推送三件事下单是整个系统最容易出问题的地方。我把这一步拆成了三个必须做对的环节。第一个环节是幂等。客人手抖连点、网络超时重试都可能产生重复订单。我的做法是前端生成一个客户端订单号UUID随请求带上后端用SETNX写到 Redis键就是这个客户端订单号过期时间 10 分钟。如果键已存在直接返回上一次的结果不再创建新记录。这个方案实现简单效果很稳。第二个环节是库存与价格校验。这里绝不能信任前端传过来的价格。后端必须重新从数据库读取商品当前价格和库存做一次校验价格以服务端为准。库存这块酒吧的场景比较特殊——很多酒水是“无限量”的瓶装啤酒有些是限量的特调用的某款基酒。我的做法是商品表里加一个“是否管理库存”的布尔字段只有开启的商品才做扣减和校验。第三个环节是实时推送。订单写入数据库后立刻通过 WebSocket 推送到两个地方吧台端新订单提醒和客人端下单成功确认。推送的消息体我只带必要字段——session 号、商品名、数量、规格、备注不带完整对象减少序列化开销。这一环节有个我踩过的坑推送失败不能影响主流程。一开始我把推送写在事务里推送一失败整个下单就回滚结果 WiFi 抖一下客人的单就下不进去。后来改成推送放在事务提交之后失败只记日志靠客人端主动拉取兜底。这个改动看起来小但把下单成功率从百分之九十几提到了接近百分之百。4.3 出单与状态流转调酒师的操作体验决定成败这套系统最终是给吧台和调酒师用的他们的操作体验如果不好系统就会被弃用。我见过太多店系统上线一周服务员嫌麻烦又回到手写单子。所以出单端我做了几个针对性设计第一新订单必须有强提醒。声音加弹窗而且声音要能选因为不同店铺环境噪音不同。这个功能看起来low但实际最有效。我还加了一个“未读计数”显示在页面标题上调酒师切到别的页面也能看到还有几单没处理。第二状态切换要一步到位。从“待接单”到“制作中”到“已完成”每个状态一个大按钮手指点一下就切换不要弹窗确认。调酒师手上都是湿的操作要极简。第三支持批量操作。同一桌点了五杯一样的酒应该能一键全部标记完成而不是点五次。状态流转的实现在后端是个状态机只允许特定状态之间跳转当前状态允许流转到触发方待接单制作中、已取消吧台制作中已完成吧台已完成无-已取消无吧台或客人非法流转直接拒绝并记日志。这个约束避免了“已完成”的单子被误改回“待接单”导致重复出单的问题——这种事故在手工操作的系统里非常常见。5. 结算与支付这块做不好前面全白干结算的复杂度在酒吧场景里被放大了好几倍。正餐大多是一桌一单一次结清酒吧是一桌多单、多人分摊、还可能中途换人。5.1 支付与订单解耦的设计前面提过payment 表独立于 order_item。这个设计的具体价值在于一个 session 的总消费额是固定的但支付可以由多笔组成。比如一桌消费 380 元A 付了 200B 付了 180系统里就是两条 payment 记录session 的已付金额累加达到总额后自动标记“已结算”。AA 分摊的实现也就顺理成章前端请求“按人数平分”后端算出每份金额生成待支付记录每个人扫码支付自己那份。这里有个金额精度问题必须处理——380 除以 3 除不尽。我的做法是把余数加到第一份上保证各份之和严格等于总额避免出现差一分钱对不平的情况。5.2 支付回调的可靠性处理微信支付的回调是异步的而且可能重复推送。这块必须做幂等用支付单号做唯一索引回调进来先查是否已处理已处理的直接返回成功。还有一个坑我要重点说回调到达时session 可能已经被关闭了。比如客人支付成功但回调延迟了十分钟期间服务员误操作把台清了。这种情况下不能简单丢弃回调必须把支付记录补进去并标记为“异常待核”让老板能在后台看到并手动处理。这个兜底机制我在实际运营中救过至少两次账。5.3 退款与异常订单酒吧的退款场景主要是“点错了”和“酒没了”。我的规则是未接单的订单项客人可以自助取消自动退款已接单未完成的客人申请吧台确认后取消退款已完成的只能吧台操作需要填写原因。退款走微信支付的原路退回这个接口调用的成功率不是百分之百所以必须有重试队列。我用 Redis 的 list 做一个简单的延迟重试队列失败后每隔五分钟重试一次最多三次三次都失败就转人工。6. 踩过的坑与排查实录这一节是我最想写的部分因为前面那些内容你在官方文档里多少能找到影子但下面这些都是真金白银换来的。6.1 跨年夜爆单那次事故的完整复盘那是系统上线后第一个跨年晚上十一点开始订单量陡增。前半小时还正常到十一点四十吧台端开始出现“新订单不推送”的情况。客人那边显示下单成功吧台收不到。排查过程是这样的先看服务端日志WebSocket 连接数在正常范围没有异常断开再看 Redis发布订阅的频道有消息在发最后定位到问题在吧台端的 WebSocket 客户端——它订阅的消息队列积压了。因为跨年夜订单太多吧台端的处理逻辑里有个同步的日志上报往一个第三方统计服务发请求每个消息都发一次网络一慢就堵住了。根因很清楚在消息处理的同步链路里做了网络请求。修复方式是把日志上报改成异步队列消息处理只做内存操作和界面更新。改完之后延迟立刻恢复正常。这次事故给我的教训是任何在实时消息处理链路上的外部调用都必须是异步的而且要能丢弃。统计、埋点、日志这些东西丢几条没关系但绝不能阻塞主流程。6.2 常见问题速查表我把这两年遇到的高频问题整理成一张表遇到类似的可以直接对照排查现象可能原因排查手段处理方式客人扫码提示桌台被占用上次会话未正常关闭查 session 表该桌台状态手动关闭僵尸会话检查定时任务是否正常下单成功但吧台没收到WebSocket 断连或消息阻塞查吧台端连接状态、服务端推送日志加重连机制消息处理链路去同步调用支付成功但订单未结算回调丢失或重复处理失败查 payment 表与微信订单号比对补单机制标记异常待核同一订单重复出单状态流转未做约束查 order_item 状态变更日志加状态机约束禁止非法流转高峰期页面卡顿首屏请求过多或图片过大用开发者工具看网络面板图片压缩、接口合并、加本地缓存金额对不上浮点数计算精度问题检查金额计算是否用整数分全部改为以“分”为单位的整数运算注意金额计算这个坑我一定要强调——数据库里金额字段用整数存“分”不要用 float 或 double。0.1 加 0.2 不等于 0.3 这种事在支付系统里是致命的。我第一版用的 decimal虽然精度没问题但前端展示转换麻烦后来统一改成整数分前后端都清爽。6.3 几个不起眼但很值钱的实操心得心得一给每个店铺做配置隔离别写死。有的店要收服务费有的不收有的店支持自助取消有的必须服务员确认。这些差异如果写死在代码里每接一家新店就要改一次代码。我的做法是建一张配置表把这类开关全做成配置项新店接入只改数据不改代码。这个决定在当时多花了两天但后面接第二家、第三家店的时候省下的时间远超这个投入。心得二日志要带 session_id 和 trace_id。出问题的时候客人说“我下的单没了”你只能靠这两个 ID 去把整条链路串起来。没有它们排查基本靠猜。心得三给服务员做个“极简模式”。后台功能做全了之后我发现服务员根本不用那些复杂功能他们就要三样看哪桌在消费、改单、结账。所以我单独做了一个极简入口三个大按钮培训成本几乎为零。功能不是越多越好是越贴合使用者的真实动线越好。心得四一定要做灰度。新功能先在一张桌子上试或者先给一家店上观察一晚上再全量。酒吧的营业时间特殊出问题都在深夜你没有第二天早上慢慢修的机会。7. 上线之后运营视角的几个关键指标系统上线不是终点是起点。老板真正关心的是数据所以后台看板的设计也很重要。我做的看板包含几个核心指标实时桌台状态——哪桌在消费、开了多久、当前金额。这个是老板和店长看得最频繁的。时段销售曲线——按小时统计销售额和订单数。这个数据能帮老板判断什么时候该加人手什么时候该做活动。商品销售排行——哪些酒卖得好哪些滞销。这个直接指导采购和菜单调整。客单价与翻台率——这两个是经营的核心指标。酒吧的翻台率算法跟餐厅不一样因为很多客人是长时间停留的我的做法是按 session 时长分段统计而不是简单算次数。这些数据都从 session、order_item、payment 三张表里聚合出来加一些定时任务做预计算避免实时查询拖慢主库。7.1 数据看板的技术实现细节预计算这块我用的是最简单的方案每小时跑一次定时任务把上一个小时的聚合结果写进统计表。看板查询只查统计表不碰原始表。这个方案的问题是有最多一小时的延迟但实际运营中老板并不需要秒级的销售统计一小时完全够用。如果将来数据量大了可以换成按天预聚合加实时增量补偿的方式。但就目前这个量级简单方案就是最好的方案不要过度设计。8. 一些关于成本和周期的实话最后说点实在的。这套系统如果外包报价从两三万到十几万都有差别主要在后期维护和定制程度。如果自己组团队做两个熟悉小程序和后端的人第一版能用的版本大概需要四到六周这还是有经验的前提下。成本的大头不在开发在云服务器、数据库托管、支付通道的对接调试、以及上线后的持续维护。我见过太多人低估了最后一项——系统上线只是开始后面客人提需求、老板改规则、平台接口升级这些都是持续投入。我个人的建议是如果你是店主先想清楚你到底要什么。如果只是要个扫码点单市面上现成的 SaaS 有更便宜的方案如果你有多店管理、深度定制、数据要自己掌握这些需求那自建才有意义。别为了“有个自己的系统”而自建那是本末倒置。写到这里基本上把整个链路都过了一遍。这套系统我前后做了两年多从最初被各种细节折磨到现在能够比较从容地应对各种情况中间最大的体会是技术方案没有绝对的对错只有跟场景匹不匹配。酒吧这个场景最特殊的地方在于它是“社交场所里的消费行为”人的因素远大于流程因素所以设计的每一步都要问自己一句——喝了两杯酒的人能不能用得明白。这才是判断一个方案好不好的最终标准。