☰
餐饮点餐外卖小程序源码开箱即用实战:跑通支付闭环与避坑指南
2026/10/15 6:51:19 网站建设 项目流程

简介:一份微信小程序餐饮点餐外卖源代码包,专为餐饮行业开发者与小程序学习者设计,提供开箱即用的完整解决方案,覆盖在线点餐、外卖配送、叫号排队及微信支付等核心业务场景。包内共521个文件,以js、wxml、wxss、json等典型小程序文件为主,另含若干wxs与png资源,分别承担业务逻辑、页面结构、样式配置与数据定义等职责,整体压缩包仅833KB,便于快速下载与部署。项目代码结构清晰,从用户登录、菜品展示、购物车到订单管理与数据统计均有对应实现,可直接作为二次开发基底,也可用于学习微信开发者工具、WXML/WXSS及小程序支付对接等实践技能。目前已有2331人学习浏览,适合具备基础前端知识、希望快速搭建餐饮小程序原型或研究完整商城流程的开发者参考。

1. 餐饮点餐外卖小程序真能开箱即用吗:先证明它不是一个空壳

拿到一套“微信小程序-餐饮点餐外卖-源代码 开箱即用”,很多人第一反应是导入开发者工具,然后期待它直接跑起来。但真实情况往往是:十套源码里七八套要先折腾依赖、数据库、接口地址和没写进文档的路径问题。“开箱即用”这四个字,准确含义是结构完整、缺的东西少,而不是零配置。

一套真正能用的点餐外卖源码,至少应该包含三个部分:小程序前端、服务端接口、数据库初始化脚本,再加一份能说清前后端怎么对接的说明。适合用它的人,是拿毕业设计、帮餐饮店最快上线、接小单做二次开发的从业者。这篇笔记从一套模拟项目X的实战改造出发,讲怎么判断源码能不能用、怎么以最快速度跑通点餐、外卖、结算和支付回调,以及最容易被坑到的五个位置。先说结论:判断一套源码值不值得用,永远先看数据流通不通,再看界面好不好看。

2. 看懂一套点餐小程序源码:目录拆解、技术栈与最小运行单元

拿到源码先别急着点编译。先花半小时把目录结构看明白,能避免后面几个小时的无头苍蝇式排查。一套餐饮点餐外卖小程序源码,通常由小程序端、服务端和数据库三部分组成,但技术栈不同,跑通方式差异很大。这一章先解决一个核心问题:这套源码到底是什么形态的,它的最小运行单元是什么。

2.1 技术栈判断:原生小程序、uni-app、还是云开发

先判断技术栈,因为不同技术栈的启动方式和踩坑点完全不同。我拿到源码会先看根目录和后缀名,一分钟内就能确认形态。

形态关键特征启动成本适合场景
原生小程序大量 .wxml / .wxss / .js 文件最低,直接导入开发者工具开箱即用诉求最匹配
uni-app主要是 .vue 文件,根目录有 package.json需要 npm install 和 cli 编译同一套代码要同时出 H5、App
云开发没有独立服务端目录,有 cloudfunctions无需自己的服务器,但学云开发规则不想买服务器、怕运维的人

原生小程序是最好的“开箱即用”形态,因为不依赖 Node 环境和编译链路,导入工具就能预览。uni-app 次之,但 npm 依赖版本很容易翻车,经常出现 install 成功但编译失败的情况。云开发则要特别注意:它没有传统后端目录,数据库权限和登录态都走云能力,如果源码里混用了普通 request 和云函数,排查起来非常头疼。

判断方法很简单:看根目录有没有 app.json,有就是原生;看有没有大量 .vue 文件,有就是 uni-app;看有没有 cloudfunctions 目录,有就是云开发。这一步判断错了,后面所有步骤都白搭。

2.2 前后端目录结构:小程序端、服务端、数据库各负责什么

确认技术栈后,下一步是对照目录结构建立全局认知。一套常见餐饮点餐外卖源码的目录长这样:

├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 首页,展示店铺和推荐菜 │ │ ├── menu/ # 点餐页,左侧分类右侧菜品 │ │ ├── cart/ # 购物车页 │ │ ├── order/ # 订单列表 │ │ ├── checkout/ # 结算页 │ │ └── address/ # 外卖地址管理 │ ├── components/ # 公用的加购按钮、菜品卡片 │ ├── utils/ # request.js、格式化工具 │ ├── app.js │ └── app.json ├── server/ # 后端服务 │ ├── controllers/ # 接口逻辑 │ ├── models/ # 数据模型 │ └── routes/ # 路由定义 ├── sql/ # 数据库初始化脚本 │ └── init.sql └── README.md

如果拿到手的源码没有对应的 server 目录和 sql 脚本,那它多半只是“半套”源码,所谓开箱即用就不成立。前后端各自管什么要理清:前端 pages 管交互,utils/request.js 管接口请求;服务端管菜品、订单、支付这些业务规则;sql 目录负责把数据库表结构一次性建好。三层之间靠接口串联,任何一个环节断掉,点餐流程就卡住。

这套目录是标准结构,实际源码可能有变体,比如前端叫 client、服务端叫 api,但责任边界基本一致。带着“每一层该干什么”的预期去看目录,比从头开始读代码高效得多。

2.3 从 project.config.json 到 app.json:两个入口文件的正确读法

开箱即用翻车最多的位置,就是入口配置文件。这是小程序启动时第一个读取的文件,字段不对直接编译失败。

{ "appid": "touristappid", "compileType": "miniprogram", "libVersion": "latest", "setting": { "urlCheck": false, "es6": true, "minified": true }, "projectname": "canteen-order" }

字段含义要读懂:appid 是租户标识,touristappid 表示游客模式,能用但真机预览和调用登录接口会受限;libVersion 是基础库版本,设成 latest 省事,但老代码可能依赖旧 API,版本升了反而报错;urlCheck 必须了解,开发阶段设 false 才能请求 http 本地接口,发布时这句会失效,后面专门讲。compileType 固定是 miniprogram,写成别的直接不认。

另一个入口是 app.json,它管页面注册和底部导航。一个最常见的开箱失败原因:页面文件存在,但 app.json 的 pages 列表漏注册或路径写错,编译直接报 page not found。代码里添加新页面时容易漏掉这一步。检查方法就是逐个路径对比文件系统,少一个都不行。看懂这两个文件,等于拿到了这套点餐小程序的门钥匙。

3. 本地跑通点餐与外卖的完整链路:从导入到下单到支付回调

结构看完,开始真正跑链路。跑通的目标不是看到菜单页,而是能从点餐一路走到支付回调。这个闭环走通了,这套源码才真正算“开箱即用”。我按最小必要步骤操作,不引入多余的扩展。

3.1 环境准备:工具版本、基础库、本地调试与真机预览的区别

本地跑通最少需要四样东西:小程序开发者工具、Node.js、MySQL 数据库、一个能跑的服务端。开发者工具用稳定版即可,别追 beta,beta 的基础库兼容性会引入玄学问题。Node.js 用 18 或 20 LTS 版本,老源码在更高版本上偶尔会有加密库兼容问题。

启动顺序有讲究:先启动数据库,再启动服务端,最后打开小程序工具。反着来会出现接口全部超时,让人误以为是源码写错了。验证数据库起来了,用一行命令看端口:

netstat -ano | grep 3306

看到 3306 在监听说明 MySQL 正常。接着启动服务端,看它连接的数据库名和账号密码和本地是否一致,不一致去改服务端配置,不要动数据库。最后在小程序工具里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,这是本地开发调试 http 接口的必需开关,不勾则所有请求被拦。

一个必须提前知道的区别:模拟器里能通,真机预览大概率不通。原因有两点:一是真机强制校验域名,二是 localhost 在手机上指手机自己,不是你的电脑。真机预览要把接口地址改成电脑的局域网 IP,比如 192.168.x.x。这一步属于开箱即用最常见的“掉链子”位置,后文避坑章节还会细说。

3.2 配好第一套请求封装:域名与 token

接口请求封装是前后端对接的核心枢纽。餐饮点餐外卖的每个页面操作,本质都是对这个封装的调用。常见的封装长这样:

const BASE_URL = 'http://127.0.0.1:8080'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success(res) { if (res.statusCode === 200) { resolve(res.data); } else if (res.statusCode === 401) { wx.removeStorageSync('token'); // token 失效,跳转登录页 wx.navigateTo({ url: '/pages/login/login' }); } else { reject(res); } }, fail: reject }); }); } module.exports = { request, BASE_URL };

这段封装干了三件事:拼接口地址、带 token、统一处理错误。BASE_URL 是唯一需要改的地方,本地调试用 127.0.0.1,真机预览改局域网 IP,上线改 https 域名。Authorization 从本地缓存读 token,所有需要登录态的接口自动带上。401 统一清 token 并跳登录,比每个页面单独判断省心得多。

这里的参数坑在于:Content-Type 用 application/json 时,GET 请求不要传对象给一些老后端,否则会变成 query string 导致解析差异。token 的存取要配套,登录成功后 wx.setStorageSync('token', res.data.token),登出时清除。很多源码把 token 写死在 header 里,那是假登录态,换用户就出问题。

3.3 跑通一单:从提交购物车到订单到支付回调

封装备好后,跑通一单是验证整条链路的关键动作。我一般不会先测支付,先测“创建订单”接口,因为这个接口会把菜品、规格、价格、地址全都串起来。

async function submitOrder() { const cart = getCartFromStore(); if (!cart.length) return; const orderId = await request('/api/order/create', 'POST', { shopId: store.shopId, items: cart.map(i => ({ dishId: i.dishId, specId: i.specId, count: i.count })), deliveryType: store.deliveryType, // 1 到店自取,2 外卖 address: store.address, remark: store.remark, clientToken: generateUUID() // 幂等键,防止重复提交 }); const payParams = await request('/api/pay/unified', 'POST', { orderId }); wx.requestPayment({ ...payParams, success: () => request('/api/order/confirm', 'POST', { orderId }), fail: err => toast('支付取消') }); }

这个调用链的顺序不能乱:先创建订单拿到 orderId,再请求支付参数,调起收银台,最后支付成功回调确认订单。重点看三处。第一处是 clientToken,这是幂等键,防止用户双击按钮产生两笔相同订单,服务端必须查重。第二处是金额:items 里的价格是给用户展示用的,最终金额必须由服务端按菜品表和规格表重算,前端传什么价都不可信。第三处是支付参数:payParams 里包含 timeStamp、nonceStr、package 等字段,这些必须由服务端从支付平台换取,不能在前端拼。

支付回调和下单接口不同,它像黑匣子:你在小程序里看不到回调怎么进来。服务端要提供一个异步通知接口,支付平台付完款后主动请求这个地址,把订单状态从待支付改成已支付。没有回调或回调验签失败,订单会一直卡在待支付。排查支付问题,先看服务端有没有收到通知,再看验签结果,而不是反复在前端重置数据。这一步走通,整条点餐外卖链路才真正闭环。

4. 点餐逻辑与外卖计价:前端交互、后端字段与数据库表设计

链路能走通之后,要理解这套源码里面的业务逻辑是怎么组织起来的。点餐不是简单的列表展示,外卖也不是只有“下单”一个动作。这一章把程序逻辑拆开,让你知道改哪些东西能改变行为,而不是对着代码瞎试。

4.1 点餐页面交互:分类切换、菜品规格、加购交互怎么实现

点餐页是所有交互的集中地。一个标准点餐页有三块:左侧分类栏、右侧菜品列表、底部购物车栏。核心难点在于购物车状态要跨页面共享,菜单页加了菜,购物车角标要变,结算页要能读到同样的数据。

const store = { cart: [], selectedSpecs: {}, addToCart(dish, spec, count) { const key = dish.id + '_' + spec.id; const existed = this.cart.find(item => item.key === key); if (existed) { existed.count += count; } else { this.cart.push({ key, dishId: dish.id, specId: spec.id, dishName: dish.name, specName: spec.name, price: spec.price, count }); } }, getTotalCount() { return this.cart.reduce((sum, item) => sum + item.count, 0); }, getTotalPrice() { return this.cart.reduce((sum, item) => sum + item.price * item.count, 0); } };

这个 store 模块要理解三个关键设计。key 是“菜品加规格”的组合,同一道菜有大份小份就是两条不同的购物车明细,不能只按 dishId 合并。getTotalPrice 在前端只做展示,不参与下单金额计算,下单时服务端会重算一遍。count 变化要在页面通过 setData 触发更新,很多源码的购物车角标不刷新,问题就出在这里——修改了 store 数据但没有同步到页面。

规格选择是另一个高频交互点。点“加购”先弹规格弹窗,选完规格才真正加入购物车。无规格菜品则直接加购。这个分支逻辑要写在弹窗组件里,不要在菜品卡片里判断,否则后期加规格会牵扯一堆页面改动。

4.2 配送费与满减:这部分逻辑放在服务端

配送费和满减规则,新手容易写进前端,我强烈反对。原因很直接:前端算的钱只是“预估价”,用户只要改本地代码就能改配送费。正式结算必须以服务端计算结果为准。

function calcDeliveryFee(distanceKm, orderAmount) { let fee = 0; // 满 30 元免配送费 if (orderAmount >= 30) { return 0; } // 基础配送费 2.5 元,超出 3 公里每公里加 1 元 fee = 2.5; if (distanceKm > 3) { fee += (distanceKm - 3) * 1; } return Math.round(fee * 100) / 100; }

这个函数体现三个规则设计逻辑。满减阈值和配送费单价都是可配置项,建议不要写死在函数里,而是放到数据库的 shop 表或 fee_rule 表。distanceKm 是从用户地址到门店的实际距离,由服务端根据坐标计算,前端只传地址ID,不传距离。返回结果保留两位小数,涉及金额计算时不要用浮点数直接比较,用分做单位或转成整数再运算。

后端下单选完价后,要把 totalAmount、deliveryFee、items 快照存进订单表。这个“快照”设计很关键:菜品价格以后涨价了,历史订单显示的还是下单时价格,不会因为菜品表变更而数据错乱。

4.3 数据库表设计:一份可落地的点餐外卖库结构

源码的数据库结构基本决定扩展上限。我见过把购物车存服务端的,见过订单明细把菜品名字直接拼字符串的,短期能跑,加需求就崩。一份可落地的点餐外卖库,核心表就这几张:

表名关键字段说明
userid, openid, nickname, phoneopenid 是用户唯一标识
shopid, name, address, business_hours多门店扩展前提
categoryid, shop_id, name, sort菜品分类
dishid, category_id, name, image, sales菜品主表
dish_specid, dish_id, spec_name, price规格表,一份大份一份小份就是两行
ordersid, user_id, shop_id, total_amount, delivery_fee, status订单主表
order_itemid, order_id, dish_id, spec_id, price, count订单明细快照
delivery_fee_ruleid, min_amount, fee, distance_limit配送费规则

建表时要注意几个细节。orders 表要加索引,按用户查订单和按店铺查订单是两种最常见查询。order_item 里的 price 是快照价,和 dish_spec 的当前价解耦。status 字段用整型枚举,不要直接用字符串,枚举可读性好且排序方便。

CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, shop_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, delivery_fee DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3配送中 4已完成', pay_order_no VARCHAR(64), create_time DATETIME NOT NULL, INDEX idx_user (user_id), INDEX idx_status (status) );

这张表建好后,订单的状态流转靠 UPDATE 语句,不是靠 INSERT 新记录。pay_order_no 存支付平台的交易号,对账时要靠它回溯。create_time 用 DATETIME,不要用 TIMESTAMP,业务查询和报表统计都更直观。表结构定了,点餐和外卖的底层逻辑就都稳住了。

5. 开箱即用五大坑:现象、原因与排查脚本

这一章直接给避坑指南。每一段都是真实踩过的,按“现象、原因、解决”的格式写,方便你对照排查。

5.1 编译不过:页面路径与根目录配置不匹配

现象:导入源码后点编译,直接报“page not found”或“app.json: not found”,有时报错指向一个根本不存在的路径。很多人的第一反应是工具坏了,重装一遍开发者工具,结果问题依旧。

原因:app.json 的 pages 列表和实际文件目录对不上。要么是源码作者加了新页面忘记在 pages 里注册,要么是大小写问题——小程序路径严格区分大小写,Menu 和 menu 是两个完全不同的路径。

解决:写一个快速脚本,扫描 pages 目录下的所有 .wxml 文件,再和 app.json 的 pages 字段比对,找出缺失项。

const fs = require('fs'); const path = require('path'); const appJson = JSON.parse(fs.readFileSync('./app.json', 'utf8')); function scanPages(dir) { const result = []; fs.readdirSync(dir, { withFileTypes: true }).forEach(entry => { if (entry.isDirectory()) { result.push(...scanPages(path.join(dir, entry.name))); } else if (entry.name.endsWith('.wxml')) { result.push(path.join(dir, entry.name).replace(/\.wxml$/, '').replace(/\\/g, '/')); } }); return result; } const actualPages = scanPages('./pages'); const missing = appJson.pages.filter(p => !actualPages.includes(p)); console.log('缺失或未注册的页面:', missing);

这个脚本解决的是“找不到页面”这种最基础的问题,路径对比是字符串精确匹配,跑一遍补上缺失项就能编译过。改完再编译,如果还报别的错,继续看控制台,不要重启工具浪费时间。

5.2 真机请求全部失败:域名校验与本地调试

现象:模拟器里点餐加购下单都正常,一放到真机预览就全部白屏或请求超时,控制台报“request:fail url not in domain list”。

原因:平台的小程序对真机请求有严格限制,只能用配置过的合法域名,且必须是 HTTPS。开发者工具里的“不校验合法域名”只在开发环境有效,真机不认。

解决:开发阶段,把 BASE_URL 从 127.0.0.1 改成电脑的局域网 IP,并且手机和电脑连同一个 WiFi。如果仍然不通,先关掉手机流量,确保 WiFi 环境是通的。正式阶段,必须有一个备案过的 HTTPS 域名,并把接口域名填到小程序后台的 request 合法域名列表里。没有这个配置,代码做得再好也上不了线。这一步是开箱即用到上线之间最硬的一道门槛,没有任何捷径。

5.3 接口正常但页面空白:字段大小写与渲染时机

现象:network 面板里请求返回 200,数据也有,但页面一片空白,或某个菜品图片加载不出来。

原因:前端渲染死循环或数据没到位就 setData。常见的有两种:一是后端返回的字段叫 DishName,前端模板写的是 dishName,大小写不匹配;二是页面 onLoad 里请求数据,渲染层已经执行完,数据回来没有再次 setData。

解决:先看 console 的报错,以报错为准。没有报错就逐字段比对接口返回和模板渲染的字段名。另一个高效做法是模拟器里切换到 AppData 面板,展开页面的 data 字段,看数据到底到没到前端。如果数据到了还空白,那就是模板里字段名的问题。很多源码在迁移数据库时会把字段名改掉,但前端没同步,这种问题在“开箱即用”源码里高频出现,值得第一个排查。

5.4 服务端连接不上:MySQL 版本认证插件问题

现象:服务端启动失败,日志报 Communications link failure,或“Unable to load authentication plugin 'caching_sha2_password'”。

原因:MySQL 8 默认的认证插件是 caching_sha2_password,但很多老服务端代码用的连接驱动只认 mysql_native_password。这是版本认知差,不是源码的 bug。

解决:两种方式选一种。推荐改服务端连接配置,让驱动使用支持新插件的版本;快速解决则是把数据库用户的认证插件改回去:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

要注意这条 SQL 只适合本地开发环境。生产环境用这种方式会有安全隐患,正确做法是升级服务端的 MySQL 连接驱动或调整连接参数。改完插件后要重启服务端,不是只刷一下配置就生效。

5.5 支付没回调:回调地址与签名验证

现象:收银台能正常唤起,钱也付了,但订单状态一直停在“待支付”。服务端日志里看不到任何支付回调记录。

原因:回调地址不可达或验签失败。支付平台的回调是服务端到服务端的 HTTP 请求,必须是一个公网能访问的地址。本地开发环境用内网穿透工具暴露出来的临时域名,经常因为域名变化或平台风控导致回调失败。验签失败则是回调参数被篡改或签名算法实现不对。

解决:按顺序排查三步。第一步,看服务端有没有收到回调请求,没收到就是地址不可达,换稳定的穿透方案或直接部署到测试服务器。第二步,收到但验签报错,用支付平台提供的验签工具对报文重新签名,对比本地签名结果。第三步,验签通过但订单没更新,检查回调报文里的 out_trade_no 和本地订单号是否一致,以及回调处理函数有没有在响应里正确返回成功标识。支付回调有自己一套状态码约定,返回的内容不对,支付平台会认为回调失败并反复重试,这也会造成数据错乱。

6. 让源码从“能跑”到“敢上线”:验证清单、性能与二次开发

开箱即用解决了“能跑”的问题,但“敢上线”是另一回事。上线的意思是数据和钱都是真的,用户点错了没有后悔药。我有一套固定的收尾流程,做完才放心发布。

功能验证清单要逐项过:重复点击下单按钮会不会产生两笔订单;断网状态下提交订单会不会卡死;用户篡改前端价格后下单,服务端能不能拦截;外卖地址跨城市时配送费是否准确;商家端收到新订单的通知是否及时。这些问题不用全覆盖,但前三个必须测,因为它们直接关系资金安全。

验证项测试方式预期结果
重复下单快速双击提交按钮服务端只生成一笔订单
金额篡改改请求里的 price服务端按数据库价格重算
支付回调模拟回调通知状态正确流转
断网重试飞行模式关 WiFi不产生错误订单

性能优化有两个低成本高收益的点。第一是图片全部走 CDN,点餐页的菜品图是大头,本地图带宽扛不住。第二是启用小程序分包,把 checkout、order 这类低频页面放到分包里,减少首包体积,点餐页加载速度能明显提升。这两个做完,体验就超过大部分同类源码。

二次开发方向看业务诉求:多门店支持要在所有业务表加 shop_id 并按此过滤;扫码点餐是给菜单页加一个 shopId 参数;优惠券则要在订单创建和配送费计算之间插入抵扣环节。这些改动都有共同规律——先改数据库,再改服务端接口,最后动前端。顺序反了,就会出现前端传了新字段、后端不认的尴尬局面。

我最早接触一套点餐源码时,上来就改页面样式和颜色,两天后发现订单数据根本对不上,最后只能把前后端字段重新对了一遍才找回状态。那次教训后,我给自己定了条规矩:先走通支付回调闭环,再做任何界面定制。这条规矩帮我避开了无数次返工。

希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询