简介:这是一套面向餐饮从业者、小程序开发者及流量主的全开源外卖扫码点餐微信小程序源码,旨在解决线下餐厅数字化转型中的点餐效率低、系统封闭、二次开发难等痛点。资源包含完整前后端代码,前端基于WXML/WXSS/JS实现扫码进店、菜单浏览、下单支付等交互流程,后端支持PHP主流架构,集成菜品管理、订单处理、用户登录等核心模块,适配流量主广告位嵌入与商业运营拓展。压缩包共2000个文件,主体为6898个PHP后端逻辑文件、305个JS前端脚本、174个CSS样式文件及454个PNG图标资源,辅以JSON配置、MD文档和LICENSE协议,总大小23.51MB,结构清晰、注释完备,便于学习调试与功能定制。已有4649人学习下载,开发者可直接部署上线,亦可深入研究微信小程序与PHP服务端协同机制、二维码动态生成与订单状态同步等关键技术实现。
1. 外卖扫码点餐全开源小程序源码:为什么它不是“拿来即用”的玩具,而是中小餐饮数字化落地的最小可行切口?
你扫过餐厅桌角那个泛黄的二维码吗?十次里有八次跳进一个卡顿、样式错乱、下单后没通知、订单状态永远“待接单”的小程序——这背后大概率跑着一套被反复打包转卖、缺文档、无维护、连基础支付回调都没配通的“开源”代码。而标题里这个标价2400元的.rar包,本质不是一份“成品软件”,而是一套结构完整、模块可拆、接口清晰、已通过微信小程序平台基础审核要求的点餐系统骨架。它解决的不是“要不要做线上点餐”,而是“如何用低于外包1/5的成本,在两周内让一家30平米的酸辣粉店拥有可收款、可出单、可查库存的轻量级数字前台”。适用对象非常具体:有基础前端认知的店主本人、刚入行的独立开发者、高校课程设计需要真实业务闭环的学生。它不承诺高并发、不内置AI推荐、不做SaaS多租户隔离——但能把扫码→选品→加购→微信支付→厨房打印小票→订单状态同步这条主链路,用最直白的WXML+JS+云开发逻辑跑通。下面我们就从解压那一刻开始,一层层剥开它的真实能力边界与落地必调项。
2. 从解压到真机扫码:三步跑通本地开发环境与基础功能验证
2.1 解压后第一眼该看什么:识别项目结构中的“可信信号”
拿到.rar文件后,不要急着导入微信开发者工具。先用7-Zip或WinRAR解压到空文件夹,用 VS Code 打开,重点检查以下四个目录是否存在且非空:
├── miniprogram/ # 小程序主体(必须存在) │ ├── pages/ # 页面目录(至少含 index/index、order/order、pay/pay) │ ├── components/ # 自定义组件(如 goods-card、order-list) │ ├── utils/ # 工具函数(request.js 必须有,且含 wx.request 封装) │ └── app.js # 全局逻辑(检查是否初始化云开发 wx.cloud.init) ├── cloudfunctions/ # 云函数目录(必须存在,至少含 addOrder、updateOrderStatus) ├── project.config.json # 微信项目配置(检查 "appid" 是否为占位符如 "wx1234567890") └── README.md # 作者说明(重点看“部署前必读”章节)提示:若
cloudfunctions/目录为空,或utils/request.js里全是// TODO:注释,说明这是早期未完成版本,建议立即停止——这不是“需调试”,而是“不可用”。
2.2 微信开发者工具配置:绕过“体验版未开启”的玄学报错
很多新手卡在第一步:导入项目后,模拟器显示白屏,控制台报Error: experience version not open。这不是代码问题,是微信平台侧的权限开关未触发。正确操作路径如下:
- 在微信开发者工具顶部菜单栏点击「详情」→「本地设置」
- 勾选「使用 npm 模块」和「增强编译」(必须!否则
async/await语法报错) - 切换到「项目设置」标签页,确认「AppID」已填入你自己的小程序 AppID(不是
wx...占位符) - 关键一步:点击右上角「…」→「上传」,弹窗中不点上传,直接关闭——此操作会强制触发平台对该项目的“首次注册”,5秒后刷新模拟器即可加载
# 验证云开发是否就绪:在开发者工具控制台执行 wx.cloud.init({ env: 'your-env-id' // 替换为你在云开发控制台创建的环境ID }) .then(() => console.log('云开发初始化成功')) .catch(err => console.error('云开发失败:', err))参数说明:
env值必须与你在 微信公众平台-云开发 中创建的环境ID完全一致(如prod-abc123),大小写敏感。若此处报env not found,说明环境未创建或ID输错。
2.3 真机扫码联调:用“测试号”绕过线上域名备案限制
小程序上线前必须配置request合法域名,但本地调试时无需备案。方法是启用微信官方提供的测试号:
- 登录 微信公众平台测试号管理页
- 扫码绑定你的个人微信号(非企业号)
- 在开发者工具中,点击「项目」→「小程序信息」→「基础信息」,将
AppID替换为测试号分配的wx...ID - 重新编译,此时生成的二维码任何微信均可扫码,且能调用
wx.login、wx.request等全部接口
注意:测试号有效期7天,到期后需重新绑定。生产环境部署时,必须将
request合法域名配置为已备案的 HTTPS 域名(如https://api.yourshop.com),且证书由腾讯云/阿里云等主流CA签发。
3. 核心业务链路拆解:扫码→点餐→支付→出单,每一步谁在干活?
3.1 扫码入口:index/index.js中的onLoad如何动态解析桌号
用户扫桌角二维码时,微信会将?table=08这类参数传给首页。源码中关键逻辑在miniprogram/pages/index/index.js的onLoad生命周期:
// miniprogram/pages/index/index.js Page({ data: { tableNo: '' }, onLoad(options) { // options.table 即二维码携带的桌号,如 "08" const tableNo = options.table || '01' this.setData({ tableNo }) // 关键:根据桌号拉取对应门店的菜单 wx.cloud.callFunction({ name: 'getMenuByTable', data: { tableNo } }).then(res => { this.setData({ menuList: res.result.menu }) }) } })逻辑说明:
getMenuByTable是一个云函数,它接收tableNo后查询数据库中tables集合,找到该桌所属门店ID(如store_001),再关联查询menus集合中storeId === 'store_001'的菜品。这样同一套代码可支撑多门店,只需在数据库中维护tables表的映射关系。
3.2 加购与购物车:utils/cart.js的内存级实现为何足够用
本项目未使用云数据库存购物车,而是采用小程序本地缓存 + 内存对象双保险:
// miniprogram/utils/cart.js class Cart { constructor() { this.items = [] // 内存数组,页面切换不丢失 } addItem(goodsId, name, price, count = 1) { const exist = this.items.find(i => i.goodsId === goodsId) if (exist) { exist.count += count } else { this.items.push({ goodsId, name, price, count }) } // 同时写入本地缓存,防止杀进程后丢失 wx.setStorageSync('cart', this.items) } getItems() { // 优先读内存,内存为空则读缓存 return this.items.length > 0 ? this.items : wx.getStorageSync('cart') || [] } } export default new Cart()参数说明:
goodsId必须与数据库goods集合中的_id字段严格一致;price以“分”为单位存储(如1800表示18元),避免浮点数精度问题。此设计牺牲了多端同步能力,但换来零云调用延迟——对单店扫码场景,用户从加购到下单平均耗时降低300ms。
3.3 支付闭环:pay/pay.js如何用云调用安全生成预支付订单
微信支付要求prepay_id必须由服务端生成,禁止前端直接调用统一下单API。本项目通过云调用(CloudBase Call)实现:
// miniprogram/pages/pay/pay.js onPayClick() { const cartItems = cart.getItems() wx.cloud.callFunction({ name: 'unifiedOrder', // 对应 cloudfunctions/unifiedOrder/index.js data: { orderNo: `ORD${Date.now()}`, // 订单号规则 amount: cartItems.reduce((sum, i) => sum + i.price * i.count, 0), goodsList: cartItems.map(i => ({ id: i.goodsId, name: i.name })) } }).then(res => { // res.result.payParams 是后端返回的支付参数对象 wx.requestPayment({ ...res.result.payParams, success: () => this.onPaymentSuccess(), fail: (err) => console.error('支付失败', err) }) }) }关键点:
unifiedOrder云函数内部使用wx-server-sdk调用微信支付统一下单接口,其mch_id(商户号)、key(API密钥)均从云开发环境变量读取,绝不硬编码在前端。这是支付安全的底线。
4. 避坑指南:那些让开发者连续熬夜却查不到日志的典型翻车现场
4.1 现象:扫码后页面空白,控制台无报错,network面板显示appservice?t=xxx请求 404
原因:project.config.json中miniprogramRoot路径配置错误,或解压时未保留原始目录层级(如把miniprogram/文件夹内容直接拖进项目根目录)。
解决:删除当前项目,重新解压.rar,确保miniprogram/是子目录而非根目录;检查project.config.json中"miniprogramRoot": "miniprogram/"是否存在且值正确。
4.2 现象:点击“去支付”按钮,弹出“支付验证失败”,错误码requestPayment:fail invalid sign
原因:云函数unifiedOrder返回的payParams中paySign签名计算错误,常见于商户密钥(key)未在云开发环境变量中正确设置,或timeStamp使用了毫秒时间戳(应为秒级)。
解决:登录云开发控制台 →「环境设置」→「环境变量」,确认WECHAT_MCH_KEY值与微信支付商户平台「API安全」中设置的API密钥完全一致;检查云函数代码中timeStamp: Math.floor(Date.now()/1000)是否为整数秒。
4.3 现象:厨房打印机无反应,云函数printOrder日志显示printer not found
原因:cloudfunctions/printOrder/index.js中硬编码了打印机SN码(如'SN123456789'),但实际采购的打印机SN码不同。
解决:进入云开发数据库printers集合,新增一条记录,sn字段填入你打印机的实际SN码(通常在打印机背面标签),status设为online;修改云函数中const printerSN = 'SN123456789'为动态查询:const printer = await db.collection('printers').where({status:'online'}).limit(1).get()。
4.4 现象:菜品图片全部显示为默认占位图,network面板看到图片请求 403
原因:云存储中图片文件 ACL(访问控制)未设为“公有读”,或utils/request.js中图片URL拼接错误(如漏掉/导致cloud://envid/food.jpg变成cloud://envidfood.jpg)。
解决:登录云开发控制台 →「云存储」→「文件列表」,选中所有图片文件 →「批量操作」→「设为公有读」;检查utils/request.js中图片地址生成逻辑,确保const url = 'cloud://' + envId + '/' + filePath中envId后有/。
4.5 现象:同一桌号多次扫码,订单状态混乱,出现重复接单
原因:cloudfunctions/updateOrderStatus云函数未做幂等性校验,多次调用导致status字段被反复覆盖。
解决:在云函数中增加状态跃迁判断:
// 只允许 status 从 'pending' → 'accepted',禁止 'accepted' → 'accepted' if (oldStatus === 'accepted' && newStatus === 'accepted') { return { success: false, msg: '订单已接单,不可重复操作' } }5. 生产环境部署四件套:域名、SSL、云函数、数据库,一个都不能少
5.1 域名与HTTPS:用腾讯云免费证书搞定合规要求
微信要求request接口必须走 HTTPS,且域名需在公众平台备案。实操中我们选择腾讯云:
- 在腾讯云「云解析DNS」购买一个二级域名(如
api.yourshop.com,年费约30元) - 在「SSL证书服务」申请免费型 DV 证书(品牌:TrustAsia,支持微信校验)
- 证书签发后,下载 Nginx 版本,上传至云服务器(或直接绑定到云开发静态网站托管)
- 在微信公众平台「开发管理」→「服务器域名」中,将
https://api.yourshop.com添加到request合法域名
注意:证书必须包含
www.和裸域(如yourshop.com),否则https://yourshop.com/api会被微信拦截。腾讯云免费证书默认只含裸域,需在申请时勾选“自动包含 www”。
5.2 云函数发布:从本地调试到线上可用的三个必检项
云函数是业务逻辑核心,发布前务必验证:
| 检查项 | 操作方式 | 不通过后果 |
|---|---|---|
| 环境变量注入 | 进入云开发控制台 →「云函数」→「函数详情」→「环境变量」,确认WECHAT_APPID,WECHAT_MCH_ID,WECHAT_MCH_KEY全部存在且值正确 | 支付、登录、模板消息全部失效 |
| 超时时间 ≥ 10s | 函数详情 →「函数配置」→「超时时间」设为15 | unifiedOrder等支付类函数因网络波动超时,用户支付页卡死 |
| 内存 ≥ 256MB | 同上 →「内存规格」设为256 | getMenuByTable查询大量菜品时 OOM 崩溃 |
发布命令(需安装cloudbase-cli):
# 在 cloudfunctions/ 目录下执行 tcb fn deploy unifiedOrder --region ap-shanghai --timeout 15 --memory 2565.3 数据库初始化:用 JSON 模板一键填充基础数据
云开发数据库需手动创建集合并导入初始数据。项目README.md中应提供init-data.json示例:
// init-data.json { "stores": [ { "_id": "store_001", "name": "总店", "address": "XX路123号" } ], "tables": [ { "_id": "table_001", "no": "01", "storeId": "store_001" } ], "goods": [ { "_id": "goods_001", "name": "酸辣粉", "price": 1200, "storeId": "store_001" } ] }导入步骤:
- 登录云开发控制台 →「数据库」→「导入数据」
- 选择
init-data.json,勾选「清空目标集合」 - 点击「开始导入」,等待状态变为「成功」
血泪经验:
goods集合中price字段必须为整数(单位:分),若误填12.00(浮点数),后续计算总价时1200 * 2 = 2400.0000000000005,导致支付金额校验失败。
5.4 打印机对接:用标准 HTTP API 适配任意品牌热敏打印机
本项目厨房打印不依赖特定SDK,而是调用打印机厂商提供的HTTP接口。以常见品牌为例:
| 品牌 | 接口地址 | 请求方式 | 关键Header |
|---|---|---|---|
| 易联云 | http://192.168.1.100:8008/pos | POST | Content-Type: application/json |
| 新大陆 | http://192.168.1.100:8080/print | GET | X-Printer-SN: SN123456789 |
云函数printOrder中只需按品牌切换 URL 和参数:
// cloudfunctions/printOrder/index.js const PRINTER_CONFIG = { 'yilianyun': { url: 'http://192.168.1.100:8008/pos', method: 'POST' }, 'xinludalu': { url: 'http://192.168.1.100:8080/print', method: 'GET' } } const config = PRINTER_CONFIG[printerBrand] wx.request({ url: config.url, method: config.method, data: buildPrintData(order), // 构建打印JSON success: () => console.log('打印成功') })提示:打印机IP必须与云函数所在服务器在同一局域网(云开发云函数无法直连内网设备),因此需将
printOrder部署到自有云服务器,或改用支持公网回调的智能打印机(如易联云“云打印”模式)。
6. 进阶技巧:用「订单状态机」替代 if-else,让业务逻辑不再随需求膨胀
6.1 为什么传统状态判断会失控?
初期代码常这样写:
// ❌ 反模式:状态分支爆炸 if (status === 'pending') { if (action === 'accept') updateStatus('accepted') else if (action === 'cancel') updateStatus('canceled') } else if (status === 'accepted') { if (action === 'complete') updateStatus('completed') else if (action === 'reject') updateStatus('rejected') } // ……当新增“退款中”“已退款”状态时,这里要加12行一旦运营提出“顾客下单后15分钟未接单自动取消”,就得在pending分支里塞定时器逻辑,代码迅速变成黑匣子。
6.2 用状态机表驱动:把规则从代码里抽出来
我们在云数据库新建order_status_rules集合,存一张状态跃迁表:
| fromStatus | toStatus | action | allowed | timeoutMinutes | notifyUser |
|---|---|---|---|---|---|
| pending | accepted | accept | true | - | true |
| pending | canceled | cancel | true | - | true |
| pending | canceled | auto | true | 15 | true |
| accepted | completed | complete | true | - | true |
| completed | refunded | refund | true | - | true |
云函数updateOrderStatus改为查表驱动:
// cloudfunctions/updateOrderStatus/index.js exports.main = async (event) => { const { orderId, action } = event const order = await db.collection('orders').doc(orderId).get() // 查状态机表,确认本次操作是否允许 const rule = await db.collection('order_status_rules') .where({ fromStatus: order.data.status, action: action, allowed: true }).limit(1).get() if (!rule.data.length) { throw new Error(`状态[${order.data.status}]不允许执行[${action}]`) } // 执行状态更新 await db.collection('orders').doc(orderId).update({ status: rule.data[0].toStatus, updatedAt: Date.now() }) // 若需通知用户,触发模板消息 if (rule.data[0].notifyUser) { await sendTemplateMsg(orderId, rule.data[0].toStatus) } }6.3 运营后台可视化配置:让店主自己改规则
最后一步,用小程序pages/admin/status-rules实现简易后台:
<!-- miniprogram/pages/admin/status-rules.wxml --> <view class="rule-item" wx:for="{{rules}}" wx:key="id"> <text>{{item.fromStatus}} → {{item.toStatus}}</text> <switch checked="{{item.allowed}}" bindchange="toggleRule" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />