简介:这套外卖扫码点餐全开源小程序源码,是一份可直接用于微信端扫码点餐场景的完整项目,面向小程序开发者、PHP后端工程师以及希望自建点餐系统的小型餐饮商家,能大幅缩短从零开发与联调的成本。包内共9320个文件,压缩包约24.66MB,以PHP后端逻辑、Vue管理端、JS/WXML小程序端和CSS样式为核心,辅以JSON配置、Markdown说明及大量PNG/GIF/JPG界面素材,整体结构较完整。资源并非单一页面,而是覆盖用户扫码浏览菜单、购物车、下单支付、商家接单与订单通知等环节,并包含数据库脚本、支付相关配置、HTTPS证书示例、部署说明等周边文件;有编程基础的人可据此二次开发,扩展优惠券、会员、评价等功能。目前已有736人浏览学习,适合用来学习微信支付对接、小程序前后端协作及餐饮系统常见业务逻辑。
1. 外卖扫码点餐小程序源码下载回来后,先要分清它能不能跑
你在站长下载站拿到的那份“外卖扫码点餐全开源小程序源码【价值2400元】.zip”,解压后大概率是两层嵌套目录:一层放小程序前端,一层放服务端。决定这套源码能不能跑起来的,从来不是代码体积,而是数据库脚本、支付配置和部署说明是否完整。见多了前端做得很完整、后端 SQL 却是空壳,或者商品数据写死在 JSON 文件里、顾客扫码只能看不能点的例子。下面的内容按“解压→建库→改配置→拉起支付→上线前排查”的顺序,把一套常见的外卖扫码点餐小程序源码跑通为止,中途会遇到的坑逐个点出来。
2. 扫码点餐源码解包:前端框架、后端语言与数据库表一眼定位
2.1 先分清三类“全开源”:模板、完整版、SaaS 拆包
市面流通的扫码点餐源码,形态基本分三种。第一种是纯前端模板,没有后端,商品数据写死在pages/data/goods.js里,适合当界面原型。第二种是前后端完整版,小程序端加 API 服务端加 SQL 脚本,能真正跑通下单支付。第三种是 SaaS 多商户系统的拆包版,原项目包含商户后台、骑手端、平台管理端,分发时只留下小程序端和部分 API,并依靠请求头里的固定 token 做授权校验。
判断一步到位,看 zip 根目录就够了。扫码点餐本质上就是一个轻量级小程序商城,商品换成菜品,支付入口前置到桌码,所以结构上和商城系统几乎同构。下列表格里列的目录特征,比 README 里的宣传词可靠得多。
| 形态 | 典型目录特征 | 是否含后端 | 能跑到哪一步 |
|---|---|---|---|
| 纯前端模板 | miniprogram/或pages/+utils/ | 无 | 菜单展示、加购物车、本地计算价格 |
| 前后端完整版 | client/+server/+order.sql | 有 | 扫码、下单、支付、订单查询 |
| SaaS 拆包版 | mp-weixin/+api/+docs/ | 残缺 | 能进首页,常卡在授权或 401 |
拆包版最难缠,它把“后端校验”和“前端渲染”拆开卖。如果解压后只有一个 README 写着“申请者请联系作者开通授权”,说明核心校验逻辑根本没给你,这套源码换个商家 ID 就无法工作。识别方法:全局搜Authorization,在公共请求文件里出现次数超过三处,就要怀疑它依赖远端授权服务。
前端的商品展示页做得再漂亮,只要请求层带了硬编码 token,数据源就不可控。真正的全开源包应该能让你在本地新建一家店,从前台到后台完整走通。判断标准不在于有没有源码,而在于能不能脱离原作者的服务器独立运行。
2.2 前端框架定位:原生微信小程序还是 uniapp 微信小程序
拿到前端目录先别急着打开微信开发者工具,先看两个标志文件。原生微信小程序一定有app.json、app.js、project.config.json,而 uniapp 微信小程序项目带的是manifest.json、pages.json、uni.scss,且常见src/pages结构。
# 在解压后的前端目录执行 ls -1 # 有 app.json、sitemap.json -> 原生小程序,直接导入微信开发者工具 # 有 manifest.json、pages.json -> uniapp 项目,需要 HBuilderX 或 npm run build:mp-weixin如果是 uniapp 项目,老教程会让你装 HBuilderX,再“运行到小程序模拟器”;新项目可以直接npm i && npm run build:mp-weixin,然后导入dist/build/mp-weixin。两种方式都在dist下生成一份原生小程序产物,微信开发者工具实际只认这份产物,改源码后必须重新构建。
实操时有个容易忽略的点:project.config.json里的miniprogramRoot是否指向正确目录。uniapp 产物一般放在dist/build/mp-weixin,如果miniprogramRoot还写着上一层的.,导入后微信开发者工具会提示“找不到入口文件”。
提示:老项目常见原生写法,新项目常见 uniapp。两种都能交付,但排错方向不同——原生项目直接改
app.json的页面路由,uniapp 则要回到pages.json里改,改错文件位置会导致页面白屏且不好定位。
2.3 后端定位与数据表设计
2.3.1 从目录反推后端语言和框架
后端目录通常叫server、api、backend或admin。看两个文件:有没有composer.json(PHP)、pom.xml(Java)、package.json(Node);有没有application/或app/下的控制器目录。
最常见组合是 ThinkPHP 5/6 写 API,MySQL 存数据,这类项目入口在public/index.php,路由形如index.php/api/store/info。如果看到webman或Hyperf,属于进阶框架,需求不复杂时不必优先选它,社区资料少,出了问题要自己翻框架源码。Java 版则常配 MyBatis-Plus,表结构里一般有mybatis-plus的注解或 XML 文件,跑通起来比 PHP 版重,但订单和库存的并发处理能力更好。
2.3.2 核心数据表:从 SQL 脚本反推业务边界
要看透一套源码,读 SQL 最快。扫码点餐系统再怎么封装,绕不开商家、菜品、订单三张主表,加一张订单明细表。
-- 商家表:一家店对应一个桌码 CREATE TABLE `store` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '店铺名称', `logo` varchar(255) DEFAULT '' COMMENT '店铺logo', `business_status` tinyint(1) DEFAULT '1' COMMENT '1营业中 0打烊', `create_time` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表'; -- 商品表:归属商户、分类,价格用 decimal CREATE TABLE `goods` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `store_id` int(11) unsigned NOT NULL COMMENT '商家id', `category_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '菜品分类', `name` varchar(100) NOT NULL COMMENT '菜品名称', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '售价,单位元', `stock` int(11) NOT NULL DEFAULT '9999' COMMENT '库存,-1为不限', `status` tinyint(1) DEFAULT '1' COMMENT '1上架 0下架', PRIMARY KEY (`id`), KEY `idx_store_category` (`store_id`,`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -- 订单表:状态机是整套系统的核心 CREATE TABLE `order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `store_id` int(11) unsigned NOT NULL COMMENT '商家id', `table_no` varchar(20) DEFAULT '' COMMENT '桌号', `total_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额,单位元', `status` tinyint(1) DEFAULT '1' COMMENT '1待支付 2已支付 3已上菜 4完成 0取消', `pay_time` int(11) DEFAULT NULL COMMENT '支付时间', `create_time` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';看表结构时留意三个细节。price用decimal(10,2)而不是float,说明作者考虑了金额精度,改造空间大。stock默认 9999 是“不限制库存”的老写法,下单扣库存时要先判断这个边界值。订单表有pay_time而没有refund_time,说明基础版本很可能不含退款流程,接入微信支付后,退款要么去商户平台手动操作,要么自己补一张退款流水表。
订单明细表同样关键,它记录下单时刻的商品快照。如果明细里只存goods_id而不存goods_name和price,那订单列表页一定得去回查商品表,商品改名或改价后历史订单会显示错乱,这种设计在点餐场景里属于缺陷。
3. 本地跑通外卖点餐小程序:解压、建库、改配置的三步流程
3.1 解压 zip:密码、中文乱码与目录路径
源码包的 zip 经常带两层压缩,外层还可能有密码。与其找 zip 压缩包密码破解工具,不如回下载页看作者有没有把密码写在介绍里,大部分开源包的密码是站点域名或固定引导语。Windows 下直接右键解压遇到“压缩包已损坏”或中文目录乱码,换 Bandizip 或 7-Zip 打开,语言编码选“自动检测”。命令行环境则显式指定编码:
# Linux 下解压并强制 UTF-8 文件名 unzip -O UTF-8 order-diancan.zip -d /data/www/order ls /data/www/order # macOS 自带 unzip 不支持 -O,改用 7z 处理 7z x order-diancan.zip -o/data/www/order解压后记住一个原则:目录路径不要带中文、空格和#符号。微信开发者工具的构建环节对含空格路径兼容得很差,报错往往出现在编译阶段而不是导入阶段,排查成本高。我一般会把项目放在D:/workspace/order这类纯英文路径下再开始改代码。
3.2 建库导库与后端启动
后端以最常见的 ThinkPHP 项目为例。先启动 phpstudy 或宝塔里的 Nginx/Apache,确认 PHP 版本切到 7.4 左右——老项目跑在 PHP 8.0 以上会因each()、implode()传参顺序这类兼容问题直接 500,这是第一个大坑。
# 1. 创建数据库,字符集和 SQL 脚本保持一致 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS diancan DEFAULT CHARACTER SET utf8mb4;" # 2. 导入业务表 mysql -uroot -p diancan < server/order.sql # 3. 进入后端口,改数据连接配置 # ThinkPHP5 改 application/database.php,TP6 改 .env 的 database 段 # 4. 启动后端,0.0.0.0 是为了让真机可以访问 cd server && php think run --host 0.0.0.0 --port 8080这里有个经常被绕过的步骤:Nginx 伪静态规则。ThinkPHP 的接口地址形如index.php/api/xxx,伪静态没配置会被 Nginx 直接按路径找文件,返回“404 Not Found”。phpstudy 的 Nginx 配置里加上:
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } }改完重启 Nginx,用curl http://127.0.0.1:8080/api/index验证,能返回 JSON 就算通了。如果返回“数据库连接失败”,优先检查数据库账号密码是否写在正确位置,而不是怀疑 PHP 扩展缺失。Redis 未启动导致的报错也常见,先redis-cli ping确认服务在跑,再查代码里的缓存驱动配置。
3.3 小程序端导入与接口地址修改
微信开发者工具里选择“导入项目”,目录指向前端文件夹,AppID 可以先填测试号,登录微信公众平台后申请即可,不影响代码逻辑调试。导入完成后第一件事是替换接口域名,老源码常把 baseUrl 散落在utils/api.js和多个页面里,统一收敛到一个共享配置里更省事:
// utils/config.js module.exports = { // 本地联调用局域网 IP,不要写 localhost baseUrl: 'http://192.168.1.101:8080', // 扫码场景下网络波动大,默认 5000 可能不够 requestTimeout: 8000 }// utils/request.js const config = require('./config.js') function request(url, data, method = 'GET') { return new Promise((resolve, reject) => { wx.request({ url: config.baseUrl + url, method, data, timeout: config.requestTimeout, header: { 'content-type': 'application/json', // 老项目可能要求固定 token,联调阶段先留着 Authorization: wx.getStorageSync('token') || '' }, success: res => { if (res.data.code === 0) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail: err => reject(err) }) }) }注意两个点:localhost在小程序真机上指向手机自己,不是开发电脑,所以联调必须用局域网 IP;开发阶段需要在“详情 → 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,否则本地发出的请求会被微信直接拦截,报错样式还是request:fail。
3.4 必改配置与域名校验表
跑通本地的最小配置项,按优先级排序如下。真正影响本地跑通的只有前两项,后三项留到上线前处理。
| 配置项 | 位置 | 值示例 | 漏改的后果 |
|---|---|---|---|
| 数据库连接 | server/.env或database.php | DB_HOST=127.0.0.1 | 后端 500,SQL 报错 |
| API 基础地址 | client/utils/config.js | http://192.168.1.101:8080 | 小程序端 request:fail |
| 小程序 AppID | 微信开发者工具“详情” | 测试号亦可 | 真机预览受限 |
| 商户号/支付密钥 | server/application/api/config.php | 上线前填真实值 | 支付拉起失败 |
| 合法域名 | mp.weixin.qq.com 后台 | https://api.xxx.com | 正式环境无法请求 |
支付密钥这类信息,老源码里很容易找占位符,全局搜mchid或merchant_id就能定位。Authorization请求头如果是硬编码的Bearer xxx,属于 SaaS 拆包版残留逻辑,后端校验一旦失效所有接口都会打回 401,处理办法是回到 token 机制或者直接去掉这行注入。
4. 从扫码到支付:点餐小程序主链路的代码级拆解
4.1 扫码进入:scene 参数解析与 store 定位
桌码的载体是“小程序码”,用户扫出来之后,微信会把码内参数放到页面onLoad的options.scene里。这个参数需要先decodeURIComponent再拆解。
// pages/index/index.js const { getStoreInfo } = require('../../utils/api.js') Page({ onLoad(options) { let storeId = 0 if (options.scene) { // scene 是 URL 编码过的字符串,约定格式 store_id=12&table_no=A08 const scene = decodeURIComponent(options.scene) const params = {} scene.split('&').forEach(item => { const [k, v] = item.split('=') params[k] = v }) storeId = Number(params.store_id) this.setData({ tableNo: params.table_no || '' }) } else if (options.store_id) { // 开发调试时,直接编译模式里传 store_id 更方便 storeId = Number(options.store_id) } if (!storeId) { wx.showToast({ title: '二维码参数无效', icon: 'none' }) return } this.setData({ storeId }) this.fetchStoreAndMenu(storeId) }, fetchStoreAndMenu(storeId) { getStoreInfo(storeId).then(info => { // 营业状态必须放在渲染菜单之前判断 if (info.business_status !== 1) { wx.showToast({ title: '店铺休息中', icon: 'none' }) return } // 顶部标题跟随店铺名动态变化 wx.setNavigationBarTitle({ title: info.name }) this.setData({ storeInfo: info, goodsList: info.goods_list }) }) } })scene 参数最长 32 个可见字符,不要塞 JSON 或超长 ID,约定store_id和table_no两个字段足够。这段代码也顺带实现了“小程序动态设置标题”的需求,把默认的“点餐”改成实际店铺名,顾客扫码后能立刻确认进对了店。
4.2 菜品列表与购物车的本地状态管理
扫码点餐的购物车,常见实现是“本地缓存 + 下单时合并提交”,不建购物车表。好处是后端压力小,坏处是刷新页面购物车丢失,对堂食场景影响不大。
// utils/cart.js 中的加购逻辑 const CART_KEY = 'CART_' function getCart(storeId) { return wx.getStorageSync(CART_KEY + storeId) || [] } function addToCart(storeId, goods) { const cart = getCart(storeId) const existed = cart.find(item => item.goods_id === goods.id) if (existed) { existed.count += 1 } else { cart.push({ goods_id: goods.id, name: goods.name, price: goods.price, count: 1 }) } wx.setStorageSync(CART_KEY + storeId, cart) return cart }cart.find()是 ES6 语法,微信开发者工具默认支持,但老项目中“ES6 转 ES5”开关没打开时,低版本安卓会报find is not a function。遇到这种情况,要么把project.config.json里的es6字段设为true,要么换成for循环。购物车总价计算注意用Number(item.price)做数值转换,避免字符串拼接导致总价变成一串数字。
菜品若有辣度、冰量这类规格,这套结构就不够用。需要给goods表加sku字段或者在购物车条目里加spec对象,价格计算优先用 SKU 里的价格,没有 SKU 才回退到商品单价。
4.3 提单与微信支付的接口约定
4.3.1 小程序端支付参数组装
下单和支付分两步:先调POST /api/order/create创建订单,拿到统一下单返回的支付参数,再用wx.requestPayment拉起收银台。
// pages/checkout/checkout.js submitOrder() { const { storeId, tableNo, cart, remark } = this.data if (!cart.length) { wx.showToast({ title: '购物车为空', icon: 'none' }) return } wx.request({ url: `${config.baseUrl}/api/order/create`, method: 'POST', data: { store_id: storeId, table_no: tableNo, goods_list: cart.map(item => ({ goods_id: item.goods_id, count: item.count })), remark: remark || '' }, success: (res) => { if (res.data.code === 0) { const pay = res.data.data.payment wx.requestPayment({ timeStamp: pay.timeStamp, nonceStr: pay.nonceStr, package: pay.package, signType: 'MD5', paySign: pay.paySign, success: () => { wx.redirectTo({ url: `/pages/order/detail?id=${res.data.data.order_id}` }) }, fail: () => { // 用户取消也走 fail,应提示订单已生成可稍后支付,而不是“支付失败” wx.showToast({ title: '订单已生成,可稍后支付', icon: 'none' }) } }) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) } } }) }支付相关的timeStamp、nonceStr、package、paySign必须全部由后端生成,前端只是搬运,任何一项缺失都会报“商户参数校验失败”。signType常见MD5和HMAC-SHA256,要和后端统一下单时传的值保持一致。
4.3.2 后端回调与订单状态流转
支付成功后,微信服务器异步回调商户配置的回调地址,这个回调才是订单状态真正变更的地方。前端wx.requestPayment的 success 只是客户端视角,不能作为入账依据。
// thinkphp 风格支付回调入口(结构示例) public function notify() { $data = json_decode(file_get_contents('php://input'), true); // 1. 验签:老项目多为 V2 商户KEY签名,V3 需平台证书验签 // 2. 金额校验:回调金额与订单金额必须一致,单位不一致直接拒收 $order = OrderModel::where('order_no', $data['out_trade_no'])->find(); if (!$order || bccomp($order['total_price'], $data['total_fee'] / 100, 2) !== 0) { return json(['code' => 'FAIL', 'msg' => '金额不一致']); } // 3. 幂等更新:只有待支付状态才能改为已支付 OrderModel::where('id', $order['id'])->where('status', 1)->update([ 'status' => 2, 'pay_time' => time(), 'trade_no' => $data['transaction_id'] ]); // 4. 返回固定成功标记,告诉微信不要重试 return json(['code' => 'SUCCESS', 'message' => '成功']); }bccomp做金额比较是为了避免浮点精度问题。键名out_trade_no、total_fee、transaction_id是微信支付老接口的惯用字段,源码里搜得到这些键名说明走的是 V2 协议;搜索resource、ciphertext则是 V3。V2 回调 URL 必须 HTTPS 且域名已完成备案,否则微信支付平台根本不会发起回调。
订单状态流转,从创建到完成一共六种:
| 状态码 | 含义 | 触发点 |
|---|---|---|
| 0 | 已取消 | 用户取消或超时未支付 |
| 1 | 待支付 | 下单成功未支付 |
| 2 | 已支付 | 微信支付回调成功 |
| 3 | 已上菜 | 商家后台点“上菜” |
| 4 | 已完成 | 用户确认或商家操作完成 |
| 5 | 退款 | 商家后台操作退款 |
老版本里常见反过来的定义:status=0表示待支付,status=1表示已支付。改任何订单列表页之前,先翻一次 SQL 里的 COMMENT 注释确认状态枚举,再动手写逻辑,避免把“待支付”和“已支付”渲染颠倒。
5. 上线前排查清单:合法域名、支付回调与高频报错
5.1 高频报错对照表
跑通本地之后,下面六类问题提前自查一遍,能省下大量联调时间。
| 现象 | 根因 | 处理方式 |
|---|---|---|
request:fail url not in domain list | 正式环境未配置合法域名 | 后台配置 request 合法域名,必须是 HTTPS |
| 页面白屏、菜单一直 loading | store_id没解析到或接口 404 | 先看 Network 请求 URL,再查 scene 参数 |
| 所有接口 401 | 全局请求头 token 失效 | 去掉硬编码 token,或检查登录态续期 |
| 下单报“店铺休息中” | business_status字段不符 | 改数据库对应字段,再重新进页面 |
| 支付后订单仍是待支付 | 回调地址错误或验签失败 | 查后端日志,对比金额单位 |
后端 500、Call to undefined function | PHP 扩展缺失或 PHP8 兼容 | 切回 PHP 7.4,检查 fileinfo、redis 扩展 |
5.2 支付回调排查的三个固定动作
支付回调出问题,不要急着改代码。先看后端日志里有没有微信支付平台的请求记录。完全没有记录,说明回调 URL 没配或域名不可达;有记录但返回 500,再去看验签逻辑;返回 SUCCESS 但订单没变化,十有八九是幂等更新里where('status', 1)把二次回调挡住,或者金额单位差 100 倍。
本地调试用 HTTP 跑通整条链路后,上线时最容易漏掉回调地址改 HTTPS。微信支付平台要求回调地址必须 HTTPS 且域名已备案。最有效的排查手段是在回调入口加一行日志把请求体原样打出来,用微信支付商户平台的“回调通知调试”工具手动触发一次,问题出在哪一层立刻清楚。
5.3 真机预览的最后一公里
上线前必须用真机测试,模拟器覆盖不了摄像头扫码和真机网络环境。拉上店长用自己手机扫码进店,先看加载速度,再走一遍完整下单流程。店里 WiFi 如果接了强制跳转认证的门禁,小程序请求会被掐断,这是点餐系统线下场景最常见的问题。
真机适配注意刘海屏和底部安全区。自定义导航栏时,先调用wx.getMenuButtonBoundingClientRect()拿到胶囊按钮位置,再配合wx.getSystemInfoSync()的状态栏高度计算导航栏高度,别在 iPhone 上把返回键顶进状态栏。同时把编译模式里的store_id换成店铺真实 ID,重新生成一张带 scene 参数的体验版二维码,从扫码进店到支付完成整个流程走一遍;最后检查小程序备案信息,备注按营业执照经营范围填写,餐饮店就写“餐饮服务、外卖送餐服务”,不要写“软件开发”,业务对不上会被审核退回。确认顶部标题、桌号、购物车数量和订单状态全部正确,这套源码才算正式接手。
本文还有配套的精品资源,点击获取