当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。使用该接口时的一些注意事项如下:
退款申请接口的请求地址为:https://api.mch.weixin.qq.com/v3/refund/domestic/refunds
退款申请接口的请求参数说明如下:
参数名 | 变量 | 描述 |
---|---|---|
子商户号 | sub_mchid | 子商户的商户号(服务商模式下) |
微信支付订单号 | transaction_id | 原支付交易对应的微信订单号 |
商户订单号 | out_trade_no | 原支付交易对应的商户订单号 |
商户退款单号 | out_refund_no | 商户系统内部的退款单号 |
退款原因 | reason | 若商户传入,会在下发给用户的退款消息中体现退款原因 |
退款结果回调url | notify_url | 异步接收微信支付退款结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效,优先回调当前传的这个地址。 |
退款资金来源 | funds_account | 若传递此参数则使用对应的资金账户退款,否则默认使用未结算资金退款(仅对老资金流商户适用) |
金额信息 | amoun | t 订单金额信息 |
退款商品 | goods_detail | 指定商品退款需要传此参数,其他场景无需传递 |
接下来分别给出直连商户与服务商两种模式下的支付退款申请接口的相关数据结构以及函数。
//退款金额信息
type RefundCreateAmount struct {
//退款金额,单位为分
Refund int `json:"refund"`
//原订单金额,单位为分
Total int `json:"total"`
//退款币种:CNY(人民币),境内商户号仅支持人民币。
Currency string `json:"currency"`
}
//微信支付退款申请请求参数
type RefundCreateReq struct {
//子商户的商户号(服务商)
Sub_mchid string `json:"sub_mchid,omitempty"`
//商户系统内部订单号
Out_trade_no string `json:"out_trade_no,omitempty"`
//微信支付订单号
Transaction_id string `json:"transaction_id,omitempty"`
//商户系统内部的退款单号
Out_refund_no string `json:"out_refund_no"`
//退款原因
Reason string `json:"reason,omitempty"`
//退款结果回调的URL,不允许携带查询串
Notify_url string `json:"notify_url,omitempty"`
//退款金额信息
Amount RefundCreateAmount `json:"amount"`
}
//微信退款订单信息
type RefundOrderInfo struct {
//服务商户号(普通商户)
Mchid string `json:"mchid"`
//服务商户号(服务商)
Sp_mchid string `json:"sp_mchid"`
//子商户的商户号(服务商)
Sub_mchid string `json:"sub_mchid"`
//微信支付退款单号
Refund_id string `json:"refund_id"`
//商户系统内部的退款单号
Out_refund_no string `json:"out_refund_no"`
//微信支付订单号
Transaction_id string `json:"transaction_id"`
//商户系统内部订单号
Out_trade_no string `json:"out_trade_no"`
//退款渠道
Channel string `json:"channel"`
//退款入账账户
User_received_account string `json:"user_received_account"`
//退款成功时间
Success_time string `json:"success_time"`
//退款创建时间
Create_time string `json:"create_time"`
//退款状态
Status string `json:"status"`
//退款所使用资金对应的资金账户类型
Funds_account string `json:"funds_account"`
//订单金额信息
Amount RefundOrderAmount `json:"amount"`
}
//提交退款申请(服务商模式)
func refundOrderX(ent *MchParam, data RefundCreateReq) (RefundOrderInfo, error) {
data_body, _ := json.Marshal(data)
var pret RefundOrderInfo
const url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"
result, err := WxPayPostV3(ent, url, data_body)
if err != nil {
return pret, err
}
err = json.Unmarshal([]byte(result), &pret)
if err != nil {
return pret, err
}
return pret, nil
}
//提交退款申请(普通商户模式)
func refundOrder(ent *MchParam, data RefundCreateReq) (RefundOrderInfo, error) {
data.Sub_mchid = ""
data_body, _ := json.Marshal(data)
var pret RefundOrderInfo
const url = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"
result, err := WxPayPostV3(ent, url, data_body)
if err != nil {
return pret, err
}
err = json.Unmarshal([]byte(result), &pret)
if err != nil {
return pret, err
}
return pret, nil
}
当商户申请的退款有结果后(退款状态为:退款成功、退款关闭、退款异常),微信会把相关结果发送给商户,商户需要进行接收处理,并返回应答。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP请求头中:Wechatpay-Signature。商户应当验证签名,以确认请求来自微信,而不是其他的第三方。
退款通知HTTP应答码为200且返回状态码为SUCCESS才会当做商户接收成功。如果微信收到的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。如果在所有通知频率后没有收到微信侧回调。商户应调用退款查询接口确认退款状态。
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
退款通知的处理逻辑如下:
1)首先从请求的header中获取签名
2)使用微信支付 的平台私钥(不是商户私钥 )进行签名验证。
3)使用APIv3密钥对支付订单数据进行解密
首先看看退款通知的相关参数说明:
参数名 | 变量 | 描述 |
---|---|---|
通知ID | id | 通知的唯一ID |
通知创建时间 | create_time | 通知创建的时间 |
通知类型 | event_type | 通知的类型:REFUND.SUCCESS:退款成功通知REFUND.ABNORMAL:退款异常通知REFUND.CLOSED:退款关闭通知 |
通知简要说明 | summary | 通知简要说明 |
通知数据类型 | resource_type | 通知的资源数据类型,支付成功通知为encrypt-resource |
通知数据 | resource | 通知资源数据JSON格式 |
退款结果对重要的数据进行了加密,商户需要用商户密钥进行解密后才能获得结果通知的内容。商户对resource对象进行解密后,得到的通知参数,具体字段含义如下:
参数名 | 变量 | 描述 |
---|---|---|
直连商户号 | mchid | 直连商户的商户号(直连模式下)。 |
服务商户号 | sp_mchid | 服务商户号(服务商模式下)。 |
子商户号 | sub_mchid | 子商户的商户号(服务商模式下)。 |
商户订单号 | out_trade_no | 返回的商户订单号 |
微信支付订单号 | transaction_id | 微信支付订单号 |
商户退款单号 | out_refund_no | 商户退款单号 |
微信支付退款单号 | refund_id | 微信退款单号 |
退款状态 | refund_status | 退款状态,枚举值:SUCCESS:退款成功CLOSE:退款关闭ABNORMAL:退款异常 |
退款成功时间 | success_time | 退款成功时间 |
退款入账账户 | user_received_account | 取当前退款单的退款入账方。 |
金额信息 | amount | 金额信息 |
退款通知相关的数据结构定义如下:
//退款金额信息
type RefundOrderAmountCB struct {
//退款金额,单位为分
Refund int `json:"refund"`
//原订单金额,单位为分
Total int `json:"total"`
//退款币种:CNY(人民币),境内商户号仅支持人民币。
Currency string `json:"currency"`
//用户支付金额,单位为分
Payer_total int `json:"payer_total,omitempty"`
//退款给用户的金额,不包含所有优惠券金额
Payer_refund int `json:"payer_refund,omitempty"`
}
//微信退款订单信息
type RefundOrderInfoCB struct {
//服务商户号(普通商户)
Mchid string `json:"mchid"`
//服务商户号(服务商)
Sp_mchid string `json:"sp_mchid"`
//子商户的商户号(服务商)
Sub_mchid string `json:"sub_mchid"`
//微信支付订单号
Transaction_id string `json:"transaction_id"`
//商户系统内部订单号
Out_trade_no string `json:"out_trade_no"`
//微信支付退款单号
Refund_id string `json:"refund_id"`
//商户系统内部的退款单号
Out_refund_no string `json:"out_refund_no"`
//退款状态,SUCCESS:退款成功 CLOSE:退款关闭 ABNORMAL:退款异常
Refund_status string `json:"refund_status"`
//退款成功时间
Success_time string `json:"success_time"`
//退款入账账户
User_received_account string `json:"user_received_account"`
//订单金额信息
Amount RefundOrderAmountCB `json:"amount"`
}
接下来看看一个退款通知处理的示例代码:
func HandlerRefundCB(w http.ResponseWriter, r *http.Request) {
var ret_info RefundOrderInfoCB
body, err := ioutil.ReadAll(r.Body)
if err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
if len(body) < 1 {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//读取签名验证所需的参数
var sing_param WxSignParam
err = sing_param.InitFromRequest(r, string(body))
if err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//获取平台证书,并进行签名验证
plat_certificate := GetPlatCertificate(ent, sing_param.CertSerial)
err = ResponseValidate(&sing_param, plat_certificate);
if err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//body数据解析
var ent_cb WeixinPayNotice
if err = json.Unmarshal(body, &ent_cb); err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//数据解密
decryptBytes, err := DecryptAES256GCM(
ent.MchAPIKey,
ent_cb.Resource.AssociatedData,
ent_cb.Resource.Nonce,
ent_cb.Resource.Ciphertext)
if err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//订单数据解析
err = json.Unmarshal([]byte(decryptBytes), &ret_info)
if err != nil {
wxpay4go.HttpCallBackReturn(w, 500,"FAIL", "FAIL")
return
}
//其他业务逻辑开始
//..........................
//其他业务逻辑结束
wxpay4go.HttpCallBackReturn(w, 200, "SUCCESS", "SUCCESS")
}
退款通知HTTP应答码为200且返回状态码为SUCCESS才会当做商户接收成功。如果微信收到的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。如果在所有通知频率后没有收到微信侧回调。商户应调用退款查询接口确认退款状态。
退款有一定延时,建议在提交退款申请后1分钟发起退款状态查询,一般来说零钱支付的退款5分钟内到账,银行卡支付的退款1-3个工作日到账。
微信支付为直连商户与服务商提供了不同的退款查询接口(接口的地址与参数都不相同),接下来分别介绍两种模式下的退款查询接口的使用方式。
1)服务商模式
接口的格式为:
https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/{out_refund_no}?sub_mchid={sub_mchid}
接口中的请求参数说明如下:
两种查询方式返回结果相同,返回结果的数据结构如下所示:
参数名 | 变量 | 描述 |
---|---|---|
微信支付退款单号 | refund_id | 微信支付退款单号 |
商户退款单号 | out_refund_no | 商户系统内部的退款单号 |
微信支付订单号 | transaction_id | 微信支付交易订单号 |
商户订单号 | out_trade_no | 原支付交易对应的商户订单号 |
退款渠道 | channel | 枚举值:ORIGINAL:原路退款BALANCE:退回到余额OTHER_BALANCE:原账户异常退到其他余额账户OTHER_BANKCARD:原银行卡异常退到其他银行卡 |
退款入账账户 | user_received_account | 取当前退款单的退款入账方 |
退款成功时间 | success_time | 退款成功时间,当退款状态为退款成功时有返回。 |
退款创建时间 | create_time | 退款受理时间 |
退款状态 | status | 枚举值:SUCCESS:退款成功CLOSED:退款关闭PROCESSING:退款处理中ABNORMAL:退款异常 |
资金账户 | funds_account | 退款所使用资金对应的资金账户类型 |
金额信息 | amount | 金额详细信息 |
优惠退款信息 | promotion_detail | 优惠退款信息 |
以下是退款查询的服务商模式的代码以及直连商户模式的代码:
//查询单笔退款(服务商模式)
func queryRefundOrderX(ent *MchParam, out_refund_no string, mchid string) (RefundOrderInfo, error) {
var ret_info RefundOrderInfo
url := fmt.Sprintf("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/%s?sub_mchid=%s", out_refund_no, mchid)
result, err := WxPayGetV3(ent, url)
if err != nil {
fmt.Println(err)
return ret_info, err
}
err = json.Unmarshal([]byte(result), &ret_info)
if err != nil {
fmt.Println(err)
return ret_info, err
}
return ret_info, nil
}
//查询单笔退款(直连商户模式)
func queryRefundOrder(ent *MchParam, out_refund_no string, mchid string) (RefundOrderInfo, error) {
var ret_info RefundOrderInfo
url := fmt.Sprintf("https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/%s", out_refund_no)
result, err := WxPayGetV3(ent, url)
if err != nil {
fmt.Println(err)
return ret_info, err
}
err = json.Unmarshal([]byte(result), &ret_info)
if err != nil {
fmt.Println(err)
return ret_info, err
}
return ret_info, nil
}