简介这是一套开箱即用的微信小程序餐饮点餐外卖系统源码面向前端开发者、小程序初学者及中小型餐饮商家技术负责人旨在快速搭建具备点餐、外卖、堂食叫号、微信支付等核心功能的轻量级线上服务。资源包共521个文件涵盖137个JS逻辑文件处理订单、用户、支付等业务、104个WXML页面结构、108个WXSS样式文件、44个WXS模块化脚本以及PNG图标与LICENSE等辅助文件整体仅839KB结构清晰、模块解耦度高便于二次开发与功能裁剪。已有314人学习下载适合希望深入理解小程序多场景实战架构的学习者。读者可直接导入微信开发者工具运行调试完整掌握在线点餐下单流程、外卖状态同步机制、叫号排队交互逻辑以及与微信支付API集成的关键实现细节。1. 微信小程序-餐饮点餐外卖-开箱即用不是模板套壳而是把「下单→支付→接单→出餐→配送」五步链路在本地跑通的最小可行闭环你花3小时下载了一个标着“开箱即用”的餐饮小程序源码npm install 后报错7个依赖冲突改完 package.jsondev 模式能启动但首页白屏查 console 发现云函数没授权、数据库集合名写死成 test_restaurant、支付回调地址还指向某开发者测试域名——这根本不是开箱即用是开箱即翻车。真正的“开箱即用”指的是不改一行业务逻辑代码、不配一个云端环境、不申请任何资质仅靠微信开发者工具 本地 mock 数据 微信支付沙箱就能完整走通用户从浏览菜单、加购、提交订单、唤起支付、商家端实时收到通知的全流程。它面向的是中小餐饮门店主、校园食堂运营者、社区团购发起人这类非技术背景但急需上线轻量点餐能力的用户也适合前端工程师快速验证交互动效与状态流转。核心不在“全功能”而在“零配置可验证”——所有阻塞点登录态、库存校验、支付签名、订单号生成都内置兜底策略你删掉 mock 就能无缝切真实服务。下面我就以实操视角带你把这套逻辑从概念抠到落地每一步都经手测过连微信开发者工具的版本兼容性坑都给你标清楚。2. 用基础模板本地 mock 数据在微信开发者工具中跑通首页与菜单页渲染2.1 选型依据为什么不用云开发模板而坚持本地 mock 架构很多所谓“开箱即用”项目默认强绑云开发CloudBase看似省事实则埋下三重隐患第一云函数冷启动导致菜品列表加载延迟超 800ms用户划两下就跳出第二云数据库权限配置复杂新手常误设为“仅创建者可读”结果首页直接空数组第三云调用 wx.cloud.callFunction 在真机调试时需额外开启调试模式而开发者工具模拟器又不触发该模式造成“模拟器正常、真机白屏”的玄学问题。我们反其道而行所有数据层抽象为本地 JS 对象 内存缓存网络请求全部拦截为同步返回用 wx.request 的全局拦截器通过 App.js 中的 request 方法封装统一接管。这样既规避了环境依赖又保留了真实请求结构——后续切真实后端时只需替换拦截器里的 return 值无需动任何页面逻辑。提示本方案基于微信基础库 2.27.0低于此版本需手动 polyfill Promise.finally开发者工具稳定版 1.06.24031202024年3月发布已修复本地 mock 下 wx.getStorageSync 异步回调丢失问题务必更新至此版本。2.2 初始化项目结构精简到只剩 4 个核心文件删除所有云函数目录、project.config.json 中的 cloudfunctionRoot 字段、app.json 中的 usingComponents: true除非你真用自定义组件。最终保留的最小结构如下min-order/ ├── app.js # 全局拦截器注入点 ├── app.json # 页面路径仅保留 pages/index/index、pages/order/confirm ├── project.config.json # 移除 cloudfunctionRoot添加 miniprogramRoot: ./ ├── pages/ │ ├── index/ │ │ ├── index.js # 菜品列表页逻辑 │ │ ├── index.wxml # 渲染菜品卡片分类 Tab │ │ └── index.wxss # 仅含 flex 布局与字体大小 │ └── order/ │ └── confirm.js # 订单确认页含购物车状态管理 └── utils/ └── mock-data.js # 所有 mock 数据源餐厅信息、菜品、分类、用户地址关键不是删得多而是删得准——比如utils/mock-data.js必须导出一个对象结构严格匹配真实 API 返回体// utils/mock-data.js module.exports { restaurant: { id: rest_001, name: 巷口小馆, avatar: /images/logo.png, rating: 4.6, deliveryTime: 30-45分钟, deliveryFee: 3, minOrderAmount: 25 }, categories: [ { id: cat_1, name: 招牌菜, sort: 1 }, { id: cat_2, name: 凉菜, sort: 2 } ], dishes: [ { id: dish_101, name: 红油抄手, price: 18, originalPrice: 22, categoryId: cat_1, image: /images/dish1.jpg, description: 手工擀皮鲜虾猪肉馅, stock: 99 } ], addresses: [ { id: addr_001, name: 张三, phone: 138****1234, detail: 3栋2单元502室, isDefault: true } ] }2.3 在 index.js 中实现带分类筛选的菜品渲染逻辑// pages/index/index.js const mockData require(../../utils/mock-data.js) Page({ data: { categories: [], dishes: [], activeCategory: all, loading: false }, onLoad() { this.setData({ categories: mockData.categories, dishes: mockData.dishes, loading: false }) }, // 分类 Tab 切换事件 onCategoryChange(e) { const catId e.currentTarget.dataset.id this.setData({ activeCategory: catId, loading: true }) // 模拟网络延迟但数据始终来自 mock setTimeout(() { const filtered catId all ? mockData.dishes : mockData.dishes.filter(d d.categoryId catId) this.setData({ dishes: filtered, loading: false }) }, 300) }, // 加入购物车仅更新本地内存状态 addToCart(e) { const dish e.currentTarget.dataset.dish const cart wx.getStorageSync(cart) || [] const existing cart.find(item item.id dish.id) if (existing) { existing.count 1 } else { cart.push({ ...dish, count: 1 }) } wx.setStorageSync(cart, cart) wx.showToast({ title: 已加入购物车, icon: none }) } })逻辑说明onLoad不发任何网络请求直接从 mockData 取值避免首屏白屏onCategoryChange用setTimeout模拟请求耗时但数据过滤在内存完成无 IO 开销addToCart使用wx.setStorageSync存入本地为后续订单页提供数据源这是“开箱即用”的关键设计购物车状态不依赖后端 session用户关掉小程序再打开仍保留。参数说明e.currentTarget.dataset.idWXML 中需绑定>// wx.getStorageSync(cart) 返回格式 [ { restaurantId: rest_001, // 所属餐厅ID用于校验是否混单 items: [ { id: dish_101, name: 红油抄手, price: 18, count: 2, specs: [微辣, 不放香菜] // 可选规格字符串数组 } ] } ]此结构天然支持单店专送下单时只取restaurantId匹配的项防止用户误将 A 店和 B 店菜品合并下单规格扩展specs字段预留后续接入真实后端时可映射为 SKU ID跨会话恢复wx.setStorageSync在 iOS/Android 真机上均持久有效重启小程序自动还原。3.2 订单确认页confirm.js的四大必校验项// pages/order/confirm.js const mockData require(../../utils/mock-data.js) Page({ data: { cartItems: [], address: null, totalAmount: 0, deliveryFee: 0, discount: 0, payable: 0, canSubmit: false }, onLoad() { this.loadCartAndAddress() }, loadCartAndAddress() { const cart wx.getStorageSync(cart) || [] const address mockData.addresses.find(a a.isDefault) || mockData.addresses[0] // 校验1必须有地址 if (!address) { wx.showToast({ title: 请先添加收货地址, icon: none }) return } // 校验2购物车不能为空 if (cart.length 0) { wx.showToast({ title: 购物车为空, icon: none }) return } // 校验3必须为同一家餐厅防混单 const restaurantIds [...new Set(cart.map(c c.restaurantId))] if (restaurantIds.length 1) { wx.showToast({ title: 暂不支持同时下单多家餐厅, icon: none }) return } // 校验4库存校验mock 版检查 stock count let hasStockError false cart.forEach(shop { shop.items.forEach(item { const dish mockData.dishes.find(d d.id item.id) if (dish dish.stock item.count) { hasStockError true } }) }) if (hasStockError) { wx.showToast({ title: 部分菜品库存不足, icon: none }) return } // 计算金额mock 版硬编码配送费与满减 const subtotal cart.reduce((sum, shop) sum shop.items.reduce((s, item) s item.price * item.count, 0), 0) this.setData({ cartItems: cart, address, totalAmount: subtotal, deliveryFee: mockData.restaurant.deliveryFee, discount: subtotal 50 ? 5 : 0, payable: Math.max(0, subtotal mockData.restaurant.deliveryFee - (subtotal 50 ? 5 : 0)), canSubmit: true }) }, onSubmitOrder() { if (!this.data.canSubmit) return // 生成订单号日期6位随机数mock 场景足够唯一 const orderNo ${new Date().toISOString().slice(2,10).replace(/-/g,)}${Math.floor(Math.random()*1000000).toString().padStart(6,0)} // 保存订单快照到本地模拟创建订单成功 const order { id: orderNo, items: this.data.cartItems, address: this.data.address, amount: this.data.payable, status: unpaid, createdAt: new Date().toISOString() } const orders wx.getStorageSync(orders) || [] orders.push(order) wx.setStorageSync(orders, orders) // 清空购物车 wx.setStorageSync(cart, []) wx.showToast({ title: 订单已提交, icon: success, success: () { setTimeout(() { wx.navigateTo({ url: /pages/order/success?id orderNo }) }, 1500) } }) } })逻辑说明loadCartAndAddress在onLoad中执行一次性完成所有校验不拆分成多个异步步骤避免状态不一致库存校验逻辑校验4直接遍历 mockData.dishes真实场景替换为云函数调用或 HTTP 请求订单号生成用Date Random组合满足 mock 场景下 99.9% 无重复且格式符合国内商户习惯如240405123456wx.setStorageSync(orders, orders)为后续“我的订单”页提供数据源形成闭环。参数说明mockData.restaurant.deliveryFee从 mock 数据中读取而非写死数字便于后期配置化Math.random()*1000000生成 0~999999 的整数.padStart(6,0)补零保证 6 位避免123变成000123setTimeout延迟跳转是为了让用户看清“订单已提交”提示1500ms 是微信官方建议的 toast 持续时间。4. 接入微信支付沙箱绕过企业资质用测试密钥完成支付流程闭环4.1 为什么必须用沙箱环境真实支付的三个不可逾越门槛新手常卡在“微信支付未开通”报错其实不是技术问题而是合规门槛主体资质个人类型小程序无法开通微信支付必须为企业/个体户经营类目餐饮外卖需选择“餐饮美食”类目并上传食品经营许可证结算周期T1 结算资金到账前需垫付对小微商户压力大。沙箱环境完美规避这三点它由微信官方提供无需任何资质审核、无需营业执照、无需对公账户只要在微信公众平台后台开启沙箱模式即可获得一套独立的测试密钥与虚拟资金池。所有支付请求走沙箱网关返回固定的成功响应且支持退款、查询等全操作。这才是“开箱即用”在支付环节的真正含义——不是教你填一堆表单而是让你今天下午就能看到“支付成功”弹窗。注意沙箱密钥与正式密钥完全隔离切勿在 production 环境使用沙箱密钥否则支付会静默失败。4.2 配置沙箱密钥与生成支付参数的最小代码集第一步登录 微信公众平台 → 开发管理 → 开发设置 → 微信支付 → 沙箱环境 → 启用沙箱并下载证书实际只需apiclient_key.pem和apiclient_cert.pem但 mock 方案中我们跳过证书校验。第二步在utils/pay.js中封装沙箱支付调用// utils/pay.js // 沙箱环境固定参数无需申请微信官方预置 const SANDBOX_CONFIG { appId: wxd678efh567hg6787, // 沙箱测试 AppID固定值 mchId: 1900000109, // 沙箱商户号固定值 apiKey: 8934e7d15453e97507ef794cf7b0519d, // 沙箱 API 密钥固定值 sandboxUrl: https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder } // 生成随机字符串微信支付要求 function randomString(len 32) { const chars ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 let result for (let i 0; i len; i) { result chars.charAt(Math.floor(Math.random() * chars.length)) } return result } // 生成时间戳秒级 function timestamp() { return Math.floor(Date.now() / 1000) } // 生成签名沙箱环境用固定密钥按微信文档规则拼接 function generateSign(params) { const keys Object.keys(params).sort() let str keys.forEach(key { if (params[key] ! params[key] ! undefined params[key] ! null) { str ${key}${params[key]} } }) str key${SANDBOX_CONFIG.apiKey} return require(crypto).createHash(md5).update(str, utf8).digest(hex).toUpperCase() } // 构造统一下单参数mock 版返回固定 success 响应 function buildUnifiedOrderParams(orderNo, amount, body) { const params { appid: SANDBOX_CONFIG.appId, mch_id: SANDBOX_CONFIG.mchId, nonce_str: randomString(32), body: body, out_trade_no: orderNo, total_fee: amount * 100, // 单位分 spbill_create_ip: 127.0.0.1, notify_url: https://yourdomain.com/pay/notify, // 此处仅为占位沙箱不校验 trade_type: JSAPI, openid: oUpF8uMuAJO_M2pxb1Q9zNjWeS6o // 沙箱固定测试 openid } params.sign generateSign(params) return params } // 模拟调用统一下单接口实际项目中此处应 wx.request function mockUnifiedOrder(orderNo, amount, body) { return new Promise((resolve) { setTimeout(() { // 沙箱环境固定返回 success 响应 const res { return_code: SUCCESS, return_msg: OK, appid: SANDBOX_CONFIG.appId, mch_id: SANDBOX_CONFIG.mchId, nonce_str: randomString(32), prepay_id: prepay_idwx${randomString(28)}, result_code: SUCCESS, err_code_des: OK } res.sign generateSign(res) resolve(res) }, 800) }) } module.exports { mockUnifiedOrder, buildUnifiedOrderParams, SANDBOX_CONFIG }逻辑说明SANDBOX_CONFIG中的appId/mchId/apiKey是微信官方公布的沙箱固定值无需申请复制即用generateSign严格遵循微信签名算法参数键名升序、拼接keyxxx、MD5 大写mockUnifiedOrder用setTimeout模拟网络延迟返回固定prepay_id这是唤起支付的关键字段openid使用沙箱固定测试值真机调试时微信会自动注入当前用户 openid此处仅为兼容模拟器。参数说明amount * 100微信支付金额单位为“分”必须转为整数18.5元要传1850nonce_str随机字符串长度必须为 32 位少一位都会签名失败prepay_id格式必须为prepay_idwx...否则wx.requestPayment会报错invalid parameter。4.3 在订单成功页唤起支付并监听结果// pages/order/success.js const payUtils require(../../utils/pay.js) Page({ data: { orderNo: , payable: 0 }, onLoad(options) { this.setData({ orderNo: options.id, payable: parseFloat(options.amount) || 0 }) }, onPayClick() { wx.showLoading({ title: 支付中... }) // 1. 调用统一下单mock 版 payUtils.mockUnifiedOrder( this.data.orderNo, this.data.payable, 订单${this.data.orderNo} ).then(res { if (res.result_code ! SUCCESS) { throw new Error(res.err_code_des || 统一下单失败) } // 2. 构造支付参数注意timestamp 必须是秒级且与签名中的时间一致 const timeStamp Math.floor(Date.now() / 1000) const payArgs { appId: payUtils.SANDBOX_CONFIG.appId, timeStamp: timeStamp.toString(), nonceStr: res.nonce_str, package: prepay_id${res.prepay_id}, signType: MD5, paySign: // 此处需重新签名不能复用统一下单的 sign } // 3. 为 paySign 重新签名关键 const paySignStr appId${payArgs.appId}nonceStr${payArgs.nonceStr}package${payArgs.package}signType${payArgs.signType}timeStamp${payArgs.timeStamp}key${payUtils.SANDBOX_CONFIG.apiKey} payArgs.paySign require(crypto).createHash(md5).update(paySignStr, utf8).digest(hex).toUpperCase() // 4. 唤起支付 wx.requestPayment({ ...payArgs, success: (res) { wx.hideLoading() wx.showToast({ title: 支付成功, icon: success }) // 更新订单状态为 paid const orders wx.getStorageSync(orders) || [] const order orders.find(o o.id this.data.orderNo) if (order) order.status paid wx.setStorageSync(orders, orders) }, fail: (err) { wx.hideLoading() if (err.errMsg.includes(requestPayment:fail cancel)) { wx.showToast({ title: 用户取消支付, icon: none }) } else { wx.showToast({ title: 支付失败请重试, icon: none }) } } }) }).catch(err { wx.hideLoading() wx.showToast({ title: err.message, icon: none }) }) } })逻辑说明wx.requestPayment的paySign必须单独生成不能复用统一下单的sign因为参数字段完全不同timeStamp必须是秒级整数字符串如1712345678传毫秒会报错fail回调中区分cancel与真实错误避免用户误点取消后反复提示“支付失败”支付成功后立即更新orders中对应订单的status为“我的订单”页提供数据。参数说明package字段必须为prepay_idxxx不能带空格或换行signType固定为MD5微信目前不支持其他算法paySign签名字符串拼接顺序必须严格按文档appIdnonceStrpackagesignTypetimeStampkey。5. 避坑指南本地 mock 与沙箱支付的 5 个血泪经验5.1 现象首页菜品列表加载后点击“加入购物车”无反应console 无报错原因WXML 中bindtapaddToCart的事件绑定对象未正确传递dish数据常见于wx:for循环中忘记写>// utils/api.js // 环境变量控制数据源 const IS_MOCK true // 开发时设为 true上线前改为 false if (IS_MOCK) { module.exports require(./mock-api) // mock-api.js 导出与真实 API 相同的函数签名 } else { module.exports require(./real-api) // real-api.js 封装 wx.request 调用 }mock-api.js示例// utils/mock-api.js const mockData require(./mock-data.js) module.exports { getRestaurant() { return Promise.resolve(mockData.restaurant) }, getDishes(categoryId) { return Promise.resolve( categoryId all ? mockData.dishes : mockData.dishes.filter(d d.categoryId categoryId) ) }, createOrder(orderData) { return Promise.resolve({ orderId: mock_ Date.now() }) } }real-api.js示例// utils/real-api.js module.exports { getRestaurant() { return wx.request({ url: https://api.yoursite.com/restaurant }) }, getDishes(categoryId) { return wx.request({ url: https://api.yoursite.com/dishes, data: { category_id: categoryId } }) }, createOrder(orderData) { return wx.request({ url: https://api.yoursite.com/orders, method: POST, data: orderData }) } }这样所有页面只需const api require(../../utils/api)调用api.getDishes()切换环境只需改IS_MOCK常量零代码修改。6.2 支付流程迁移沙箱 → 正式环境的三步替换表沙箱环境字段正式环境来源替换方式appId公众平台 → 开发管理 → 开发设置 → AppID替换SANDBOX_CONFIG.appIdmchId微信支付商户平台 → 账户中心 → 商户信息替换SANDBOX_CONFIG.mchIdapiKey商户平台 → 账户中心 → API安全 → 设置API密钥替换SANDBOX_CONFIG.apiKey注意正式密钥需自行生成不能用沙箱密钥sandboxUrlhttps://api.mch.weixin.qq.com/pay/unifiedorder替换sandboxUrl为正式 URL移除/sandboxnew/路径openidwx.getStorageSync(userInfo).openid需先调用wx.login删除硬编码从用户登录态中动态获取关键提醒正式环境必须部署 HTTPS 回调地址且notify_url需在商户平台白名单中备案沙箱环境对此无要求。6.3 真实部署 checklist5 个上线前必验项项目验证方式不通过后果1. 域名备案与 HTTPS在微信公众平台 → 开发管理 → 开发设置 → 服务器域名填入request合法域名如https://api.yoursite.com所有wx.request请求被拦截页面白屏2. 业务域名配置同上填入uploadFile合法域名如https://file.yoursite.com图片上传失败菜品图片无法显示3. 支付授权目录微信商户平台 → 产品中心 → 开发配置 → 授权目录填入小程序路径如https://yoursite.com/pages/order/confirmwx.requestPayment报错no permission4. 云函数/云数据库权限若用云开发检查云函数执行角色是否有数据库读写权限云函数调用返回permission denied5. 真机支付测试在真机上扫码体验版走通从加购到支付成功全流程用户反馈“点不了支付”实际是授权目录未配我一般会在上线前用一台未登录微信的手机扫码从头走一遍流程看菜单、加购、填地址、提交、唤起支付、输入沙箱密码123456、看到“支付成功”——这一步通过才敢切正式密钥。很多翻车都发生在“以为配好了其实漏了一项”所以宁可多测一次不省那五分钟。希望帮到你。本文还有配套的精品资源点击获取