简介:这是一份用于学习微信小程序开发的完整星巴克咖啡门店界面源码,适合小程序初学者以及想提升移动端界面布局与交互设计能力的开发者。项目中通过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和蓝牙等底层能力会有兼容坑 | 需要同时出小程序+App |
| Taro | React语法、社区活跃、组件生态好 | 对新手不友好、自定义原生组件时比较绕 | 团队本身就是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输出里的serial=XXXXXXXX才是你要填的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_id=xxxx" signType: 'RSA', paySign: payParams.paySign, success: resolve, fail: (err) => { // err.errMsg === 'requestPayment:fail cancel' 表示用户主动取消 reject(err); } }); }); }这里有一个很多教程没提的细节:package字段的值必须是完整的prepay_id=xxx,不能只传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-spacing="120",意思是让光标和输入框底部留有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。希望这篇拆解能帮你少走点弯路。
本文还有配套的精品资源,点击获取