业务示例代码
更新时间:2026.04.231. 目标
通过本教程的学习,你应该可以:
通过发起转账接口,向指定微信用户发起商家转账(用户确认模式),并引导用户在微信内确认收款
通过撤销转账接口,在用户确认收款之前,可撤销转账
通过商户单号查询转账单或微信转账单号查询转账单查询转账单的当前状态
通过商家转账回调通知处理微信支付下发的回调,识别转账终态(成功/失败/撤销)
通过商户单号申请电子回单、商户单号查询电子回单、微信单号申请电子回单、微信单号查询电子回单申请并查询转账单的电子回单,通过下载电子回单下载回单文件
正确处理接口返回的错误码和转账单据状态,针对不同情况给出对应的业务处理逻辑
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 和 openidnotify_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从发起转账接口的返回结果中获取
不同端的调起方式:
APP 端(Android):通过微信 OpenSDK 的
sendReq方法拉起,参考 Android 调起用户确认收款APP 端(iOS):通过微信 OpenSDK 的
sendReq方法拉起,参考 iOS 调起用户确认收款小程序 / H5 端:通过 JSAPI 调起,参考 JSAPI 调起用户确认收款
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}

