简介:一套完整的微信小程序点餐外卖系统源码,面向希望快速上手小程序开发或搭建同类订餐应用的开发者与学习者。资源将前端小程序界面与后端服务逻辑整合在一起,涉及菜品浏览、下单支付、订单处理、配送跟踪、评价等常见业务场景,既可作为入门学习案例,也能作为二次开发基础。资源包共1147个文件,压缩后约3.91MB。主要文件类型包括wxml/wxss/js等前端页面逻辑,php/html用于后端接口与管理页面,png/gif/jpg提供界面与菜品图片素材,json/config保存配置数据,dat与functions则可能是缓存或功能模块文件。整体目录结构清晰,便于按模块学习和调试。目前已有4964人学习下载,热度较高。通过学习源码可掌握小程序前后端交互、数据库设计、微信支付集成等关键流程;也能参考其代码组织方式,快速改造出符合自身需求的外卖点餐小程序。
1. 微信小程序点餐外卖完整源码:一次解压、三方对接与业务拆解的实战
从标题看,这个 zip 里装的不是“一个能直接上架的小程序”,而是一套“作者自己电脑上能跑的工程快照”。真正落地的点餐外卖小程序,至少要同时具备三样东西:小程序端 UI 工程、后端下单/支付接口、一份能跑起来的微信支付商户配置。所以下载后第一天要做的并不是打开压缩包找代码,而是确认这三样东西分别对应包里的哪个目录、哪份配置文件、哪张数据库表。
初次对接的人最常见的卡点有两个:一是把“完整源码”理解成“解压即用”,导入微信开发者工具后报 AppID 不存在或云环境未创建;二是把前后端硬接,后端地址还停留在 localhost,真机预览直接 request 失败。下面这套路径,从 zip 解压开始,把项目结构、小程序配置、购物车与下单、微信支付 v3 对接、上线排错的流程完整走一遍,重点讲那些作者没写在 README 里的参数。
2. 拆开 zip 看结构:小程序端、后端、数据库与三方配置
拿到微信小程序-点餐外卖小程序完整源码.zip之后,先不要双击解压到桌面,先在命令行看清单。这样能提前判断包里是原生小程序还是 uniapp 产物,以及是否带cloudfunctions目录。zip包本身也是一个文件,解压失败往往不在密码,而在下载完整性。
# 列内容,不要先解压 unzip -l ./微信小程序-点餐外卖小程序完整源码.zip | head -40 # 解压到独立目录,避免和已有项目混在一起 unzip -O gbk -q ./微信小程序-点餐外卖小程序完整源码.zip -d takeout-appunzip -l先列 zip 内的文件清单,可以快速判断包里有没有miniprogram、cloudfunctions、README.md这几类关键目录。-O gbk指定文件名编码,解决 Windows 上压缩的中文目录名在 macOS/Linux 下乱码的问题;如果命令行版本不支持-O,换成图形工具 7-Zip 或 Python 的zipfile处理。-d是解压到目标目录,而不是把 zip 复制过去。
如果解压时报error read zip archive或End-of-central-directory signature not found,说明文件下载不完整,或者文件被网页重命名过,根本不是有效 zip。先用file看真实类型,有些“源码.zip”实际是 rar 或自解压 exe。
2.1 小程序目录与 app.json 路由
解压后典型目录结构如下:
takeout-app/ ├── miniprogram/ │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── pages/ │ │ ├── index/ │ │ ├── menu/ │ │ ├── cart/ │ │ ├── order/ │ │ └── me/ │ ├── components/ │ ├── images/ │ └── utils/ ├── cloudfunctions/ │ ├── login/ │ ├── orderCreate/ │ └── paymentCallback/ ├── project.config.json ├── README.md └── doc/app.json是小程序的“根路由”,点餐外卖的核心页面都在这里声明。注意 tabBar 并不是越多越好,页面一旦注册为 tab,wx.redirectTo就无法跳过去,业务跳转只能用switchTab。看源码时先看tabBar.list里放了哪几个页面,就能知道作者把“购物车”是做成独立 tab 还是页面内抽屉。
{ "pages": [ "pages/index/index", "pages/cart/cart", "pages/user/order/list/list", "pages/me/me" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/user/order/list/list", "text": "订单" }, { "pagePath": "pages/me/me", "text": "我的" } ] }, "window": { "navigationStyle": "custom" }, "style": "v2" }这个配置里购物车没有放在 tabBar,而是在 menu 页通过弹出层完成,减少页面栈切换。navigationStyle改成custom后,自定义顶部导航栏需要通过wx.getMenuButtonBoundingClientRect()拿到胶囊位置,算出高度。网上很多“微信小程序顶部导航栏高度”脚本在这一步写死44px,到刘海屏、灵动岛机型就全乱了。
2.2 点餐外卖的数据模型:先看表,再看代码
完整源码往往带着数据库初始化脚本,但云开发版没有导出集合结构,只能从cloudfunctions里读字段。常见做法是看db.collection('orders').add那段云函数,字段表如下:
| 集合 | 关键字段 | 说明 |
|---|---|---|
| categories | _id, name, sort, status | 菜品分类,前后端用同一排序字段 |
| dishes | _id, categoryId, name, price, stock, status | price 用“分”存储,避免浮点误差 |
| cart | 一般不入库,客户端缓存 | 购物车是会话级状态,放库里反而会超时 |
| orders | _id, userId, orderNo, status, items, totalFee | totalFee 必须服务端计算 |
| addresses | _id, userId, receiver, phone, detail | 外卖需要,堂食点位不需要 |
点餐外卖的价格字段,八成源码用Number保存,用¥19.90这种字符串展示,但计算时会遇到浮点问题。靠谱的做法是定义price * 100整数,渲染时再除 100,便于和微信支付 v3 的“分”对齐。订单号orderNo要生成唯一业务号,不能直接用Date.now(),因为同一毫秒并发时会撞号。可以自己拼接前缀加时间加随机数,或者使用后端分布式 ID。
3. 用微信开发者工具把完整源码跑通:AppID、基准路径与自定义导航
3.1 导入前必须改的三个参数
project.config.json里的appid是最常见的坑。作者留下的touristappid或wx123456无法使用云开发,登录态也会失败。打开项目前先做三件事:去 mp 后台申请一个自己的小程序 AppID;找到utils/config.js或app.js里的wx.cloud.init;检查cloudfunctions每个函数目录下是否有package.json,云函数要单独npm install。
// utils/config.js 示例 const env = 'takeout-1a2b3c'; // 云开发环境 ID,不是 AppID module.exports = { baseUrl: 'https://api.example.com/takeout', // 自建后端时使用 cloudEnv: env, status: { success: 0 } };cloudEnv填的是微信云开发控制台里的“环境 ID”,形如takeout-xxxx,AppID 是wx开头;两个一旦填反,登录云函数会返回env not found。baseUrl这一项在云开发版本里用不到,但源码如果是“自建后端版”,必须改成你自己备案域名下的 HTTPS 地址,真机预览不允许 http。
3.2 小程序的请求封装与登录态
先看登录态。用wx.login换取 code,传给云函数 login;自建后端版则是拿 code 换openid后签发token。两个版本的区别是:云开发版每个云函数通过cloud.getWXContext()直接拿OPENID,不需要传 token;自建后端版需要每次请求 header 里带 Authorization。
// utils/request.js const { baseUrl, status } = require('./config'); function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${baseUrl}${path}`, method, data, header: { Authorization: wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === status.success) { resolve(res.data.data); } else { // 401 时统一重新登录 if (res.data.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/me/me' }); } reject(res.data); } }, fail: reject }); }); }这里把 HTTP 状态码和业务码分开处理,后端即使返回 200,业务逻辑也可能失败。token 过期后用 401 触发重新登录,不要每个页面都写一遍判断。调试模式下,在工具面板勾选“不校验合法域名”能跳过域名限制,但真机预览时这个开关不会被带到手机上,仍然需要合法域名。
3.3 修改刚进入的加载页:顶部导航栏高度与启动跳转
很多完整源码会把启动逻辑放在pages/index/index.js,但“完整”不代表逻辑正确。进入小程序后先显示哪个页面,取决于app.json的pages数组第一项,而不是文件目录顺序。如果想让用户先看到一个居中的 logo 加载页,再决定去登录还是去首页,要单独加一个pages/launch/launch页面,并把它放在pages数组第一位。
// pages/launch/launch.js Page({ async onLoad() { const token = wx.getStorageSync('token'); // 先读取本地缓存,再决定跳转方式 if (token) { wx.switchTab({ url: '/pages/index/index' }); } else { wx.redirectTo({ url: '/pages/me/me' }); } } });注意wx.switchTab只对 tabBar 页面生效,redirectTo不适用于 tab 页面,所以用switchTab走首页、redirectTo走登录页。启动页千万别在onLoad里做耗时网络请求后再跳转,用户会盯着白屏;正确做法是请求放在跳转后的页面里做,加载页只做路由判断。
自定义导航时使用wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置:
const rect = wx.getMenuButtonBoundingClientRect(); const navBarHeight = (rect.top - statusBarHeight) * 2 + rect.height;这里量出来的是一个具体数值,不是所有机型都是 44px。rect.top是胶囊顶边到屏幕顶边的距离,statusBarHeight是状态栏高度,两者差值就是导航栏上下边距。拿到后把它缓存到全局,后续页面直接使用,不要再重复计算。
4. 点餐外卖业务代码怎么改:购物车、下单与订单状态机
4.1 购物车:页面缓存、规格拆分与 UI 刷新
点餐外卖最容易被改坏的代码是 cart。购物车是典型的“跨页面状态”,同时涉及app.globalData和wx.setStorageSync。加购时先查是否已存在同样dishId和规格spec的行,存在就加数量,不存在就新增一行。规格不同不能合并,口味也会影响价格。
// utils/cart.js function addToCart(dish, options) { const globalData = getApp().globalData; const cart = globalData.cart || []; const found = cart.find(item => item.dishId === dish._id && JSON.stringify(item.spec) === JSON.stringify(options.spec) ); if (found) { found.count += 1; } else { cart.push({ dishId: dish._id, title: dish.name, price: dish.price, spec: options.spec, count: 1 }); } globalData.cart = cart; wx.setStorageSync('cart', cart); // 跨页面保持 }found的判断必须比较JSON.stringify(item.spec) === JSON.stringify(options.spec),否则后加的“少冰”会合并到“正常冰”里。setStorageSync写整个 cart 数组,数据量不超过 1MB,不需要做增量同步。注意globalData变化不会自动触发页面渲染,每次加购后要手动setData:
Page({ addDish(e) { // ... 调用 addToCart this.setData({ cartNum: getApp().globalData.cart.length }); } });购物车在 menu 页通常只是显示角标,到 cart 页才逐条列出;角标数量用cart.length而不是 sum(count),用户点了 3 份同一个菜,角标应该是 1 还是 3,产品定义各不同,但代码里要用明确的字段区分。
4.2 下单云函数:金额重算、库存扣减与订单号生成
下单是不能放在客户端的。客户端传来的items只是参考,服务端要根据菜品表最新价格重新计算总金额。订单号和支付单号要唯一,用前缀 + 时间戳 + 随机数。库存扣减最好用事务,简单场景用_.inc(-n),但要防止库存变成负数。
// cloudfunctions/orderCreate/index.js const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const _ = db.command; exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); const { items, address, remark } = event; const orderNo = `TO${Date.now()}${Math.floor(Math.random() * 1000)}`; let totalFee = 0; for (const item of items) { const res = await db.collection('dishes').doc(item.dishId).get(); const dish = res.data; if (!dish || dish.stock < item.count) { throw new Error(`stock-not-enough: ${item.dishId}`); } totalFee += dish.price * item.count; } const order = { orderNo, userId: OPENID, items, totalFee, address, remark, status: 'PENDING_PAYMENT', createdAt: db.serverDate() }; await db.collection('orders').add({ data: order }); for (const item of items) { await db.collection('dishes') .doc(item.dishId) .update({ data: { stock: _.inc(-item.count) } }); } return { orderNo, totalFee }; };_.inc(-item.count)是原子操作,但仍然需要先查一遍库存,否则同时下单会超卖;更严格的场景要使用数据库事务db.startTransaction(),把“查库存、扣库存、创建订单”放在一个事务里。PENDING_PAYMENT表示待支付,支付回调后改状态。这里的totalFee单位是分,返回给前端用(totalFee / 100).toFixed(2)展示。
4.3 订单状态字段与流转规则
用整数状态0/1/2/3的写法在点餐外卖里维护成本极高。推荐直接用语义化字符串枚举,对接支付回调时一眼能看出状态。状态表:
| 状态 | 含义 | 操作方 | 下一状态 |
|---|---|---|---|
PENDING_PAYMENT | 待支付 | 用户支付/15分钟超时 | PAID/CANCELLED |
PAID | 已支付 | 商家接单 | ACCEPTED |
ACCEPTED | 已接单 | 骑手取餐 | DELIVERING |
DELIVERING | 配送中 | 用户确认送达 | COMPLETED |
CANCELLED | 已取消 | 系统/用户 | 终态 |
REFUNDING | 退款中 | 商户后台 | REFUNDED |
状态流转必须只由后端订单状态机驱动,小程序端不能直接把PAID改成DELIVERING,否则“先送达后接单”会出现在真机上。超过 15 分钟未支付要主动关单,调用微信支付关单接口/v3/pay/transactions/out-trade-no/{out_trade_no}/close,同时把数据库订单置为CANCELLED,否则微信会一直保留待支付订单。
5. 微信支付 v3 对接:签名头、回调幂等与退款
5.1 构造 v3 请求签名
很多早期源码还在用 v2 的 MD5 加签方式,现在微信支付新商户基本都要走 v3。v3 的签名头是WECHATPAY2-SHA256-RSA2048,签名内容由请求方法、请求路径、时间戳、随机串和请求体拼接而成。
const crypto = require('crypto'); function wechatSign({ method, urlPath, body, mchId, serialNo, privateKey }) { const timestamp = Math.floor(Date.now() / 1000).toString(); const nonceStr = crypto.randomBytes(16).toString('hex'); const message = `${method}\n${urlPath}\n${timestamp}\n${nonceStr}\n${body}\n`; const signature = crypto .createSign('RSA-SHA256') .update(message) .sign(privateKey, 'base64'); return { authorization: `WECHATPAY2-SHA256-RSA2048 mchid="${mchId}",` + `nonce_str="${nonceStr}",signature="${signature}",` + `timestamp="${timestamp}",serial_no="${serialNo}"`, nonceStr, timestamp }; }urlPath不带 query,body是 JSON 字符串,privateKey是商户 API 私钥文件内容而非证书文件。serial_no是上传 API 证书后在商户平台看到的“证书序列号”,不是证书内容里的serialNumber。这个签名方法只能放在云函数或自建后端,放前端会直接把私钥暴露给用户。
5.2 统一下单与支付回调
调用 JSAPI 下单接口:
curl -X POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi \ -H "Authorization: $AUTH" \ -H "Content-Type: application/json" \ -d '{ "appid": "wx1234567890", "mchid": "1620000000", "description": "外卖订单", "out_trade_no": "TO1720000000000123", "notify_url": "https://api.example.com/wechat/pay/notify", "amount": { "total": 1990, "currency": "CNY" }, "payer": { "openid": "oOpenIdXXX" } }'total单位是分,不是元;out_trade_no要和小程序下单云函数返回的orderNo保持一致。notify_url需要是 HTTPS 公网地址,且路径不能带 query。下单成功后会返回prepay_id,前端再用wx.requestPayment发起支付,支付参数需要后端用同样签名方式生成。
5.3 回调幂等与查单
支付回调可能被微信重试多次,收到回调第一件事不是改订单状态,而是先判断当前订单状态。用out_trade_no查数据库,如果已经是PAID,直接返回成功,否则再把状态从PENDING_PAYMENT改成PAID。同时记录transaction_id和回调原始报文。
// cloudfunctions/paymentCallback/index.js exports.main = async (event) => { const { out_trade_no, transaction_id, trade_state } = event; const order = await db.collection('orders') .where({ orderNo: out_trade_no }).get(); if (order.data.length === 0 || order.data[0].status === 'PAID') { return { code: 'SUCCESS' }; } await db.collection('orders').doc(order.data[0]._id).update({ data: { status: 'PAID', transactionId: transaction_id, paidAt: db.serverDate() } }); return { code: 'SUCCESS' }; };先查重再更新,避免并发回调把状态覆盖。不要用trade_state作为唯一判断依据,后端还要通过查单接口确认金额一致。如果商户平台显示“由于小程序违规,支付功能暂时无法使用”,那不是签名问题,是账号资格问题,需要到 mp 后台处理后再继续。
5.4 退款参数与对账
退款接口路径是/v3/refund/domestic/refunds,请求体里amount对象包含refund和total两个字段。关键参数如下:
| 参数 | 取值 | 注意 |
|---|---|---|
| transaction_id | 微信支付订单号 | 与 out_trade_no 二选一 |
| out_trade_no | 商户订单号 | 优先用这个查库 |
| out_refund_no | 商户退款单号 | 自己生成,前后一致 |
| amount.refund | 退款金额(分) | 不能超过原订单金额 |
| amount.total | 原订单金额(分) | 用于校验 |
| reason | 退款原因 | 会展示给用户,注意措辞 |
退款状态是靠回调通知的,同一个商户退款单号重复提交会返回原单或错误码,开发时要先查单再退款。对账时下载交易账单,把微信支付返回的transaction_id和数据库订单逐笔核对,发现不一致的订单要触发人工处理。
6. 上线前检查与排错:zip 源码包的完整性与真机预览
完整源码从 zip 落地到真机,需要经历导入、编译、预览、上传四步。常见报错和排查顺序是白屏先看 console,接口失败先看域名白名单,云函数报错先看日志。但有一个容易被忽略的点:你拿到的 zip 本身可能不完整。
6.1 用命令行验证 zip 完整性
zip 不是普通文件夹,解压报错很常见。用zip -T测试完整性:
zip -T ./微信小程序-点餐外卖小程序完整源码.zipzip -T会遍历每个压缩条目并重新计算 CRC,如果输出OK但某个文件解压不出来,可以用unzip -t指定文件。常见错误error read zip archive是下载不完整,需要重新下载;invalid zip archive: could not find EOCD是文件被修改过,把 rar 后缀改成 zip 也会出现。先file检查类型再解压。
6.2 伪加密与密码
有一些源码 zip 会在文件头加一个伪加密标志0x09,此时命令行解压会提示输入密码,但用十六进制编辑器把标志改为0x00后可以直接解出。这不是“破解”,只是 zip 压缩包的伪加密开关。遇到真正的密码加密,先看压缩包注释和 README,很多作者会把密码直接写在 release 说明里。“zip 压缩包密码破解工具”消耗时间且容易误报,源码包本身不跑起来没有价值,花两小时破解不如自己搭一套模板。
6.3 验证完整可编译的小技巧
不要直接上传体验版。先解压后执行一次构建,再检查project.config.json里是否设置了miniprogramRoot,以及cloudfunctions下每个函数是否有package.json。打开微信开发者工具后,在详情面板确认基础库版本,点餐外卖这种项目太老的基础库会报wx.requestPayment不存在。换新手机调试时,一定要把开发工具的“下次编译时清缓存”打开,这样可以确定你跑的不是上一份旧代码。
本文还有配套的精品资源,点击获取