首页 / 资讯中心 / 文章详情

从扫码到支付:餐饮点餐小程序完整开发实战解析

从扫码到支付:餐饮点餐小程序完整开发实战解析 ★ FEATURED ARTICLE
去年帮一家开社区餐馆的朋友做了一套点餐小程序从需求沟通到联调上线前后花了三周左右。系统名字就叫“迦娃餐馆点餐系统”听起来不起眼但它把店里点菜、下单、出单、结账这条线完整串起来了。这篇就把这套小程序的整体设计思路、核心实现和实际踩过的坑都整理出来给正在做或者打算做同类系统的人一个参考。这套系统的核心场景并不复杂客人进店扫桌上的二维码打开小程序点选菜品加购下单后厨出单前台结账。但真正动手做的时候你会发现里面涉及商品管理、购物车规则、订单状态流转、支付回调、打印机联动、数据统计等多个模块每个模块都有不少细节任何一个环节不顺店里用起来都会很别扭。我写这篇文章主要是给三类人看一是准备给餐馆、食堂、茶饮店做点餐小程序的开发者二是产品经理或运营想了解点餐系统的功能边界三是正在学习小程序开发、想做一个完整实战项目的学生。内容不绕弯子直接讲怎么设计、怎么落地。1. 需求梳理与整体设计思路1.1 餐馆的真实痛点与使用场景做系统之前我特意去朋友店里蹲了两天观察高峰期和大厅的实际运作。这家店有七八张桌子后厨两个人前台一个收银员再加上服务员基本属于典型的小型正餐店。原来的流程是客人坐下服务员拿纸质菜单过去点菜客人勾选后服务员手工输入收银机后厨看单做菜。问题很明显高峰期经常出现服务员忙不过来、客人催菜、手写单看不清、账单算错的情况菜品价格或做法一变旧菜单作废重新印刷成本不低再加上第三方外卖平台抽成高店里自己又没有沉淀客流。所以真正的需求可以拆成几个点客人扫码就能看到菜单不需要服务员逐桌介绍加购和下单操作要足够简单年纪大的客人也能看懂下单后订单要自动传到后厨最好有小票或屏显结账要支持在线支付和到店付款两种方式商家后台能管理菜品、分类、库存、订单和基础报表系统要轻量店里不用买新硬件用现有手机和打印机就能跑这套需求的本质是覆盖一个“从顾客进店到吃完离店”的完整闭环。做设计的时候不能只盯着小程序端那几个页面而是要把顾客、服务员、后厨、收银员四类角色放进去一起看。1.2 用现成SaaS还是自己开发需求理清之后第一个实际问题不是技术而是“要不要用现成的点餐SaaS”。市面上成熟的产品一年几千块就能搞定功能也挺全。但朋友考虑之后还是决定自研我帮他把账算了一下对比项通用SaaS方案自研小程序方案年费用2000元至6000元不等域名服务器或云开发约几百到一千菜品字段扩展受平台限制完全自己定义品牌与UI定制有限可完全定制数据归属在平台方自己掌控可导出功能叠加平台方规划按需开发可对接打印机、会员系统维护成本低自己负责自研的优点主要是灵活性和数据归属代价是前期开发需要投入时间。对于想做长期生意的餐馆来说菜单、顾客数据、订单流水都是资产放在自己手里更踏实。而且很多SaaS的打印对接、菜品规格设定都有各种限制不如自己做省心。1.3 核心流程与角色划分整套系统涉及三类角色顾客、商家包括收银员和后厨、管理员。顾客端的核心路径是扫桌码进入小程序打开首页看到分类和推荐菜品点进某个菜品查看详情和规格加购购物车确认提交订单选择“在线支付”或“到店付”支付成功后等待出餐。如果是到店付直接生成“待出餐”状态不用等支付回调。商家端主要分两块。后厨看到的是“待出餐”和“制作中”的订单列表点击“开始制作”“出餐完成”来更新状态前台收银员看到的是全量订单处理取消、退款、结账、打印小票。管理员的角色在后台负责菜品分类维护、上下架、价格库存调整、桌位二维码管理、营业数据查看。这里有一个设计重点订单状态不能只有“已支付”“已完成”两个状态至少要拆成五到六个状态否则后厨和前台没法协作。下一章详细讲状态机的设计。2. 技术选型与项目架构2.1 前端方案原生小程序还是uni-app前端我用的是微信原生小程序。考虑过uni-app因为以后可能要发支付宝、抖音端但最终评估下来这个项目只需要微信端原生开发足够。原生小程序的好处是对微信api支持最直接比如登录、支付、订阅消息都不需要中间层转换调试工具也稳代码跑在真机上不容易出兼容性怪问题。uni-app适合多端复用但引入框架就多了一层编译文档和坑也更多。对一个纯微信端项目来说没有必要。页面上没有用复杂的UI框架就是小程序原生组件加少量自定义样式。点餐这种场景页面主要就是列表、弹窗、购物车栏、表单用原生组件完全够引入第三方UI反而增加体积和样式覆盖成本。2.2 后端选型云开发还是自建服务后端是这套系统里最需要权衡的地方。我当时对比了两条路线。方案一是微信云开发。云函数负责业务逻辑云数据库存订单和菜单云存储放菜品图片不用自建服务器也不用备案域名开发速度非常快。适合小体量业务、并发不高的场景。方案二自建后端常见的是Java后端或Node.js后端部署在一台云主机上数据库用MySQL再加一个Redis做缓存。好处是灵活可控方便以后扩容和做复杂业务但需要搭环境、处理域名HTTPS证书、配置反向代理还牵扯到备案的问题。对一家小店来说这个成本和复杂度偏高。最后我给朋友选的是“原生小程序 微信云开发”的组合。云函数处理下单逻辑和支付调用云数据库存核心业务数据商家后台用小程序的一个独立Tab页面承载。整个系统没有一台传统服务器但跑得很稳。这里有一个很重要的点支付回调怎么办。微信支付的回调地址必须是HTTPS合法域名云函数可以配合微信云开发的HTTP访问服务来接收回调把支付结果写入数据库再更新订单状态。后面章节会细讲。2.3 数据库集合与核心表结构云数据库是文档型数据库每个集合可以理解成一张表。这套系统的集合设计如下users顾客用户信息包含openid、昵称、手机号、会员等级categories菜品分类包含名称、排序值、是否启用dishes菜品包含名称、描述、价格、图片、分类ID、规格组、库存、是否上下架tables桌位包含桌号、二维码参数、状态carts购物车这个其实放前端本地存储更合理后面讲orders订单主表包含订单号、用户、桌号、金额、状态、支付方式、支付单号order_items订单明细包含菜品快照、价格、数量、规格文本messages订阅消息发送记录用于出餐和取餐提醒订单主表和订单明细必须分开。因为菜品价格和名称可能后续会改订单里要保存的是下单那一刻的快照而不是去关联实时菜品表。明细表存一份冗余快照这样回看订单时显示的就是原来的内容不会因为菜品改名而变乱。3. 核心细节设计与实现要点3.1 菜品规格与购物车规则设计做点餐系统最容易翻车的点是“规格”问题。一个菜可能有辣度选择微辣、中辣、特辣可能有口味备注不要香菜、少盐还可能有加料选项加一份肉、加一份饭。如果只在菜品表里放一个“价格”字段客人根本没法正常下单。我的做法是建立“规格组”的概念。一个菜品下可以有多个规格组每个规格组里包含若干规格项规格项可以单独加价。比如“辣度”是一个规格组里面有“不辣”“微辣”“中辣”“特辣”四项“加料”是另一个规格组“加鸡蛋”加2元“加牛肉”加8元。在数据库里每个菜品存一个规格组数组。在小程序端点开菜品弹层时展示所有规格组用户选完才高亮“加入购物车”按钮。购物车数据不要每次都查服务端也不要全放服务端。我的方案是购物车主体放小程序本地缓存数据结构如下cartItem: { dishId: 菜品ID, skuKey: 辣度-中辣|加料-加鸡蛋, dishName: 麻辣香锅, price: 28, // 单价 count: 1, image: 缩略图URL, specText: 中辣 / 加鸡蛋, stock: 100 // 下单前快照校验用 }加购、减购、清空都在本地完成页面交互非常流畅。真正提交订单时把购物车数据原样传给云函数云函数逐条校验菜品是否仍在上架状态、价格是否一致、库存是否充足。这样可以防止有人改本地缓存价格下单。3.2 订单状态机与异常处理订单状态是整个系统的核心枢纽设计得不好前台、后厨、顾客三方都会迷糊。我定义了如下状态流转状态值含义触发动作可流转到0待支付用户提交订单1支付成功、5超时取消1已支付/待出餐支付回调成功或到店付提交2后厨接单2制作中后厨点击开始3出餐完成3待取餐/待上菜后厨出餐4顾客确认或商家确认完成4已完成结账或用户确认终态5已取消用户取消、商家取消或超时终态6退款中/已退款用户申请退款或商家退款终态这里有个容易忽略的细节在线支付订单用户提交订单后但没完成支付订单不能直接进入后厨。所以提交订单和支付是两个动作。订单先创建成“待支付”状态附带一个15分钟的支付超时时间。15分钟内没支付订单自动取消释放库存。这个逻辑我是用一个云函数定时任务实现的每5分钟扫描一次超过15分钟仍未支付的订单把状态改成已取消并回补库存。“到店付”模式不需要支付回调提交订单直接进入“已支付/待出餐”状态。这个模式适合一些不习惯微信支付或者临时没电的客人。3.3 支付接入的完整链路微信小程序支付的核心流程如下用户在小程序端点击“去支付”前端调用云函数传入订单号云函数校验订单归属和金额调用微信支付统一下单接口获取预支付参数包括appId、timeStamp、nonceStr、package、signType、paySign云函数返回参数给前端前端调用wx.requestPayment弹出支付面板用户输入密码完成支付微信服务器异步回调商家的HTTPS回调地址回调里校验签名更新订单状态为“已支付”触发后厨出单这个链路里回调和轮询同时处理容易出现重复更新。解决办法是给订单加一个“支付状态”字段回调处理时先查一次订单如果已经处理过就不再重复更新。云函数里我用事务操作来保证库存扣减和订单状态更新不产生脏数据。具体到“云开发跑微信支付”这个方案云函数里调用支付统一下单使用的是云开发自带的cloud.cloudPay.unifiedOrder接口不需要自己存储商户证书。回调地址用HTTPS触发器的URL。这样省去了部署传统后端的过程整体链路依然完整。3.4 后厨出单与打印机联动方案点餐系统的成败很多时候就看打印这一环。下单后如果后厨没收到单子整个流程就断了。打印机我调研下来市面上主流有三种方案。第一种是“云打印机”商家购买一台自带云服务的打印机平台提供API小程序或服务端把订单内容推给打印机服务机器自动打印。优点是不用自己维护打印服务缺点是耗材成本高而且这些打印机对第三方开发支持参差不齐。第二种是“局域网打印方案”店里找一台常开的电脑或平板运行一个服务监听订单用系统打印机打印小票。成本低但多一个维护点电脑休眠断电就完蛋。第三种是中转打印方案商家手机下载一个特定AppApp监听云函数推送的订单调用手机蓝牙打印机打印。考虑到这家店的实际情况我做的是双保险云函数在订单支付成功后自动调用打印服务的接口把订单内容推给后厨的云打印机同时商家手机上的商家端小程序订阅消息也会收到“新订单”通知如果打印机故障还能手机顶上。我用的云打印机支持TCP指令和HTTP接口两种方式最后选了HTTP推送简单直接。小票模板包含订单号、桌号、菜品明细、数量、备注、下单时间行宽控制在小票纸的32个汉字宽度内。这里的经验是打印模板里的字体要用等宽字体或者计算好字数换行别指望HTML模板自动换行不然打印出来是歪的。3.5 桌码与进场引导每张桌子桌上贴一个二维码扫码后小程序自动带上桌号参数。这里有一个细节二维码的内容不能直接用小程序路径加一个参数就完事因为可能存在微信外部扫码。正确做法是二维码内容指向一个小程序码通过scene参数传递桌号和餐厅ID用户打开小程序后从onLoad的options里解析scene再解码得到桌号。我生成的桌码用了wxacode.getUnlimited接口cloud的云函数调用它可以生成无限量的小程序码每个码绑定不同的scene参数。顾客扫码进入小程序后首页顶栏显示当前桌号这样客人确认自己没进错地方商家也能在后厨单上看到桌号。如果客人想换桌可以手动改桌号但这个权限要控制免得有人随便换。写得很明了。我继续往下写。4. 实操过程与核心代码4.1 菜单页与分类滚动的实现菜单页是顾客打开小程序后第一个核心页面布局上采用“左侧分类栏 右侧菜品列表”的经典结构。左侧分类栏一般是可滚动列表右侧对应的菜品列表也是一组可滚动列表。实现联动时我用了小程序的双列表滚动监听右侧滚动时计算当前处于哪个分类区块把左侧对应项滚动到视野内并高亮点击左侧分类时右侧直接滚动到对应区块。菜品列表的每一项包含图片、名称、月售量、价格和“加购”按钮。点击图片或名称区域弹出SKU选择弹层点击“加购”直接按默认规格加入购物车这是提高操作效率的关键。实测下来高峰期顾客点菜的节奏非常快尽量减少点击次数比花哨的动效更有价值。购物车栏固定在页面底部显示总数量和总金额同时显示一个“去结算”按钮。加购时有顶部的轻提示不打断操作流。这种设计保证了连贯性不会因为反复弹窗而让用户烦。4.2 下单云函数的业务逻辑订单提交的云函数是整套系统最核心的一段代码它的流程必须严谨// 提交订单云函数 exports.main async (event) { const { tableId, items, payType, remark } event const { OPENID } cloud.getWXContext() // 1. 校验用户和桌位 const table await db.collection(tables).doc(tableId).get() if (!table.data || table.data.status disabled) { throw new Error(桌位不可用) } // 2. 逐一校验菜品库存、价格、上下架状态 let totalAmount 0 const orderItems [] for (const item of items) { const dishRef await db.collection(dishes).doc(item.dishId).get() const dish dishRef.data if (!dish || dish.status ! on_sale) { throw new Error(商品不可售${item.dishName}) } if (dish.stock item.count) { throw new Error(库存不足${item.dishName}) } // 计算规格加价后的实际价格 const unitPrice dish.price calcSpecAddPrice(dish, item.skuKey) totalAmount unitPrice * item.count orderItems.push({ dishId: dish._id, dishName: dish.name, image: dish.image, skuKey: item.skuKey, specText: item.specText, unitPrice, count: item.count, amount: unitPrice * item.count }) } // 3. 创建订单主记录状态为待支付 const orderNo generateOrderNo() const orderRes await db.collection(orders).add({ data: { orderNo, openid: OPENID, tableId, tableName: table.data.name, items: orderItems, totalAmount, payType, // wechat 或 onthe spot status: payType onthe spot ? 1 : 0, createTime: db.serverDate(), updateTime: db.serverDate(), remark } }) // 4. 扣减库存 await updateStockBatch(items) // 5. 返回订单信息和支付参数 if (payType wechat) { const payRes await cloud.cloudPay.unifiedOrder({ body: 迦娃餐馆-订单 orderNo, outTradeNo: orderNo, spbillCreateIp: 127.0.0.1, subMchId: 商户号, totalFee: Math.round(totalAmount * 100), tradeType: JSAPI, openid: OPENID }) return { orderId: orderRes._id, payParams: payRes.payment } } return { orderId: orderRes._id, status: 1 } }这段代码的核心逻辑是“前端参数只做展示后端重新计算价格和校验库存”。购物车里显示的金额只是给顾客看的真实金额必须在云函数里根据菜品当前价格与规格加价重新计算一遍防止有人改前端数据占便宜。4.3 支付回调与幂等处理支付回调是最容易出问题的地方。微信官方会多次通知同一笔订单网络抖动也会导致回调重复。我在HTTP触发器对应的云函数里加了一个状态检查exports.main async (event) { // 从event中解析微信支付回调的报文 const { returnCode, resultCode, outTradeNo, totalFee } event if (returnCode SUCCESS resultCode SUCCESS) { const orderRes await db.collection(orders).where({ orderNo: outTradeNo }).get() const order orderRes.data[0] // 幂等判断已经是已支付状态则直接返回成功 if (order order.status 1) { return { errcode: 0, errmsg: ORDER_ALREADY_PAID } } // 金额校验防止篡改 if (Math.round(order.totalAmount * 100) ! totalFee) { throw new Error(金额不一致) } await db.collection(orders).doc(order._id).update({ data: { status: 1, payTime: db.serverDate() } }) // 触发打印推送和订阅消息 await pushPrint(order) await sendSubscribeMessage(order) return { errcode: 0, errmsg: SUCCESS } } }幂等处理的意义很大。我第一次上线时就遇到了回调重复通知结果订单状态被反复从“制作中”改回“待出餐”后厨那台打印机打了三张小票。加了这个判断之后这类问题就再也没出现过。4.4 商家端订单管理与出餐联动商家端我设计成小程序内的一个独立Tab仅对管理员和店员开放。进入时校验登录身份。订单列表页支持按状态筛选“待出餐”是最重要的后厨要一眼看到新单。每张订单卡片显示订单号、桌号、下单时间、商品明细、备注和总金额。后厨操作按钮很直接点击“开始制作”状态变2同时前端刷新列表点击“出餐完成”状态变3同时给顾客微信推送订阅消息提醒“您的餐品已出餐请耐心等待”。这里需要特别说明订阅消息的限制。微信的订阅消息用户每授权一次只能发一次。顾客下单时我弹窗请求用户授权一次性订阅消息授权后这次订单的出餐提醒可以发送。但顾客如果不授权出餐提醒就发不出去。所以不能依赖订阅消息做唯一提醒手段菜单屏、店内广播、前台喊号始终是最可靠的方式。4.5 菜品管理与数据初始化的后台设计菜品管理放在商家端的“管理”Tab里。管理员登录后可以操作编辑分类新增、排序、停用编辑菜品名称、描述、价格、图片上传、库存、上下架、规格组配置桌位管理生成桌码二维码、调整桌名订单管理查看全部订单退款操作数据统计今日营收、订单量、菜品销量Top10、时段分布图片上传用的云存储菜品图片统一走云存储的临时链接。这里有个坑云存储的临时链接会过期如果菜品图片直接存临时链接过一段时间菜品图就会裂。我的处理是菜品图片用云存储的fileID存储小程序端通过wx.cloud.getTempFileURL动态换取链接这样就不会过期。这是很多新手会忽略的细节。数据统计页面我用了一个简单的方式订单按天的维度聚合云函数定时生成当日汇总。实时性要求不高但给老板的“今天一共卖了多少”这个数字一定要准。我这里是采用“已支付订单金额总和”来统计未支付的订单一律不计入营收。5. 常见问题与排查技巧实录5.1 支付成功但订单状态没变这个问题我调试了很久才定位。一般情况下支付回调到达云函数后订单状态会更新。但如果出现支付成功、订单却一直停在“待支付”的情况排查思路是先看微信支付商户平台的支付记录确认回调是否发送成功检查回调云函数的日志看有没有抛异常确认云函数的执行环境能访问微信支付回调地址有没有被安全策略拦截检查订单号格式是否一致有些时候订单号里不经意带了空格导致回调匹配不到订单实操建议所有回调处理日志都要完整打印尤其是“收到的支付通知”和“更新订单结果”这两条日志。小程序云开发的日志查询很方便把promise顺序打印出来问题基本一眼就能看出来。5.2 下单后库存被扣了但订单消失这个问题常常发生在“云函数异常中断”的时候。云函数在扣库存之后、写订单成功之前挂掉了就会出现库存减少却查不到订单的情况。解决思路是调整顺序先写订单再扣库存。如果扣库存失败可以靠一个定时补偿任务来闭环扫描已支付但库存扣减异常的订单重新执行扣减或者回滚。另一个更稳妥的方案是引入订单幂等键。前端提交订单时生成一个uuid作为幂等键存入订单表后台如果发现重复提交就拒绝。我上线后遇到过一次用户的连续双击导致两笔订单同时生成加了这个机制就解决了。5.3 后厨打印机不出单这个问题排查顺序一般是确认打印机电源和网络状态云打印机看指示灯确认订单状态确实是“已支付/待出餐”不是卡在“待支付”检查云函数里推送打印服务的调用日志看返回结果检查打印机绑定的终端号、API Key等配置是否失效由于是双保险设计即使打印机故障商家端也会通过订阅消息收到新订单提醒不至于漏单。5.4 小程序审核被拒的常见原因点餐类小程序在微信审核时可能碰到几个坑涉及食品经营和交易需要补充“商家自营-餐饮”类目且要有对应的食品经营许可证如果用到微信支付必须开通微信支付商户号并且在后台配置支付目录和回调域名小程序码的scene参数不要太长微信有限制桌号这类短字符串没问题页面里不允许出现诱导分享、虚假营销内容用户隐私协议要明确声明收集了哪些信息、用于什么目的小程序后台的隐私保护指引要填完整这些审核问题如果没提前处理上线时间会被拖很久。我建议在开发第一天就顺手把小程序类目、服务协议、隐私声明都配置好不要等到开发完再走流程。5.5 数据库读性能与并发问题小餐馆的并发量并不高但高峰期几十单集中提交时云数据库如果频繁读写还是会出现响应变慢。我的优化做法是把分类和菜品这类基础数据加缓存在前端小程序启动时带上版本号只有在菜品版本变更时才重新拉取。这样大部分页面加载不需要访问数据库。对于下单操作始终保持“单订单单事务”的逻辑不做批量订单操作。云数据库的文档级事务足够应付这类场景。如果要进一步扩展可以把高频读的数据放到Redis缓存把订单写入改成队列异步处理但现阶段这些优化还不需要。6. 一点个人经验总结做这套系统最大的感触是真正难的不是技术而是把餐馆里那些“约定俗成”的流程翻译成系统逻辑。比如“这个菜不要葱”“这桌要先上饮料”“这单是老板朋友打折”这些在纸质时代靠人吼、靠口头交代的事情到了系统里都要有对应的字段或备注通道否则店里用起来还不如手写方便。我能给正在做类似项目的人三条建议第一开发前一定要去店里待几小时看真实流程而不是坐在电脑前凭空设计功能表。第二把打印和支付这两条外部依赖的链路放在开发计划的前面。它们是最容易出问题、也最耽误上线的部分越早打通越好。第三给商家预留一个“人肉兜底”的逃生通道。任何自动化功能都有可能故障店内广播、手写单、前台手动改状态这些能力一定要保留不能把系统变成一个黑盒。这样不管是人适应系统还是系统适应人都有缓冲空间。这套系统上线之后朋友店里高峰期服务员跑动的频率明显降了客人的催菜也少了后厨出单准确率提升了很多。能把一家小店的点餐体验打理顺我觉得这趟折腾值了。如果你们正好也在做同类项目希望这篇能帮你少踩几个坑。
阅读完成 · 觉得有帮助?
咨询建站