简介这套微信小程序餐饮点餐外卖项目源码包面向具备基础前端知识、想快速上手小程序开发的初中级开发者也适合餐饮商家技术团队参考。包内包含小程序前端页面、逻辑、样式及配置文件共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.jsApp({ onLaunch() { if (!wx.cloud) { console.error(当前微信基础库版本过低请升级后重试) return } wx.cloud.init({ env: diancan-3g1a2b, // 云开发环境 ID注意不是环境名称 traceUser: true // 追踪用户访问用于控制台看访问数据 }) } })env参数写环境 ID而不是你在控制台里起的那个中文名称。traceUser建议开着它能让云开发控制台按用户维度统计访问量排查问题时很好用。云函数内部的初始化却不太一样常见写法是cloud.DYNAMIC_CURRENT_ENV意思是当前云函数属于哪个环境就自动使用哪个环境不需要硬编码环境 IDconst 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}fromprepay }) })这里把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 单”这样的数据而不是靠感觉决定下一个上架什么菜。我的习惯是每一次迭代都先看这张表再决定改菜品搭配、调价格还是优化配送费门槛。功能上多一个秒杀弹窗不如把销量垫底的菜下架换掉。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?