云开发出现之前,小程序要做微信支付,必须自己购买服务器、部署后端、申请HTTPS证书、配置签名逻辑,整个链路又长又容易出错。而借助云开发的云支付能力,统一下单可以直接在云函数里完成,签名和加密全部由SDK内部处理,开发者只需要关注业务参数本身。本文以cloudPay.unifiedOrder为核心,把从零到调起支付收款的完整流程拆开讲清楚,并汇总实际开发中踩过的坑。

准备工作:云开发环境与商户号开通
在写任何代码之前,有三件事必须先完成,缺一不可。第一是小程序必须开通云开发环境,在微信开发者工具顶部点击云开发按钮,按提示创建环境即可,建议环境名和备注写清楚,避免后期多环境混乱。第二是申请微信支付商户号,这一步在小程序管理后台的微信支付入口操作,如果还没有商户号,需要先去微信支付平台申请,准备好营业执照、对公账户等资料,审核通常需要一到三个工作日。
第三步也是最容易被忽略的一步:绑定商户号。商户号申请下来后,需要在云开发控制台(或通过wx-server-sdk的云函数)将商户号与云开发环境绑定。绑定时需要用到商户号的API密钥,这个密钥在商户平台的账户中心设置,一旦设置请妥善保管,泄露后别人可以直接操作你的资金接口。绑定成功后,系统会返回一个subMchId(子商户号),后面调用统一下单时会用到。
需要注意,绑定的商户号主体最好与小程序主体一致,或者通过服务商模式关联。如果两者主体不一致又没有服务商关联,下单时会直接报错,报错信息往往不够直观,容易让人误以为是代码问题。
编写云函数调用统一下单接口
准备工作就绪后,就可以写云函数了。先在小程序项目里新建一个云函数目录(比如叫pay),然后在目录下执行npm install安装wx-server-sdk依赖。云函数的核心是调用cloud.cloudPay.unifiedOrder方法,传入订单号、金额、商品描述、回调云函数名等参数。
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
const res = await cloud.cloudPay.unifiedOrder({
body: '小程序商城-商品订单', // 商品描述
outTradeNo: 'ORDER' + Date.now(), // 商户侧订单号,需保证唯一
spbillCreateIp: '127.0.0.1', // 云函数场景可写本机IP
subMchId: '1900000001', // 绑定后得到的子商户号
totalFee: 100, // 订单金额,单位为分
envId: 'your-env-id', // 云开发环境ID
functionName: 'payCallback' // 支付回调云函数名
})
return res.payment
}这段代码有几个关键点值得展开说明。outTradeNo是商户自己生成的订单号,同一个商户号下不能重复,重复的话微信会返回订单号已存在的错误,实践中常见的做法是前缀加时间戳加随机数。totalFee的单位是分而不是元,这是新手最容易犯的错误之一,100代表一元,如果传了10000用户就要付一百元,上线前务必反复核对。envId和functionName指定了支付成功后微信服务器回调哪个环境的哪个云函数,回调函数不需要暴露任何公网地址,这正是云开发支付相对传统方式最大的优势。
返回值中的res.payment是给小程序端用的支付参数对象,直接返回即可,不需要自己再拼装签名。如果返回结果中没有payment字段,多半是下单参数有问题,建议把完整返回打出来排查,错误码和错误描述都会包含在里面。
小程序端调起支付与回调函数处理
云函数部署好之后,小程序端通过wx.cloud.callFunction调用它,拿到payment对象后直接传给wx.requestPayment,就能拉起支付界面。
wx.cloud.callFunction({
name: 'pay',
data: {
orderId: 'xxx'
}
}).then(res => {
const payment = res.result
wx.requestPayment({
...payment,
success: () => {
// 前端感知支付成功,真正状态以回调为准
console.log('用户完成支付')
},
fail: (err) => {
console.log('支付取消或失败', err)
}
})
})这里有一个原则必须强调:前端的success回调只能作为交互提示,不能作为发货依据。用户可以通过某些手段伪造前端回调,真正的支付结果要以服务端(云函数)收到的回调为准。所以还需要写一个payCallback云函数,专门接收微信的支付结果通知。
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
exports.main = async (event) => {
// event 中包含 returnCode、outTradeNo、transactionId 等字段
if (event.returnCode === 'SUCCESS' && event.resultCode === 'SUCCESS') {
const db = cloud.database()
await db.collection('orders').where({
outTradeNo: event.outTradeNo
}).update({
data: {
status: 'paid',
transactionId: event.transactionId,
paidAt: new Date()
}
})
}
return { errcode: 0, errmsg: 'OK' }
}回调云函数里拿到outTradeNo后去数据库更新订单状态,同时保存transactionId(微信支付订单号),后续退款和对账都要用它。回调函数必须正常返回,否则微信会重复推送通知,虽然重复推送本身可以当作一种重试保障,但最好还是在更新订单时做幂等处理,比如先判断订单状态再决定是否更新。
常见问题与注意事项汇总
第一类问题是参数错误。body商品描述不能为空,建议包含中文描述但不要带特殊符号;totalFee必须为正整数且单位是分;subMchId填错会报商户号与appid不匹配的错误,要确认绑定关系是否生效,绑定后偶尔需要几分钟同步时间,不要刚绑定就立刻测试。
第二类问题是回调收不到。排查思路是:确认回调云函数名与下单时传的functionName完全一致,包括大小写;确认envId填写的是正确的环境ID;确认回调云函数已经上传部署。另外回调函数里如果抛出异常,微信会认为通知失败并重试,所以函数内部要尽量用try-catch包住业务逻辑,保证总能返回成功。
第三类问题是重复订单号。同一个outTradeNo在未支付状态下再次下单会报错,业务上应该在生成订单时就把它存入数据库,并在用户取消或超时后生成新订单号重新发起支付。还有一点容易被忽视:云函数返回给前端的payment对象有时效性,生成后如果用户长时间不支付,再次拉起会失败,此时需要重新调用下单云函数获取新的支付参数。
最后是安全方面。云函数内部要校验请求来源,比如校验openid是否属于该订单的用户,防止有人用别人的openid给别人的订单付款或探测接口;金额不要由前端直接传入,应由服务端根据订单号查库计算,否则改包工具可以直接篡改价格。把这几条守住,一个稳定可用的支付流程就基本成型了。