
简介一款面向微信小程序平台的餐饮外卖O2O开源模板以 v1.6.9 版本发布适合小程序开发者、独立站运营者及餐饮商家进行二次开发。模板围绕线上下单、菜单展示、支付、订单管理、配送跟踪等外卖核心链路设计源码开放便于技术人员深入调整业务逻辑、界面样式与接口交互也能按不同餐饮场景增加营销、会员、多门店等扩展功能。压缩包约65.84MB文件数量与类型明细暂缺下载后建议先核对目录与运行环境。目前已有1240人学习浏览。这一版本经过多轮迭代通常包含搜索栏、分类导航、商品展示、购物车、个人中心等典型页面模块整体目录结构清晰对熟悉WXML、WXSS与微信小程序API的开发者而言可以省去从零搭建基础框架的时间快速实现线上外卖与订单管理能力也可在现有模板基础上进行品牌化定制接入支付、地图与配送等扩展能力。1. 云贝餐饮外卖O2O v1.6.9 是个什么样的开源模板很多人拿到这个 zip 时的第一反应是解压、打开微信开发者工具、编译、跑起来然后开始改店名和菜品。实际上云贝餐饮外卖O2O v1.6.9 这类开源小程序模板价值不在“跑通”而在“省掉从零搭建 O2O 前端框架的时间”。你看到的是一个已经把点餐、购物车、订单、支付、配送、会员中心页面串起来的原生微信小程序工程v1.6.9 说明它迭代过多个版本基础交互和页面分工相对成熟但后端、域名、支付商户号、图片资源全部需要自己接。适合外包团队拿来做餐饮客户演示、独立开发者快速搭点餐原型、新手研究小程序登录态和支付链路的真实写法。接下来按“拆结构、跑起来、改业务、验发布”的顺序把这条链路走完。2. 微信小程序模板源码的目录结构与O2O业务模块拆解2.1 从zip包到可见结构先看目录再动手解压后第一步不是双击 project.config.json而是先整体扫一遍目录。云贝这类 O2O 模板按原生小程序语法组织根目录上必须存在 app.js、app.json、app.wxss三件套缺一个微信开发者工具都不认。pages 目录下按业务模块分文件夹常见有 index首页、shop门店/店铺、cart购物车、order订单、user我的、coupon优惠券等components 放自定义组件比如菜品卡片、购物车角标utils 放请求封装、登录态、公共方法static 或 images 放静态图片资源。yunbei-o2o-v1.6.9/ ├── app.js ├── app.json ├── app.wxss ├── project.config.json ├── pages/ │ ├── index/ # 首页banner、分类、店铺列表 │ ├── shop/ # 门店详情、菜品列表、规格选择 │ ├── cart/ # 购物车 │ ├── order/ # 确认订单、订单列表、订单详情 │ ├── user/ # 我的、地址、优惠券 │ └── pay/ # 支付结果 ├── components/ │ ├── goods-card/ # 菜品卡片组件 │ └── count-editor/ # 数量加减组件 └── utils/ ├── request.js # wx.request 统一封装 ├── auth.js # 登录态与 token 管理 ├── cart.js # 购物车计算 └── config.js # 接口域名与公共参数这段树状结构拿到的包不一定同名但骨架基本一致。判断一个模板是不是原生小程序最直接的方式就是看根目录有没有 app.json有就用微信开发者工具直接导入如果只有 main.js、manifest.json那是 uniapp 工程需要先转换成微信小程序或通过 HBuilderX 发行到微信平台。云贝 v1.6.9 这个包内的页面文件是 .js/.json/.wxml/.wxss 四件套走的是原生语法别用 HBuilderX 硬开否则会看到一堆编译报错。目录的意义不只是找文件。app.json 里注册了所有页面pages 数组的第一项就是启动页改启动页顺序时要注意页面路径大小写必须和真实文件一致否则编译直接提示“页面路径未找到”。模板源码里 WXML 和 WXSS 已经把版式排好你改数据源就能换内容但页面结构和组件引用关系不要乱动删除一个组件前先全局搜一下被谁引用避免出现“组件未注册”的运行时错误。2.2 核心O2O流程菜品-购物车-订单-支付的数据闭环餐饮外卖 O2O 的前端闭环可以压缩成四条线浏览门店和菜品、把菜品加进购物车并确认规格、提交订单到服务端、支付后同步订单状态。云贝模板里购物车是整条链路的枢纽它把商品 ID、规格 ID、数量、价格、选中态算好才进入确认订单页。购物车数据一般放在全局缓存里而不是堆在页面 data 中因为页面切换后 data 会被回收。购物车的常见实现是维护一个以 skuId 为 key 的 objectvalue 包含数量、选中状态、快照价格而不是往数组里 push 整份菜品数据。这样合并相同 SKU、修改数量、计算总价都只需要遍历 key。改模板的 utils/cart.js 时addCart、decrementCart、getCartTotal 这类方法的返回值要保持原样其他地方可能多处依赖。模板的数据流大致是这样首页请求店铺列表店铺页请求菜品列表菜品项绑定加购事件加购后 tabBar 上的角标变化进入购物车页后通过结算按钮携带总价跳确认订单页。前端只负责组装参数真正的金额校验必须放在服务端。模板源码里大概率会看到提交订单只做前端校验的写法这是演示代码的常见状态正式上线前要在服务端重算金额、库存、配送费防止客户端传值被篡改。下面是一段典型的订单参数拼装逻辑function buildOrderParams(cartItems, addressInfo, remark) { const goods cartItems .filter((item) item.checked) // 只带被选中的商品 .map((item) ({ skuId: item.skuId, goodsId: item.goodsId, quantity: item.quantity, goodsSpec: item.specText, // 比如 大杯 去冰 半糖 price: item.price, // 快照价格单位元 })); let totalAmount 0; goods.forEach((g) { totalAmount g.price * g.quantity; }); return { shopId: cartItems[0].shopId, goods: goods, address: addressInfo || {}, remark: remark || , totalAmount: totalAmount.toFixed(2), deliveryFee: 0, // 配送费由服务端计算 }; }这段代码有几个点需要看明白。filter 先把未勾选的商品剔除避免把购物车里的干扰项带进订单goodsSpec 是拼接好的规格文本只做展示后端需要拆解原始规格 ID 时应该传 specId 而不是文本price 在加购时就要快照不能在下单时再拉一次否则后台改价会导致前端金额和服务端不一致totalAmount 只用于前端展示服务端收到请求后必须按自己的菜品位与配送规则重新计算。改接口时模板的 utils/config.js 里通常有 baseURL 和公共 header。最常踩的坑是只替换了 config.js 里的域名但部分页面里仍然硬编码了 http:// 地址发布后在线上环境请求被拦截。处理方式是全局搜 “http://” 和 “https://”逐个替换成 config 里的变量一个都不要留。2.3 模板里的状态管理与权限态这类模板为了降低上手门槛一般不会引入 Vuex 或 Redux而是用 globalData wx.setStorageSync 组合。app.js 里的 globalData 保存用户信息、登录态、首页缓存页面 onLoad 时从 storage 恢复。这套方案对中小外卖项目够用但有两个明显的坑globalData 不持久化冷启动后要重新拉数据多个页面同时写同一个 storage key 会互相覆盖比如订单详情页写入“当前订单”缓存列表页也写入同名 key跳转时就会拿错数据。权限态集中在登录部分。O2O 模板通常用 wx.login 获取 code发给服务端换 openid 和 token后续请求带上 token。模板的 utils/auth.js 里一般有 getToken、setToken、checkLogin 三个方法。接入真实后端时建议把所有 wx.request 都收敛到一个 request 封装里在 401 时统一跳转登录页而不是每个页面单独判断。提示openid 交换接口必须由你自己的服务端转发小程序前端不能直接调微信接口拿 openid否则 appsecret 一旦在代码或抓包里暴露账号安全就是大问题。框架源码里如果出现直连微信接口的写法多数是演示用上线前一定要移掉。wx.login 返回的 code 有效期只有 5 分钟且只能用一次模板里常见的 token 缓存没有做静默刷新用户 token 过期后会被迫重新登录。商用场景建议在 request 封装里加一层请求返回 401 时用 wx.login 重新拿 code换新 token 后重放原请求避免用户正在下单时突然跳登录页打断流程。功能点常见请求路径示例关键入参需要留意的点登录换取 tokenPOST /api/auth/wxlogincode, userInfocode 一次性5 分钟有效获取店铺列表GET /api/shop/listlongitude, latitude需要按距离排序获取菜品列表GET /api/goods/listshopId, categoryId注意规格与库存联动提交订单POST /api/order/creategoods, address, remark金额必须服务端重算支付下单POST /api/pay/unifiedorderId, payType需要服务端签名这张表不需要和模板源码里的路径完全一致它更像一个约定。你接后端时先和后端对齐这套接口语义再改模板里的 request 调用比一边改页面一边问接口要高效得多。3. 在微信开发者工具里导入并跑通云贝餐饮外卖O2O模板3.1 用微信开发者工具导入解压后的源码包打开微信开发者工具选择“导入项目”把刚才解压的云贝餐饮外卖O2O目录整个选中。这里有几个前置条件要满足项目路径不能包含中文、空格或特殊字符否则编译阶段会报“文件查找失败”AppID 可以先选“测试号”模板里大部分页面不依赖特殊接口权限测试号足够跑通界面和交互工具会检查 project.config.json 里的 appid 字段如果你填了自己的小程序 AppID但该小程序没有注册“微信支付”和“配送服务”后续调用这些能力时会被提示权限不足这和模板本身无关。导入后先不要急着点编译。打开 project.config.json检查 appid、projectname 和 miniprogramRoot 三个字段。miniprogramRoot 如果不存在默认指项目根目录如果模板里把源码放在 dist 或 miniprogram 子目录就要在这里显式声明否则工具会看不到 app.json。编译前还需要做一步本地设置。点击工具栏“详情 - 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”这一步的作用是让本地开发环境可以请求 http 或 IP 地址的后端接口。很多新手拿到模板后报“request:fail url not in domain list”就是因为没有勾这个选项。注意这只是本地开发时的开关上传代码后域名校验强制生效不能把“不校验域名”当成上线方案。3.2 配置AppID与本地设置AppID 配置直接影响登录和支付功能的表现。测试号 AppID 下wx.login 能够正常获取 code但服务端无法用这个 code 换取 openid因为测试号没有正式的小程序主体。如果你只是想看页面效果测试号没问题如果你要走真实的登录、支付、退款流程必须换成已认证的小程序 AppID并保证该 AppID 下已经开通微信支付商户号并关联好支付证书。切换 AppID 有两种方式一是在开发者工具导入时直接选“使用我的 AppID”二是在 project.config.json 里修改 appid 后重新打开项目。修改 appid 后要留意缓存之前用测试号产生的 storage 数据不一定兼容新 AppID建议在模拟器里执行“清缓存 - 清除全部缓存”再重新编译。某些模板会把环境判断写在 app.js 里用 wx.getAccountInfoSync().miniProgram.envVersion 区分开发版、体验版、正式版。云贝这类 O2O 模板一般不会主动处理你如果自己做环境变量要保持三个版本映射到不同的 baseURL避免开发环境把测试订单写到正式库。3.3 修改刚进入的加载页面与顶部导航栏高度“刚进入的加载页面”在这类模板里通常指 app.json 的 pages 数组第一项也可能是首页自己渲染的 loading 状态。直接改 pages 数组的顺序是最简单的做法但要注意如果要跳到的是 tabBar 页面启动页里不能使用 wx.redirectTo必须使用 wx.switchTab否则会报错如果只是想在首页显示一个骨架屏推荐在首页 onLoad 里加一个 setTimeout模板 WXML 里预留 loading 结点数据到达后隐藏。如果你要的是“先显示一个广告页再进入首页”的效果常见做法是新建一个 pages/loading 页面把它放进 pages 数组第一位{ pages: [ pages/loading/loading, pages/index/index ], window: { navigationBarTitleText: 云贝外卖, navigationStyle: custom } }navigationStyle 设置为 custom 后右上角胶囊和状态栏区域不再使用微信默认导航页面的头部背景会延伸到状态栏背后。此时必须在 WXML 里手动加占位 view并在 onLoad 里读取状态栏高度和菜单按钮位置const info wx.getWindowInfo(); const menu wx.getMenuButtonBoundingClientRect(); this.setData({ statusBarHeight: info.statusBarHeight, navBarHeight: menu.bottom menu.top - info.statusBarHeight, });这段代码里的 statusBarHeight 是状态栏高度不同机型差异很大比如 iPhone 13 和红米 K40 能差出 15 像素navBarHeight 通过胶囊按钮的 bottom 和 top 计算出来的其实是“胶囊底到屏幕顶”的距离再减去状态栏高度得到的就是自定义导航栏的安全高度。这个值比写死 44 更可靠建议用它统一控制加载页标题和返回按钮的位置。要是你觉得自定义导航动态计算太麻烦最简单的方式是把 app.json 里的 navigationStyle 删掉改用微信默认导航栏标题居中、状态栏颜色都由微信自己处理代价是导航栏背景色不能做成毛玻璃渐变。3.4 常见导入报错与解决导入云贝这类模板源码时编译报错集中在几个固定位置下面是高频问题台账报错现象可能原因处理方式app.json 未找到页面路径pages 数组里的路径和实际目录大小写不一致按实际文件名修正 pages 路径invalid appid填了不存在的 AppID 或小程序类型不符换测试号或已认证小程序Component is not found组件路径写错或未在 json 中声明检查 usingComponents 路径402/500 请求失败脚本或样式缺少分号或引用了不存在的全局变量按报错文件定位修复url not in domain list请求域名未在后台配置或本地校验未关详情-本地设置-勾选不校验域名其中“Component is not found”是模板改造时最容易出现的。比如把 components/goods-card 改名成 goods-card-new但页面 json 里仍然写旧路径编译时不会直接报红色错误而是在控制台打 warning页面里组件区域空白。遇到这种情况全局搜旧组件名逐个替换。另外模板里如果有 npm 依赖导入后需要在工具菜单栏“工具 - 构建 npm”构建完成后 dist 目录会生成 miniprogram_npm 文件夹。不执行这一步插件和第三方库的 require 会全部报错。4. 基于云贝餐饮外卖O2O模板源码改造业务参数与接口4.1 接口对接从mock数据切换到真实API模板默认展示的数据多半来自本地 mock 或写死的 JSON而 O2O 项目真正跑起来后店铺、菜品、订单都是实时数据。第一步是打开 utils/config.js把 baseURL 从 mock 地址改成你的后端网关地址。接着把 request.js 里的公共逻辑补全比如每个请求都带上 token以及统一拦截业务错误码const config require(./config.js); function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: config.baseURL url, method, data, header: { content-type: application/json, Authorization: wx.getStorageSync(token) || , }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: reject, }); }); } module.exports { request };这段封装里有个约定后端返回结构是 { code: 0, data, msg }code 非 0 即业务失败。为什么要把成功判断放在全局而不是每个页面自己 if因为外卖场景的页面多首页、店铺、购物车都有请求每个页面重复写错误弹窗会导致样式和文案不一致。统一处理后页面里只需要处理成功回调。失败时的错误提示会直接以 toast 弹出用户在抢单和结算场景里能立刻看到原因。接真实 API 后还需要处理登录态。模板里 wx.login 拿到的 code 要先发给后端后端返回自己的 token。每次冷启动时 app.js 都会执行 auth.js 里的 checkLogin如果 token 过期不能只清缓存要在 request 层做一次静默重放。具体做法捕获 401调用 refreshLogin 回到“wx.login - 换 token”流程成功后重新发起刚才失败的请求。这个逻辑很关键否则用户正在确认订单时被踢回登录页体验会非常差。4.2 下单、支付与回调的参数设计云贝模板的确认订单页提交按钮会经过“创建订单 - 支付下单 - 微信支付”三步。创建订单时的参数直接决定后端能不能正确分账和打印小票。下面是一张建议字段表参数名类型说明shopIdstring门店 ID多店模式必须传goodsarray商品列表含 skuId、quantity、price 快照addressobject收货人、电话、经纬度deliveryFeenumber配送费以后端返回为准totalAmountstring订单总额仅展示服务端重算payTypestringwechat / balance / cashremarkstring用户备注需做长度限制真正容易翻车的是支付环节。服务端在微信支付统一下单后返回一组支付参数小程序端用 wx.requestPayment 拉起收银台。因为 package 是 JavaScript 里的保留字后端返回的 package 字段必须在接口层先映射成 packageValue否则数据绑定会失败async function onPay(orderId) { const payParams await request(/api/pay/unified, POST, { orderId }); wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.packageValue, signType: RSA, paySign: payParams.paySign, success: () { wx.redirectTo({ url: /pages/order/detail?id${orderId} }); }, fail: (err) { if (err.errMsg err.errMsg.indexOf(cancel) -1) return; wx.showToast({ title: 支付失败, icon: none }); }, }); }支付回调不要轻信前端的 success因为用户可能支付成功但客户端与微信通信中断导致 success 没触发。订单状态的最终裁决必须基于服务端收到微信支付回调 V3 通知后的结果。模板里如果只在成功回调里跳转订单详情需要改成“跳转详情后由详情页轮询订单状态接口”轮询间隔建议 2 秒一次创建订单后 30 次即停止并清理定时器。4.3 配送范围、营业时间与门店配置模板的店铺页通常会展示是否营业、配送范围、起送价。这些配置不应该写死在前端而要由后端返回。营业时间的判断前端只做展示拦截比如营业时间外不允许加购。下面这段代码是比较常见的判断逻辑function isShopOpen(businessHours) { const now new Date(); const day now.getDay(); const hours businessHours[day] || businessHours.default; if (!hours) return false; const current String(now.getHours()).padStart(2, 0) : String(now.getMinutes()).padStart(2, 0); return hours.some((h) current h.open current h.close); }这里最容易被忽略的是跨天营业时间。比如 22:00 到次日 02:00如果后端返回 hours [{open: 22:00, close: 02:00}]这段代码在 01:00 时会判定为不营业。正确做法是把跨天时间段拆分成 [22:00-23:59] 和 [00:00-02:00] 两个区间或者在后端直接给出“是否营业”的布尔值前端不要自己推算。配送范围的判断也一样模板里如果写了圆形半径仅供参考。真实场景要拿用户地址经纬度调后端接口计算骑行距离尤其是多门店竞态时需要由服务端决定哪个门店接单而不是前端选最近的。云贝模板的配置页里一般只有行云流水的静态参数商用时要把它改成动态下发才能支持临时调整营业时间、节假日闭店、暴雨天气调整配送范围。4.4 图片与附件路径处理wx.env.user_data_path模板的菜品图片通常放在 static/images 或使用网络 URL。本地图片在真机上读取很快但小程序安装包有主包 2MB、总包 20MB 的限制图片放多了包体积会膨胀。更常见的做法是菜品图上传到对象存储数据库存 CDN 地址。如果你要做用户头像缓存、保存营销海报等能力需要写入本地文件系统。wx.env.USER_DATA_PATH 就是小程序的用户数据目录不同基础库版本基本稳定const fs wx.getFileSystemManager(); const filePath wx.env.USER_DATA_PATH /yunbei_avatar.png; fs.writeFile({ filePath: filePath, data: base64Data, encoding: base64, success(res) { console.log(saved:, filePath); }, fail(res) { console.error(writeFile fail:, res); }, });注意 USER_DATA_PATH 目录在小程序退出后不保证永久保留微信在存储空间紧张时可能清理该目录所以它只适合做临时缓存重要数据必须上传到服务端。模板源码里如果碰见类似路径拼接建议把文件名带上前缀比如 shopId_orderId避免多店模式下的同名文件互相覆盖。至于那些所谓的小程序图片提取工具在这个场景里其实用不上因为源码包完整、静态资源全在本地解压后直接复制 static 目录就行。5. 发布前验证与性能优化的几个必做动作5.1 真机预览与体验版全链路验证模拟器里跑通只代表逻辑基本正确外卖 O2O 项目发布前一定要在真机上完整走一遍流程。使用工具右上角“预览”生成二维码手机微信扫码进入开发版然后在真机上完成“登录 - 选择门店 - 加购 - 下单 - 支付 - 看订单状态”的闭环。这里重点验证三件事自定义导航栏在不同机型上的高度是否正常微信支付的跳转是否顺畅订单详情轮询在应用切后台后是否仍然在跑。切后台后定时器不会自动暂停比较稳妥的做法是在页面 onHide 里 clearInterval在 onShow 里重新拉一次订单状态并恢复轮询。很多模板没写这个逻辑发布后用户锁屏再回来会看到订单状态长时间不更新甚至重复弹出支付成功提示。5.2 分包加载与首屏性能云贝 v1.6.9 的页面集中在主包如果后端返回的菜品图过大首页加载速度会明显变慢。建议把 order 和 user 下的深层页面放进分包。subPackages 的 root 必须与 pages 下目录一一对应{ subPackages: [ { root: pages/order/, pages: [ list/list, detail/detail ] }, { root: pages/user/, pages: [ address/list, coupon/list ] } ] }配置分包后页面跳转路径不用改仍然是 /pages/order/detail/detail 这种写法但主包体积会明显缩小。跑一次“真机调试 - 性能面板”看首页的渲染耗时和网络请求耗时。如果发现单个菜品图片过大可以在上传时压缩到 750px 宽度以下并用 WebP 格式首屏轮播图改为后台按需加载。5.3 用 Charles 抓包验证接口数据发布前的接口验证建议直接在电脑端抓包。手机与电脑处于同一个局域网手机设置 HTTP 代理指向电脑的 Charles 监听端口安装并信任 Charles 的 CA 证书后微信小程序里的 wx.request 请求就会出现在抓包面板。重点看三处登录请求的 Authorization 是否每次正确携带提交订单的 payload 里 totalAmount 是否和后端计算一致支付回跳时订单详情轮询的 URL 参数是否带着 orderId。抓包时如果发现模板代码里有 console.log 打出了用户手机号或完整地址需要全局检索一遍把 console.log 去掉或改成只在开发环境下输出。最后在 project.config.json 里把 urlCheck 从 false 改为 true再上传体验版确认域名校验在新环境里能正常通过。这一步不做很可能出现“开发者工具里点餐正常体验版里 request 全部失败”这类经典上线事故。本文还有配套的精品资源点击获取