應用場景
當交易發生之後一段時間內,由於買家或者賣家的原因需要退款時,賣家可以通過退款接口將支付款退還給買家,微信支付將在收到退款請求並且驗證成功之後,按照退款規則將支付款按原路退到買家帳號上。
注意事項
-
交易時間超過一年的訂單無法提交退款
-
微信支付退款支持單筆交易分多次退款,多次退款需要提交原支付訂單的商戶訂單號和設置不同的退款單號。申請退款總金額不能超過訂單金額。 一筆退款失敗後重新提交,請不要更換退款單號,請使用原商戶退款單號
-
請求頻率限制:150qps,即每秒鐘正常的申請退款請求次數不超過150次
錯誤或無效請求頻率限制:6qps,即每秒鐘異常或錯誤的退款申請請求不超過6次 -
每個支付訂單的部分退款次數不能超過50次
-
爲保證支付安全需要下載支付api簽名證書
實現
- 發送退款申請
/**
* @do 自動退款問題
* @parm order_uuid
* @return status
*/
public function wechatRefund()
{
//todo 自己的數據驗證
$input = file_get_contents('php://input');
$input = json_decode($input, true);
$payment = Db::table('payment')
->alias('p')
->join('mall_orders o', 'o.id = p.oid')
->field('p.transaction_id, p.total_fee')
->where('o.uuid', $input['uuid'])
->find();
if($input['type'] == 1)
{
$payConfig = Config::get('app.WECHAT_PAY_CONFIG');
}else{
$payConfig = Config::get('app.WECHAT_JSAPI_PAY_CONFIG');
}
$refundData = array(
'appid' => $payConfig['appid'],
'mch_id' => $payConfig['mch_id'],
'nonce_str' => '13vssdfsfsfewffffwfw',//隨機字符串
// 'out_trade_no' => '',//和 transaction_id 二選一
'transaction_id' => $payment['transaction_id'],//微信支付流水號(微信交易單號)
'out_refund_no' => 'r242668049786'.rand(100,999),//todo 商家退款單號 自己生成自己的規則
'refund_fee' => $payment['total_fee'],//這裏可以少於總額
'total_fee' => $payment['total_fee'],
'notify_url' => 'http://'.$_SERVER['HTTP_HOST'].'/api/v2/payment/refundNotify'// 異步鏈接
);
$refundData['sign'] = $this->getWeixinSign($refundData, $payConfig['key']); //簽名
$xml = "<xml>";
foreach ($refundData as $key=>$val)
{
$xml.="<".$key.">".$val."</".$key.">";
}
$xml.="</xml>";
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
$data = $this->curlClient($url, $xml, $input['type']);
if($data)
{
$wxReturn = json_decode(json_encode(simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return json($wxReturn);
}else{
//請求失敗處理
}
}
/**
* @do 發佈請求
* @param $url 請求連接
* @param $xml 請求XML數據
* @param $type 1-app 2-jsapi (根據不同渠道配置 不同api證書)
* @return xml數據
*/
private function curlClient($url, $xml, $type = 1)
{
$ch = curl_init();
//設置超時
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLCERT, getcwd().'/../extend/wechat/apiclient_cert.pem');
//默認格式爲PEM,可以註釋
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLKEY,getcwd().'/../extend/wechat/apiclient_key.pem');
//設置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求結果爲字符串且輸出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//運行curl
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
/**
* @do 微信簽名加密
* @param 數據參數 加密key
* @return 加密完數據
*/
private function getWeixinSign($data,$key){
ksort($data);
$buff = "";
foreach ($data as $k => $v)
{
if($k != "sign" && $v != "" && !is_array($v)){
$buff .= $k . "=" . $v . "&";
}
}
$buff = trim($buff, "&") . "&key=".$key;
$string = md5($buff);
//簽名步驟四:所有字符轉爲大寫
$result = strtoupper($string);
return $result;
}
- 示例展示
失敗或者錯誤
{
"return_code": "SUCCESS",
"return_msg": "OK",
"appid": "wx2decb7568432132",
"mch_id": "150133123213",
"nonce_str": "899bAeRBHvKzBiVG",
"sign": "B47634E491630AB2C93AA3BE35DE960A",
"result_code": "FAIL",
"err_code": "REFUND_FEE_MISMATCH",
"err_code_des": "訂單金額或退款金額與之前請求不一致,請覈實後再試"
}
{
"return_code": "SUCCESS",
"return_msg": "OK",
"appid": "wx2decb7568411323",
"mch_id": "15013312313",
"nonce_str": "iuedVg1fXt3Khhyg",
"sign": "1396FF43DADDC5EBDBE7756A2747C485",
"result_code": "FAIL",
"err_code": "ERROR",
"err_code_des": "訂單已全額退款"
}
成功事例
{
"return_code": "SUCCESS",
"return_msg": "OK",
"appid": "wx2decb75684533321326",
"mch_id": "15013211312312",
"nonce_str": "MEGUyLDW1jvI8lH8",
"sign": "43BE291FC639D6D9EFC9FE72E6038E44",
"result_code": "SUCCESS",
"transaction_id": "4200000443201911147320071718",
"out_trade_no": "s242933595518",
"out_refund_no": "r242668049786143",
"refund_id": "50300602272019111413238713216",
"refund_channel": [],
"refund_fee": "1",
"coupon_refund_fee": "0",
"total_fee": "1",
"cash_fee": "1",
"coupon_refund_count": "0",
"cash_refund_fee": "1"
}
上面只是申請了退款申請,至於成功與否還是根據回調的結果來看
/**
* @do 退款結果通知
* @return success
*/
public function refundNotify()
{
//處理微信支付回調
$testxml = file_get_contents("php://input"); //接收微信發送的支付成功信息
$result = XMLDataParse($testxml);
if($result)
{
$payConfig = Config::get('app.WECHAT_PAY_CONFIG');
if($result['appid'] != $payConfig['appid'])
{
$payConfig = Config::get('app.WECHAT_JSAPI_PAY_CONFIG');
}
$info = $this->refund_decrypt($result['req_info'], $payConfig['key']);
$result['req_info'] = $info;
save_payment_log('wechat', '微信退款回調開始','weChatNotify', json_encode($result, FILE_APPEND));
//todo 根據回調信息處理後續邏輯
}
}
private function refund_decrypt($str, $key) {
$decrypt = base64_decode($str, true);
$info = $this->xmlToArray(openssl_decrypt($decrypt , 'aes-256-ecb', $key, OPENSSL_RAW_DATA));
return $info;
}
private function xmlToArray($xml)
{
libxml_disable_entity_loader(true); // 禁止引用外部xml實體
$jsonxml = json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA));
$result = json_decode($jsonxml, true);
return $result;
}
- 返回示例
{
"return_code":"SUCCESS",
"appid":"wx2decb75684323232",
"mch_id":"15013232323",
"nonce_str":"c45de72094424945a7f40ab878918d24",
"req_info":{
"out_refund_no":"r242668049786278",
"out_trade_no":"s242988257840",
"refund_account":"REFUND_SOURCE_RECHARGE_FUNDS",
"refund_fee":"1",
"refund_id":"50300602332019111413219448409",
"refund_recv_accout":"支付用戶零錢",
"refund_request_source":"API",
"refund_status":"SUCCESS",
"settlement_refund_fee":"1",
"settlement_total_fee":"1",
"success_time":"2019-11-14 14:48:06",
"total_fee":"1",
"transaction_id":"4200000451201911142685392734"
}
}