简介:一套面向餐饮商户的微信小程序源码模板,既适合需要快速搭建点餐、外卖、订单管理等服务的商户,也适合想通过真实项目入门微信小程序开发的初学者。压缩包内共202个文件,其中JS文件负责商品、服务、折扣等核心业务逻辑,WXML与WXSS构架页面和样式,JSON完成配置,PNG图片与GIF动图提供界面素材,整体仅689KB,目录清晰,便于按模块阅读和二次修改。目前已有46人学习浏览。该模板内置菜品展示、促销信息、在线支付、订单处理等常用功能,并涵盖商品、服务、折扣、个人中心等控制模块,开发者可依据实际需求调整页面与逻辑,有效减少重复开发成本;同时可作为学习微信小程序前后端交互的完整示例,帮助理解基础框架、生命周期和数据处理方式,为中初级开发者提供一个扎实的练手起点。
1. 餐饮商户模板:一份能省掉两周开发量的微信小程序起点
拿到「餐饮商户模板.zip」这个压缩包,很多人的第一反应是解压后塞进微信开发者工具,能编译通过就长舒一口气。但实际接手过这类模板的人会告诉你:模板的意义从来不是「能跑」,而是「改得动」。这份餐饮模板的基础工程里,通常已经预制了底部 TabBar、点餐页、菜单分类、购物车、订单列表和结算流程这几条核心链路,你要做的不是从零定义一个门店小程序,而是把菜品数据结构、门店信息、支付参数和风格样式换成自己的。适合谁?适合手里有一家或几家实体餐饮店、想快速上线小程序点餐能力的商户运营者,也适合接私活的开发者——拿模板做二次交付,比从空项目搭框架至少省两周。但前提是,你得先看懂这个压缩包里的目录结构、数据流和几个最容易翻车的配置点。这篇文章就把解压、改造、联调和上线的完整路径拆开讲清楚,包括那些模板作者没写在 README 里的坑。
2. 解压与工程体检:先搞清模板目录里每一块在管什么
拿到 zip 后不要急着拖进开发者工具,先做一次「工程体检」。解压后你会看到典型的原生小程序目录结构,这个结构决定了后续所有改动的位置。很多第一次接触小程序的开发者,会在 page 目录下迷路,或者把业务逻辑写进了 app.js 导致全局变量混乱。所以这一章先把模板的家底盘明白。
2.1 压缩包内的标准目录结构与 app.json 的路由配置
原生微信小程序模板的核心目录一般长这样:
├── app.js # 全局逻辑:启动时拉取门店信息、登录态管理 ├── app.json # 全局配置:页面路由、window 样式、tabBar ├── app.wxss # 全局样式:公共颜色、字体、按钮样式 ├── project.config.json # 项目配置:appid、编译设置、上传设置 ├── sitemap.json # 索引配置:控制页面是否被微信索引 ├── images/ # 静态图片资源:图标、默认图 ├── utils/ # 工具函数:请求封装、价格计算、时间格式化 └── pages/ # 页面目录,每个页面一个文件夹 ├── index/ # 首页:门店展示 + 推荐菜品 ├── menu/ # 点餐页:左侧分类、右侧菜品列表 ├── cart/ # 购物车页:已选菜品、数量调整 ├── order/ # 订单页:订单列表、订单详情 └── mine/ # 我的页:登录状态、会员信息、设置先从app.json读起,这是整个小程序的「路由总表」。模板能正常工作,靠的就是 pages 数组里第一个元素被当作启动页。改模板的第一步,永远是把 pages 数组里的页面顺序调整成你自己的业务入口——如果做快餐扫码点餐,index 应该指向 menu 页;如果做预订型餐厅,index 应该保留为门店介绍页。
{ "pages": [ "pages/index/index", "pages/menu/menu", "pages/cart/cart", "pages/order/order", "pages/mine/mine" ], "window": { "navigationBarTitleText": "餐饮商户", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f5" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/menu/menu", "text": "点餐" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }这段配置的关键点在于 tabBar 的 pagePath 必须与 pages 数组里的路径完全一致,多一个斜杠、少一个 .json 后缀都会导致编译报错「tabBar list 第几项路径错误」。我对接过的几个餐饮模板里,商家自己改页面文件名后忘了同步 tabBar 配置,这是最高频的翻车点。另外navigationBarTitleText决定了导航栏上的店铺名,如果模板里写的是「餐饮商户」这种占位文案,你需要在全局 window 或每个页面的 json 里改成真实门店名。
2.2 project.config.json 里的 appid 与编译模式:为什么你打开是黑屏
模板作者通常会在 project.config.json 里留一个测试 appidtouristappid,它对应微信开发者工具的「游客模式」。用游客模式打开模板,基础功能基本能跑,但涉及到 wx.login、手机号获取、微信支付这些能力时,调用的 API 会被直接拒绝。这就是很多人解压模板后点「预览」黑屏或按钮无响应的原因。进入开发者工具后,第一件事是在「详情」面板里把你的小程序 AppID 填进去,然后检查 project.config.json 里这些字段:
{ "appid": "wx0000000000000000", "compileType": "miniprogram", "libVersion": "2.32.3", "setting": { "urlCheck": false, "es6": true, "enhance": true, "postcss": true, "minified": true } }urlCheck是开发者工具里最容易被忽略的一项。它在「设置 - 项目设置」里对应「不校验合法域名」。开发阶段把它关掉,你才能用 http://localhost 或局域网 IP 请求后端接口;但上线前必须把它打开,否则体验版和正式版会直接拦截非 https 的请求。libVersion 建议保持在 2.30 以上,低于 2.20 的版本不支持手机号快速验证组件等新能力。另一个直接影响显示效果的是「渲染模式」,在开发者工具右上角切到「Skyline 渲染」或「WebView 渲染」,两种渲染对 CSS 的兼容性有差异,如果模板页面上元素错位,先切换渲染模式试试,这大概率能解决七成布局问题。
2.3 从 app.js 读懂模板的数据初始化:门店信息从哪来
模板的 app.js 里通常藏着门店信息的初始化逻辑,这一块是餐饮模板的核心。我看过的多数餐饮模板,会在onLaunch里发起一次请求,拉取门店基础信息(店名、地址、电话、营业时间、公告),然后把数据挂到全局变量上供所有页面共用。
// app.js App({ globalData: { shopInfo: null, userInfo: null, cartList: [], orderFrom: null }, onLaunch() { // 从服务端拉取门店信息,失败时使用模板内置的 fallback 数据 this.loadShopInfo() }, loadShopInfo() { wx.request({ url: 'https://api.example.com/shop/info', method: 'GET', success: (res) => { if (res.data.code === 0) { this.globalData.shopInfo = res.data.data } }, fail: () => { // 离线兜底:保证模板在无网络环境下也能展示界面 this.globalData.shopInfo = { name: '示例餐厅', address: 'XX路XX号', phone: '13800000000', announcement: '本店支持在线点餐' } } }) } })这段代码的逻辑很清晰:成功时用服务端返回的数据覆盖全局变量,失败时用模板内置的兜底数据。改模板时你需要做两件事——把url换成你的服务端接口地址,以及把fail回调里的默认门店信息改成你自己的真实信息。注意fail分支绝对不是「写死」那么简单,它决定了开发阶段页面不白屏的底线。如果模板里没有这个兜底逻辑,建议你手动补上,否则在开发者工具里关掉网络模拟断网,整个小程序直接空白。globalData里那个cartList是跨页面共享的购物车数据,点餐页加菜、购物车页改数量、结算页算金额,全靠这个数组在不同页面之间传递状态。
3. 从模板到可用:点餐页、购物车与订单数据流改造
模板的目录结构看明白了,接下来就是动真格的地方。餐饮小程序的核心不是花哨的 UI,而是「用户选菜 - 加购 - 下单 - 支付」这条数据流的闭环。模板通常把这套逻辑写了个大概,但菜品数据是写死的 JSON 数组,金额计算也可能只覆盖了基础场景。这一章把这三块关键代码掰开揉碎讲清楚。
3.1 将写死的菜品数组替换为服务端菜品分类数据
模板里的 menu 页,菜品数据大概率长这样:
// pages/menu/menu.js Page({ data: { categories: ['招牌菜', '热菜', '凉菜', '主食'], dishes: [ { id: 1, name: '招牌红烧肉', category: '招牌菜', price: 58, image: '/images/dish1.png' }, { id: 2, name: '蒜蓉时蔬', category: '热菜', price: 28, image: '/images/dish2.png' } ] } })这种写死的数组只适合做 Demo,真实上线时必须从服务端拉取。原因很简单:你要改价格、上下架菜品、调整排序,不可能每次发版。替换的思路是保留模板的渲染结构,把数据源从本地常量改为接口返回。这里用一个小技巧——保持 data 里的字段名与接口返回一致,这样页面模板的wx:for循环不需要任何改动。
// pages/menu/menu.js 改造后的获取菜品逻辑 Page({ data: { categories: [], dishes: [], activeCategory: 0, loading: true }, onLoad() { this.fetchMenu() }, fetchMenu() { wx.request({ url: 'https://api.example.com/menu/list', method: 'GET', success: (res) => { if (res.data.code === 0) { const list = res.data.data // 从菜品列表中提取不重复的分类,保持与后端返回顺序一致 const categories = [...new Set(list.map(item => item.category))] this.setData({ categories: categories, dishes: list, loading: false }) } }, fail: () => { this.setData({ loading: false }) wx.showToast({ title: '菜单加载失败', icon: 'none' }) } }) } })逻辑说明:categories不再由前端维护,而是从返回的菜品列表里动态提取。这样后端新增一个分类时,前端不需要发版。注意[...new Set(...)]的去重写法依赖 ES6 转换,project.config.json 里的es6开关必须打开,否则低版本基础库会报语法错误。页面的 WXML 里通常会用wx:for遍历 categories 渲染左侧分类栏,再用wx:for遍历 dishes 渲染右侧菜品列表。「加载中」状态也要保留——真实网络环境下,接口响应比起本地数据慢得多,没有 loading 提示会让用户以为小程序卡死了。
3.2 购物车逻辑的边界处理:数量上限、售罄与规格选择
模板自带的购物车逻辑一般是最简陋的:加号、减号、删菜品。但真实餐饮场景远比这个复杂。我处理过的餐饮模板,至少有三个边界必须补上——菜品售罄、单菜购买数量上限、规格选择(比如微辣/中辣/特辣,或者选份量大小)。
// 购物车核心操作改造 Page({ data: { cartList: [], totalPrice: 0, totalCount: 0 }, addToCart(e) { const dish = e.currentTarget.dataset.dish // 售罄校验:服务端返回的 soldOut 为 true 时,直接拦截 if (dish.soldOut) { wx.showToast({ title: '该菜品已售罄', icon: 'none' }) return } // 单菜数量上限校验:模板里常见的默认值是 99,实际场景建议改 20 const existing = this.data.cartList.find(item => item.id === dish.id) if (existing && existing.count >= 20) { wx.showToast({ title: '单菜最多购买20份', icon: 'none' }) return } // 存在则数量 +1,不存在则新增一条,价格快照到加入时的值 if (existing) { existing.count += 1 } else { this.data.cartList.push({ ...dish, count: 1 }) } this.setData({ cartList: this.data.cartList }) this.calcTotal() }, calcTotal() { let totalPrice = 0 let totalCount = 0 this.data.cartList.forEach(item => { totalPrice += item.price * item.count totalCount += item.count }) // 金额保留两位小数,避免浮点误差 this.setData({ totalPrice: Math.round(totalPrice * 100) / 100, totalCount: totalCount }) } })逻辑说明:addToCart的参数来自 WXML 里>// pages/order/order.js 下单核心逻辑 Page({ data: { remark: '', payMethod: 'wxpay', address: '' }, submitOrder() { const cartList = getApp().globalData.cartList if (cartList.length === 0) { wx.showToast({ title: '购物车不能为空', icon: 'none' }) return } // 把菜品 id 和数量传给后端,价格由后端根据数据库实时价格计算 const orderItems = cartList.map(item => ({ dishId: item.id, count: item.count, spec: item.spec || '默认' })) wx.request({ url: 'https://api.example.com/order/create', method: 'POST', data: { items: orderItems, remark: this.data.remark, address: this.data.address }, success: (res) => { if (res.data.code === 0) { // 拿到后端下单结果后,用 orderId 请求支付参数 const orderId = res.data.data.orderId wx.request({ url: 'https://api.example.com/pay/unified', method: 'POST', data: { orderId: orderId }, success: (payRes) => { wx.requestPayment({ timeStamp: payRes.data.timeStamp, nonceStr: payRes.data.nonceStr, package: payRes.data.package, signType: 'RSA', paySign: payRes.data.paySign, success: () => { wx.showToast({ title: '支付成功', icon: 'success' }) } }) } }) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) } } }) } })
这段代码是你把模板接到真实后端时的标准姿势。「先创建订单、再请求支付参数、最后拉起支付」三步走,顺序不能乱。模板里如果直接把价格写进wx.requestPayment的package参数,那多半是个只能演示的假支付。注意package这个字段名是微信支付接口要求的关键字,ES6 对象简写时不能改名——很多开发者在这里踩坑,写成package: res.data.package是对的,但简写成package在某些低版本 iOS 上有兼容问题,建议保留显式键值对写法。支付成功后,记得调用wx.setStorageSync清空购物车,避免用户支付完回来发现购物车还是满的——模板里经常漏掉这步。
4. 登录与手机号获取:餐饮模板里绕不开的两个官方能力
餐饮商户模板几乎没有不用到登录和手机号能力的。用户进小程序先「微信一键登录」拿身份标识,结算时再「手机号快速验证」关联会员系统。这两个能力模板里常常只留了按钮,逻辑是空的,需要你自己填。这一章把这两块的最新实现方式讲清楚——因为官方接口迭代过几次,网上很多教程已经过时了。
4.1 wx.login 拿到 code 换 openid:模板里登录态的完整闭环
微信小程序的登录逻辑自从上线以来就没变过核心流程,但细节年年调整。现在的标准做法是:
// utils/auth.js 登录封装 function login() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { if (res.code) { // 把 code 传给后端,后端用 code 加上 appid 和 secret 换 openid/session_key wx.request({ url: 'https://api.example.com/auth/login', method: 'POST', data: { code: res.code, userInfo: getApp().globalData.userInfo }, success: (resp) => { // 后端返回自定义登录态 token,存入 storage wx.setStorageSync('token', resp.data.token) resolve(resp.data) }, fail: reject }) } else { reject(res) } } }) }) }wx.login返回的code有效期只有五分钟,而且只能使用一次。后端拿 code 调微信接口换openid和session_key,然后自己发一个 token 给前端。模板里如果直接把这个 token 存到globalData,会导致小程序冷启动后 token 丢失,所以必须同步写入wx.setStorageSync。每次业务请求在 Header 里带上 token,后端验 token 解析用户身份。这里有个易踩的坑:不要在onLaunch里同步调wx.login,因为它走网络请求,会拖慢启动速度。正确做法是打开小程序先正常浏览,在用户点击「登录」或「下单」时再触发登录流程。
4.2 手机号快速验证组件:一键获取用户手机号的合规姿势
餐饮行业做会员管理,手机号几乎是必填项。以前的做法是用wx.getPhoneNumber配合后端解密,但 2023 年后官方收紧了对这个接口的权限——现在个人主体小程序无法直接申请,必须是企业主体且通过认证。如果你接的模板里还写着wx.getPhoneNumber这个 API,大概率是过时写法。当前合规做法是用官方提供的「手机号快速验证组件」:
<!-- pages/mine/mine.wxml 手机号快速验证组件 --> <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber" class="phone-btn" > 手机号一键登录 </button>// pages/mine/mine.js 对应的处理函数 Page({ onGetPhoneNumber(e) { // 用户拒绝授权时,e.detail 里没有 code if (e.detail.errMsg.indexOf('ok') > -1) { // code 是动态令牌,传给后端换取真实手机号 const phoneCode = e.detail.code wx.request({ url: 'https://api.example.com/auth/phone', method: 'POST', data: { code: phoneCode }, success: (res) => { if (res.data.code === 0) { // 后端解密后返回手机号,用掩码展示 this.setData({ phone: res.data.data.phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') }) } } }) } else { wx.showToast({ title: '需要手机号才能使用会员功能', icon: 'none' }) } } })代码里的关键点在e.detail.code——它不是手机号本身,而是一个五分钟有效期的动态令牌。后端拿这个 code 调微信接口才能换到真实手机号,前端永远不应该拿到明文手机号(这是合规要求)。bindgetphonenumber组件要求企业主体小程序才能正常返回 code,个人主体和未认证的小程序点按钮会直接报错。如果模板里的登录页还是旧版样式,你需要把按钮换成open-type="getPhoneNumber"。注意这个组件和wx.login的 code 不是同一个东西,不要混用——很多开发者把手机号组件的 code 当成 wx.login 的 code 传给后端做登录,接口直接 400。
4.3 授权弹窗的时序问题:拒绝授权后怎么引导用户重新触发
几乎所有餐饮模板都存在一个体验问题:用户第一次进小程序看到微信授权弹窗,手滑点了拒绝,之后菜单页、下单页需要用户信息的功能全部失效。微信官方对这类授权弹窗的机制是——wx.getUserProfile或手机号组件的授权弹窗,一旦用户拒绝,短时间内无法再次弹出。这里有一个在模板改造时几乎必须做的兜底逻辑:在「我的」页面里放一个显眼的引导按钮,让用户手动重新触发授权。
// pages/mine/mine.js 重新触发授权 Page({ data: { isAuthorized: false }, onShow() { // 每次进入我的页面,检查 storage 里是否有登录态 const token = wx.getStorageSync('token') this.setData({ isAuthorized: !!token }) }, handleAuth() { // 无 token 时触发整体登录流程:先 login 再拉手机号 if (!this.data.isAuthorized) { login().then(() => { this.setData({ isAuthorized: true }) wx.showToast({ title: '登录成功', icon: 'success' }) }) } } })逻辑说明:onShow里检查 token 是为了让页面每次展示时同步最新的登录状态——用户可能在小程序的其它页面完成了登录,回到「我的」页时按钮状态要联动更新。login()是 4.1 节里封装的那个 Promise 方法。注意这里的引导不要让用户反复点授权——微信对同一用户的授权弹窗频次有限制,连续触发会被系统静默拦截。演示时我发现,弹窗被拦截后按钮毫无反应,用户会认为小程序坏了。
5. 模板改造避坑:编译错误、导航栏与真机调试的血泪经验
这一章专门写给准备拿模板动手改的人。我整理了改造餐饮商户模板过程中最常见、最容易卡住半天的问题,每条都是现象→原因→解决的完整链路。这些坑不踩一遍你不会信,但知道了就能省下大量排错时间。
5.1 页面路径大小写不匹配:真机正常、开发者工具报错的「玄学」
现象:开发者工具编译正常,模拟器里页面跳转也正常,但一上传代码用真机预览,点某个按钮跳转页面时报错「pageRouteBuilder: page not found」。
原因:模板解压后,部分页面文件名是小写开头(比如pages/order/Order.js),但 app.json 里注册的路径写的是全小写pages/order/order。开发者工具的文件系统不区分大小写,能自动匹配;真机上 iOS 文件系统也不敏感,但部分 Android 机型文件系统区分大小写,导致找不到页面文件。
解决:打开 app.json,逐个核对 pages 数组里的路径与 pages 目录下实际文件名的大小写,全部统一为小写。这是一个纯手工作业,没有捷径——用脚本的话,可以写一个 Node.js 脚本遍历目录自动比对并重命名,但处理前先备份。
5.2 顶部导航栏高度适配:iPhone 刘海屏与安卓状态栏重叠
现象:模板首页在开发者工具里导航栏位置正常,真机上一看,店铺名和状态栏的时间、电量重叠在一起,或者导航栏高度在 iPhone 15 和安卓机上明显不一致。
原因:小程序默认使用navigationStyle: default,导航栏高度由系统决定,模板设计时用了固定 padding-top,比如padding-top: 44px。但不同机型的导航栏高度不同——iPhone 13/14 是 44px,iPhone 15 Pro 是 47px,安卓常见的是 48px 上下。固定值必然在部分机型上错位。
解决:改掉写死的 padding,使用小程序官方提供的胶囊按钮位置来动态计算。拿到菜单按钮的边界值,导航栏底部的位置就是胶囊按钮的底部加上系统状态栏高度。
// utils/navbar.js 动态计算导航栏占位高度 function getNavBarHeight() { const systemInfo = wx.getWindowInfo() const menuButton = wx.getMenuButtonBoundingClientRect() // 状态栏高度 + 胶囊按钮高度 + 上下间隔,得到导航栏整体高度 const navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height return { statusBarHeight: systemInfo.statusBarHeight, navBarHeight: navBarHeight, menuButton: menuButton } }逻辑说明:wx.getMenuButtonBoundingClientRect()返回胶囊按钮相对屏幕左上角的位置和尺寸,这是做自定义导航栏时最核心的 API。(menuButton.top - systemBarHeight)是胶囊距状态栏底部的距离,通常等于 4px,乘以 2 后再加上胶囊高度,正好等于导航栏总高度。拿到这个值后,页面顶部要用 padding 或 margin 撑开时,直接从这个函数取值,而不是写死 44px。这个方法在模板改造中适用性极强——凡是页面顶部需要自定义 header 的,都建议走这个封装。
5.3 rpx 尺寸换算在真机上的偏差:iphone 与安卓横屏的显示差异
现象:模板里写width: 750rpx,模拟器看着占满全屏,打到真机上右边超出屏幕一截,或者上下留白不一样。
原因:rpx 的换算规则是 750rpx 等于屏幕宽度,但这里的「屏幕宽度」在 iPhone 上是逻辑宽度 375px 乘以 2 倍率得出 750rpx;在部分安卓机上逻辑宽度是 412px 或 432px,如果模板里用了calc(750rpx - 20px)这类混合运算,就可能出现偏差。尤其是横屏状态,rpx 失效——它固定按竖屏宽度换算。
解决:不要在 rpx 表达式里混入 px。模板里常见的错误写法是width: calc(100% - 20rpx),这种没问题;但height: calc(750rpx - 100px)就有问题。统一用 rpx 或统一用 px。另外横屏适配——餐饮小程序极少需要横屏,建议在page.json里加上"pageOrientation": "portrait"锁定竖屏,省掉所有横屏适配工作量。注意 iPhone 的 safe-area 底部还要单独处理,模板里的底部购物车栏如果没加padding-bottom: env(safe-area-inset-bottom),在全面屏 iPhone 上会出现底部按钮被 Home Indicator 遮住的问题。
5.4 分包与主包体积超限:模板图片素材撑爆 2MB 限制
现象:模板本身没多少代码,但图片素材多——菜品图、Banner 图、图标文件加起来动不动超过 2MB。开发者工具上传时直接报错「代码包大小超出限制」。
原因:小程序主包上限 2MB,分包上限 2MB,总包上限 20MB(按 2024 年的规则)。餐饮模板里常见的 3MB 压缩包解压后通常 6-10MB,图片是体积大头。
解决:第一步,把所有图片用在线压缩工具压到 100KB 以下,PNG 图标能转 WebP 的就转 WebP。第二步,把点餐页、菜单页拆成独立分包,因为用户打开小程序往往只看首页和点餐页,把低频页面拆进分包让主包瘦身。修改 app.json:
{ "pages": [ "pages/index/index", "pages/menu/menu" ], "subPackages": [ { "root": "pages/order", "pages": [ "order/order", "order/detail" ] }, { "root": "pages/mine", "pages": [ "mine/mine" ] } ] }分包的关键规则:分包根目录不能引用主包的文件,分包的 JS 不能 require 主包之外的模块。模板里如果utils里的工具函数被分包页面引用,需要把它放进主包的 common 目录。另外一个容易忽略的点——分包内的页面跳转要用wx.navigateTo({ url: '/pages/order/order/order' }),路径要写全,不能省略分包 root 前缀。
5.5 蓝牙打印机联调失败:餐饮模板里最容易被低估的硬件对接
现象:模板的「订单管理」页面预留了打印按钮,点了没反应,或者 console 报错wx.createBLEConnection:fail。
原因:餐饮小程序对接蓝牙小票打印机,需要在小程序里完成蓝牙扫描、设备连接、数据写入三步。模板通常只写了按钮 UI,蓝牙逻辑是空的。很多接单的开发者第一次搞蓝牙,在扫描到设备但连接不上这一步卡住。
解决:连接不上 90% 是deviceId用错。wx.startBluetoothDevicesDiscovery返回的deviceId是蓝牙物理地址,部分安卓机返回的是大写带冒号格式(AA:BB:CC:DD:EE:FF),而wx.createBLEConnection要求传大写且不带冒号的deviceId。在使用前做一次格式化:
// 蓝牙连接前归一化 deviceId function normalizeDeviceId(deviceId) { return deviceId.replace(/:/g, '').toUpperCase() }另外需要在app.json里声明蓝牙权限——虽然小程序蓝牙 API 不需要在 app.json 里加权限字段,但在部分安卓机型上需要用户在系统设置里手动开启「附近设备」权限,否则扫描结果为空。这事儿没有代码层面能解决的方案,只能在小程序引导页里加一段「请前往系统设置打开蓝牙和定位权限」的说明。定位权限是这里最大的坑——蓝牙扫描在 Android 上要求同时开启定位服务,否则扫描不到任何设备。
5.6 「真机预览」可以但「上传体验版」白屏:一个让所有新手懵掉的现象
现象:开发者工具点「预览」,手机微信扫码打开一切正常。但点「上传」后,在微信公众平台生成体验版二维码,扫码打开却是白屏,而且 console 没有任何报错。
原因:预览走的是开发者工具的本地服务,跳过域名校验;体验版走的是微信 CDN,正式请求时urlCheck强制生效。模板里的接口如果还是 http:// 或者 IP 地址,体验版直接拦截请求,页面数据空白,但因为请求失败没有 toast 提示,看起来就是白屏。
解决:这不是玄学,是域名校验。排查步骤就两步——第一,确认接口全部用 https;第二,登录小程序后台,在「开发管理 - 开发设置 - 服务器域名」里把 request 合法域名加进去。常见失误是只在https://api.example.com加了域名,但模板里图片域、上传域也在生效——downloadFile合法域名需要单独配置。另外体验版白屏还有一个隐蔽原因:ES6 转 ES5 开关没开,低版本 Android 微信自带的浏览器内核不支持新语法,代码运行时报错但不显示。把 project.config.json 里的es6: true确认开启,再试试。
6. 跑通之后:把模板变成自己的商户小程序,还差这四步
模板在你电脑上跑通了、接口也联调完了,但这距离「上线」还差最后几公里。我连续改造过多个餐饮商户模板,每次都在这个阶段发现遗漏,于是养成了固定的交付前检查清单。这一章就给你这份清单,照着做一遍,相当于给项目做了一回体检。
第一步,替换所有残留的占位信息。用编辑器全局搜索「示例餐厅」「13800000000」「XX路XX号」这类模板占位文案,替换成真实门店信息。这个动作看起来简单,但很容易漏——门店公告、营业时间、发票抬头、邮件通知模板里都有可能出现。我习惯用 VS Code 的「在文件中替换」功能全局处理,替换完再用「搜索排除 node_modules」检查一遍。漏掉一个占位信息,上线后发现用户看到的是「示例餐厅」,相当尴尬。
第二步,检查隐私协议与用户授权。登录和手机号获取都涉及用户隐私信息收集,小程序后台要求必须有隐私保护指引,而且要在前端代码里接入「隐私协议弹窗」。微信官方提供wx.requirePrivacyAuthorize接口,在 App 的onLaunch里调用:
// app.js onLaunch 里增加隐私授权检查 onLaunch() { if (wx.requirePrivacyAuthorize) { wx.requirePrivacyAuthorize({ success: () => {}, fail: () => { // 用户拒绝隐私授权,后续引导去设置页开启 wx.showModal({ title: '提示', content: '需要同意隐私保护指引才能使用完整功能', confirmText: '去设置', success: (res) => { if (res.confirm) { wx.openSetting() } } }) } }) } }注意wx.requirePrivacyAuthorize是 2023 年后才有的 API,旧版基础库不支持,需要加if判断做兼容。模板里如果有自定义的登录弹窗,记得在弹窗里同步加上隐私协议的勾选入口。
第三步,用「体验版」跑一遍完整下单链路。真机预览和体验版是两个环境,我见过太多人栽在「预览正常、体验版白屏」这种问题上——上一章已经讲过根因。所以改造完成后,一定要上传体验版,用真实手机走一遍完整流程:进首页、查菜单、加购物车、提交订单、支付、收到推送通知。这中间要重点验证两件事:支付的回调通知是否到达后端(用微信支付商户平台的「交易中心」核对订单),以及支付成功后购物车是否清空。
第四步,配置合法域名并开启效验。在微信公众平台配置好全部合法域名后,把 project.config.json 里的urlCheck改回true,重新编译跑一遍。域名校验生效后,开发者工具里的模拟器请求会被拦截,这是正常现象——你要做的是确认所有请求都走了 https 且域名在白名单里。有一个常见遗漏:wx.uploadFile和wx.downloadFile需要单独配 uploadFile 合法域名和 downloadFile 合法域名,它们和 request 合法域名是分开的。餐饮模板里如果有点餐评价晒图功能,几乎必然用到 uploadFile。
最后多说一句我自己的教训。第一次拿模板给餐饮客户交付时,我以为代码没问题就等于项目完成,结果上线当晚用户反馈「微信支付按钮点了毫无反应」——查了半天发现是商户号没有开通对应的支付产品权限,模板代码本身没问题,是商户平台侧配置没到位。自那以后,我交付前都会把「支付权限是否开通并关联 AppID」列入强制检查项。另外不要把模板里的门店内部管理功能(比如后厨出票、分单打印)上线到用户端小程序——这些应该放在独立的管理端小程序里,否则用户端会非常臃肿。微信小程序个人主体能做的功能也有限,把用户端搞得太复杂,审核时大概率驳回。希望这篇笔记帮你在改造餐饮商户模板的路上少踩几个坑,最好一次通关。
本文还有配套的精品资源,点击获取