# 消息推送能力处理小程序虚拟支付回调

云开发支持通过[消息推送](/miniprogram/dev/wxcloudservice/wxcloud/guide/functions/openapi)能力接收小程序虚拟支付的回调通知。开发者可以在云函数中处理支付结果、更新订单状态、发放虚拟商品等业务逻辑，无需搭建独立的服务器来接收回调。

## 功能说明

使用云函数接收虚拟支付回调具有以下特点：

- 无需搭建独立服务器，降低开发和运维成本
- 云函数运行在云端，回调处理更安全可靠
- 自动重试机制，确保消息送达（详见注意事项）
- 与云开发体系无缝集成，开发体验更友好

## 接入步骤

### 第 1 步：开通虚拟支付能力

在微信小程序管理后台开通虚拟支付能力。

参考文档：[小程序虚拟支付接入指引](/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.md)

> **注意**：iOS 端虚拟支付需要单独申请并满足额外条件，详见 [iOS 端虚拟支付接入指引](/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.md)。

### 第 2 步：配置消息推送

在云开发控制台配置消息推送，将虚拟支付回调推送到云函数。

#### 配置步骤

1. 在开发者工具中，前往「云开发」-「设置」-「其他设置」-「消息推送」
2. 选择推送模式为「云函数」
3. 点击「添加消息推送配置」，根据业务需求配置相应的虚拟支付事件
4. 为每个事件选择：
   - **消息类型**：`event`
   - **事件类型**：选择对应的虚拟支付事件（见下图）
   - **云函数环境**：选择目标云环境
   - **云函数**：选择用于接收回调的云函数

![虚拟支付事件配置](https://res8.wxqcloud.qq.com.cn/wxdoc/3d35ef43-65bc-4621-a212-b7a0fd43187e.png)

#### 虚拟支付事件类型说明

| 事件类型 | 说明 |
|---------|------|
| 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：沙箱环境） |

不同事件类型的详细字段说明请参考：[小程序虚拟支付消息推送](/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.md#%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81)

#### 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` 字段判断事件类型并执行相应的业务逻辑。

```js
// 云函数入口文件
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 {
          ErrCode: 0,
          ErrMsg: 'success',
          IosRefundQueryResponse: {
            result_code: 1,           // 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 API](/miniprogram/dev/api/payment/wx.requestVirtualPayment.md)
- [签名详解](/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.md#_2-4-%E7%AD%BE%E5%90%8D%E8%AF%A6%E8%A7%A3)

#### 道具直购示例

```js
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)
  }
})
```

#### 代币充值示例

```js
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)
  }
})
```

### 第 5 步：应答回调处理

云函数接收到虚拟支付回调后，需要根据不同事件类型返回对应的应答格式。

#### 标准应答格式

对于大部分虚拟支付回调事件（如 `xpay_goods_deliver_notify`、`xpay_coin_pay_notify`、`xpay_refund_notify` 等），云函数处理完业务逻辑后，必须返回以下格式表示处理成功：

```js
return {
  ErrCode: 0,
  ErrMsg: 'success'
}
```

若返回非 0 或未正确返回，微信会认为回调处理失败并持续重试。

#### iOS 退款问询应答格式

`xpay_subscribe_ios_refund_query_notify` 事件的应答格式与其他事件不同，需要在标准应答基础上额外包含 `IosRefundQueryResponse` 对象：

**应答参数：**

| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| result_code | int32 | 是 | 结果码。0-放过，建议退款；1-拦截，拒绝退款 |
| result_info | string | 否 | 结果描述 |
| evidence | string | 是 | 决策凭据，业务需返回建议退款/拒绝退款的依据，用于退款审计 |

```js
return {
  ErrCode: 0,
  ErrMsg: 'success',
  IosRefundQueryResponse: {
    result_code: 1,           // 0-放过，建议退款；1-拦截，拒绝退款
    result_info: '拒绝退款',   // 选填，结果描述
    evidence: '已在购买页注明，此数字内容购买后不支持退款'  // 必填，决策凭据，用于退款审计
  }
}
```

**evidence 字段要求：**

`evidence` 字段是**必填项**，需要详细说明建议退款或拒绝退款的决策凭据，用于退款审计。示例：

```js
// 好的 evidence 示例
evidence: '订单未发货，建议退款'
evidence: '用户已使用 100 个道具，累计消耗 80%，不建议退款'
evidence: '用户已激活 VIP 权益并使用 15 天，不建议退款'

// 不好的 evidence 示例
evidence: '不同意'  // 过于简单
evidence: ''        // 空字符串
```

> **注意**：
> - **必须在 3 秒内响应**，否则视为超时
> - Apple 会连续问询 3 次，3 次超时后微信将返回「不确定」给 Apple
> - 建议将业务逻辑优化到 1 秒内完成

## 注意事项

### 1. 必须返回成功响应

云函数处理完回调后，**必须返回** `{ "ErrCode": 0 }` 表示处理成功，否则微信会认为回调失败并持续重试。

```js
// 正确的返回格式
return {
  ErrCode: 0,
  ErrMsg: 'success'
}
```

### 2. 幂等性处理

由于网络等原因，可能会收到重复的回调通知，需要在业务逻辑中做好幂等性处理：

```js
// 使用订单号作为唯一标识，避免重复处理
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` 事件与其他回调事件有重要区别，应答格式、响应时限及 `evidence` 字段要求详见第 5 步：应答回调处理。

#### 退款流程说明

1. 用户在 **App Store** 申请退款（无法通过开发者主动发起）
2. Apple 发起退款问询 → 开发者响应（建议/拒绝）
3. Apple 根据自身策略做出**最终决定**（开发者响应仅供参考）
4. 退款批准后 → 触发 `xpay_refund_notify` 事件 → 开发者回收虚拟资产

### 4. 发货可靠性保障

**重要**：由于 `wx.requestVirtualPayment` 的 success 回调可能因异常（如微信退出）而丢失，**不能在小程序端 success 回调中发货**。

必须通过以下至少一种方式保证发货：

#### 方式一：云函数消息推送（推荐）

在云函数中接收 `xpay_goods_deliver_notify` 事件并发货（本文档介绍的方式）。

#### 方式二：轮询查询

小程序端支付后，通过云函数轮询调用 `/xpay/query_order` 接口查询订单状态，确认支付成功后发货。

```js
// 云函数：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
}
```

**建议**：同时实现消息推送和轮询查询两种方式，提高可靠性。
