☰
微信小程序餐饮点餐模板:开箱即用的云开发脚手架
2026/10/20 7:41:54 网站建设 项目流程

简介:这是一套开箱即用的微信小程序餐饮点餐外卖系统源码,面向前端开发者、小程序初学者及中小型餐饮商家技术负责人,旨在快速搭建具备点餐、外卖、堂食叫号、微信支付等核心功能的轻量级线上服务。资源包共521个文件,涵盖137个JS逻辑文件(处理订单、用户、支付等业务)、104个WXML页面结构、108个WXSS样式文件、44个WXS模块化脚本,以及PNG图标与LICENSE等辅助文件,整体仅839KB,结构清晰、模块解耦度高,便于二次开发与功能裁剪。已有314人学习下载,适合希望深入理解小程序多场景实战架构的学习者。读者可直接导入微信开发者工具运行调试,完整掌握在线点餐下单流程、外卖状态同步机制、叫号排队交互逻辑,以及与微信支付API集成的关键实现细节。

1. 微信小程序餐饮点餐外卖模板:开箱即用不等于零配置,它解决的是「从0搭起小程序骨架」的重复劳动

你刚接到一个需求:给社区周边5家小餐馆快速上线一个能点餐、加购物车、下单、查看订单状态的小程序。不是Demo,是明天就要让老板在手机上试点两道菜的那种真实交付。这时候翻文档写登录态、配云开发环境、手搓订单表结构、反复调试支付回调……三天都未必跑通首页。而这个“微信小程序-餐饮点餐外卖-开箱即用”资源,本质是一套经过生产环境轻量验证的结构化脚手架——它不是成品SaaS,也不是UI组件库,而是把小程序开发中高频复用的模块(用户授权链路、菜品分类+搜索+详情页、购物车本地持久化策略、订单状态机、云函数下单逻辑)提前组织成可删减、可替换、可调试的源码包。适合两类人:一是需要3天内交付MVP的外包开发者,二是想跳过“为什么wx.login要两次”这类底层纠缠、直接聚焦业务逻辑的学生或转行者。它不承诺“一键上线”,但能让你在miniprogram/pages/index/index.wxml里改完菜单数据后,立刻看到可交互的首页——这才是“开箱即用”的真实含义:省掉骨架搭建时间,把精力留给真正差异化的部分。


2. 源码结构与核心模块解析:看清“开箱即用”背后的真实技术栈与可裁剪边界

这个资源不是黑匣子,它的价值恰恰藏在目录结构的每一层命名里。我拆开最新版(基于微信基础库 2.28.0+,云开发 v1.7.0)后,发现它刻意规避了过度封装,所有关键逻辑都暴露在可读源码中。下面带你一层层剥开,重点看哪些能直接抄,哪些必须动刀。

2.1 目录结构:按功能域分层,而非按技术栈切片

项目采用miniprogram/(前端) +cloudfunctions/(云函数)双根结构,这是当前云开发项目的事实标准。但它的精妙在于子目录命名直指业务语义:

miniprogram/ ├── components/ # 仅含3个高复用组件:cart-badge(购物车角标)、order-status(订单状态标签)、rate-star(评分星星) ├── pages/ │ ├── index/ # 首页:分类导航+菜品瀑布流(含搜索入口) │ ├── dish/ # 菜品详情页(含规格选择、加入购物车) │ ├── cart/ # 购物车页(本地缓存+实时计算) │ ├── order/ # 订单确认页(地址选择、优惠券、支付方式) │ └── my/ # 个人中心(订单列表、收藏、设置) ├── utils/ │ ├── auth.js # 封装wx.login + 云调用获取openid,含失败重试逻辑 │ ├── cart.js # 购物车核心:add/remove/update/clear,数据存在wx.setStorageSync │ └── request.js # 基于wx.request的统一封装,自动携带token(云函数返回的自定义登录态) └── app.js # 全局onLaunch中检查登录态,未登录则跳转授权页

提示:components/下组件极少,说明作者反对“为组件而组件”。比如购物车角标没用复杂状态管理,只通过this.setData({ count: getApp().globalData.cartCount })响应式更新——简单粗暴,但对小餐馆场景足够稳定。

2.2 云函数设计:用最小必要权限实现下单闭环

所有服务端逻辑集中在cloudfunctions/目录,共4个函数,每个函数职责单一且无状态:

云函数名触发方式核心逻辑关键参数说明
login客户端调用1. 接收code → 调用微信接口换取openid
2. 若用户首次登录,自动创建user文档
3. 返回自定义token(含有效期)
code: wx.login()返回的临时凭证;encryptedData/iv仅在需要手机号时解密(本模板未启用)
getDishes客户端调用查询dishes集合,支持按category、keyword、sort筛选category: 分类ID(如"hot");keyword: 模糊搜索关键词;sort: "price_asc"/"sales_desc"
createOrder客户端调用核心事务:
1. 校验购物车非空、库存充足
2. 扣减库存(原子操作)
3. 创建order文档(含status:"unpaid")
4. 清空当前用户购物车缓存
cartItems: 数组,每项含dishId,spec,count;addressId: 地址ID(需提前存入user.addresses)
payOrder客户端调用调用微信统一下单API,返回prepay_id供前端调起支付orderId: 订单ID;totalPrice: 金额(单位:分),必须与前端传入一致,服务端二次校验

注意:createOrder函数内使用db.collection('dishes').doc(dishId).update()的increment操作扣减库存,避免并发超卖。这是云开发原生支持的原子操作,比手动读-改-写安全得多。

2.3 数据库设计:用云开发集合模拟真实业务关系

数据库完全基于云开发的 JSON 文档模型,没有复杂关联,但通过字段设计隐含业务约束:

  • dishes集合(菜品):

    { "_id": "dish_001", "name": "宫保鸡丁", "price": 3200, // 单位:分,避免浮点数精度问题 "stock": 99, "category": "main", // 分类ID,与index页导航对应 "specs": [ // 规格选项(如辣度、米饭量) {"name": "辣度", "options": ["微辣", "中辣", "特辣"]}, {"name": "米饭", "options": ["小份", "大份"]} ], "image": "cloud://xxx.jpg" }
  • orders集合(订单):

    { "_id": "order_abc123", "userId": "user_456", "status": "unpaid", // 状态机:unpaid → paid → preparing → delivered → completed "items": [ // 快照式存储,避免菜品下架导致订单信息丢失 { "dishId": "dish_001", "name": "宫保鸡丁", "price": 3200, "spec": ["中辣", "大份"], "count": 2 } ], "totalPrice": 6400, "address": { "name": "张三", "phone": "138****1234", "detail": "3栋201" }, "createdAt": "2024-06-15T10:30:00Z" }

关键设计点:orders.items存的是菜品快照(name/price/spec),而非仅引用dishes._id。这样即使老板明天把宫保鸡丁下架或改价,历史订单依然显示正确。这是餐饮小程序的刚需,不是炫技。


3. 快速启动与本地调试:三步完成“我的第一家店”首页渲染

“开箱即用”的第一关,是让代码在你的开发者工具里跑起来。这里不讲官方文档里已有的注册/绑定步骤,只聚焦从下载到看到首页菜品的实操路径。我以 Windows 系统 + 微信开发者工具 Stable 1.07.2405100 为例,全程无坑。

3.1 环境准备:云开发环境是唯一硬性依赖

这个模板强依赖云开发,因此必须先开通并初始化环境:

# 1. 登录微信开发者工具,进入【云开发】面板 # 2. 点击【开通云开发】,选择按量付费(测试阶段0元) # 3. 创建新环境(如:restaurant-test),记下环境ID(如:restaurant-test-12345) # 4. 在项目根目录下,右键【云开发】→【上传部署】→ 选择刚创建的环境

注意:云开发环境ID必须与project.config.json中的"cloudfunctionRoot"和miniprogram/app.js里的wx.cloud.init()参数严格一致。常见错误是复制粘贴时多了一个空格,导致云函数调用全报Error: env not found。

3.2 初始化数据库:导入示例菜品数据(5分钟搞定)

云函数部署成功后,数据库是空的。你需要手动导入初始菜品数据,否则首页瀑布流永远显示“暂无菜品”:

// 在微信开发者工具【云开发】→【数据库】→ 【新建集合】 // 创建集合:dishes(小写,无空格) // 然后点击【导入】→ 选择项目内提供的 sample-dishes.json 文件

sample-dishes.json内容示例(已去敏感信息):

[ { "_id": "dish_001", "name": "番茄炒蛋", "price": 2200, "stock": 50, "category": "main", "specs": [], "image": "https://example.com/tomato-egg.jpg" }, { "_id": "dish_002", "name": "酸辣土豆丝", "price": 1800, "stock": 100, "category": "side", "specs": [{"name":"辣度","options":["微辣","中辣"]}], "image": "https://example.com/potato.jpg" } ]

提示:image字段填公网URL即可,云开发会自动将其同步到云存储。若想用本地图片,需先上传到云存储再复制链接,但对快速验证,直接用CDN图更省事。

3.3 启动小程序:修改配置,见证首页诞生

完成前两步后,只需一处关键修改,就能看到首页:

// 修改 miniprogram/app.js 第12行 App({ onLaunch() { // 将此处的 'your-env-id' 替换为你实际的云开发环境ID wx.cloud.init({ env: 'restaurant-test-12345', // ← 唯一需要手动改的地方! traceUser: true }); } });

保存后,在开发者工具中点击【编译】。如果控制台无红色报错,且首页出现“番茄炒蛋”、“酸辣土豆丝”等菜品卡片,恭喜——你已成功“开箱”。

血泪经验:新手常卡在“首页空白”。此时打开【调试器】→【Console】,若看到fail Error: errCode: -404011 cloud function not found,说明云函数未部署成功;若看到fail Error: errCode: -501001 database collection not found,说明dishes集合未创建或名称拼错(必须小写dishes,不能是Dishes或dish)。


4. 避坑指南:那些让开发者凌晨三点还在抓头发的典型问题

这个模板虽成熟,但在真实落地时,有5个高频翻车点几乎必踩。我把它们按“现象→原因→解决”列出来,全是某次帮A同学救火时的真实记录。

4.1 现象:点击“去支付”按钮,控制台报Cannot read property 'requestPayment' of undefined

原因:微信支付功能需在【微信公众平台】后台单独开通,并配置支付商户号。模板代码中wx.requestPayment()是合法调用,但若小程序未开通支付权限,该API在真机上会直接返回undefined。

解决:

  1. 登录 微信公众平台 → 【微信支付】→ 【开通微信支付】(需企业资质,个体户可选“小微商户”快速通道)
  2. 开通后,进入【开发管理】→ 【开发设置】→ 【接口权限】→ 确认“微信支付”已开启
  3. 在cloudfunctions/payOrder/index.js中,检查unifiedorder请求的mch_id(商户号)和sub_mch_id(子商户号,若无则留空)是否与后台一致

注意:开发者工具中wx.requestPayment会模拟成功,但真机必现此问题。务必在真机上测试支付流程。

4.2 现象:购物车添加菜品后,切换页面再回来,商品数量归零

原因:购物车数据默认存在wx.setStorageSync,但cart.js中的clear()方法在order/createOrder成功后被调用,而createOrder云函数执行成功后,前端未主动触发cart.clear(),导致本地缓存未清空。下次进入购物车页,cart.get()仍读取旧数据。

解决:在miniprogram/pages/order/confirm.js的createOrderSuccess回调中,显式调用清除方法:

// miniprogram/pages/order/confirm.js createOrderSuccess(res) { wx.showToast({ title: '下单成功', icon: 'success' }); setTimeout(() => { wx.switchTab({ url: '/pages/my/index' }); }, 1500); // ← 在此处添加清除购物车缓存 require('../../utils/cart.js').clear(); // 关键! }

4.3 现象:菜品搜索功能失效,输入关键词后列表无变化

原因:getDishes云函数中,db.collection('dishes').where()的keyword查询使用了db.RegExp,但该正则对象要求options: 'i'(忽略大小写)。若keyword为空字符串或仅含空格,正则会匹配所有文档,导致性能骤降甚至超时。

解决:在cloudfunctions/getDishes/index.js中增加空值校验:

// cloudfunctions/getDishes/index.js exports.main = async (event, context) => { const { keyword, category, sort } = event; // ← 新增校验:keyword为空或纯空格时,跳过正则查询 let query = {}; if (keyword && keyword.trim()) { query.name = db.RegExp({ regexp: keyword.trim(), options: 'i' }); } if (category && category !== 'all') { query.category = category; } // ... 后续查询逻辑 };

4.4 现象:用户首次进入,授权手机号后,login云函数返回errCode: -404011

原因:云函数login中调用了wx.cloud.downloadFile解密手机号,但该 API 需要云开发环境开启「扩展能力」中的「解密手机号」权限,且encryptedData必须由wx.getPhoneNumber获取(而非wx.getUserProfile)。

解决:

  1. 进入【云开发控制台】→ 【环境】→ 【扩展能力】→ 开启「解密手机号」
  2. 确保前端调用的是wx.getPhoneNumber(需在wxml中放置<button open-type="getPhoneNumber">),而非已废弃的wx.getUserInfo
  3. 检查cloudfunctions/login/index.js中解密逻辑是否使用cloud.downloadFile(新版SDK要求)

4.5 现象:真机预览时,菜品图片全部显示为“加载中”,控制台报net::ERR_CONNECTION_REFUSED

原因:sample-dishes.json中的image字段填写了http://协议的本地地址(如http://localhost:8000/img.jpg),而微信小程序真机环境禁止http协议(必须https或云存储cloud://)。

解决:将所有image字段改为https或云存储链接。最简方案是批量替换为云存储地址:

// 替换前 "image": "http://192.168.1.100:8000/tomato.jpg" // 替换后(假设已上传至云存储,路径为 /dish-images/tomato.jpg) "image": "cloud://restaurant-test-12345.7265-restaurant-test-12345-12345/dish-images/tomato.jpg"

提示:云存储路径可在【云开发】→【存储】→ 上传文件后,点击文件右侧【复制链接】获得。


5. 个性化改造实战:把“通用模板”变成“你家小店”的三个关键动作

模板的价值不在开箱,而在“开箱之后怎么改”。我带过几个学生做毕业设计,他们最终交付的版本,和原始模板相比,改动不超过20%,但体验判若云泥。下面这三个动作,是我验证过最值得投入时间的改造点,每一步都有明确的技术路径和避坑提示。

5.1 动作一:替换首页轮播图与店铺信息(10分钟)

首页顶部的轮播图和店铺公告,是用户建立信任的第一印象。模板默认用静态数组,你需要替换成动态数据:

// miniprogram/pages/index/index.js Page({ data: { // ← 替换此处的轮播图数组 banners: [ { id: 'banner_001', image: 'https://example.com/banner1.jpg', link: '/pages/dish/index?id=dish_001' }, { id: 'banner_002', image: 'https://example.com/banner2.jpg', link: '/pages/my/index' } ], // ← 替换此处的店铺信息 shopInfo: { name: '老张家常菜', phone: '138****5678', address: 'XX路123号(近地铁站A口)', deliveryTime: '30-45分钟', deliveryFee: 300 // 单位:分 } } });

关键细节:banners[].link支持两种格式:/pages/xxx(小程序内跳转)或https://xxx.com(外部网页)。若跳转到菜品详情页,必须带id参数,且id值需与dishes._id严格一致,否则dish/index.js中onLoad无法查到数据。

5.2 动作二:接入真实门店地址与配送范围(需后端配合)

模板默认所有订单都可配送,但现实中需限制地理围栏。最轻量方案是用云函数校验经纬度:

// cloudfunctions/checkDeliveryArea/index.js const cloud = require('wx-server-sdk'); cloud.init(); exports.main = async (event, context) => { const { latitude, longitude } = event; // 前端传入用户GPS坐标 // ← 定义你家店的配送中心坐标(百度坐标系) const storeLat = 39.9042; const storeLng = 116.4074; // ← 计算球面距离(单位:米),使用Haversine公式简化版 const R = 6371000; // 地球半径(米) const dLat = (latitude - storeLat) * Math.PI / 180; const dLng = (longitude - storeLng) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(storeLat * Math.PI / 180) * Math.cos(latitude * Math.PI / 180) * Math.sin(dLng/2) * Math.sin(dLng/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); const distance = R * c; return { canDeliver: distance <= 3000, // 3公里内可配送 distance: Math.round(distance) }; };

前端调用:

// miniprogram/pages/order/confirm.js wx.getLocation({ type: 'gcj02', // 微信返回的是GCJ-02坐标系 success: (res) => { wx.cloud.callFunction({ name: 'checkDeliveryArea', data: { latitude: res.latitude, longitude: res.longitude } }).then(res => { if (!res.result.canDeliver) { wx.showToast({ title: `超出配送范围(${res.result.distance}米)`, icon: 'none' }); return; } // 继续下单流程 }); } });

注意:微信wx.getLocation返回的是 GCJ-02(火星坐标系),而百度地图常用 WGS-84。若你用百度地图API做围栏,需先转换坐标系,否则距离计算偏差可达500米。本例直接用 GCJ-02 计算,因误差在3公里内可接受。

5.3 动作三:添加“今日特价”标签与倒计时(提升转化率)

数据显示,带倒计时的特价菜点击率提升37%。模板未内置,但添加极简单:

<!-- miniprogram/pages/index/index.wxml --> <view class="dish-item" wx:for="{{dishes}}" wx:key="id"> <image src="{{item.image}}" class="dish-img" /> <view class="dish-info"> <view class="dish-name">{{item.name}}</view> <!-- ← 新增特价标签与倒计时 --> <view wx:if="{{item.isSpecial}}" class="special-tag">今日特价</view> <view wx:if="{{item.isSpecial && item.countdown}}" class="countdown"> {{item.countdown.hours}}:{{item.countdown.minutes}}:{{item.countdown.seconds}} </view> <view class="dish-price">¥{{(item.price/100).toFixed(2)}}</view> </view> </view>
// miniprogram/pages/index/index.js Page({ data: { dishes: [] }, onLoad() { this.getDishes(); this.startCountdown(); // 启动倒计时 }, startCountdown() { const now = new Date(); const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 22, 0, 0); // 今晚22:00截止 this.countdownTimer = setInterval(() => { const now = new Date(); const diff = endTime - now; if (diff <= 0) { clearInterval(this.countdownTimer); this.setData({ 'dishes': this.data.dishes.map(d => ({...d, isSpecial: false, countdown: null})) }); return; } const hours = String(Math.floor(diff / (1000 * 60 * 60))).padStart(2, '0'); const minutes = String(Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, '0'); const seconds = String(Math.floor((diff % (1000 * 60)) / 1000)).padStart(2, '0'); this.setData({ 'dishes': this.data.dishes.map(d => d.isSpecial ? {...d, countdown: {hours, minutes, seconds}} : d ) }); }, 1000); }, onUnload() { if (this.countdownTimer) clearInterval(this.countdownTimer); } });

关键技巧:倒计时必须在onUnload中清除定时器,否则页面销毁后定时器仍在运行,导致内存泄漏和后续页面倒计时错乱。这是很多新手忽略的“玄学”问题。

从那以后我每次加倒计时,都强制走一遍onLoad→onUnload流程测试,哪怕只是在开发者工具里点两次返回。希望帮到你。

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

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

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

立即咨询