diff --git a/extend/payment/AlipayCert.php b/extend/payment/AlipayCert.php new file mode 100644 index 0000000000000000000000000000000000000000..b6d5a94b15585091ec4aea521370da45615a0c66 --- /dev/null +++ b/extend/payment/AlipayCert.php @@ -0,0 +1,709 @@ +config = $params; + } + + /** + * 配置信息 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2018-09-19 + * @desc description + */ + public function Config() + { + // 基础信息 + $base = [ + 'name' => '支付宝', // 插件名称 + 'version' => '1.0.0', // 插件版本 + 'apply_version' => '不限', // 适用系统版本描述 + 'apply_terminal'=> ['pc','h5', 'ios', 'android'], // 适用终端 默认全部 ['pc', 'h5', 'ios', 'android', 'alipay', 'weixin', 'baidu', 'toutiao'] + 'desc' => '2.0证书通信版本,适用PC+H5+APP,即时到帐支付方式,买家的交易资金直接打入卖家支付宝账户,快速回笼交易资金。 立即申请', // 插件描述(支持html) + 'author' => 'Devil', // 开发者 + 'author_url' => 'http://shopxo.net/', // 开发者主页 + ]; + + // 配置信息 + $element = [ + [ + 'element' => 'input', + 'type' => 'text', + 'default' => '', + 'name' => 'appid', + 'placeholder' => '应用ID', + 'title' => '应用ID', + 'is_required' => 0, + 'message' => '请填写应用ID', + ], + [ + 'element' => 'textarea', + 'name' => 'rsa_public', + 'placeholder' => '应用公钥', + 'title' => '应用公钥', + 'is_required' => 0, + 'rows' => 6, + 'message' => '请填写应用公钥', + ], + [ + 'element' => 'textarea', + 'name' => 'rsa_private', + 'placeholder' => '应用私钥', + 'title' => '应用私钥', + 'is_required' => 0, + 'rows' => 6, + 'message' => '请填写应用私钥', + ], + [ + 'element' => 'textarea', + 'name' => 'cert_content', + 'placeholder' => '应用证书', + 'title' => '应用证书', + 'is_required' => 0, + 'rows' => 6, + 'message' => '请填写应用证书', + ], + [ + 'element' => 'textarea', + 'name' => 'out_cert_content', + 'placeholder' => '支付宝证书', + 'title' => '支付宝证书', + 'is_required' => 0, + 'rows' => 6, + 'message' => '请填写支付宝证书', + ], + [ + 'element' => 'textarea', + 'name' => 'out_root_cert_content', + 'placeholder' => '支付宝ROOT证书', + 'title' => '支付宝ROOT证书', + 'is_required' => 0, + 'rows' => 6, + 'message' => '请填写支付宝ROOT证书', + ], + ]; + + return [ + 'base' => $base, + 'element' => $element, + ]; + } + + /** + * 支付入口 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2018-09-19 + * @desc description + * @param [array] $params [输入参数] + */ + public function Pay($params = []) + { + // 参数 + if(empty($params)) + { + return DataReturn('参数不能为空', -1); + } + + // 配置信息 + if(empty($this->config) || empty($this->config['appid']) || empty($this->config['rsa_public']) || empty($this->config['rsa_private']) || empty($this->config['cert_content']) || empty($this->config['out_cert_content']) || empty($this->config['out_root_cert_content'])) + { + return DataReturn('支付缺少配置', -1); + } + + // 支付方式 + switch(APPLICATION_CLIENT_TYPE) + { + // web + case 'pc' : + case 'h5' : + if(IsMobile()) + { + $ret = $this->PayMobile($params); + } else { + $ret = $this->PayWeb($params); + } + break; + + // app,头条小程序 + case 'ios' : + case 'android' : + case 'toutiao' : + $ret = $this->PayApp($params); + break; + + default : + $ret = DataReturn('没有相关支付模块['.APPLICATION_CLIENT_TYPE.']', -1); + } + + return $ret; + } + + /** + * app支付 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2019-10-29 + * @desc description + * @param [array] $params [输入参数] + */ + private function PayApp($params = []) + { + // 支付参数 + $parameter = array( + 'app_id' => $this->config['appid'], + 'method' => 'alipay.trade.app.pay', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'return_url' => $params['call_back_url'], + 'notify_url' => $params['notify_url'], + 'app_cert_sn' => $this->GetCertSNFromContent($this->config['cert_content']), + 'alipay_root_cert_sn' => $this->GetRootCertSNFromContent($this->config['out_root_cert_content']), + ); + $biz_content = array( + 'product_code' => 'QUICK_MSECURITY_PAY', + 'subject' => $params['name'], + 'out_trade_no' => $params['order_no'], + 'total_amount' => (string) $params['total_price'], + 'timeout_express' => $this->OrderAutoCloseTime(), + ); + $parameter['biz_content'] = json_encode($biz_content, JSON_UNESCAPED_UNICODE); + + // 生成签名参数+签名 + $parameter['sign'] = $this->MyRsaSign($this->GetSignContent($parameter)); + + // 生成支付参数 + $value = ''; + $i = 0; + foreach($parameter as $k=>$v) + { + if($v != '' && $v != null && "@" != substr($v, 0, 1)) + { + if($i == 0) + { + $value .= $k.'='.urlencode($v); + } else { + $value .= '&'.$k.'='.urlencode($v); + } + $i++; + } + } + unset($k, $v); + return DataReturn('处理成功', 0, $value); + } + + /** + * [PayMobile wap手机支付] + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @datetime 2018-09-28T00:41:09+0800 + * @param [array] $params [输入参数] + */ + private function PayMobile($params = []) + { + // 支付参数 + $parameter = array( + 'app_id' => $this->config['appid'], + 'method' => 'alipay.trade.wap.pay', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'return_url' => $params['call_back_url'], + 'notify_url' => $params['notify_url'], + 'app_cert_sn' => $this->GetCertSNFromContent($this->config['cert_content']), + 'alipay_root_cert_sn' => $this->GetRootCertSNFromContent($this->config['out_root_cert_content']), + ); + $biz_content = array( + 'product_code' => 'QUICK_WAP_WAY', + 'subject' => $params['name'], + 'out_trade_no' => $params['order_no'], + 'total_amount' => (string) $params['total_price'], + 'timeout_express' => $this->OrderAutoCloseTime(), + ); + $parameter['biz_content'] = json_encode($biz_content, JSON_UNESCAPED_UNICODE); + + // 生成签名参数+签名 + $parameter['sign'] = $this->MyRsaSign($this->GetSignContent($parameter)); + + // 接口则直接返回 + if(APPLICATION == 'app') + { + $result = [ + 'data' => $parameter, + 'url' => 'https://openapi.alipay.com/gateway.do?charset=utf-8', + ]; + return DataReturn('success', 0, $result); + } + + // web端输出执行form表单post提交 + exit($this->BuildRequestForm($parameter)); + } + + /** + * [PayWeb PC支付] + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @datetime 2018-09-28T00:23:04+0800 + * @param [array] $params [输入参数] + */ + private function PayWeb($params = []) + { + // 支付参数 + $parameter = array( + 'app_id' => $this->config['appid'], + 'method' => 'alipay.trade.page.pay', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'return_url' => $params['call_back_url'], + 'notify_url' => $params['notify_url'], + 'app_cert_sn' => $this->GetCertSNFromContent($this->config['cert_content']), + 'alipay_root_cert_sn' => $this->GetRootCertSNFromContent($this->config['out_root_cert_content']), + ); + $biz_content = array( + 'product_code' => 'FAST_INSTANT_TRADE_PAY', + 'subject' => $params['name'], + 'out_trade_no' => $params['order_no'], + 'total_amount' => (string) $params['total_price'], + 'timeout_express' => $this->OrderAutoCloseTime(), + ); + $parameter['biz_content'] = json_encode($biz_content, JSON_UNESCAPED_UNICODE); + + // 生成签名参数+签名 + $parameter['sign'] = $this->MyRsaSign($this->GetSignContent($parameter)); + + // 输出执行form表单post提交 + exit($this->BuildRequestForm($parameter)); + } + + /** + * 订单自动关闭的时间 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2021-03-24 + * @desc description + */ + public function OrderAutoCloseTime() + { + return intval(MyC('common_order_close_limit_time', 30, true)).'m'; + } + + /** + * 支付回调处理 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2018-09-19 + * @desc description + * @param [array] $params [输入参数] + */ + public function Respond($params = []) + { + // 请求参数 + $data = empty($_POST) ? $_GET : array_merge($_GET, $_POST); + + // 查询订单参数 + $parameter = array( + 'app_id' => $this->config['appid'], + 'method' => 'alipay.trade.query', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'app_cert_sn' => $this->GetCertSNFromContent($this->config['cert_content']), + 'alipay_root_cert_sn' => $this->GetRootCertSNFromContent($this->config['out_root_cert_content']), + ); + $biz_content = array( + 'out_trade_no' => $data['out_trade_no'], + 'trade_no' => $data['trade_no'], + ); + $parameter['biz_content'] = json_encode($biz_content, JSON_UNESCAPED_UNICODE); + + // 生成签名参数+签名 + $parameter['sign'] = $this->MyRsaSign($this->GetSignContent($parameter)); + + // 执行请求 + $result = $this->HttpRequest('https://openapi.alipay.com/gateway.do', $parameter); + $key = str_replace('.', '_', $parameter['method']).'_response'; + if(isset($result[$key]['code']) && $result[$key]['code'] == 10000) + { + if(in_array($result[$key]['trade_status'], ['TRADE_SUCCESS'])) + { + return DataReturn('支付成功', 0, $this->ReturnData($data)); + } else { + return DataReturn('支付失败、状态'.'['.$result[$key]['trade_status'].']', -1); + } + } + return DataReturn($result[$key]['sub_msg'].'['.$result[$key]['sub_code'].']', -1000); + } + + /** + * [ReturnData 返回数据统一格式] + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @datetime 2018-10-06T16:54:24+0800 + * @param [array] $data [返回数据] + */ + private function ReturnData($data) + { + // 返回数据固定基础参数 + $data['trade_no'] = $data['trade_no']; // 支付平台 - 订单号 + $data['buyer_user'] = $data['seller_id']; // 支付平台 - 用户 + $data['out_trade_no'] = $data['out_trade_no']; // 本系统发起支付的 - 订单号 + $data['subject'] = isset($data['subject']) ? $data['subject'] : ''; // 本系统发起支付的 - 商品名称 + $data['pay_price'] = $data['total_amount']; // 本系统发起支付的 - 总价 + + return $data; + } + + /** + * 退款处理 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2019-05-28 + * @desc description + * @param [array] $params [输入参数] + */ + public function Refund($params = []) + { + // 参数 + $p = [ + [ + 'checked_type' => 'empty', + 'key_name' => 'order_no', + 'error_msg' => '订单号不能为空', + ], + [ + 'checked_type' => 'empty', + 'key_name' => 'trade_no', + 'error_msg' => '交易平台订单号不能为空', + ], + [ + 'checked_type' => 'empty', + 'key_name' => 'refund_price', + 'error_msg' => '退款金额不能为空', + ], + ]; + $ret = ParamsChecked($params, $p); + if($ret !== true) + { + return DataReturn($ret, -1); + } + + // 退款原因 + $refund_reason = empty($params['refund_reason']) ? $params['order_no'].'订单退款'.$params['refund_price'].'元' : $params['refund_reason']; + + // 退款参数 + $parameter = array( + 'app_id' => $this->config['appid'], + 'method' => 'alipay.trade.refund', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0', + 'app_cert_sn' => $this->GetCertSNFromContent($this->config['cert_content']), + 'alipay_root_cert_sn' => $this->GetRootCertSNFromContent($this->config['out_root_cert_content']), + ); + $biz_content = array( + 'out_trade_no' => $params['order_no'], + 'trade_no' => $params['trade_no'], + 'refund_amount' => (string) $params['refund_price'], + 'refund_reason' => $refund_reason, + ); + $parameter['biz_content'] = json_encode($biz_content, JSON_UNESCAPED_UNICODE); + + // 生成签名参数+签名 + $parameter['sign'] = $this->MyRsaSign($this->GetSignContent($parameter)); + + // 执行请求 + $result = $this->HttpRequest('https://openapi.alipay.com/gateway.do', $parameter); + $key = str_replace('.', '_', $parameter['method']).'_response'; + if(isset($result[$key]['code']) && $result[$key]['code'] == 10000) + { + // 统一返回格式 + $data = [ + 'out_trade_no' => isset($result[$key]['out_trade_no']) ? $result[$key]['out_trade_no'] : '', + 'trade_no' => isset($result[$key]['trade_no']) ? $result[$key]['trade_no'] : '', + 'buyer_user' => isset($result[$key]['buyer_user_id']) ? $result[$key]['buyer_user_id'] : '', + 'refund_price' => isset($result[$key]['refund_fee']) ? $result[$key]['refund_fee'] : 0.00, + 'return_params' => $result[$key], + ]; + return DataReturn('退款成功', 0, $data); + } + + // 直接返回支付信息 + return DataReturn($result[$key]['sub_msg'].'['.$result[$key]['sub_code'].']', -1000); + } + + /** + * 建立请求,以表单HTML形式构造(默认) + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2019-03-15 + * @desc description + * @param [array] $params [请求参数数组] + * @return [string] [提交表单HTML文本] + */ + private function BuildRequestForm($params) + { + $html = "
"; + foreach($params as $key=>$val) + { + if(!empty($val)) + { + $val = str_replace("'", "'", $val); + $html .= ""; + } + } + + //submit按钮控件请不要含有name属性 + $html .= "
"; + + $html .= ""; + + return $html; + } + + /** + * [HttpRequest 网络请求] + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @datetime 2017-09-25T09:10:46+0800 + * @param [string] $url [请求url] + * @param [array] $data [发送数据] + * @return [mixed] [请求返回数据] + */ + private function HttpRequest($url, $data) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $body_string = ''; + if(is_array($data) && 0 < count($data)) + { + foreach($data as $k => $v) + { + $body_string .= $k.'='.urlencode($v).'&'; + } + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body_string); + } + $headers = array('content-type: application/x-www-form-urlencoded;charset=UTF-8'); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $reponse = curl_exec($ch); + if(curl_errno($ch)) + { + return false; + } else { + $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if(200 !== $httpStatusCode) + { + return false; + } + } + curl_close($ch); + return json_decode($reponse, true); + } + + /** + * 获取签名内容 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2019-03-15 + * @desc description + * @param [array] $params [需要签名的参数] + */ + public function GetSignContent($params) + { + ksort($params); + $string = ""; + $i = 0; + foreach($params as $k => $v) + { + if(!empty($v) && "@" != substr($v, 0, 1)) + { + if ($i == 0) { + $string .= "$k" . "=" . "$v"; + } else { + $string .= "&" . "$k" . "=" . "$v"; + } + $i++; + } + } + unset($k, $v); + return $string; + } + + /** + * [MyRsaSign 签名字符串] + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @datetime 2017-09-24T08:38:28+0800 + * @param [string] $prestr [需要签名的字符串] + * @return [string] [签名结果] + */ + private function MyRsaSign($prestr) + { + if(stripos($this->config['rsa_private'], '-----') === false) + { + $res = "-----BEGIN RSA PRIVATE KEY-----\n"; + $res .= wordwrap($this->config['rsa_private'], 64, "\n", true); + $res .= "\n-----END RSA PRIVATE KEY-----"; + } else { + $res = $this->config['rsa_private']; + } + return openssl_sign($prestr, $sign, $res, OPENSSL_ALGO_SHA256) ? base64_encode($sign) : null; + } + + /** + * 从证书内容中提取序列号 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2021-09-15 + * @desc description + * @param [string] $cert_content [证书内容发] + * @return [string] [序列号] + */ + public function GetCertSNFromContent($cert_content) + { + $ssl = openssl_x509_parse($cert_content); + return md5($this->ArrayToString(array_reverse($ssl['issuer'])).$ssl['serialNumber']); + } + + /** + * 数组转字符串 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2021-09-15 + * @desc description + * @param [array] $array [数组] + * @return [string] [字符串] + */ + public function ArrayToString($array) + { + $string = []; + if ($array && is_array($array)) + { + foreach($array as $key=>$value) + { + $string[] = $key.'='.$value; + } + } + return implode(',', $string); + } + + /** + * 提取根证书序列号 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2021-09-15 + * @desc description + * @param [string] $cert_content [证书] + * @return [string] [序列号] + */ + public function GetRootCertSNFromContent($cert_content) + { + $array = explode("-----END CERTIFICATE-----", $cert_content); + $sn = null; + for($i = 0; $i < count($array) - 1; $i++) + { + $ssl[$i] = openssl_x509_parse($array[$i] . "-----END CERTIFICATE-----"); + if(strpos($ssl[$i]['serialNumber'],'0x') === 0){ + $ssl[$i]['serialNumber'] = $this->hex2dec($ssl[$i]['serialNumberHex']); + } + if($ssl[$i]['signatureTypeLN'] == "sha1WithRSAEncryption" || $ssl[$i]['signatureTypeLN'] == "sha256WithRSAEncryption") + { + if($sn == null) + { + $sn = md5($this->ArrayToString(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']); + } else { + + $sn = $sn . "_" . md5($this->ArrayToString(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']); + } + } + } + return $sn; + } + + /** + * 0x转高精度数字 + * @author Devil + * @blog http://gong.gg/ + * @version 1.0.0 + * @date 2021-09-15 + * @desc description + * @param [int] $hex [数字] + * @return [int|string] [转换的数据] + */ + public function hex2dec($hex) + { + $dec = 0; + $len = strlen($hex); + for($i = 1; $i <= $len; $i++) + { + $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i)))); + } + return $dec; + } +} +?> \ No newline at end of file