简介:微信外卖小程序模板是一套可直接运行的完整源码,主要面向需要快速搭建外卖业务的小程序开发者,覆盖了网上订餐、购物车管理、订单结算、支付确认及商家处理等核心流程。资源包共27个文件,大小仅148KB,内含json、js、wxss、wxml四种工程文件,分别承担配置、逻辑、样式与页面结构,另有png图片资源和一份md说明文档,整体目录结构清晰,方便按模块查阅。已有123人学习/下载,适合作为小程序开发入门实战或二次开发基础。源码根据真实外卖场景设计,从菜单浏览到购物车金额动态变化,再到支付成功反馈均有完整交互处理;同时保留了工具函数、全局配置等可复用部分,配合README文档中关于各模块功能与使用方法的说明,可帮助开发者少走弯路,快速产出可演示的外卖小程序demo。
1. 微信外卖小程序模板源码的定位与上手路径
开发过外卖类小程序的人都有体会:真正的成本不在 UI,而在订单流转那一整套状态逻辑。商品列表、购物车、下单支付、商家接单,每一环都牵涉数据一致性和支付安全性。这个微信外卖小程序模板源码,正是把这条业务闭环完整落地的一份工程化示例,它不是只给界面的静态模板,而是把用户选菜、加购、结算、支付回调、订单状态管理串起来跑的完整方案。
这套源码适合两类人:一类是刚接触小程序开发、想搞清楚一套真实业务代码如何组织的初级开发者;另一类是要做本地生活、校园外卖、餐饮 SaaS 的从业者,可以直接在此基础上改品牌、接后端、换支付参数,省掉从零搭建的时间。源码包解压后直接用微信开发者工具导入即可编译运行,项目配置、页面结构、工具函数都齐全。下面我按代码的实际调用链,从入口文件开始拆到支付和订单处理。
2. 从目录结构到页面注册:小程序入口与数据流分析
2.1 解压后先看什么:核心文件清单
拿到微信外卖小程序模板_完整源码.zip,解压后第一件事别急着打开开发者工具,先看最外层的README.md、project.config.json和app.json。一个外包项目交付时,README.md通常写的是运行环境要求和二次开发说明;project.config.json记录的是项目名称、AppID、编译设置,如果里面appid字段是测试号,你直接导入工具时会弹出游客模式,不影响跑通本地页面。
app.wxss是全局样式文件,常见的做法是把主题色、通用按钮样式、间距变量集中定义在这里,子页面的 wxss 只写差异部分。这个模板的app.wxss里一般会预设外卖类的小程序最常用的卡片式布局、价格红色高亮、底部操作栏样式,二次开发时改主题色只需动这一个文件。
2.2 app.json 的信息配置逻辑
app.json是全局配置文件,它决定小程序启动时加载哪些页面、 tabBar 如何渲染、窗口用什么样式。它的 pages 数组里第一项就是首页,模板里通常注册了pages/index/index、pages/goods/goods、pages/logs/logs三个页面,分别对应商品浏览、菜品详情/下单、订单记录。下面是模板中常见的配置结构,我基于使用微信开发者工具导入后生成的基础配置做了整理:
{ "pages": [ "pages/index/index", "pages/goods/goods", "pages/logs/logs" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#ff6633", "navigationBarTitleText": "外卖点餐", "navigationBarTextStyle": "white" }, "style": "v2", "sitemapLocation": "sitemap.json" }这段配置里,pages数组控制了页面路由的注册顺序和首屏入口;window里的navigationBarBackgroundColor和navigationBarTitleText直接决定顶部导航的品牌感和标题文案。sitemapLocation指向sitemap.json,它管理小程序的索引收录规则,与页面可见性相关。
2.3 utils 层:请求封装与公共函数
再看utils/util.js。真实项目中这个文件通常承担三类职责:日期格式化、金额单位转换(分为单位转元)、以及wx.request的 Promise 化封装。模板里最常见的封装方式是这样的:
function formatTime(date) { const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() return [year, month, day].map(formatNumber).join('-') } function formatNumber(n) { return n.toString().padStart(2, '0') } function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: url, method: method, data: data, header: { 'Content-Type': 'application/json' }, success: res => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else { reject(res) } }, fail: err => reject(err) }) }) } module.exports = { formatTime, request }这里的formatTime用的是padStart补零,而不是传统的三元判断,代码更紧凑。request方法把wx.request包成 Promise,原因是小程序的回调方式在订单提交流程里嵌套层数深,代码很容易变成回调地狱。实际开发中,header里还会附带Authorization令牌字段,模板里通常预留了这个位置,方便你接入用户登录态。页面里引入这个模块时,用const util = require('../../utils/util.js')即可。数据流方向是:页面文件调用 util 层方法,util 层去请求后端或本地模拟接口,再把结果返回给页面渲染。
3. 购物车增减与总价计算:订单数据模型的设计与实现
外卖小程序的业务核心在购物车,因为用户所有的操作——选菜、改数量、算优惠、提交订单——最终都会汇总到这组数据上。这个模板的购物车逻辑值得细看,它没有把购物车数据散落在各个页面,而是由全局数据或 storage 统一维护,这种方式避免了页面间通信产生的状态混乱。
3.1 菜品数据结构的字段约定
在模板的goods.js或模拟数据里,菜品对象通常会这样定义字段:
const product = { id: 101, name: '黄焖鸡米饭', price: 22.80, originalPrice: 28.00, sales: 356, categoryId: 'rice', image: '/assets/food/chicken.png' }price是实际售卖价,originalPrice用于展示划线价,sales用来做销量排序。分类字段categoryId的作用是在页面左侧分类栏与右侧菜品列表之间建立映射关系。如果你接手后要接真实后端,这些字段名要和数据库表字段对齐,建议把模拟数据模块单独拆成一个mock-data.js,替换成本地 JSON 文件或云开发数据库时不影响页面代码。
3.2 购物车数据状态管理
购物车的核心问题有两个:一是数据存在哪里,二是状态变化时怎么通知视图更新。模板中常见方案是,把购物车对象挂到Page实例的 data 上,每次加减商品后手动setData刷新。下面是一段在商品列表页处理加购和数量变更的关键代码:
data: { cart: {}, // { [productId]: quantity } cartCount: 0, cartTotalPrice: '0.00' }, addToCart(e) { const id = e.currentTarget.dataset.id const cart = { ...this.data.cart } cart[id] = (cart[id] || 0) + 1 const total = this.calcTotal(cart) this.setData({ cart: cart, cartCount: total.count, cartTotalPrice: total.price }) }, updateCart(e) { const id = e.currentTarget.dataset.id const delta = e.currentTarget.dataset.delta // 1 或 -1 const cart = { ...this.data.cart } if (cart[id]) { cart[id] += delta if (cart[id] <= 0) { delete cart[id] } } const total = this.calcTotal(cart) this.setData({ cart: cart, cartCount: total.count, cartTotalPrice: total.price }) }, calcTotal(cart) { let count = 0 let price = 0 Object.keys(cart).forEach(id => { const product = this.productMap[id] const quantity = cart[id] count += quantity price += product.price * quantity }) return { count: count, price: price.toFixed(2) } }这里的cart用对象结构而非数组,好处是通过菜品id直接取数量,查询复杂度从 O(n) 降到 O(1),频繁增减商品时性能更好。setData不能只传局部字段,要把cartCount和cartTotalPrice一并更新,否则底部操作栏的角标和总价不会同步刷新,这是模板里最常被忽略的问题。toFixed(2)是前端展示精度的常规处理,但真实支付场景中,后端必须按数据库中的价格重新核算订单金额,前端的cartTotalPrice只负责展示,不能作为支付扣款依据。
3.3 购物车到订单的转换落库
当用户点击结算时,购物车数据要被序列化传给后端下单。这一步常见做法是,先把购物车cart对象转换成订单明细数组,再提交。明细结构一般长这样:
const orderItems = Object.keys(this.data.cart).map(id => ({ productId: id, quantity: this.data.cart[id], price: this.productMap[id].price, // 用于后端核价参考 subtotal: (this.productMap[id].price * this.data.cart[id]).toFixed(2) }))后端收到这两条数据后,不能直接信任price和subtotal字段,而要按productId查询数据库中的实时价格重新计算,防止前端篡改价格。下面是一张简化的订单明细表结构,源码中如果使用云开发或自建 MySQL,字段设计基本会落在这套思路上:
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_id | varchar(32) | 订单号,后端生成 |
| product_id | int | 菜品 ID |
| product_name | varchar(64) | 下单时菜品名称快照 |
| price | decimal(10,2) | 下单时单价快照 |
| quantity | int | 购买数量 |
| subtotal | decimal(10,2) | 小计金额 |
把product_name、price冗余到订单明细表中而不是联表查询,是为了防止商家后来修改菜品名称或价格导致历史订单显示错乱,这是外卖类系统一个很容易踩的数据一致性问题。
4. 下单支付与商家接单:订单状态机的流转与接口预留
4.1 支付前的登录态与订单创建
外卖小程序的微信支付必须走wx.requestPayment,而这一步的前置条件是用户已经登录并且后端生成了有效的预支付订单。模板中常见的下单流程是这样组织的:检查wx.getStorageSync('token')是否存在,不存在则引导用户调用wx.login换取登录态;存在则直接请求后端创建订单接口,拿到支付参数后再拉起支付。
下面的代码展示了下单方法的骨架,重点在参数组织与支付回调的处理:
submitOrder() { const token = wx.getStorageSync('token') if (!token) { wx.navigateTo({ url: '/pages/login/login' }) return } const orderData = { shopId: this.data.shopId, items: this.buildOrderItems(), remark: this.data.remark, addressId: this.data.addressId } util.request('/api/order/create', 'POST', orderData).then(res => { if (res.code !== 0) { wx.showToast({ title: res.msg, icon: 'none' }) return } const payment = res.data.payment wx.requestPayment({ timeStamp: payment.timeStamp, nonceStr: payment.nonceStr, package: payment.packageValue, signType: 'RSA', paySign: payment.paySign, success: () => { // 支付成功,跳转到订单详情页 wx.redirectTo({ url: '/pages/order-detail/order-detail?id=' + res.data.orderId }) }, fail: (err) => { // 用户主动取消或支付失败,跳回购物车 wx.showToast({ title: '支付未完成', icon: 'none' }) } }) }) }requestPayment里的timeStamp是秒级时间戳字符串,nonceStr是随机字符串,package是prepay_id=xxx格式的字符串,这些参数全部由后端调用微信支付接口后返回,前端只是透传。特别注意package字段不能擅自改名,否则微信支付会直接报错。signType根据商户平台配置选择 MD5、HMAC-SHA256 或 RSA,模板里如果写的是 MD5,你那边的支付证书配置也需要保持一致。
4.2 订单状态机的流转定义
创建订单后,订单会经历多个状态变更,模板在订单列表页logs或商家端页面通常用status整数字段来管理。理解这个字段的取值和流转条件,比理解任何一个页面 UI 都重要,因为所有列表筛选和按钮显隐都由它驱动。下面是模板中常见的一种状态定义方式:
| 状态值 | 状态名 | 触发时机 | 用户端表现 |
|---|---|---|---|
| 0 | 待付款 | 订单创建成功 | 显示“去支付”按钮 |
| 1 | 待接单 | 支付成功回调 | 显示“等待商家接单” |
| 2 | 制作中 | 商家点击开始制作 | 显示“制作中” |
| 3 | 配送中 | 骑手点击取餐 | 显示配送进度 |
| 4 | 已完成 | 用户确认收货 | 可评价店铺 |
| 5 | 已取消 | 支付前取消或超时未付 | 显示“重新下单”入口 |
| 6 | 退款中 | 商家发起退款 | 显示退款进度 |
实际项目中,status变更不能全放在客户端,必须由后端在支付回调、商家操作、配送更新等事件里驱动,客户端只负责展示状态对应的 UI。模板中如果后端是模拟的,通常会准备一份本地订单数组并提供一个模拟接口,演示状态从 0 到 4 的完整跳转。
4.3 商家端订单列表的筛选与处理
商家端的订单处理界面,核心是一个带条件筛选的订单列表。状态筛选的本质是 SQL 的WHERE查询条件,电商后台常见的实现方式是顶部 tab 按status值切换,加上一个搜索框按订单号或手机号模糊查询。模板中给出的商家端代码结构往往长这样:
filterOrders(status) { const all = this.data.allOrders const filtered = status === -1 ? all : all.filter(order => order.status === status) this.setData({ filteredOrders: filtered, currentStatus: status }) }status = -1表示全部订单,这样做隐藏筛选时可避免额外逻辑判断。搜索框的bindinput事件里,用order.orderNo.includes(keyword)做前端过滤,等到接入真实后端时再换成请求参数keyword传给服务端,用 SQL 的LIKE查询完成,前端和接口两层的过滤逻辑可以无缝互换。状态变更按钮在模板里一般会在订单卡片底部,通过wx:if按status判断显示“接单”“开始制作”“完成配送”中的哪一个,方法内部调接口后,把本地order.status置为新值并重新渲染当前列表。
4.4 模板里没有但实战必须补的接口
如果你把这个模板用于生产,后端至少还需要补三个关键接口:支付回调接口/api/pay/notify、订单查询接口/api/order/detail、退款接口/api/order/refund。支付回调必须由后端接收微信服务器发来的 POST 请求,验签后更新订单状态,不能依赖前端跳转来改动状态。退款接口要注意金额不能超过原订单实付金额,并且要记录退款操作人和退款流水号,方便对账。模板中预留的utils/request.js只需要在后端地址字段填上你的域名,其他逻辑基本不用改动。
5. 源码改造进阶:预加载、防重复提交与权限校验
拿到模板源码后,最快见效的三处改造是:商品页数据的预加载、订单提交的防重复与登录态有效性的校验。
先看预加载。很多模板在onLoad里用wx.request拉菜品列表,用户从首页点进菜单页时会有明显白屏等待。常见做法是把onLoad里的数据请求挪到onPreload,或者在上一个页面跳转前先用wx.setStorageSync缓存上一次的菜品数据,页面onLoad时先渲染缓存,再在onShow里静默更新。模板里pages/index/index和pages/goods/goods的数据流是单向的,你只需要在首页跳转函数里提前请求数据并写入传入参数,即可获得到达菜单页时内容已渲染的体验。
再处理重复提交。外卖这种高并发场景,用户连点两次“提交订单”会生成两笔相同订单。模板里不一定有合法的防重逻辑,实战中要在submitOrder入口加一个锁标志:
isSubmitting: false, submitOrder() { if (this.data.isSubmitting) { return } this.setData({ isSubmitting: true }) util.request('/api/order/create', 'POST', orderData) .then(res => this.handlePayment(res)) .finally(() => { setTimeout(() => { this.setData({ isSubmitting: false }) }, 1500) }) }这里的setTimeout延时 1500 毫秒是为了避免网络抖动导致的瞬间状态恢复,给用户一个明确的“正在提交”反馈。如果后端已生成但支付未完成,isSubmitting锁也不会阻止用户从订单列表继续支付,因为在handlePayment里已经记录下了orderId。
最后是登录态校验。wx.login拿到的code每次都会变化,模板里的token过期后接口会返回 401,实战中建议在request方法里统一拦截错误码:
if (res.statusCode === 401) { wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) return Promise.reject(new Error('登录态已失效')) }这条规则放在utils/util.js里,可以保证所有页面共用同一套会话失效处理,而不是等到哪个页面踩了 401 才想起来处理。顺手把token加进request的header中,后续接任何接口都不必每个页面重复添加鉴权头。完成这三处改造后,这套源码的骨架已经具备上线级健壮性,剩余的工作就是切换到真实后端和支付参数。
本文还有配套的精品资源,点击获取