
简介一套基于微信小程序的外卖扫码点餐全开源源码专为中小餐饮商家、独立开发者及小程序学习者打造解决从零开发点餐系统周期长、成本高的问题。源码完整覆盖“扫码—浏览菜单—下单—支付—商家接单”核心链路已集成微信支付、订单状态推送、数据加密等常见安全机制并附带数据库建表语句、后端API接口示例及部署配置说明便于直接运行或二次开发。资源包为zip格式共9320个文件约24.66MB其中PHP文件占6898个构成后端服务主体另有JavaScript、Vue、CSS构建前端交互界面JSON用于数据交换Markdown文档提供使用指引其余为界面切图、字体和辅助脚本整体目录层级清晰适合按模块研读。目前已有736人加入学习无论是想快速上线一套点餐系统还是研究微信小程序与PHP后端的完整协作方式这份源码都能提供充足素材也可在此基础扩展会员、优惠券等功能。1. 扫码点餐不只是省一个服务员从桌码到订单状态机餐饮门店的痛点往往不在“点餐”本身而在高峰期的人效。顾客等服务员、服务员跑桌台、后厨听口头报单任何一个环节出现差错都会直接变成退单和差评。外卖扫码点餐小程序的核心价值是把“桌台身份”和“下单链路”绑定到一张二维码上顾客扫桌码看到的是本桌菜品下单后订单进入商家端的待处理队列后厨按单出餐整个过程不再依赖人力传话。这套全开源小程序源码的价值在于它不是那种只有前端界面的演示项目而是包含了完整的后端接口、数据库表设计和商户端处理逻辑。从源码结构看后端基于 PHP 组件体系carbon、phpunit 等一批 composer 工具链文件在源码包里前端是微信小程序原生语法适合已经有小程序基础、想直接跑到线上的个人开发者或小型餐饮技术服务商。本文从数据模型、下单接口、支付回调和部署验证四条线拆开讲每一条都按能直接复现的标准来写。2. 数据模型设计四张核心表如何撑起扫码点餐2.1 从桌码到订单的关系链扫码点餐和普通商城最大的区别在于商城是“人找店”扫码点餐是“码找人”。顾客扫的码里携带门店 ID 和桌台 ID这两个参数贯穿整个下单流程。因此数据模型的第一层是门店、桌台、菜品、订单四张表的关系设计。常见做法是把门店和桌台分开建表而不是把桌台信息直接写死在二维码里。原因是门店可能有多层楼、多个区域桌台需要支持换桌、并桌和禁用单独建表才能灵活维护。菜品表则要冗余门店 ID方便商家后台做菜品管理时不需要跨店查询。2.2 核心建表 SQL 与字段选型以下 SQL 是从这套源码的常见结构反推的标准实现可直接用于 MySQL 5.7 或 8.0字符集用 utf8mb4避免菜品名里的特殊字符乱码。CREATE TABLE shop ( id int(11) NOT NULL AUTO_INCREMENT, shop_name varchar(64) NOT NULL COMMENT 门店名称, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 营业状态 1营业 0休息, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门店表; CREATE TABLE table ( id int(11) NOT NULL AUTO_INCREMENT, shop_id int(11) NOT NULL COMMENT 所属门店, table_no varchar(16) NOT NULL COMMENT 桌号如 A12, qr_scene varchar(128) NOT NULL COMMENT 二维码scene参数, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0空闲 1就餐中, PRIMARY KEY (id), UNIQUE KEY uk_shop_table (shop_id, table_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT桌台表; CREATE TABLE dish ( id int(11) NOT NULL AUTO_INCREMENT, shop_id int(11) NOT NULL, category_id int(11) NOT NULL COMMENT 菜品分类, dish_name varchar(64) NOT NULL, price decimal(10,2) NOT NULL COMMENT 单价注意用 decimal, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存0表示不计库存, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id), KEY idx_shop_category (shop_id, category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT菜品表; CREATE TABLE order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 业务订单号, shop_id int(11) NOT NULL, table_id int(11) NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3已完成 4已取消, pay_time datetime DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_shop_status (shop_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这里有几个选型要点。金额字段必须用decimal(10,2)而不是float因为浮点数在累加和比较时会丢失精度餐饮订单涉及到对账一分钱都不能差。qr_scene字段存的是二维码里的 scene 参数格式如shop1table12而不是存整个二维码链接这样即使二维码图片更换域名数据库里的记录也不需要改。订单号用独立字段并加唯一索引后续支付回调、对账查询都走这个字段避免用自增 ID 做业务关联。2.3 订单状态机的边界status 字段虽然只是一个整数但它的流转规则直接决定了整个点餐系统的稳定性。状态迁移是有限集合待支付只能到已支付或已取消已支付后进入制作中制作中到已完成中间不允许跳转。这个约束不能只靠前端隐藏按钮后端在下单、支付回调、商家接单三个接口里都必须校验当前状态。如果不做状态机校验会出现重复支付回调把订单状态覆盖成已支付之前的值或者商家在顾客未付款时就接单出餐的严重问题。源码中通常在OrderService里封装一个changeStatus()方法每次变更都带当前状态作为条件执行 SQL UPDATE 的 WHERE 子句这一步是生产环境的基本功。3. 订单接口链路库存扣减、支付回调与幂等设计3.1 下单接口的完整调用链扫码点餐的前端在小程序端后端接口至少需要提供菜单列表、提交订单、支付参数、订单状态查询、商家接单、订单完成这六类能力。以下是一组标准的接口映射表接口路径方法入参要点返回要点/api/menu/listGETshop_id, table_id分类菜品列表/api/order/submitPOSTtable_id, items[{dish_id, quantity}], remarkorder_no, 应付金额/api/order/payParamsGETorder_no, openid微信支付所需参数/api/order/statusGETorder_no当前状态/api/merchant/acceptPOSTorder_no接单status 1→2/api/order/completePOSTorder_no完成status 2→3订单提交流程是整条链路的重点常见实现用事务保证一致性先锁桌台再逐项扣减菜品库存最后生成订单。下面是一段基于 ThinkPHP 6 风格的提交订单核心代码这套源码的 PHP 组件结构与此类似。// application/api/controller/Order.php 部分逻辑 public function submit() { $input json_decode(file_get_contents(php://input), true); $tableId $input[table_id] ?? 0; $items $input[items] ?? []; if (empty($tableId) || empty($items)) { return json([code 400, msg 参数错误]); } Db::startTrans(); try { // 1. 锁桌台防止多人同时扫码操作同一桌 $table Db::name(table)-where(id, $tableId)-lock(true)-find(); if (!$table || $table[status] ! 0) { throw new \Exception(该桌已有人就餐或不可用); } // 2. 计算总额并校验菜品状态 $total 0; foreach ($items as $item) { $dish Db::name(dish)-where(id, $item[dish_id])-lock(true)-find(); if (!$dish || $dish[status] ! 1) { throw new \Exception(菜品已下架 . $item[dish_id]); } $total bcmul($dish[price], $item[quantity], 2); } // 3. 生成订单号并入库 $orderNo date(YmdHis) . str_pad(mt_rand(1, 9999), 4, 0, STR_PAD_LEFT); $orderId Db::name(order)-insertGetId([ order_no $orderNo, shop_id $table[shop_id], table_id $tableId, total_amount $total, status 0 ]); // 4. 订单明细入库 foreach ($items as $item) { Db::name(order_item)-insert([ order_id $orderId, dish_id $item[dish_id], quantity $item[quantity], price $dish[price] ]); } Db::commit(); return json([code 0, data [order_no $orderNo, amount $total]]); } catch (\Exception $e) { Db::rollback(); return json([code 500, msg $e-getMessage()]); } }lock(true)生成的是SELECT ... FOR UPDATE这是 MySQL 行级锁的写法作用是让同一个桌台的并发下单请求排队执行。没有这行锁两个顾客同时扫同一个桌码提交订单会出现桌台状态被覆盖或库存扣成负数的问题。金额计算用bcmul而不是直接相乘是为了避免浮点运算误差这在 PHP 的 decimal 计算中是标准做法。3.2 微信支付回调的验签与幂等支付回调是扫码点餐最容易出问题的地方。微信服务器会以 POST 请求回调你配置的 notify_url回调数据是 XML 格式包含out_trade_no业务订单号、transaction_id微信支付单号、result_code、total_fee等字段。收到回调后必须先验签再更新订单状态。验签的逻辑是取出所有非空字段按参数名 ASCII 码升序排序拼接成key1value1key2value2格式末尾拼上商户密钥MD5 后与sign字段比对。下面是核心验签与订单更新逻辑。// 简化版回调验签 幂等更新 $xml file_get_contents(php://input); $data json_decode(json_encode(simplexml_load_string($xml, SimpleXMLElement, LIBXML_NOCDATA)), true); unset($data[sign]); ksort($data); $signStr urldecode(http_build_query($data)) . key . $wechatPayKey; if (md5($signStr) ! $xmlSign) { // 返回给微信的失败响应不要 echo true return 验签失败; } if ($data[result_code] SUCCESS $data[return_code] SUCCESS) { // 幂等更新WHERE status0 保证重复回调不重复处理 $updated Db::name(order) -where(order_no, $data[out_trade_no]) -where(status, 0) -update([status 1, pay_time date(Y-m-d H:i:s), transaction_id $data[transaction_id]]); if ($updated) { // 通知商家端有新订单 $this-notifyMerchant($data[out_trade_no]); } }WHERE status0这一句是关键。微信服务器在没收到成功响应时会重试多次回调如果回调处理没有幂等保护同一笔订单会被更新两次商户端收到两条新单通知后厨重复出餐。加了status0条件后第二次回调进来时订单已经是已支付状态更新受影响行数为 0直接跳过通知逻辑。第一次回调处理完返回给微信的响应必须是success字符串微信在收到success或SUCCESS时才会停止重试。3.3 商家端接单与桌台状态回流支付成功后桌台状态需要从“空闲”变成“就餐中”这一步放在接单或支付回调里做都可以。常见做法是放在商家确认接单时因为顾客可能支付成功后立即要求退款如果提前锁定桌台退款完成的订单还需要额外处理桌台释放逻辑。接单接口的逻辑相对简单UPDATE order SET status2 WHERE order_no? AND status1同样带状态条件防止并发重复接单。桌台状态更新为就餐中完成后接口把订单置为 3桌台置为空闲。这里要注意一张桌如果有多笔订单加菜场景桌台释放必须等所有订单都完成不能只看一单。这个边界在源码中通常通过查询该桌是否存在状态小于 3 的订单来判断。4. 从源码到线上环境部署与小程序后台配置4.1 源码目录结构识别与运行环境拿到源码包先不要急着上传服务器先把目录结构过一遍。这套源码的根目录下能看到random_compat.phar、carbon.bat、phpunit.bat、psysh.bat等文件这些是 PHP composer 依赖工具链和开发辅助脚本说明后端是 PHP 项目开发环境里用过 composer 安装依赖。.browserslistrc文件通常在管理后台前端项目中使用用于控制 CSS 自动补全的浏览器兼容范围。整体环境依赖如下组件版本建议说明PHP7.4 或 8.0兼容常用 ThinkPHP / Laravel 版本MySQL5.7需要支持 utf8mb4 与 InnoDB 行锁Nginx1.18静态分离伪静态配置微信小程序账号企业主体个人主体无法开通微信支付SSL 证书必须微信支付回调要求 HTTPS配置 PHP 时要注意几个扩展pdo_mysql必须开启bcmath用于金额计算curl用于调用微信 APIopenssl用于支付签名。源码如果基于 ThinkPHP 6还需要在 Nginx 里做 pathinfo 伪静态。4.2 Nginx 站点配置参考小程序前端的接口请求全部走 HTTPSNginx 配置需要同时处理 PHP 解析和静态资源。下面是一份基础的 server 块配置。server { listen 443 ssl http2; server_name yourdomain.com; ssl_certificate /etc/nginx/cert/yourdomain.pem; ssl_certificate_key /etc/nginx/cert/yourdomain.key; root /var/www/order; # 源码部署目录 index index.php index.html; # 伪静态ThinkPHP 路由重写 location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # 支付回调不缓存 location /api/pay/notify { add_header Cache-Control no-store; } }rewrite ^(.*)$ /index.php?s$1 last;是 ThinkPHP 的典型伪静态规则s参数指向路由参数。如果你的源码是 Laravel则需要改成rewrite ^(.*)$ /index.php/$1 last;。支付回调接口单独禁用缓存防止微信回调被 CDN 或 Nginx 缓存层拦截。部署完成后用curl -I https://yourdomain.com/api/menu/list验证接口是否正常响应。4.3 小程序后台的域名白名单配置小程序端不是随便配置一个接口地址就能跑通的。微信公众平台后台的“开发管理 - 开发设置”里需要配置request 合法域名为你的后端域名uploadFile 合法域名如果涉及用户上传图片也要配置downloadFile 合法域名用于加载菜品图片。这几个域名必须是备案过的而且不能带端口号。开发者工具调试阶段可以勾选“不校验合法域名、web-view 业务域名”选项但真机预览时必须关闭。很多新手把接口从http://192.168.1.100改成线上域名后安卓机正常、苹果机请求全部失败大概率就是苹果对 SSL 证书链校验更严格检查证书是否完整。另外如果你是从 Gitee 拉取的源码注意仓库里的配置文件可能带有本地调试地址上线前全局搜索127.0.0.1和localhost并替换。域名的备案信息需要如实填写小程序简介和类目餐饮类目通常需要《食品经营许可证》资质。5. 上线前要验证的四件事并发下单、回调幂等与异常对账扫码点餐系统最怕的不是功能不做而是上线后在高并发或异常场景下静默出错。验证不能停留在“点一单能成功”的程度建议按以下四个维度逐项压测。第一并发下单时的库存准确性。用 ApacheBench 模拟同一桌台同时提交多笔订单命令如下ab -n 100 -c 20 -T application/json \ -p payload.json \ https://yourdomain.com/api/order/submitpayload.json里的 body 固定传同一个table_id和菜品 ID。跑完后查看数据库桌台 100 个请求里只能有 1 个提交成功因为status0的锁和条件更新其余应该被事务回滚。再查菜品库存扣减量必须是成功订单的数量不能出现负数。如果库存变成负数说明扣减没有走条件更新WHERE stock quantity需要回头检查下单接口。第二支付回调的重复投递模拟。微信的沙箱环境不对外开放但你可以用 Postman 或 curl 向 notify_url 手动 POST 一份伪造的 XML 数据连续发送两遍。第二遍的订单状态不应发生变化商家端也不应收到重复通知。如果第二遍订单状态被覆盖成待支付说明回调更新没有带WHERE status0这个条件必须修复后才能上线。curl -X POST https://yourdomain.com/api/pay/notify \ -H Content-Type: text/xml \ -d xmlout_trade_no202501011200001234/out_trade_noresult_codeSUCCESS/result_codetotal_fee1/total_fee/xml第三异常订单的对账机制。生产环境中订阅消息不可控商家可能漏看订单提醒。常见做法是后台提供一个按时间段拉取“已支付未接单”订单列表的接口门店每天开店后先查一次超过 5 分钟未接单的订单自动推送提醒。验证方式是把一笔订单的支付时间改成 10 分钟前看提醒任务能否正确触发。第四菜品图片加载的域名白名单。小程序里image标签加载的图片域名必须配置在downloadFile 合法域名里否则真机上图片裂开。验证方法用微信开发者工具的“真机调试”直接扫码体验如果图片不显示先看控制台的downloadFile:fail url not in domain list报错再检查下载域名配置。500 个downloadFile合法性校验有效期的限制也要留意。最后留一个调试技巧源码里var-dump-server.bat是 PHP 的调试工具可用于本地环境输出变量信息辅助定位问题线上环境切忌开启APP_DEBUGtrue否则错误堆栈会直接暴露在接口响应里餐饮系统一旦被扫出调试信息订单数据面临被恶意查询的风险。本文还有配套的精品资源点击获取