├── .gitignore ├── composer.json ├── LICENSE ├── src ├── Pay.php └── Pay │ ├── Paypal.php │ ├── Baidu.php │ ├── Unionpay.php │ ├── Bytedance.php │ ├── Alipay.php │ └── Wechat.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fengkui/pay", 3 | "description": "以最简单的方式,整合微信支付、支付宝支付、银联支付、百度支付、字节跳动、Paypal支付。", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "fengkui", 8 | "email": "1161634940@qq.com" 9 | } 10 | ], 11 | "require": { 12 | "fengkui/supports": "*" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "fengkui\\": "src" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 fengkui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/Pay.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2020-10-13T17:50:31+08:00 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2020-10-22T18:35:29+08:00 7 | */ 8 | namespace fengkui; 9 | 10 | use Exception; 11 | 12 | /** 13 | * 小程序基类 14 | */ 15 | class Pay 16 | { 17 | /** 18 | * $config 相关配置 19 | */ 20 | protected static $config = []; 21 | 22 | /** 23 | * [__construct 构造函数] 24 | * @param [type] $config [传递小程序相关配置] 25 | */ 26 | public function __construct(array $config=[]){ 27 | $config && self::$config = $config; 28 | } 29 | 30 | /** 31 | * [__callStatic 模式方法(当我们调用一个不存在的静态方法时,会自动调用 __callStatic())] 32 | * @param [type] $method [方法名] 33 | * @param [type] $params [方法参数] 34 | * @return [type] [description] 35 | */ 36 | public static function __callStatic($method, $params) 37 | { 38 | $app = new self(...$params); 39 | return $app->create($method); 40 | } 41 | 42 | /** 43 | * [create 实例化命名空间] 44 | * @param [type] $method [description] 45 | * @return [type] [description] 46 | */ 47 | protected static function create($method) 48 | { 49 | $method = ucfirst(strtolower($method)); 50 | $className = __CLASS__ . '\\' . $method; 51 | if (!class_exists($className)) { // 当类不存在是自动加载 52 | spl_autoload_register(function($method){ 53 | $filename = dirname(__FILE__) . DIRECTORY_SEPARATOR . basename (__CLASS__) . '/' . $method . '.php'; 54 | if (is_readable($filename)) { 55 | require $filename; 56 | } 57 | }, true, true); 58 | $className = $method; 59 | } 60 | 61 | 62 | if (class_exists($className)) { 63 | return new $className(self::$config); 64 | } else { 65 | throw new Exception("ClassName [{$className}] Not Exists"); 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/Pay/Paypal.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2025-06-23 22:23:01 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2025-08-23 12:23:45 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use fengkui\Supports\Http; 11 | 12 | /** 13 | * PayPal 支付 - PC网页支付,原生PHP实现 14 | */ 15 | class Paypal 16 | { 17 | private static $sandboxUrl = 'https://api-m.sandbox.paypal.com'; 18 | private static $apiUrl = 'https://api-m.paypal.com'; 19 | private static $baseUrl; 20 | 21 | private static $config = [ 22 | 'client_id' => '', 23 | 'client_secret' => '', 24 | 'webhook_id' => '', 25 | 'is_sandbox' => true, 26 | 'notify_url' => '', 27 | 'return_url' => '', 28 | 'cancel_url' => '', 29 | 'encrypt_key' => '', // 用于加密解密的数据密钥(base64编码的对称密钥) 30 | ]; 31 | 32 | public function __construct($config = NULL) 33 | { 34 | $config && self::$config = array_merge(self::$config, $config); 35 | self::$baseUrl = !empty(self::$config['is_sandbox']) ? self::$sandboxUrl : self::$apiUrl; 36 | } 37 | 38 | // 获取access_token 39 | protected static function getAccessToken() 40 | { 41 | $auth = base64_encode(self::$config['client_id'] . ':' . self::$config['client_secret']); 42 | $headers = [ 43 | "Authorization: Basic $auth", 44 | "Content-Type: application/x-www-form-urlencoded" 45 | ]; 46 | $body = "grant_type=client_credentials"; 47 | $response = Http::post(self::$baseUrl . '/v1/oauth2/token', $body, $headers, false); 48 | $json = is_string($response) ? json_decode($response, true) : $response; 49 | return $json['access_token'] ?? null; 50 | } 51 | 52 | // 下单,返回PayPal跳转链接 53 | public static function unifiedOrder($order) 54 | { 55 | $accessToken = self::getAccessToken(); 56 | $url = self::$baseUrl . '/v2/checkout/orders'; 57 | $headers = [ 58 | "Authorization: Bearer {$accessToken}", 59 | "Content-Type: application/json" 60 | ]; 61 | $data = [ 62 | "intent" => "CAPTURE", 63 | "purchase_units" => [[ 64 | "amount" => [ 65 | "currency_code" => $order['currency'] ?? 'USD', 66 | "value" => $order['amount'], 67 | ], 68 | "description" => $order['description'] ?? '', 69 | ]], 70 | "application_context" => [ 71 | "return_url" => $order['return_url'] ?? self::$config['return_url'], 72 | "cancel_url" => $order['cancel_url'] ?? self::$config['cancel_url'], 73 | ] 74 | ]; 75 | $body = json_encode($data); 76 | $res = Http::post($url, $body, $headers, false); 77 | $resArr = is_string($res) ? json_decode($res, true) : $res; 78 | if (!empty($resArr['links'])) { 79 | foreach ($resArr['links'] as $link) { 80 | if ($link['rel'] === 'approve') { 81 | return $link['href']; 82 | } 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | // 支付捕获(回调后确认支付) 89 | public static function capture($orderId) 90 | { 91 | $accessToken = self::getAccessToken(); 92 | $url = self::$baseUrl . "/v2/checkout/orders/{$orderId}/capture"; 93 | $headers = [ 94 | "Authorization: Bearer {$accessToken}", 95 | "Content-Type: application/json" 96 | ]; 97 | $res = Http::post($url, '', $headers, false); 98 | return is_string($res) ? json_decode($res, true) : $res; 99 | } 100 | 101 | // 查询订单 102 | public static function query($orderId) 103 | { 104 | $accessToken = self::getAccessToken(); 105 | $url = self::$baseUrl . "/v2/checkout/orders/{$orderId}"; 106 | $headers = [ 107 | "Authorization: Bearer {$accessToken}", 108 | "Content-Type: application/json" 109 | ]; 110 | $res = Http::get($url, [], $headers); 111 | return is_string($res) ? json_decode($res, true) : $res; 112 | } 113 | 114 | // 退款 115 | public static function refund($captureId, $amount, $currency = 'USD') 116 | { 117 | $accessToken = self::getAccessToken(); 118 | $url = self::$baseUrl . "/v2/payments/captures/{$captureId}/refund"; 119 | $headers = [ 120 | "Authorization: Bearer {$accessToken}", 121 | "Content-Type: application/json" 122 | ]; 123 | $data = [ 124 | "amount" => [ 125 | "value" => $amount, 126 | "currency_code" => $currency 127 | ] 128 | ]; 129 | $body = json_encode($data); 130 | $res = Http::post($url, $body, $headers, false); 131 | return is_string($res) ? json_decode($res, true) : $res; 132 | } 133 | 134 | /** 135 | * PayPal 回调验签(风格参考 wechat.php) 136 | * @param string $body 回调原始内容 137 | * @param array $headers HTTP头(区分大小写,推荐 getallheaders() 直接传入) 138 | * @return bool 139 | */ 140 | public static function verifyNotify($body, $headers) 141 | { 142 | $webhook_id = self::$config['webhook_id'] ?? ''; 143 | if (!$webhook_id) { 144 | return false; 145 | } 146 | 147 | $accessToken = self::getAccessToken(); 148 | if (!$accessToken) { 149 | return false; 150 | } 151 | 152 | $url = self::$baseUrl . '/v1/notifications/verify-webhook-signature'; 153 | 154 | $verifyData = [ 155 | 'auth_algo' => $headers['PAYPAL-AUTH-ALGO'] ?? $headers['Paypal-Auth-Algo'] ?? '', 156 | 'cert_url' => $headers['PAYPAL-CERT-URL'] ?? $headers['Paypal-Cert-Url'] ?? '', 157 | 'transmission_id' => $headers['PAYPAL-TRANSMISSION-ID'] ?? $headers['Paypal-Transmission-Id'] ?? '', 158 | 'transmission_sig' => $headers['PAYPAL-TRANSMISSION-SIG'] ?? $headers['Paypal-Transmission-Sig'] ?? '', 159 | 'transmission_time' => $headers['PAYPAL-TRANSMISSION-TIME'] ?? $headers['Paypal-Transmission-Time'] ?? '', 160 | 'webhook_id' => $webhook_id, 161 | 'webhook_event' => is_string($body) ? json_decode($body, true) : $body 162 | ]; 163 | 164 | // webhook_event 解析失败, 直接用原始字符串 165 | if (empty($verifyData['webhook_event'])) { 166 | $verifyData['webhook_event'] = $body; 167 | } 168 | 169 | $verifyHeaders = [ 170 | "Content-Type: application/json", 171 | "Authorization: Bearer {$accessToken}" 172 | ]; 173 | 174 | $res = Http::post($url, json_encode($verifyData), $verifyHeaders, false); 175 | $resArr = is_string($res) ? json_decode($res, true) : $res; 176 | return isset($resArr['verification_status']) && $resArr['verification_status'] === 'SUCCESS'; 177 | } 178 | 179 | /** 180 | * 解密回调中的加密信息 181 | * @param string $encryptedData base64编码的密文 182 | * @param string $iv base64编码的初始向量(如有) 183 | * @param string $aad 附加认证数据(如有) 184 | * @param string $tag base64编码的认证标签(如有) 185 | * @return string|false 解密后的明文,失败返回 false 186 | */ 187 | public static function decryptResource($encryptedData, $iv = '', $aad = '', $tag = '') 188 | { 189 | // 兼容你的配置方式,建议密钥为32字节的base64字符串 190 | $key = self::$config['encrypt_key'] ?? ''; 191 | if (!$key) return false; 192 | 193 | $key = base64_decode($key); 194 | $cipher = 'aes-256-gcm'; 195 | 196 | // PayPal 回调一般使用 AES-256-GCM 加密 197 | $ciphertext = base64_decode($encryptedData); 198 | $iv = base64_decode($iv); 199 | $tag = base64_decode($tag); 200 | 201 | // openssl_decrypt 支持 AAD(附加认证数据),仅在传入时使用 202 | $plaintext = openssl_decrypt( 203 | $ciphertext, 204 | $cipher, 205 | $key, 206 | OPENSSL_RAW_DATA, 207 | $iv, 208 | $tag, 209 | $aad ? $aad : "" 210 | ); 211 | 212 | return $plaintext === false ? false : $plaintext; 213 | } 214 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Pay

2 | 3 | [![Latest Stable Version](http://poser.pugx.org/fengkui/pay/v)](https://packagist.org/packages/fengkui/pay) [![Total Downloads](http://poser.pugx.org/fengkui/pay/downloads)](https://packagist.org/packages/fengkui/pay) [![Latest Unstable Version](http://poser.pugx.org/fengkui/pay/v/unstable)](https://packagist.org/packages/fengkui/pay) [![License](http://poser.pugx.org/fengkui/pay/license)](https://packagist.org/packages/fengkui/pay) 4 | 5 | 开发了多次支付,每次都要翻文档、找之前的项目复制过来,费时费事,为了便于支付的开发, 6 | 干脆自己去造了一个简单轮子,整合支付(微信、支付宝、银联、百度、字节跳动、Paypal)相关开发。 7 | 8 | **!!请先熟悉 相关支付 说明文档!!请具有基本的 debug 能力!!** 9 | 10 | 欢迎 Star,欢迎 PR! 11 | 12 | ## 特点 13 | - 丰富的扩展,支持微信(商户直连和服务商)、支付宝、银联、百度、字节跳动、Paypal 14 | - 集成沙箱模式(支付宝、银联),便于开发者调试 15 | - 符合 PSR 标准,方便的与你的框架集成 16 | - 单文件结构清晰、简单,每个类单独封装扩展,便于单独使用 17 | 18 | ## 运行环境 19 | - PHP 7.0+ 20 | - composer 21 | 22 | ## 使用文档 23 | - [https://docs.fengkui.net/pay/](https://docs.fengkui.net/pay/) 24 | 25 | ## 支持的支付 26 | ### 1、微信(Wechat) 27 | 28 | | method | 描述 | 29 | | :----: | :----: | 30 | | js | JSAPI下单 | 31 | | app | APP支付 | 32 | | h5 | H5支付 | 33 | | scan | Navicat支付 | 34 | | xcx | 小程序支付 | 35 | | query | 查询订单 | 36 | | close | 关闭订单 | 37 | | notify | 支付结果通知 | 38 | | refund | 申请退款 | 39 | | refundQuery | 查询退款 | 40 | | transfer | 商家转账 | 41 | | transferQuery | 查询商家转账 | 42 | | transferCancel | 撤销商家转账 | 43 | | profitSharing | 请求分账 | 44 | | profitsharingUnfreeze | 解冻剩余资金 | 45 | | profitsharingQuery | 查询分账结果/查询分账剩余金额 | 46 | | profitsharingReturn | 请求分账回退 | 47 | | receiversAdd | 添加分账接收方 | 48 | | receiversDelete | 删除分账接收方 | 49 | 50 | ### 2、支付宝(Alipay) 51 | 52 | | method | 描述 | 53 | | :----: | :----: | 54 | | web | 电脑网页支付 | 55 | | wap | 手机网站支付 | 56 | | xcx | 小程序支付 | 57 | | face | 发起当面付 | 58 | | app | app支付(JSAPI) | 59 | | query | 查询订单 | 60 | | close | 关闭订单 | 61 | | notify | 支付宝异步通知 | 62 | | refund | 订单退款 | 63 | | refundQuery | 查询订单退款 | 64 | | transfer | 转账到支付宝账户 | 65 | | transQuery | 查询转账到支付宝 | 66 | | relationBind | 分账关系绑定与解绑 | 67 | | relationQuery | 查询分账关系 | 68 | | settle | 统一收单交易结算接口 | 69 | | settleQuery | 交易分账查询接口 | 70 | | onsettleQuery | 分账比例查询 && 分账剩余金额查询 | 71 | | getToken | 获取access_token和user_id | 72 | | doGetUserInfo | 获取会员信息 | 73 | 74 | ### 3、银联(Union) 75 | 76 | | method | 描述 | 77 | | :----: | :----: | 78 | | web | 电脑在线网关支付 | 79 | | wap | 手机网页支付 | 80 | | query | 查询订单 | 81 | | notify | 银联异步通知 | 82 | | refund | 订单退款/交易撤销 | 83 | 84 | ### 4、百度(Baidu) 85 | 86 | | method | 描述 | 87 | | :----: | :----: | 88 | | xcx | 小程序支付 | 89 | | refund | 申请退款 | 90 | | notify | 支付结果通知 | 91 | 92 | ### 5、字节跳动(Bytedance) 93 | 94 | | method | 描述 | 95 | | :----: | :----: | 96 | | createOrder | 下单支付 | 97 | | queryOrder | 订单查询 | 98 | | notifyOrder | 订单回调验证 | 99 | | createRefund | 订单退款 | 100 | | queryRefund | 退款查询 | 101 | | settle | 分账请求 | 102 | | querySettle | 分账查询 | 103 | 104 | ### 6、Paypal(Paypal) 105 | 106 | | method | 描述 | 107 | | :----: | :----: | 108 | | getAccessToken | 获取access_token | 109 | | unifiedOrder | PayPal跳转链接 | 110 | | capture | 支付捕获 | 111 | | query | 查询订单 | 112 | | refund | 退款 | 113 | 114 | 115 | ## 安装 116 | ```shell 117 | composer require fengkui/pay 118 | ``` 119 | 120 | ## 完善相关配置 121 | ```php 122 | # 微信支付配置 123 | $wechatConfig = [ 124 | 'xcxid' => '', // 小程序 appid 125 | 'appid' => '', // 微信支付 appid 126 | 'mchid' => '', // 微信支付 mch_id 商户收款账号 127 | 'key' => '', // 微信支付 apiV3key(尽量包含大小写字母,否则验签不通过,服务商模式使用服务商key) 128 | 'appsecret' => '', // 公众帐号 secert (公众号支付获取 code 和 openid 使用) 129 | 130 | 'sp_appid' => '', // 服务商应用 ID 131 | 'sp_mchid' => '', // 服务商户号 132 | 133 | 'notify_url' => '', // 接收支付状态的连接 改成自己的回调地址 134 | 'redirect_url' => '', // 公众号支付,调起支付页面 135 | 136 | // 服务商模式下,使用服务商证书 137 | 'serial_no' => '', // 商户API证书序列号(可不传,默认根据证书直接获取) 138 | 'cert_client' => './cert/apiclient_cert.pem', // 证书(退款,红包时使用) 139 | 'cert_key' => './cert/apiclient_key.pem', // 商户API证书私钥(Api安全中下载) 140 | 141 | 'public_key_id' => '', // 平台证书序列号或支付公钥ID 142 | // (支付公钥ID请带:PUB_KEY_ID_ 前缀,默认根据证书直接获取,不带前缀) 143 | 'public_key' => './cert/public_key.pem', // 平台证书或支付公钥(Api安全中下载) 144 | // (微信支付新申请的,已不支持平台证书,老版调用证书列表,自动生成平台证书,注意目录权限) 145 | ]; 146 | # 支付宝支付配置 147 | $alipayConfig = [ 148 | 'app_id' => '', // 开发者的应用ID 149 | 'public_key' => '', // 支付宝公钥,一行字符串 150 | 'private_key' => '', // 开发者私钥去头去尾去回车,一行字符串 151 | 152 | 'notify_url' => '', // 异步接收支付状态 153 | 'return_url' => '', // 同步接收支付状态 154 | 'sign_type' => 'RSA2', // 生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,默认使用RSA2 155 | 'is_sandbox' => false, // 是否使用沙箱调试,true使用沙箱,false不使用,默认false不使用 156 | ]; 157 | # 银联支付配置 158 | $unionConfig = [ 159 | 'mchid' => '', // 商户号 160 | 'sign_pwd' => '', //商户私钥证书密码 161 | 'sign_path' => './cert/acp_test_sign.pfx', //商户私钥证书(签名使用)5.1.0 162 | // 'sign_path' => './cert/700000000000001_acp.pfx', //签名证书路径5.0.0 163 | 'verify_path' => './cert/verify_sign_acp.cer', //银联公钥证书(商户验签使用) 164 | 'acp_root' => './cert/acp_test_root.cer', //根证书 165 | 'acp_middle' => './cert/acp_test_middle.cer', //中级证书 166 | 167 | 'notify_url' => '', // 异步接收支付状态 168 | 'return_url' => '', // 同步接收支付状态 169 | 'is_sandbox' => false, // 是否使用沙箱调试,true使用沙箱,false不使用,默认false不使用 170 | ]; 171 | # 百度支付配置 172 | $baiduConfig = [ 173 | 'deal_id' => '', // 百度收银台的财务结算凭证 174 | 'app_key' => '', // 表示应用身份的唯一ID 175 | 'private_key' => '', // 私钥原始字符串 176 | 'public_key' => '', // 平台公钥 177 | 'notify_url' => '', // 支付回调地址 178 | ]; 179 | # 字节跳动支付配置 180 | $bytedanceConfig = [ 181 | 'app_id' => '', // App ID 182 | 'salt' => '', // 支付密钥值 183 | 'token' => '', // 回调验签的Token 184 | 'notify_url' => '', // 支付回调地址 185 | 'thirdparty_id' => '', // 第三方平台服务商 id,非服务商模式留空 186 | ]; 187 | # Paypal支付配置 188 | $paypalConfig = [ 189 | 'client_id' => '', 190 | 'client_secret' => '', 191 | 'webhook_id' => '', 192 | 'is_sandbox' => true, 193 | 'notify_url' => '', 194 | 'return_url' => '', 195 | 'cancel_url' => '', 196 | 'encrypt_key' => '', // 用于加密解密的数据密钥(base64编码的对称密钥) 197 | ]; 198 | ``` 199 | 200 | ## 使用说明 201 | 202 | ### 单独使用 203 | ```php 204 | $pay = new \fengkui\Pay\Wechat($wechatConfig); // 微信 205 | $pay = new \fengkui\Pay\Alipay($alipayConfig); // 支付宝 206 | $pay = new \fengkui\Pay\Unionpay($unionConfig); // 银联 207 | $pay = new \fengkui\Pay\Baidu($baiduConfig); // 百度 208 | $pay = new \fengkui\Pay\Bytedance($bytedanceConfig); // 字节跳动 209 | $pay = new \fengkui\Pay\Paypal($paypalConfig); // 字节跳动 210 | ``` 211 | 212 | ### 公共使用 213 | ```php 214 | 217 | * @Date: 2021-06-01T14:55:21+08:00 218 | * @Last Modified by: [FENG] <1161634940@qq.com> 219 | * @Last Modified time: 2021-06-15 15:39:01 220 | */ 221 | require_once('./vendor/autoload.php'); 222 | 223 | // 通用支付 224 | class Payment 225 | { 226 | // 支付类实例化 227 | protected static $pay = ''; 228 | // 支付类型 229 | protected static $type = ''; 230 | // 支付相关配置 231 | protected static $config = []; 232 | 233 | /** 234 | * [_initialize 构造函数(获取支付类型与初始化配置)] 235 | * @return [type] [description] 236 | */ 237 | public function _initialize() 238 | { 239 | self::$type = $_GET['type'] ?? 'alipay'; 240 | self::config(); 241 | } 242 | 243 | /** 244 | * [config 获取配置] 245 | * @param string $type [description] 246 | * @return [type] [description] 247 | */ 248 | protected static function config($type='') 249 | { 250 | $type = $type ?: self::$type; 251 | 252 | // 相关配置 253 | $alipayConfig = []; 254 | 255 | if (in_array($type, ['wechat', 'baidu', 'bytedance', 'alipay', 'union'])) { 256 | $config = $type . "Config"; 257 | self::$config = $config; 258 | } else { 259 | die('当前类型配置不存在'); 260 | } 261 | 262 | $type && self::$pay =(new \fengkui\Pay())::$type(self::$config); 263 | } 264 | 265 | // 支付方法 266 | public function pay() 267 | { 268 | $order = [ 269 | 'body' => 'subject-测试', // 商品描述 270 | 'order_sn' => time(), // 商户订单号 271 | 'total_amount' => 0.01, // 订单金额 272 | ]; 273 | $result = self::$pay->web($order); // 直接跳转链接 274 | echo $result; 275 | } 276 | 277 | } 278 | ``` 279 | 280 | ## 一起喝可乐 281 |
282 | 283 |
284 | 285 | **请备注一起喝可乐,以便感谢支持** 286 | 287 | ## LICENSE 288 | MIT 289 | -------------------------------------------------------------------------------- /src/Pay/Baidu.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2020-09-27T16:28:31+08:00 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2021-06-15T16:53:07+08:00 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use fengkui\Supports\Http; 11 | 12 | /** 13 | * Baidu 百度支付 14 | */ 15 | class Baidu 16 | { 17 | // 统一订单管理URL 18 | private static $paymentUrl = 'https://openapi.baidu.com/rest/2.0/smartapp/pay/paymentservice/'; 19 | 20 | // 支付相关配置 21 | private static $config = array( 22 | 'deal_id' => '', // 百度收银台的财务结算凭证 23 | 'app_key' => '', // 表示应用身份的唯一ID 24 | 'private_key' => '', // 私钥原始字符串 25 | 'public_key' => '', // 平台公钥 26 | 'notify_url' => '', // 支付回调地址 27 | ); 28 | 29 | /** 30 | * [__construct 构造函数] 31 | * @param [type] $config [传递支付相关配置] 32 | */ 33 | public function __construct($config=NULL){ 34 | $config && self::$config = array_merge(self::$config, $config); 35 | } 36 | 37 | /** 38 | * [xcxPay 百度小程序支付] 39 | * @param [type] $order [订单信息数组] 40 | * @return [type] [description] 41 | * $order = array( 42 | * 'body' => '', // 产品描述 43 | * 'total_amount' => '', // 订单金额(分) 44 | * 'order_sn' => '', // 订单编号 45 | * ); 46 | */ 47 | public static function xcx($order) 48 | { 49 | if(!is_array($order) || count($order) < 3) 50 | die("数组数据信息缺失!"); 51 | 52 | $config = self::$config; 53 | $requestParamsArr = array( 54 | 'appKey' => $config['app_key'], 55 | 'dealId' => $config['deal_id'], 56 | 'tpOrderId' => $order['order_sn'], 57 | 'totalAmount' => $order['total_amount'], 58 | ); 59 | $rsaSign = self::makeSign($requestParamsArr, $config['private_key']); // 声称百度支付签名 60 | $bizInfo = array( 61 | 'tpData' => array( 62 | "appKey" => $config['app_key'], 63 | "dealId" => $config['deal_id'], 64 | "tpOrderId" => $order['order_sn'], 65 | "rsaSign" => $rsaSign, 66 | "totalAmount" => $order['total_amount'], 67 | "returnData" => '', 68 | "displayData" => array( 69 | "cashierTopBlock" => array( 70 | array( 71 | [ "leftCol" => "订单名称", "rightCol" => $order['body'] ], 72 | [ "leftCol" => "数量", "rightCol" => "1" ], 73 | [ "leftCol" => "订单金额", "rightCol" => $order['total_amount'] ] 74 | ) 75 | ) 76 | ), 77 | "dealTitle" => $order['body'], 78 | "dealSubTitle" => $order['body'], 79 | "dealThumbView" => "https://b.bdstatic.com/searchbox/icms/searchbox/img/swan-logo.png", 80 | ), 81 | "orderDetailData" => '' 82 | ); 83 | 84 | $bdOrder = array( 85 | 'dealId' => $config['deal_id'], 86 | 'appKey' => $config['app_key'], 87 | 'totalAmount' => $order['total_amount'], 88 | 'tpOrderId' => $order['order_sn'], 89 | 'dealTitle' => $order['body'], 90 | 'signFieldsRange' => 1, 91 | 'rsaSign' => $rsaSign, 92 | 'bizInfo' => json_encode($bizInfo, JSON_UNESCAPED_UNICODE), 93 | ); 94 | return $bdOrder; 95 | } 96 | 97 | /** 98 | * [find 查询订单] 99 | * @param [type] $orderSn [开发者订单] 100 | * @param [type] $accessToken [access_token] 101 | * @return [type] [description] 102 | */ 103 | public static function find($orderSn, $accessToken) 104 | { 105 | $config = self::$config; 106 | $url = self::$paymentUrl . 'findByTpOrderId'; 107 | $params = [ 108 | 'access_token' => $accessToken, // 获取开发者服务权限说明 109 | 'tpOrderId' => $orderSn, // 开发者订单 110 | 'pmAppKey' => $config['app_key'], // 调起百度收银台的支付服务 111 | ]; 112 | $response = Http::get($url, $params); 113 | $result = json_decode($response, true); 114 | 115 | return $result; 116 | } 117 | 118 | /** 119 | * [cancel 关闭订单] 120 | * @param [type] $orderSn [开发者订单] 121 | * @param [type] $accessToken [access_token] 122 | * @return [type] [description] 123 | */ 124 | public static function cancel($orderSn, $accessToken) 125 | { 126 | $config = self::$config; 127 | $url = self::$paymentUrl . 'cancelOrder'; 128 | $params = [ 129 | 'access_token' => $accessToken, // 获取开发者服务权限说明 130 | 'tpOrderId' => $orderSn, // 开发者订单 131 | 'pmAppKey' => $config['app_key'], // 调起百度收银台的支付服务 132 | ]; 133 | $response = Http::get($url, $params); 134 | $result = json_decode($response, true); 135 | 136 | return $result; 137 | } 138 | 139 | /** 140 | * [refund baidu支付退款] 141 | * @param [type] $order [订单信息] 142 | * @param [type] $type [退款类型] 143 | * $order = array( 144 | * 'order_sn' => '', // 订单编号 145 | * 'refund_sn' => '', // 退款编号 146 | * 'refund_amount' => '', // 退款金额(分) 147 | * 'body' => '', // 退款原因 148 | * 'access_token' => '', // 获取开发者服务权限说明 149 | * 'order_id' => '', // 百度收银台订单 ID 150 | * 'user_id' => '', // 百度收银台用户 id 151 | * ); 152 | */ 153 | public static function refund($order=[], $type=1) 154 | { 155 | $config = self::$config; 156 | 157 | $params = array( 158 | 'access_token' => $order['access_token'], // 获取开发者服务权限说明 159 | // 'applyRefundMoney' => $order['refund_amount'], // 退款金额,单位:分。 160 | 'bizRefundBatchId' => $order['refund_sn'], // 开发者退款批次 161 | 'isSkipAudit' => 1, // 是否跳过审核,不需要百度请求开发者退款审核请传 1,默认为0; 0:不跳过开发者业务方审核;1:跳过开发者业务方审核。 162 | 'orderId' => $order['order_id'], // 百度收银台订单 ID 163 | 'refundReason' => $order['reason'], // 退款原因 164 | 'refundType' => $type, // 退款类型 1:用户发起退款;2:开发者业务方客服退款;3:开发者服务异常退款。 165 | 'tpOrderId' => $order['order_sn'], // 开发者订单 ID 166 | 'userId' => $order['user_id'], // 百度收银台用户 id 167 | 'pmAppKey' => $config['app_key'], // 调起百度收银台的支付服务 168 | ); 169 | !empty($order['refund_amount']) && $params['applyRefundMoney'] = $order['refund_amount']; 170 | 171 | $url = self::$paymentUrl . 'applyOrderRefund'; 172 | $response = Http::post($url, $params); 173 | $result = json_decode($response, true); 174 | // // 显示错误信息 175 | // if ($result['msg']!='success') { 176 | // return false; 177 | // // die($result['msg']); 178 | // } 179 | return $result; 180 | } 181 | 182 | /** 183 | * [findRefund 查询退款订单] 184 | * @param [type] $orderSn [开发者订单] 185 | * @param [type] $accessToken [access_token] 186 | * @return [type] [description] 187 | */ 188 | public static function findRefund($orderSn, $userId, $accessToken) 189 | { 190 | $config = self::$config; 191 | $url = self::$paymentUrl . 'findOrderRefund'; 192 | $params = [ 193 | 'access_token' => $accessToken, // 获取开发者服务权限说明 194 | 'tpOrderId' => $orderSn, // 开发者订单 195 | 'userId' => $userId, // 百度收银台用户 ID 196 | 'pmAppKey' => $config['app_key'], // 调起百度收银台的支付服务 197 | ]; 198 | $response = Http::get($url, $params); 199 | $result = json_decode($response, true); 200 | 201 | return $result; 202 | } 203 | 204 | /** 205 | * [notify 回调验证] 206 | * @return [array] [返回数组格式的notify数据] 207 | */ 208 | public static function notify() 209 | { 210 | $data = $_POST; // 获取xml 211 | $config = self::$config; 212 | if (!$data || empty($data['rsaSign'])) 213 | die('暂无回调信息'); 214 | 215 | $result = self::verifySign($data, $config['public_key']); // 进行签名验证 216 | // 判断签名是否正确 判断支付状态 217 | if ($result && $data['status']==2) { 218 | return $data; 219 | } else { 220 | return false; 221 | } 222 | } 223 | 224 | /** 225 | * [success 通知支付状态] 226 | */ 227 | public static function success() 228 | { 229 | $array = ['errno'=>0, 'msg'=>'success', 'data'=> ['isConsumed'=>2] ]; 230 | die(json_encode($array)); 231 | } 232 | 233 | /** 234 | * [error 通知支付状态] 235 | */ 236 | public static function error() 237 | { 238 | $array = ['errno'=>0, 'msg'=>'success', 'data'=> ['isErrorOrder'=>1, 'isConsumed'=>2] ]; 239 | die(json_encode($array)); 240 | } 241 | 242 | /** 243 | * [makeSign 使用私钥生成签名字符串] 244 | * @param array $assocArr [入参数组] 245 | * @param [type] $rsaPriKeyStr [私钥原始字符串,不含PEM格式前后缀] 246 | * @return [type] [签名结果字符串] 247 | */ 248 | public static function makeSign(array $assocArr, $rsaPriKeyStr) 249 | { 250 | $sign = ''; 251 | if (empty($rsaPriKeyStr) || empty($assocArr)) { 252 | return $sign; 253 | } 254 | if (!function_exists('openssl_pkey_get_private') || !function_exists('openssl_sign')) { 255 | throw new Exception("openssl扩展不存在"); 256 | } 257 | $rsaPriKeyPem = self::convertRSAKeyStr2Pem($rsaPriKeyStr, 1); 258 | $priKey = openssl_pkey_get_private($rsaPriKeyPem); 259 | if (isset($assocArr['sign'])) { 260 | unset($assocArr['sign']); 261 | } 262 | ksort($assocArr); // 参数按字典顺序排序 263 | $parts = array(); 264 | foreach ($assocArr as $k => $v) { 265 | $parts[] = $k . '=' . $v; 266 | } 267 | $str = implode('&', $parts); 268 | openssl_sign($str, $sign, $priKey); 269 | openssl_free_key($priKey); 270 | 271 | return base64_encode($sign); 272 | } 273 | 274 | /** 275 | * [verifySign 使用公钥校验签名] 276 | * @param array $assocArr [入参数据,签名属性名固定为rsaSign] 277 | * @param [type] $rsaPubKeyStr [公钥原始字符串,不含PEM格式前后缀] 278 | * @return [type] [验签通过|false 验签不通过] 279 | */ 280 | public static function verifySign(array $assocArr, $rsaPubKeyStr) 281 | { 282 | if (!isset($assocArr['rsaSign']) || empty($assocArr) || empty($rsaPubKeyStr)) { 283 | return false; 284 | } 285 | if (!function_exists('openssl_pkey_get_public') || !function_exists('openssl_verify')) { 286 | throw new Exception("openssl扩展不存在"); 287 | } 288 | 289 | $sign = $assocArr['rsaSign']; 290 | unset($assocArr['rsaSign']); 291 | if (empty($assocArr)) { 292 | return false; 293 | } 294 | ksort($assocArr); // 参数按字典顺序排序 295 | $parts = array(); 296 | foreach ($assocArr as $k => $v) { 297 | $parts[] = $k . '=' . $v; 298 | } 299 | $str = implode('&', $parts); 300 | $sign = base64_decode($sign); 301 | $rsaPubKeyPem = self::convertRSAKeyStr2Pem($rsaPubKeyStr); 302 | $pubKey = openssl_pkey_get_public($rsaPubKeyPem); 303 | $result = (bool)openssl_verify($str, $sign, $pubKey); 304 | openssl_free_key($pubKey); 305 | 306 | return $result; 307 | } 308 | 309 | /** 310 | * [convertRSAKeyStr2Pem 将密钥由字符串(不换行)转为PEM格式] 311 | * @param [type] $rsaKeyStr [原始密钥字符串] 312 | * @param integer $keyType [0 公钥|1 私钥,默认0] 313 | * @return [type] [PEM格式密钥] 314 | */ 315 | public static function convertRSAKeyStr2Pem($rsaKeyStr, $keyType = 0) 316 | { 317 | $pemWidth = 64; 318 | $rsaKeyPem = ''; 319 | 320 | $begin = '-----BEGIN '; 321 | $end = '-----END '; 322 | $key = ' KEY-----'; 323 | $type = $keyType ? 'RSA PRIVATE' : 'PUBLIC'; 324 | 325 | $keyPrefix = $begin . $type . $key; 326 | $keySuffix = $end . $type . $key; 327 | 328 | $rsaKeyPem .= $keyPrefix . "\n"; 329 | $rsaKeyPem .= wordwrap($rsaKeyStr, $pemWidth, "\n", true) . "\n"; 330 | $rsaKeyPem .= $keySuffix; 331 | 332 | if (!function_exists('openssl_pkey_get_public') || !function_exists('openssl_pkey_get_private')) { 333 | return false; 334 | } 335 | if ($keyType == 0 && false == openssl_pkey_get_public($rsaKeyPem)) { 336 | return false; 337 | } 338 | if ($keyType == 1 && false == openssl_pkey_get_private($rsaKeyPem)) { 339 | return false; 340 | } 341 | 342 | return $rsaKeyPem; 343 | } 344 | 345 | } 346 | -------------------------------------------------------------------------------- /src/Pay/Unionpay.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2024-05-12 17:20:18 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2024-06-14 14:11:00 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use Exception; 11 | use RuntimeException; 12 | use fengkui\Supports\Http; 13 | 14 | /** 15 | * 银联支付(更新中) 16 | */ 17 | class Unionpay 18 | { 19 | 20 | //沙盒地址 21 | private static $sandurl = 'https://gateway.test.95516.com/gateway/api'; 22 | //正式地址 23 | private static $apiurl = 'https://gateway.95516.com/gateway/api'; 24 | //网关地址 25 | private static $gateway; 26 | 27 | private static $config = array( 28 | 'mchid' => '', // 商户号 29 | 'sign_pwd' => '', //商户私钥证书密码 30 | 'sign_path' => './cert/acp_test_sign.pfx', //商户私钥证书(签名使用)5.1.0 31 | // 'sign_path' => './cert/700000000000001_acp.pfx', //签名证书路径5.0.0 32 | 'verify_path' => './cert/verify_sign_acp.cer', //银联公钥证书(商户验签使用) 33 | 'acp_root' => './cert/acp_test_root.cer', //根证书 34 | 'acp_middle' => './cert/acp_test_middle.cer', //中级证书 35 | 36 | 'notify_url' => '', // 异步接收支付状态 37 | 'return_url' => '', // 同步接收支付状态 38 | 39 | 'is_sandbox' => false, // 是否使用沙箱调试,true使用沙箱,false不使用,默认false不使用 40 | ); 41 | 42 | /** 43 | * [__construct 构造函数] 44 | * @param [type] $config [传递相关配置] 45 | */ 46 | public function __construct($config=NULL){ 47 | $config && self::$config = array_merge(self::$config, $config); 48 | self::$gateway = !empty(self::$config['is_sandbox']) ? self::$sandurl : self::$apiurl; //请求地址,判断是否使用沙箱,默认不使用 49 | } 50 | 51 | 52 | public static function unifiedOrder($order, $type=false) 53 | { 54 | // 获取配置项 55 | $config = self::$config; 56 | 57 | if (isset($order['total_amount'])) { // 请求参数(修改原始键名) 58 | $order['orderDesc'] = $order['body']; unset($order['body']); // 描述 59 | $order['orderId'] = (string)$order['order_sn']; unset($order['order_sn']); //商户订单号 60 | $order['txnAmt'] = $order['total_amount']; unset($order['total_amount']); //交易金额,单位分 61 | } 62 | 63 | // 订单失效时间 64 | if (!empty($params['time_expire'])) { 65 | preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']); 66 | $time = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time(); 67 | $params['payTimeout'] = date('YmdHis', $time); 68 | unset($order['time_expire']); 69 | } 70 | 71 | //请求参数 72 | $params = array( 73 | 'signMethod' => '01', // 签名方式 74 | 'version' => '5.1.0', // 版本号 75 | 'encoding' => 'UTF-8', // 编码方式 76 | 'merId' => $config['mchid'], // 商户代码 77 | 'accessType' => '0', // 接入类型 78 | 'currencyCode' => $order['currency'] ?? '156', // 交易币种 79 | 'backUrl' => self::$config['notify_url'], // 后台通知地址 80 | 81 | 'certId' => self::getCertId(self::$config['sign_path'], self::$config['sign_pwd']), //证书ID 82 | 'txnTime' => date('YmdHis'), // 订单发送时间 83 | ); 84 | 85 | $params = $type ? $order : array_merge($params, $order); 86 | 87 | if ($params['accessType'] == 1 && (empty($params['acqInsCode']))) { 88 | throw new \Exception("[ acqInsCode ] 接入类型为收单机构接入时,收单机构代码 需上送"); 89 | } 90 | if ($params['accessType'] == 2 && (empty($params['subMerId']) || empty($params['subMerName']) || empty($params['subMerAbbr']))) { 91 | throw new \Exception("[ subMerId|subMerName|subMerAbbr ] 接入类型为收单机构接入时,二级商户代码、名称、简称 需上送"); 92 | } 93 | 94 | // dump($params);die; 95 | $params["signature"] = self::makeSign($params); 96 | return $params; 97 | } 98 | 99 | // 在线网关支付 100 | public static function web($order){ 101 | $order['bizType'] = '000201'; // 产品类型 102 | $order['txnType'] = '01'; // 交易类型 ,01:消费 103 | $order['txnSubType'] = '01'; // 交易子类型, 01:自助消费 104 | $order['channelType'] = '07'; // 渠道类型 07:PC,平板 08:手机 105 | $order['frontUrl'] = self::$config['return_url']; // 前台通知地址 106 | 107 | $params = self::unifiedOrder($order); 108 | $result = self::buildRequestForm(self::$gateway . '/frontTransReq.do', $params); 109 | return $result; 110 | } 111 | 112 | // wap支付 113 | public static function wap($order){ 114 | $order['bizType'] = '000201'; // 产品类型 115 | $order['txnType'] = '01'; // 交易类型 ,01:消费 116 | $order['txnSubType'] = '01'; // 交易子类型, 01:自助消费 117 | $order['channelType'] = '08'; // 渠道类型 07:PC,平板 08:手机 118 | $order['frontUrl'] = self::$config['return_url']; // 前台通知地址 119 | 120 | $params = self::unifiedOrder($order); 121 | $result = self::buildRequestForm(self::$gateway . '/frontTransReq.do', $params); 122 | return $result; 123 | } 124 | 125 | /** 126 | * [scan 二维码支付] 127 | * @param [array] $order [支付订单信息] 128 | * @param boolean $type [是否为预支付 true 是,false 否(默认)] 129 | * @return [type] [description] 130 | */ 131 | public static function scan($order, $type=false){ 132 | $order['bizType'] = '000000'; // 产品类型 133 | $order['txnType'] = $type ? '02' : '01'; // 交易类型 ,01:消费 02 预支付 134 | $order['txnSubType'] = $type ? '05' : '07'; // 交易子类型, 07: 申请消费二维码 05:申请预授权二维码 135 | $order['channelType'] = '08'; // 渠道类型 07:PC,平板 08:手机 136 | 137 | $params = self::unifiedOrder($order); 138 | $response = Http::post(self::$gateway . '/queryTrans.do', $params); 139 | parse_str($response, $result); 140 | unset($result['signPubKeyCert']); 141 | unset($result['signature']); 142 | return $result; 143 | } 144 | 145 | /** 146 | * [query 查询订单] 147 | * @param [type] $orderId [订单编号] 148 | * @return [type] [description] 149 | */ 150 | public static function query($order) { 151 | if(empty($order['order_sn']) || empty($order['txn_time'])){ 152 | die("订单数组信息缺失!"); 153 | } 154 | $order = [ 155 | 'orderId' => $order['order_sn'], 156 | 'txnTime' => $order['txn_time'], // 订单支付时间 157 | ]; 158 | 159 | $order['bizType'] = '000000'; // 产品类型 160 | $order['channelType'] = '08'; // 交易子类 161 | $order['txnType'] = '00'; // 交易类型 162 | $order['txnSubType'] = '00'; // 交易子类 163 | 164 | $params = self::unifiedOrder($order); 165 | $response = Http::post(self::$gateway . '/queryTrans.do', $params); 166 | parse_str($response, $result); 167 | unset($result['signPubKeyCert']); 168 | unset($result['signature']); 169 | return $result; 170 | } 171 | 172 | /** 173 | * [refund 订单退款/交易撤销] 174 | * @param [type] $order [退款信息] 175 | * @param boolean $type [是否为交易撤销 true 交易撤销,false 退款(默认)] 176 | * @return [type] [description] 177 | */ 178 | public static function refund($order, $type=false) { 179 | $config = self::$config; 180 | if(empty($order['refund_sn']) || empty($order['query_id'])){ 181 | die("订单数组信息缺失!"); 182 | } 183 | 184 | $order = [ 185 | 'orderId' => $order['refund_sn'], // 退款单号 186 | 'txnAmt' => $order['refund_amount'], // 退款金额 187 | 'origQryId' => $order['query_id'], // 原交易查询流水号 188 | ]; 189 | 190 | $order['bizType'] = '000000'; // 产品类型 191 | $order['channelType'] = '07'; // 交易子类 192 | $order['txnType'] = $type ? '31' : '04'; // 交易类型 193 | $order['txnSubType'] = '00'; // 交易子类 194 | 195 | $params = self::unifiedOrder($order); 196 | $response = Http::post(self::$gateway . '/backTransReq.do', $params); 197 | parse_str($response, $result); 198 | unset($result['signPubKeyCert']); 199 | unset($result['signature']); 200 | return $result; 201 | } 202 | 203 | // 银联异步通知 204 | public static function notify($response = null){ 205 | $config = self::$config; 206 | $response = $response ?: $_POST; 207 | $result = is_array($response) ? $response : json_decode($response, true); 208 | $signature = $result['signature'] ?? ''; 209 | 210 | // 不参与签名 211 | unset($result['signature']); 212 | $rst = self::verifySign($result, $signature); 213 | if(!$rst) 214 | return false; 215 | return $result; 216 | } 217 | 218 | /** 219 | * [makeSign 生成签名] 220 | * @param [type] $data [加密数据] 221 | * @return [type] [description] 222 | */ 223 | public static function makeSign($params) 224 | { 225 | $config = self::$config; 226 | // 拼接生成签名所需的字符串 227 | ksort($params); 228 | $params_str = urldecode(http_build_query($params)); 229 | $result = false; 230 | 231 | if($params['signMethod'] == '01') { 232 | $private_key = self::getSignPrivateKey(); 233 | // 转换成key=val&串 234 | if($params['version'] == '5.0.0'){ 235 | $params_sha1x16 = sha1($params_str, FALSE ); 236 | // 签名 237 | $result = openssl_sign($params_sha1x16, $signature, $private_key, OPENSSL_ALGO_SHA1); 238 | } else if($params['version'] == '5.1.0'){ 239 | //sha256签名摘要 240 | $params_sha256x16 = hash('sha256',$params_str); 241 | // 签名 242 | $result = openssl_sign($params_sha256x16, $signature, $private_key, 'sha256'); 243 | } 244 | } else if($params['signMethod']=='11') { 245 | if (!checkEmpty($config['secure_key'])) { 246 | $params_before_sha256 = hash('sha256', $config['secure_key']); 247 | $params_before_sha256 = $params_str.'&'.$params_before_sha256; 248 | $params_after_sha256 = hash('sha256', $params_before_sha256); 249 | $signature = base64_decode($params_after_sha256); 250 | $result = true; 251 | } 252 | } else if($params['signMethod']=='12') { 253 | throw new \Exception("[ 404 ] signMethod=12未实现"); 254 | } else { 255 | throw new \Exception("[ 404 ] signMethod不正确"); 256 | } 257 | 258 | if (!$result) 259 | throw new \Exception("[ 404 ] >>>>>签名失败<<<<<<<"); 260 | 261 | $signature = base64_encode($signature); 262 | return $signature; 263 | } 264 | 265 | // 验签函数 266 | protected static function verifySign($params, $signature) 267 | { 268 | $config = self::$config; 269 | $signature = base64_decode($signature); 270 | // 拼接生成签名所需的字符串 271 | ksort($params); 272 | $params_str = urldecode(http_build_query($params)); 273 | $isSuccess = false; 274 | 275 | if($params['signMethod']=='01') 276 | { 277 | $public_key = self::getVerifyPublicKey($params); // 公钥 278 | if($params['version']=='5.0.0'){ 279 | $params_sha1x16 = sha1($params_str, FALSE); 280 | $isSuccess = openssl_verify($params_sha1x16, $signature, $public_key, OPENSSL_ALGO_SHA1); 281 | } else if($params['version']=='5.1.0'){ 282 | $params_sha256x16 = hash('sha256', $params_str); 283 | $isSuccess = openssl_verify($params_sha256x16, $signature, $public_key, "sha256" ); 284 | } 285 | } else if($params['signMethod']=='11') { 286 | if (!checkEmpty($config['secure_key'])) { 287 | $params_before_sha256 = hash('sha256', $config['secure_key']); 288 | $params_before_sha256 = $params_str.'&'.$params_before_sha256; 289 | $params_after_sha256 = hash('sha256',$params_before_sha256); 290 | $isSuccess = $params_after_sha256 == $signature_str; 291 | } 292 | } else if($params['signMethod']=='12') { 293 | throw new \Exception("[ 404 ] sm3没实现"); 294 | } else { 295 | throw new \Exception("[ 404 ] signMethod不正确"); 296 | } 297 | return $isSuccess == 1 ? true : false;; 298 | } 299 | 300 | // 获取证书ID(SN) 301 | private static function getCertId($path, $pwd=false) 302 | { 303 | $extension = pathinfo($path, PATHINFO_EXTENSION); 304 | if (strtolower($extension) == 'pfx') { 305 | $pkcs12certdata = file_get_contents($path); 306 | openssl_pkcs12_read($pkcs12certdata, $certs, $pwd); 307 | $x509data = $certs['cert']; 308 | } else { 309 | $x509data = file_get_contents($path); 310 | } 311 | openssl_x509_read($x509data); 312 | $certdata = openssl_x509_parse($x509data); 313 | return $certdata['serialNumber']; 314 | } 315 | 316 | // 取签名证书私钥 317 | private static function getSignPrivateKey() 318 | { 319 | $pkcs12 = file_get_contents(self::$config['sign_path']); 320 | openssl_pkcs12_read($pkcs12, $certs, self::$config['sign_pwd']); 321 | return $certs['pkey']; 322 | } 323 | 324 | // 验证并获取签名证书 325 | private static function getVerifyPublicKey($params) 326 | { 327 | $config = self::$config; 328 | 329 | if($params['version']=='5.0.0'){ 330 | //先判断配置的验签证书是否银联返回指定的证书是否一致 331 | if(self::getCertId(self::$config['verify_path']) != $params['certId']) { 332 | throw new \Exception('Verify sign cert is incorrect'); 333 | } 334 | $public_key = @file_get_contents(self::$config['verify_path']); 335 | } else if($params['version']=='5.1.0'){ 336 | $public_key = $params['signPubKeyCert'] ?: @file_get_contents($config['verify_path']); 337 | } 338 | 339 | if (empty($public_key) || !in_array($params['version'], ['5.0.0', '5.1.0'])) 340 | throw new \Exception("[ 404 ] validate signPubKeyCert by rootCert failed with error"); 341 | 342 | openssl_x509_read($public_key); 343 | $certInfo = openssl_x509_parse($public_key); 344 | 345 | if ($certInfo['validFrom_time_t'] > time() || $certInfo['validTo_time_t'] < time()) { 346 | throw new \Exception("[ 404 ] >>>>>证书已到期失效<<<<<<<"); 347 | } 348 | $acpArry = array( 349 | $_SERVER ['DOCUMENT_ROOT'] . trim($config['acp_root'], '.'), 350 | $_SERVER ['DOCUMENT_ROOT'] . trim($config['acp_middle'], '.') 351 | ); 352 | $result = openssl_x509_checkpurpose($public_key, X509_PURPOSE_ANY, $acpArry); 353 | if($result !== TRUE) 354 | throw new \Exception("[ 404 ] validate signPubKeyCert by rootCert failed with error"); 355 | return $public_key; 356 | } 357 | 358 | 359 | // 校验$value是否非空 360 | protected static function checkEmpty($value) { 361 | if (!isset($value)) 362 | return true; 363 | if ($value === null) 364 | return true; 365 | if (trim($value) === "") 366 | return true; 367 | return false; 368 | } 369 | 370 | /** 371 | * 建立请求,以表单HTML形式构造(默认) 372 | * @param $url 请求地址 373 | * @param $params 请求参数数组 374 | * @return 提交表单HTML文本 375 | */ 376 | protected static function buildRequestForm($url, $params) { 377 | 378 | $sHtml = "
"; 379 | foreach($params as $key=>$val){ 380 | if (false === self::checkEmpty($val)) { 381 | $val = str_replace("'","'",$val); 382 | $sHtml.= ""; 383 | } 384 | } 385 | //submit按钮控件请不要含有name属性 386 | $sHtml = $sHtml."
"; 387 | $sHtml = $sHtml.""; 388 | 389 | return $sHtml; 390 | } 391 | 392 | } -------------------------------------------------------------------------------- /src/Pay/Bytedance.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2020-05-13 17:02:49 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2023-04-12T14:02:14+08:00 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use fengkui\Supports\Http; 11 | 12 | /** 13 | * Bytedance 字节跳动支付 14 | * 小程序担保支付(V1) 15 | */ 16 | class Bytedance 17 | { 18 | // 接口版本 19 | const EDITON = 'v1'; 20 | 21 | // 统一下订单管理 22 | private static $ecpayUrl = 'https://developer.toutiao.com/api/apps/ecpay/'; 23 | // 服务端预下单 24 | private static $createOrderUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_order'; 25 | // 订单查询 26 | private static $queryOrderUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_order'; 27 | // 退款 28 | private static $createRefundUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/create_refund'; 29 | // 查询退款 30 | private static $queryRefundUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_refund'; 31 | // 分账请求 32 | private static $settleUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/settle'; 33 | // 查询分账 34 | private static $querySettleUrl = 'https://developer.toutiao.com/api/apps/ecpay/v1/query_settle'; 35 | // 服务商进件 36 | private static $addMerchantUrl = 'https://developer.toutiao.com/api/apps/ecpay/saas/add_merchant'; 37 | // 分账方进件 38 | private static $addSubMerchantUrl = 'https://developer.toutiao.com/api/apps/ecpay/saas/add_sub_merchant'; 39 | 40 | // 支付相关配置 41 | private static $config = array( 42 | 'app_id' => '', // App ID 43 | 'salt' => '', // 支付密钥值 44 | 'token' => '', // 回调验签的Token 45 | 'notify_url' => '', // 支付回调地址 46 | 'thirdparty_id' => '', // 第三方平台服务商 id,非服务商模式留空 47 | ); 48 | 49 | /** 50 | * [__construct 构造函数] 51 | * @param [type] $config [传递支付相关配置] 52 | */ 53 | public function __construct($config=NULL){ 54 | $config && self::$config = array_merge(self::$config, $config); 55 | } 56 | 57 | /** 58 | * [createOrder 下单支付] 59 | * @param [type] $order [description] 60 | * @return [type] [description] 61 | * $order = array( 62 | * 'body' => '', // 产品描述 63 | * 'total_amount' => '', // 订单金额(分) 64 | * 'order_sn' => '', // 订单编号 65 | * ); 66 | */ 67 | public static function createOrder($order) 68 | { 69 | $config = self::$config; 70 | $params = [ 71 | 'app_id' => $config['app_id'], // 是 小程序 AppID 72 | 'out_order_no' => (string)$order['order_sn'], // 是 开发者侧的订单号, 同一小程序下不可重复 73 | 'total_amount' => $order['total_amount'], // 是 支付价格; 接口中参数支付金额单位为[分] 74 | 'subject' => $order['body'], // 是 商品描述; 长度限制 128 字节,不超过 42 个汉字 75 | 'body' => $order['body'], // 是 商品详情 76 | 'valid_time' => 3600 * 2, // 是 订单过期时间(秒); 最小 15 分钟,最大两天 77 | // 'sign' => '', // 是 开发者对核心字段签名, 签名方式见文档附录, 防止传输过程中出现意外 78 | // 'cp_extra' => '', // 否 开发者自定义字段,回调原样回传 79 | // 'notify_url' => $config['notify_url'], // 否 商户自定义回调地址 80 | // 'thirdparty_id' => '', // 否 第三方平台服务商 id,非服务商模式留空 81 | 'disable_msg' => 1, // 否 是否屏蔽担保支付的推送消息,1-屏蔽 0-非屏蔽,接入 POI 必传 82 | // 'msg_page' => '', // 否 担保支付消息跳转页 83 | // 'store_uid' => '', // 否 多门店模式下,门店 uid 84 | ]; 85 | !empty($order['cp_extra']) && $params['cp_extra'] = $order['cp_extra']; 86 | !empty($config['notify_url']) && $params['notify_url'] = $config['notify_url']; 87 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 88 | if (!empty($config['msg_page'])) { 89 | $params['disable_msg'] = 0; 90 | $params['msg_page'] = $config['msg_page']; 91 | } 92 | 93 | $params['sign'] = self::makeSign($params); 94 | // dump($params);die; 95 | $url = self::$createOrderUrl; 96 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 97 | $result = json_decode($response, true); 98 | return $result; 99 | } 100 | 101 | /** 102 | * [queryOrder 订单查询] 103 | * @param [type] $orderSn [开发者侧的订单号, 不可重复] 104 | * @return [type] [description] 105 | */ 106 | public static function queryOrder($orderSn) 107 | { 108 | $config = self::$config; 109 | $params = [ 110 | 'app_id' => $config['app_id'], // 小程序 AppID 111 | 'out_order_no' => (string)$orderSn, // 开发者侧的订单号, 不可重复 112 | // 'sign' => '', // 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 113 | // 'thirdparty_id' => '', // 服务商模式接入必传 第三方平台服务商 id,非服务商模式留空 114 | ]; 115 | 116 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 117 | $params['sign'] = self::makeSign($params); 118 | 119 | $url = self::$queryOrderUrl; 120 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 121 | $result = json_decode($response, true); 122 | return $result; 123 | } 124 | 125 | /** 126 | * [notifyOrder 订单回调验证] 127 | * @return [array] [返回数组格式的notify数据] 128 | */ 129 | public static function notifyOrder() 130 | { 131 | $config = self::$config; 132 | $response = file_get_contents('php://input', 'r'); 133 | $result = json_decode($response, true); 134 | if (!$result || empty($result['msg'])) 135 | throw new \Exception("[400] 暂无回调信息"); 136 | $body = [ 137 | 'msg' => $result['msg'], 138 | 'nonce' => $result['nonce'], 139 | 'timestamp' => $result['timestamp'], 140 | ]; 141 | // 判断签名是否正确 判断支付状态 142 | $verifySign = self::verifySign($body); 143 | if (empty($result['msg_signature']) || $result['msg_signature'] != $verifySign) 144 | throw new \Exception("[401] 签名错误"); 145 | return json_decode($result['msg'], true); // 进行签名验证 146 | } 147 | 148 | /** 149 | * [createRefund 订单退款] 150 | * @param [type] $order [订单相关信息] 151 | * @return [type] [description] 152 | * $order = array( 153 | * 'order_sn' => '', // 订单编号 154 | * 'refund_sn' => '', // 退款编号 155 | * 'total_amount' => '', // 订单金额(分) 156 | * 'body' => '', // 退款原因 157 | * ); 158 | */ 159 | public static function createRefund($order) 160 | { 161 | $config = self::$config; 162 | $params = [ 163 | 'app_id' => $config['app_id'], // 是 小程序 id 164 | 'out_order_no' => (string)$order['order_sn'], // 是 商户分配订单号,标识进行退款的订单 165 | 'out_refund_no' => (string)$order['refund_sn'], // 是 商户分配退款号 166 | 'refund_amount' => $order['refund_amount'], // 是 退款金额,单位[分] 167 | 'reason' => $order['reason'] ?? '用户申请退款', // 是 退款理由,长度上限 100 168 | // 'cp_extra' => '', // 否 开发者自定义字段,回调原样回传 169 | // 'notify_url' => '', // 否 商户自定义回调地址 170 | // 'sign' => '', // 是 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 171 | // 'thirdparty_id' => '', // 否,服务商模式接入必传 第三方平台服务商 id,非服务商模式留空 172 | 'disable_msg' => 1, // 否 是否屏蔽担保支付消息,1-屏蔽 173 | // 'msg_page' => '', // 否 担保支付消息跳转页 174 | // 'all_settle' => '', // 否 是否为分账后退款,1-分账后退款;0-分账前退款。分账后退款会扣减可提现金额,请保证余额充足 175 | ]; 176 | 177 | !empty($order['cp_extra']) && $params['cp_extra'] = $order['cp_extra']; 178 | !empty($order['all_settle']) && $params['all_settle'] = $order['all_settle']; 179 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 180 | if (!empty($config['msg_page'])) { 181 | $params['disable_msg'] = 0; 182 | $params['msg_page'] = $config['msg_page']; 183 | } 184 | 185 | $params['sign'] = self::makeSign($params); 186 | 187 | $url = self::$queryOrderUrl; 188 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 189 | $result = json_decode($response, true); 190 | return $result; 191 | } 192 | 193 | /** 194 | * [queryRefund 退款查询] 195 | * @param [type] $refundSn [开发者侧的订单号, 不可重复] 196 | * @return [type] [description] 197 | */ 198 | public static function queryRefund($refundSn) 199 | { 200 | $config = self::$config; 201 | $params = [ 202 | 'app_id' => $config['app_id'], // 小程序 AppID 203 | 'out_refund_no' => $refundSn, // 开发者侧的退款号 204 | // 'sign' => '', // 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 205 | // 'thirdparty_id' => '', // 服务商模式接入必传 第三方平台服务商 id,非服务商模式留空 206 | ]; 207 | 208 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 209 | $params['sign'] = self::makeSign($params); 210 | 211 | $url = self::$queryRefundUrl; 212 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 213 | $result = json_decode($response, true); 214 | return $result; 215 | } 216 | 217 | /** 218 | * [notifyRefund 退款回调验证] 219 | * @return [array] [返回数组格式的notify数据] 220 | */ 221 | public static function notifyRefund() 222 | { 223 | $config = self::$config; 224 | $response = file_get_contents('php://input', 'r'); 225 | $result = json_decode($response, true); 226 | if (!$result || empty($result['msg'])) 227 | throw new \Exception("[400] 暂无回调信息"); 228 | $body = [ 229 | 'msg' => $result['msg'], 230 | 'nonce' => $result['nonce'], 231 | 'timestamp' => $result['timestamp'], 232 | ]; 233 | // 判断签名是否正确 判断支付状态 234 | $verifySign = self::verifySign($body); 235 | if (empty($result['msg_signature']) || $result['msg_signature'] != $verifySign) 236 | throw new \Exception("[401] 签名错误"); 237 | return json_decode($result['msg'], true); // 进行签名验证 238 | } 239 | 240 | 241 | 242 | /** 243 | * [settle 分账请求] 244 | * @param [type] $order [分账信息] 245 | * @return [type] [description] 246 | * $order = array( 247 | * 'body' => '', // 产品描述 248 | * 'total_amount' => '', // 订单金额(分) 249 | * 'order_sn' => '', // 商户分配订单号 250 | * 'settle_sn' => '', // 开发者侧的结算号 251 | * ); 252 | */ 253 | public static function settle($order) 254 | { 255 | $config = self::$config; 256 | $params = [ 257 | 'app_id' => $config['app_id'], // 是 小程序 AppID 258 | 'out_order_no' => (string)$order['order_sn'], // 是 商户分配订单号,标识进行结算的订单 259 | 'out_settle_no' => (string)$order['settle_sn'], // 是 开发者侧的结算号, 不可重复 260 | 'settle_desc' => $order['body'], // 是 结算描述,长度限制 80 个字符 261 | // 'cp_extra' => '', // 否 开发者自定义字段,回调原样回传 262 | // 'notify_url' => '', // 否 商户自定义回调地址 263 | // 'sign' => '', // 是 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 264 | // 'thirdparty_id' => '', // 否,服务商模式接入必传 第三方平台服务商 id,非服务商模式留空 265 | // 'settle_params' => '', // 否,其他分账方信息,分账分配参数 SettleParameter 数组序列化后生成的 json 格式字符串 266 | ]; 267 | 268 | !empty($order['cp_extra']) && $params['cp_extra'] = $order['cp_extra']; 269 | !empty($order['settle_params']) && $params['settle_params'] = $order['settle_params']; 270 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 271 | $params['sign'] = self::makeSign($params); 272 | 273 | $url = self::$settleUrl; 274 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 275 | $result = json_decode($response, true); 276 | return $result; 277 | } 278 | 279 | /** 280 | * [querySettle 分账查询] 281 | * @param [type] $settleSn [开发者侧的订单号, 不可重复] 282 | * @return [type] [description] 283 | */ 284 | public static function querySettle($settleSn) 285 | { 286 | $config = self::$config; 287 | $params = [ 288 | 'app_id' => $config['app_id'], // 小程序 AppID 289 | 'out_settle_no' => $settleSn, // 开发者侧的分账号 290 | // 'sign' => '', // 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 291 | // 'thirdparty_id' => '', // 服务商模式接入必传 第三方平台服务商 id,非服务商模式留空 292 | ]; 293 | 294 | !empty($config['thirdparty_id']) && $params['thirdparty_id'] = $config['thirdparty_id']; 295 | $params['sign'] = self::makeSign($params); 296 | 297 | $url = self::$querySettleUrl; 298 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 299 | $result = json_decode($response, true); 300 | return $result; 301 | } 302 | 303 | /** 304 | * [notifySettle 分账回调验证] 305 | * @return [array] [返回数组格式的notify数据] 306 | */ 307 | public static function notifySettle() 308 | { 309 | $config = self::$config; 310 | $response = file_get_contents('php://input', 'r'); 311 | $result = json_decode($response, true); 312 | if (!$result || empty($result['msg'])) 313 | throw new \Exception("[400] 暂无回调信息"); 314 | $body = [ 315 | 'msg' => $result['msg'], 316 | 'nonce' => $result['nonce'], 317 | 'timestamp' => $result['timestamp'], 318 | ]; 319 | // 判断签名是否正确 判断支付状态 320 | $verifySign = self::verifySign($body); 321 | if (empty($result['msg_signature']) || $result['msg_signature'] != $verifySign) 322 | throw new \Exception("[401] 签名错误"); 323 | return json_decode($result['msg'], true); // 进行签名验证 324 | } 325 | 326 | /** 327 | * [addMerchant 服务商进件] 328 | * @param [type] $accessToken [授权码兑换接口调用凭证] 329 | * @param [type] $componentId [小程序第三方平台应用] 330 | * @param integer $urlType [链接类型:1-进件页面 2-账户余额页] 331 | */ 332 | public static function addMerchant($accessToken, $componentId, $urlType=1) 333 | { 334 | $params = [ 335 | 'component_access_token' => $accessToken, // 是 授权码兑换接口调用凭证 336 | 'thirdparty_component_id' => $componentId, // 是 小程序第三方平台应用 id 337 | 'url_type' => $urlType, // 是 链接类型:1-进件页面 2-账户余额页 338 | ]; 339 | 340 | $url = self::$addMerchantUrl; 341 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 342 | $result = json_decode($response, true); 343 | return $result; 344 | } 345 | 346 | /** 347 | * [addSubMerchant 分账方进件] 348 | * @param [type] $thirdpartyId [小程序第三方平台应用] 349 | * @param [type] $merchantId [商户 id,用于接入方自行标识并管理进件方。由服务商自行分配管理] 350 | * @param integer $urlType [链接类型:1-进件页面 2-账户余额页] 351 | */ 352 | public static function addSubMerchant($thirdpartyId, $merchantId, $urlType=1) 353 | { 354 | $params = [ 355 | 'thirdparty_id' => $thirdpartyId, // 是 小程序第三方平台应用 id 356 | 'sub_merchant_id' => $merchantId, // 是 商户 id,用于接入方自行标识并管理进件方。由服务商自行分配管理 357 | 'url_type' => $urlType, // 是 链接类型:1-进件页面 2-账户余额页 358 | // 'sign' => '', // 开发者对核心字段签名, 签名方式见文档, 防止传输过程中出现意外 359 | ]; 360 | 361 | $params['sign'] = self::makeSign($params); 362 | 363 | $url = self::$addSubMerchantUrl; 364 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE)); 365 | $result = json_decode($response, true); 366 | return $result; 367 | } 368 | 369 | /** 370 | * [success 通知状态] 371 | */ 372 | public static function success() 373 | { 374 | $array = ['err_no'=>0, 'err_tips'=>'success']; 375 | die(json_encode($array)); 376 | } 377 | 378 | /** 379 | * [makeSign 生成秘钥] 380 | * @param [type] $data [加密数据] 381 | * @return [type] [description] 382 | */ 383 | public static function makeSign($data) { 384 | $config = self::$config; 385 | $rList = array(); 386 | foreach($data as $k => $v) { 387 | if (in_array($k, ['other_settle_params', 'app_id', 'sign', 'thirdparty_id'])) 388 | continue; 389 | $value = trim(strval($v)); 390 | $len = strlen($value); 391 | if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\"") 392 | $value = substr($value,1, $len-1); 393 | $value = trim($value); 394 | if ($value == "" || $value == "null") 395 | continue; 396 | array_push($rList, $value); 397 | } 398 | array_push($rList, $config['salt']); 399 | sort($rList, 2); 400 | return md5(implode('&', $rList)); 401 | } 402 | 403 | /** 404 | * [verifySign 验证签名] 405 | * @param [type] $data [回调数据] 406 | * @return [type] [description] 407 | */ 408 | public static function verifySign($data) 409 | { 410 | $config = self::$config; 411 | $filtered = []; 412 | foreach ($data as $key => $value) { 413 | if (in_array($key, ['msg_signature', 'type'])) 414 | continue; 415 | $filtered[] = is_string($value)? trim($value): $value; 416 | } 417 | $filtered[] = trim($config['token']); 418 | sort($filtered, SORT_STRING); 419 | $filtered = trim(implode('', $filtered)); 420 | return sha1($filtered); 421 | } 422 | 423 | 424 | } 425 | -------------------------------------------------------------------------------- /src/Pay/Alipay.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2022-02-26T19:25:26+08:00 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2024-04-03 09:26:13 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use Exception; 11 | use RuntimeException; 12 | use fengkui\Supports\Http; 13 | 14 | /** 15 | * Alipay 支付宝支付(更新中) 16 | */ 17 | class Alipay 18 | { 19 | //沙盒地址 20 | private static $sandurl = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do'; 21 | //正式地址 22 | private static $apiurl = 'https://openapi.alipay.com/gateway.do'; 23 | //网关地址(设置为公有,外部需要调用) 24 | private static $gateway; 25 | // 请求使用的编码格式 26 | private static $charset = 'utf-8'; 27 | // 仅支持JSON 28 | private static $format='JSON'; 29 | // 调用的接口版本 30 | private static $version = '1.0'; 31 | // 商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2 32 | private static $signType = 'RSA2'; 33 | // 订单超时时间 34 | private static $timeout = '15m'; 35 | 36 | private static $config = array( 37 | 'app_id' => '', // 开发者的应用ID 38 | 'xcx_id' => '', // 小程序 appid 39 | 'public_key' => '', // 支付宝公钥,一行字符串 40 | 'private_key' => '', // 开发者私钥去头去尾去回车,一行字符串 41 | 42 | 'notify_url' => '', // 异步接收支付状态 43 | 'return_url' => '', // 同步接收支付状态 44 | 'sign_type' => 'RSA2', // 生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,默认使用RSA2 45 | 'is_sandbox' => false, // 是否使用沙箱调试,true使用沙箱,false不使用,默认false不使用 46 | ); 47 | 48 | /** 49 | * [__construct 构造函数] 50 | * @param [type] $config [传递相关配置] 51 | */ 52 | public function __construct($config=NULL){ 53 | $config && self::$config = array_merge(self::$config, $config); 54 | isset(self::$config['sign_type']) && self::$signType = self::$config['sign_type']; 55 | self::$gateway = !empty(self::$config['is_sandbox']) ? self::$sandurl : self::$apiurl; //请求地址,判断是否使用沙箱,默认不使用 56 | } 57 | 58 | public static function unifiedOrder($order, $params, $type=false) 59 | { 60 | // 获取配置项 61 | $config = self::$config; 62 | //请求参数 63 | $requestParams = array( 64 | 'out_trade_no' => !empty($order['order_sn']) ? (string)$order['order_sn'] : '', //唯一标识,订单编号(必须) 65 | // 'product_code' => $order['product_code'], 66 | 'total_amount' => $order['total_amount'] ?? '', //付款金额,单位:元 67 | 'subject' => $order['body'] ?? '购买商品', //订单标题 68 | "timeout_express" => self::$timeout, //该笔订单允许的最晚付款时间,逾期将关闭交易。取值范围:1m~15d。m-分钟,h-小时,d-天 69 | ); 70 | 71 | // 订单失效时间 72 | if (!empty($order['time_expire'])) { 73 | preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']); 74 | $time = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time(); 75 | $requestParams['time_expire'] = date('Y-m-d H:i:s', $time); 76 | unset($order['time_expire']); 77 | } 78 | 79 | $requestParams = $type ? $order : array_merge($requestParams, $order); 80 | //公共参数 81 | $commonParams = array( 82 | 'app_id' => $config['app_id'], 83 | // 'method' => $params['method'], // 接口名称 84 | 'format' => self::$format, 85 | 'return_url' => $config['return_url'], //同步通知地址 86 | 'charset' => self::$charset, 87 | 'sign_type' => self::$signType, 88 | 'timestamp' => date('Y-m-d H:i:s'), 89 | 'version' => self::$version, 90 | 'notify_url' => $config['notify_url'], //异步通知地址 91 | 'biz_content' => json_encode($requestParams, JSON_UNESCAPED_UNICODE), 92 | ); 93 | $commonParams = array_merge($commonParams, $params); 94 | // dump($commonParams);die; 95 | $commonParams["sign"] = self::makeSign($commonParams); 96 | return $commonParams; 97 | } 98 | 99 | // 电脑网页支付 100 | public static function web($order, $method='get'){ 101 | $order['product_code'] = 'FAST_INSTANT_TRADE_PAY'; // 销售产品码,与支付宝签约的产品码名称。注:目前电脑支付场景下仅支持FAST_INSTANT_TRADE_PAY 102 | $params['method'] = 'alipay.trade.page.pay'; // 接口名称 103 | 104 | $params = self::unifiedOrder($order, $params); 105 | if($method=='get'){ 106 | $preString=self::getSignContent($params, true); 107 | //拼接GET请求串 108 | $result = self::$gateway."?".$preString; 109 | }else{ 110 | $result = self::buildRequestForm(self::$gateway, $params); 111 | } 112 | 113 | return $result; 114 | } 115 | 116 | // 发起手机网站支付 117 | public static function wap($order, $method='get'){ 118 | $order['product_code'] = 'QUICK_WAP_WAY'; // 销售产品码,商家和支付宝签约的产品码。手机网站支付为:QUICK_WAP_WAY 119 | $params['method'] = 'alipay.trade.wap.pay'; // 接口名称 120 | 121 | $params = self::unifiedOrder($order, $params); 122 | if($method=='get'){ 123 | $preString=self::getSignContent($params, true); 124 | //拼接GET请求串 125 | $result = self::$gateway."?".$preString; 126 | }else{ 127 | $result = self::buildRequestForm(self::$gateway, $params); 128 | } 129 | return $result; 130 | } 131 | 132 | // 发起当面付 133 | public static function face($order){ 134 | $order['product_code'] = 'FACE_TO_FACE_PAYMENT'; 135 | empty($order['scene']) && $order['scene'] = 'bar_code'; // 支付场景。bar_code(默认):当面付条码支付场景; security_code:当面付刷脸支付场景,对应的auth_code为fp开头的刷脸标识串; 136 | $params['method'] = 'alipay.trade.precreate'; // 接口名称 137 | 138 | $params = self::unifiedOrder($order, $params); 139 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 140 | $result = json_decode($response, true); 141 | $result = $result['alipay_trade_precreate_response']; 142 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 143 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 144 | } 145 | return $result; 146 | } 147 | 148 | // app支付(JSAPI) 149 | public static function app($order){ 150 | $order['product_code'] = 'QUICK_MSECURITY_PAY'; //销售产品码,商家和支付宝签约的产品码,APP支付为固定值QUICK_MSECURITY_PAY 151 | $params['method'] = 'alipay.trade.app.pay'; // 接口名称 152 | 153 | $result = self::unifiedOrder($order, $params); 154 | return http_build_query($result); 155 | } 156 | 157 | // JSAPI支付(小程序) 158 | public static function xcx($order){ 159 | $config = self::$config; 160 | if(empty($order['order_sn']) || empty($order['total_amount']) || (empty($order['buyer_id']) && empty($order['buyer_open_id']))){ 161 | die("订单数组信息缺失!"); 162 | } 163 | $order['product_code'] = 'JSAPI_PAY'; //销售产品码,商家和支付宝签约的产品码,APP支付为固定值QUICK_MSECURITY_PAY 164 | $order['op_app_id'] = $config['xcx_id']; 165 | $params['app_id'] = $config['xcx_id']; // 替换app_id 166 | $params['method'] = 'alipay.trade.create'; // 接口名称 167 | 168 | $params = self::unifiedOrder($order, $params); 169 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 170 | $result = json_decode($response, true); 171 | $result = $result['alipay_trade_create_response']; 172 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 173 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 174 | } 175 | return $result; 176 | } 177 | 178 | /** 179 | * [query 查询订单] 180 | * @param [type] $orderSn [订单编号] 181 | * @param boolean $type [支付宝支付订单编号,是否是支付宝支付订单号] 182 | * @return [type] [description] 183 | */ 184 | public static function query($orderSn, $type = false) { 185 | $order = $type ? ['trade_no' => $orderSn] : ['out_trade_no' => $orderSn]; 186 | $params['method'] = 'alipay.trade.query'; // 接口名称 187 | $params = self::unifiedOrder($order, $params, true); 188 | 189 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 190 | $result = json_decode($response, true); 191 | $result = $result['alipay_trade_query_response']; 192 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 193 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 194 | } 195 | return $result; 196 | } 197 | 198 | /** 199 | * [close 关闭订单] 200 | * @param [type] $orderSn [订单编号] 201 | * @param boolean $type [支付宝支付订单编号,是否是支付宝支付订单号] 202 | * @return [type] [description] 203 | */ 204 | public static function close($orderSn, $type = false) { 205 | $order = $type ? ['trade_no' => $orderSn] : ['out_trade_no' => $orderSn]; 206 | $params['method'] = 'alipay.trade.close'; // 接口名称 207 | $params = self::unifiedOrder($order, $params, true); 208 | 209 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 210 | $result = json_decode($response, true); 211 | $result = $result['alipay_trade_close_response']; 212 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 213 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 214 | } 215 | return $result; 216 | } 217 | 218 | // 支付宝异步通知 219 | public static function notify($response = null){ 220 | $config = self::$config; 221 | $response = $response ?: $_POST; 222 | $result = is_array($response) ? $response : json_decode($response, true); 223 | $sign = $result['sign'] ?? ''; 224 | 225 | //不参与签名 226 | unset($result['sign']); 227 | unset($result['sign_type']); 228 | $rst = self::verifySign($result, $sign); 229 | if(!$rst) 230 | return false; 231 | return $result; 232 | } 233 | 234 | // 订单退款 235 | public static function refund($order) 236 | { 237 | $config = self::$config; 238 | if(empty($order['refund_sn']) || empty($order['refund_amount']) || (empty($order['order_sn']) && empty($order['trade_no']))){ 239 | die("订单数组信息缺失!"); 240 | } 241 | 242 | $refund['refund_amount'] = $order['refund_amount']; 243 | $refund['out_request_no'] = (string)$order['refund_sn']; 244 | 245 | empty($order['order_sn']) || $refund['out_trade_no'] = (string)$order['order_sn']; 246 | empty($order['trade_no']) || $refund['trade_no'] = $order['trade_no']; 247 | empty($order['reason']) || $refund['refund_reason'] = $order['reason']; 248 | 249 | $params['method'] = 'alipay.trade.refund'; 250 | 251 | $params = self::unifiedOrder($refund, $params, true); 252 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 253 | $result = json_decode($response, true); 254 | $result = $result['alipay_trade_refund_response']; 255 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 256 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 257 | } 258 | return $result; 259 | } 260 | 261 | /** 262 | * [refundQuery 退款查询] 263 | */ 264 | public static function refundQuery($order) { 265 | if(empty($order['refund_sn']) || (empty($order['order_sn']) && empty($order['trade_no']))){ 266 | die("订单数组信息缺失!"); 267 | } 268 | $refund['out_request_no'] = (string)$order['refund_sn']; 269 | 270 | empty($order['order_sn']) || $refund['out_trade_no'] = (string)$order['order_sn']; 271 | empty($order['trade_no']) || $refund['trade_no'] = $order['trade_no']; 272 | 273 | $params['method'] = 'alipay.trade.fastpay.refund.query'; // 接口名称 274 | $params = self::unifiedOrder($refund, $params, true); 275 | 276 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 277 | $result = json_decode($response, true); 278 | $result = $result['alipay_trade_fastpay_refund_query_response']; 279 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 280 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 281 | } 282 | return $result; 283 | } 284 | 285 | // 转账到支付宝 286 | public static function transfer($order) { 287 | $order = array( 288 | 'order_title' => $order['body'], 289 | 'out_biz_no' => (string)$order['order_sn'], 290 | 'trans_amount' => $order['amount'], 291 | 'biz_scene' => 'DIRECT_TRANSFER', 292 | 'product_code' => 'TRANS_ACCOUNT_NO_PWD', 293 | 'payee_info' => $order['payee_info'], 294 | ); 295 | 296 | $params['method'] = 'alipay.fund.trans.uni.transfer'; // 接口名称 297 | $params = self::unifiedOrder($order, $params, true); 298 | 299 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 300 | $result = json_decode($response, true); 301 | $result = $result['alipay_fund_trans_uni_transfer_response']; 302 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 303 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 304 | } 305 | return $result; 306 | } 307 | 308 | // 转账查询 309 | public static function transQuery($order) { 310 | if(empty($order['order_sn']) && empty($order['order_id']) && empty($order['pay_fund_order_id'])){ 311 | die("订单数组信息缺失!"); 312 | } 313 | 314 | empty($order['order_id']) || $transfer['order_id'] = (string)$order['order_id']; 315 | empty($order['pay_fund_order_id']) || $transfer['pay_fund_order_id'] = (string)$order['pay_fund_order_id']; 316 | if (!empty($order['order_sn'])) { 317 | $transfer['out_biz_no'] = (string)$order['order_sn']; 318 | 319 | $transfer['product_code'] = $order['product_code'] ?? 'TRANS_ACCOUNT_NO_PWD'; 320 | $transfer['biz_scene'] = $order['biz_scene'] ?? 'DIRECT_TRANSFER'; 321 | } 322 | 323 | $params['method'] = 'alipay.fund.trans.common.query'; // 接口名称 324 | $params = self::unifiedOrder($transfer, $params, true); 325 | 326 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 327 | $result = json_decode($response, true); 328 | $result = $result['alipay_fund_trans_common_query_response']; 329 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 330 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 331 | } 332 | return $result; 333 | } 334 | 335 | // 分账关系绑定与解绑 336 | public static function relationBind($account, $type = true) { 337 | $order['out_request_no'] = (string)$account['order_sn']; 338 | $order['receiver_list'] = $account['list']; // 分账接收方列表,单次传入最多20个 339 | $params['method'] = 'alipay.trade.royalty.relation.' . ($type ? 'bind' : 'unbind'); // 接口名称 340 | $params = self::unifiedOrder($order, $params, true); 341 | 342 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 343 | $result = json_decode($response, true); 344 | $result = $result['alipay_trade_royalty_relation_' . ($type ? 'bind' : 'unbind') . '_response']; 345 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 346 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 347 | } 348 | return $result; 349 | } 350 | 351 | // 查询分账关系 352 | public static function relationQuery($orderSn) { 353 | $order = ['out_request_no' => $orderSn]; 354 | 355 | $params['method'] = 'alipay.trade.royalty.relation.batchquery'; // 接口名称 356 | $params = self::unifiedOrder($order, $params, true); 357 | 358 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 359 | $result = json_decode($response, true); 360 | $result = $result['alipay_trade_royalty_relation_batchquery_response']; 361 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 362 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 363 | } 364 | return $result; 365 | } 366 | 367 | // 统一收单交易结算接口 368 | public static function settle($order) { 369 | $settle = array( 370 | 'out_request_no' => (string)$order['order_sn'], 371 | 'trade_no' => (string)$order['trade_no'], 372 | 'royalty_parameters' => $order['list'], 373 | 'royalty_mode' => 'async', 374 | ); 375 | 376 | $params['method'] = 'alipay.trade.order.settle'; // 接口名称 377 | $params = self::unifiedOrder($settle, $params, true); 378 | 379 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 380 | $result = json_decode($response, true); 381 | $result = $result['alipay_trade_order_settle_response']; 382 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 383 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 384 | } 385 | return $result; 386 | } 387 | 388 | // 交易分账查询接口 389 | public static function settleQuery($order = null) { 390 | if (!empty($order['order_sn']) && !empty($order['trade_no'])) { 391 | $settle['out_request_no'] = (string)$order['order_sn']; 392 | $settle['trade_no'] = (string)$order['trade_no']; 393 | } elseif (!empty($order['settle_no'])) { 394 | $settle['settle_no'] = (string)$order['settle_no']; 395 | } else { 396 | die('参数缺失'); 397 | } 398 | $params['method'] = 'alipay.trade.order.settle.query'; // 接口名称 399 | $params = self::unifiedOrder($settle, $params, true); 400 | 401 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 402 | $result = json_decode($response, true); 403 | $result = $result['alipay_trade_order_settle_query_response']; 404 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 405 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 406 | } 407 | return $result; 408 | } 409 | 410 | // 分账比例查询 && 分账剩余金额查询 411 | public static function onsettleQuery($orderSn, $type = false) { 412 | $order = $type ? ['out_request_no' => $orderSn] : ['trade_no' => $orderSn]; 413 | $params['method'] = 'alipay.trade.' . ($type ? 'royalty.rate' : 'order.onsettle') . '.query'; // 接口名称 414 | $params = self::unifiedOrder($order, $params, true); 415 | 416 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 417 | $result = json_decode($response, true); 418 | $result = $result['alipay_trade_' . ($type ? 'royalty_rate' : 'order_onsettle') . '_query_response']; 419 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 420 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 421 | } 422 | return $result; 423 | } 424 | 425 | // 获取会员信息 426 | public static function doGetUserInfo($token) 427 | { 428 | $params['method'] = 'alipay.user.info.share'; // 接口名称 429 | $params['auth_token'] = $token; // 接口名称 430 | $params = self::unifiedOrder([], $params, true); 431 | 432 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 433 | $result = json_decode($response, true); 434 | $result = $result['alipay_user_info_share_response']; 435 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 436 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 437 | } 438 | return $result; 439 | } 440 | 441 | /** 442 | * 获取access_token和user_id 443 | */ 444 | public function getToken($type = true) 445 | { 446 | $config = self::$config; 447 | //通过code获得access_token和user_id 448 | if (isset($_GET['auth_code'])){ 449 | //获取code码,以获取openid 450 | $params = array( 451 | 'app_id' => $config['app_id'], 452 | 'method' => 'alipay.system.oauth.token', // 接口名称 453 | 'format' => self::$format, 454 | 'charset' => self::$charset, 455 | 'sign_type' => self::$signType, 456 | 'timestamp' => date('Y-m-d H:i:s'), 457 | 'version' => self::$version, 458 | 'grant_type' =>'authorization_code', 459 | 'code' => $_GET['auth_code'], 460 | ); 461 | 462 | $params["sign"] = self::makeSign($params); 463 | $response = Http::post(self::$gateway . '?charset=' . self::$charset, $params); 464 | $result = json_decode($response, true); 465 | $result = $result['alipay_system_oauth_token_response']; 466 | if (isset($result['code']) && $result['code'] != 10000) { // 错误抛出异常 467 | throw new \Exception("[" . $result['code'] . "] " . $result['sub_code']. ' ' . $result['sub_msg']); 468 | } 469 | return $result; 470 | } else { 471 | //触发返回code码 472 | $scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']=='on' ? 'https://' : 'http://'; 473 | $redirectUrl = urlencode($scheme.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']); 474 | $_SERVER['QUERY_STRING'] && $redirectUrl = $baseUrl.'?'.$_SERVER['QUERY_STRING']; 475 | $urlObj['app_id'] = $config['app_id']; 476 | $urlObj['scope'] = $type ? 'auth_base' : 'auth_user'; 477 | $urlObj['redirect_uri'] = urldecode($redirectUrl); 478 | $bizString = http_build_query($urlObj); 479 | $url = 'https://openauth' . ($config['is_sandbox'] ? '-sandbox.dl.alipaydev' : '.alipay') . '.com/oauth2/publicAppAuthorize.htm?' . $bizString; 480 | Header("Location: $url"); 481 | exit(); 482 | } 483 | } 484 | 485 | /** 486 | * 拼接GET请求相关参数 487 | * $urlencode相关参数encode 488 | */ 489 | public static function getSignContent($params, $urlencode = false) { 490 | ksort($params); 491 | $stringToBeSigned = ""; 492 | $i = 0; 493 | foreach ($params as $k => $v) { 494 | if (false === self::checkEmpty($v) && "@" != substr($v, 0, 1)) { 495 | // 转换成目标字符集 496 | $v = self::characet($v, self::$charset); 497 | if ($i == 0) { 498 | $stringToBeSigned .= "$k" . "=" . ($urlencode ? urlencode($v) : "$v"); 499 | } else { 500 | $stringToBeSigned .= "&" . "$k" . "=" . ($urlencode ? urlencode($v) : "$v"); 501 | } 502 | $i++; 503 | } 504 | } 505 | unset ($k, $v); 506 | return $stringToBeSigned; 507 | } 508 | 509 | /** 510 | * 转换字符集编码 511 | */ 512 | protected static function characet($data, $targetCharset) { 513 | if (!empty($data)) { 514 | $fileType = self::$charset; 515 | if (strcasecmp($fileType, $targetCharset) != 0) { 516 | $data = mb_convert_encoding($data, $targetCharset, $fileType); 517 | //$data = iconv($fileType, $targetCharset.'//IGNORE', $data); 518 | } 519 | } 520 | return $data; 521 | } 522 | /** 523 | * 校验$value是否非空 524 | */ 525 | protected static function checkEmpty($value) { 526 | if (!isset($value)) 527 | return true; 528 | if ($value === null) 529 | return true; 530 | if (trim($value) === "") 531 | return true; 532 | return false; 533 | } 534 | 535 | // 生成签名 536 | protected static function makeSign($data) { 537 | $data = self::getSignContent($data); 538 | $priKey = self::$config['private_key']; 539 | $res = "-----BEGIN RSA PRIVATE KEY-----\n" . 540 | wordwrap($priKey, 64, "\n", true) . 541 | "\n-----END RSA PRIVATE KEY-----"; 542 | ($res) or die('您使用的私钥格式错误,请检查RSA私钥配置'); 543 | if (self::$signType == "RSA2") { 544 | //OPENSSL_ALGO_SHA256是php5.4.8以上版本才支持 545 | openssl_sign($data, $sign, $res, version_compare(PHP_VERSION,'5.4.0', '<') ? SHA256 : OPENSSL_ALGO_SHA256); 546 | } else { 547 | openssl_sign($data, $sign, $res); 548 | } 549 | $sign = base64_encode($sign); 550 | return $sign; 551 | } 552 | 553 | // 验签函数(用于查询支付宝数据) 554 | protected static function verifySign($data, $sign) { 555 | $public_key = self::$config['public_key']; 556 | $search = [ 557 | "-----BEGIN PUBLIC KEY-----", 558 | "-----END PUBLIC KEY-----", 559 | "\n", 560 | "\r", 561 | "\r\n" 562 | ]; 563 | $public_key = str_replace($search,"",$public_key); 564 | $public_key=$search[0] . PHP_EOL . wordwrap($public_key, 64, "\n", true) . PHP_EOL . $search[1]; 565 | 566 | if (self::$signType == 'RSA') { 567 | $result = (bool)openssl_verify(self::getSignContent($data), base64_decode($sign), openssl_get_publickey($public_key)); 568 | } elseif (self::$signType == 'RSA2') { 569 | $result = (bool)openssl_verify(self::getSignContent($data), base64_decode($sign), openssl_get_publickey($public_key), OPENSSL_ALGO_SHA256); 570 | } 571 | return $result; 572 | } 573 | 574 | /** 575 | * 建立请求,以表单HTML形式构造(默认) 576 | * @param $url 请求地址 577 | * @param $params 请求参数数组 578 | * @return 提交表单HTML文本 579 | */ 580 | protected static function buildRequestForm($url, $params) { 581 | 582 | $sHtml = "
"; 583 | foreach($params as $key=>$val){ 584 | if (false === self::checkEmpty($val)) { 585 | $val = str_replace("'","'",$val); 586 | $sHtml.= ""; 587 | } 588 | } 589 | //submit按钮控件请不要含有name属性 590 | $sHtml = $sHtml."
"; 591 | $sHtml = $sHtml.""; 592 | 593 | return $sHtml; 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /src/Pay/Wechat.php: -------------------------------------------------------------------------------- 1 | 4 | * @Date: 2019-09-06 09:50:30 5 | * @Last Modified by: [FENG] <1161634940@qq.com> 6 | * @Last Modified time: 2025-03-14 22:13:12 7 | */ 8 | namespace fengkui\Pay; 9 | 10 | use Exception; 11 | use RuntimeException; 12 | use fengkui\Supports\Http; 13 | 14 | /** 15 | * Wechat 微信支付 16 | * 新版(V3)接口(更新中) 17 | */ 18 | class Wechat 19 | { 20 | const AUTH_TAG_LENGTH_BYTE = 16; 21 | // 是否是服务商 22 | private static $facilitator = false; 23 | 24 | // 新版相关接口 25 | // GET 获取平台证书列表 26 | private static $certificatesUrl = 'https://api.mch.weixin.qq.com/v3/certificates'; 27 | // 统一下订单管理 28 | private static $transactionsUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/'; 29 | // 统一下订单管理(服务商) 30 | private static $partnerTransactionsUrl = 'https://api.mch.weixin.qq.com/v3/pay/partner/transactions/'; 31 | // 申请退款 32 | private static $refundUrl = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds'; 33 | // 商家转账 34 | // private static $batchesUrl = 'https://api.mch.weixin.qq.com/v3/transfer/batches'; 35 | private static $transferBillsUrl = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'; 36 | 37 | // 请求分账 38 | private static $profitSharingUrl = 'https://api.mch.weixin.qq.com/v3/profitsharing'; 39 | // 静默授权,获取code 40 | private static $authorizeUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize'; 41 | // 通过code获取access_token以及openid 42 | private static $accessTokenUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token'; 43 | 44 | // 支付完整配置 45 | private static $config = array( 46 | 'xcxid' => '', // 小程序 appid 47 | 'appid' => '', // 微信支付 appid 48 | 'mchid' => '', // 微信支付 mch_id 商户收款账号 49 | 'key' => '', // 微信支付 apiV3key(尽量包含大小写字母,否则验签不通过,服务商模式使用服务商key) 50 | 'appsecret' => '', // 公众帐号 secert (公众号支付获取 code 和 openid 使用) 51 | 52 | 'sp_appid' => '', // 服务商应用 ID 53 | 'sp_mchid' => '', // 服务商户号 54 | 55 | 'notify_url' => '', // 接收支付状态的连接 改成自己的回调地址 56 | 'redirect_url' => '', // 公众号支付,调起支付页面 57 | 58 | // 服务商模式下,使用服务商证书 59 | 'serial_no' => '', // 商户API证书序列号(可不传,默认根据证书直接获取) 60 | 'cert_client' => './cert/apiclient_cert.pem', // 证书(退款,红包时使用) 61 | 'cert_key' => './cert/apiclient_key.pem', // 商户API证书私钥(Api安全中下载) 62 | 63 | 'public_key_id' => '', // 平台证书序列号或支付公钥ID 64 | // (支付公钥ID请带:PUB_KEY_ID_ 前缀,默认根据证书直接获取,不带前缀) 65 | 'public_key' => './cert/public_key.pem', // 平台证书或支付公钥(Api安全中下载) 66 | // (微信支付新申请的,已不支持平台证书,老版调用证书列表,自动生成平台证书,注意目录权限) 67 | ); 68 | 69 | /** 70 | * [__construct 构造函数] 71 | * @param [type] $config [传递微信支付相关配置] 72 | */ 73 | public function __construct($config=NULL){ 74 | $config && self::$config = array_merge(self::$config, $config); 75 | if (self::$config['sp_appid'] && self::$config['sp_mchid']) { 76 | self::$facilitator = true; // 服务商模式 77 | self::$transactionsUrl = self::$partnerTransactionsUrl; 78 | } 79 | } 80 | 81 | /** 82 | * [unifiedOrder 统一下单] 83 | * @param [type] $order [订单信息(必须包含支付所需要的参数)] 84 | * @param boolean $type [区分是否是小程序,是则传 true] 85 | * @return [type] [description] 86 | * $order = array( 87 | * 'body' => '', // 产品描述 88 | * 'order_sn' => '', // 订单编号 89 | * 'total_amount' => '', // 订单金额(分) 90 | * ); 91 | */ 92 | public static function unifiedOrder($order, $type=false) 93 | { 94 | $config = self::$config; 95 | // 获取配置项 96 | $params = array( 97 | // 'appid' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID 98 | // 'mchid' => $config['mchid'], // 直连商户的商户号 99 | 'description' => $order['body'], // 商品描述 100 | 'out_trade_no' => (string)$order['order_sn'], // 商户系统内部订单号 101 | 'notify_url' => $config['notify_url'], // 通知URL必须为直接可访问的URL 102 | 'amount' => ['total' => (int)$order['total_amount'], 'currency' => $order['currency'] ?? 'CNY'], // 订单金额信息 103 | ); 104 | if (self::$facilitator) { 105 | $params['sp_appid'] = $config['sp_appid']; // 服务商应用ID 106 | $params['sp_mchid'] = $config['sp_mchid']; // 服务商户号 107 | $params['sub_appid'] = $type ? $config['xcxid'] : $config['appid']; // 子商户的应用ID 108 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 109 | !empty($order['openid']) && $params['payer'] = ['sub_openid' => $order['openid']]; 110 | } else { 111 | $params['appid'] = $type ? $config['xcxid'] : $config['appid']; // 由微信生成的应用ID 112 | $params['mchid'] = $config['mchid']; // 直连商户的商户号 113 | !empty($order['openid']) && $params['payer'] = ['openid' => $order['openid']]; 114 | } 115 | 116 | !empty($params['payer']) && $params['scene_info'] = ['payer_client_ip' => self::get_ip()]; // IP地址 117 | !empty($order['attach']) && $params['attach'] = $order['attach']; // 附加数据 118 | !empty($order['settle_info']) && $params['settle_info'] = ['profit_sharing' => $order['settle_info'] ? true : false, ]; // 结算信息 119 | 120 | // 订单失效时间 121 | if (!empty($order['time_expire'])) { 122 | preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']); 123 | $time = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time(); 124 | $params['time_expire'] = date(DATE_ATOM, $time); 125 | } 126 | 127 | if (in_array($order['type'], ['ios', 'android', 'wap'])) { 128 | $params['scene_info'] = ['payer_client_ip' => self::get_ip()]; 129 | $params['scene_info']['h5_info'] = ['type' => $order['type']]; 130 | $url = self::$transactionsUrl . 'h5'; // 拼接请求地址 131 | } else { 132 | $url = self::$transactionsUrl . strtolower($order['type']); // 拼接请求地址 133 | } 134 | isset($order['_url']) && $url = $order['_url']; 135 | 136 | // 获取post请求header头 137 | $header = self::createAuthorization($url, $params, 'POST'); 138 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 139 | $result = json_decode($response, true); 140 | if (isset($result['code']) && isset($result['message'])) { 141 | throw new \Exception("[" . $result['code'] . "] " . $result['message']); 142 | } 143 | 144 | return $result; 145 | } 146 | 147 | /** 148 | * [query 查询订单] 149 | * @param [type] $orderSn [订单编号] 150 | * @param boolean $type [微信支付订单编号,是否是微信支付订单号] 151 | * @return [type] [description] 152 | */ 153 | public static function query($orderSn, $type = false) 154 | { 155 | $config = self::$config; 156 | $url = self::$transactionsUrl . ($type ? 'id/' : 'out-trade-no/') . $orderSn; 157 | 158 | if (self::$facilitator) { 159 | $params['sp_mchid'] = $config['sp_mchid']; 160 | $params['sub_mchid'] = $config['mchid']; 161 | } else { 162 | $params['mchid'] = $config['mchid']; 163 | } 164 | 165 | $header = self::createAuthorization($url, $params, 'GET'); 166 | $response = Http::get($url, $params, $header); 167 | $result = json_decode($response, true); 168 | 169 | return $result; 170 | } 171 | 172 | /** 173 | * [close 关闭订单] 174 | * @param [type] $orderSn [微信支付订单编号] 175 | * @return [type] [description] 176 | */ 177 | public static function close($orderSn) 178 | { 179 | $config = self::$config; 180 | if (self::$facilitator) { 181 | $params['sp_mchid'] = $config['sp_mchid']; // 服务商户号 182 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 183 | } else { 184 | $params['mchid'] = $config['mchid']; // 直连商户的商户号 185 | } 186 | $url = self::$transactionsUrl . 'out-trade-no/' . $orderSn . '/close'; 187 | 188 | $header = self::createAuthorization($url, $params, 'POST'); 189 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 190 | $result = json_decode($response, true); 191 | 192 | return true; 193 | } 194 | 195 | /** 196 | * [js 获取jssdk需要用到的数据] 197 | * @param [type] $order [订单信息数组] 198 | * @return [type] [description] 199 | */ 200 | public static function js($order=[]){ 201 | $config = self::$config; 202 | if (!is_array($order) || count($order) < 3) 203 | die("订单数组信息缺失!"); 204 | if (count($order) == 4 && !empty($order['openid'])) { 205 | $data = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息) 206 | return $data; 207 | } 208 | $code = !empty($order['code']) ? $order['code'] : ($_GET['code'] ?? ''); 209 | $redirectUri = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . rtrim($_SERVER['REQUEST_URI'], '/') . '/'; // 重定向地址 210 | $params = ['appid' => $config['appid']]; 211 | // 如果没有get参数没有code;则重定向去获取code; 212 | if (empty($code)) { 213 | $params['redirect_uri'] = $redirectUri; // 返回的url 214 | $params['response_type'] = 'code'; 215 | $params['scope'] = 'snsapi_base'; 216 | $params['state'] = $order['order_sn']; // 获取订单号 217 | 218 | $url = self::$authorizeUrl . '?'. http_build_query($params) .'#wechat_redirect'; 219 | } else { 220 | $params['secret'] = $config['appsecret']; 221 | $params['code'] = $code; 222 | $params['grant_type'] = 'authorization_code'; 223 | 224 | $response = Http::get(self::$accessTokenUrl, $params); // 进行GET请求 225 | $result = json_decode($response, true); 226 | $order['openid'] = $result['openid']; // 获取到的openid 227 | $data = self::xcx($order, false, false); // 获取支付相关信息(获取非小程序信息) 228 | 229 | if (!empty($order['code'])) { 230 | return $data; 231 | } 232 | $url = $config['redirect_url'] ?? $redirectUri; 233 | $url .= '?data=' . json_encode($data, JSON_UNESCAPED_UNICODE); 234 | } 235 | header('Location: '. $url); 236 | die; 237 | } 238 | 239 | /** 240 | * [app 获取APP支付需要用到的数据] 241 | * @param [type] $order [订单信息数组] 242 | * @return [type] [description] 243 | */ 244 | public static function app($order=[], $log=false) 245 | { 246 | if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){ 247 | die("订单数组信息缺失!"); 248 | } 249 | $order['type'] = 'app'; // 获取订单类型,用户拼接请求地址 250 | $result = self::unifiedOrder($order, true); 251 | if (!empty($result['prepay_id'])) { 252 | $data = array ( 253 | 'appId' => self::$config['appid'], // 微信开放平台审核通过的移动应用appid 254 | 'timeStamp' => (string)time(), 255 | 'nonceStr' => self::get_rand_str(32, 0, 1), // 随机32位字符串 256 | 'prepayid' => $result['prepay_id'], 257 | ); 258 | $data['paySign'] = self::makeSign($data); 259 | $data['partnerid'] = self::$config['mchid']; 260 | $data['package'] = 'Sign=WXPay'; 261 | return $data; // 数据小程序客户端 262 | } else { 263 | return $log ? $result : false; 264 | } 265 | } 266 | 267 | /** 268 | * [h5 微信H5支付] 269 | * @param [type] $order [订单信息数组] 270 | * @return [type] [description] 271 | */ 272 | public static function h5($order=[], $log=false) 273 | { 274 | $order['type'] = isset($order['type']) ? strtolower($order['type']) : 'wap'; 275 | if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || !in_array($order['type'], ['ios', 'android', 'wap'])){ 276 | die("订单数组信息缺失!"); 277 | } 278 | $result = self::unifiedOrder($order); 279 | if (!empty($result['h5_url'])) { 280 | return $result['h5_url']; // 返回链接让用户点击跳转 281 | } else { 282 | return $log ? $result : false; 283 | } 284 | } 285 | 286 | /** 287 | * [xcx 获取jssdk需要用到的数据] 288 | * @param [type] $order [订单信息数组] 289 | * @param boolean $log [description] 290 | * @param boolean $type [区分是否是小程序,默认 true] 291 | * @return [type] [description] 292 | */ 293 | public static function xcx($order=[], $log=false, $type=true) 294 | { 295 | if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['openid'])){ 296 | die("订单数组信息缺失!"); 297 | } 298 | $order['type'] = 'jsapi'; // 获取订单类型,用户拼接请求地址 299 | $config = self::$config; 300 | $result = self::unifiedOrder($order, $type); 301 | if (!empty($result['prepay_id'])) { 302 | $data = array ( 303 | 'appId' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的应用ID 304 | 'timeStamp' => (string)time(), 305 | 'nonceStr' => self::get_rand_str(32, 0, 1), // 随机32位字符串 306 | 'package' => 'prepay_id='.$result['prepay_id'], 307 | ); 308 | $data['paySign'] = self::makeSign($data); 309 | $data['signType'] = 'RSA'; 310 | return $data; // 数据小程序客户端 311 | } else { 312 | return $log ? $result : false; 313 | } 314 | } 315 | 316 | /** 317 | * [scan 微信扫码支付] 318 | * @param [type] $order [订单信息数组] 319 | * @return [type] [description] 320 | */ 321 | public static function scan($order=[], $log=false) 322 | { 323 | if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){ 324 | die("订单数组信息缺失!"); 325 | } 326 | $order['type'] = 'native'; // Native支付 327 | $result = self::unifiedOrder($order); 328 | 329 | if (!empty($result['code_url'])) { 330 | return urldecode($result['code_url']); // 返回链接扫码跳转 331 | } else { 332 | return $log ? $result : false; 333 | } 334 | } 335 | 336 | /** 337 | * [notify 回调验证] 338 | * @return [array] [返回数组格式的notify数据] 339 | */ 340 | public static function notify($is_verify = true) 341 | { 342 | $config = self::$config; 343 | $response = file_get_contents('php://input', 'r'); 344 | if ($is_verify) { // 是否进行签名验证 345 | $server = $_SERVER; 346 | if (empty($response) || empty($server['HTTP_WECHATPAY_SIGNATURE'])) 347 | return false; 348 | $body = [ 349 | 'timestamp' => $server['HTTP_WECHATPAY_TIMESTAMP'], 350 | 'nonce' => $server['HTTP_WECHATPAY_NONCE'], 351 | 'data' => $response, 352 | ]; 353 | // 验证应答签名 354 | $verifySign = self::verifySign($body, trim($server['HTTP_WECHATPAY_SIGNATURE']), trim($server['HTTP_WECHATPAY_SERIAL'])); 355 | if (!$verifySign) { 356 | throw new \Exception("[ 401 ] SIGN_ERROR 签名错误"); 357 | } 358 | } 359 | $result = json_decode($response, true); 360 | $event_type_array = ['TRANSACTION.SUCCESS', 'MCHTRANSFER.BILL.FINISHED']; 361 | if (empty($result['event_type']) || !in_array($result['event_type'], $event_type_array)) { 362 | return false; 363 | } 364 | // 加密信息 365 | $associatedData = $result['resource']['associated_data']; 366 | $nonceStr = $result['resource']['nonce']; 367 | $ciphertext = $result['resource']['ciphertext']; 368 | $data = $result['resource']['ciphertext'] = self::decryptToString($associatedData, $nonceStr, $ciphertext); 369 | 370 | return json_decode($data, true); 371 | } 372 | 373 | /** 374 | * [refund 微信支付退款] 375 | * @param [type] $order [订单信息] 376 | * @param [type] $type [是否是小程序] 377 | */ 378 | public static function refund($order) 379 | { 380 | $config = self::$config; 381 | if(empty($order['refund_sn']) || empty($order['refund_amount']) || (empty($order['order_sn']) && empty($order['transaction_id']))){ 382 | die("订单数组信息缺失!"); 383 | } 384 | $params = array( 385 | 'out_refund_no' => (string)$order['refund_sn'], // 商户退款单号 386 | 'funds_account' => 'AVAILABLE', // 退款资金来源 387 | 'amount' => [ 388 | 'refund' => (int)$order['refund_amount'], 389 | 'currency' => $order['currencyy'] ?? 'CNY', 390 | ] 391 | ); 392 | if (!empty($order['transaction_id'])) { 393 | $params['transaction_id'] = $order['transaction_id']; 394 | $orderDetail = self::query($order['transaction_id'], true); 395 | } else { 396 | $params['out_trade_no'] = $order['order_sn']; 397 | $orderDetail = self::query($order['order_sn']); 398 | } 399 | $params['amount']['total'] = (int)$orderDetail['amount']['total']; 400 | empty($order['reason']) || $params['reason'] = $order['reason']; 401 | empty($order['notify_url']) || $params['notify_url'] = $order['notify_url']; 402 | self::$facilitator && $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 403 | 404 | $url = self::$refundUrl; 405 | $header = self::createAuthorization($url, $params, 'POST'); 406 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 407 | $result = json_decode($response, true); 408 | 409 | return $result; 410 | } 411 | 412 | /** 413 | * [refundQuery 查询退款] 414 | * @param [type] $refundSn [退款单号] 415 | * @return [type] [description] 416 | */ 417 | public static function refundQuery($refundSn) 418 | { 419 | $config = self::$config; 420 | $url = self::$refundUrl . '/' . $refundSn; 421 | if (self::$facilitator) { 422 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 423 | } else { 424 | $params = ''; 425 | } 426 | 427 | $header = self::createAuthorization($url, $params, 'GET'); 428 | $response = Http::get($url, $params, $header); 429 | $result = json_decode($response, true); 430 | 431 | return $result; 432 | } 433 | 434 | /** 435 | * [transfer 商家转账] 436 | * @param array $order [订单相关信息] 437 | * @return [type] [description] 438 | */ 439 | public static function transfer($order = []) 440 | { 441 | $config = self::$config; 442 | if (empty($order['order_sn']) || empty($order['openid']) || empty($order['amount']) || empty($order['remark']) || empty($order['scene_id']) || empty($order['scene_info'])) 443 | die("订单数组信息缺失!"); 444 | 445 | if ($order['amount'] >= 200000 && empty($order['name'])) 446 | die("单笔金额大于两千,请填写用户姓名"); 447 | 448 | $params = array( 449 | 'appid' => $config['appid'] ?: $config['xcxid'], // 商户账号appid,企业号corpid即为此AppID) 450 | 'out_bill_no' => (string)$order['order_sn'], // 商户订单号 451 | 'transfer_scene_id' => (string)$order['scene_id'], // 转账场景ID 452 | 'openid' => (string)$order['openid'], // 商户订单号 453 | 454 | 'transfer_amount' => $order['amount'], // 转账金额(分) 455 | 'transfer_remark' => $order['remark'], // 转账备注(微信用户会收到该备注) 456 | 'transfer_scene_report_infos' => $order['scene_info'], // 转账场景下需报备的内容 457 | ); 458 | 459 | !empty($order['name']) && $params['user_name'] = self::getEncrypt($order['name']); // 收款方真实姓名 460 | !empty($order['notify_url']) && $params['notify_url'] = $order['notify_url']; // 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。 461 | !empty($order['body']) && $params['user_recv_perception'] = $order['body']; // 用户收款时感知到的收款原因将根据转账场景自动展示默认内容 462 | 463 | $url = self::$transferBillsUrl; 464 | self::$facilitator = false; // 关闭服务商模式 465 | $header = self::createAuthorization($url, $params, 'POST'); 466 | $header[] = 'Wechatpay-Serial: ' . ($config['public_key_id'] ?? self::certificates(false)); 467 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 468 | $result = json_decode($response, true); 469 | 470 | return $result; 471 | } 472 | 473 | /** 474 | * [transferCancel 撤销商家转账] 475 | * @param [type] $orderSn [商户单号] 476 | * @return [type] [description] 477 | */ 478 | public static function transferCancel($orderSn) 479 | { 480 | $url = self::$transferBillsUrl . '/out-bill-no/' . $orderSn . '/cancel'; 481 | 482 | $header = self::createAuthorization($url, $params = '', 'GET'); 483 | $response = Http::get($url, $params, $header); 484 | $result = json_decode($response, true); 485 | 486 | return $result; 487 | } 488 | 489 | /** 490 | * [transferQuery 查询商家转账] 491 | * @param [type] $orderSn [商户单号/微信批次单号] 492 | * @param boolean $type [是否为微信批次单号查询] 493 | * @return [type] [description] 494 | */ 495 | public static function transferQuery($orderSn, $type = false) 496 | { 497 | $url = self::$transferBillsUrl . ($type ? '/transfer-bill-no/' . $orderSn : '/out-bill-no/' . $orderSn); 498 | 499 | $header = self::createAuthorization($url, $params = '', 'GET'); 500 | $response = Http::get($url, $params, $header); 501 | $result = json_decode($response, true); 502 | 503 | return $result; 504 | } 505 | 506 | /** 507 | * [profitSharing 请求分账] 508 | * @param array $order [description] 509 | * @return [type] [description] 510 | */ 511 | public static function profitSharing($order = []) 512 | { 513 | $config = self::$config; 514 | if (empty($order['list']) && isset($order['openid']) && !empty($order['amount'])) 515 | $order['list'] = [['account' => $order['openid'], 'amount' => $order['amount']]]; 516 | if(empty($order['transaction_id']) || (empty($order['order_sn']) && empty($order['list']))){ 517 | die("订单数组信息缺失!"); 518 | } 519 | $list = []; 520 | foreach ($order['list'] as $k => $v) { 521 | $detail = []; 522 | if (empty($v['account']) || empty($v['amount'])) 523 | die("请填写分账详细信息!"); 524 | 525 | // 分账接收方类型 526 | $detail['type'] = isset($v['type']) ? $v['type'] : (mb_strlen($v['account']) != 28 ? 'MERCHANT_ID' : (self::$facilitator ? 'PERSONAL_SUB_OPENID' : 'PERSONAL_OPENID')); 527 | $detail['account'] = $v['account']; // 分账接收方账号 528 | !empty($v['name']) && $detail['user_name'] = self::getEncrypt($v['name']); // 分账个人接收方姓名 529 | $detail['amount'] = $v['amount']; // 分账金额 530 | $detail['description'] = $v['description'] ?? ($order['description'] ?? '商家发起分账'); // 分账描述 531 | 532 | $list[] = $detail; 533 | } 534 | 535 | $params = array( 536 | 'transaction_id' => $order['transaction_id'], // 微信订单号 537 | 'out_order_no' => (string)$order['order_sn'], // 商户分账单号 538 | 'receivers' => $list, // 分账接收方列表 539 | 'unfreeze_unsplit' => $order['unfreeze'] ?? true, // 是否解冻剩余未分资金 540 | ); 541 | 542 | if (self::$facilitator) { 543 | $params['appid'] = $config['sp_appid']; // 服务商应用ID 544 | $params['sub_appid'] = $config['appid'] ?: $config['xcxid']; // 子商户的应用ID 545 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 546 | } else { 547 | $params['appid'] = $config['appid'] ?: $config['xcxid']; // 商户账号appid 548 | } 549 | 550 | $url = self::$profitSharingUrl . '/orders'; 551 | $header = self::createAuthorization($url, $params, 'POST'); 552 | $header[] = 'Wechatpay-Serial: ' . ($config['public_key_id'] ?? self::certificates(false)); 553 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 554 | $result = json_decode($response, true); 555 | 556 | return $result; 557 | } 558 | 559 | /** 560 | * [transferQuery 解冻剩余资金] 561 | * @param [type] $order [商户单号及微信单号] 562 | * @return [type] [description] 563 | */ 564 | public static function profitsharingUnfreeze($order=[]) 565 | { 566 | if (empty($order['transaction_id']) || empty($order['order_sn'])) 567 | die("转账单号缺失"); 568 | 569 | $params['transaction_id'] = $order['transaction_id']; 570 | $params['out_order_no'] = $order['order_sn']; 571 | $params['description'] = $order['reason'] ?? '解冻全部剩余资金'; 572 | self::$facilitator && $params['sub_mchid'] = self::$config['mchid']; // 子商户的商户号 573 | 574 | $url = self::$profitSharingUrl . '/orders/unfreeze'; 575 | $header = self::createAuthorization($url, $params, 'POST'); 576 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 577 | $result = json_decode($response, true); 578 | 579 | return $result; 580 | } 581 | 582 | /** 583 | * [profitsharingQuery 查询分账结果/查询分账剩余金额] 584 | * @param [type] $order [分账单号等] 585 | * @return [type] [description] 586 | */ 587 | public static function profitsharingQuery($order = []) 588 | { 589 | if (is_array($order) && (empty($order['transaction_id']) || empty($order['order_sn']))) { 590 | die("支付单号缺失"); 591 | 592 | $params['transaction_id'] = $order['transaction_id']; 593 | self::$facilitator && $params['sub_mchid'] = self::$config['mchid']; // 子商户的商户号 594 | $url = self::$profitSharingUrl . '/orders/' . $order['order_sn']; 595 | } else { 596 | $params = ''; 597 | $url = self::$profitSharingUrl . '/transactions/' . $order . '/amounts'; 598 | } 599 | 600 | $header = self::createAuthorization($url, $params, 'GET'); 601 | $response = Http::get($url, $params, $header); 602 | $result = json_decode($response, true); 603 | 604 | return $result; 605 | } 606 | 607 | /** 608 | * [profitsharingReturn 请求分账回退] 609 | * @param [type] $account [分账接收方账号] 610 | * @param string $type [与分账方的关系类型] 611 | * @param string $name [分账个人接收方姓名] 612 | * @return [type] [description] 613 | */ 614 | public function profitsharingReturn($order = []) 615 | { 616 | $config = self::$config; 617 | 618 | if(empty($order['return_sn']) || empty($order['return_amount']) || (empty($order['order_sn']) && empty($order['order_id']))){ 619 | die("订单数组信息缺失!"); 620 | } 621 | $params = array( 622 | 'out_return_no' => (string)$order['return_sn'], // 商户回退单号 623 | 'return_mchid' => $order['return_mchid'], // 回退商户号 624 | 'amount' => $order['return_amount'], // 回退金额 625 | ); 626 | if (!empty($order['order_id'])) { // 微信分账单号 627 | $params['order_id'] = $order['order_id']; 628 | } else { // 商户分账单号 629 | $params['out_order_no'] = $order['order_sn']; 630 | } 631 | 632 | $params['description'] = $order['reason'] ?? '用户申请退款'; // 回退描述 633 | self::$facilitator && $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 634 | 635 | $url = self::$profitSharingUrl . '/return-orders'; 636 | $header = self::createAuthorization($url, $params, 'POST'); 637 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 638 | $result = json_decode($response, true); 639 | 640 | return $result; 641 | } 642 | 643 | /** 644 | * [receiversAdd 添加分账接收方] 645 | * @param [type] $account [分账接收方账号] 646 | * @param string $type [与分账方的关系类型] 647 | * @param string $name [分账个人接收方姓名] 648 | * @return [type] [description] 649 | */ 650 | public function receiversAdd($account, $type='USER', $name='') 651 | { 652 | $config = self::$config; 653 | 654 | $params['account'] = $account; // 分账接收方账号 655 | $name && $params['user_name'] = self::getEncrypt($name); // 分账个人接收方姓名 656 | 657 | $params['type'] = mb_strlen($account) != 28 ? 'MERCHANT_ID' : 'PERSONAL_OPENID'; // 分账接收方类型 658 | 659 | // 与分账方的关系类型 660 | if (in_array($type, ['STORE', 'STAFF', 'STORE_OWNER', 'PARTNER', 'HEADQUARTER', 'BRAND', 'DISTRIBUTOR', 'USER', 'SUPPLIER'])) { 661 | $params['relation_type'] = $type; 662 | } else { 663 | $params['relation_type'] = 'CUSTOM'; 664 | $params['custom_relation'] = $type; 665 | } 666 | 667 | if (self::$facilitator) { 668 | $params['type'] == 'PERSONAL_OPENID' && $params['type'] = 'PERSONAL_SUB_OPENID'; // 服务商跟换分账接收方类型 669 | $params['appid'] = $config['sp_appid']; // 服务商应用ID 670 | $params['sub_appid'] = $config['appid'] ?: $config['xcxid']; // 子商户的应用ID 671 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 672 | } else { 673 | $params['appid'] = $config['appid'] ?: $config['xcxid']; // 商户账号appid 674 | } 675 | 676 | $url = self::$profitSharingUrl . '/receivers/add'; 677 | $header = self::createAuthorization($url, $params, 'POST'); 678 | $header[] = 'Wechatpay-Serial: ' . ($config['public_key_id'] ?? self::certificates(false)); 679 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 680 | $result = json_decode($response, true); 681 | 682 | return $result; 683 | } 684 | 685 | /** 686 | * [receiversDelete 删除分账接收方] 687 | * @param [type] $account [分账接收方账号] 688 | * @param string $name [分账个人接收方姓名] 689 | * @return [type] [description] 690 | */ 691 | public function receiversDelete($account, $name='') 692 | { 693 | $config = self::$config; 694 | 695 | $params['account'] = $account; // 分账接收方账号 696 | $name && $params['user_name'] = self::getEncrypt($name); // 分账个人接收方姓名 697 | $params['type'] = mb_strlen($account) != 28 ? 'MERCHANT_ID' : 'PERSONAL_OPENID'; // 分账接收方类型 698 | 699 | if (self::$facilitator) { 700 | $params['type'] == 'PERSONAL_OPENID' && $params['type'] = 'PERSONAL_SUB_OPENID'; // 服务商跟换分账接收方类型 701 | $params['appid'] = $config['sp_appid']; // 服务商应用ID 702 | $params['sub_appid'] = $config['appid'] ?: $config['xcxid']; // 子商户的应用ID 703 | $params['sub_mchid'] = $config['mchid']; // 子商户的商户号 704 | } else { 705 | $params['appid'] = $config['appid'] ?: $config['xcxid']; // 商户账号appid 706 | } 707 | 708 | $url = self::$profitSharingUrl . '/receivers/delete'; 709 | $header = self::createAuthorization($url, $params, 'POST'); 710 | $response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header); 711 | $result = json_decode($response, true); 712 | 713 | return $result; 714 | } 715 | 716 | /** 717 | * [success 通知支付状态] 718 | */ 719 | public static function success() 720 | { 721 | $str = ['code'=>'SUCCESS', 'message'=>'成功']; 722 | die(json_encode($str, JSON_UNESCAPED_UNICODE)); 723 | } 724 | 725 | /** 726 | * [createAuthorization 获取接口授权header头信息] 727 | * @param [type] $url [请求地址] 728 | * @param array $params [请求参数] 729 | * @param string $method [请求方式] 730 | * @return [type] [description] 731 | */ 732 | // 生成v3 Authorization 733 | public static function createAuthorization($url, $params=[], $method='POST') { 734 | $config = self::$config; 735 | // 商户号(服务商模式使用服务商商户号) 736 | $mchid = self::$facilitator ? $config['sp_mchid'] : $config['mchid']; 737 | $method = strtoupper($method); 738 | // 证书序列号 739 | if (empty($config['serial_no'])) { 740 | $certFile = @file_get_contents($config['cert_client']); 741 | $certArr = openssl_x509_parse($certFile); 742 | $serial_no = $certArr['serialNumberHex']; 743 | } else { 744 | $serial_no = $config['serial_no']; 745 | } 746 | // 解析url地址 747 | $url_parts = parse_url($url); 748 | $url = $url_parts['path'] . (!empty($url_parts['query']) ? '?'.$url_parts['query'] : ""); 749 | if (in_array($method, ['GET'])) { 750 | $query_string = ($params && is_array($params)) ? http_build_query($params) : $params; 751 | $url = $query_string ? $url . (stripos($url, "?") !== false ? "&" : "?") . $query_string : $url; 752 | $params = ''; 753 | } 754 | // 生成签名 755 | $body = [ 756 | 'method' => $method, 757 | 'url' => $url, 758 | 'time' => time(), // 当前时间戳 759 | 'nonce' => self::get_rand_str(32, 0, 1), // 随机32位字符串 760 | 'data' => is_array($params) ? json_encode($params, JSON_UNESCAPED_UNICODE) : $params, // POST/PUT/DELETE请求时 需要 转JSON字符串 761 | ]; 762 | $sign = self::makeSign($body); 763 | // Authorization 类型 764 | $schema = 'WECHATPAY2-SHA256-RSA2048'; 765 | // 生成token 766 | $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $mchid, $body['nonce'], $body['time'], $serial_no, $sign); 767 | 768 | $header = [ 769 | 'Content-Type:application/json', 770 | 'Accept:application/json', 771 | 'User-Agent:*/*', 772 | 'Authorization: '. $schema . ' ' . $token 773 | ]; 774 | return $header; 775 | } 776 | 777 | /** 778 | * [makeSign 生成签名] 779 | * @param [type] $data [加密数据] 780 | * @return [type] [description] 781 | */ 782 | public static function makeSign($data) 783 | { 784 | $config = self::$config; 785 | if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { 786 | throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); 787 | } 788 | // 拼接生成签名所需的字符串 789 | $message = ''; 790 | foreach ($data as $value) { 791 | $message .= $value . "\n"; 792 | } 793 | // 商户私钥 794 | $private_key = self::getPrivateKey($config['cert_key']); 795 | // 生成签名 796 | openssl_sign($message, $sign, $private_key, 'sha256WithRSAEncryption'); 797 | $sign = base64_encode($sign); 798 | return $sign; 799 | } 800 | 801 | /** 802 | * [verifySign 验证签名] 803 | * @param [type] $data [description] 804 | * @param [type] $sign [description] 805 | * @param [type] $serial [description] 806 | * @return [type] [description] 807 | */ 808 | public static function verifySign($data, $sign, $serial) 809 | { 810 | $config = self::$config; 811 | if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { 812 | throw new \RuntimeException("当前PHP环境不支持SHA256withRSA"); 813 | } 814 | $sign = \base64_decode($sign); 815 | // 拼接生成签名所需的字符串 816 | $message = ''; 817 | foreach ($data as $value) { 818 | $message .= $value . "\n"; 819 | } 820 | // 获取证书相关信息(平台公钥) 821 | $publicKey = self::certificates(true, $serial); 822 | // 验证签名 823 | $recode = \openssl_verify($message, $sign, $publicKey, 'sha256WithRSAEncryption'); 824 | return $recode == 1 ? true : false; 825 | } 826 | 827 | //获取私钥 828 | public static function getPrivateKey($filepath) 829 | { 830 | return openssl_pkey_get_private(file_get_contents($filepath)); 831 | } 832 | 833 | //获取公钥 834 | public static function getPublicKey($filepath) 835 | { 836 | return openssl_pkey_get_public(file_get_contents($filepath)); 837 | } 838 | 839 | /** 840 | * [certificates 获取证书] 841 | * @return [type] [description] 842 | */ 843 | public static function certificates($type = true, $serial = '') 844 | { 845 | $config = self::$config; 846 | 847 | $publicKey = @file_get_contents($config['public_key']); 848 | if ($publicKey) { // 判断证书是否存在 849 | $certificate = openssl_x509_parse($publicKey); 850 | if ($certificate['validTo_time_t'] > time()) { // 是否是所需证书 851 | if ($serial && $certificate['serialNumberHex'] != $serial) { 852 | throw new \Exception("[ 401 ] 微信支付公钥ID或平台证书序列号匹配失败"); 853 | } 854 | return $type ? $publicKey : $certificate['serialNumberHex']; // 返回证书信息 855 | } 856 | } 857 | 858 | $url = self::$certificatesUrl; 859 | $params = ''; 860 | 861 | $header = self::createAuthorization($url, $params, 'GET'); 862 | $response = Http::get($url, $params, $header); 863 | $result = json_decode($response, true); 864 | if (empty($result['data'])) { 865 | throw new \Exception("[" . $result['code'] . "] " . $result['message'] . ",请使用微信支付公钥验签及数据加密"); 866 | } 867 | foreach ($result['data'] as $key => $certificate) { 868 | if (strtotime($certificate['expire_time']) > time()) { 869 | $publicKey = self::decryptToString( 870 | $certificate['encrypt_certificate']['associated_data'], 871 | $certificate['encrypt_certificate']['nonce'], 872 | $certificate['encrypt_certificate']['ciphertext'] 873 | ); 874 | 875 | if ($publicKey) { // 生成public_key证书文件 876 | file_put_contents($config['public_key'], $publicKey); 877 | return $type ? $publicKey : $certificate['serial_no']; // 返回证书信息 878 | break; // 终止循环 879 | } else { 880 | throw new \Exception("[ 404 ] public_key 生成失败,加密字符串解析为空,请检查配置 key 是否匹配"); 881 | } 882 | } 883 | } 884 | } 885 | 886 | /** 887 | * [getEncrypt 将字符串信息进行加密] 888 | * @param [type] $str [description] 889 | * @return [type] [description] 890 | */ 891 | public static function getEncrypt($str) { 892 | //$str是待加密字符串 893 | $publicKey = self::certificates(); 894 | $encrypted = ''; 895 | if (openssl_public_encrypt($str, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING)) { 896 | //base64编码 897 | $sign = base64_encode($encrypted); 898 | } else { 899 | throw new Exception('encrypt failed'); 900 | } 901 | return $sign; 902 | } 903 | 904 | /** 905 | * [decryptToString 证书和回调报文解密] 906 | * @param [type] $associatedData [附加数据包(可能为空)] 907 | * @param [type] $nonceStr [加密使用的随机串初始化向量] 908 | * @param [type] $ciphertext [Base64编码后的密文] 909 | * @return [type] [description] 910 | */ 911 | public static function decryptToString($associatedData, $nonceStr, $ciphertext) 912 | { 913 | $config = self::$config; 914 | $ciphertext = base64_decode($ciphertext); 915 | if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) { 916 | return false; 917 | } 918 | 919 | // ext-sodium (default installed on >= PHP 7.2) 920 | if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && 921 | \sodium_crypto_aead_aes256gcm_is_available()) { 922 | return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']); 923 | } 924 | 925 | // ext-libsodium (need install libsodium-php 1.x via pecl) 926 | if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && 927 | \Sodium\crypto_aead_aes256gcm_is_available()) { 928 | return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']); 929 | } 930 | 931 | // openssl (PHP >= 7.1 support AEAD) 932 | if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { 933 | $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE); 934 | $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE); 935 | 936 | return \openssl_decrypt($ctext, 'aes-256-gcm', $config['key'], \OPENSSL_RAW_DATA, $nonceStr, 937 | $authTag, $associatedData); 938 | } 939 | 940 | throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php'); 941 | } 942 | 943 | /** fengkui.net 944 | * [get_rand_str 获取随机字符串] 945 | * @param integer $randLength [长度] 946 | * @param integer $addtime [是否加入当前时间戳] 947 | * @param integer $includenumber [是否包含数字] 948 | * @return [type] [description] 949 | */ 950 | public static function get_rand_str($randLength=6, $addtime=0, $includenumber=1) 951 | { 952 | if ($includenumber) 953 | $chars='abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789'; 954 | $chars='abcdefghijklmnopqrstuvwxyz'; 955 | 956 | $len = strlen($chars); 957 | $randStr = ''; 958 | for ($i=0; $i<$randLength; $i++){ 959 | $randStr .= $chars[rand(0, $len-1)]; 960 | } 961 | $tokenvalue = $randStr; 962 | $addtime && $tokenvalue = $randStr . time(); 963 | return $tokenvalue; 964 | } 965 | 966 | /** fengkui.net 967 | * [get_ip 定义一个函数get_ip() 客户端IP] 968 | * @return [type] [description] 969 | */ 970 | public static function get_ip() 971 | { 972 | if (getenv("HTTP_CLIENT_IP")) 973 | $ip = getenv("HTTP_CLIENT_IP"); 974 | else if(getenv("HTTP_X_FORWARDED_FOR")) 975 | $ip = getenv("HTTP_X_FORWARDED_FOR"); 976 | else if(getenv("REMOTE_ADDR")) 977 | $ip = getenv("REMOTE_ADDR"); 978 | else $ip = "Unknow"; 979 | 980 | if(preg_match('/^((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1 -9]?\d))))$/', $ip)) 981 | return $ip; 982 | else 983 | return ''; 984 | } 985 | } 986 | --------------------------------------------------------------------------------