# 消息推送处理虚拟支付回调
云开发支持通过云函数消息推送能力接收小程序虚拟支付的回调通知。开发者可以在云函数中处理支付结果、更新订单状态、发放虚拟商品等业务逻辑,无需搭建独立的服务器来接收回调。
# 功能说明
使用云函数接收虚拟支付回调具有以下特点:
- 无需搭建独立服务器,降低开发和运维成本
- 云函数运行在云端,回调处理更安全可靠
- 自动重试机制,确保消息送达(详见注意事项)
- 与云开发体系无缝集成,开发体验更友好
# 接入步骤
# 第 1 步:开通虚拟支付能力
在微信小程序管理后台开通虚拟支付能力。
参考文档:小程序虚拟支付接入指引
注意:iOS 端虚拟支付需要单独申请并满足额外条件,详见 iOS 端虚拟支付接入指引。
# 第 2 步:配置消息推送
在云开发控制台配置消息推送,将虚拟支付回调推送到云函数。
# 配置步骤
- 在开发者工具中,前往「云开发」-「设置」-「其他设置」-「消息推送」
- 选择推送模式为「云函数」
- 点击「添加消息推送配置」,根据业务需求配置相应的虚拟支付事件
- 为每个事件选择:
- 消息类型:
event - 事件类型:选择对应的虚拟支付事件(见下图)
- 云函数环境:选择目标云环境
- 云函数:选择用于接收回调的云函数
- 消息类型:
# 虚拟支付事件类型说明
| 事件类型 | 说明 |
|---|---|
| xpay_goods_deliver_notify | 道具发货通知,支付成功后推送,用于发放道具 |
| xpay_coin_pay_notify | 代币支付通知,使用代币购买道具后推送 |
| xpay_complaint_notify | 用户投诉通知,用户发起投诉时推送 |
| xpay_subscribe_signing_result_notify | 订阅签约结果通知,用户完成订阅签约后推送 |
| xpay_subscribe_pay_fail_notify | 订阅支付失败通知,订阅扣费失败时推送 |
| xpay_subscribe_ios_refund_query_notify | iOS 订阅退款问询通知。用户在 App Store 申请退款后,Apple 会向开发者发起退款问询(共 3 次),开发者需在 3 秒内 根据业务情况(发货状态、使用情况等)响应建议退款或拒绝退款。Apple 将参考开发者意见做出最终退款决定 |
| xpay_refund_notify | 退款通知,退款操作完成后推送(包括 iOS 退款最终审批通过) |
# 回调数据格式
虚拟支付回调推送到云函数时,event 对象包含以下通用字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| ToUserName | String | 小程序原始ID |
| FromUserName | String | 固定为微信官方的openid |
| CreateTime | Number | 消息发送时间 |
| MsgType | String | 消息类型,固定为 event |
| Event | String | 事件类型(如 xpay_goods_deliver_notify) |
| OpenId | String | 用户openid |
| OutTradeNo | String | 业务订单号 |
| Env | Number | 环境配置(0:现网环境,1:沙箱环境) |
不同事件类型的详细字段说明请参考:小程序虚拟支付消息推送
# iOS 订阅退款问询事件参数
xpay_subscribe_ios_refund_query_notify 事件包含以下专属字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| refund_time | String | 退款问询时间,Unix 时间戳 |
| order_time | String | 该笔退款对应的原订单交易时间,Unix 时间戳 |
| channel_bill | String | Apple 支付票据号 |
| bundleid | String | 应用的 Apple bundleid |
| product_id | String | 道具 ID |
| p_count | String | 道具/代币数量 |
| refund_request_reason | String | 用户请求退款的原因 |
| provide_status | String | 发货状态(0: 未发货,1: 已发货,2: 发货中) |
| pay_order_id | String | 退款对应的支付订单号 |
特别说明:
- Apple 会连续发起 3 次 退款问询
- 开发者必须在 3 秒内 返回响应
- 如果 3 次问询均超时未应答,微信将向 Apple 返回「不确定」,退款决定权完全由 Apple 处理
- 开发者的响应仅作为参考,最终退款决定由 Apple 做出
- Apple 最终批准退款后,会通过
xpay_refund_notify事件通知开发者
# 第 3 步:创建回调云函数
创建一个云函数用于接收虚拟支付回调通知。例如使用同一个云函数处理所有虚拟支付事件,根据 Event 字段判断事件类型并执行相应的业务逻辑。
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
// 云函数入口函数
exports.main = async (event, context) => {
console.log('收到虚拟支付回调:', event)
try {
const { Event, OpenId, OutTradeNo } = event
// 根据不同的事件类型处理
switch (Event) {
case 'xpay_goods_deliver_notify':
// TODO: 处理道具发货
// 1. 检查订单是否已处理(幂等性)
// 2. 发放道具到用户账户
// 3. 更新订单状态
break
case 'xpay_coin_pay_notify':
// TODO: 处理代币支付
break
case 'xpay_refund_notify':
// TODO: 处理退款
break
case 'xpay_complaint_notify':
// TODO: 处理用户投诉
break
case 'xpay_subscribe_signing_result_notify':
// TODO: 处理订阅签约结果
break
case 'xpay_subscribe_pay_fail_notify':
// TODO: 处理订阅支付失败
break
case 'xpay_subscribe_ios_refund_query_notify':
// 处理 iOS 订阅退款问询(必须在 3 秒内响应,返回格式不同)
// 根据业务规则判断是否建议退款
return {
result_code: 0, // 0-建议退款,1-拒绝退款
result_info: '建议退款',
evidence: '订单未发货,建议退款' // 必填,说明决策依据
}
default:
console.log('未知事件类型:', Event)
}
// 返回成功响应
return {
ErrCode: 0,
ErrMsg: 'success'
}
} catch (error) {
console.error('处理回调失败:', error)
// 返回错误,微信会自动重试
return {
ErrCode: -1,
ErrMsg: error.message
}
}
}
# 第 4 步:小程序端发起虚拟支付
在小程序中调用 wx.requestVirtualPayment 发起虚拟支付。该 API 需要基础库 2.19.2 及以上版本支持。
参考文档:
# 道具直购示例
wx.requestVirtualPayment({
// 支付参数(需转为 JSON 字符串)
signData: JSON.stringify({
offerId: '123', // 米大师侧申请的应用 id
buyQuantity: 1, // 购买数量
env: 0, // 0:正式环境,1:沙箱环境
currencyType: 'CNY', // 币种
productId: 'product_001', // 道具 ID(道具直购时必填)
goodsPrice: 100, // 道具单价,单位:分(道具直购时必填)
outTradeNo: 'ORDER_' + Date.now(), // 业务订单号(8-32位)
attach: 'extra_data' // 透传数据,发货通知时会返回
}),
paySig: 'xxx', // 支付签名,详见签名文档
signature: 'xxx', // 用户态签名,详见签名文档
mode: 'short_series_goods', // 道具直购
success: (res) => {
console.log('支付成功', res)
// 注意:不要在此处发货!
// 支付成功后,微信会推送回调到云函数,在云函数中发货
},
fail: ({ errMsg, errCode }) => {
console.error('支付失败', errMsg, errCode)
}
})
# 代币充值示例
wx.requestVirtualPayment({
signData: JSON.stringify({
offerId: '123',
buyQuantity: 100, // 充值代币数量
env: 0,
currencyType: 'CNY',
outTradeNo: 'COIN_' + Date.now(),
attach: 'coin_recharge'
}),
paySig: 'xxx',
signature: 'xxx',
mode: 'short_series_coin', // 代币充值
success: (res) => {
console.log('充值成功', res)
},
fail: ({ errMsg, errCode }) => {
console.error('充值失败', errMsg, errCode)
}
})
# 注意事项
# 1. 必须返回成功响应
云函数处理完回调后,必须返回 { "ErrCode": 0 } 表示处理成功,否则微信会认为回调失败并持续重试。
// 正确的返回格式
return {
ErrCode: 0,
ErrMsg: 'success'
}
# 2. 幂等性处理
由于网络等原因,可能会收到重复的回调通知,需要在业务逻辑中做好幂等性处理:
// 使用订单号作为唯一标识,避免重复处理
const order = await db.collection('virtual_orders').where({
bill_no: bill_no
}).get()
if (order.data.length > 0 && order.data[0].status === 'delivered') {
console.log('订单已处理,跳过')
return {
ErrCode: 0,
ErrMsg: 'success'
}
}
# 3. iOS 退款问询特殊处理
xpay_subscribe_ios_refund_query_notify 事件与其他回调事件有重要区别:
# 响应格式不同
iOS 退款问询需要返回 专用响应格式,而非标准的 { ErrCode: 0 } 格式:
// iOS 退款问询响应格式
return {
result_code: 0, // 0-建议退款,1-拒绝退款
result_info: '建议退款', // 结果描述
evidence: '订单未发货,建议退款' // 决策依据(必填)
}
// 其他虚拟支付回调的响应格式
return {
ErrCode: 0,
ErrMsg: 'success'
}
# 响应时限要求
- 必须在 3 秒内响应,否则视为超时
- Apple 会连续问询 3 次,3 次超时后微信将返回「不确定」给 Apple
- 建议将业务逻辑优化到 1 秒内完成
# evidence 字段要求
evidence 字段是必填项,需要详细说明建议退款或拒绝退款的依据,用于退款审计。示例:
// 好的 evidence 示例
evidence: '订单未发货,建议退款'
evidence: '用户已使用 100 个道具,累计消耗 80%,不建议退款'
evidence: '用户已激活 VIP 权益并使用 15 天,不建议退款'
// 不好的 evidence 示例
evidence: '不同意' // 过于简单
evidence: '' // 空字符串
# 退款流程说明
- 用户在 App Store 申请退款(无法通过开发者主动发起)
- Apple 发起退款问询 → 开发者响应(建议/拒绝)
- Apple 根据自身策略做出最终决定(开发者响应仅供参考)
- 退款批准后 → 触发
xpay_refund_notify事件 → 开发者回收虚拟资产
# 4. 发货可靠性保障
重要:由于 wx.requestVirtualPayment 的 success 回调可能因异常(如微信退出)而丢失,不能在小程序端 success 回调中发货。
必须通过以下至少一种方式保证发货:
# 方式一:云函数消息推送(推荐)
在云函数中接收 xpay_goods_deliver_notify 事件并发货(本文档介绍的方式)。
# 方式二:轮询查询
小程序端支付后,通过云函数轮询调用 /xpay/query_order 接口查询订单状态,确认支付成功后发货。
// 云函数:queryOrderStatus
const cloud = require('wx-server-sdk')
cloud.init()
exports.main = async (event, context) => {
const { bill_no } = event
// 调用查询订单接口
// 注意:需要自行实现 HTTP 请求和签名逻辑
const result = await queryOrderFromWechat(bill_no)
if (result.order_status === 'paid') {
// 发货
await deliverGoods(bill_no)
}
return result
}
建议:同时实现消息推送和轮询查询两种方式,提高可靠性。