微信小程序外卖源码二次开发:购物车、订单状态与支付回调拆解
2026/9/14 1:47:22 网站建设 项目流程

简介:云贝餐饮外卖O2O v1.6.9 开源小程序.zip是一款面向微信小程序开发者和餐饮商家的源码模板,基于微信小程序框架构建,覆盖菜单展示、在线下单、支付、订单管理、配送跟踪等核心外卖闭环,适合快速搭建或二次定制餐饮外卖应用。压缩包约65.84MB,文件总数与类型明细暂时缺失,不过源码类模板通常包含页面WXML/WXSS、逻辑JS、项目配置及说明文档,可导入微信开发者工具查看。目前已有1240人学习下载,适用于熟悉或希望进阶微信小程序开发的个人与团队。源码开放原始代码,支持根据业务需求调整界面、增加功能、优化性能;对商家而言,也可借助模板快速上线一套符合行业习惯的外卖小程序,降低从零开发的成本。持续关注v1.6.9版本的迭代更新与技术社区,能帮助使用者保持应用竞争力。

1. 一份 v1.6.9 外卖小程序源码,怎么拆才不算浪费

拿到云贝餐饮外卖 O2O v1.6.9 这套开源微信小程序模板时,大多数人第一反应是直接导入微信开发者工具,看到首页出来就以为完事了。实际跑一遍你会发现,这套源码的价值不在那个能浏览的菜单页,而在你改第一行代码之前对它的理解:订单状态机怎么流转、购物车数据怎么持久化、支付回调怎么和本地订单对齐,这三个问题不搞清楚,后续每一次二次开发都会在联调时返工。适合读这篇文章的人,是手里已经有小程序基础、想拿一套完整业务源码做改造的开发者,或者是餐饮商家侧的技术负责人,想评估这套模板能不能支撑真实门店的外卖订单。本文会从目录结构讲起,逐步落到下单链路、接口层封装、版本升级排查,最后给一个实际切换线上接口的改造技巧。

2. 源码目录与服务层结构:先定位业务入口,再谈改代码

2.1 微信小程序原生工程的标准骨架

解压 zip 之后,第一件事不是看页面,而是看根目录下的app.json。它是整个小程序的注册中心,pages 数组里第一个元素就是启动页。云贝这套模板的 pages 顺序通常是pages/index/index优先,也就是首页作为冷启动入口。这里有一个容易忽略的细节:tabBar配置决定了底部导航是否生效,如果tabBar.list里的pagePath和 pages 数组里的路径不一致,编译期不会报错,但点击底部 tab 会白屏。收到这种二手模板,第一步应当做一致性校验。

{ "pages": [ "pages/index/index", "pages/order/order", "pages/cart/cart", "pages/user/user" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/order/order", "text": "订单" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/user/user", "text": "我的" } ] } }

这段配置的核心逻辑是让四个主页面平级注册,tab 切换时不会重新触发 onLoad,而是走 onShow。对 O2O 外卖场景来说,购物车页必须保持内存状态,否则用户选了几个菜切到首页再切回来,购物车被清空,体验直接崩掉。参数说明:pagePath必须写相对路径,不带.js后缀;text是 tab 下显示的文字,长度建议控制在 4 个汉字以内;iconPathselectedIconPath是可选字段,如果不配,tab 上就只有文字。

2.2 utils 与 api 目录:接口层是所有二次开发的起点

云贝这个版本的utils/request.js封装了 wx.request 的 Promise 化处理,api/目录下按业务域拆分了接口函数。看源码时优先读这个文件,因为它决定了你后续接真实后端时改动范围有多大。如果模板里所有请求都直接调用wx.request,那说明接口层没有收敛,接真实接口时得全局搜索替换,工作量大得多。

// utils/request.js 核心片段 const request = (url, method = 'GET', data = {}, header = {}) => { return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method, data, header: Object.assign({ 'content-type': 'application/json' }, header), success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else if (res.statusCode === 401) { wx.navigateTo({ url: '/pages/login/login' }); } else { reject(res); } }, fail: (err) => reject(err) }); }); };

这段代码的要点在于把状态码判断收敛到一处:2xx 直接 resolve,401 统一踢到登录页,其余错误抛给调用方。实际业务里需要再补一层业务码判断,因为不少后端接口即使 HTTP 200,返回体里code也可能是非零错误码。我一般会在 resolve 之前加一个if (res.data.code !== 0)的分支,统一 Toast 错误信息。参数说明:baseUrlconfig.js里维护,切换环境只改这个文件;header的默认 content-type 对 GET 请求也生效,如果后端不接收 JSON 格式的 GET,可以按请求类型动态调整。

2.3 模板页面的组件拆分逻辑

components/目录,云贝把商品卡片、数量步进器、订单卡片做成了自定义组件。这种拆分的好处是首页、搜索页、分类页可以复用同一套商品展示逻辑。每个组件由.js.json.wxml.wxss四个文件组成,组件的properties定义了外部传入的参数。改组件时特别注意:properties里的字段名如果和组件内部 data 里的字段名冲突,会出现属性覆盖后显示异常的问题,排查时可以先看组件 wxml 里绑定的字段到底是来自 properties 还是 data。

// components/goods-card/index.js Component({ properties: { goods: { type: Object, value: {} }, showStepper: { type: Boolean, value: true } }, methods: { handleAddToCart() { this.triggerEvent('addtocart', { goods: this.properties.goods }); } } });

这里的核心事件机制是triggerEvent,子组件不直接操作全局购物车数据,而是向上抛事件交给页面来处理。这样设计的好处是组件可以在不同页面复用而不污染数据流;坏处是如果页面的 bind 事件没写,点击加号按钮会毫无反应,而且不报错。排查这类问题时看页面 wxml 里有没有bind:addtocart="onAddToCart"这样的绑定。

3. 购物车与下单链路:状态管理的关键路径

3.1 购物车为什么不能直接存在组件里

拿到这个模板你会发现购物车数据没有用全局状态库,而是在pages/cart/cart.js里用getApp().globalData.cartList存储。这是一个很务实的做法:外卖场景购物车字段少、拼单复杂度低,引入 MobX 或 Redux 反而增加理解成本。globalData 的生命周期和小程序实例一致,冷启动后首次访问是空数组,需要在app.js的 onLaunch 里从 storage 恢复。

// app.js 片段 onLaunch() { const cart = wx.getStorageSync('cartList'); this.globalData.cartList = cart || []; }, addToCart(goods) { const list = this.globalData.cartList; const idx = list.findIndex(item => item.id === goods.id); if (idx > -1) { list[idx].count += 1; } else { list.push(Object.assign({ count: 1 }, goods)); } this.globalData.cartList = list; wx.setStorageSync('cartList', list); }

这个实现的关键在于同 ID 商品合并数量,而不是重复插入。实际改造时还需要考虑规格维度:同一道菜选「微辣」和「中辣」应当视为不同购物车项,判断条件不能只看 goods id,要把 sku 标识一起拼接。参数说明:wx.setStorageSync每次调用都会全量写入,购物车列表大时会有性能损耗,但外卖场景几十个条目完全没问题;findIndex是 ES6 方法,基础库版本高于 2.0 都没问题。

3.2 下单页的数据组装与校验

下单页pages/confirm/confirm.js在整个链路里承担的是「把购物车数据变成订单数据」的角色。它干的事有三件:从 globalData 读取购物车,分门店分组(如果支持多门店);计算总价并校验起送价;提交订单到后端或本地模拟接口。很多模板在本地演示模式下没有真实后端,提交按钮走的是 wx.cloud 或 setTimeout 模拟成功,这在实际联调时是第一个坑。

// 下单前的校验逻辑 const canSubmit = (cartList, shopInfo) => { if (!cartList.length) return { ok: false, msg: '购物车为空' }; const total = cartList.reduce((sum, item) => sum + item.price * item.count, 0); if (total < shopInfo.minPrice) { return { ok: false, msg: `未达起送价 ¥${shopInfo.minPrice}` }; } return { ok: true, total }; };

这段代码覆盖了两种最常见的下单失败原因:空购物车和未达起送价。真实场景还需要补配送费计算、满减活动判断、优惠券抵扣三个模块,建议顺着total的累加逻辑打断点看每步数值是否符合预期。参数说明:reduce的初始值必须传0,不传的话数组为空时会报错;shopInfo.minPrice如果来自接口返回的字符串类型,要先Number()转换再做比较。

3.3 模拟支付与真实支付的回调差异

模板里的支付多半是一个wx.requestPayment调用,参数从后端下单接口返回。但本地演示时没有服务端签名,常见做法是先走一个mockPay()直接把订单置为已支付。这里有个隐患:模拟支付跳过了金额校验,如果后续要切真实支付,必须把下单接口、支付参数获取、支付回调三段逻辑全部替换。建议先读api/order.js里 submitOrder 函数,看它的返回结构是否包含timeStampnonceStrpackagesignTypepaySign这五个字段,缺任何一个wx.requestPayment都会直接 fail。

wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: 'RSA', paySign: res.data.paySign, success: (payRes) => { // 支付成功,跳转订单详情 wx.redirectTo({ url: `/pages/order-detail/order-detail?id=${orderId}` }); }, fail: (err) => { // 用户取消支付,保留订单为待支付状态 this.setData({ paying: false }); } });

关键逻辑在 fail 回调:用户主动取消和支付异常都走这里,但业务含义不同。取消支付应该允许用户回到订单页继续付款,而支付异常需要弹错误码。判断方式是看err.errMsg是否包含requestPayment:fail cancel,包含则是用户主动取消。参数说明:package的写法是prepay_id=xxx,不能只传xxxsignType必须和商户平台配置一致,用MD5还是RSA以后端签名为准,建议让后端同学在接口文档里标注。

4. 订单管理与配送跟踪:状态机是业务的核心

4.1 订单列表的类型切换实现

订单页通常有「全部 / 待付款 / 待配送 / 已完成」四个 tab,云贝模板用currentType控制列表筛选。实现方式有两种:前端本地过滤全部订单数据,或者每次切换 tab 重新请求接口。本地过滤适合订单量少的场景,接口分页查询则适合真实部署。看代码时注意请求参数里的status对应关系:0 待付款、1 待接单、2 配送中、3 已完成,不同版本可能用字符串,连调时要和后端确认枚举值表。

// 订单列表请求示例 fetchOrders(status) { request(`/api/order/list`, 'GET', { status }).then((res) => { this.setData({ orderList: res.data.list }); }).catch(() => { wx.showToast({ title: '订单加载失败', icon: 'none' }); }); }

这里的请求函数没有写 loading 状态,实际使用中要在请求前wx.showLoading、请求后wx.hideLoading,否则弱网环境下用户反复点击 tab 会顺序错乱。参数说明:status不传或传空字符串时后端应返回全部订单;wx.showToasticon只有successerrorloadingnone四个值,想要自定义图标需要image字段。

4.2 配送跟踪如何用 map 组件承接

模板中配送页通常是 web-view 内嵌 H5 地图,或者 map 组件加 marker 标记。map 组件是小程序内置组件里少数不推荐频繁 setData 的,因为经纬度数据高频更新会导致渲染卡顿。云贝的做法是定时器每 10 秒拉一次骑手位置,用wx.createMapContexttranslateMarker做平滑移动。

const mapCtx = wx.createMapContext('map', this); setInterval(() => { request('/api/order/rider-location', 'GET', { orderId }).then((res) => { mapCtx.translateMarker({ markerId: 1, destination: { latitude: res.data.latitude, longitude: res.data.longitude }, duration: 500, animationEnd: () => {} }); }); }, 10000);

这段代码的要点是translateMarker替代直接改 marker 的经纬度,动画过渡比硬跳体验好得多。实际项目中注意两点:定时器要在页面 onUnload 里 clearInterval,否则页面销毁后还在请求接口;每 10 秒一次请求可以用wx.stopLocationUpdate配合后台推送降低耗电,但地图精度要求高的场景还是轮询稳妥。参数说明:duration单位是毫秒,值设太短会看起来像瞬移,建议和轮询间隔保持 1/20 左右的比例;markerId是地图上 marker 的唯一标识,多个骑手时每个骑手对应不同 id。

4.3 订单状态被跳过时怎么排查

外卖业务里最怕的状态问题是「已支付但商家没收到」,也就是订单状态没有从「待接单」流转到「已接单」。用这套模板自测时,可以在支付成功回调里加一行日志输出订单 id,然后去接单端手动确认。如果后端接口正常但前端页面不刷新,多半是onShow里没有重新拉订单详情,页面从后台切回时仍显示旧状态。

onShow() { const orderId = this.options.id; if (orderId) { this.fetchOrderDetail(orderId); } }

onShow 和 onLoad 的执行时机差异是这类问题的根源:onLoad 只在页面创建时执行一次,onShow 每次页面从后台恢复都会触发。支付成功后跳转订单详情页,新页面 onLoad 会执行;但如果用户切到微信聊天再切回来,只有 onShow 触发,不加这个逻辑就看不到状态更新。参数说明:this.options拿到的参数类型是字符串,直接拼进请求 URL 没问题,但用来比较数字类型时要先转换。

5. v1.6.9 升级差异与兼容性排查:换版本前必须做的事

5.1 从旧版本升级的差异对比

如果你之前用过 v1.5 或 v1.6 的云贝模板,v1.6.9 主要的改动集中在三个方面:订单模块从本地 mock 数据改成接口驱动、WXS 过滤器替代了部分 JS 端的价格格式化、以及组件库公共样式的抽离。直接覆盖源码文件会有残留文件问题,旧版本有而新版本删除了的页面会在app.json里报找不到路径,编译直接失败。正确做法是保留project.config.json的 appid 配置,其余全部用新版文件覆盖,再用微信开发者工具的「代码质量」面板跑一遍静态检查。

# 在项目根目录执行,检查无引用文件 grep -r "pages/order/order" app.json

这条命令的作用是确认 app.json 里的页面路径在磁盘上真实存在。实战中覆盖升级后最常见的报错是module "utils/util.js" is not defined,原因是新版把工具函数拆到了utils/index.js,老页面还在 import 旧路径。处理方式是用全局搜索替换,把utils/util批量改成utils/index,然后逐个页面跑编译。

5.2 基础库版本与 API 兼容矩阵

云贝 v1.6.9 用到的部分 API 有基础库最低版本要求,比如wx.requestPayment的最低版本是 1.2.0,wx.createMapContext是 1.0.0,而wx.getUserProfile要求 2.10.4 以上。如果线上用户的基础库版本偏低,这些 API 调用会直接失败。排查手段是在app.json里配置"requiredBackgroundModes"和 lazyCodeLoading,同时掌握wx.canIUse来做 API 存在性检测。

if (wx.canIUse('getUserProfile')) { wx.getUserProfile({ desc: '用于完善会员资料', success: (res) => {}, fail: (err) => {} }); } else { // 降级方案:直接弹窗让用户填写昵称 wx.showModal({ title: '提示', content: '请手动输入昵称' }); }

这段代码解决的是不同系统版本微信对授权接口支持不一致的问题。老版本微信里wx.getUserProfile不存在,不判断直接调用会报TypeError: wx.getUserProfile is not a function。参数说明:wx.canIUse的参数格式是API名.参数.返回值,也可以只写 API 名做粗粒度判断;降级方案里弹窗收集用户昵称的方式虽然体验差,但至少保证功能可用。

5.3 微信开发者工具中常见的编译告警处理

拿到源码导入工具时,常见的 warning 有两类:一类是Some selectors are not allowed in component wxss,这是因为组件 wxss 里写了标签选择器或 ID 选择器,小程序组件样式隔离默认不允许;另一类是property is not supported,对应的是 WXSS 里写了不兼容的 CSS 属性。处理方法并不复杂,把标签选择器全部改成 class 选择器,把不支持的属性用标准属性替代。

/* 错误写法 */ view { margin: 10rpx; } /* 正确写法 */ .goods-item { margin: 10rpx; }

样式选择器的隔离规则是组件化开发必须遵守的约束,标签选择器会穿透组件边界,导致页面其他部分被意外影响。调试时可以用工具右上角的样式隔离开关临时关闭隔离来看效果,但发布前必须改回 class 写法。参数说明:组件 json 文件里"styleIsolation": "apply-shared"可以允许页面样式影响组件,但会引入命名冲突风险,不建议默认打开。

6. 接入真实后端接口的最小改造套路

最后说一个实际接真实后端时最省事的做法:保留前端模板的请求封装,只替换baseUrl和登录态管理。云贝这套模板里所有接口都走utils/request.js,所以接后端时只需要改两个文件,第一是config.js里的baseUrl指向你们的服务端地址,第二是在 request 的 header 里注入 token。token 的获取方式通常是先调微信登录接口换 code,再把 code 传给后端换取 openid 和 session_key,模板自带的 mock 登录不会做这一步,需要自己补。

// config.js module.exports = { baseUrl: 'https://api.yourdomain.com', tokenKey: 'token' };
// request.js 里注入 token const token = wx.getStorageSync(config.tokenKey); if (token) { header['Authorization'] = 'Bearer ' + token; }

这里最关键的是登录时序:页面 onLoad 时如果发现本地没有 token,不能直接跳登录页,要先静默调用wx.login拿 code,再用 code 请求后端换取 token。这个过程可能在用户看到首页之前就要完成,所以建议在app.js的 onLaunch 里做一次,并且用 Promise 包起来,页面请求接口时等登录态就绪。参数说明:Authorization的 Bearer 前缀是后端约定的格式,有的后端用token字段而不是 header 头,对接时以接口文档为准;wx.login拿到的 code 五分钟内有效,且只能使用一次,不能缓存复用。

有一点容易被忽略:模板内置的地址管理、商品分类这些数据,在上线前必须把假数据清理干净。假数据通常写死在data里或者在onLoad里直接赋值,排查时全局搜索mockdemotest关键词,找到后全部切到接口调用。最后在开发者工具里打开「真机调试」,用一个测试微信号走一遍选餐、加购、下单、支付、查看骑手位置的完整流程,订单状态和金额每个环节都要核对,这套模板才算真正落地到你的项目里。

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

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

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

立即咨询