微信小程序的支付与退款功能

微信支付

微信官方微信支付产品有付款码支付,JSAPI支付,Native支付,App支付,H5支付,小程序支付,人脸支付等不同的支付产品,在这里我们讲解微信小程序支付

  • 业务流程
    首先我们先看一下微信官方小程序支付文档所罗列的支付业务流程时序图
    微信小程序的支付与退款功能_第1张图片
    1、微信小程序用户进入产品详情页点击支付按钮
    2、调用小程序登陆API,返回用户openid
    3、商户系统处理订单业务
    4、调用微信统一下单API
    5、将微信返回的prepay_id配合其他数据再次签名,返回给前端
    6、前段用返回的参数,调用微信支付,返回支付结果
    7、微信异步通知商户支付结果
代码示例


namespace App\Http\Controllers\api;


class WeChatPay
{
    protected $app_id; //小程序 app id
    protected $app_secret;//小程序的 secret
    protected $mch_id;//小程序商户号
    protected $key;//商户号 key
    protected $notify_url;//支付回调地址
    protected $pay_url;//统一下单请求地址
    protected $trade_type;//支付交易类型
    protected $order;
    protected $sign_type;
    protected $refund_url;

    public function __construct($order)
    {
        $this->app_id = '';
        $this->app_secret = '';
        $this->key = '';
        $this->mch_id = '';
        $this->notify_url = '';
        $this->pay_url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $this->refund_url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
        $this->trade_type = 'JSAPI';
        $this->order = $order;
    }

    /**
     * 微信对外支付方法
     */
    public function pay()
    {
        //统一下单 返回与支付信息
        $pre_pay_id = $this->unifiedOrder($this->order);
        //再次签名
        $package = $this->signAgain($pre_pay_id);
        return $package;
    }

    //微信统一下单
    private function unifiedOrder($order)
    {
        $post_data = $this->getPayData($order);
        $wx_return_data = $this->httpCurl($post_data, $this->pay_url);
        $wx_return_data = $this->xmlToArray($wx_return_data);
        if ($wx_return_data['return_code'] == 'SUCCESS' && $wx_return_data['result_code'] == 'SUCCESS') {
            $pre_pay_id = $wx_return_data['prepay_id'];
            return $pre_pay_id;
        }
        //记录错误信息或者抛出异常
        Log::error('统一下单失败,微信返回信息:' . json_encode($wx_return_data));
        throw new \Exception($wx_return_data['return_msg'], 401);
    }

    //根据预下单id再次签名

    private function getPayData($order)
    {
        $params = [
            'appid'            => $this->app_id,
            'mch_id'           => $this->mch_id,
            'nonce_str'        => $this->createNoncestr(),
            'sign_type'        => $this->sign_type,
            'body'             => $order['pro_desc'],
            'attach'           => $order['attach'], //此处传入的 attach,在回调的时候会原样返回
            'out_trade_no'     => $order['order_num'],
            'fee_type'         => 'CNY',
            'total_fee'        => $order['total_fee'],  //单位为分
            'spbill_create_ip' => $_SERVER['REMOTE_ADDR'],//终端ip
            'notify_url'       => $this->notify_url,
            'trade_type'       => $this->trade_type,
            'openid'           => $order['openid'], //交易类型为JSAPI时,必传
        ];
        $params['sign'] = $this->makeSign($params);
        $data = $this->arrayToXml($params);
        return $data;
    }

    /**
     * 创建32位随机字符串
     * @param int $length
     * @return string
     */
    private function createNoncestr($length = 32)
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = "";
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }

    /**
     * 获取签名
     * @param $params array  所有发送的数据集合
     * @return string string 签名信息
     */
    private function makeSign($params)
    {
        ksort($params);
        $string = $this->formatIntoQueryStr($params);
        $string = strtoupper(md5($string . "&key=" . $this->key));
        return $string;
    }

    /**
     * 格式化成查询字符串
     * @param $params
     * @return string
     */
    private function formatIntoQueryStr($params)
    {
        $tmp_array = [];
        foreach ($params as $key => $value) {
            if (empty($value)) continue;
            $tmp_array[] = $key . '=' . $value;
        }
        $string = implode("&", $tmp_array);
        unset($tmp_array);
        return $string;
    }

    /**
     * 将数组转换为xml字符串
     * @param $arg array
     * @return bool|string
     */
    private function arrayToXml($arg)
    {
        if (!is_array($arg) || count($arg) == 0) return false;
        $xml = "";
        foreach ($arg as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . " . $key . ">";
            } else {
                $xml .= "<" . $key . "> . $val . "]]> . $key . ">";
            }
        }
        $xml .= "";
        return $xml;
    }

    private function httpCurl($xml, $url,$is_need_pem = false, $second = 30, $headers = [])
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);

        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);

        //设置header
        if ($headers) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        } else {
            curl_setopt($ch, CURLOPT_HEADER, FALSE);
        }
        //是否需要使用证书:cert 与 key 分别属于两个.pem文件
        if ($is_need_pem) {
            $cert_path = '';
            $key_path = '';
            //默认格式为PEM,可以注释
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, $cert_path);
            //默认格式为PEM,可以注释
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, $key_path);
        }

        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        //post提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        //运行curl
        $data = curl_exec($ch);
        //返回结果
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            throw new \Exception("curl出错,错误码:$error");
        }
    }

    /**
     * 通过 simpleXML 将xml转换为数组,如果xml某一项为空,会将该项转换为空数组
     * @param $xml
     * @return bool|false|mixed|SimpleXMLElement|string
     */
    private function xmlToArray($xml)
    {
        if (!$xml) return false;
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $data = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
        $data = json_encode($data);
        $data = json_decode($data, true);
        return $data;
    }

    /**
     * 将xml转换为数组
     * @param $xml
     * @return array
     */
    private function xml2array($xml)
    {
        $p = xml_parser_create();
        xml_parse_into_struct($p, $xml, $value_arr, $index_arr);
        xml_parser_free($p);
        $data = [];
        foreach ($index_arr as $key => $value) {
            if ($key == 'xml' || $key == 'XML') continue;
            $tag = strtolower($value_arr[$value[0]]['tag']);
            $value = $value_arr[$value[0]]['value'];
            $data[$tag] = $value;
        }
        return $data;
    }

    /**
     * 返回再次签名信息
     * @param $pre_pay_id
     * @return array
     */
    private function signAgain($pre_pay_id)
    {
        $data = [
            'appId' => $this->app_id,
            'timeStamp' => time(),
            'nonceStr' => $this->createNoncestr(),
            'package' => 'prepay_id=' . $pre_pay_id,
            'signType' => $this->sign_type,
        ];
        $sign = $this->makeSign($data);
        unset($data['appId']);
        $data['paySign'] = $sign;
        return $data;
    }

    /**
     * 微信支付回调
     * @return string
     */
    public function payNotify()
    {
        $xml = file_get_contents("php://input");
        $data = $this->xml2array($xml);
        //微信支付回调其他业务操作

        return "
                  
                  
                ";
    }

    /**
     * 微信退款
     * @param $order
     */
    public function refund($order)
    {
        $xml = $this->getRefundXml($order);
        $wx_refund_result = $this->httpCurl($xml, $this->refund_url, true);
        $result = $this->xml2array($wx_refund_result);

        if( $result['return_code'] == 'SUCCESS'){
            //退款成功,处理业务逻辑
        }else{
            throw new \Exception('微信退款错误信息:'.$result['return_msg'],401);
        }

    }

    /**
     * @param $order
     * @return array
     */
    private function getRefundXml($order)
    {
        $data = [
            'appid'          => $this->app_id,
            'mch_id'         => $this->mch_id,
            'nonce_str'      => $this->createNoncestr(),
            'sign_type'      => $this->sign_type,
            'out_trade_no'   => $order['trade_no'],
            'out_refund_no'  => $order['refund_no'],
            'total_fee'      => $order['total_fee'],
            'refund_fee'     => $order['refund_fee'],//单位为分
            'refund_desc'    => $order['refund_desc'],//退款原因
            'refund_account' => 'REFUND_SOURCE_RECHARGE_FUNDS',//退款资金来源
        ];
        $sign = $this->makeSign($data);
        $data['sign'] = $sign;
        $xml = $this->arrayToXml($data);
        return $xml;
    }

}

注意其中重要的环节需要记录日志和异常处理

你可能感兴趣的:(后端工程师之路)