简介面向餐饮行业的微信小程序点餐外卖完整源码开箱即用主打在线点餐、外卖配送、叫号排队与微信支付等场景适合餐饮商家、小程序开发团队及有前端基础的开发者快速落地或二次开发。资源包共521个文件仅833KB以JavaScript逻辑、WXML页面结构、WXSS样式、JSON配置为主要组成并包含WXS辅助脚本、PNG图标及说明文档目录划分明确便于定位修改。功能上覆盖菜品展示、购物车、订单管理、用户注册登录、配送范围计算、排队叫号、支付对接以及基础数据统计形成相对完整的轻量餐饮小程序业务流程。已有2331人学习浏览对于想学习微信小程序整体架构或希望定制餐饮应用场景的开发者是一份值得参考的源代码。1. 微信小程序餐饮点餐外卖一个订单模型打穿堂食、自取和外卖三种场景做餐饮小程序最容易被误导的一点是以为「外卖」和「堂食」要写两套逻辑。实际拆过这份开箱即用的微信小程序餐饮点餐外卖源码后你会发现真正核心的就是一条订单链路选菜 → 购物车 → 下单 → 支付 → 出餐/配送堂食扫码、到店自取、外卖配送只是订单上的一个类型字段。项目用的是微信小程序原生语法加云开发没有自建服务器拿到源码改一改 AppID 就能跑通全流程。适合个人开发者做二次开发交活也适合餐饮商户快速上线一套能用的点餐商城。2. 总体架构原生小程序 云开发目录结构与数据模型先看清2.1 技术栈选型与整体分层这套源码的工程结构是典型的「小程序前端 云函数 云数据库」三层。前端没有用 uni-app 或 Taro 这类跨端框架而是直接用微信小程序原生 WXML / WXSS / JavaScript 写的好处是零编译负担微信开发者工具里导入就能跑排查问题也直接看原生堆栈。后端逻辑集中在 cloudfunctions 目录下的云函数里云数据库负责持久化。为什么「开箱即用」多半会选云开发而不是自建 Node 服务核心原因是省掉了三件事域名备案、HTTPS 证书配置、服务器部署。云函数本身就是 HTTP 接口的替代品小程序端通过 wx.cloud.callFunction 直接调用不走 URL 请求也就没有「合法域名校验」这一关。我之前接过一个外卖类外包项目自建后端光调 TLS 证书和服务器备案就花了两天这类用云开发的源码拿过来当天就能把下单链路跑通体验差距确实明显。2.2 目录结构前端页面和云函数怎么对应拿到源码后第一件事应该是看目录而不是急着点编译。一份合格的餐饮点餐小程序目录大概长这样miniprogram/ ├── app.js # 小程序入口初始化云开发环境 ├── app.json # 页面路由与窗口配置 ├── pages/ │ ├── index/ # 首页店铺信息、轮播图、公告 │ ├── menu/ # 点餐页分类切换、菜品列表、加购 │ ├── cart/ # 购物车页数量调整、金额计算 │ ├── checkout/ # 确认订单页配送方式、地址、备注 │ ├── order/ # 订单列表状态筛选、订单详情 │ └── profile/ # 我的登录信息、地址管理 ├── components/ │ ├── product-card/ # 菜品卡片组件 │ ├── cart-bar/ # 底部购物车栏组件 │ └── order-status/ # 订单状态标签组件 ├── utils/ │ ├── request.js # 云函数调用封装 │ ├── cart.js # 本地购物车缓存管理 │ └── format.wxs # 金额、时间格式化 └── package.json cloudfunctions/ ├── login/ # 登录获取 openid创建用户记录 ├── product/ # 菜品查询分类 菜品列表 ├── cart/ # 购物车校验服务端金额重算 ├── order/ # 下单创建订单、扣减库存 ├── pay/ # 支付统一下单并返回支付参数 └── orderStatus/ # 状态流转接单、出餐、配送完成目录里有两个设计细节值得注意。一是购物车没有做云数据库持久化而是落到本地 Storage这个后面会细讲二是每个云函数独立成目录意味着它们可以单独部署单独更新而不需要整包上传。我在改造时会把 order 和 pay 拆开调用因为支付云函数通常要配更长的超时时间跟普通查询函数放一起反而容易互相拖累。2.3 数据库集合设计与订单状态机餐饮点餐商城的数据模型核心就是「店铺 - 分类 - 菜品 - 订单 - 用户」这五张表。云数据库里每个集合对应一个 JSON 文件这套源码的初始化数据大概包含集合名关键字段说明shopname, logo, notice, deliveryFee店铺基本信息首页加载categoryname, sort菜品分类如热菜、凉菜、饮品productname, price, image, categoryId, stock菜品上下架与库存orderorderNo, items, totalFee, status, type订单主表含配送方式addressuserId, name, phone, detail用户收货地址useropenid, nickname, avatar用户信息登录时创建订单状态是这个项目的灵魂。源码里订单状态不是字符串散着存的而是定义了一套状态机0 待支付 → 1 已支付/待接单 → 2 制作中 → 3 配送中/待自取 → 4 已完成外加-1 已取消作为异常分支。每次状态变更都走同一个云函数前端拿到状态码后映射成不同文案和按钮。这个设计在二次开发时省了大功夫。比如你想加一个「退款」状态不需要每个页面改条件判断只需要在 orderStatus 云函数里加一个分支同时在订单状态组件里补一套对应样式即可。状态机的字段命名也统一order 集合里 type 字段区分1 堂食、2 自取、3 外卖三种经营模式的差异被压缩到一个字段里。3. 核心模块拆解从点餐到订单完成的一条完整链路3.1 点餐页的菜品加载分类联动与列表渲染点餐页是用户接触最多的页面源码里的实现分成三步加载分类 → 加载菜品 → 分类和菜品联动。数据来源是 product 云函数它会根据传入的 categoryId 或分页参数返回对应数据。// cloudfunctions/product/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() const _ db.command exports.main async (event) { const { categoryId, page 1, pageSize 20 } event const where {} if (categoryId) where.categoryId categoryId where.status _.neq(0) // 过滤下架菜品status 为 0 表示下架 const countResult await db.collection(product).where(where).count() const listResult await db.collection(product) .where(where) .skip((page - 1) * pageSize) .limit(pageSize) .get() return { list: listResult.data, total: countResult.total, hasMore: page * pageSize countResult.total } }这段逻辑里两个细节值得注意。第一status: _.neq(0)用的是数据库指令意思是只查 status 不等于 0 的记录这种写法比前端拿到全部数据再过滤要高效得多菜品上千条时差距尤其明显。第二skip limit做的是分页查询hasMore字段用来控制前端触底加载避免一次拉全量数据导致首屏白屏。小程序端在 menu 页的 onLoad 里先调分类接口拿到分类列表后默认选中第一个再调菜品接口。切换分类时前端不会重新拉整个列表而是先判断该分类之前是否加载过加载过就直接展示缓存数据没有才重新请求。这个「分类级缓存」的做法很实用用户在几个分类间来回切换时几乎感觉不到延迟。3.2 购物车本地缓存为主服务端下单时重算金额购物车这段代码值得抄。它没有把购物车数据存进云数据库而是放在小程序本地 Storagekey 为 cart_ 加上用户 openid 后几位。原因很简单购物车是高频读写、低频结算的数据每次加减菜品都写数据库不仅慢而且费用高存本地则秒开秒改。// utils/cart.js 购物车管理核心逻辑 const CART_KEY cart_data function getCart() { return wx.getStorageSync(CART_KEY) || [] } function addToCart(product) { const cart getCart() const index cart.findIndex(item item.productId product._id) if (index -1) { cart[index].count 1 } else { cart.push({ productId: product._id, name: product.name, price: product.price, image: product.image, count: 1, stock: product.stock }) } wx.setStorageSync(CART_KEY, cart) return cart } function getTotalFee() { const cart getCart() return cart.reduce((sum, item) sum item.price * item.count, 0) } module.exports { getCart, addToCart, getTotalFee }购物车项里没有单独存「小计金额」因为金额随时会变存了还得维护同步逻辑。每次 getTotalFee 时实时计算价格以服务端下单时重算的结果为准本地金额只是展示用。这里的隐患是「本地购物车里的价格是旧的」。如果商家后台改了菜品价格用户购物车还显示旧价结算时就会出问题。这套源码的解法是下单时把购物车全量传给 order 云函数服务端逐条按数据库当前价格重算总金额不以客户端提交的金额为准。这条是关键防线改后端时千万不要省否则用户改一下请求参数就能以低价下单。3.3 下单链路订单生成 → 支付参数 → 状态流转下单是整条链路里最重的环节源码把它拆成了三个云函数order 负责创建订单pay 负责返回支付参数orderStatus 负责后续状态流转。这样拆是为了超时控制——创建订单可能要查多张表支付要调微信支付接口两者对时长的要求完全不同。// cloudfunctions/order/index.js 创建订单核心逻辑 const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const { wxContext } cloud.getWXContext() const openid wxContext.OPENID const { items, address, type 3, remark } event // 服务端重算金额 let totalFee 0 for (const item of items) { const prod await db.collection(product).doc(item.productId).get() if (!prod.data || prod.data.status 0) { return { code: -1, msg: 菜品${item.name}已下架 } } const fee prod.data.price * item.count totalFee fee // 扣减库存库存不足时回滚 const stockRes await db.collection(product) .where({ _id: item.productId, stock: _.gte(item.count) }) .update({ data: { stock: _.inc(-item.count) } }) if (stockRes.stats.updated 0) { return { code: -1, msg: 菜品${item.name}库存不足 } } } // 生成订单号并落库 const orderNo D Date.now() Math.floor(Math.random() * 1000) const orderData { orderNo, openid, items, address, type, remark, status: 0, totalFee, createTime: Date.now() } const addRes await db.collection(order).add({ data: orderData }) return { code: 0, orderId: addRes._id, totalFee, orderNo } }这段代码里有两个容易翻车的点。第一库存扣减用的是条件更新where({ _id: item.productId, stock: _.gte(item.count) })只有当库存大于等于购买数量时才执行stock: _.inc(-item.count)如果更新影响行数为 0说明库存不够直接返回失败。这套写法比「先查库存再扣减」要安全因为查和扣之间可能有其他请求并发插入。第二订单状态初始为 0待支付此时订单已经落库。如果用户中途放弃支付会留下大量待支付订单所以源码里还有一个定时清理逻辑把超过 30 分钟未支付的订单状态置为 -1同时把库存加回去。这个兜底逻辑看着不起眼实际运营时能省很多客服扯皮。支付云函数的逻辑是拿到前面生成的 orderId调微信支付的云调用接口把 totalFee 转成分传给微信支付返回给小程序的是一组支付参数。// cloudfunctions/pay/index.js 支付参数生成 const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main async (event) { const { wxContext } cloud.getWXContext() const openid wxContext.OPENID const { orderId, totalFee } event const res await cloud.cloudPay.unifiedOrder({ body: 餐饮点餐订单, outTradeNo: orderId _ Date.now(), spbillCreateIp: 127.0.0.1, subMchId: 你的商户号, totalFee, // 单位是分前端传过来前要先 *100 envId: 你的云环境ID, functionName: payCallback // 支付结果回调的云函数名 }) return { code: 0, payment: res.payment // 包含 timeStamp、nonceStr、package、signType、paySign } }小程序端拿到 payment 后调用wx.requestPayment拉起收银台。支付完成后微信服务器会触发 payCallback 云函数里面把订单状态从 0 改成 1。需要注意 totalFee 单位是分前端金额如果是元一定要乘 100这个坑我在自己的项目里踩过支付金额差 100 倍用户付款后对不上账排查半天才发现是单位问题。3.4 堂食、自取、外卖三种模式怎么用一套代码兼容这一个设计是源码里最亮眼的部分。确认订单页有三个 tab堂食、自取、外卖。切换 tab 不会跳转页面只是改变 order 集合里的 type 字段。type 1 堂食需要选择桌号或扫码进入时已绑定不需要配送地址type 2 自取需要选择自取时间不需要配送地址type 3 外卖需要选择或新建收货地址计算配送费。下单时前端根据 type 决定是否校验 address 字段订单列表页则根据 type 显示不同文案——堂食显示「桌号 12 号」自取显示「到店自取」外卖显示具体的配送地址。后端订单表完全不用变只是查询时多一个 type 筛选条件。我当初改造时把堂食和外卖做成了两个独立页面结果维护成本翻倍每个状态都要改两处。后来看到一个朋友的项目直接从这个源码改把两种模式收归一套页面才意识到这个「一个订单模型打穿三种场景」的思路有多省事。二次开发时如果遇到「要加一种新的履约方式」优先考虑加 type 枚举值而不是复制页面。4. 部署运行开箱即用不等于零配置关键步骤逐项过4.1 导入项目和配置 AppID「开箱即用」指的是代码完整、依赖齐全但微信小程序必须有 AppID 才能真机预览和调用云开发。拿到源码后先在微信公众平台注册一个小程序账号类型选个人或企业都可以然后在开发者工具里用该账号的 AppID 导入项目。导入时有两点要注意。第一语言框架选「JavaScript」不用选 TypeScript 模板源码是原生 JS 写的选错会导致编译报错。第二package.json 文件里如果有依赖需要在工具里执行「工具 - 构建 npm」虽然这套源码的云函数端依赖很少但保险起见还是构建一次否则有时会出现找不到模块的玄学问题。导入成功后会看到模拟器里出现首页。这时不用急着预览直接看 Console 面板大概率会报「cloud init 失败」之类的错因为云开发环境还没开通。4.2 开通云开发环境导入数据库集合部署云函数这一节是跑通全流程的核心我按顺序拆开讲。第一步在开发者工具顶部点击「云开发」按钮开通云开发创建一个新环境。环境 ID 会生成一串类似xxx-1a2b3c的字符串记下来后面要填。第二步在app.js里找到wx.cloud.init的调用把env字段的值改成你自己的环境 ID。// app.js 开头部分 App({ onLaunch() { if (!wx.cloud) { console.error(当前基础库过低请使用 2.2.3 以上版本) return } wx.cloud.init({ env: 你的环境ID, // 改成你自己的云环境 ID traceUser: true }) } })第三步在云开发控制台里创建数据库集合集合名要和代码里对应shop、category、product、order、address、user。创建好后把源码 database 目录下的 JSON 文件导入对应集合JSON 里是初始的店铺信息、分类、菜品数据。导入时注意选择「覆盖」模式避免重复导入产生多余记录。第四步部署云函数。在开发者工具的资源管理器中找到 cloudfunctions 目录对每个子目录右键选择「上传并部署云端安装依赖」。这一步会把云函数代码连同 node_modules 一起上传到云端每个函数都要单独部署一次。全部部署完成后在模拟器里重新编译首页应该能正常显示店铺信息和菜品列表了。此时点「加购」再「去结算」如果能正常下单说明整条链路已经通了。我第一次跑这套流程时容易漏掉某个云函数没部署导致点某个按钮报「FunctionName 不合法」后来就养成习惯每部署一个函数就刷新一下控制台看云函数列表是否全亮。4.3 真机预览与体验版发布的几个配置项模拟器跑通之后真机预览会遇到两件模拟器没有的事。一是真机预览要求基础库版本不低于 2.2.3 或更高老版本手机上可能白屏需要在 app.json 里检查requiredBackgroundModes和sitemapLocation等配置项是否兼容。二是云支付需要商户号pay 云函数里的subMchId和envId必须换成自己申请的微信支付商户号。如果是个人小程序无法申请微信支付这时可以注释掉支付环节把订单状态直接置为 1用「货到付款」模式跑通全流程。这也是这个源码比较友好的地方支付被封装成独立云函数去掉它不会影响其他模块。体验版发布前还建议做一次「不校验合法域名」的确认在开发者工具详情 - 本地设置里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。因为云开发调用不走 URL一般不会触发域名校验但如果你改造时引入了外部图片资源或 web-view 组件就必须在公众平台配置 downloadFile 合法域名否则真机上图片会裂这是餐饮小程序最容易出现的线上事故。5. 常见问题排查云函数超时、支付回调异常、数据不同步的血泪经验5.1 云函数执行超时导致下单失败现象点击「提交订单」转圈很久最后提示「云函数调用超时」但订单库里又没有新记录。原因云函数默认超时时间是 3 秒order 云函数里如果菜品数量多、循环查库耗时太长很容易超过这个阈值。另外如果云函数运行环境是低配置的免费套餐冷启动本身就会占掉不少时间。解决进入云开发控制台 - 云函数 - order - 配置把超时时间调大到 10 秒甚至 20 秒。同时优化代码把循环单查doc(item.productId).get()改成一次where批量查回所有菜品再在内存里做匹配减少数据库调用次数。这个优化很值得做我在一个菜单 50 个菜品的项目里实测批量查询后耗时缩短到原来的四分之一。5.2 本地购物车价格与数据库不一致现象用户加购后商家后台改了价格用户结算时前端显示旧价支付金额实际按新价算用户投诉「金额变了」。原因购物车在本地缓存时把 price 字段也存了下来前端计算金额直接用缓存价而服务端下单时按数据库重算两边不一致。解决两个方向同时做。前端在下单前调用 product 云函数把购物车里所有 productId 传过去服务端返回这批菜品的最新价格和上架状态前端拿到后先刷新购物车再让用户确认。另外在下单云函数里保持「以服务端重算金额为准」的硬逻辑即使前端传来的是旧价后端也不接受。这样用户看到的价格和实际支付金额就统一了。5.3 支付成功后订单状态没变现象用户微信支付扣款成功但订单列表里还是「待支付」订单状态一直是 0。原因支付回调云函数没有正确拿到 openid。常见错误写法是在云函数里用前端传的 openid 查订单而正确做法是回调云函数通过cloud.getWXContext()拿上下文里的 OPENID因为支付回调是微信服务器触发的没有前端身份。// payCallback/index.js 正确取 openid 的方式 exports.main async (event) { const { wxContext } cloud.getWXContext() const openid wxContext.OPENID // 从这里取不要用 event.openid // 根据 openid 和 outTradeNo 更新订单状态 const res await db.collection(order) .where({ orderNo: event.outTradeNo }) .update({ data: { status: 1, payTime: Date.now() } }) return res }解决统一从wxContext.OPENID取用户身份不要信任前端传值。这类问题最容易出现在「复制粘贴代码时把别的项目的回调逻辑搬过来」openid 来源不同排查时要先看cloud.getWXContext()有没有被正确调用。5.4 菜品图片在真机上显示不全现象模拟器里菜品图正常真机上有的图片裂了有的加载很慢。原因图片资源存放在云存储里但云存储的权限默认是「仅创建者可读写」。如果菜品图片是运营后台上传的用户端读取时没有权限自然裂图。另一种情况是图片用了外部链接但没有配 downloadFile 合法域名。解决去云开发控制台 - 存储 - 权限设置把存储权限改为「所有用户可读仅创建者可读写」同时把图片链接改成云存储的 fileID 格式cloud://开头这样小程序端可以直接渲染。真机上所有图片都必须走合法域名或云存储这是餐饮项目最容易掉坑的地方我自己上线时也因为这个被审核打回过一次。5.5 用户重新打开小程序后登录态失效现象用户隔几天再打开小程序购物车还在但点「我的」时头像昵称变成空白或者下单时提示「请先登录」。原因登录逻辑只在onLaunch时执行一次而wx.login返回的 code 换 openid 是有时效的。用户切后台超过一定时间登录态静默过期但前端没有重新触发登录。解决把登录云函数改成「按需调用 缓存刷新」。在 app.js 里维护一个isLogin标志每次启动先检查 user 集合里有没有当前 openid 的记录没有或信息不完整就重新调 login 云函数。我还见过这个源码的改法在 checkout 页面onShow里强制检查登录态未登录就拉起登录流程确保用户在下单前一定是登录状态这样支付回调才能正确关联用户。6. 二次开发实战给订单加上「优惠券抵扣」的完整改造思路改造一个项目最好的切入点是挑一个不影响核心链路、又能完整走一遍「前端页面 → 本地状态 → 云函数 → 数据落库」的功能。这里以「优惠券抵扣」为例讲清楚从哪下手。第一步给 order 集合加两个字段couponId 和 discountFee。couponId 记录用户用哪张券discountFee 记录抵扣金额。然后加一个 coupon 集合字段包含 userId、amount、threshold满减门槛、expireTime、status0 未用 1 已用。第二步在用户端「我的」页面加一个优惠券入口列表展示可用券和不可用券。这里可以复用 order-status 组件改造成券状态标签不用新写组件。第三步改 checkout 页面。用户选完菜品后展示可用券列表选择一张券前端计算应付金额payFee totalFee - discountFee同时把这张券的 id 和抵扣金额存到临时变量里。第四步改下单云函数。这是最关键的地方服务端必须重新校验券是否可用不能信前端传的金额。// order/index.js 增加优惠券校验与抵扣 // 在服务端重算 totalFee 之后 let payFee totalFee let selectedCoupon null if (event.couponId) { const couponRes await db.collection(coupon).doc(event.couponId).get() const coupon couponRes.data // 校验券归属、状态、有效期、满减门槛 if (coupon.userId ! openid || coupon.status ! 0) { return { code: -1, msg: 优惠券不可用 } } if (coupon.expireTime Date.now()) { return { code: -1, msg: 优惠券已过期 } } if (totalFee coupon.threshold) { return { code: -1, msg: 未满足满${coupon.threshold}元使用条件 } } payFee totalFee - coupon.amount selectedCoupon coupon } // 订单落库时用 payFee 作为应付金额并记录优惠信息注意几点优惠券状态改为已用不能只在创建订单时改要在支付成功回调里改防止用户下单后不支付占着券如果订单超时取消券状态要回滚为未用。所以第五步是改 payCallback 和定时清理逻辑payCallback 里把 couponId 对应的券 status 置为 1超时取消订单时把 status 改回 0。我改造完这个功能后习惯性地把所有涉及金额的云函数都过了一遍「服务端重算」的检查从那以后我每次给餐饮项目加优惠、加折扣、加运费减免都强制走一遍「先看服务端有没有按自己的逻辑重算金额」的流程而不是只改前端。这种东西用户可能感知不到但少了这一步上线后一定会有对不上账的一天。希望这篇拆解能帮你在二次开发时少踩几个类似的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?