应用场景
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
注意事项
-
交易时间超过一年的订单无法提交退款
-
微信支付退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交,请不要更换退款单号,请使用原商户退款单号
-
请求频率限制: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"
}
}