酒吧这门生意白天看着安静一到晚上九点半之后就是另一套节奏卡座满员、散台翻台、服务员端着托盘在人缝里穿行吧台那边堆着一摞手写单子谁先谁后全靠喊。我在几个夜场项目里都见过同一个场景——高峰期点单靠吼出酒靠记结账靠算最后对账靠猜。酒吧点餐小程序系统要解决的从来不是把菜单搬到手机上这么简单它真正要接管的是桌台状态、下单节奏、吧台排队、库存扣减和结账对账这条完整链路。这篇内容面向的是打算自己动手做一套、或者正在评估要不要做的开发者和门店经营者我会从需求拆解一路讲到技术选型、数据模型、核心功能落地、夜间峰值处理再到实际踩过的坑把方案摊开讲透能直接抄的部分我会写清楚参数和代码不能直接抄的部分我会讲清楚为什么这么选。1. 需求拆解酒吧点餐和正餐点餐差在哪1.1 场景差异决定了技术形态很多人第一反应是拿正餐扫码点餐系统改一改就用实测下来基本都要返工。正餐的节奏是坐下来、看菜单、慢慢点、吃完结账一顿饭可能就下一单酒吧的节奏是开台、先来一轮、喝完了再加、加三四轮、最后一起结。这意味着订单不是一次性的而是围绕一个开台记录持续追加的。数据模型上如果按正餐那样一单一桌设计第二天你会发现同一张桌有七八条孤立订单对账时根本拼不起来。再一个是菜品结构。正餐的 SKU 通常是菜名份量酒吧的 SKU 是酒名容量温度整瓶/半打/单支是否加冰是否配果盘属性组合能翻出十几倍。像洋酒这类还涉及存酒业务客人这瓶没喝完存在店里下次来接着喝这就要求系统里有存酒记录和核销流程。我在一个项目里因为前期没做存酒模块开业第二周就被店长追着加需求改动量比预想大得多。第三是时间分布极度集中。工作日可能一晚上几十单周五周六晚上十点到凌晨一点这两个多小时能吃掉整晚七成的单量。这种脉冲式流量对系统的要求不是平均吞吐而是峰值扛不扛得住、降级方案有没有。1.2 角色与权限怎么划分才不乱酒吧里的角色比想象中多梳理不清楚后面权限系统会一团乱麻。我一般按这五类来切角色主要动作权限边界顾客扫码、浏览、下单、支付、催单、呼叫服务只能看自己桌的订单服务员代客下单、改单、退单、开台、换台、结账管辖区域内的桌台吧台/出品接单、制作、出酒确认、缺货标记只看订单明细不看金额店长全部数据、改价、免单、日结全店财务/老板报表、对账、导出只读关键是改价和免单权限必须收口。我见过一个店把改价权限开给所有服务员一个月下来流水对不上查了半天发现是现场给熟客抹零抹多了系统里没留痕。正确做法是改价必须走审批至少留下操作人、原价、新价、理由四个字段日结时能一键导出差异单。1.3 三个必须提前想清楚的边界问题第一个是先付还是后付。快消型酒吧清吧、livehouse适合下单即付减少跑单带卡座最低消费的适合开台付押金、离场结算。两种模式在代码里是两条不同的状态流转路径最好在配置层做成开关别写死。第二个是是否允许顾客自助开台。允许的话扫码即开台体验好但可能被路过的人误开不允许的话需要服务员确认多一步操作但可控。我的建议是首次扫码弹一个确认开台的轻确认成本很低但能挡掉大部分误操作。第三个是库存扣减的粒度。是按 SKU 扣还是按原料扣酒吧的酒水大多是整瓶出SKU 级扣减就够用但如果店里做特调鸡尾酒就得考虑基酒的原瓶消耗。前者简单后者要引入配方表工作量差一个量级。前期别贪SKU 级先跑起来。2. 技术选型前端、后端与实时通信的取舍2.1 小程序端原生还是跨端框架这个选择的判断标准其实只有一个——你的店有几家、未来半年会不会做第二个平台。只有一家店、只跑一个平台原生开发最稳包体积小、扫码唤起快、坑最少。如果打算同时上多个平台或者要复用到 H5、店内平板那就上跨端框架。我在实际项目里踩过的坑是跨端框架在低端安卓机上的首屏时间会比原生慢 200 到 500 毫秒酒吧环境本来就信号拥堵这个差距在排队扫码时会被人感知到。所以我的折中方案是——顾客端点单用原生写员工端吧台、服务员用跨端框架打包成 App 或 H5因为员工端对首屏不敏感但对多端复用需求高。包体积也要控制。小程序主包一般限制在 2MB 左右酒水图片如果用原图很容易爆。做法是把图片全部走 CDN 加参数裁剪列表页用 200x200 的缩略图详情页再加载大图主包只留图标和骨架屏。这一招能把主包压到 1MB 以内。2.2 后端语言别为了架构而架构单店或三五家店的规模微服务是负担不是优势。我推荐模块化单体一个进程、一个数据库代码按领域分模块桌台、订单、商品、支付、报表模块之间通过内部接口调用将来真要拆再拆。语言选型上我给个实际对比方案优势适合场景注意点Node.js开发快、WebSocket 生态好、前后端同语言中小型项目、实时推送多CPU 密集任务弱别在进程里跑重计算Java生态成熟、事务处理稳、招人容易连锁多店、需要强一致内存占用高小机器跑不动Go并发强、部署单文件、内存省峰值压力大、预算有限开发速度略慢于 Node我最近两个项目都用的 Node.js 加 TypeScript原因是吧台端要长连接推送Node 的事件循环天然适合这类 IO 密集场景。数据库用 MySQL 8缓存用 Redis这套组合在 2 核 4G 的机器上跑单店完全够用。2.3 实时通信轮询、WebSocket 还是 MQTT吧台端必须实时看到新订单服务员端要实时看到出品状态。三个方案的实测差异很明显短轮询实现最简单3 秒一次请求。问题是 10 个吧台终端就是每秒 3 次以上请求而且新订单平均延迟 1.5 秒高峰期吧台会抱怨单子来得慢。WebSocket延迟毫秒级双向通信能主动推。缺点是要处理断线重连、心跳保活服务端要维护连接数。50 个终端以内毫无压力。MQTT适合终端多、网络不稳定的场景有 QoS 等级保证。但引入 Broker 增加运维成本单店规模属于杀鸡用牛刀。我的选择是 WebSocket 加降级兜底连接正常走推送心跳超时或连续重连失败三次自动切回 5 秒轮询。这个兜底非常重要——酒吧的 WiFi 经常被上百台手机挤爆纯 WebSocket 方案在信号差的时候会静默丢消息吧台以为没单实际单子已经下了。心跳参数我一般设成客户端每 25 秒发一次 ping服务端 60 秒没收到就判定断线。这个数值比很多默认值保守原因是移动网络切换从 WiFi 到 4G时连接容易假死心跳太松会拖很久才发现。2.4 数据库与缓存的分工分工原则很简单钱和状态以 MySQL 为准速度和并发靠 Redis 挡。MySQL 里存订单、桌台、商品、支付流水、操作日志。Redis 里存三类东西桌台实时状态读多写多、商品库存计数原子扣减、接口幂等键和限流计数有过期时间。库存扣减这块要特别说一下。直接用SELECT ... FOR UPDATE加行锁在峰值时会排队几十个并发就能把响应拖到秒级。我用的方案是 Redis 加 Lua 脚本做原子预扣成功后再异步落库-- KEYS[1] stock:sku:{skuId} -- ARGV[1] 扣减数量 local stock tonumber(redis.call(GET, KEYS[1]) or -1) local qty tonumber(ARGV[1]) if stock 0 then return -2 -- 库存键不存在回源 end if stock qty then return -1 -- 库存不足 end redis.call(DECRBY, KEYS[1], qty) return stock - qty返回值设计成三种状态很重要-2表示缓存里没有这个键需要回源数据库加载-1表示确实不够大于等于 0 表示扣减成功并返回剩余量。少了-2这个分支缓存刚重启时会把所有商品都判成缺货这个坑我在上线第一天就踩到了。3. 数据模型设计桌台、SKU 与订单的三层结构3.1 核心表怎么建我把核心表分成三层场地层、商品层、交易层。场地层包括area区域如吧台区、卡座区、露台和table桌台。桌台表里除了桌号还要存区域 ID、容纳人数、最低消费、当前状态、当前开台记录 ID。最低消费这个字段看着不起眼实际很关键——离场结算时要判断是否达标不达标按最低消费收取。商品层是category分类、product商品如某品牌威士忌、sku规格如700ml 整瓶半打套餐。商品和 SKU 拆开的好处是列表页只查 product点进去才查 sku减少数据传输。交易层是session开台记录、order订单批次、order_item订单明细、payment支付流水。这里的关键设计是session 和 order 是一对多一次开台可以对应多个订单批次第一轮、第二轮但结账时按 session 汇总。这样既保留了加单的灵活性又保证了对账的完整性。3.2 桌台状态机要画死在代码里桌台状态如果没有严格的状态机最后一定会出现显示空闲实际有人的鬼故事。我固定的六个状态和流转规则状态含义可流转到IDLE空闲OPENINGOPENING已开台未下单ORDERING, IDLEORDERING点单中SETTLING, ORDERINGSETTLING待结账CLEANINGCLEANING清理中IDLELOCKED锁定维修/预留IDLE所有状态变更必须走统一的changeTableStatus方法方法内部做三件事校验流转合法性、写状态变更日志、发 WebSocket 广播。绝对不允许在业务代码里直接 update 状态字段这是我定下的死规矩违反过的项目无一例外都出现过状态漂移。还有一个细节开台要有超时自动释放。服务员开了台客人没来、或者客人走了没结账桌台会一直挂着。我设的是开台后 30 分钟内无任何订单状态从 OPENING 自动回落 IDLEORDERING 状态则由店长手动处理不自动释放避免把正在喝酒的客人误清台。3.3 加单、并台、换台的实现要点加单最简单往当前 session 追加一个新 order 批次即可注意带上批次号方便吧台区分这是第二轮。并台两桌合并稍微麻烦。做法是选一张主桌把从桌的 session 挂到主桌的 session 下从桌状态置为 SETTLING 并标记已并入 X 桌。结账时只结主桌从桌不再单独出单。历史订单的桌号字段保留原值不做修改这样审计时能追溯。换台本质上是修改 session 的 table_id。要注意同步两件事原桌状态回 IDLE新桌状态改为 ORDERING同时给吧台推一条桌号变更通知否则服务员端着酒找不到人。我在一个项目里漏了这步通知结果一晚上有三单酒送错桌。4. 核心功能落地从扫码到出酒的完整链路4.1 扫码进店与桌号绑定的防伪处理二维码里不能只放明文的桌号否则谁都能改。我的做法是生成一个带签名的 scene 参数格式大致是t12sab12cd34其中s是tableId 时间戳 密钥的哈希前 8 位。服务端校验签名不通过就跳到通用菜单页不绑定任何桌台只能看不能下单。落地页的处理逻辑按这个顺序判断先校验签名再看桌台状态。IDLE 状态提示确认开台OPENING 和 ORDERING 直接进菜单SETTLING 提示该桌正在结账LOCKED 提示该桌暂不可用。这个顺序不能反先判断状态再校验签名会给攻击者提供探测桌台状态的通道。还有一个容易被忽略的点同一张桌多人扫码。五个人同时扫如果不做处理会开出五个 session。解决办法是以 table_id 为 key 加 Redis 分布式锁开台操作串行化第二个人进来时读到已有 session 直接复用。锁的过期时间设 5 秒足够别设太长否则第一个人开台失败会把桌锁死。4.2 SKU 与规格别让用户在菜单里迷路酒吧菜单最大的问题是选择太多。一款威士忌有整瓶、半打、单支、加冰、常温、配什么软饮全列出来用户直接懵。我的处理是两级收敛列表页只展示商品和起步价点击后弹出规格选择面板面板里按容量—温度—搭配三个维度分组每组单选用大按钮默认值预选好。数据结构上用属性表加 SKU 表。属性表定义容量有哪些选项、温度有哪些选项SKU 表存具体的组合和价格。生成 SKU 时用笛卡尔积但要注意过滤无效组合——比如半打和单支是互斥的不能同时选。这个过滤规则建议在后台配置时手动勾选有效组合别用程序自动生成全部实际营业中总有一些组合是你永远不卖的。套餐的处理要单独说。酒吧常见的洋酒4 瓶软饮果盘套餐不能简单当成一个 SKU 卖因为要拆到吧台和果盘两个出品口。我的方案是套餐表加子项表下单时按子项分别生成 order_item但对外显示为一个套餐价格取套餐价。结算时按子项成本分摊方便后续算毛利。4.3 加菜、退菜、催单的状态同步这三个操作是高频动作也是状态最容易乱的地方。加菜走新增 order 批次不修改原订单。好处是吧台按批次制作服务员按批次送不会出现这杯是第二轮的还是第一轮的这种混乱。退菜要分外场和吧台两个视角。服务员在顾客端发起退单订单进入REFUNDING状态吧台端必须收到确认请求——因为酒可能已经开了。吧台确认未制作则直接退款确认已制作则转店长审批。这个双确认机制听着麻烦但能挡掉大量点了又不要的纠纷。催单别做成一个纯通知。我见过做成弹窗提醒的吧台根本不看。正确做法是把催单次数记在 order_item 上催一次加一吧台端列表按催单次数倒序高亮显示。数据驱动比弹窗有效十倍。4.4 支付与结算把异常路径想全支付这块正常路径没什么好说的难的是异常路径。我列了几个必须处理的场景支付回调丢失是最常见的。方案是双保险客户端支付成功后主动查一次订单状态同时服务端定时任务每 30 秒扫一遍支付中超过 2 分钟的订单去主动查询。两者谁先到算谁通过订单号的唯一索引保证不会重复入账。金额不一致也要防。回调里的金额必须和订单金额做比对不一致时进人工审核队列别自动入账。这个校验我用DECIMAL(10,2)存金额绝不用浮点数也别在代码里做浮点运算全部转成以分为单位的整数计算。押金和最低消费的处理要单独设计。开台收押金的话结算时先扣押金再收差额押金原路退回的部分要生成独立的退款流水。最低消费不达标的差额作为最低消费补差单独记一项报表里要能分开看否则算不清真实客单价。4.5 吧台端与打印的联动吧台端是整个系统的核心工作面设计原则只有一个一眼能看清该做什么。界面按三列排待接单、制作中、已出酒。新单进来自动置顶并响铃超时未接单的我设的是 3 分钟变红闪烁。打印机这块酒吧环境油污多、纸卷换得勤故障率比餐厅高。我的做法是云打印加本地兜底主用云打印机走网络下发同时保留一台 USB 小票机接在吧台电脑上云打印连续失败两次就自动切本地。打印内容只留必要信息——桌号、下单时间、商品名、规格、数量、备注别打一长串流水号吧台看不清。还有个小细节断网重打。网络恢复后要能补打漏掉的单子但不能把已经打过的重复打。实现方式是打印任务表里存状态PENDING的才打打完置DONE重连时只捞PENDING的。5. 夜间峰值处理扛住那三小时5.1 峰值到底有多高先算一笔账。一家 40 桌的中型酒吧周五晚高峰上座率八成32 桌。每桌平均 5 人每人平均下一单加上加单一晚总订单量大概 400 到 600 单。这些单子集中在 21:30 到 00:30 三个小时内折算下来平均每分钟 3 到 4 单但真实分布是极不均匀的——开场那半小时和跨年节点瞬时能到每分钟 30 单以上。按每分钟 30 单、每单 5 个明细算写入 QPS 大约 150再加上菜单浏览的读请求总 QPS 峰值 300 到 500。这个量级单机完全能扛关键不是性能而是不能出错——峰值时段出一单错现场就是一场混乱。5.2 库存超卖的三种防法超卖是酒吧最痛的问题客人点了没货现场体验极差。三层防护第一层是预扣用前面那段 Lua 脚本在 Redis 里扣扣不到直接拦。第二层是下单幂等客户端生成一个 requestId服务端用唯一索引挡住重复提交。第三层是异步落库加对账每 5 分钟把 Redis 的库存值和数据库比一次不一致就以数据库为准回写 Redis同时告警。这里有个取舍要说清楚库存预扣会导致下单占库存但未支付如果用户放弃支付库存要回滚。我的做法是预扣时给订单设一个 15 分钟的支付窗口超时未支付由定时任务把库存加回去。15 分钟这个值不是拍脑袋是看了实际支付转化数据后定的——超过 15 分钟还没付的八成是不会付了。5.3 幂等、限流与降级幂等的实现要覆盖三个入口下单、支付回调、退款。下单用 requestId 唯一索引支付回调用订单号加状态机的状态校验已经是 PAID 的直接返回成功退款用退款单号唯一索引。限流按接口分开设。菜单查询这类读接口放宽到单 IP 每秒 20 次下单接口收到单 IP 每秒 2 次、单桌每秒 1 次。这个值看着很紧但实际营业中一桌一秒下一单已经很快了超过的要么是脚本要么是误触。降级预案我准备了三级一级是关掉非核心功能比如推荐、评价、图片大图二级是把实时推送降级成轮询三级是只保留下单和支付两个接口其余返回缓存数据。降级开关做成后台可手动触发的别等出事再去改代码发版。6. 常见问题与排查实录6.1 问题速查表下面这张表是我在几个项目里攒下来的按出现频率排序现象可能原因排查动作解决方式扫码后桌号不对二维码复用旧桌号 / 签名校验绕过核对二维码生成记录和桌台变更日志换台时同步重新生成二维码订单重复出现在吧台客户端重复点击 / 缺幂等键查同一 requestId 的记录数补唯一索引前端加按钮防抖支付成功但订单未支付回调丢失 / 验签失败查支付渠道流水和回调日志启动主动查询补偿任务吧台收不到新单WebSocket 断连未重连 / 心跳参数过松查连接日志和心跳间隔缩短心跳加轮询降级库存显示有货但下不了单Redis 与数据库不一致比对两边库存值定时对账回写加告警打印小票缺行打印模板字符集问题 / 纸宽设置错用纯英文测试打印统一 UTF-8纸宽按 58mm 配结账金额对不上改价未留痕 / 最低消费计算遗漏导出订单变更日志强制改价审批补最低消费逻辑深夜系统变慢定时任务撞上高峰查任务执行时间分布把报表、对账任务挪到凌晨 4 点后6.2 三个只有踩过才知道的坑第一个坑是时区。服务器默认 UTC业务按本地时间算今日营业额结果凌晨 0 点到 8 点的单子被算到前一天去了。这个 bug 极其隐蔽因为白天测试完全正常只有跨零点才会暴露。解决办法是全链路统一用本地时区数据库连接串里显式指定别依赖默认值。第二个坑是二维码贴在桌面上被磨损。听起来不是技术问题但真发生过——客人扫不出来服务员只能手输桌号手输就出错。后来我们在系统里加了桌号手动选择的兜底入口扫码失败时服务员可以人工指定但会记一条日志方便统计哪些桌的码该换了。第三个坑是凌晨的定时任务。日结、报表、库存对账这些任务如果定时在 0 点跑正好撞上跨年、节假日的高峰尾巴。我现在的统一做法是所有重任务排在凌晨 4 点到 6 点之间且必须能被打断——任务里加检查点检测到当前 QPS 超过阈值就暂停10 分钟后再续。7. 上线节奏别指望一次全量推开系统做完不等于能用酒吧的现场变量太多了。我的上线节奏一般是三步走。第一步是影子运行通常跑三到五天。系统正常接收订单但不作为唯一依据服务员仍然用原来的方式记录每天打烊后拿系统数据和实际单据对一遍。这一步的目的是找出那些需求文档里没写的业务动作比如某个卡座的老客有固定折扣某个时段的服务费怎么算这些只能靠对账对出来。第二步是单区域灰度选一个区域比如露台或者某个卡座区先切到系统其余区域照旧。这一步主要验证硬件——打印机、平板、WiFi 覆盖。酒吧的 WiFi 是个大坑金属装修、人多、酒水泼溅AP 掉线是常态灰度期要重点看有没有死角。第三步是全量切换但前两周保留人工兜底。我会在吧台留一台平板专门跑员工端出问题能立刻手动补单。真正让我放心撤掉兜底的信号是连续一周没有出现系统单和实际单不一致的情况。这套东西做完一套酒吧点餐系统算是真正立住了。技术选型上没有什么惊艳的选择Node.js、MySQL、Redis、WebSocket都是成熟得不能再成熟的东西。真正决定成败的是那些不起眼的细节——桌台状态机守不守得住、幂等键有没有漏、心跳参数是不是够保守、定时任务有没有避开高峰。我个人的体会是这类系统的复杂度从来不在写代码本身而在你是否愿意花时间去现场看一晚上看服务员怎么用、吧台怎么骂、客人在哪一步卡住。看懂了再去改代码改一次顶十次。