简介:这是一套面向餐饮行业商家与小程序开发者的微信小程序点餐外卖源码,覆盖在线点餐、外卖配送、叫号排队、微信支付、用户与订单管理、数据统计分析等完整业务链路,适合希望快速搭建餐饮小程序或学习小程序工程实践的中初级开发者直接复用与二次定制。资源包共521个文件,以137个js业务逻辑脚本、108个wxss样式表、104个json配置、102个wxml页面结构为主,另有44个wxs脚本及少量图片与说明文档,压缩包约833KB,目录结构清晰,便于按模块检索阅读。目前已有2331人学习下载。代码涵盖菜品展示、购物车、订单状态流转、地图定位与配送范围设定、叫号系统与前台点餐打通、支付验证与退款机制等实现细节,可作为开箱即用的项目骨架,也可对照学习WXML、WXSS与JavaScript在小程序中的协作方式,快速理解餐饮类小程序的整体架构与关键模块设计。
1. 餐饮点餐小程序:从扫码到出餐,一套能跑通的完整链路
扫码点餐这件事,真正落到代码上,难点从来不是页面画得多好看,而是「购物车 → 下单 → 订单状态流转 → 后厨出餐」这条链路能不能闭环。我见过太多餐饮点餐商城的 Demo,菜单页做得花里胡哨,一点下单就报错,或者订单状态永远停在「待接单」。这份微信小程序餐饮点餐外卖源码,走的是开箱即用的路子:前端小程序负责点餐交互,后端提供菜品、订单、桌台接口,数据库存菜单和订单流水,三块拼起来就是一个能真实跑通的点餐系统。适合谁?想快速搭一个点餐原型接私活的前端,想拿它当小程序全栈练手的学生,以及需要在此基础上加会员、加营销的二次开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把它拆开讲。
2. 项目定位与技术选型:为什么是这套组合
2.1 它到底解决了餐饮场景里的哪些事
餐饮点餐商城这类项目,核心业务其实就四件事:看菜单、加购物车、下单、看订单状态。听起来简单,但每一件都有坑。看菜单要处理分类切换和规格选择(比如「微辣/中辣/特辣」「大杯/中杯」),加购物车要处理同一菜品不同规格算不算同一行,下单要处理桌号绑定和金额计算,订单状态要处理「待付款 → 已付款 → 制作中 → 已完成」的流转。
这份源码把这几件事都覆盖了。它支持堂食扫码点餐(扫桌台码进入对应桌号)、外卖下单(填地址)、菜品分类浏览、规格选择、购物车增减、订单列表与详情、订单状态展示。后台侧提供菜品管理、分类管理、订单管理的基础接口。也就是说,它不是只有一个点餐页的空壳,而是把「用户端 + 管理端接口 + 数据存储」这条线拉通了。
从适用场景看,它最适合两类人:一类是想拿一个能跑的点餐系统做二次开发,比如加个优惠券、加个会员积分;另一类是学习小程序全栈开发,想找一个业务完整、代码量适中的项目练手。餐饮点餐这个场景的好处是业务边界清晰,不像电商那么复杂,但该有的交互(购物车、订单、状态机)一个不少,拿来练手性价比很高。
2.2 技术栈拆解:小程序原生 + 轻量后端
技术选型上,前端是微信小程序原生开发(WXML + WXSS + JS),没有上 uni-app 或 Taro 这类跨端框架。这个选择对点餐场景是合理的:点餐小程序基本只在微信里跑,跨端需求弱,原生开发能直接用微信的登录、支付、扫码能力,少一层编译,调试也直接。
后端我一般会选 Node.js(Express 或 Koa)或者 Java(Spring Boot)。这份源码走的是轻量路线,接口以 RESTful 风格提供,返回 JSON。数据库用 MySQL 存菜品、分类、订单、用户这几张核心表。为什么不用云开发?云开发确实省事,但二次开发时想迁到自己的服务器会麻烦,用独立后端 + MySQL 的组合,部署自由度高,改起来也顺手。
整体数据流是这样的:小程序端发起请求 → 后端接口处理业务逻辑 → 查/写 MySQL → 返回 JSON → 小程序渲染。订单状态流转由后端控制,前端只负责展示,这样状态机不会乱。
| 层级 | 技术选型 | 职责 |
|---|---|---|
| 前端 | 微信小程序原生 | 点餐交互、购物车、订单展示 |
| 后端 | Node.js / Spring Boot | 菜品、订单、桌台接口 |
| 数据库 | MySQL | 菜单、订单、用户数据存储 |
| 通信 | RESTful + JSON | 前后端数据交换 |
2.3 目录结构:先认清哪块代码管哪件事
拿到源码第一步别急着跑,先把目录结构看一遍,知道改功能该去哪个文件夹。这类小程序项目的目录结构通常长这样:
project/ ├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 首页(菜品分类浏览) │ │ ├── menu/ # 菜单点餐页 │ │ ├── cart/ # 购物车 │ │ ├── order/ # 订单列表与详情 │ │ └── mine/ # 个人中心 │ ├── components/ # 公共组件(菜品卡片、规格弹窗) │ ├── utils/ │ │ ├── request.js # 请求封装 │ │ └── cart.js # 购物车本地逻辑 │ ├── app.js # 全局逻辑 │ └── app.json # 页面路由与窗口配置 ├── server/ # 后端服务 │ ├── routes/ # 接口路由 │ ├── controllers/ # 业务逻辑 │ ├── models/ # 数据库操作 │ └── app.js # 服务入口 └── sql/ └── init.sql # 建表与初始数据前端重点看pages/menu(点餐主逻辑)和utils/cart.js(购物车),后端重点看routes和controllers。sql/init.sql是建表脚本,跑之前先执行它,不然接口查不到数据会一脸懵。
3. 跑起来:环境准备到下单成功的完整步骤
3.1 环境准备与依赖安装
跑这个项目需要三样东西:微信开发者工具、Node.js 环境(如果用 Node 后端)、MySQL 数据库。版本上,微信开发者工具用稳定版即可,Node.js 建议 14 以上,MySQL 5.7 或 8.0 都行。
先把后端依赖装上。进server目录,执行:
cd server npm install这一步会装 Express、mysql2、cors 这些依赖。如果npm install卡住,常见原因是网络问题,可以换淘宝镜像:
npm config set registry https://registry.npmmirror.com装完依赖,别急着启动,先把数据库配好,不然服务起来连不上库会直接报错退出。
3.2 数据库初始化:建表和导入初始菜单
数据库这一步是新手最容易翻车的地方。先登录 MySQL,建一个库:
CREATE DATABASE food_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;字符集一定要用utf8mb4,不然菜品名里的特殊字符或者 emoji 会存进去变问号。建完库,导入建表脚本:
mysql -u root -p food_order < sql/init.sqlinit.sql里通常包含这几张核心表:category(分类)、dish(菜品)、orders(订单)、order_item(订单明细)、user(用户)。导入完可以查一下确认:
USE food_order; SELECT id, name, price FROM dish LIMIT 5;能查到菜品数据,说明初始数据进去了。如果查出来是空的,检查init.sql里有没有 INSERT 语句,有些源码只建表不插数据,需要自己补几条测试菜品。
3.3 后端配置与启动:改对数据库连接
数据库好了,回到后端改配置。找到数据库连接配置(一般在server/config.js或server/app.js里),把账号密码改成自己的:
// server/config.js module.exports = { db: { host: 'localhost', user: 'root', password: '你的数据库密码', // 改成自己的 database: 'food_order', port: 3306 }, port: 3000 // 后端服务端口 };改完启动服务:
node app.js看到控制台打印「server running on port 3000」之类的字样,说明后端起来了。这时候可以在浏览器里访问一个测试接口验证,比如http://localhost:3000/api/dish/list,能返回 JSON 数组就说明接口通了。如果返回 500,多半是数据库连接没配对,回头看password和database有没有写错。
3.4 小程序端配置与真机预览
后端通了,打开微信开发者工具,导入miniprogram目录。导入后第一件事是改请求地址。找到utils/request.js:
// utils/request.js const BASE_URL = 'http://localhost:3000'; // 改成你的后端地址 function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); // 约定 code=0 为成功 } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail: reject }); }); } module.exports = request;这里有个关键点:微信开发者工具里勾选「不校验合法域名」,否则localhost请求会被拦。真机预览时,localhost要换成电脑的局域网 IP(比如192.168.x.x),手机和电脑连同一个 WiFi 才能通。这一步不通,页面就是一片空白,别以为是代码问题。
配置完,点编译,首页应该能加载出菜品分类和列表。点进菜品、选规格、加购物车、下单,走一遍完整流程,订单能在订单页看到,这条链路就算跑通了。
4. 核心模块拆解:购物车、下单、订单状态怎么写的
4.1 购物车逻辑:同菜品不同规格怎么算
购物车是点餐系统里逻辑最密的地方。核心问题是:同一个菜品,选了不同规格(微辣 vs 特辣),算一行还是两行?合格的做法是算两行,因为它们是不同的「商品单元」。判断依据是「菜品 ID + 规格组合」是否一致。
// utils/cart.js // 添加菜品到购物车 function addToCart(cart, dish, spec) { // 用 dishId + 规格拼成唯一 key,区分不同规格 const key = dish.id + '_' + JSON.stringify(spec); const exist = cart.find(item => item.key === key); if (exist) { exist.count += 1; // 已存在则数量 +1 } else { cart.push({ key, dishId: dish.id, name: dish.name, price: dish.price, spec, // 规格对象,如 {辣度:'微辣'} count: 1 }); } return cart; } // 计算购物车总价 function calcTotal(cart) { return cart.reduce((sum, item) => sum + item.price * item.count, 0); }逻辑说明:key是购物车去重的核心,把菜品 ID 和规格序列化后拼起来,保证「同菜同规格」才合并。calcTotal用 reduce 累加,注意价格单位——如果后端存的是「分」,前端展示要除以 100,这个坑后面会讲。参数上,spec是个对象,规格选择弹窗选完后传进来,结构要和后端下单时保持一致,不然下单接口对不上。
4.2 下单接口:金额计算必须在后端做
下单是安全敏感操作,金额绝对不能信前端传的。前端只传「菜品 ID + 规格 + 数量」,后端根据菜品 ID 查数据库里的真实价格,重新算一遍总价。这是血泪经验:前端传的金额可以被篡改,后端不算价就是给羊毛党开门。
// server/controllers/order.js async function createOrder(req, res) { const { userId, tableId, items } = req.body; let total = 0; const orderItems = []; // 遍历前端传来的菜品,用数据库价格重新计算 for (const item of items) { const [dish] = await db.query( 'SELECT id, name, price FROM dish WHERE id = ?', [item.dishId] ); if (!dish) { return res.json({ code: 1, msg: '菜品不存在' }); } total += dish.price * item.count; // 用数据库价格算,不信前端 orderItems.push({ dishId: dish.id, name: dish.name, price: dish.price, spec: item.spec, count: item.count }); } // 写入订单主表和明细表 const orderId = await db.query( 'INSERT INTO orders (user_id, table_id, total, status) VALUES (?, ?, ?, ?)', [userId, tableId, total, 'pending'] ); for (const oi of orderItems) { await db.query( 'INSERT INTO order_item (order_id, dish_id, spec, count, price) VALUES (?, ?, ?, ?, ?)', [orderId, oi.dishId, JSON.stringify(oi.spec), oi.count, oi.price] ); } res.json({ code: 0, data: { orderId, total } }); }逻辑说明:先循环查菜品真实价格累加总价,再写订单主表和明细表。status初始为pending(待付款)。参数上,tableId是堂食桌号,外卖场景可以传 null 或地址 ID。注意订单主表和明细表要一起写,只写主表的话订单详情页查不到菜品。
4.3 订单状态流转:一个状态机管住全流程
订单状态是点餐系统的「黑匣子」,状态乱了整个系统就乱了。合格的做法是定义清楚状态和允许的流转方向,后端做校验,不允许跳状态。
// server/utils/orderStatus.js // 定义状态和允许的下一步 const STATUS_FLOW = { pending: ['paid', 'cancelled'], // 待付款 → 已付款 / 已取消 paid: ['cooking'], // 已付款 → 制作中 cooking: ['completed'], // 制作中 → 已完成 completed: [], // 已完成是终态 cancelled: [] // 已取消是终态 }; function canTransfer(from, to) { return STATUS_FLOW[from] && STATUS_FLOW[from].includes(to); } // 更新订单状态时校验 async function updateStatus(orderId, to) { const [order] = await db.query('SELECT status FROM orders WHERE id = ?', [orderId]); if (!canTransfer(order.status, to)) { throw new Error(`不允许从 ${order.status} 转到 ${to}`); } await db.query('UPDATE orders SET status = ? WHERE id = ?', [to, orderId]); }逻辑说明:STATUS_FLOW用对象描述状态机,canTransfer判断流转是否合法。这样即使前端传了非法状态,后端也会拦下来。参数上,状态值建议用英文常量,前端展示时再映射成中文,避免中文字符串在数据库和接口里传来传去出编码问题。
5. 避坑与排查:跑不起来时先看这几条
5.1 请求全部失败,页面空白
现象:小程序编译后首页一片空白,控制台报「request:fail」或「不在以下 request 合法域名列表中」。
原因:微信开发者工具默认校验合法域名,localhost和局域网 IP 都不在白名单里。
解决:开发者工具右上角「详情 → 本地设置」,勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。真机预览时,把BASE_URL换成电脑局域网 IP,并确认手机和电脑同一 WiFi。
5.2 接口返回 500,后端日志报数据库连接错误
现象:后端启动正常,但一调接口就 500,日志里出现ER_ACCESS_DENIED_ERROR或ECONNREFUSED。
原因:数据库账号密码配错,或者 MySQL 服务没启动,或者库名写错。
解决:先用命令行mysql -u root -p手动登录验证账号密码;确认 MySQL 服务在跑(systemctl status mysql或服务面板查看);核对config.js里的database字段和实际库名一致。
5.3 金额显示成 100 倍或 0.01 倍
现象:菜品价格显示成 1800 而不是 18,或者显示成 0.18。
原因:数据库存的价格单位是「分」,前端直接展示了;或者反过来,数据库存「元」,前端又除了 100。
解决:统一单位。常见做法是数据库存「分」(整数,避免浮点误差),接口返回「分」,前端展示时除以 100 并toFixed(2)。下单计算时全程用「分」,最后展示再转。这个坑不统一,后面加优惠券、算折扣会连环出错。
5.4 购物车加了菜,切页面回来就没了
现象:在菜单页加了购物车,切到订单页再切回来,购物车空了。
原因:购物车数据只存在页面data里,页面卸载就丢了。
解决:购物车要持久化。简单做法是存wx.setStorageSync('cart', cart),页面onLoad时读回来。更规范的做法是把购物车提到全局(app.globalData.cart)或者用状态管理。注意存本地只是体验优化,下单时仍以后端算价为准。
5.5 订单状态改了但列表不刷新
现象:后端把订单状态改成「已完成」,但小程序订单列表还是显示「制作中」。
原因:列表页数据是onLoad时拉的,切回来没重新请求。
解决:在订单列表页的onShow里重新拉数据,而不是只在onLoad拉。onShow每次页面显示都会触发,能保证状态最新。如果列表长,可以只刷新当前可见的那几条,避免整页闪烁。
6. 二次开发:加会员、加营销、接支付的改造思路
跑通之后,真正有价值的是往上加东西。这份源码的骨架清晰,加功能不算难,但有几个改造点值得先说清楚。
加会员体系,核心是加一张member表,存用户 ID、等级、积分、余额。下单成功后给积分,积分规则写在订单完成的回调里。注意积分变动要和订单状态流转绑在一起,别在「待付款」就加积分,不然用户取消订单积分就白送了。我一般会在updateStatus转到completed时触发积分累加,这样逻辑最干净。
加优惠券,要加coupon(券模板)和user_coupon(用户领的券)两张表。下单时前端选券,后端校验券是否有效、是否满足门槛,然后在算价环节扣减。这里的关键是:券的校验和扣减必须在后端同一个事务里做,不然并发下单可能一张券用两次。金额计算顺序建议是「菜品总价 → 减券 → 加配送费 → 最终价」,顺序错了对不上账。
接微信支付,这是从 Demo 走向真实可用的关键一步。流程是:后端调微信统一下单接口拿到prepay_id,返回给前端,前端调wx.requestPayment拉起支付,支付成功后微信回调后端,后端把订单状态从pending改成paid。注意回调接口要做签名验证,并且要幂等——微信可能重复回调,同一订单别重复改状态。
改造时验证方法很简单:每加一个功能,把「下单 → 支付 → 状态流转 → 数据落库」这条链路完整走一遍,看数据库里订单、明细、积分、券的状态是否一致。别只测页面,页面会骗人,数据库不会。
说个我自己的教训。早些年改一个点餐项目,加了个「满减」功能,前端算完价传给后端,后端直接信了。上线第二天就有人把前端金额改成 0.01 下单,一晚上薅了几十单。从那以后我每次做金额相关的改造,都强制走一遍「后端重新算价 + 数据库对账」的流程,前端传的金额只当参考,绝不当依据。希望这份源码你拿去改的时候,也把这个习惯带上,能少踩不少坑。
本文还有配套的精品资源,点击获取