微信小程序咖啡点单系统开发实战:支付对接与蓝牙打印

发布时间:2026/9/9 1:01:05
微信小程序咖啡点单系统开发实战:支付对接与蓝牙打印 简介这是一份用于学习微信小程序开发的完整星巴克咖啡门店界面源码适合小程序初学者以及想提升移动端界面布局与交互设计能力的开发者。项目中通过WXML与WXSS构建了商品展示、购物车、订单处理、历史记录、个人中心等典型页面并演示了内置组件使用、数据绑定、wx.request网络请求、navigator页面跳转、自定义组件封装、rpx响应式样式以及生命周期管理等关键技术。压缩包为rar格式共63个文件大小约1.47MB以27个png、9个js、9个json、8个wxss、7个wxml、3个jpg为主图片素材与逻辑代码分离目录结构清晰便于逐模块对照阅读。目前已有4133人学习/下载。通过学习这份源码既能理解小程序从页面结构到样式控制再到交互反馈的完整开发流程也可直接借鉴其咖啡主题视觉风格与移动端适配方案对于独立开发同类型轻应用很有参考价值。 从“微信小程序星巴克咖啡源码”这个标题入手我猜很多朋友其实想要的并不是星巴克官方那种闭源商业项目而是想快速搭一个**风格类似、链路完整点单-支付-取餐**的咖啡类小程序用来做毕设、面试作品或者商家自用。我自己前后做过两版咖啡点单小程序第一版是纯前端模拟第二版接入了真实后端和微信支付v3踩了不少坑也积累了一些能直接复用的经验。这篇文章就把整个项目的拆解思路、核心页面实现、支付对接跟蓝牙打印这些硬骨头一次讲清楚。1. 项目整体设计与技术选型1.1 核心需求拆解不只是“看起来像星巴克”先想清楚你要做什么。很多人一上来就盯着“星巴克”三个字想复刻绿色双尾美人鱼的界面其实这是最容易走偏的地方。星巴克小程序的精髓在于会员体系 门店选择 预点单 支付闭环界面风格反而是次要的。真正需要落地的核心功能是这样一串链路用户登录拿到微信头像和昵称建立本地会员身份选择门店区分“到店自取”和“外卖配送”两种履约方式浏览菜单按分类经典咖啡、冷萃、茶瓦纳、烘焙筛选商品商品详情页自定义浓度、温度、杯型、加料这块是咖啡类目跟普通电商最大的区别购物车逻辑支持改规格、清空、合计优惠微信支付下单成功后生成取餐码订单列表和订单详情附带进度状态制作中、待取餐、已完成如果只是做前端演示到第四步就可以收工。但如果要真正“能用”支付跟订单状态机是绕不开的。这也是我在选型时反复权衡的点。1.2 技术栈对比原生小程序 vs uni-app vs Taro热词里有“uniapp开发微信小程序”和“hbuilderx”几个关键词很多新手会被带偏。我直接把三套方案的实际情况摆出来你们少走弯路。方案优点缺点适合场景原生小程序性能最好、调试直接、支付API不需要封装代码只能在微信生态用单平台上线、毕设、快速交付uni-app一套代码多端发布、vue语法上手快包体积偏大、canvas和蓝牙等底层能力会有兼容坑需要同时出小程序AppTaroReact语法、社区活跃、组件生态好对新手不友好、自定义原生组件时比较绕团队本身就是React栈我的建议是除非你明确要做多端否则老老实实用原生小程序。这套“星巴克咖啡”项目里用到了蓝牙打印、支付回调、地图选店这类底层能力用原生写能省掉大量“框架帮你做了但又没完全做”的折腾时间。如果你已经用uni-app写了后面的蓝牙打印那节内容也可以参考踩过坑的地方我都会标出来。1.3 项目目录结构规划在动手写页面之前先把目录规划好这决定了后面迭代时会不会越写越乱。我比较推荐的工程结构是这样├── cloudfunctions/ # 云函数如果走云开发 ├── pages/ │ ├── index/ # 门店列表 / 首页 │ ├── menu/ # 菜单点单页 │ ├── goods/ # 商品详情 │ ├── cart/ # 购物车 │ ├── order/ # 确认订单 │ ├── payResult/ # 支付结果 │ └── orderList/ # 订单列表 ├── components/ │ ├── stepper/ # 数量加减器 │ └── specs-panel/ # 规格选择面板 ├── utils/ │ ├── request.js # 请求封装含token刷新 │ ├── cart.js # 购物车本地缓存逻辑 │ ├── pay.js # 支付业务流程封装 │ └── format.wxs # 价格格式化等 └── app.json注意到我在cart逻辑里单独抽了一层而不是直接在页面里操作globalData这是因为购物车数据需要在菜单页、详情页、购物车页三个地方同步不抽出来必然会出现“加了商品返回列表数量没变”的经典bug。2. 核心页面与交互细节解析2.1 菜单点单页自定义组件是必须的菜单页大概是整个小程序里最复杂的页面它要处理左右分栏联动左侧分类右侧商品列表、商品卡片跳转、底部购物车栏的实时状态。很多源码实现直接在一个Page里堆代码看起来也能跑但等你加第三十个商品的时候setData的卡顿就会让你很痛苦。我推荐的写法是把“数量加减”和“规格选择”拆成两个自定义组件。数量加减组件stepper内部维护一个数字通过triggerEvent把新数量抛给页面页面再更新购物车缓存。规格选择则用popup形式的半屏弹层里面渲染杯型、温度、浓缩份数、糖浆等选项组用户确认后把完整sku信息回传。这里有一个细节规格选择弹层的选项状态必须在确认时才写入购物车而不是用户点了一下“大杯”就立刻生效。因为用户可能在弹层里反复切换如果每次切换都写缓存会出现“退出弹层后购物车被半成品规格污染”的问题。正确做法是弹层内部维护一份临时selectedMap确认按钮触发后才一次性回传。2.2 顶部导航栏与胶囊对齐热词里有人搜“微信小程序顶部导航栏高度”和“自定义标题上边距”这是咖啡小程序里最容易被忽略的适配问题。默认导航栏丑不算大事关键是如果你要做自定义导航比如把品牌logo和门店切换放在导航栏同一行就必须处理胶囊按钮的位置。下面这段工具函数是实测过各种机型的直接放到utils里就行function getNavBarInfo() { const windowInfo wx.getWindowInfo(); const menuButton wx.getMenuButtonBoundingClientRect(); // 导航栏高度 胶囊底部到状态栏底部的距离 * 2 胶囊高度 const navBarHeight (menuButton.top - windowInfo.statusBarHeight) * 2 menuButton.height; return { statusBarHeight: windowInfo.statusBarHeight, // 状态栏高度 navBarHeight: navBarHeight, // 导航栏总高度 menuRight: windowInfo.windowWidth - menuButton.right, // 胶囊右侧间距 menuTop: menuButton.top - windowInfo.statusBarHeight, // 胶囊距状态栏距离 menuHeight: menuButton.height }; }这里有个2023年后新出现的坑wx.getSystemInfoSync已经废弃了必须要用wx.getWindowInfo不然在部分iOS版本上拿到的状态栏高度会是0。你在网上能搜到很多老源码还在用旧API直接复制会导致自定义导航栏在iPhone 14 Pro这种灵动屏机型上顶到状态栏外面去。2.3 咖啡定制的数据建模咖啡类小程序跟普通电商有个本质区别每个商品不是一个单纯的SKU而是“基础商品 一组可选项”的组合。我举个例子同一款拿铁可以选择大杯/中杯可以选单份浓缩/双份浓缩可以加热奶/换燕麦奶还可以加一份香草糖浆。在数据层我用了一种相对简单的方案就是把规格选项序列化成一个specId// 商品详情渲染时用户选完所有维度后生成 const selectedSpec { cupSize: grande, // 大杯 temp: hot, // 热 shots: double, // 双份浓缩 milk: oat, // 燕麦奶 syrup: [vanilla] // 加料数组 }; // 把这个对象转成稳定字符串作为购物车条目的唯一key const specKey JSON.stringify(selectedSpec);购物车里的每个条目实际上是goodsId specKey的组合这样才能支持“同一款拿铁一杯热的一杯冰的”同时存在。很多源码在这一点上偷懒直接把商品ID当购物车key结果就是加了一杯冰拿铁之后再加热拿铁会把前面的覆盖掉这是非常严重的逻辑错误。3. 微信支付v3对接从下单到回调的完整链路3.1 为什么建议直接上V3而非V2有人会问旧教程里都是V2为啥要折腾V3核心原因是V3的证书体系更安全而且微信支付官方对V2部分接口已经开始限制新商户直接不让开通V2。从2023年开始做新项目建议直接学V3不然等你的小程序上线后才发现接口用不了那时候再改就麻烦大了。V3最核心的变化有三个我用自己的话翻译一遍使用商户私钥对请求做RSA-SHA256签名而不是V2的MD5回调通知使用AES-256-GCM解密而不是明文敏感信息如银行卡号用平台公钥加密后再传输3.2 服务端统一下单的关键代码小程序端其实只需要做一件事把订单信息发给后端后端调微信支付接口拿到prepay_id再把签名后的参数返回给前端。前端再用wx.requestPayment拉起收银台。下面这段是后端Node.js的实现思路const crypto require(crypto); const axios require(axios); // 构造签名串这是V3里面最容易出错的一步 function buildSignMessage(method, url, timestamp, nonce, body) { return ${method}\n${url}\n${timestamp}\n${nonce}\n${body}\n; } function sign(privateKey, message) { const signer crypto.createSign(RSA-SHA256); signer.update(message); return signer.sign(privateKey, base64); } // 统一下单 async function createOrder(payload) { const timestamp Math.floor(Date.now() / 1000).toString(); const nonce crypto.randomBytes(16).toString(hex); const body JSON.stringify(payload); const signature sign(MERCHANT_PRIVATE_KEY, buildSignMessage(POST, /v3/pay/transactions/jsapi, timestamp, nonce, body)); const response await axios.post(https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi, payload, { headers: { Authorization: WECHATPAY2-SHA256-RSA2048 mchid${MCH_ID},nonce_str${nonce},signature${signature},timestamp${timestamp},serial_no${CERT_SERIAL_NO}, Content-Type: application/json } } ); return response.data.prepay_id; }我在第一次写这段代码时卡了两天没调通后来发现是商户证书序列号的获取方式搞错了。记住签名用的serial_no是商户API证书的序列号不是证书文件本身的内容。在微信商户平台可以下载到证书用openssl能看到它的序列号openssl x509 -in apiclient_cert.pem -noout -serial输出里的serialXXXXXXXX才是你要填的CERT_SERIAL_NO。3.3 小程序端拉起支付后端把prepay_id连同时间戳、随机串、签名一起返回给小程序后前端这样处理function wxPay(payParams) { return new Promise((resolve, reject) { wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, // 注意这里是字符串 prepay_idxxxx signType: RSA, paySign: payParams.paySign, success: resolve, fail: (err) { // err.errMsg requestPayment:fail cancel 表示用户主动取消 reject(err); } }); }); }这里有一个很多教程没提的细节package字段的值必须是完整的prepay_idxxx不能只传prepay_id那一串。另外signType要写RSA而不是RSA-SHA256小程序端只认这个写法。如果报错说signType不对检查一下是不是后端返回时多拼了空格。3.4 支付回调解密与订单状态更新支付成功后的回调处理是很多人忽略的环节直接决定你的订单能不能正确变成“已支付”。V3回调的加密方式是AES-256-GCM解密代码可以参考下面这段const crypto require(crypto); function decryptCallback(ciphertext, nonce, associatedData, apiV3Key) { const key Buffer.from(apiV3Key, utf8); const authTag ciphertext.slice(ciphertext.length - 16); const encryptedData ciphertext.slice(0, ciphertext.length - 16); const decipher crypto.createDecipheriv(aes-256-gcm, key, Buffer.from(nonce, utf8)); decipher.setAuthTag(authTag); decipher.setAAD(Buffer.from(associatedData, utf8)); let decoded decipher.update(encryptedData, base64, utf8); decoded decipher.final(utf8); return JSON.parse(decoded); }这里的apiV3Key是你在商户平台自己设置的32位API v3密钥。很多人在解密回调时卡住就是因为ciphertext和associatedData的顺序或者长度搞错了。记住回调body里的ciphertext字段就是密文nonce字段和associated_data字段要原样传进去。回调处理完按微信的要求必须在5秒内返回{code:SUCCESS}如果业务处理比较耗时建议先把订单状态标记为“支付确认中”并立刻返回成功再异步去更新库存和发送通知。4. 提升体验的进阶功能蓝牙打印与小票输出4.1 蓝牙打印的场景与选型热词里有人搜“微信小程序 蓝牙打印”这个功能在咖啡店场景里非常实用——用户在小程序下单后店里需要一个打印机自动打小票。在本项目中我采用的是商家端小程序监听蓝牙广播、连接热敏打印机的方案。选型上要注意市面上便宜的蓝牙热敏打印机大多是商用的“ESC/POS指令集”一般支持BLE或经典蓝牙。微信小程序的wx.openBluetoothAdapter、wx.startBluetoothDevicesDiscovery主要支持BLE低功耗蓝牙如果你的打印机只支持经典蓝牙就会搜不到设备。我实测过一个几十块钱的便携打印机它就是经典蓝牙小程序直接连不上最后只能换了一台支持BLE的。买打印机之前一定问清楚卖家是否支持BLE透传。4.2 连接与打印的关键代码// 第一步初始化蓝牙 await wx.openBluetoothAdapter(); // 第二步开始搜索 await wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false }); // 第三步监听发现新设备 wx.onBluetoothDeviceFound((res) { res.devices.forEach(device { // 过滤打印机设备通常广播名称里有printer或58mm等关键词 if (device.name device.name.includes(Printer)) { // 保存deviceId用于后续连接 } }); }); // 第四步连接设备获取服务 const { deviceId } await wx.createBLEConnection({ deviceId }); const { services } await wx.getBLEDeviceServices({ deviceId }); // 找到可写的characteristic一般是0xFFE0服务下的0xFFE2特征 await wx.writeBLECharacteristicValue({ characteristicId: FFE2, value: Array.from(textEncoder.encode(这是小票内容\n)) });打印小票需要把文本转成字节数组这里有个坑小程序的wx.writeBLECharacteristicValue的value参数是ArrayBuffer的字节数组不能直接把字符串传进去。需要用TextEncoder或者手动循环把字符转成Unicode码然后再拼成ArrayBuffer。如果直接传字符串会报invalid value错误。4.3 小票模板设计小票内容一般是这样的格式星享咖啡 地址幸福路88号 电话0571-88888888 -------------------------------- 拿铁(大杯/热/燕麦奶) 1 32.00 美式(中杯/冰) 2 20.00 -------------------------------- 合计: 72.00 取餐码: A32 -------------------------------- 感谢惠顾欢迎再次光临注意打印机的字符宽度58mm纸通常是32个字符一行的宽度超过会折行或乱码。汉字在ESC/POS指令里按2个字符宽度计算所以排版时要算好中英文混排的对齐。我自己写了一个补空格对齐的小工具核心思路就是先计算字符串的显示宽度中文算2英文算1再补空格到目标宽度。5. 常见问题与排查技巧实录5.1 支付报错“该商户暂不支持此支付方式”这个报错我在调试时遇到过最常见的原因是商户号的支付目录没有配置正确。在微信商户平台「产品中心-AppID账号管理」里需要关联小程序AppID并且在「开发配置-支付配置」里把小程序页面的合法域名和支付目录加上。如果你用的不是标准配置路径检查一下商户平台那边的产品权限是不是没开通JSAPI支付。5.2 导航栏适配在iOS上错位前面说过旧APIgetSystemInfoSync废弃的问题如果你拷贝的老代码还在用很可能会出现“iOS上状态栏高度正常但部分安卓机器上胶囊按钮跟导航栏重叠”的现象。解决办法是全部替换为wx.getWindowInfo。另外还有一个细节不要在onLoad里取导航栏信息并缓存因为部分安卓机型在冷启动时胶囊按钮的坐标可能还没来得及渲染拿到的值是错的。最好在首次onReady之后获取或者开启右上角的“进入时重新渲染”。5.3 蓝牙打印连上但打不出内容连上设备、数据也显示写成功但打印机没有任何反应这通常有三个原因写入的数据用的是writeBLECharacteristicValue但目标特征不支持写只支持通知需要在getBLEDeviceCharacteristics里查看properties是否包含write或writeNoResponse字节数组长度太长BLE单包传输一般限制在20字节左右需要分包写入每次写完要延时30ms以上再写下一包打印机关联的服务UUID和特征UUID不是标准的FFE0/FFE2不同品牌会有差异我在这个项目里写了一个分包函数每次发不超过17个字节问题就解决了。很多打印机对连续写入还没准备好所以分包延时一定要加够。5.4 输入框被软键盘遮挡热词里有人提“uniapp软键盘遮挡查询内容”这个问题在小程序里也很常见。微信官方推荐的做法是使用adjust-position属性配合cursor-spacing。在input上加上cursor-spacing120意思是让光标和输入框底部留有120px的间距键盘弹起时页面会自动上推。但如果你的页面里有固定定位的元素这个上推会失效需要手动用wx.onKeyboardHeightChange来监听键盘高度动态调整底部元素的位置。5.5 swiper嵌套video全屏错位这属于Media容器常见的层级问题星巴克小程序里也用到了swiper展示商品图和介绍视频。问题描述是iOS上video进入全屏后退出时页面错位。这个根因是swiper内部对webview的滚动位置做了缓存全屏切换时缓存被污染。目前比较推荐的解决方案是在video的fullscreenchange事件里监听全屏状态退出全屏时强制刷新swiper的current值比如先置为-1再恢复为原值。这个方法有点hack但实测能解决大多数错位问题。5.6 小程序违规导致支付禁用的应急处理热词里有人搜“由于小程序违规支付功能暂时无法使用”这个不是代码问题而是账号维度的风控。如果在开发调试时看到这个提示先确认你的AppID是不是个人主体。个人主体的小程序无法开通微信支付。如果主体没问题是企业那就要去微信公众平台查看站内信看具体是哪个类目资质不符合还是有什么用户投诉。不要试图绕过风控正确做法是提交申诉材料把资质补充完整。开发阶段建议准备一个测试专用的企业主体小程序避免主力账号受到影响。写在最后的经验总结这款咖啡小程序我自己前前后后迭代了三轮最大的心得是别把“星巴克”当成UI模板要当成一种服务流程来思考。真正难的不是把杯子画得像而是点单逻辑、规格选择、支付状态、取餐码这些链路数据的一致性。最开始我也在网上找过各种“星巴克源码”但大多数其实是套壳demo要么支付是假的要么购物车逻辑有严重bug。与其花时间在元代码上找补不如把核心链路吃透哪怕从零开始写最多也就两三天就能跑通一个能展示的demo。希望这篇拆解能帮你少走点弯路。本文还有配套的精品资源点击获取