简介:这套微信小程序餐饮点餐外卖项目源码包,面向具备基础前端知识、想快速上手小程序开发的初中级开发者,也适合餐饮商家技术团队参考。包内包含小程序前端页面、逻辑、样式及配置文件,共521个文件,主要由137个js逻辑文件、108个wxss样式文件、104个json配置文件和102个wxml页面文件组成,另有wxs辅助脚本与png图标资源,压缩包大小839KB,结构清晰便于查找。已有314人学习下载。通过该项目可完整掌握在线点餐下单、外卖配送整合、叫号排队、微信支付、用户账号系统、配送管理与数据分析等核心业务模块的实现思路,理解前后端接口交互与本地数据管理方式。源码基于真实业务场景组织目录,注释相对完整,可在此基础上直接改造成自有餐饮品牌小程序,快速落地上线,省去从零搭建的时间成本。
1. 微信小程序点餐外卖的开箱即用:不是能预览,是能上线收款
“微信小程序-餐饮点餐外卖-开箱即用”,听起来就是导入开发者工具就能跑的现成模板。很多朋友把这箱东西打开后,看到首页菜品图、购物车能加菜,就以为项目已经完成,结果真机预览时登录失败、定位空白、支付按钮点不动,这时候才意识到:开箱即用的“箱”根本不是代码,而是你还没填完的账号、资质和业务参数。真正可用的点餐外卖小程序,是要让商家能收钱、能出餐、能算清配送费。这篇文章从选型到落地,把跑通这个方向的关键配置、必调参数和上线坑一次讲透。
2. 开箱即用的技术选型:原生小程序、云开发与自建后端怎么挑
2.1 三种形态的取舍:跑通速度与改造成本的平衡
市面上“开箱即用”的餐饮点餐外卖小程序,通常分成三种形态。它们的共同点是前端页面和基础业务逻辑都备好了,差别在于后端跑在哪里、你能改多深。
第一种是原生小程序加微信云开发。这是最匹配“开箱即用”四个字的组合。云开发提供了云函数、数据库和存储,免去自己买服务器、配 HTTPS、做域名备案的流程,数据库权限又和小程序登录态天然打通。商家在管理后台改完菜品价格,用户端下一次请求就能拿到新数据,不需要发版。对没有技术团队的快餐店,或想做私域外卖的独立门店,这类模板通常半天就能跑通。
第二种是 uni-app 加自建后端。适合要同时发布微信小程序、支付宝小程序甚至 H5 的项目。前端界面可以跨端复用,但后端接口、数据库、对象存储都得自己维护,模板里往往只给一个前端壳子。你需要在本地起 MySQL 或 MongoDB,再调整网络请求域名,开发周期比云开发模板多出三到五天,好处是不被微信生态锁死。
第三种是原生小程序加自建后端。多出现在需要对接企业已有 ERP、会员系统或厨房打印系统的场景。这类模板前端可以开箱,后端是一团需要梳理的黑匣子。对只想做点餐外卖的独立门店来说,这个方向太重了。
选型时我一般只问三个问题:第一,要不要多端发布;第二,有没有自己的服务器和运维人力;第三,会不会重度改造业务流程。三个都不需要,就选云开发形态,把精力留给运营配置,而不是留给基础设施。
2.2 一套模板的长相:目录结构与核心配置文件
先认识箱子里有什么,再讨论怎么开箱。常见云开发形态的模板,会有这样一套目录:
project/ ├── cloudfunctions/ # 云函数目录 │ ├── login/ # 登录:换取 openid │ ├── getMenu/ # 获取菜品列表,支持分类筛选 │ ├── placeOrder/ # 创建订单,服务端重算金额 │ ├── payOrder/ # 发起微信支付,返回支付参数 │ └── payCallback/ # 支付结果回调,更新订单状态 ├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 点餐首页,展示分类和菜品 │ │ ├── cart/ # 购物车页,可改份数 │ │ ├── order/ # 订单列表和详情 │ │ └── mine/ # 用户中心,查看收货地址 │ ├── app.js # 云开发初始化入口 │ ├── app.json # 全局配置:页面路由、权限声明 │ └── app.wxss # 全局样式 ├── admin/ # 商家网页管理后台 ├── project.config.json # 开发者工具的项目配置,AppID 填在这里 └── database/ └── seed.json # 初始数据:分类、菜品、店铺配置这份结构的核心思路是前后端彻底拆开。页面只负责展示和收集操作,真正的菜单读取、价格计算、订单落库都在云函数里完成。这样做还有一个好处:商家在管理后台改了价格,小程序端不用发版,重新请求一次就是新数据。坏处是有些模板没有做成 UI 管理后台,改菜品还要打开数据库集合手动加记录,你在评估模板时要确认这一点。
导入工具后第一个要看的是project.config.json。它至少包含miniprogramRoot、cloudfunctionRoot、appid三项,其中cloudfunctionRoot告诉开发者工具“哪些目录是需要上传的云函数”。不少模板为了开源方便,把appid留空,你打开时会直接弹invalid appid。这不是项目坏了,是你还没把自己的 AppID 填进去。
接着是database/seed.json。它里面通常放着一份“模拟餐厅”的菜单和价格,所谓开箱即用,就是靠这套初始数据把前端页面撑起来。但你真正营业时不可能用里面的菜品名,所以第二个动作是到云开发控制台把这套数据清空,换成自己的分类和菜品。很多翻车现场都出在这里:页面显示的是模板自带的菜,老板还以为是上线成功了。
最后要检查云函数目录下每个子目录里有没有config.json。有些模板依赖云函数配置里的环境变量,比如usePro、storeId,没有这些变量,云函数会报Cannot read property。你拿到模板后不要急着上传全部云函数,先点开每个config.json,把环境变量和权限设置过一遍,再统一上传。这一步能省掉后续一大半排查时间。
3. 把项目跑起来:从导入模板到真实下单的最小路径
3.1 AppID 与云环境 ID 配置:10 个报错里有 8 个在这
拿到模板后,我见过最多的反馈是“导入就报错”。新手看到红色报错就慌,其实根因往往只有两个:AppID 没换成自己的,或者云环境 ID 填了一个不存在的环境。
先说 AppID。注册小程序时,个人主体和企业主体都能拿到 AppID,都能开发,但餐饮点餐外卖涉及在线交易和配送,个人主体的小程序在类目审核阶段就过不去。所以注册主体最好选企业或个体工商户,否则后面会出现“功能正常但审核失败”的尴尬局面。
拿到 AppID 后,在开发者工具右上角点“详情”,找到“基本信息”里的 AppID,把它填进project.config.json:
{ "appid": "wx1234567890abcdef", "projectname": "mini-diancan", "cloudfunctionRoot": "cloudfunctions/" }appid是每个小程序唯一的身份标识,云开发资源也挂在这个 ID 下。用测试号虽然能编译,但云开发和微信支付都用不了,所以别为了省事用测试 AppID。
然后是云开发环境。开通云开发后,控制台会生成一个环境 ID,类似diancan-3g1a2b。这个 ID 要同时出现在前端初始化和云函数初始化两类代码里。先看miniprogram/app.js:
App({ onLaunch() { if (!wx.cloud) { console.error('当前微信基础库版本过低,请升级后重试') return } wx.cloud.init({ env: 'diancan-3g1a2b', // 云开发环境 ID,注意不是环境名称 traceUser: true // 追踪用户访问,用于控制台看访问数据 }) } })env参数写环境 ID,而不是你在控制台里起的那个中文名称。traceUser建议开着,它能让云开发控制台按用户维度统计访问量,排查问题时很好用。
云函数内部的初始化却不太一样,常见写法是cloud.DYNAMIC_CURRENT_ENV,意思是当前云函数属于哪个环境,就自动使用哪个环境,不需要硬编码环境 ID:
const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })这样做的收益是:同一套代码部署到测试环境和正式环境时,不用改任何参数。如果你看到云函数报env not found,大概率是前端app.js里的环境 ID 写错,或者这个环境还没创建。到云开发控制台确认一下环境列表,把 ID 复制粘贴过来,不要手输。
3.2 跑通“菜单-购物车-下单”的最小数据流
配置完成后,先不要碰支付和配送,我们把“加菜-创建订单”这条链路跑通就算成功。这条链路由三步组成:页面请求菜单、购物车本地汇总、云函数创建订单。
点餐首页加载时,会在onLoad里调用getMenu云函数:
wx.cloud.callFunction({ name: 'getMenu', data: { category: '' // 传空字符串表示获取全部分类下的菜品 } }).then(res => { const list = res.result.list this.setData({ menuList: list }) }).catch(err => { wx.showToast({ title: '菜单加载失败', icon: 'none' }) })这个调用的data参数用来传分类筛选条件,getMenu云函数内部会根据category判断是返回全部菜品,还是只返回某一个分类下的菜品。如果返回空数组,先查数据库里有没有数据,不要急着改前端,八成是seed.json还没导入。
购物车不需要实时同步到后端,我习惯把它放在本地缓存wx.setStorage。用户点“去结算”时,前端把购物车数据整理成goodsList传给placeOrder云函数:
const cart = this.data.cart const goodsList = cart.map(item => ({ id: item._id, // 只传菜品 ID,不传价格 count: item.count // 购买份数 })) wx.cloud.callFunction({ name: 'placeOrder', data: { goodsList, deliveryType: 'self', // 先写死为到店自取,绕过配送逻辑 address: null } }).then(res => { const orderId = res.result.orderId wx.navigateTo({ url: `/pages/order/confirm?orderId=${orderId}&from=prepay` }) })这里把deliveryType写成self是刻意为之。先验证下单链路,不触发配送费计算和地址校验。等这条链路通了,再改成delivery。别一上来把所有配送参数都打开,出了问题会很难定位是前端传参错误,还是后端计算错误。
3.3 云函数 placeOrder 的写法与参数说明:金额必须后端重算
placeOrder是整个点餐项目最核心的云函数,它决定了订单金额能不能被信任。很多“开箱即用”的模板为了演示方便,会在前端把总价算好传到后端,这是绝对不可以直接上线的做法。客户端传一个价格,用户把数据抓包一改,就能以 0.01 元买一大堆菜。
我一般会在云函数里重新遍历一遍商品,从数据库读取实时价格再算总价。核心代码:
const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event, context) => { const { OPENID } = cloud.getWXContext() const { goodsList, deliveryType, address, remark } = event if (!Array.isArray(goodsList) || goodsList.length === 0) { throw new Error('购物车不能为空') } // 1. 服务端读取菜品价格,重新计算总价 let goodsTotal = 0 for (const item of goodsList) { const goodsRes = await db.collection('goods').doc(item.id).get() const goods = goodsRes.data if (!goods.onsale) throw new Error(`商品 ${goods.name} 已下架`) goodsTotal += goods.price * item.count } // 2. 根据配送类型处理配送费 let totalFee = Math.round(goodsTotal * 100) / 100 if (deliveryType === 'delivery') { const cfgRes = await db.collection('config').doc('delivery').get() const { deliveryFee, freeThreshold } = cfgRes.data if (goodsTotal < freeThreshold) { totalFee = Math.round((goodsTotal + deliveryFee) * 100) / 100 } } // 3. 落库 const order = { openid: OPENID, goodsList, deliveryType, address: address || null, remark: remark || '', totalFee: totalFee, status: 0, // 0=待支付 createTime: db.serverDate(), updateTime: db.serverDate() } const addResult = await db.collection('orders').add({ data: order }) return { orderId: addResult._id, totalFee } }参数说明:goodsList是数组,每项只传id和count,不传价格。deliveryType是字符串,支持self(到店自取)和delivery(外卖配送)。address在自取场景传null,外卖场景必须有省市区和详细地址。remark是用户备注,可空。
为什么价格计算用Math.round(x * 100) / 100?因为浮点运算会出现0.1 + 0.2 = 0.30000000000000004,不处理的话订单金额和支付金额可能对不上。更稳妥的做法是全程用“分”做整数计算,因为微信支付接口要求的就是整数分。模板里如果是按“元”计算的,建议尽快改成以分为单位,能省掉未来大量对账的麻烦。
订单写好,前端拿到orderId跳转到支付确认页。到这里,最小数据流已经通了:用户能看到菜品、能加购物车、能提交订单、后端落了库。支付不是最难的部分,但容易被几个参数卡住,下一章专门讲商家上线前必须配置的运营参数。
4. 商家上线前必须配置的参数:菜品、营业时间、配送范围和支付
4.1 数据库集合设计:从一份菜品到一套菜单
一个能支撑点餐外卖的数据库,最少需要四张集合:分类表categories、菜品表goods、订单表orders、全局配置表config。多数模板还会加一张用户表users,但最初我们可以不依赖它。
categories文档结构简单,只做展示分类:
const categoryDoc = { _id: 'c01', name: '招牌热菜', // 分类名 sort: 1 // 排序值,越小越靠前 }goods才是一道菜的完整画像。我常用的字段大概长这样:
const goodsDoc = { _id: 'g01', name: '招牌黄焖鸡', // 菜品名 categoryId: 'c01', // 关联分类 _id price: 28, // 现价(元) originalPrice: 32, // 划线价,可空 cover: 'cloud://xxx', // 菜品图,建议上传到云存储 description: '微辣,默认配米饭', stock: 100, // 可售库存,-1 代表不限 onsale: true, // 是否上架 sales: 0, // 累计销量,排序用 sort: 0 }price用元做展示单位可以,但计算金额时要在前端拿到后乘 100 转成分。categoryId用字符串关联,不需要数据库外键。前端查询时先用分类表查出所有分类,再按分类分别拉取菜品。个人开发阶段别去做复杂联表,云开发也不支持 SQL 里那种 join,硬凑只会让逻辑变脆。
config集合是整个店铺的“总闸”,后面提到的营业时间、配送费、公告全在这里。为了通用,模板一般用doc id区分配置类型,比如delivery和shop。
初始化数据不要手动敲。我一般会写一个initData云函数,第一次部署后跑一次,它把database/seed.json里的内容灌进集合。常见做法如下:
exports.main = async () => { const seed = require('./seed.json') for (const collectionName of Object.keys(seed)) { const items = seed[collectionName] for (const item of items) { await db.collection(collectionName).add({ data: item }) } } return { initialized: true } }这段代码每次执行前,需要确认集合已经创建。如果集合不存在,add会报collection not exists。所以跑数据初始化前,先到云开发控制台把categories、goods、config、orders四个集合建好。别指望模板自动建集合,大多数模板没有这个能力。
4.2 配送费、起送价和营业时间:三个直接影响利润的参数
餐饮外卖的利润,经常不是亏在菜品成本上,而是亏在配送费设置上。配送费太高没人下单,太低又覆盖不住运力成本。开箱即用模板通常会把配送参数集中在config集合的一张文档里,建议这样配:
const deliveryConfig = { _id: 'delivery', storeName: '模拟餐厅', storeNotice: '本店外卖仅支持 3 公里内配送', businessHoursStart: '10:00', // 开始营业时间,24小时制 businessHoursEnd: '20:30', // 结束营业时间 deliveryRadius: 3000, // 配送半径,单位米 deliveryFee: 3, // 每单基础配送费 freeThreshold: 30, // 满免配送费金额 selfPickupEnabled: true // 是否允许到店自取 }deliveryRadius是业务层面限制,不是硬限制。很多模板只在前端用地图判断了一次,真正兜底必须放到服务端判断。freeThreshold是最容易被忽略的参数,它直接影响客单价。我见过一个店把起送价设成 20,免配送费门槛设成 50,结果单均只有 21 元,每单都要贴配送费。这类配置要基于真实菜单价格算,不能拍脑袋。
营业时间建议由服务端判断,不要只在前端做展示。用户可以修改手机时间绕过前端判断,但不能绕过云函数。在placeOrder云函数中加一段:
const now = new Date() const currentMinutes = now.getHours() * 60 + now.getMinutes() const [startHour, startMinute] = '10:00'.split(':').map(Number) const [endHour, endMinute] = '20:30'.split(':').map(Number) const start = startHour * 60 + startMinute const end = endHour * 60 + endMinute if (currentMinutes < start || currentMinutes > end) { throw new Error('当前不在营业时间内,无法下单') }这段逻辑要放在读取完config之后、计算金额之前。把'10:00'和'20:30'改成从config读取的数据,不要硬编码,这样商家在管理后台改完营业时间后立即生效。另一个容易被忽略的边界是跨天打烊,比如凌晨 02:00 打烊,时间判断要跨天处理,模板里往往没做,需要自己补。
4.3 微信支付接入:三个必填参数与支付回调配置
支付是整个项目里最容易翻车的地方之一,因为有三个硬参数不能复制粘贴:微信支付商户号、商户 APIv3 密钥、商户证书序列号。三个参数错一个,支付都会下单失败。
开通路径是小程序后台关联微信支付商户号,或者直接注册一个新的商户号。然后到微信支付商户平台的“账户中心”里找到mchid,在“API 安全”里设置 APIv3 密钥,并下载商户证书。云开发环境里发起支付可以不用自己调证书,但商户号和密钥必须配置在云开发控制台的“微信支付”菜单里。你只需要填一次参数,云开发会把证书逻辑包装成云调用。
云开发发起支付的参数如下:
const payResult = await cloud.cloudPay.unifiedOrder({ body: '模拟餐厅-订单', // 商品描述,会展示在支付凭证里 outTradeNo: orderNo, // 商户订单号,必须唯一 spbillCreateIp: '127.0.0.1', // 终端 IP,支持字符串即可 subMchId: '1600000000', // 微信支付商户号 totalFee: totalFeeInCents, // 单位:分,整数 envId: 'diancan-3g1a2b', // 云开发环境 ID functionName: 'payCallback' // 支付结果回调云函数 })subMchId就是你的商户号。outTradeNo我习惯用时间戳拼随机数,比如20250413120000123。totalFee必须和订单金额一致,而且是整数分,比如 28 元就传2800。functionName是处理支付结果的云函数,支付成功后,微信异步把结果推给这个云函数。
支付回调里最容易犯的错是不校验金额就直接更新订单状态。正确姿势是:
const orderRes = await db.collection('orders') .where({ orderNo: outTradeNo }) .get() const order = orderRes.data[0] if (!order) throw new Error('订单不存在') if (order.totalFee * 100 !== totalFee) throw new Error('金额不一致') if (order.status === 0) { await db.collection('orders').doc(order._id).update({ data: { status: 1, payTime: db.serverDate() } }) }status === 0的判断很关键,它保证同一个支付通知不会被重复处理两次。支付回调是异步的,可能同一秒来多个通知,这个幂等判断能保住订单状态不被覆盖。支付配置的最后一环是证书序列号,在云开发控制台填写的证书序列号不是证书文件本身,而是一串字母数字。很多人在这一步把序列号和商户号搞混,导致一直报invalid cert,复制的时候要逐字符对上,不要手输。
5. 避坑指南:餐饮外卖小程序上线审核与真机运行的高频事故
5.1 类目与资质:功能全部正常,审核却打回
现象:代码已经可以在开发者工具里完整走通点餐和支付,高高兴兴提交审核,结果半天后收到一条“你的小程序涉及餐饮服务,需补充相关资质”的驳回。
原因:很多开箱即用模板在演示时用了“餐饮店场景”,但小程序主体如果没有添加“餐饮服务”类目,或业务逻辑里包含外卖配送,审核就可能要求上传《食品经营许可证》或相应门店资质。个人主体小程序尤其容易被卡,因为个人主体无法提供餐饮类资质。
解决:提交审核前,先到微信公众平台后台“设置-服务内容声明-小程序类目”里添加“餐饮服务”类目,并上传许可证。如果只是给一个门店做展示和到店自取点单,先用“餐饮服务-点餐”类目,不要碰“同城配送”相关字眼,避免引来更严格的审核。如果资质一时办不下来,就不要硬做外卖配送,把deliveryType固定为self,先跑通到店自取业务。
5.2 真机定位与授权:开发者工具正常,手机上一片空白
现象:在开发者工具里首页定位、周边门店都正常,预览到真机上页面一直加载中,或调用getLocation直接失败,控制台报getLocation:fail the api need to be declared in requiredPrivateInfos。
原因:这是微信隐私政策收紧后的经典翻车。开发者工具默认放开很多权限,真机必须在小程序后台声明使用位置信息的理由,同时代码里也要在app.json中声明requiredPrivateInfos。很多老模板只有permission没有requiredPrivateInfos,于是真机上连接口都进不去。
解决:在app.json中加上这段:
{ "requiredPrivateInfos": ["getLocation"], "permission": { "scope.userLocation": { "desc": "用于计算配送距离和展示附近门店" } } }与此同时,还要在小程序后台的“用户隐私保护指引”里勾选“位置信息”,并填写收集目的。前后台不一致,真机依然失败。这类问题不属于代码逻辑,而属于配置遗漏。排查顺序是:后台隐私指引、app.json、页面授权调用代码,按这个顺序查,基本五分钟内定位。
5.3 支付回调与超时关单:两个最容易丢钱的坑
现象:用户在微信里确确实实付了款,但订单列表仍显示“待支付”;另一伙人遇到用户没付款,订单却一直占着库存,其他顾客买不到菜。
原因:第一个现象通常是支付回调functionName配错,或者回调云函数内部报错没有被处理。第二个现象是模板里往往没有超时关单逻辑,待支付订单会永远挂在status: 0,造成虚假库存占用。
解决:支付回调要保证更新订单是幂等的,在payCallback中先查原订单,校验金额和当前状态,只允许从待支付变成已支付。超时关单用云函数定时触发器,每分钟扫一次待支付且创建时间超过 30 分钟的订单:
const expiredTime = new Date(Date.now() - 30 * 60 * 1000) const expiredOrders = await db.collection('orders') .where({ status: 0, createTime: _.lt(expiredTime) }) .get() for (const order of expiredOrders.data) { await db.collection('orders').doc(order._id).update({ data: { status: 5, closeReason: '超时未支付' } }) // 回补库存,避免占用 for (const item of order.goodsList) { await db.collection('goods').doc(item.id).update({ data: { stock: _.inc(item.count) } }) } }状态值5在这里代表已取消,具体用几要看模板里的状态机定义,不要想当然写成2或3。定时触发器里回补库存这步很容易漏,漏掉之后用户卡了 30 分钟的单,库存却被锁到第二天。
5.4 库存扣减与配送边界:超卖和“地图玄学”
现象:最后一个卤鸡腿同时被三个人下单,系统卖出去了三个鸡腿,实际库存只有一份,门店被迫取消两单。配送距离明明显示 3.2 公里超过范围,用户却仍能提交订单。
原因:库存扣减用了“先查询再更新”的写法,两个用户同时查到剩余 1,同时去更新,导致超卖。配送边界如果只在前端用两点之间直线距离算,和实际骑行路线差了很远,“地图玄学”就是这么来的。
解决:库存扣减必须用原子更新,把“检查库存充足”和“扣减库存”合并到一个条件里。云开发可以这样操作:
const updateRes = await db.collection('goods') .where({ _id: item.id, stock: _.gte(item.count), // 要求库存足够才执行更新 onsale: true }) .update({ data: { stock: _.inc(-item.count) } }) if (updateRes.stats.updated === 0) { throw new Error(`${item.name} 库存不足`) }配送边界则建议使用地图服务商提供的路线规划或距离计算 API,而不是直接拿 GPS 点对点算直线距离。直线距离 3 公里,实际骑行距离往往在 3.5 到 4.5 公里之间,所以不能直接拿两点坐标当配送距离。至少要在服务端用地图服务的坐标距离接口计算,或者用一个保守的换算系数估算。服务端计算的另一个好处是,接口拿到的坐标来源可控,能防止用户伪造经纬度绕过配送范围。
5.5 订单状态混乱:没有状态机,退款和配送互相打架
现象:商家已经出餐并点击开始配送,用户同时申请取消,结果前端把订单状态直接置成“已取消”,但骑手已经取餐在路上,最后门店白送一单。
原因:页面各自为政,每个页面都直接update订单状态,没有统一的流转校验。这类问题通常不在首次上线时暴露,而是在运营两周后,客服开始接到退款和配送冲突的投诉时,才意识到状态乱了。
解决:把所有状态变更收口到一个updateOrderStatus云函数里,只允许下面这些流转:
| 当前状态 | 下一状态 | 触发方 |
|---|---|---|
| 0 待支付 | 1 已支付 / 5 已取消 | 支付回调 / 超时关单 |
| 1 已支付 | 2 制作中 / 5 已取消 | 商家确认 / 用户退款 |
| 2 制作中 | 3 配送中 / 6 退款中 | 商家出餐 / 用户申请退款 |
| 3 配送中 | 4 已完成 | 用户确认收货 |
| 6 退款中 | 5 已取消 | 商家同意退款 |
状态机不复杂,难在所有人都遵守它。把状态定义放在一个常量文件里,云函数内部也读这个文件,前端展示时根据状态码翻译文案,不要在业务代码里出现裸的status = 2,否则改需求时满屏都是魔法数字,早晚踩坑。
6. 再往前走一步:用每日订单报表反推菜单,而不是急着加功能
到上一步,小程序已经能正常营业,但“开箱即用”的模板往往不带数据分析。作为一线交付习惯,我会在第二周就给它补一个报表云函数,因为餐饮外卖的核心不是功能多,而是知道每天哪些菜在赚钱、哪些菜在做无用功。
一个很实用的技巧是写一个dailyReport云函数,对当天订单按菜品维度做聚合:
const _ = db.command const today = new Date() const start = new Date(`${today.getFullYear()}-${today.getMonth()+1}-${today.getDate()} 00:00:00`) const orders = await db.collection('orders') .where({ status: _.in([4, 6]), createTime: _.gte(start) }) .get() const salesMap = {} for (const order of orders.data) { for (const item of order.goodsList) { if (!salesMap[item.id]) { salesMap[item.id] = { count: 0, amount: 0 } } const goods = await db.collection('goods').doc(item.id).get() salesMap[item.id].count += item.count salesMap[item.id].amount += item.count * goods.data.price } } return salesMap你可以把它挂到一个云函数定时触发器,每天 23:50 运行一次,把结果写入集合。第二天打开后台就能看到“昨日招牌黄焖鸡卖出 80 份、折扣区只出 3 单”这样的数据,而不是靠感觉决定下一个上架什么菜。
我的习惯是:每一次迭代都先看这张表,再决定改菜品搭配、调价格还是优化配送费门槛。功能上多一个秒杀弹窗,不如把销量垫底的菜下架换掉。希望帮到你。
本文还有配套的精品资源,点击获取