PHP微信支付退款(申请)功能对接,完成退款到用户零钱

34 阅读4分钟

php申请微信用户退款接口。

@auth Milo

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家账号上。

文档地址:pay.weixin.qq.com/wiki/doc/ap…

特别注意:需要双向证书!

什么是双向证书?

微信支付双向证书是指在进行微信支付时,服务器与客户端之间进行通信时,双方相互进行签名校验的一种机制。这主要是为了确保双方的身份安全,保护支付交易的安全性。 具体来说,双向证书包括服务器证书和客户端证书。服务器证书用于验证微信支付服务器的身份,确保客户端与正确的服务器进行通信。而客户端证书则是商户自行申请的,用于验证商户的身份,确保微信支付系统能够正确识别商户的证书,从而保证支付接口的安全性。 在通信过程中,服务器和客户端会相互交换证书并进行验证,通过比对签名等信息来确认对方的身份是否真实有效。如果验证通过,双方将建立起安全的通信连接,从而进行后续的支付交易操作。

简单理解,就是微信服务器上生成证书,你本地也需要用微信证书工具生成,然后放到你的客户端

工作开始前准备内容:

  1. wxappid【微信公众号|微信小程序appid】
  2. wxmchid【微信商户平台商户号】
  3. paykey【微信商户平台支付秘钥】
  4. apiclient_cert.pem【证书文件】
  5. apiclient_key.pem【私钥文件】

实现代码:

调用接口

    // 退款
    public function refund_price() {
        $id = input('id/d',0);
        if(empty($id)) return ajaxArray(0,'数据异常');
        // 查询订单
        $order = model('order')->get($id);
        $appid = pay_config('wxappid');
        $mch_id = pay_config('wxmchid'); // 商户号昂
        $key = pay_config('paykey'); //Api密钥
        $notify_url = '您的退款结果通知URL'; // 如果需要接收退款结果通知,请设置 
        // 退款请求参数
        $refund_params = array(
            'appid' => $appid,
            'mch_id' => $mch_id,
            'nonce_str' => $this->getNonceStr(), // 生成随机字符串,用于签名
            'out_trade_no' => $order['out_trade_no'],
            'total_fee' => floatval($order['price']) * 100, // 单位为分,* 100,不允许小数点的出现
            'refund_fee' => floatval($order['price']) * 100,// 单位为分,* 100,不允许小数点的出现
            'out_refund_no' => $this->generateRefundNo(),
            // 其他可能需要的参数,如 refund_desc、op_user_id 等
        );
        // 对退款请求参数按字典序排序并生成签名
        ksort($refund_params);
        $refund_params['sign'] = $this->generateSign($refund_params, $key); // 使用您的签名生成函数
        // 将退款请求参数转换为 XML 格式
        $xml = $this->arrayToXml($refund_params);        
        // 使用 cURL 或其他 HTTP 客户端发起 POST 请求
        $url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml'));

        // 添加证书相关设置
        $cert_path = ROOT_PATH . 'public/wxpay/apiclient_*****_cert.pem'; // 替换为您的证书文件的实际路径
        $key_path = ROOT_PATH . 'public/wxpay/apiclient_*****_key.pem'; // 替换为您的私钥文件的实际路径
        curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLCERT, $cert_path);
        curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
        curl_setopt($ch, CURLOPT_SSLKEY, $key_path);
        
        $response = curl_exec($ch);
        //返回结果
        if($response){
            curl_close($ch);
        } else {
            $error = curl_errno($ch);
            $errorMsg =  "curl出错,错误码:$error" . "<br>";
            curl_close($ch);
            return ajaxArray(0, '退款失败', $errorMsg);
        }
       
        // 解析退款响应 XML 并检查退款结果
        $response_data = $this->xmlToArray($response);
 
        if ($this->checkResponseSign($response_data, $key)) { // 使用您的验签函数验证响应签名
            // 退款成功,处理退款结果数据
            // 修改订单状态
            model('order')->where('id',$id)->update(['status' => 4]);
            return ajaxArray(1,'退款成功');
        } else {
            // 验签失败,可能存在安全风险,应记录并处理异常
            return ajaxArray(0,'退款失败',$response_data);
        }
    }  

封装的一些方法在这里

    // 生成随机字符串
    function getNonceStr($length = 32)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $nonceStr = '';

        for ($i = 0; $i < $length; $i++) {
            $nonceStr .= $chars[mt_rand(0, strlen($chars) - 1)];
        }

        return $nonceStr;
    }
    // 签名生成函数
    function generateSign(array $params, string $key)
    {
        unset($params['sign']); //移除 sign 字段并重新计算签名
        ksort($params); // 字典序排序  
        $stringA = urldecode(http_build_query($params)) . "&key=" . $key; // 拼接字符串  
        $sign = strtoupper(md5($stringA)); // MD5加密并转为大写  
        return $sign; 
    }
    // 请求参数转换为 XML 格式
    function arrayToXml(array $data)
    {
        $xml = '<xml>';
        foreach ($data as $key => $value) {
            if (is_numeric($value)) {
                $xml .= "<{$key}>{$value}</{$key}>";
            } else {
                $xml .= "<{$key}><![CDATA[{$value}]]></{$key}>";
            }
        }
        $xml .= '</xml>';

        return $xml;
    }
    // 解析 XML 
    function xmlToArray(string $xml)
    {
        libxml_disable_entity_loader(true);
        $xml = html_entity_decode($xml);
        $data = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
        $json = json_encode($data);
        $response_data = json_decode($json, true);

        return $response_data;
    }
    // 验签函数验证响应签名
    function checkResponseSign(array $responseData, string $key)
    {
        if (!isset($responseData['return_code']) || $responseData['return_code'] !== 'SUCCESS') {
            return false; // 如果返回码不是 SUCCESS,直接返回验签失败
        }

        if (!isset($responseData['sign'])) {
            return false; // 如果没有 sign 字段,直接返回验签失败
        }
        
        $calculatedSign = $this->generateSign($responseData, $key);

        // 比较计算出的签名与返回的签名
        return $calculatedSign === $responseData['sign'];
    }
    // 退货订单号
    function generateRefundNo()
    {
        $chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-|*@';
        $randomPart = '';
        
        for ($i = 0; $i < 30; $i++) { // 减少随机部分长度为30,为时间戳留出空间
            $randomPart .= $chars[random_int(0, strlen($chars) - 1)];
        }
    
        $timestamp = strval(time());  // 当前时间戳(以秒为单位)
    
        // 将时间戳拼接到随机部分前面,保持总长度为64个字符
        $refundNo = substr($timestamp, -24) . $randomPart;  // 取最近24位(大约8年)时间戳
    
        return $refundNo;
    }

至此,恭喜您,退款完成喽,需要注意的是,我们请求的接口是申请退款,并非直接退款,为确保退款成功且有效,请访问查询退款接口,查询是否真实退款完成。 谢谢观看,不足之处还望指出,大家共同进步~加油。

Tomorrow will be better!