---
url: "https://pay.weixin.qq.com/doc/v3/merchant/4018940876.md"
title: "业务示例代码"
doc_id: 4018940876
---

>更新时间：2026.04.23

## 1. 目标

通过本教程的学习，你应该可以：

- 通过[发起转账](https://pay.weixin.qq.com/doc/v3/merchant/4012716434.md)接口，向指定微信用户发起商家转账（用户确认模式），并引导用户在微信内确认收款

- 通过[撤销转账](https://pay.weixin.qq.com/doc/v3/merchant/4012716458.md)接口，在用户确认收款之前，可撤销转账

- 通过[商户单号查询转账单](https://pay.weixin.qq.com/doc/v3/merchant/4012716437.md)或[微信转账单号查询转账单](https://pay.weixin.qq.com/doc/v3/merchant/4012716457.md)查询转账单的当前状态

- 通过[商家转账回调通知](https://pay.weixin.qq.com/doc/v3/merchant/4012712115.md)处理微信支付下发的回调，识别转账终态（成功/失败/撤销）

- 通过[商户单号申请电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716452.md)、[商户单号查询电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716436.md)、[微信单号申请电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716456.md)、[微信单号查询电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716455.md)申请并查询转账单的电子回单，通过[下载电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4013866774.md)下载回单文件

- 正确处理接口返回的错误码和转账单据状态，针对不同情况给出对应的业务处理逻辑


## 2. 前置准备

### 2.1 商户配置

在调用任何接口之前，需要先创建商户配置。以下配置在所有接口调用中通用：

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
)

// NewMchConfig 创建商户配置
func NewMchConfig() (*wxpay_utility.MchConfig, error) {
	return wxpay_utility.CreateMchConfig(
		"19xxxxxxxx",                 // 商户号，是由微信支付系统生成并分配给每个商户的唯一标识符，商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
		"1DDE55AD98Exxxxxxxxxx",      // 商户API证书序列号，如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径，本地文件路径
		"PUB_KEY_ID_xxxxxxxxxxxxx",   // 微信支付公钥ID，如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径，本地文件路径
	)
}
```

## 3. 转账到用户零钱

本章节按照商户实际开发流程，介绍如何完成一笔完整的商家转账操作。

转账单状态流转

![](https://gtimg.wechatpay.cn/resource/xres/mmpaydoc/static/img/ff6f543e9aa7d659908fef44ceaf5902.png)

### 3.1 发起转账

商户调用[发起转账](https://pay.weixin.qq.com/doc/v3/merchant/4012716434.md)接口创建转账单。接口调用成功后，转账单进入 `WAIT_USER_CONFIRM`（待用户确认）状态，接口返回的 `package_info` 将用于后续拉起微信收款确认页面。

关键参数说明：

- `appid` / `openid`：需保证一致性，后续拉起确认收款页面时也需使用相同的 appid 和 openid

- `notify_url`（可选）：如需被动接收转账终态通知，在此传入回调地址

- `user_name`：收款用户姓名，转账金额 ≥ 2000 元时必填，需使用微信支付公钥加密

- `transfer_scene_id`：转账场景ID，需在商户平台提前申请


示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleTransferToUser 发起商家转账（用户确认模式），参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716434
func HandleTransferToUser(config *wxpay_utility.MchConfig) error {
	// 使用发起转账接口：POST /v3/fund-app/mch-transfer/transfer-bills，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716434
	// 收款用户姓名，转账金额=2000元时必填；需使用微信支付公钥加密，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013053257
	encryptedUserName, err := wxpay_utility.EncryptOAEPWithPublicKey("张三", config.WechatPayPublicKey())
	if err != nil {
		return err
	}

	req := &TransferToUserRequest{
		// 商户应用唯一标识，与商户号有绑定关系，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013070756
		Appid: wxpay_utility.String("wxf636efh567hg4356"),
		// 商户系统内部的商家单号，由数字、大小写字母组成，在商户系统内部唯一
		OutBillNo: wxpay_utility.String("plfk2020042013"),
		// 转账场景ID，可前往"商户平台-产品中心-商家转账"中申请，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013774588
		TransferSceneId: wxpay_utility.String("1000"),
		// 收款用户在商户appid下的唯一标识，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013070756
		Openid: wxpay_utility.String("o-MYE42l80oelYMDE34nYD456Xoy"),
		// 收款用户姓名；需使用微信支付公钥加密，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013053257
		UserName: wxpay_utility.String(encryptedUserName),
		// 转账金额，单位为"分"，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716434
		TransferAmount: wxpay_utility.Int64(400000),
		// 转账备注，用户收款时可见，UTF8编码，最多允许32个字符
		TransferRemark: wxpay_utility.String("新会员开通有礼"),
		// 异步接收微信支付结果通知的回调地址，必须为HTTPS公网地址
		NotifyUrl: wxpay_utility.String("https://www.weixin.qq.com/wxpay/pay.php"),
		// 用户收款时感知的收款原因，不填将展示转账场景的默认内容
		UserRecvPerception: wxpay_utility.String("现金奖励"),
		// 转账场景报备信息，需按转账场景准确填写，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013774588
		TransferSceneReportInfos: []TransferSceneReportInfo{
			{
				InfoType:    wxpay_utility.String("活动名称"),
				InfoContent: wxpay_utility.String("新会员有礼"),
			},
			{
				InfoType:    wxpay_utility.String("奖励说明"),
				InfoContent: wxpay_utility.String("注册会员抽奖一等奖"),
			},
		},
	}

	resp, err := TransferToUser(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理发起转账接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：请求不符合业务规则
			// 解决方式：请参阅产品介绍、开发指引和接口规则，确认请求参数正确
		case "NO_AUTH":
			// 错误：NO_AUTH
			// 描述：没有相关权限
			// 解决方式：请参阅产品介绍、开发指引，确认已开通商家转账权限
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：签名验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试，若多次失败请通过查单接口确认订单状态后再决定是否重试
		case "NOT_ENOUGH":
			// 错误：NOT_ENOUGH
			// 描述：资金不足
			// 解决方式：确认出资商户余额充足
		case "FREQUENCY_LIMIT_EXCEED":
			// 错误：FREQUENCY_LIMIT_EXCEED
			// 描述：频率超限
			// 解决方式：接口请求频率超限，当前请求结果不明确，请降低请求接口频率后，使用相同参数重试
		case "RATELIMIT_EXCEEDED":
			// 错误：RATELIMIT_EXCEEDED
			// 描述：频率超限
			// 解决方式：接口或同一单号请求频率超限，当前请求结果不明确，请降低请求接口频率，使用相同参数重试
		case "FREQUENCY_LIMIT":
			// 错误：FREQUENCY_LIMIT
			// 描述：频率超限
			// 解决方式：接口或同一单号请求频率超限，当前请求结果不明确，请降低请求接口频率，使用相同参数重试
		case "ALREADY_EXISTS":
			// 错误：ALREADY_EXISTS
			// 描述：单号重复，商家单号已存在
			// 解决方式：请通过查单接口确认当前单号的转账状态，根据订单的实际状态判断是否需要进行用户确认等后续操作
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理发起转账的单据状态
func handleStatus(resp *TransferToUserResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case TRANSFERBILLSTATUS_ACCEPTED:
		// 转账已受理，可原单重试（非终态）
		if resp.TransferBillNo == nil {
			return fmt.Errorf("transfer_bill_no is nil")
		}
	case TRANSFERBILLSTATUS_PROCESSING:
		// 转账锁定资金中，如果一直停留在该状态，建议检查账户余额是否足够（非终态）
		if resp.TransferBillNo == nil {
			return fmt.Errorf("transfer_bill_no is nil")
		}
	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
		// 待收款用户确认，当前转账单据资金已锁定，可拉起微信收款确认页面进行收款确认（非终态）。
		// 商户APP通过微信Open SDK的sendReq方法拉起用户确认收款页, 参考APP调起用户确认收款的指引
		// https://pay.weixin.qq.com/doc/v3/merchant/4012719576
		// https://pay.weixin.qq.com/doc/v3/merchant/4012719578
		if resp.PackageInfo != nil {
			fmt.Printf("package信息: %s\n", *resp.PackageInfo)
		}
	case TRANSFERBILLSTATUS_TRANSFERING:
		// 转账中，可拉起微信收款确认页面再次重试确认收款（非终态）
	case TRANSFERBILLSTATUS_SUCCESS:
		// 转账成功，表示转账单据已成功（终态）
	case TRANSFERBILLSTATUS_FAIL:
		// 转账失败，该笔转账单据已失败，若需重新向用户转账，请重新生成单据并再次发起（终态）
	case TRANSFERBILLSTATUS_CANCELING:
		// 转账撤销中，商户撤销请求受理成功，该笔转账正在撤销中，需查单确认撤销的转账单据状态（非终态）
	case TRANSFERBILLSTATUS_CANCELLED:
		// 转账撤销完成，代表转账单据已撤销成功（终态）
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

### 3.2 拉起用户确认收款页面

发起转账成功后，转账单进入 `WAIT_USER_CONFIRM` 状态，此时需要在前端拉起微信收款确认页面，引导用户确认收款。

注意事项：

- 拉起确认收款页面时使用的 `appid` 和 `openid` 必须与发起转账时传入的一致

- 拉起前建议先调用查询接口确认转账单仍处于 `WAIT_USER_CONFIRM` 状态

- `package_info` 从发起转账接口的返回结果中获取


不同端的调起方式：

- APP 端（Android）：通过微信 OpenSDK 的 `sendReq` 方法拉起，参考 [Android 调起用户确认收款](https://pay.weixin.qq.com/doc/v3/merchant/4012719576.md)

- APP 端（iOS）：通过微信 OpenSDK 的 `sendReq` 方法拉起，参考 [iOS 调起用户确认收款](https://pay.weixin.qq.com/doc/v3/merchant/4012719578.md)

- 小程序 / H5 端：通过 JSAPI 调起，参考 [JSAPI 调起用户确认收款](https://pay.weixin.qq.com/doc/v3/merchant/4012716430.md)


### 3.3 查询转账单状态

在转账流程的各个阶段，商户都可以主动查询转账单的当前状态。支持两种查询方式：

#### 3.3.1 通过商户单号查询

使用商户系统内部的商家单号查询转账单状态，接口文档参考：[商户单号查询转账单](https://pay.weixin.qq.com/doc/v3/merchant/4012716437.md)

适用场景：

- 拉起确认收款页面前，确认转账单仍处于 `WAIT_USER_CONFIRM` 状态

- 撤销转账前，确认转账单尚未被用户确认

- 发起转账后，主动轮询确认转账终态


示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleGetTransferBillByOutNo 通过商户单号查询转账单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716437
func HandleGetTransferBillByOutNo(config *wxpay_utility.MchConfig) error {
	// 使用商户单号查询转账单接口：GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
	req := &GetTransferBillByOutNoRequest{
		// 商户系统内部的商家单号，在商户系统内部唯一
		OutBillNo: wxpay_utility.String("plfk2020042013"),
	}

	resp, err := GetTransferBillByOutNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理商户单号查询转账单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理查询转账单的单据状态
func handleStatus(resp *TransferBillEntity) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case TRANSFERBILLSTATUS_ACCEPTED:
		// 转账已受理，可原单重试（非终态）
	case TRANSFERBILLSTATUS_PROCESSING:
		// 转账锁定资金中，如果一直停留在该状态，建议检查账户余额是否足够（非终态）
	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
		// 待收款用户确认，当前转账单据资金已锁定（非终态）
	case TRANSFERBILLSTATUS_TRANSFERING:
		// 转账中（非终态）
	case TRANSFERBILLSTATUS_SUCCESS:
		// 转账成功（终态）
	case TRANSFERBILLSTATUS_FAIL:
		// 转账失败，该笔转账单据已失败（终态）
		if resp.FailReason != nil {
			// 失败原因详见 fail_reason 字段
		}
	case TRANSFERBILLSTATUS_CANCELING:
		// 转账撤销中（非终态）
	case TRANSFERBILLSTATUS_CANCELLED:
		// 转账撤销完成（终态）
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

#### 3.3.2 通过微信转账单号查询

使用微信支付系统返回的转账单号查询转账单状态，接口文档参考：[微信转账单号查询转账单](https://pay.weixin.qq.com/doc/v3/merchant/4012716457.md)

适用场景：

- 通过回调通知获取到微信转账单号后，进一步查询详细信息


示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleGetTransferBillByNo 通过微信转账单号查询转账单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716457
func HandleGetTransferBillByNo(config *wxpay_utility.MchConfig) error {
	// 使用微信转账单号查询转账单接口：GET /v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}
	req := &GetTransferBillByNoRequest{
		// 微信转账单号，微信商家转账系统返回的唯一标识
		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
	}

	resp, err := GetTransferBillByNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理微信转账单号查询转账单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理查询转账单的单据状态
func handleStatus(resp *TransferBillEntity) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case TRANSFERBILLSTATUS_ACCEPTED:
		// 转账已受理，可原单重试（非终态）
	case TRANSFERBILLSTATUS_PROCESSING:
		// 转账锁定资金中，如果一直停留在该状态，建议检查账户余额是否足够（非终态）
	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
		// 待收款用户确认，当前转账单据资金已锁定（非终态）
	case TRANSFERBILLSTATUS_TRANSFERING:
		// 转账中（非终态）
	case TRANSFERBILLSTATUS_SUCCESS:
		// 转账成功（终态）
	case TRANSFERBILLSTATUS_FAIL:
		// 转账失败，该笔转账单据已失败（终态）
		if resp.FailReason != nil {
			// 失败原因详见 fail_reason 字段
		}
	case TRANSFERBILLSTATUS_CANCELING:
		// 转账撤销中（非终态）
	case TRANSFERBILLSTATUS_CANCELLED:
		// 转账撤销完成（终态）
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

### 3.4 撤销转账

在用户确认收款之前，商户可以对转账单发起撤销操作。撤销后转账单状态流转为 `CANCELING` → `CANCELLED`。

注意事项：

- 可撤销的状态： `ACCEPTED`（转账已受理）、 `PROCESSING`（转账锁定资金中）、 `WAIT_USER_CONFIRM`（待用户确认），即用户确认收款之前的状态均可撤销

- 撤销前建议先调用查询接口确认转账单状态

- 撤销操作为异步处理，需通过查询接口确认最终撤销结果


接口文档参考：[撤销转账](https://pay.weixin.qq.com/doc/v3/merchant/4012716458.md)

示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleCancelTransfer 撤销转账，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716458
func HandleCancelTransfer(config *wxpay_utility.MchConfig) error {
	// 使用撤销转账接口：POST /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel
	req := &CancelTransferRequest{
		// 商户系统内部的商家单号，在商户系统内部唯一
		OutBillNo: wxpay_utility.String("plfk2020042013"),
	}

	resp, err := CancelTransfer(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理撤销转账接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理撤销转账的单据状态
func handleStatus(resp *CancelTransferResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case "CANCELING":
		// 转账撤销中，商户撤销请求受理成功，该笔转账正在撤销中（非终态），请通过查单接口确认撤销状态
	case "CANCELLED":
		// 转账撤销完成，代表转账单据已撤销成功（终态）
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

### 3.5 接收转账回调通知

如果商户在发起转账时传入了 `notify_url`，当转账单到达终态（ `SUCCESS` / `FAIL` / `CANCELLED`）时，微信支付会通过回调通知商户。

注意事项：

- 接收回调通知前，商户需要先设置[APIv3密钥](https://pay.weixin.qq.com/doc/v3/merchant/4013053267.md)

- 回调通知是被动接收方式，与主动查询互为补充

- 收到回调后应先返回 HTTP 200 应答，再异步处理业务逻辑，避免应答超时

- 需要对回调内容进行签名验证，确保通知来源可信

- 商户侧对微信支付回调IP有防火墙策略限制的，请参考[回调通知注意事项](https://pay.weixin.qq.com/doc/v3/merchant/4012075420.md#2.-%E5%9B%9E%E8%B0%83%E5%A4%84%E7%90%86%E9%80%BB%E8%BE%91%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)


回调文档参考：[商家转账回调通知](https://pay.weixin.qq.com/doc/v3/merchant/4012712115.md)

示例代码

```
package notify

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

// MchTransferNotify 商家转账回调通知内容，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012712115
// resource 中 ciphertext 解密后字段
type MchTransferNotify struct {
	// 商户系统内部的商家单号，在商户系统内部唯一
	OutBillNo string `json:"out_bill_no"`
	// 微信单号，微信商家转账系统返回的唯一标识
	TransferBillNo string `json:"transfer_bill_no"`
	// 商家转账订单状态：SUCCESS-转账成功，FAIL-转账失败，CANCELLED-已撤销
	State string `json:"state"`
	// 微信支付分配的商户号
	MchId string `json:"mch_id"`
	// 转账总金额，单位为"分"
	TransferAmount int64 `json:"transfer_amount"`
	// 用户在商户appid下的唯一标识
	Openid string `json:"openid"`
	// 单已失败或者已退资金时，会返回订单失败原因（选填）
	FailReason string `json:"fail_reason,omitempty"`
	// 单据创建时间，遵循rfc3339标准格式
	CreateTime string `json:"create_time"`
	// 最后一次状态变更时间，遵循rfc3339标准格式
	UpdateTime string `json:"update_time"`
}

// HandleMchTransferNotify 处理商家转账回调通知，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012712115
// 当商家转账单据到终态后（SUCCESS/FAIL/CANCELLED），微信支付会通过此回调通知商户
func HandleMchTransferNotify(config *wxpay_utility.MchConfig, request *http.Request) error {
	// 商户APIv3密钥，如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053267
	const apiv3Key = "your apiv3 key"

	// 读取微信支付通知的请求体
	notifyBody, err := io.ReadAll(request.Body) // 微信支付通知的原始字符串
	if err != nil {
		fmt.Println(err)
		return err
	}
	headers := &request.Header
	notification, err := wxpay_utility.ParseNotification(
		config.WechatPayPublicKeyId(),
		config.WechatPayPublicKey(),
		apiv3Key,
		headers,
		notifyBody,
	)
	if err != nil {
		fmt.Println(err)
		return err
	}

	// 解析商家转账回调内容
	mchTransferNotify := &MchTransferNotify{}
	if err = json.Unmarshal([]byte(notification.Plaintext), mchTransferNotify); err != nil {
		fmt.Println(err)
		return err
	}

	// 处理商家转账的事件和单据状态
	switch notification.EventType {
	case "MCHTRANSFER.BILL.FINISHED":
		// 商家转账单据终态通知，处理单据状态
		switch mchTransferNotify.State {
		case "SUCCESS":
			// 转账成功
			return nil
		case "FAIL":
			// 转账失败，若需重新向用户转账，请重新生成单据并再次发起
			return nil
		case "CANCELLED":
			// 转账已撤销，若需重新向用户转账，请重新生成单据并再次发起
			return nil
		default:
			// 状态非法，报错返回
			return fmt.Errorf("unknown state: %s", mchTransferNotify.State)
		}
	default:
		// 不关心的事件类型，打印日志后忽略
		fmt.Printf("unknown event type: %s, ignore\n", notification.EventType)
		return nil
	}
}

// WriteSuccessResponse 给微信支付回调返回成功应答
// 验签通过后，返回HTTP状态码200，无需返回应答报文，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012712115
func WriteSuccessResponse(w http.ResponseWriter) {
	w.WriteHeader(http.StatusOK)
}

// WriteFailResponse 给微信支付回调返回失败应答
// 验签不通过时，返回HTTP状态码4XX/5XX，并返回JSON格式的错误信息，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012712115
func WriteFailResponse(w http.ResponseWriter, code string, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusBadRequest)
	resp := map[string]string{
		"code":    code,
		"message": message,
	}
	jsonBytes, err := json.Marshal(resp)
	if err != nil {
		fmt.Println(err)
		return
	}
	if _, err = w.Write(jsonBytes); err != nil {
		fmt.Println(err)
	}
}

// MchTransferNotifyHandler 商家转账回调通知HTTP入口，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012712115
// 应当先应答成功（返回200/204）再处理后续业务逻辑，推荐异步处理，以避免应答超时
func MchTransferNotifyHandler(config *wxpay_utility.MchConfig) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := HandleMchTransferNotify(config, r); err != nil {
			WriteFailResponse(w, "FAIL", "处理通知失败")
			return
		}

		// 先应答成功，再异步处理后续业务逻辑
		WriteSuccessResponse(w)
	}
}
```

## 4. 电子回单

转账成功后，如需留存转账凭证，可申请并下载电子回单。本章节介绍获取电子回单的完整流程。

前置要求：

- 转账单状态必须为 `SUCCESS`（转账成功）

- 发起转账时需传入收款用户姓名（ `user_name` 字段）

- 仅支持六个月内的转账单据


电子回单状态流转

![](https://gtimg.wechatpay.cn/resource/xres/mmpaydoc/static/img/4d0c521ad209cc873f64ab19693b5ba7.png)

### 4.1 申请电子回单

申请生成电子回单，支持通过商户单号或微信转账单号两种方式申请。

#### 4.1.1 通过商户单号申请

接口文档参考：[商户单号申请电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716452.md)

示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleAcceptElecsignByOutNo 通过商户单号申请电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716452
func HandleAcceptElecsignByOutNo(config *wxpay_utility.MchConfig) error {
	// 使用商户单号申请电子回单接口：POST /v3/fund-app/mch-transfer/elecsign/out-bill-no，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716452
	// 申请前请确认：仅支持状态为SUCCESS、已传入收款用户姓名、六个月内的转账单据
	req := &AcceptElecsignByOutNoRequest{
		// 商户创建转账单据使用的单号，长度5-32个字符
		OutBillNo: wxpay_utility.String("plfk2020042013"),
	}

	resp, err := AcceptElecsignByOutNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理商户单号申请电子回单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		case "FREQUENCY_LIMIT_EXCEED":
			// 错误：FREQUENCY_LIMIT_EXCEED
			// 描述：频率超限
			// 解决方式：接口请求频率超限，当前请求结果不明确，请降低请求接口频率后，使用相同参数重试
		case "RATELIMIT_EXCEEDED":
			// 错误：RATELIMIT_EXCEEDED
			// 描述：频率超限
			// 解决方式：接口或同一单号请求频率超限，当前请求结果不明确，请降低请求接口频率，使用相同参数重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理申请电子回单的单据状态
func handleStatus(resp *AcceptElecsignResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case ELECSIGNSTATUS_GENERATING:
		// 表示当前电子回单已受理成功并在处理中，通过查询电子回单接口获取下载链接，用于下载电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716436
	case ELECSIGNSTATUS_FINISHED:
		// 表示当前电子回单已处理完成，通过查询电子回单接口获取下载链接，用于下载电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716436
	case ELECSIGNSTATUS_FAILED:
		// 已失败，当前电子回单生成失败（终态），可重新申请
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

#### 4.1.2 通过微信转账单号申请

接口文档参考：[微信单号申请电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716456.md)

示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleAcceptElecsignByNo 通过微信单号申请电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716456
func HandleAcceptElecsignByNo(config *wxpay_utility.MchConfig) error {
	// 使用微信单号申请电子回单接口：POST /v3/fund-app/mch-transfer/elecsign/transfer-bill-no，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716452
	// 申请前请确认：仅支持状态为SUCCESS、已传入收款用户姓名、六个月内的转账单据
	req := &AcceptElecsignByNoRequest{
		// 转账单据对应的微信支付单号，长度30-64个字符
		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
	}

	resp, err := AcceptElecsignByNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理微信单号申请电子回单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理申请电子回单的单据状态
func handleStatus(resp *AcceptElecsignResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case ELECSIGNSTATUS_GENERATING:
		// 表示当前电子回单已受理成功并在处理中，通过查询电子回单接口获取下载链接，用于下载电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716436
	case ELECSIGNSTATUS_FINISHED:
		// 表示当前电子回单已处理完成，通过查询电子回单接口获取下载链接，用于下载电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716436
	case ELECSIGNSTATUS_FAILED:
		// 已失败，当前电子回单生成失败（终态），可重新申请
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

### 4.2 查询电子回单

申请电子回单后，需要通过查询接口轮询电子回单的生成状态。当状态为 `FINISHED` 时，可获取下载地址。

#### 4.2.1 通过商户单号查询

接口文档参考：[商户单号查询电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716436.md)

示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleQueryElecsignByOutNo 通过商户单号查询电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716436
func HandleQueryElecsignByOutNo(config *wxpay_utility.MchConfig) error {
	// 使用商户单号查询电子回单接口：GET /v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
	req := &QueryElecsignByOutNoRequest{
		// 商户创建转账单据使用的单号，长度5-32个字符
		OutBillNo: wxpay_utility.String("plfk2020042013"),
	}

	resp, err := QueryElecsignByOutNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理商户单号查询电子回单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理查询电子回单的单据状态
func handleStatus(resp *QueryElecsignResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case ELECSIGNSTATUS_GENERATING:
		// 生成中，当前电子回单已受理成功并在处理中（非终态），可继续轮询查询
	case ELECSIGNSTATUS_FINISHED:
		// 已完成，当前电子回单已处理完成（终态），download_url 字段包含下载地址
		if resp.DownloadUrl == nil {
			return fmt.Errorf("download_url is nil")
		}
	case ELECSIGNSTATUS_FAILED:
		// 已失败，当前电子回单生成失败（终态），可重新申请
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

#### 4.2.2 通过微信转账单号查询

接口文档参考：[微信单号查询电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716455.md)

示例代码

```
package transfer

import (
	"demo/wxpay_utility" // 引用微信支付工具库，参考：https://pay.weixin.qq.com/doc/v3/merchant/4015119334
	"errors"
	"fmt"
)

// HandleQueryElecsignByNo 通过微信单号查询电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012716455
func HandleQueryElecsignByNo(config *wxpay_utility.MchConfig) error {
	// 使用微信单号查询电子回单接口：GET /v3/fund-app/mch-transfer/elecsign/transfer-bill-no/{transfer_bill_no}
	req := &QueryElecsignByNoRequest{
		// 转账单据对应的微信支付单号，长度30-64个字符
		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
	}

	resp, err := QueryElecsignByNo(config, req)
	if err != nil {
		return handleError(err)
	}

	return handleStatus(resp)
}

// handleError 处理微信单号查询电子回单接口的错误码
func handleError(err error) error {
	var apiError *wxpay_utility.ApiException
	if errors.As(err, &apiError) {
		fmt.Printf("状态码: %d\n", apiError.StatusCode())
		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
		switch apiError.ErrorCode() {
		case "PARAM_ERROR":
			// 错误：PARAM_ERROR
			// 描述：参数错误
			// 解决方式：请根据错误提示正确传入参数
		case "INVALID_REQUEST":
			// 错误：INVALID_REQUEST
			// 描述：HTTP 请求不符合微信支付 APIv3 接口规则
			// 解决方式：请参阅接口规则，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012081709
		case "SIGN_ERROR":
			// 错误：SIGN_ERROR
			// 描述：验证不通过
			// 解决方式：请参阅签名常见问题，参考：https://pay.weixin.qq.com/doc/v3/merchant/4012072670
		case "SYSTEM_ERROR":
			// 错误：SYSTEM_ERROR
			// 描述：系统异常，请稍后重试
			// 解决方式：请稍后重试
		default:
			// 其他类型错误
		}
	}
	return err
}

// handleStatus 处理查询电子回单的单据状态
func handleStatus(resp *QueryElecsignResponse) error {
	if resp.State == nil {
		return fmt.Errorf("state is nil")
	}
	switch *resp.State {
	case ELECSIGNSTATUS_GENERATING:
		// 生成中，当前电子回单已受理成功并在处理中（非终态），可继续轮询查询
	case ELECSIGNSTATUS_FINISHED:
		// 已完成，当前电子回单已处理完成（终态），download_url 字段包含下载地址
		if resp.DownloadUrl == nil {
			return fmt.Errorf("download_url is nil")
		}
	case ELECSIGNSTATUS_FAILED:
		// 已失败，当前电子回单生成失败（终态），可重新申请
	default:
		return fmt.Errorf("unknown status: %s", *resp.State)
	}
	return nil
}
```

### 4.3 下载电子回单

查询电子回单接口返回状态为 `FINISHED` 时，可通过返回的 `download_url` 下载电子回单文件，并使用 `hash_value` 和 `hash_type` 进行文件完整性校验。

接口文档参考：[下载电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4013866774.md)

注意事项：

- 回单申请完成后的有效期为 90 天，过期后需要重新申请。

- 请务必对比下载的回单文件的摘要值与查询接口返回的摘要值的一致性，确保得到的回单文件的真实性和完整性。

- 下载地址的有效期为 10 分钟，超过 10 分钟后需要重新通过


[申请电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716452.md)和[查询电子回单](https://pay.weixin.qq.com/doc/v3/merchant/4012716436.md)接口获取下载地址（不需要重新申请）。

示例代码

```
package transfer

import (
	"demo/wxpay_utility"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
)

// HandleDownloadElecsign 下载电子回单，参考：https://pay.weixin.qq.com/doc/v3/merchant/4013866774
// downloadUrl、hashValue、hashType 从查询电子回单接口返回结果中获取
func HandleDownloadElecsign(config *wxpay_utility.MchConfig) error {
	const (
		downloadUrl   = "https://api.mch.weixin.qq.com/v3/transferdownload/elecvoucherfile?source=elecsign&token=xxx" // 查询电子回单接口返回的 download_url
		hashValue     = "01BD163ACF221DD73DBEEA8903F0340DBBF417A38583246D8A027480EC4F0EA0"                             // 查询电子回单接口返回的 hash_value
		hashType      = "SM3"                                                                                         // 查询电子回单接口返回的 hash_type，支持 SHA256 和 SM3
		method        = "GET"
		localFilePath = "downloaded_file.pdf" // 电子回单文件保存的本地路径，文件格式为PDF
	)

	parsedURL, err := url.Parse(downloadUrl)
	if err != nil {
		fmt.Println(err)
		return err
	}
	canonicalURL := parsedURL.RequestURI() // 返回 path?query 部分

	httpRequest, err := http.NewRequest(method, downloadUrl, nil)
	if err != nil {
		fmt.Println(err)
		return err
	}
	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(),
		config.PrivateKey(), method, canonicalURL, nil)
	if err != nil {
		fmt.Println(err)
		return err
	}
	httpRequest.Header.Set("Authorization", authorization)

	client := &http.Client{}
	httpResponse, err := client.Do(httpRequest)
	if err != nil {
		fmt.Println(err)
		return err
	}
	defer httpResponse.Body.Close()

	if httpResponse.StatusCode != http.StatusOK {
		fmt.Println("http status code:", httpResponse.StatusCode)
		return fmt.Errorf("http status code: %d", httpResponse.StatusCode)
	}

	// 流式写入本地文件
	outFile, err := os.Create(localFilePath)
	if err != nil {
		fmt.Println(err)
		return err
	}
	defer outFile.Close()

	if _, err = io.Copy(outFile, httpResponse.Body); err != nil {
		fmt.Println(err)
		return err
	}
	outFile.Close() // 显式关闭，确保数据落盘后再读取

	// 打开文件流进行哈希校验
	f, err := os.Open(localFilePath)
	if err != nil {
		fmt.Println(err)
		return err
	}
	defer f.Close()

	var actualHash string
	switch hashType {
	case "SHA256":
		actualHash, err = wxpay_utility.GenerateSHA256FromStream(f)
		if err != nil {
			fmt.Println(err)
			return err
		}
	case "SM3":
		actualHash, err = wxpay_utility.GenerateSM3FromStream(f)
		if err != nil {
			fmt.Println(err)
			return err
		}
	default:
		return fmt.Errorf("unknown hash type: %s", hashType)
	}

	if strings.ToUpper(actualHash) != hashValue {
		fmt.Println("hash mismatch")
		return fmt.Errorf("hash mismatch: expected %s, got %s", hashValue, actualHash)
	}

	fmt.Println("download success")
	return nil
}
```