简介:这是一套面向餐饮行业开发者与小程序学习者的微信小程序点餐外卖源码,基于微信开发者工具与 WXML、WXSS、JavaScript 技术栈构建,覆盖在线点餐、外卖配送、叫号排队、微信支付、用户与订单管理、数据统计分析等完整业务模块,适合希望快速搭建餐饮小程序或研究小程序工程实践的初中级开发者直接复用与二次定制。资源包共 521 个文件,以 137 个 js 业务逻辑脚本、108 个 wxss 样式表、104 个 json 配置、102 个 wxml 页面结构及 44 个 wxs 脚本为主,另含少量图片与说明文档,整体约 833KB,目录结构清晰,便于按模块检索阅读。目前已有 2331 人学习下载。通过研读源码,读者可掌握菜品展示、购物车、订单状态流转、支付验证与退款、地图定位配送等实现思路,并以此为基础按自身需求进行定制化开发。
1. 餐饮点餐小程序:从扫码到出餐,一套能跑通的完整链路
扫码点餐这件事,用户端体验就三步:扫码、选菜、下单。但落到代码上,背后是菜单渲染、规格选择、购物车合并、订单状态流转、后厨出餐一整套链路。这份微信小程序餐饮点餐外卖源码,把这条链路完整跑通了——不是那种只画了页面的空壳 Demo,而是带后台管理、带订单状态机、带规格 SKU 计算的可用工程。
适合谁?想快速搭一套点餐系统的独立开发者、需要给中小餐饮店做定制的外包团队、以及想学小程序完整项目架构的学生。你拿到手改改菜品数据、换个配色就能上线试跑,二次开发的入口也留得很清楚。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆一遍。
2. 项目架构与技术选型:前后端怎么分工
2.1 整体分层:小程序端 + 服务端 + 管理后台
这套源码的结构是典型的三段式。小程序端负责用户交互,服务端提供 API 和订单处理,管理后台给商家用来看订单、改菜品、管库存。三者通过 HTTP 接口通信,数据格式统一用 JSON。
小程序端用的是原生微信小程序框架(WXML + WXSS + JS),没有上 uni-app 或 Taro 这类跨端方案。好处是包体积小、启动快、调用微信原生能力(比如微信支付、获取手机号)没有中间层损耗。服务端常见做法是 Node.js + Express 或 Koa,数据库用 MySQL,缓存层用 Redis 存购物车和会话。管理后台一般是 Vue + Element UI 或者 React + Ant Design 的单页应用。
为什么这么选?餐饮点餐场景对实时性要求不算极端,但对稳定性要求高——饭点高峰期不能崩。原生小程序 + 轻量 Node 服务端的组合,部署简单,一台 2 核 4G 的云服务器就能扛住中小型餐厅的并发。如果你要上多门店连锁,那服务端得加一层网关做路由分发,这是后话。
2.2 目录结构:拿到源码先看这几个文件夹
解压之后,根目录一般长这样:
project-root/ ├── miniprogram/ # 小程序端源码 │ ├── pages/ # 页面目录 │ │ ├── index/ # 首页(菜单展示) │ │ ├── cart/ # 购物车 │ │ ├── order/ # 订单确认与支付 │ │ ├── orderList/ # 订单列表 │ │ └── mine/ # 个人中心 │ ├── components/ # 公共组件 │ │ ├── dish-card/ # 菜品卡片 │ │ ├── spec-popup/ # 规格选择弹窗 │ │ └── cart-bar/ # 底部购物车栏 │ ├── utils/ # 工具函数 │ │ ├── request.js # 网络请求封装 │ │ └── cart.js # 购物车逻辑 │ ├── app.js # 小程序入口 │ ├── app.json # 全局配置 │ └── app.wxss # 全局样式 ├── server/ # 服务端源码 │ ├── routes/ # 路由定义 │ ├── controllers/ # 业务逻辑 │ ├── models/ # 数据模型 │ ├── middlewares/ # 中间件(鉴权、日志) │ └── app.js # 服务入口 ├── admin/ # 管理后台 │ ├── src/ │ │ ├── views/ # 页面 │ │ └── api/ # 接口封装 │ └── package.json └── README.md # 部署说明拿到源码第一件事不是急着跑,而是把miniprogram/pages/和server/routes/这两个目录翻一遍。页面目录决定了用户能干什么,路由目录决定了后端能响应什么。两边对上了,说明项目结构是完整的。
2.3 技术栈清单与版本注意事项
| 层级 | 技术选型 | 常见版本 | 备注 |
|---|---|---|---|
| 小程序端 | 微信原生框架 | 基础库 2.20+ | 需在开发者工具中调最低版本 |
| 服务端 | Node.js + Express | Node 16+ | 建议用 LTS 版本 |
| 数据库 | MySQL | 5.7 / 8.0 | 8.0 注意密码加密方式 |
| 缓存 | Redis | 6.x | 存购物车和 session |
| 管理后台 | Vue 2 + Element UI | Vue 2.6 | 部分老项目用 Vue 2 |
| 支付 | 微信支付 V2/V3 | — | 需商户号配置 |
提示:如果服务端用的是 MySQL 8.0,连接时可能报
ER_NOT_SUPPORTED_AUTH_MODE错误,原因是 8.0 默认用caching_sha2_password加密方式,而老版本 mysql 驱动不支持。解决办法是改用户密码加密方式为mysql_native_password,或者升级 mysql 驱动包。
3. 跑起来:环境搭建与首次启动
3.1 小程序端导入与配置修改
打开微信开发者工具,选择「导入项目」,目录指向miniprogram/文件夹。导入后第一件事是改app.js里的全局配置:
// app.js App({ globalData: { baseUrl: 'http://localhost:3000/api', // 改成你的服务端地址 userInfo: null, cartList: [] // 全局购物车数据 }, onLaunch() { // 检查本地是否有 token,有则自动登录 const token = wx.getStorageSync('token'); if (token) { this.globalData.token = token; } } })baseUrl是唯一必须改的地方。本地调试时填http://localhost:3000/api,但注意微信开发者工具需要勾选「不校验合法域名」才能请求 localhost。上线前必须换成备案过的 HTTPS 域名,并在小程序后台配置 request 合法域名。
cartList放在 globalData 里是为了让购物车数据在多个页面之间共享——菜单页加菜、购物车页改数量、订单页结算,都读同一份数据。这是小程序开发里很常见的做法,比用状态管理库轻量。
3.2 服务端启动:数据库建表和依赖安装
服务端启动分三步。第一步装依赖:
cd server npm install第二步建数据库。源码里一般会带一个init.sql文件,直接导入:
mysql -u root -p your_database_name < init.sql这个 SQL 文件会创建几张核心表:dishes(菜品)、categories(分类)、orders(订单)、order_items(订单明细)、users(用户)。如果源码没带 SQL 文件,你需要根据models/目录下的模型定义手动建表。
第三步改数据库配置。找到server/config/db.js或类似文件:
// server/config/db.js module.exports = { host: 'localhost', port: 3306, user: 'root', password: 'your_password', // 改成你的数据库密码 database: 'restaurant', // 改成你建的库名 connectionLimit: 10 // 连接池大小,按并发量调 }connectionLimit这个参数值得说一下。默认 10 在开发环境够用,但饭点高峰期如果同时有几十个用户下单,连接池会被打满,后面的请求排队等连接,表现为接口响应变慢甚至超时。中小餐厅调到 20 到 30 比较稳妥,但也要看数据库服务器的承载能力,不是越大越好。
然后启动:
npm run dev # 或者 node app.js看到Server running on port 3000就说明服务端起来了。
3.3 管理后台启动与联调验证
管理后台通常是独立的前端项目:
cd admin npm install npm run serve启动后访问http://localhost:8080,用默认管理员账号登录(常见是 admin / 123456,具体看 README)。登录后先做一件事:添加几个菜品分类和菜品数据。因为小程序端的菜单是从服务端拉的,数据库里没数据,小程序首页就是空的。
联调验证的顺序是:管理后台加菜品 → 小程序端刷新看菜单 → 加购物车 → 下单 → 管理后台看订单。这条链路走通了,说明前后端对接没问题。
注意:微信支付在开发阶段没法真实调起,源码里一般会做一个「模拟支付」的开关。在
server/controllers/order.js里找到支付回调部分,把mockPay设为true,下单后直接标记为已支付,跳过微信支付流程。上线前记得改回来。
4. 核心模块拆解:购物车、规格 SKU 与订单状态机
4.1 购物车逻辑:本地存储与服务端同步的取舍
购物车是点餐系统里最容易出 bug 的地方。这份源码的做法是:未登录时购物车存在本地 Storage,登录后同步到服务端 Redis。先看本地购物车的核心逻辑:
// utils/cart.js const CART_KEY = 'cart_list'; // 添加菜品到购物车 function addToCart(dish, spec, count = 1) { let cart = wx.getStorageSync(CART_KEY) || []; // 用 dishId + specId 作为唯一标识,同菜品不同规格算两条 const key = `${dish.id}_${spec ? spec.id : 'default'}`; const existIndex = cart.findIndex(item => item.key === key); if (existIndex > -1) { // 已存在则累加数量 cart[existIndex].count += count; } else { cart.push({ key, dishId: dish.id, name: dish.name, price: spec ? spec.price : dish.price, specName: spec ? spec.name : '', count, image: dish.image }); } wx.setStorageSync(CART_KEY, cart); return cart; } // 计算购物车总价 function calcTotal(cart) { return cart.reduce((sum, item) => sum + item.price * item.count, 0); }关键点在key的生成规则:dishId + specId。同一道菜如果选了不同规格(比如大份/小份),必须算两条记录,不能合并。我见过有人只用dishId做 key,结果用户选了大份又选小份,数量直接叠加,价格算错,这就是血泪教训。
calcTotal用reduce累加,注意价格单位。如果数据库存的是「分」,前端展示要除以 100;如果存的是「元」,浮点数累加会有精度问题(比如 0.1 + 0.2 = 0.30000000000000004)。常见做法是全程用「分」做整数运算,展示时再转「元」。
4.2 规格 SKU 计算:多规格组合的价格与库存处理
餐饮菜品和电商商品不一样,规格通常只有一两个维度(份量、辣度、加料),不像衣服有颜色+尺码+版型那么多组合。但逻辑是一样的:每个规格组合对应一个独立的价格和库存。
// 规格选择弹窗中的价格计算 Page({ data: { dish: null, specs: [], // 规格列表 selectedSpec: null, // 当前选中的规格 finalPrice: 0 }, onLoad(options) { const dishId = options.id; this.fetchDishDetail(dishId); }, fetchDishDetail(dishId) { wx.request({ url: `${app.globalData.baseUrl}/dishes/${dishId}`, success: (res) => { const { dish, specs } = res.data; this.setData({ dish, specs, finalPrice: dish.price, // 默认显示基础价 selectedSpec: null }); } }); }, // 用户点击某个规格 onSelectSpec(e) { const spec = e.currentTarget.dataset.spec; this.setData({ selectedSpec: spec, finalPrice: spec.price // 切换规格时更新价格 }); } });这里有个细节:finalPrice初始值是dish.price,也就是基础价。如果菜品有必选规格,基础价应该设为最低规格价或者干脆不显示,等用户选完规格再显示最终价。否则用户看到 9.9 元点进去,选完规格变成 15.9 元,会有被套路的感觉。
库存扣减的时机也要注意。常见做法是下单时扣库存,而不是加入购物车时扣。因为购物车里的东西用户可能不结算,如果加购就扣库存,会出现「库存被占着但没人买」的情况。下单时扣库存也要加锁,防止超卖——用 Redis 的DECR原子操作或者数据库的UPDATE ... WHERE stock >= count都能实现。
4.3 订单状态机:从待支付到已完成的流转
订单状态是整个系统里最需要严谨对待的部分。这份源码的订单状态定义如下:
| 状态码 | 状态名 | 可执行操作 | 触发条件 |
|---|---|---|---|
| 0 | 待支付 | 支付、取消 | 用户下单 |
| 1 | 已支付/待接单 | 接单、拒单 | 支付成功回调 |
| 2 | 已接单/制作中 | 出餐 | 商家接单 |
| 3 | 已出餐/待取餐 | 完成 | 后厨出餐 |
| 4 | 已完成 | — | 用户取餐 |
| -1 | 已取消 | — | 用户取消或商家拒单 |
状态流转的核心代码在服务端:
// server/controllers/order.js const ORDER_STATUS = { PENDING_PAY: 0, PAID: 1, ACCEPTED: 2, READY: 3, COMPLETED: 4, CANCELLED: -1 }; // 状态流转白名单:定义每个状态能转到哪些状态 const STATUS_TRANSITIONS = { [ORDER_STATUS.PENDING_PAY]: [ORDER_STATUS.PAID, ORDER_STATUS.CANCELLED], [ORDER_STATUS.PAID]: [ORDER_STATUS.ACCEPTED, ORDER_STATUS.CANCELLED], [ORDER_STATUS.ACCEPTED]: [ORDER_STATUS.READY], [ORDER_STATUS.READY]: [ORDER_STATUS.COMPLETED], [ORDER_STATUS.COMPLETED]: [], [ORDER_STATUS.CANCELLED]: [] }; async function updateOrderStatus(orderId, newStatus) { const order = await OrderModel.findById(orderId); if (!order) throw new Error('订单不存在'); const allowed = STATUS_TRANSITIONS[order.status] || []; if (!allowed.includes(newStatus)) { throw new Error(`订单状态不允许从 ${order.status} 变更为 ${newStatus}`); } await OrderModel.updateStatus(orderId, newStatus); return { success: true }; }STATUS_TRANSITIONS这个白名单是防止状态乱跳的关键。没有它,可能出现「已完成的订单又被取消」或者「待支付的订单直接变成已出餐」这种玄学问题。每次状态变更前先查白名单,不合法就抛错,日志里记下来,排查问题时一目了然。
5. 避坑与排查:上线前必须处理的几个问题
5.1 微信支付回调收不到
现象:用户支付成功,但订单状态还是「待支付」,管理后台看不到新订单。
原因通常有三个:一是回调地址没配好,微信服务器 POST 不到你的接口;二是回调接口没有做签名验证,被微信判定为不安全;三是服务端用了 HTTPS 但证书链不完整,微信服务器握手失败。
解决:先在微信支付商户平台检查回调地址是否填对,必须是公网可访问的 HTTPS 地址。然后在服务端加日志,把微信回调的原始请求体打印出来,确认是否收到请求。如果收到了但验签失败,检查 API 密钥是否配置正确。证书问题用openssl s_client -connect yourdomain.com:443检查证书链是否完整。
5.2 购物车数据在页面切换后丢失
现象:用户在菜单页加了菜,切到个人中心再切回来,购物车空了。
原因:购物车数据存在页面的data里,页面卸载后数据就没了。小程序页面切换时,非 tabBar 页面会被销毁。
解决:把购物车数据存在globalData或本地 Storage 里,页面onShow时重新读取。这份源码用的是 Storage + globalData 双写:写的时候两边都写,读的时候优先读 globalData,没有再从 Storage 读。这样既保证页面切换不丢数据,又保证小程序被杀掉后重新打开数据还在。
5.3 菜品图片加载慢导致页面卡顿
现象:菜单页滚动时卡顿,图片半天加载不出来。
原因:菜品图片没有压缩,原图可能好几 MB;或者图片存在本地服务器,带宽不够。
解决:上传菜品图片时做压缩,宽度控制在 750px 以内,格式用 WebP。图片存储建议用对象存储(比如云厂商的 OSS/COS),不要直接放应用服务器上。小程序端用lazy-load属性开启图片懒加载,只加载可视区域内的图片。
5.4 订单号重复或并发下单出错
现象:两个用户同时下单,订单号一样,或者库存扣成负数。
原因:订单号生成用了时间戳,同一毫秒内多个请求会重复;库存扣减没有加锁,并发时超卖。
解决:订单号用「时间戳 + 用户ID后四位 + 随机数」的组合,或者用数据库自增ID加前缀。库存扣减用UPDATE dishes SET stock = stock - ? WHERE id = ? AND stock >= ?,靠数据库的行锁保证原子性。返回影响行数为 0 就说明库存不足,直接回滚。
5.5 开发者工具能跑,真机预览白屏
现象:微信开发者工具里一切正常,扫码真机预览打开是白屏。
原因:最常见的是baseUrl用了localhost,真机访问不到你电脑的本地服务;其次是用了开发者工具的「不校验合法域名」选项,真机没有这个选项。
解决:真机调试时,把baseUrl改成电脑的局域网 IP(比如http://192.168.1.100:3000/api),确保手机和电脑在同一个 WiFi 下。上线前必须换成备案的 HTTPS 域名。另外检查小程序后台的「开发设置」里,request 合法域名有没有配好。
6. 二次开发:换数据、加功能、改界面的实操思路
拿到源码后,最省力的改造路径是「先换数据,再加功能,最后改界面」。换数据最简单——管理后台加菜品、改分类、调价格,不用动代码。加功能就要看你的需求落在哪一层了。
比如要加一个「预约取餐」功能。小程序端需要在订单确认页加一个时间选择器,服务端需要在订单表加一个pickup_time字段,管理后台的订单列表要显示预约时间。改动涉及三个端,但每个端的改动都不大。具体做法:先在orders表加字段ALTER TABLE orders ADD COLUMN pickup_time DATETIME,然后在server/controllers/order.js的创建订单逻辑里接收并存储这个字段,小程序端用<picker mode="time">组件选时间,管理后台在表格里加一列。
再比如要改配色。小程序的全局样式在app.wxss里,主题色一般定义在page或view的 CSS 变量里。找到--primary-color这个变量,改一个值全站生效。但注意有些组件可能硬编码了颜色,需要全局搜索替换。
我一般改别人源码的习惯是:先跑通,再在本地建一个 git 分支,每改一个功能提交一次。这样改崩了随时能回滚,不会出现「改到一半忘了原来什么样」的情况。另外,改服务端代码之前先把数据库备份一份,尤其是改表结构的时候——我有次加字段忘了加默认值,导致老数据查询报错,排查了半天。
还有一个技巧:如果源码的某个模块你暂时用不上(比如外卖配送),不要删掉,用配置开关关掉就行。在app.json里把对应的页面注释掉,服务端路由里把配送相关的接口挂起。这样以后要启用的时候,改回来就行,不用重新写。
从那以后我每次拿到新源码,都强制走一遍「跑通 → 备份 → 小步改 → 验证」的流程,再急也不跳步。希望帮到你。
本文还有配套的精品资源,点击获取