业务示例代码

更新时间:2026.04.23
||

1. 目标

通过本教程的学习,你应该可以:

2. 前置准备

2.1 商户配置

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

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5)
6
7// NewMchConfig 创建商户配置
8func NewMchConfig() (*wxpay_utility.MchConfig, error) {
9	return wxpay_utility.CreateMchConfig(
10		"19xxxxxxxx",                 // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
11		"1DDE55AD98Exxxxxxxxxx",      // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
12		"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
13		"PUB_KEY_ID_xxxxxxxxxxxxx",   // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
14		"/path/to/wxp_pub.pem",       // 微信支付公钥文件路径,本地文件路径
15	)
16}

3. 转账到用户零钱

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

转账单状态流转

3.1 发起转账

商户调用发起转账接口创建转账单。接口调用成功后,转账单进入 WAIT_USER_CONFIRM(待用户确认)状态,接口返回的 package_info 将用于后续拉起微信收款确认页面。

关键参数说明:

  • appid / openid:需保证一致性,后续拉起确认收款页面时也需使用相同的 appid 和 openid

  • notify_url(可选):如需被动接收转账终态通知,在此传入回调地址

  • user_name:收款用户姓名,转账金额 ≥ 2000 元时必填,需使用微信支付公钥加密

  • transfer_scene_id:转账场景ID,需在商户平台提前申请

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleTransferToUser 发起商家转账(用户确认模式),参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716434
10func HandleTransferToUser(config *wxpay_utility.MchConfig) error {
11	// 使用发起转账接口:POST /v3/fund-app/mch-transfer/transfer-bills,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716434
12	// 收款用户姓名,转账金额=2000元时必填;需使用微信支付公钥加密,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013053257
13	encryptedUserName, err := wxpay_utility.EncryptOAEPWithPublicKey("张三", config.WechatPayPublicKey())
14	if err != nil {
15		return err
16	}
17
18	req := &TransferToUserRequest{
19		// 商户应用唯一标识,与商户号有绑定关系,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
20		Appid: wxpay_utility.String("wxf636efh567hg4356"),
21		// 商户系统内部的商家单号,由数字、大小写字母组成,在商户系统内部唯一
22		OutBillNo: wxpay_utility.String("plfk2020042013"),
23		// 转账场景ID,可前往"商户平台-产品中心-商家转账"中申请,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013774588
24		TransferSceneId: wxpay_utility.String("1000"),
25		// 收款用户在商户appid下的唯一标识,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
26		Openid: wxpay_utility.String("o-MYE42l80oelYMDE34nYD456Xoy"),
27		// 收款用户姓名;需使用微信支付公钥加密,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013053257
28		UserName: wxpay_utility.String(encryptedUserName),
29		// 转账金额,单位为"分",参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716434
30		TransferAmount: wxpay_utility.Int64(400000),
31		// 转账备注,用户收款时可见,UTF8编码,最多允许32个字符
32		TransferRemark: wxpay_utility.String("新会员开通有礼"),
33		// 异步接收微信支付结果通知的回调地址,必须为HTTPS公网地址
34		NotifyUrl: wxpay_utility.String("https://www.weixin.qq.com/wxpay/pay.php"),
35		// 用户收款时感知的收款原因,不填将展示转账场景的默认内容
36		UserRecvPerception: wxpay_utility.String("现金奖励"),
37		// 转账场景报备信息,需按转账场景准确填写,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013774588
38		TransferSceneReportInfos: []TransferSceneReportInfo{
39			{
40				InfoType:    wxpay_utility.String("活动名称"),
41				InfoContent: wxpay_utility.String("新会员有礼"),
42			},
43			{
44				InfoType:    wxpay_utility.String("奖励说明"),
45				InfoContent: wxpay_utility.String("注册会员抽奖一等奖"),
46			},
47		},
48	}
49
50	resp, err := TransferToUser(config, req)
51	if err != nil {
52		return handleError(err)
53	}
54
55	return handleStatus(resp)
56}
57
58// handleError 处理发起转账接口的错误码
59func handleError(err error) error {
60	var apiError *wxpay_utility.ApiException
61	if errors.As(err, &apiError) {
62		fmt.Printf("状态码: %d\n", apiError.StatusCode())
63		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
64		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
65		switch apiError.ErrorCode() {
66		case "PARAM_ERROR":
67			// 错误:PARAM_ERROR
68			// 描述:参数错误
69			// 解决方式:请根据错误提示正确传入参数
70		case "INVALID_REQUEST":
71			// 错误:INVALID_REQUEST
72			// 描述:请求不符合业务规则
73			// 解决方式:请参阅产品介绍、开发指引和接口规则,确认请求参数正确
74		case "NO_AUTH":
75			// 错误:NO_AUTH
76			// 描述:没有相关权限
77			// 解决方式:请参阅产品介绍、开发指引,确认已开通商家转账权限
78		case "SIGN_ERROR":
79			// 错误:SIGN_ERROR
80			// 描述:签名验证不通过
81			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
82		case "SYSTEM_ERROR":
83			// 错误:SYSTEM_ERROR
84			// 描述:系统异常,请稍后重试
85			// 解决方式:请稍后重试,若多次失败请通过查单接口确认订单状态后再决定是否重试
86		case "NOT_ENOUGH":
87			// 错误:NOT_ENOUGH
88			// 描述:资金不足
89			// 解决方式:确认出资商户余额充足
90		case "FREQUENCY_LIMIT_EXCEED":
91			// 错误:FREQUENCY_LIMIT_EXCEED
92			// 描述:频率超限
93			// 解决方式:接口请求频率超限,当前请求结果不明确,请降低请求接口频率后,使用相同参数重试
94		case "RATELIMIT_EXCEEDED":
95			// 错误:RATELIMIT_EXCEEDED
96			// 描述:频率超限
97			// 解决方式:接口或同一单号请求频率超限,当前请求结果不明确,请降低请求接口频率,使用相同参数重试
98		case "FREQUENCY_LIMIT":
99			// 错误:FREQUENCY_LIMIT
100			// 描述:频率超限
101			// 解决方式:接口或同一单号请求频率超限,当前请求结果不明确,请降低请求接口频率,使用相同参数重试
102		case "ALREADY_EXISTS":
103			// 错误:ALREADY_EXISTS
104			// 描述:单号重复,商家单号已存在
105			// 解决方式:请通过查单接口确认当前单号的转账状态,根据订单的实际状态判断是否需要进行用户确认等后续操作
106		default:
107			// 其他类型错误
108		}
109	}
110	return err
111}
112
113// handleStatus 处理发起转账的单据状态
114func handleStatus(resp *TransferToUserResponse) error {
115	if resp.State == nil {
116		return fmt.Errorf("state is nil")
117	}
118	switch *resp.State {
119	case TRANSFERBILLSTATUS_ACCEPTED:
120		// 转账已受理,可原单重试(非终态)
121		if resp.TransferBillNo == nil {
122			return fmt.Errorf("transfer_bill_no is nil")
123		}
124	case TRANSFERBILLSTATUS_PROCESSING:
125		// 转账锁定资金中,如果一直停留在该状态,建议检查账户余额是否足够(非终态)
126		if resp.TransferBillNo == nil {
127			return fmt.Errorf("transfer_bill_no is nil")
128		}
129	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
130		// 待收款用户确认,当前转账单据资金已锁定,可拉起微信收款确认页面进行收款确认(非终态)。
131		// 商户APP通过微信Open SDK的sendReq方法拉起用户确认收款页, 参考APP调起用户确认收款的指引
132		// https://pay.weixin.qq.com/doc/v3/merchant/4012719576
133		// https://pay.weixin.qq.com/doc/v3/merchant/4012719578
134		if resp.PackageInfo != nil {
135			fmt.Printf("package信息: %s\n", *resp.PackageInfo)
136		}
137	case TRANSFERBILLSTATUS_TRANSFERING:
138		// 转账中,可拉起微信收款确认页面再次重试确认收款(非终态)
139	case TRANSFERBILLSTATUS_SUCCESS:
140		// 转账成功,表示转账单据已成功(终态)
141	case TRANSFERBILLSTATUS_FAIL:
142		// 转账失败,该笔转账单据已失败,若需重新向用户转账,请重新生成单据并再次发起(终态)
143	case TRANSFERBILLSTATUS_CANCELING:
144		// 转账撤销中,商户撤销请求受理成功,该笔转账正在撤销中,需查单确认撤销的转账单据状态(非终态)
145	case TRANSFERBILLSTATUS_CANCELLED:
146		// 转账撤销完成,代表转账单据已撤销成功(终态)
147	default:
148		return fmt.Errorf("unknown status: %s", *resp.State)
149	}
150	return nil
151}

3.2 拉起用户确认收款页面

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

注意事项:

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

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

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

不同端的调起方式:

3.3 查询转账单状态

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

3.3.1 通过商户单号查询

使用商户系统内部的商家单号查询转账单状态,接口文档参考:商户单号查询转账单

适用场景:

  • 拉起确认收款页面前,确认转账单仍处于 WAIT_USER_CONFIRM 状态

  • 撤销转账前,确认转账单尚未被用户确认

  • 发起转账后,主动轮询确认转账终态

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleGetTransferBillByOutNo 通过商户单号查询转账单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716437
10func HandleGetTransferBillByOutNo(config *wxpay_utility.MchConfig) error {
11	// 使用商户单号查询转账单接口:GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
12	req := &GetTransferBillByOutNoRequest{
13		// 商户系统内部的商家单号,在商户系统内部唯一
14		OutBillNo: wxpay_utility.String("plfk2020042013"),
15	}
16
17	resp, err := GetTransferBillByOutNo(config, req)
18	if err != nil {
19		return handleError(err)
20	}
21
22	return handleStatus(resp)
23}
24
25// handleError 处理商户单号查询转账单接口的错误码
26func handleError(err error) error {
27	var apiError *wxpay_utility.ApiException
28	if errors.As(err, &apiError) {
29		fmt.Printf("状态码: %d\n", apiError.StatusCode())
30		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
31		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
32		switch apiError.ErrorCode() {
33		case "PARAM_ERROR":
34			// 错误:PARAM_ERROR
35			// 描述:参数错误
36			// 解决方式:请根据错误提示正确传入参数
37		case "INVALID_REQUEST":
38			// 错误:INVALID_REQUEST
39			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
40			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
41		case "SIGN_ERROR":
42			// 错误:SIGN_ERROR
43			// 描述:验证不通过
44			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
45		case "SYSTEM_ERROR":
46			// 错误:SYSTEM_ERROR
47			// 描述:系统异常,请稍后重试
48			// 解决方式:请稍后重试
49		default:
50			// 其他类型错误
51		}
52	}
53	return err
54}
55
56// handleStatus 处理查询转账单的单据状态
57func handleStatus(resp *TransferBillEntity) error {
58	if resp.State == nil {
59		return fmt.Errorf("state is nil")
60	}
61	switch *resp.State {
62	case TRANSFERBILLSTATUS_ACCEPTED:
63		// 转账已受理,可原单重试(非终态)
64	case TRANSFERBILLSTATUS_PROCESSING:
65		// 转账锁定资金中,如果一直停留在该状态,建议检查账户余额是否足够(非终态)
66	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
67		// 待收款用户确认,当前转账单据资金已锁定(非终态)
68	case TRANSFERBILLSTATUS_TRANSFERING:
69		// 转账中(非终态)
70	case TRANSFERBILLSTATUS_SUCCESS:
71		// 转账成功(终态)
72	case TRANSFERBILLSTATUS_FAIL:
73		// 转账失败,该笔转账单据已失败(终态)
74		if resp.FailReason != nil {
75			// 失败原因详见 fail_reason 字段
76		}
77	case TRANSFERBILLSTATUS_CANCELING:
78		// 转账撤销中(非终态)
79	case TRANSFERBILLSTATUS_CANCELLED:
80		// 转账撤销完成(终态)
81	default:
82		return fmt.Errorf("unknown status: %s", *resp.State)
83	}
84	return nil
85}

3.3.2 通过微信转账单号查询

使用微信支付系统返回的转账单号查询转账单状态,接口文档参考:微信转账单号查询转账单

适用场景:

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

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleGetTransferBillByNo 通过微信转账单号查询转账单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716457
10func HandleGetTransferBillByNo(config *wxpay_utility.MchConfig) error {
11	// 使用微信转账单号查询转账单接口:GET /v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}
12	req := &GetTransferBillByNoRequest{
13		// 微信转账单号,微信商家转账系统返回的唯一标识
14		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
15	}
16
17	resp, err := GetTransferBillByNo(config, req)
18	if err != nil {
19		return handleError(err)
20	}
21
22	return handleStatus(resp)
23}
24
25// handleError 处理微信转账单号查询转账单接口的错误码
26func handleError(err error) error {
27	var apiError *wxpay_utility.ApiException
28	if errors.As(err, &apiError) {
29		fmt.Printf("状态码: %d\n", apiError.StatusCode())
30		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
31		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
32		switch apiError.ErrorCode() {
33		case "PARAM_ERROR":
34			// 错误:PARAM_ERROR
35			// 描述:参数错误
36			// 解决方式:请根据错误提示正确传入参数
37		case "INVALID_REQUEST":
38			// 错误:INVALID_REQUEST
39			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
40			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
41		case "SIGN_ERROR":
42			// 错误:SIGN_ERROR
43			// 描述:验证不通过
44			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
45		case "SYSTEM_ERROR":
46			// 错误:SYSTEM_ERROR
47			// 描述:系统异常,请稍后重试
48			// 解决方式:请稍后重试
49		default:
50			// 其他类型错误
51		}
52	}
53	return err
54}
55
56// handleStatus 处理查询转账单的单据状态
57func handleStatus(resp *TransferBillEntity) error {
58	if resp.State == nil {
59		return fmt.Errorf("state is nil")
60	}
61	switch *resp.State {
62	case TRANSFERBILLSTATUS_ACCEPTED:
63		// 转账已受理,可原单重试(非终态)
64	case TRANSFERBILLSTATUS_PROCESSING:
65		// 转账锁定资金中,如果一直停留在该状态,建议检查账户余额是否足够(非终态)
66	case TRANSFERBILLSTATUS_WAIT_USER_CONFIRM:
67		// 待收款用户确认,当前转账单据资金已锁定(非终态)
68	case TRANSFERBILLSTATUS_TRANSFERING:
69		// 转账中(非终态)
70	case TRANSFERBILLSTATUS_SUCCESS:
71		// 转账成功(终态)
72	case TRANSFERBILLSTATUS_FAIL:
73		// 转账失败,该笔转账单据已失败(终态)
74		if resp.FailReason != nil {
75			// 失败原因详见 fail_reason 字段
76		}
77	case TRANSFERBILLSTATUS_CANCELING:
78		// 转账撤销中(非终态)
79	case TRANSFERBILLSTATUS_CANCELLED:
80		// 转账撤销完成(终态)
81	default:
82		return fmt.Errorf("unknown status: %s", *resp.State)
83	}
84	return nil
85}

3.4 撤销转账

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

注意事项:

  • 可撤销的状态:ACCEPTED(转账已受理)、PROCESSING(转账锁定资金中)、WAIT_USER_CONFIRM(待用户确认),即用户确认收款之前的状态均可撤销

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

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

接口文档参考:撤销转账

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleCancelTransfer 撤销转账,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716458
10func HandleCancelTransfer(config *wxpay_utility.MchConfig) error {
11	// 使用撤销转账接口:POST /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel
12	req := &CancelTransferRequest{
13		// 商户系统内部的商家单号,在商户系统内部唯一
14		OutBillNo: wxpay_utility.String("plfk2020042013"),
15	}
16
17	resp, err := CancelTransfer(config, req)
18	if err != nil {
19		return handleError(err)
20	}
21
22	return handleStatus(resp)
23}
24
25// handleError 处理撤销转账接口的错误码
26func handleError(err error) error {
27	var apiError *wxpay_utility.ApiException
28	if errors.As(err, &apiError) {
29		fmt.Printf("状态码: %d\n", apiError.StatusCode())
30		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
31		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
32		switch apiError.ErrorCode() {
33		case "PARAM_ERROR":
34			// 错误:PARAM_ERROR
35			// 描述:参数错误
36			// 解决方式:请根据错误提示正确传入参数
37		case "INVALID_REQUEST":
38			// 错误:INVALID_REQUEST
39			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
40			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
41		case "SIGN_ERROR":
42			// 错误:SIGN_ERROR
43			// 描述:验证不通过
44			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
45		case "SYSTEM_ERROR":
46			// 错误:SYSTEM_ERROR
47			// 描述:系统异常,请稍后重试
48			// 解决方式:请稍后重试
49		default:
50			// 其他类型错误
51		}
52	}
53	return err
54}
55
56// handleStatus 处理撤销转账的单据状态
57func handleStatus(resp *CancelTransferResponse) error {
58	if resp.State == nil {
59		return fmt.Errorf("state is nil")
60	}
61	switch *resp.State {
62	case "CANCELING":
63		// 转账撤销中,商户撤销请求受理成功,该笔转账正在撤销中(非终态),请通过查单接口确认撤销状态
64	case "CANCELLED":
65		// 转账撤销完成,代表转账单据已撤销成功(终态)
66	default:
67		return fmt.Errorf("unknown status: %s", *resp.State)
68	}
69	return nil
70}

3.5 接收转账回调通知

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

注意事项:

  • 接收回调通知前,商户需要先设置APIv3密钥

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

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

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

  • 商户侧对微信支付回调IP有防火墙策略限制的,请参考回调通知注意事项

回调文档参考:商家转账回调通知

示例代码

1package notify
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"encoding/json"
6	"fmt"
7	"io"
8	"net/http"
9)
10
11// MchTransferNotify 商家转账回调通知内容,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012712115
12// resource 中 ciphertext 解密后字段
13type MchTransferNotify struct {
14	// 商户系统内部的商家单号,在商户系统内部唯一
15	OutBillNo string `json:"out_bill_no"`
16	// 微信单号,微信商家转账系统返回的唯一标识
17	TransferBillNo string `json:"transfer_bill_no"`
18	// 商家转账订单状态:SUCCESS-转账成功,FAIL-转账失败,CANCELLED-已撤销
19	State string `json:"state"`
20	// 微信支付分配的商户号
21	MchId string `json:"mch_id"`
22	// 转账总金额,单位为"分"
23	TransferAmount int64 `json:"transfer_amount"`
24	// 用户在商户appid下的唯一标识
25	Openid string `json:"openid"`
26	// 单已失败或者已退资金时,会返回订单失败原因(选填)
27	FailReason string `json:"fail_reason,omitempty"`
28	// 单据创建时间,遵循rfc3339标准格式
29	CreateTime string `json:"create_time"`
30	// 最后一次状态变更时间,遵循rfc3339标准格式
31	UpdateTime string `json:"update_time"`
32}
33
34// HandleMchTransferNotify 处理商家转账回调通知,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012712115
35// 当商家转账单据到终态后(SUCCESS/FAIL/CANCELLED),微信支付会通过此回调通知商户
36func HandleMchTransferNotify(config *wxpay_utility.MchConfig, request *http.Request) error {
37	// 商户APIv3密钥,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053267
38	const apiv3Key = "your apiv3 key"
39
40	// 读取微信支付通知的请求体
41	notifyBody, err := io.ReadAll(request.Body) // 微信支付通知的原始字符串
42	if err != nil {
43		fmt.Println(err)
44		return err
45	}
46	headers := &request.Header
47	notification, err := wxpay_utility.ParseNotification(
48		config.WechatPayPublicKeyId(),
49		config.WechatPayPublicKey(),
50		apiv3Key,
51		headers,
52		notifyBody,
53	)
54	if err != nil {
55		fmt.Println(err)
56		return err
57	}
58
59	// 解析商家转账回调内容
60	mchTransferNotify := &MchTransferNotify{}
61	if err = json.Unmarshal([]byte(notification.Plaintext), mchTransferNotify); err != nil {
62		fmt.Println(err)
63		return err
64	}
65
66	// 处理商家转账的事件和单据状态
67	switch notification.EventType {
68	case "MCHTRANSFER.BILL.FINISHED":
69		// 商家转账单据终态通知,处理单据状态
70		switch mchTransferNotify.State {
71		case "SUCCESS":
72			// 转账成功
73			return nil
74		case "FAIL":
75			// 转账失败,若需重新向用户转账,请重新生成单据并再次发起
76			return nil
77		case "CANCELLED":
78			// 转账已撤销,若需重新向用户转账,请重新生成单据并再次发起
79			return nil
80		default:
81			// 状态非法,报错返回
82			return fmt.Errorf("unknown state: %s", mchTransferNotify.State)
83		}
84	default:
85		// 不关心的事件类型,打印日志后忽略
86		fmt.Printf("unknown event type: %s, ignore\n", notification.EventType)
87		return nil
88	}
89}
90
91// WriteSuccessResponse 给微信支付回调返回成功应答
92// 验签通过后,返回HTTP状态码200,无需返回应答报文,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012712115
93func WriteSuccessResponse(w http.ResponseWriter) {
94	w.WriteHeader(http.StatusOK)
95}
96
97// WriteFailResponse 给微信支付回调返回失败应答
98// 验签不通过时,返回HTTP状态码4XX/5XX,并返回JSON格式的错误信息,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012712115
99func WriteFailResponse(w http.ResponseWriter, code string, message string) {
100	w.Header().Set("Content-Type", "application/json")
101	w.WriteHeader(http.StatusBadRequest)
102	resp := map[string]string{
103		"code":    code,
104		"message": message,
105	}
106	jsonBytes, err := json.Marshal(resp)
107	if err != nil {
108		fmt.Println(err)
109		return
110	}
111	if _, err = w.Write(jsonBytes); err != nil {
112		fmt.Println(err)
113	}
114}
115
116// MchTransferNotifyHandler 商家转账回调通知HTTP入口,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012712115
117// 应当先应答成功(返回200/204)再处理后续业务逻辑,推荐异步处理,以避免应答超时
118func MchTransferNotifyHandler(config *wxpay_utility.MchConfig) http.HandlerFunc {
119	return func(w http.ResponseWriter, r *http.Request) {
120		if err := HandleMchTransferNotify(config, r); err != nil {
121			WriteFailResponse(w, "FAIL", "处理通知失败")
122			return
123		}
124
125		// 先应答成功,再异步处理后续业务逻辑
126		WriteSuccessResponse(w)
127	}
128}

4. 电子回单

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

前置要求:

  • 转账单状态必须为 SUCCESS(转账成功)

  • 发起转账时需传入收款用户姓名(user_name 字段)

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

电子回单状态流转

4.1 申请电子回单

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

4.1.1 通过商户单号申请

接口文档参考:商户单号申请电子回单

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleAcceptElecsignByOutNo 通过商户单号申请电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716452
10func HandleAcceptElecsignByOutNo(config *wxpay_utility.MchConfig) error {
11	// 使用商户单号申请电子回单接口:POST /v3/fund-app/mch-transfer/elecsign/out-bill-no,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716452
12	// 申请前请确认:仅支持状态为SUCCESS、已传入收款用户姓名、六个月内的转账单据
13	req := &AcceptElecsignByOutNoRequest{
14		// 商户创建转账单据使用的单号,长度5-32个字符
15		OutBillNo: wxpay_utility.String("plfk2020042013"),
16	}
17
18	resp, err := AcceptElecsignByOutNo(config, req)
19	if err != nil {
20		return handleError(err)
21	}
22
23	return handleStatus(resp)
24}
25
26// handleError 处理商户单号申请电子回单接口的错误码
27func handleError(err error) error {
28	var apiError *wxpay_utility.ApiException
29	if errors.As(err, &apiError) {
30		fmt.Printf("状态码: %d\n", apiError.StatusCode())
31		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
32		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
33		switch apiError.ErrorCode() {
34		case "PARAM_ERROR":
35			// 错误:PARAM_ERROR
36			// 描述:参数错误
37			// 解决方式:请根据错误提示正确传入参数
38		case "INVALID_REQUEST":
39			// 错误:INVALID_REQUEST
40			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
41			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
42		case "SIGN_ERROR":
43			// 错误:SIGN_ERROR
44			// 描述:验证不通过
45			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
46		case "SYSTEM_ERROR":
47			// 错误:SYSTEM_ERROR
48			// 描述:系统异常,请稍后重试
49			// 解决方式:请稍后重试
50		case "FREQUENCY_LIMIT_EXCEED":
51			// 错误:FREQUENCY_LIMIT_EXCEED
52			// 描述:频率超限
53			// 解决方式:接口请求频率超限,当前请求结果不明确,请降低请求接口频率后,使用相同参数重试
54		case "RATELIMIT_EXCEEDED":
55			// 错误:RATELIMIT_EXCEEDED
56			// 描述:频率超限
57			// 解决方式:接口或同一单号请求频率超限,当前请求结果不明确,请降低请求接口频率,使用相同参数重试
58		default:
59			// 其他类型错误
60		}
61	}
62	return err
63}
64
65// handleStatus 处理申请电子回单的单据状态
66func handleStatus(resp *AcceptElecsignResponse) error {
67	if resp.State == nil {
68		return fmt.Errorf("state is nil")
69	}
70	switch *resp.State {
71	case ELECSIGNSTATUS_GENERATING:
72		// 表示当前电子回单已受理成功并在处理中,通过查询电子回单接口获取下载链接,用于下载电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716436
73	case ELECSIGNSTATUS_FINISHED:
74		// 表示当前电子回单已处理完成,通过查询电子回单接口获取下载链接,用于下载电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716436
75	case ELECSIGNSTATUS_FAILED:
76		// 已失败,当前电子回单生成失败(终态),可重新申请
77	default:
78		return fmt.Errorf("unknown status: %s", *resp.State)
79	}
80	return nil
81}

4.1.2 通过微信转账单号申请

接口文档参考:微信单号申请电子回单

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleAcceptElecsignByNo 通过微信单号申请电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716456
10func HandleAcceptElecsignByNo(config *wxpay_utility.MchConfig) error {
11	// 使用微信单号申请电子回单接口:POST /v3/fund-app/mch-transfer/elecsign/transfer-bill-no,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716452
12	// 申请前请确认:仅支持状态为SUCCESS、已传入收款用户姓名、六个月内的转账单据
13	req := &AcceptElecsignByNoRequest{
14		// 转账单据对应的微信支付单号,长度30-64个字符
15		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
16	}
17
18	resp, err := AcceptElecsignByNo(config, req)
19	if err != nil {
20		return handleError(err)
21	}
22
23	return handleStatus(resp)
24}
25
26// handleError 处理微信单号申请电子回单接口的错误码
27func handleError(err error) error {
28	var apiError *wxpay_utility.ApiException
29	if errors.As(err, &apiError) {
30		fmt.Printf("状态码: %d\n", apiError.StatusCode())
31		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
32		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
33		switch apiError.ErrorCode() {
34		case "PARAM_ERROR":
35			// 错误:PARAM_ERROR
36			// 描述:参数错误
37			// 解决方式:请根据错误提示正确传入参数
38		case "INVALID_REQUEST":
39			// 错误:INVALID_REQUEST
40			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
41			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
42		case "SIGN_ERROR":
43			// 错误:SIGN_ERROR
44			// 描述:验证不通过
45			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
46		case "SYSTEM_ERROR":
47			// 错误:SYSTEM_ERROR
48			// 描述:系统异常,请稍后重试
49			// 解决方式:请稍后重试
50		default:
51			// 其他类型错误
52		}
53	}
54	return err
55}
56
57// handleStatus 处理申请电子回单的单据状态
58func handleStatus(resp *AcceptElecsignResponse) error {
59	if resp.State == nil {
60		return fmt.Errorf("state is nil")
61	}
62	switch *resp.State {
63	case ELECSIGNSTATUS_GENERATING:
64		// 表示当前电子回单已受理成功并在处理中,通过查询电子回单接口获取下载链接,用于下载电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716436
65	case ELECSIGNSTATUS_FINISHED:
66		// 表示当前电子回单已处理完成,通过查询电子回单接口获取下载链接,用于下载电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716436
67	case ELECSIGNSTATUS_FAILED:
68		// 已失败,当前电子回单生成失败(终态),可重新申请
69	default:
70		return fmt.Errorf("unknown status: %s", *resp.State)
71	}
72	return nil
73}

4.2 查询电子回单

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

4.2.1 通过商户单号查询

接口文档参考:商户单号查询电子回单

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleQueryElecsignByOutNo 通过商户单号查询电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716436
10func HandleQueryElecsignByOutNo(config *wxpay_utility.MchConfig) error {
11	// 使用商户单号查询电子回单接口:GET /v3/fund-app/mch-transfer/elecsign/out-bill-no/{out_bill_no}
12	req := &QueryElecsignByOutNoRequest{
13		// 商户创建转账单据使用的单号,长度5-32个字符
14		OutBillNo: wxpay_utility.String("plfk2020042013"),
15	}
16
17	resp, err := QueryElecsignByOutNo(config, req)
18	if err != nil {
19		return handleError(err)
20	}
21
22	return handleStatus(resp)
23}
24
25// handleError 处理商户单号查询电子回单接口的错误码
26func handleError(err error) error {
27	var apiError *wxpay_utility.ApiException
28	if errors.As(err, &apiError) {
29		fmt.Printf("状态码: %d\n", apiError.StatusCode())
30		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
31		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
32		switch apiError.ErrorCode() {
33		case "PARAM_ERROR":
34			// 错误:PARAM_ERROR
35			// 描述:参数错误
36			// 解决方式:请根据错误提示正确传入参数
37		case "INVALID_REQUEST":
38			// 错误:INVALID_REQUEST
39			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
40			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
41		case "SIGN_ERROR":
42			// 错误:SIGN_ERROR
43			// 描述:验证不通过
44			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
45		case "SYSTEM_ERROR":
46			// 错误:SYSTEM_ERROR
47			// 描述:系统异常,请稍后重试
48			// 解决方式:请稍后重试
49		default:
50			// 其他类型错误
51		}
52	}
53	return err
54}
55
56// handleStatus 处理查询电子回单的单据状态
57func handleStatus(resp *QueryElecsignResponse) error {
58	if resp.State == nil {
59		return fmt.Errorf("state is nil")
60	}
61	switch *resp.State {
62	case ELECSIGNSTATUS_GENERATING:
63		// 生成中,当前电子回单已受理成功并在处理中(非终态),可继续轮询查询
64	case ELECSIGNSTATUS_FINISHED:
65		// 已完成,当前电子回单已处理完成(终态),download_url 字段包含下载地址
66		if resp.DownloadUrl == nil {
67			return fmt.Errorf("download_url is nil")
68		}
69	case ELECSIGNSTATUS_FAILED:
70		// 已失败,当前电子回单生成失败(终态),可重新申请
71	default:
72		return fmt.Errorf("unknown status: %s", *resp.State)
73	}
74	return nil
75}

4.2.2 通过微信转账单号查询

接口文档参考:微信单号查询电子回单

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility" // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334
5	"errors"
6	"fmt"
7)
8
9// HandleQueryElecsignByNo 通过微信单号查询电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012716455
10func HandleQueryElecsignByNo(config *wxpay_utility.MchConfig) error {
11	// 使用微信单号查询电子回单接口:GET /v3/fund-app/mch-transfer/elecsign/transfer-bill-no/{transfer_bill_no}
12	req := &QueryElecsignByNoRequest{
13		// 转账单据对应的微信支付单号,长度30-64个字符
14		TransferBillNo: wxpay_utility.String("1330000071100999991182020050700019480001"),
15	}
16
17	resp, err := QueryElecsignByNo(config, req)
18	if err != nil {
19		return handleError(err)
20	}
21
22	return handleStatus(resp)
23}
24
25// handleError 处理微信单号查询电子回单接口的错误码
26func handleError(err error) error {
27	var apiError *wxpay_utility.ApiException
28	if errors.As(err, &apiError) {
29		fmt.Printf("状态码: %d\n", apiError.StatusCode())
30		fmt.Printf("错误码: %s\n", apiError.ErrorCode())
31		fmt.Printf("错误信息: %s\n", apiError.ErrorMessage())
32		switch apiError.ErrorCode() {
33		case "PARAM_ERROR":
34			// 错误:PARAM_ERROR
35			// 描述:参数错误
36			// 解决方式:请根据错误提示正确传入参数
37		case "INVALID_REQUEST":
38			// 错误:INVALID_REQUEST
39			// 描述:HTTP 请求不符合微信支付 APIv3 接口规则
40			// 解决方式:请参阅接口规则,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012081709
41		case "SIGN_ERROR":
42			// 错误:SIGN_ERROR
43			// 描述:验证不通过
44			// 解决方式:请参阅签名常见问题,参考:https://pay.weixin.qq.com/doc/v3/merchant/4012072670
45		case "SYSTEM_ERROR":
46			// 错误:SYSTEM_ERROR
47			// 描述:系统异常,请稍后重试
48			// 解决方式:请稍后重试
49		default:
50			// 其他类型错误
51		}
52	}
53	return err
54}
55
56// handleStatus 处理查询电子回单的单据状态
57func handleStatus(resp *QueryElecsignResponse) error {
58	if resp.State == nil {
59		return fmt.Errorf("state is nil")
60	}
61	switch *resp.State {
62	case ELECSIGNSTATUS_GENERATING:
63		// 生成中,当前电子回单已受理成功并在处理中(非终态),可继续轮询查询
64	case ELECSIGNSTATUS_FINISHED:
65		// 已完成,当前电子回单已处理完成(终态),download_url 字段包含下载地址
66		if resp.DownloadUrl == nil {
67			return fmt.Errorf("download_url is nil")
68		}
69	case ELECSIGNSTATUS_FAILED:
70		// 已失败,当前电子回单生成失败(终态),可重新申请
71	default:
72		return fmt.Errorf("unknown status: %s", *resp.State)
73	}
74	return nil
75}

4.3 下载电子回单

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

接口文档参考:下载电子回单

 注意事项:

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

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

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

申请电子回单查询电子回单接口获取下载地址(不需要重新申请)。

示例代码

1package transfer
2
3import (
4	"demo/wxpay_utility"
5	"fmt"
6	"io"
7	"net/http"
8	"net/url"
9	"os"
10	"strings"
11)
12
13// HandleDownloadElecsign 下载电子回单,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013866774
14// downloadUrl、hashValue、hashType 从查询电子回单接口返回结果中获取
15func HandleDownloadElecsign(config *wxpay_utility.MchConfig) error {
16	const (
17		downloadUrl   = "https://api.mch.weixin.qq.com/v3/transferdownload/elecvoucherfile?source=elecsign&token=xxx" // 查询电子回单接口返回的 download_url
18		hashValue     = "01BD163ACF221DD73DBEEA8903F0340DBBF417A38583246D8A027480EC4F0EA0"                             // 查询电子回单接口返回的 hash_value
19		hashType      = "SM3"                                                                                         // 查询电子回单接口返回的 hash_type,支持 SHA256 和 SM3
20		method        = "GET"
21		localFilePath = "downloaded_file.pdf" // 电子回单文件保存的本地路径,文件格式为PDF
22	)
23
24	parsedURL, err := url.Parse(downloadUrl)
25	if err != nil {
26		fmt.Println(err)
27		return err
28	}
29	canonicalURL := parsedURL.RequestURI() // 返回 path?query 部分
30
31	httpRequest, err := http.NewRequest(method, downloadUrl, nil)
32	if err != nil {
33		fmt.Println(err)
34		return err
35	}
36	authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(),
37		config.PrivateKey(), method, canonicalURL, nil)
38	if err != nil {
39		fmt.Println(err)
40		return err
41	}
42	httpRequest.Header.Set("Authorization", authorization)
43
44	client := &http.Client{}
45	httpResponse, err := client.Do(httpRequest)
46	if err != nil {
47		fmt.Println(err)
48		return err
49	}
50	defer httpResponse.Body.Close()
51
52	if httpResponse.StatusCode != http.StatusOK {
53		fmt.Println("http status code:", httpResponse.StatusCode)
54		return fmt.Errorf("http status code: %d", httpResponse.StatusCode)
55	}
56
57	// 流式写入本地文件
58	outFile, err := os.Create(localFilePath)
59	if err != nil {
60		fmt.Println(err)
61		return err
62	}
63	defer outFile.Close()
64
65	if _, err = io.Copy(outFile, httpResponse.Body); err != nil {
66		fmt.Println(err)
67		return err
68	}
69	outFile.Close() // 显式关闭,确保数据落盘后再读取
70
71	// 打开文件流进行哈希校验
72	f, err := os.Open(localFilePath)
73	if err != nil {
74		fmt.Println(err)
75		return err
76	}
77	defer f.Close()
78
79	var actualHash string
80	switch hashType {
81	case "SHA256":
82		actualHash, err = wxpay_utility.GenerateSHA256FromStream(f)
83		if err != nil {
84			fmt.Println(err)
85			return err
86		}
87	case "SM3":
88		actualHash, err = wxpay_utility.GenerateSM3FromStream(f)
89		if err != nil {
90			fmt.Println(err)
91			return err
92		}
93	default:
94		return fmt.Errorf("unknown hash type: %s", hashType)
95	}
96
97	if strings.ToUpper(actualHash) != hashValue {
98		fmt.Println("hash mismatch")
99		return fmt.Errorf("hash mismatch: expected %s, got %s", hashValue, actualHash)
100	}
101
102	fmt.Println("download success")
103	return nil
104}