前言

项目上需要接触到不同种类的支付平台,有一些其他第三方平台的支付也是为了企业没资质或为了得到更好的费率原因,因此也总结一下个人在开发中的使用记录

目标

通过合并支付的接口,让用户在选择支付上能更简单快捷

前置条件

相关支付平台都有自己的支付申请条件,因此根据需求自行了解注册 环境 PHP

正式内容

支付的需求最主要在于3个接口的实现

interface PaymentClientInterface
{

    /**
     * 支付请求
     * @param $order_out_no
     * @param $total_fee
     * @param array $param
     * @return mixed
     */
    public function pay($out_trade_no,$total_fee,$param=[]);

    /**
     * 查询支付结果
     * @return mixed
     */
    public function query($out_trade_no,$param=[]);


    /**
     * 支付回调,由于各支付平台的参数都不同,因此返回原始信息自己处理
     * @param $all
     * @return mixed
     */
    public function handle($all);

}

wechat 微信支付

  public function query($out_trade_no, $param = [])
        {
    
            $action = "/v3/pay/transactions/out-trade-no/{$out_trade_no}";
            $resp = $this->request($action, [], 'GET');
            return $resp;
        }


private function sign($url, $params, $method)
    {
        $params['mchid'] = $this->apiConfig->ExtraConfig['mch_id'];
        $serial_no = $this->apiConfig->ExtraConfig['serial_no'];
        $mch_private_key = file_get_contents("/conf/wechat_private_key.pem");
        ksort($params);
        $timestamp = time();
        $nonce = $this->getNonceStr(32);
        $body = ($method == "POST" ? json_encode($params, JSON_UNESCAPED_UNICODE) : "");

        $url_parts = parse_url($url);
        $canonical_url = $url_parts['path'] . ($method == "POST" ? "" : "?" . http_build_query($params));
        $message = $method . "\n" .
            $canonical_url . "\n" .
            $timestamp . "\n" .
            $nonce . "\n" .
            $body . "\n";

        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        $sign = base64_encode($raw_sign);
        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $params['mchid'], $nonce, $timestamp, $serial_no, $sign);
        return ['query' => $params, 'token' => $token];
    }
    
    
    
    /**
     * @param $action
     * @param $params
     * @return mixed
     * @throws GuzzleHttp\Exception\GuzzleException
     */
    private function request($action, $params, $method = "POST")
    {
        $url = $this->apiConfig->Host . $action;
        $data = $this->sign($url, $params, $method);
        $option = [
            'verify' => false,
            'headers' => [
                'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)',
                'Accept' => '*/*',
                'Content-Type' => 'application/json',
                'Authorization' => "WECHATPAY2-SHA256-RSA2048 {$data['token']}",
            ],
            'http_errors' => false,
        ];
        if ($method == "GET") {
            $option['query'] = $data['query'];
        } else {
            $option['body'] = json_encode($data['query'], JSON_UNESCAPED_UNICODE);
        }
        try {
            $request = $this->client->request($method,
                $url,
                $option
            )
                ->getBody()
                ->getContents();
            return json_decode($request, 1);
        } catch (GuzzleHttp\Exception\GuzzleException $e) {
            return ['error' => $e->getMessage()];
        } catch (\Throwable $e) {
            return ['error'=>$e->getMessage()] ;
        }

    }
    
     /**
         *
         * 产生随机字符串,不长于32位
         * @param int $length
         * @return string 产生的随机字符串
         */
        public function getNonceStr($length = 32)
        {
            $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
            $str = "";
            for ($i = 0; $i < $length; $i++) {
                $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
            }
            return $str;
        }


    /**
     * 支付请求
     * @param $order_out_no
     * @param $total_fee
     * @param array $param
     * @return mixed
     */
    public function jsapi($out_trade_no, $total_fee, $param = [])
    {
        $action = "/v3/pay/transactions/jsapi";
        $p = [
            'appid' => $this->apiConfig->ExtraConfig['appid'],
            'description' => '充值',
            'out_trade_no' => $out_trade_no,
            'amount' => [
                'total' => $total_fee * 100,
                'currency' => 'CNY',
            ],
            'notify_url' => (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] ? $_SERVER['REQUEST_SCHEME'] : request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/wxpay_notify_v3.php",
//            'notify_url' =>  "http://" . MAINSITE . "/payment/wxpay_notify_v3.php",
            'payer' => [
                'openid' => $param['openid'],
            ],
        ];
        $resp = $this->request($action, $p);
        if (isset($resp['code'])) return $resp['message'];
        $data = [
            'appId' => $this->apiConfig->ExtraConfig['appid'],
            'timeStamp' => strval(time()),
            'nonceStr' => DomainUtil::generateDomainPassword(24, false),
            'package' => "prepay_id={$resp['prepay_id']}",
        ];

        $message="{$data['appId']}\n{$data['timeStamp']}\n{$data['nonceStr']}\n{$data['package']}\n";
        openssl_sign($message,
            $raw_sign,
            file_get_contents(WWWBASE . "/conf/global/wechat_private_key.pem"),
            'sha256WithRSAEncryption');

        $data['paySign'] = base64_encode($raw_sign);
        $data['openid']=$param['openid'];
        $data['signType']='RSA';
        return ['content'=>$data,'Type'=>'JSAPI'];
    }
    
        /**
         * 支付请求
         * @param $order_out_no
         * @param $total_fee
         * @param array $param
         * @return mixed
         */
        public function pay($out_trade_no, $total_fee, $param = [])
        {
            $action = "/v3/pay/transactions/native";
            $p = [
                'appid' => $this->apiConfig->ExtraConfig['appid'],
                'description' => '充值',
                'out_trade_no' => $out_trade_no,
                'amount' => [
                    'total' => intval(strval(round($total_fee,2) * 100)),
                    'currency' => 'CNY',
                ],
                'notify_url' =>  "your website url",
            ];
            $resp = $this->request($action, $p);
            if (isset($resp['error'])&&$resp['error']) return $resp['error'];
            if(isset($resp['message'])&&$resp['message']) return $resp['message'];
            return [
                'content' => $resp['code_url'],
                'QrcodeUrl' => $resp['code_url'],
                'Type' => 'qrcode_url'];
    
        }
    
    
    /**
     * 支付回调,由于各支付平台的参数都不同,因此返回原始信息自己处理
     * @param $all
     * @return mixed
     */
    public function handle($all)
    {
        // TODO: Implement handle() method.
    }
    
        public function decode($associatedData, $nonceStr, $ciphertext)
        {
            $ciphertext = base64_decode($ciphertext);
            if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
                return false;
            }
            if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
                sodium_crypto_aead_aes256gcm_is_available()) {
                return sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->apiConfig->ExtraConfig['key']);
            }
    
            // ext-libsodium (need install libsodium-php 1.x via pecl)
            if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
                \Sodium\crypto_aead_aes256gcm_is_available()) {
                return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->apiConfig->ExtraConfig['key']);
            }
    
            // openssl (PHP >= 7.1 support AEAD)
            if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
                $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
                $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
    
                return \openssl_decrypt($ctext, 'aes-256-gcm', $this->apiConfig->ExtraConfig['key'], \OPENSSL_RAW_DATA, $nonceStr,
                    $authTag, $associatedData);
            }
            return false;
        }

alipay 支付宝支付

 public function pay($out_trade_no, $total_fee, $param = [])
        {
            $p = [
                'service' => $this->is_mobile_request()?'alipay.wap.create.direct.pay.by.user':"create_direct_pay_by_user",
                'partner' => $this->apiConfig->ExtraConfig['partner'],
                'return_url' => (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] ? $_SERVER['REQUEST_SCHEME'] : request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/alipay_return_url.php",
                'notify_url' => (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] ? $_SERVER['REQUEST_SCHEME'] : request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/alipay_notify_url.php",
                '_input_charset'=>'utf-8',
                'subject'=>'网上汇入',
                'body'=>'网上汇入',
                'out_trade_no'=>$out_trade_no,
                'total_fee'=>$total_fee,
                'payment_type'=>1,
                'show_url'=>(isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] ? $_SERVER['REQUEST_SCHEME'] : request()->server('REQUEST_SCHEME'))."://".MAINSITE,
                'seller_email'=>$this->apiConfig->ExtraConfig['seller_email'],
            ];
            $action="https://mapi.alipay.com/gateway.do";
            $resp=$this->sign($p);
            return ['content' => $action."?".http_build_query($resp), 'Type' => 'url'];
        }
        
        
       public function sign($p){
            $prestr="";
            ksort($p);
            foreach ($p as $key=>$value){
                $prestr.="{$key}={$value}&";
            }
            $prestr=trim($prestr,"&");
            $p['sign_type']='MD5';
            $p['sign']=md5($prestr.$this->apiConfig->ExtraConfig['security_code']);
            return $p;
        }
        
        
          function is_mobile_request()
            {
                $_SERVER['ALL_HTTP'] = isset($_SERVER['ALL_HTTP']) ? $_SERVER['ALL_HTTP'] : '';
                $mobile_browser = '0';
                if(isset($_SERVER['HTTP_USER_AGENT'])&&preg_match('/(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|iphone|ipad|ipod|android|xoom)/i', strtolower($_SERVER['HTTP_USER_AGENT'])))
                    $mobile_browser++;
                if((isset($_SERVER['HTTP_ACCEPT'])) and (strpos(strtolower($_SERVER['HTTP_ACCEPT']),'application/vnd.wap.xhtml+xml') !== false))
                    $mobile_browser++;
                if(isset($_SERVER['HTTP_X_WAP_PROFILE']))
                    $mobile_browser++;
                if(isset($_SERVER['HTTP_PROFILE']))
                    $mobile_browser++;
                if(isset($_SERVER['HTTP_USER_AGENT'])) {
                    $mobile_ua = strtolower(substr($_SERVER['HTTP_USER_AGENT'], 0, 4));
                    $mobile_agents = array(
                        'w3c ', 'acs-', 'alav', 'alca', 'amoi', 'audi', 'avan', 'benq', 'bird', 'blac',
                        'blaz', 'brew', 'cell', 'cldc', 'cmd-', 'dang', 'doco', 'eric', 'hipt', 'inno',
                        'ipaq', 'java', 'jigs', 'kddi', 'keji', 'leno', 'lg-c', 'lg-d', 'lg-g', 'lge-',
                        'maui', 'maxo', 'midp', 'mits', 'mmef', 'mobi', 'mot-', 'moto', 'mwbp', 'nec-',
                        'newt', 'noki', 'oper', 'palm', 'pana', 'pant', 'phil', 'play', 'port', 'prox',
                        'qwap', 'sage', 'sams', 'sany', 'sch-', 'sec-', 'send', 'seri', 'sgh-', 'shar',
                        'sie-', 'siem', 'smal', 'smar', 'sony', 'sph-', 'symb', 't-mo', 'teli', 'tim-',
                        'tosh', 'tsm-', 'upg1', 'upsi', 'vk-v', 'voda', 'wap-', 'wapa', 'wapi', 'wapp',
                        'wapr', 'webc', 'winw', 'winw', 'xda', 'xda-'
                    );
                    if(in_array($mobile_ua, $mobile_agents))
                        $mobile_browser++;
                }
                if(strpos(strtolower($_SERVER['ALL_HTTP']), 'operamini') !== false)
                    $mobile_browser++;
                // Pre-final check to reset everything if the user is on Windows
                if( isset($_SERVER['HTTP_USER_AGENT'])&&strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows') !== false)
                    $mobile_browser=0;
                // But WP7 is also Windows, with a slightly different characteristic
                if(isset($_SERVER['HTTP_USER_AGENT'])&&strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'windows phone') !== false)
                    $mobile_browser++;
                if($mobile_browser>0)
                    return true;
                else
                    return false;
            }

globalalipay 海外支付宝支付

 /**
         * 支付请求
         * @param $order_out_no
         * @param $total_fee
         * @param array $param
         * @return mixed
         */
        public function pay($out_trade_no, $total_fee, $param = [])
        {
            $p = [
    //            'product_code' => 'NEW_OVERSEAS_SELLER',
                'service' => 'create_forex_trade',
                'partner' => $this->apiConfig->ExtraConfig['partner'],
                'return_url' => $this->apiConfig->ExtraConfig['return_url'],
                'notify_url' => $this->apiConfig->ExtraConfig['notify_url'],
                '_input_charset'=>'utf-8',
                'subject'=>'網上匯入',
                'body'=>'網上匯入',
                'out_trade_no'=>$out_trade_no,
                'total_fee'=>$total_fee,
                'payment_type'=>1,
                'show_url'=>(isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] ? $_SERVER['REQUEST_SCHEME'] : request()->server('REQUEST_SCHEME'))."://".MAINSITE,
                'seller_email'=>$this->apiConfig->ExtraConfig['seller_email'],
                'currency'=>isset($param['currency'])&&$param['currency']?$param['currency']:"HKD",
            ];
            $action="https://mapi.alipay.com/gateway.do";
            $resp=$this->sign($p);
            return ['content' => $action."?".http_build_query($resp), 'Type' => 'url'];
        }
        
        
            public function sign($p){
                $prestr="";
                ksort($p);
                foreach ($p as $key=>$value){
                    $prestr.="{$key}={$value}&";
                }
                $prestr=trim($prestr,"&");
                $p['sign_type']='MD5';
                $p['sign']=md5($prestr.$this->apiConfig->ExtraConfig['security_code']);
                return $p;
            }

Tonglian 通联支付

 /**
         * 网关支付
         * @param $out_trade_no
         * @param $total_fee
         * @param array $param
         * @return array
         */
        public function gateway($out_trade_no, $total_fee, $param = [])
        {
            $p = [
                'cusid' => $this->apiConfig->ExtraConfig['cusid'],
                'appid' => $this->apiConfig->Username,
                'charset' => 'UTF-8',
    //            'returl' => '',
                'returl' =>( isset($_SERVER['REQUEST_SCHEME'])&&$_SERVER['REQUEST_SCHEME']?$_SERVER['REQUEST_SCHEME']: request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/allinpay_notify_url.php",
                'notifyurl' =>( isset($_SERVER['REQUEST_SCHEME'])&&$_SERVER['REQUEST_SCHEME']?$_SERVER['REQUEST_SCHEME']: request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/allinpay_notify_url.php",
                'trxamt' => $total_fee * 100,
                'orderid' => $out_trade_no,
                'randomstr' => time(),
                'gateid' => isset($param['bankid'])&&$param['bankid']?$param['bankid']:"",
                'paytype' =>isset($param['bankid'])&&$param['bankid']?"B2C": 'B2C,B2B',
            ];
            $p['sign'] = $this->sign($p);
            $url = "{$this->apiConfig->Host}/gateway/pay";
            $str = "<form id='myform1' method='post' action='{$url}' target='_blank' >";
            foreach ($p as $key => $value) {
                $str .= "<input type='hidden' name='{$key}' value='{$value}'/>";
            }
            $str .= '</form>';
            return ['content' => $str, 'Type' => 'form'];
        }
        
        
        /**
             * 支付请求
             * 文档url: https://aipboss.allinpay.com/know/devhelp/main.php?pid=15#mid=88
             * @param $order_out_no
             * @param $total_fee
             * @param array $param
             * @return mixed
             */
            public function pay($out_trade_no, $total_fee, $param = [])
            {
                $action = "/unitorder/pay";
                $paytype = "W01";
                if (isset($param['paytype'])) {
                    switch ($param['paytype']) {
                        case "wechat":
                            $paytype = "W01";
                            break;
                        case "alipay":
                            $paytype = "A01";
                            break;
                        case "qq":
                            $paytype = "Q01";
                            break;
                        case "union":
                            $paytype = "U01";
                            break;
                    }
                }
                $p = [
                    'trxamt' => $total_fee * 100,
                    'reqsn' => $out_trade_no,
                    'paytype' => $paytype,
                    'randomstr' => time(),
                    'body' => '充值',
                    'remark' => $out_trade_no,
                    'notify_url' =>( isset($_SERVER['REQUEST_SCHEME'])&&$_SERVER['REQUEST_SCHEME']?$_SERVER['REQUEST_SCHEME']: request()->server('REQUEST_SCHEME')) . "://" . MAINSITE . "/payment/allinpay_notify_url.php",
                ];
        
                $resp = $this->request($action, $p);
                if (isset($resp['errmsg'])) {
                    return $resp['errmsg'];
                }
                $array = [
                    'Type' => 'qrcode_url',
                    'QrcodeUrl' => $resp['payinfo'],
                    'content' => $resp['payinfo'],
                ];
                return $array;
            }
            
            
          public function gateway_query($out_trade_no, $param = [])
            {
                $action="/gateway/query";
                $p=[
                    'orderid'=>$out_trade_no,
                ];
                $resp=$this->request($action,$p);
                return isset($resp['errmsg'])?$resp['errmsg']: $resp;
            }
            
            
            /**
                 * 查询支付结果
                 * @return mixed
                 */
                public function query($out_trade_no, $param = [])
                {
                   $action="/unitorder/query";
                   $p=[
                        'reqsn'=>$out_trade_no,
                   ];
                   $resp=$this->request($action,$p);
                    return isset($resp['errmsg'])?$resp['errmsg']: $resp;
                }

 public function sign($param)
    {
        $param['key'] = $this->apiConfig->Password;
        ksort($param);
        $buff = "";
        foreach ($param as $k => $v) {
            if ($v != "" && !is_array($v)) {
                $buff .= $k . "=" . $v . "&";
            }
        }
        return md5(trim($buff, "&"));
    }

    public function request($action, $param)
    {
        $param['cusid'] = $this->apiConfig->ExtraConfig['cusid'];
        $param['appid'] = $this->apiConfig->Username;
        $param['version'] = $this->apiConfig->ExtraConfig['version'];
        $sign = $this->sign($param);
        $param["sign"] = $sign;
        $content = $this->client->request("POST", $this->apiConfig->Host . $action, [
            'form_params' => $param
        ])->getBody()->getContents();
        return json_decode($content, 1);
    }

    /**
     * 支付回调,由于各支付平台的参数都不同,因此返回原始信息自己处理
     * @param $all
     * @return mixed
     */
    public function handle($all)
    {
        $sign = $all['sign'];
        unset($all['sign']);
        $mySign = $this->sign($all);
        if (strtolower($sign) == strtolower($mySign)) {
            return [
                'out_trade_no' => $all['cusorderid'],
                'amount' => $all['trxamt'] / 100,
                'attach' => isset($all['trxreserved']) ? $all['trxreserved'] : "",
                'created_at' => date("Y-m-d H:i:s", strtotime($all['trxdate'])),
                'payed_at' => date("Y-m-d H:i:s", strtotime($all['paytime'])),
            ];
        }
        return false;

    }

    public function refund($out_trade_no, $total_fee, $param = [])
    {
        $action = "/unitorder/refund";
        $p = [
            'trxamt' => $total_fee * 100,
            'oldreqsn' => $out_trade_no,
            'reqsn'=>"{$out_trade_no}_refund",
            'remark'=>'退款'
        ];
        return $this->request($action, $p);
    }

union 银联支付


  public function pay($out_trade_no, $total_fee, $param = [])
            {
                $http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
                $returnUrl = $http_type . '/payment/unionpay_result.php?';//返回URL
                $notifyUrl = $http_type . '/payment/unionpay_result.php?';//通知URL
                $p = [
                    'version' => '1.0.0',
                    'charset' => 'UTF-8',
                    'merId' => $this->apiConfig->ExtraConfig['merId'],
                    'merAbbr' => 'merAbbr',
                    'acqCode' => '',
                    'merCode' => '',
        
                    'transType' => '01',
                    'origQid' => '',
                    'commodityUrl' => '',
                    'commodityName' => '',
                    'commodityUnitPrice' => $total_fee * 100,
                    'commodityQuantity' => 1,
                    'commodityDiscount' => 0,
                    'transferFee' => 0,
                    'orderNumber' => $out_trade_no,
                    'orderAmount' => $total_fee * 100,
                    'orderCurrency' => '156',
                    'orderTime' => date('YmdHis'),
                    'customerIp' => '',
                    'customerName' => '',
                    'defaultPayType' => '',
                    'defaultBankNumber' => '',
                    'transTimeout' => '300000',
                    'merReserved' => '',
                    'frontEndUrl' => $returnUrl,
                    'backEndUrl' => $notifyUrl,
                ];
                $url=$this->apiConfig->Host."/api/Pay.action";
                $p=$this->sign($p);
                $str = "<form id='myform1' method='post' action='{$url}' target='_blank' >";
                foreach ($p as $key => $value) {
                    $str .= "<input type='hidden' name='{$key}' id='{$key}' value='{$value}'/>";
                }
                $str .= '</form>';
                return ['content' => $str, 'Type' => 'form'];
            }
        
            public function sign($p){
                $prestr="";
                ksort($p);
                reset($p);
        
                foreach ($p as $key=>$value){
                    $prestr.="{$key}={$value}&";
                }
                $p['signMethod']='MD5';
                $p['signature']=md5($prestr.md5($this->apiConfig->ExtraConfig['sucurity_key']));
                return $p;
            }