☰
从扫码到支付:餐饮点餐小程序完整开发实战解析
2026/10/13 20:04:29 网站建设 项目流程

去年帮一家开社区餐馆的朋友做了一套点餐小程序,从需求沟通到联调上线,前后花了三周左右。系统名字就叫“迦娃餐馆点餐系统”,听起来不起眼,但它把店里点菜、下单、出单、结账这条线完整串起来了。这篇就把这套小程序的整体设计思路、核心实现和实际踩过的坑都整理出来,给正在做或者打算做同类系统的人一个参考。

这套系统的核心场景并不复杂:客人进店扫桌上的二维码,打开小程序,点选菜品,加购,下单,后厨出单,前台结账。但真正动手做的时候,你会发现里面涉及商品管理、购物车规则、订单状态流转、支付回调、打印机联动、数据统计等多个模块,每个模块都有不少细节,任何一个环节不顺,店里用起来都会很别扭。

我写这篇文章,主要是给三类人看:一是准备给餐馆、食堂、茶饮店做点餐小程序的开发者,二是产品经理或运营想了解点餐系统的功能边界,三是正在学习小程序开发、想做一个完整实战项目的学生。内容不绕弯子,直接讲怎么设计、怎么落地。

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 支付接入的完整链路

微信小程序支付的核心流程如下:

  1. 用户在小程序端点击“去支付”
  2. 前端调用云函数,传入订单号
  3. 云函数校验订单归属和金额,调用微信支付统一下单接口,获取预支付参数(包括appId、timeStamp、nonceStr、package、signType、paySign)
  4. 云函数返回参数给前端
  5. 前端调用wx.requestPayment,弹出支付面板
  6. 用户输入密码完成支付
  7. 微信服务器异步回调商家的HTTPS回调地址
  8. 回调里校验签名,更新订单状态为“已支付”,触发后厨出单

这个链路里,回调和轮询同时处理容易出现重复更新。解决办法是给订单加一个“支付状态”字段,回调处理时先查一次订单,如果已经处理过就不再重复更新。云函数里我用事务操作来保证库存扣减和订单状态更新不产生脏数据。

具体到“云开发跑微信支付”这个方案,云函数里调用支付统一下单,使用的是云开发自带的cloud.cloudPay.unifiedOrder接口,不需要自己存储商户证书。回调地址用HTTPS触发器的URL。这样省去了部署传统后端的过程,整体链路依然完整。

3.4 后厨出单与打印机联动方案

点餐系统的成败,很多时候就看打印这一环。下单后如果后厨没收到单子,整个流程就断了。

打印机我调研下来,市面上主流有三种方案。第一种是“云打印机”,商家购买一台自带云服务的打印机,平台提供API,小程序或服务端把订单内容推给打印机服务,机器自动打印。优点是不用自己维护打印服务,缺点是耗材成本高,而且这些打印机对第三方开发支持参差不齐。

第二种是“局域网打印方案”,店里找一台常开的电脑或平板,运行一个服务监听订单,用系统打印机打印小票。成本低,但多一个维护点,电脑休眠断电就完蛋。

第三种是中转打印方案,商家手机下载一个特定App,App监听云函数推送的订单,调用手机蓝牙打印机打印。

考虑到这家店的实际情况,我做的是双保险:云函数在订单支付成功后,自动调用打印服务的接口,把订单内容推给后厨的云打印机;同时商家手机上的商家端小程序订阅消息也会收到“新订单”通知,如果打印机故障还能手机顶上。我用的云打印机支持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 后厨打印机不出单

这个问题排查顺序一般是:

  1. 确认打印机电源和网络状态,云打印机看指示灯
  2. 确认订单状态确实是“已支付/待出餐”,不是卡在“待支付”
  3. 检查云函数里推送打印服务的调用日志,看返回结果
  4. 检查打印机绑定的终端号、API Key等配置是否失效

由于是双保险设计,即使打印机故障,商家端也会通过订阅消息收到新订单提醒,不至于漏单。

5.4 小程序审核被拒的常见原因

点餐类小程序在微信审核时可能碰到几个坑:

  • 涉及食品经营和交易,需要补充“商家自营-餐饮”类目,且要有对应的食品经营许可证
  • 如果用到微信支付,必须开通微信支付商户号,并且在后台配置支付目录和回调域名
  • 小程序码的scene参数不要太长,微信有限制,桌号这类短字符串没问题
  • 页面里不允许出现诱导分享、虚假营销内容
  • 用户隐私协议要明确声明收集了哪些信息、用于什么目的,小程序后台的隐私保护指引要填完整

这些审核问题如果没提前处理,上线时间会被拖很久。我建议在开发第一天就顺手把小程序类目、服务协议、隐私声明都配置好,不要等到开发完再走流程。

5.5 数据库读性能与并发问题

小餐馆的并发量并不高,但高峰期几十单集中提交时,云数据库如果频繁读写,还是会出现响应变慢。我的优化做法是:把分类和菜品这类基础数据加缓存在前端,小程序启动时带上版本号,只有在菜品版本变更时才重新拉取。这样大部分页面加载不需要访问数据库。

对于下单操作,始终保持“单订单单事务”的逻辑,不做批量订单操作。云数据库的文档级事务足够应付这类场景。如果要进一步扩展,可以把高频读的数据放到Redis缓存,把订单写入改成队列异步处理,但现阶段这些优化还不需要。

6. 一点个人经验总结

做这套系统最大的感触是:真正难的不是技术,而是把餐馆里那些“约定俗成”的流程翻译成系统逻辑。比如“这个菜不要葱”“这桌要先上饮料”“这单是老板朋友打折”,这些在纸质时代靠人吼、靠口头交代的事情,到了系统里都要有对应的字段或备注通道,否则店里用起来还不如手写方便。

我能给正在做类似项目的人三条建议:

第一,开发前一定要去店里待几小时,看真实流程,而不是坐在电脑前凭空设计功能表。

第二,把打印和支付这两条外部依赖的链路放在开发计划的前面。它们是最容易出问题、也最耽误上线的部分,越早打通越好。

第三,给商家预留一个“人肉兜底”的逃生通道。任何自动化功能都有可能故障,店内广播、手写单、前台手动改状态这些能力一定要保留,不能把系统变成一个黑盒。这样不管是人适应系统,还是系统适应人,都有缓冲空间。

这套系统上线之后,朋友店里高峰期服务员跑动的频率明显降了,客人的催菜也少了,后厨出单准确率提升了很多。能把一家小店的点餐体验打理顺,我觉得这趟折腾值了。如果你们正好也在做同类项目,希望这篇能帮你少踩几个坑。

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

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

立即咨询