├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── composer.json ├── init.php ├── src ├── Contracts │ ├── Config.php │ ├── GatewayInterface.php │ └── HttpService.php ├── Exceptions │ ├── Exception.php │ ├── GatewayException.php │ └── InvalidArgumentException.php ├── Gateways │ ├── Alipay │ │ ├── Alipay.php │ │ ├── AppGateway.php │ │ ├── PosGateway.php │ │ ├── ScanGateway.php │ │ ├── TransferGateway.php │ │ ├── WapGateway.php │ │ └── WebGateway.php │ └── Wechat │ │ ├── AppGateway.php │ │ ├── MiniappGateway.php │ │ ├── MpGateway.php │ │ ├── PosGateway.php │ │ ├── ScanGateway.php │ │ ├── TransferGateway.php │ │ ├── WapGateway.php │ │ └── Wechat.php └── Pay.php └── test ├── alipay-app.php ├── alipay-notify.php ├── alipay-pos.php ├── alipay-scan.php ├── alipay-transfer.php ├── alipay-wap.php ├── alipay-web.php ├── config.php ├── wechat-refund.php ├── wxpay-app.php ├── wxpay-miniapp.php ├── wxpay-mp.php ├── wxpay-notify.php ├── wxpay-pos.php ├── wxpay-scan.php ├── wxpay-transfer.php └── wxpay-wap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.git 3 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2017 Anyon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pay-php-sdk 2 | PHP聚合支付SDK(微信支付 + 支付宝支付) 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/zoujingli/pay-php-sdk/v/stable)](https://packagist.org/packages/zoujingli/pay-php-sdk) 5 | [![Total Downloads](https://poser.pugx.org/zoujingli/pay-php-sdk/downloads)](https://packagist.org/packages/zoujingli/pay-php-sdk) 6 | [![Latest Unstable Version](https://poser.pugx.org/zoujingli/pay-php-sdk/v/unstable)](https://packagist.org/packages/zoujingli/pay-php-sdk) 7 | [![License](https://poser.pugx.org/zoujingli/pay-php-sdk/license)](https://packagist.org/packages/zoujingli/pay-php-sdk) 8 | 9 | 欢迎 Star,欢迎 Fork! 10 | 11 | ## 特点 12 | - 代码简洁,无需加载多余组件,可应用于任何平台或框架 13 | - 隐藏开发者不需要关注的细节,完全内部实现 14 | - 根据支付宝、微信最新 API 开发集成 15 | - 高度抽象的类,免去各种拼json与xml的痛苦 16 | - 符合 PSR 标准,你可以各种方便的与你的框架集成 17 | - 文件结构清晰易理解,可以随心所欲添加本项目中没有的支付网关 18 | - 方法使用更优雅,不必再去研究那些奇怪的的方法名或者类名是做啥用的 19 | 20 | ## 声明 21 | - 代码与框架部分参考于互联网开源项目 22 | - SDK全部源码基于MIT协议开源,完全免费 23 | 24 | ## 环境 25 | - PHP 5.3+ 26 | - composer 27 | 28 | ## 配置 29 | ```php 30 | $config = [ 31 | // 微信支付参数 32 | 'wechat' => [ 33 | 'app_id' => '', // 应用ID 34 | 'mch_id' => '', // 微信支付商户号 35 | 'mch_key' => '', // 微信支付密钥 36 | 'ssl_cer' => '', // 微信证书 cert 文件 37 | 'ssl_key' => '', // 微信证书 key 文件 38 | 'notify_url' => '', // 支付通知URL 39 | ], 40 | // 支付宝支付参数 41 | 'alipay' => [ 42 | 'app_id' => '', // 应用ID 43 | 'public_key' => '', // 支付宝公钥(1行填写) 44 | 'private_key' => '', // 支付宝私钥(1行填写) 45 | 'notify_url' => '', // 支付通知URL 46 | ] 47 | ]; 48 | ``` 49 | 50 | ## 架构 51 | 52 | 由于各支付网关参差不齐,所以我们抽象了两个方法 `driver()`,`gateway()`。 53 | 54 | 两个方法的作用如下: 55 | 56 | `driver()` : 确定支付平台,如 `alipay`,`wechat`; 57 | `gateway()`: 确定支付网关,如 `app`,`pos`,`scan`,`transfer`,`wap`,`...` 58 | 59 | 具体实现可以查看源代码。 60 | 61 | ### 1、支付宝 62 | 63 | SDK 中对应的 driver 和 gateway 如下表所示: 64 | 65 | | driver | gateway | 描述 | 66 | | :----: | :-----: | :-------: | 67 | | alipay | web | 电脑支付 | 68 | | alipay | wap | 手机网站支付 | 69 | | alipay | app | APP 支付 | 70 | | alipay | pos | 刷卡支付 | 71 | | alipay | scan | 扫码支付 | 72 | | alipay | transfer   | 帐户转账(可用于平台用户提现) | 73 | 74 | ### 2、微信 75 | 76 | SDK 中对应的 driver 和 gateway 如下表所示: 77 | 78 | | driver | gateway | 描述 | 79 | | :----: | :-----: | :-------: | 80 | | wechat | mp | 公众号支付 | 81 | | wechat | miniapp | 小程序支付 | 82 | | wechat | wap | H5 支付 | 83 | | wechat | scan | 扫码支付 | 84 | | wechat | pos | 刷卡支付 | 85 | | wechat | app | APP 支付 | 86 | | wechat | transfer | 企业付款 | 87 | 88 | ## 操作 89 | 90 | 所有网关均支持以下方法 91 | 92 | - pay(array $options) 93 | 说明:支付接口 94 | 参数:数组类型,订单业务配置项,包含 订单号,订单金额等 95 | 返回:mixed 详情请看「支付网关配置说明与返回值」一节。 96 | 97 | - refund(array|string $options, $refund_amount = null) 98 | 说明:退款接口 99 | 参数:`$options` 为字符串类型仅对`支付宝支付`有效,此时代表订单号,第二个参数为退款金额。 100 | 返回:mixed 退款成功,返回 服务器返回的数组;否则返回 false; 101 | 102 | - close(array|string $options) 103 | 说明:关闭订单接口 104 | 参数:`$options` 为字符串类型时代表订单号,如果为数组,则为关闭订单业务配置项,配置项内容请参考各个支付网关官方文档。 105 | 返回:mixed 关闭订单成功,返回 服务器返回的数组;否则返回 false; 106 | 107 | - find(string $out_trade_no) 108 | 说明:查找订单接口 109 | 参数:`$out_trade_no` 为订单号。 110 | 返回:mixed 查找订单成功,返回 服务器返回的数组;否则返回 false; 111 | 112 | - verify($data, $sign = null) 113 | 说明:验证服务器返回消息是否合法 114 | 参数:`$data` 为服务器接收到的原始内容,`$sign` 为签名信息,当其为空时,系统将自动转化 `$data` 为数组,然后取 `$data['sign']`。 115 | 返回:mixed 验证成功,返回 服务器返回的数组;否则返回 false; 116 | 117 | ## 实例 118 | ```php 119 | // 实例支付对象 120 | $pay = new \Pay\Pay($config); 121 | 122 | try { 123 | $options = $pay->driver('alipay')->gateway('app')->apply($payOrder); 124 | var_dump($options); 125 | } catch (Exception $e) { 126 | echo "创建订单失败," . $e->getMessage(); 127 | } 128 | ``` 129 | 130 | ## 通知 131 | 132 | #### 支付宝 133 | ```php 134 | // 实例支付对象 135 | $pay = new \Pay\Pay($config); 136 | 137 | if ($pay->driver('alipay')->gateway()->verify($_POST)) { 138 | file_put_contents('notify.txt', "收到来自支付宝的异步通知\r\n", FILE_APPEND); 139 | file_put_contents('notify.txt', "订单单号:{$_POST['out_trade_no']}\r\n", FILE_APPEND); 140 | file_put_contents('notify.txt', "订单金额:{$_POST['total_amount']}\r\n\r\n", FILE_APPEND); 141 | } else { 142 | file_put_contents('notify.txt', "收到异步通知\r\n", FILE_APPEND); 143 | } 144 | ``` 145 | 146 | #### 微信 147 | ```php 148 | $pay = new \Pay\Pay($config); 149 | $verify = $pay->driver('wechat')->gateway('mp')->verify(file_get_contents('php://input')); 150 | 151 | if ($verify) { 152 | file_put_contents('notify.txt', "收到来自微信的异步通知\r\n", FILE_APPEND); 153 | file_put_contents('notify.txt', "订单单号:{$verify['out_trade_no']}\r\n", FILE_APPEND); 154 | file_put_contents('notify.txt', "订单金额:{$verify['total_fee']}\r\n\r\n", FILE_APPEND); 155 | } else { 156 | file_put_contents('notify.txt', "收到异步通知\r\n", FILE_APPEND); 157 | } 158 | 159 | echo "success"; 160 | ``` 161 | 162 | ## 安装 163 | ```shell 164 | // 方法一、 使用composer安装 165 | composer require zoujingli/pay-php-sdk 166 | 167 | // 方法二、 直接加载支付SDK 168 | include 'init.php' 169 | ``` -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "library", 3 | "name": "zoujingli/pay-php-sdk", 4 | "homepage": "https://github.com/zoujingli/pay-php-sdk", 5 | "description": "WxPay and AliPay development of SDK", 6 | "license": "MIT", 7 | "keywords": [ 8 | "pay-php-sdk", 9 | "alipay", 10 | "wxpay" 11 | ], 12 | "require": { 13 | "php": ">=5.3.3", 14 | "ext-curl": "*" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Pay\\": "./src" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | config = $config; 38 | } 39 | 40 | /** 41 | * 获取配置选项 42 | * @param null|string $key 43 | * @param null|string $default 44 | * @return array|mixed|null 45 | */ 46 | public function get($key = null, $default = null) 47 | { 48 | $config = $this->config; 49 | if (is_null($key)) { 50 | return $config; 51 | } 52 | if (isset($config[$key])) { 53 | return $config[$key]; 54 | } 55 | foreach (explode('.', $key) as $segment) { 56 | if (!is_array($config) || !array_key_exists($segment, $config)) { 57 | return $default; 58 | } 59 | $config = $config[$segment]; 60 | } 61 | return $config; 62 | } 63 | 64 | /** 65 | * 设置配置选项 66 | * @param string $key 67 | * @param string $value 68 | * @return array 69 | */ 70 | public function set($key, $value) 71 | { 72 | if ($key == '') { 73 | throw new InvalidArgumentException('Invalid config key.'); 74 | } 75 | // 只支持三维数组,多余无意义 76 | $keys = explode('.', $key); 77 | switch (count($keys)) { 78 | case '1': 79 | $this->config[$key] = $value; 80 | break; 81 | case '2': 82 | $this->config[$keys[0]][$keys[1]] = $value; 83 | break; 84 | case '3': 85 | $this->config[$keys[0]][$keys[1]][$keys[2]] = $value; 86 | break; 87 | default: 88 | throw new InvalidArgumentException('Invalid config key.'); 89 | } 90 | return $this->config; 91 | } 92 | 93 | /** 94 | * 判断是否有配置 95 | * @param string $offset 96 | * @return bool 97 | */ 98 | public function offsetExists($offset) 99 | { 100 | return array_key_exists($offset, $this->config); 101 | } 102 | 103 | /** 104 | * 获取配置对象 105 | * @param string $offset 106 | * @return array|mixed|null 107 | */ 108 | public function offsetGet($offset) 109 | { 110 | return $this->get($offset); 111 | } 112 | 113 | /** 114 | * 设置配置对象 115 | * @param string $offset 116 | * @param array|mixed|null $value 117 | */ 118 | public function offsetSet($offset, $value) 119 | { 120 | $this->set($offset, $value); 121 | } 122 | 123 | /** 124 | * 清除设置对象 125 | * @param string $offset 126 | */ 127 | public function offsetUnset($offset) 128 | { 129 | $this->set($offset, null); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Contracts/GatewayInterface.php: -------------------------------------------------------------------------------- 1 | $value) { 103 | if (is_string($value) && class_exists('CURLFile', false) && stripos($value, '@') === 0) { 104 | $filename = realpath(trim($value, '@')); 105 | if ($filename && file_exists($filename)) { 106 | $data[$key] = new \CURLFile($filename); 107 | } 108 | } 109 | } 110 | return $data; 111 | } 112 | } -------------------------------------------------------------------------------- /src/Exceptions/Exception.php: -------------------------------------------------------------------------------- 1 | raw = $raw; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | userConfig = new Config($config); 53 | if (is_null($this->userConfig->get('app_id'))) { 54 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 55 | } 56 | $this->config = [ 57 | 'app_id' => $this->userConfig->get('app_id'), 58 | 'method' => '', 59 | 'format' => 'JSON', 60 | 'charset' => 'utf-8', 61 | 'sign_type' => 'RSA2', 62 | 'version' => '1.0', 63 | 'return_url' => $this->userConfig->get('return_url', ''), 64 | 'notify_url' => $this->userConfig->get('notify_url', ''), 65 | 'timestamp' => date('Y-m-d H:i:s'), 66 | 'sign' => '', 67 | 'biz_content' => '', 68 | ]; 69 | } 70 | 71 | /** 72 | * 应用参数 73 | * @param array $options 74 | * @return mixed|void 75 | */ 76 | public function apply(array $options) 77 | { 78 | $options['product_code'] = $this->getProductCode(); 79 | $this->config['method'] = $this->getMethod(); 80 | $this->config['biz_content'] = json_encode($options); 81 | $this->config['sign'] = $this->getSign(); 82 | } 83 | 84 | /** 85 | * 支付宝订单退款操作 86 | * @param array|string $options 退款参数或退款商户订单号 87 | * @param null $refund_amount 退款金额 88 | * @return array|bool 89 | */ 90 | public function refund($options, $refund_amount = null) 91 | { 92 | if (!is_array($options)) { 93 | $options = ['out_trade_no' => $options, 'refund_amount' => $refund_amount]; 94 | } 95 | return $this->getResult($options, 'alipay.trade.refund'); 96 | } 97 | 98 | /** 99 | * 关闭支付宝进行中的订单 100 | * @param $options 101 | * @return array|bool 102 | */ 103 | public function close($options) 104 | { 105 | if (!is_array($options)) { 106 | $options = ['out_trade_no' => $options]; 107 | } 108 | return $this->getResult($options, 'alipay.trade.close'); 109 | } 110 | 111 | /** 112 | * 查询支付宝订单状态 113 | * @param string $out_trade_no 114 | * @return array|bool 115 | */ 116 | public function find($out_trade_no = '') 117 | { 118 | $options = ['out_trade_no' => $out_trade_no]; 119 | return $this->getResult($options, 'alipay.trade.query'); 120 | } 121 | 122 | /** 123 | * 验证支付宝支付宝通知 124 | * @param array $data 通知数据 125 | * @param null $sign 数据签名 126 | * @param bool $sync 127 | * @return array|bool 128 | */ 129 | public function verify($data, $sign = null, $sync = false) 130 | { 131 | if (is_null($this->userConfig->get('public_key'))) { 132 | throw new InvalidArgumentException('Missing Config -- [public_key]'); 133 | } 134 | $sign = is_null($sign) ? $data['sign'] : $sign; 135 | $res = "-----BEGIN PUBLIC KEY-----\n" . 136 | wordwrap($this->userConfig->get('public_key'), 64, "\n", true) . 137 | "\n-----END PUBLIC KEY-----"; 138 | $toVerify = $sync ? json_encode($data) : $this->getSignContent($data, true); 139 | return openssl_verify($toVerify, base64_decode($sign), $res, OPENSSL_ALGO_SHA256) === 1 ? $data : false; 140 | } 141 | 142 | /** 143 | * @return string 144 | */ 145 | abstract protected function getMethod(); 146 | 147 | /** 148 | * @return string 149 | */ 150 | abstract protected function getProductCode(); 151 | 152 | /** 153 | * @return string 154 | */ 155 | protected function buildPayHtml() 156 | { 157 | $sHtml = "
"; 158 | while (list($key, $val) = each($this->config)) { 159 | $val = str_replace("'", ''', $val); 160 | $sHtml .= ""; 161 | } 162 | $sHtml .= "
"; 163 | $sHtml .= ""; 164 | return $sHtml; 165 | } 166 | 167 | /** 168 | * 获取验证访问数据 169 | * @param array $options 170 | * @param string $method 171 | * @return array|bool 172 | * @throws GatewayException 173 | */ 174 | protected function getResult($options, $method) 175 | { 176 | $this->config['biz_content'] = json_encode($options); 177 | $this->config['method'] = $method; 178 | $this->config['sign'] = $this->getSign(); 179 | $method = str_replace('.', '_', $method) . '_response'; 180 | $data = json_decode($this->post($this->gateway, $this->config), true); 181 | if (!isset($data[$method]['code']) || $data[$method]['code'] !== '10000') { 182 | throw new GatewayException('GetResultError:' . $data[$method]['msg'] . ' - ' . $data[$method]['sub_code'], $data[$method]['code'], $data); 183 | } 184 | return $this->verify($data[$method], $data['sign'], true); 185 | } 186 | 187 | /** 188 | * 获取数据签名 189 | * @return string 190 | */ 191 | protected function getSign() 192 | { 193 | if (is_null($this->userConfig->get('private_key'))) { 194 | throw new InvalidArgumentException('Missing Config -- [private_key]'); 195 | } 196 | $res = "-----BEGIN RSA PRIVATE KEY-----\n" . 197 | wordwrap($this->userConfig->get('private_key'), 64, "\n", true) . 198 | "\n-----END RSA PRIVATE KEY-----"; 199 | openssl_sign($this->getSignContent($this->config), $sign, $res, OPENSSL_ALGO_SHA256); 200 | return base64_encode($sign); 201 | } 202 | 203 | /** 204 | * 数据签名处理 205 | * @param array $toBeSigned 206 | * @param bool $verify 207 | * @return bool|string 208 | */ 209 | protected function getSignContent(array $toBeSigned, $verify = false) 210 | { 211 | ksort($toBeSigned); 212 | $stringToBeSigned = ''; 213 | foreach ($toBeSigned as $k => $v) { 214 | if ($verify && $k != 'sign' && $k != 'sign_type') { 215 | $stringToBeSigned .= $k . '=' . $v . '&'; 216 | } 217 | if (!$verify && $v !== '' && !is_null($v) && $k != 'sign' && '@' != substr($v, 0, 1)) { 218 | $stringToBeSigned .= $k . '=' . $v . '&'; 219 | } 220 | } 221 | $stringToBeSigned = substr($stringToBeSigned, 0, -1); 222 | unset($k, $v); 223 | return $stringToBeSigned; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/AppGateway.php: -------------------------------------------------------------------------------- 1 | config); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/PosGateway.php: -------------------------------------------------------------------------------- 1 | getResult($options, $this->getMethod()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/ScanGateway.php: -------------------------------------------------------------------------------- 1 | getResult($options, $this->getMethod()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/TransferGateway.php: -------------------------------------------------------------------------------- 1 | getResult($options, $this->getMethod()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/WapGateway.php: -------------------------------------------------------------------------------- 1 | buildPayHtml(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Gateways/Alipay/WebGateway.php: -------------------------------------------------------------------------------- 1 | buildPayHtml(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/AppGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 42 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 43 | } 44 | $payRequest = [ 45 | 'appid' => $this->userConfig->get('app_id'), 46 | 'partnerid' => $this->userConfig->get('mch_id'), 47 | 'prepayid' => $this->preOrder($options)['prepay_id'], 48 | 'timestamp' => time() . '', 49 | 'noncestr' => $this->createNonceStr(), 50 | 'package' => 'Sign=WXPay', 51 | ]; 52 | $payRequest['sign'] = $this->getSign($payRequest); 53 | return $payRequest; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/MiniappGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 42 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 43 | } 44 | $this->config['appid'] = $this->userConfig->get('app_id'); 45 | $payRequest = [ 46 | 'appId' => $this->config['appid'], 47 | 'timeStamp' => time() . '', 48 | 'nonceStr' => $this->createNonceStr(), 49 | 'package' => 'prepay_id=' . $this->preOrder($options)['prepay_id'], 50 | 'signType' => 'MD5', 51 | ]; 52 | $payRequest['paySign'] = $this->getSign($payRequest); 53 | return $payRequest; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/MpGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 41 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 42 | } 43 | $payRequest = [ 44 | 'appId' => $this->userConfig->get('app_id'), 45 | 'timeStamp' => time() . '', 46 | 'nonceStr' => $this->createNonceStr(), 47 | 'package' => 'prepay_id=' . $this->preOrder($options)['prepay_id'], 48 | 'signType' => 'MD5', 49 | ]; 50 | $payRequest['paySign'] = $this->getSign($payRequest); 51 | return $payRequest; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/PosGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 47 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 48 | } 49 | unset($this->config['trade_type']); 50 | unset($this->config['notify_url']); 51 | return $this->preOrder($options); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/ScanGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 42 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 43 | } 44 | return $this->preOrder($options)['code_url']; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/TransferGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 49 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 50 | } 51 | $options['mchid'] = $this->config['mch_id']; 52 | $options['mch_appid'] = $this->userConfig->get('app_id'); 53 | unset($this->config['appid']); 54 | unset($this->config['mch_id']); 55 | unset($this->config['sign_type']); 56 | unset($this->config['trade_type']); 57 | unset($this->config['notify_url']); 58 | $this->config = array_merge($this->config, $options); 59 | $this->config['sign'] = $this->getSign($this->config); 60 | $data = $this->fromXml($this->post( 61 | $this->gateway, 62 | $this->toXml($this->config), 63 | ['ssl_cer' => $this->userConfig->get('ssl_cer', ''), 'ssl_key' => $this->userConfig->get('ssl_key', '')] 64 | )); 65 | if (!isset($data['return_code']) || $data['return_code'] !== 'SUCCESS' || $data['result_code'] !== 'SUCCESS') { 66 | $error = 'GetResultError:' . $data['return_msg']; 67 | $error .= isset($data['err_code_des']) ? ' - ' . $data['err_code_des'] : ''; 68 | } 69 | if (isset($error)) { 70 | throw new GatewayException($error, 20000, $data); 71 | } 72 | return $data; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/WapGateway.php: -------------------------------------------------------------------------------- 1 | userConfig->get('app_id'))) { 42 | throw new InvalidArgumentException('Missing Config -- [app_id]'); 43 | } 44 | list($data, $return_url) = [$this->preOrder($options), $this->userConfig->get('return_url')]; 45 | return is_null($return_url) ? $data['mweb_url'] : $data['mweb_url'] . '&redirect_url=' . urlencode($return_url); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Gateways/Wechat/Wechat.php: -------------------------------------------------------------------------------- 1 | userConfig = new Config($config); 65 | $this->config = [ 66 | 'appid' => $this->userConfig->get('app_id', ''), 67 | 'mch_id' => $this->userConfig->get('mch_id', ''), 68 | 'nonce_str' => $this->createNonceStr(), 69 | 'sign_type' => 'MD5', 70 | 'notify_url' => $this->userConfig->get('notify_url', ''), 71 | 'trade_type' => $this->getTradeType(), 72 | ]; 73 | } 74 | 75 | /** 76 | * 订单退款操作 77 | * @param array $options 78 | * @return array 79 | */ 80 | public function refund($options = []) 81 | { 82 | $this->config = array_merge($this->config, $options); 83 | $this->config['op_user_id'] = isset($this->config['op_user_id']) ?: $this->userConfig->get('mch_id', ''); 84 | $this->unsetTradeTypeAndNotifyUrl(); 85 | return $this->getResult($this->gateway_refund, true); 86 | } 87 | 88 | /** 89 | * 关闭正在进行的订单 90 | * @param string $out_trade_no 91 | * @return array 92 | */ 93 | public function close($out_trade_no = '') 94 | { 95 | $this->config['out_trade_no'] = $out_trade_no; 96 | $this->unsetTradeTypeAndNotifyUrl(); 97 | return $this->getResult($this->gateway_close); 98 | } 99 | 100 | /** 101 | * 查询订单状态 102 | * @param string $out_trade_no 103 | * @return array 104 | */ 105 | public function find($out_trade_no = '') 106 | { 107 | $this->config['out_trade_no'] = $out_trade_no; 108 | $this->unsetTradeTypeAndNotifyUrl(); 109 | return $this->getResult($this->gateway_query); 110 | } 111 | 112 | /** 113 | * XML内容验证 114 | * @param string $data 115 | * @param null $sign 116 | * @param bool $sync 117 | * @return array|bool 118 | */ 119 | public function verify($data, $sign = null, $sync = false) 120 | { 121 | $data = $this->fromXml($data); 122 | $sign = is_null($sign) ? $data['sign'] : $sign; 123 | return $this->getSign($data) === $sign ? $data : false; 124 | } 125 | 126 | /** 127 | * @return mixed 128 | */ 129 | abstract protected function getTradeType(); 130 | 131 | /** 132 | * @param array $config_biz 133 | * @return array 134 | */ 135 | protected function preOrder($config_biz = []) 136 | { 137 | $this->config = array_merge($this->config, $config_biz); 138 | return $this->getResult($this->gateway); 139 | } 140 | 141 | /** 142 | * 获取验证访问数据 143 | * @param string $url 144 | * @param bool $cert 145 | * @return array 146 | * @throws GatewayException 147 | */ 148 | protected function getResult($url, $cert = false) 149 | { 150 | $this->config['sign'] = $this->getSign($this->config); 151 | if ($cert) { 152 | $data = $this->fromXml($this->post($url, $this->toXml($this->config), ['ssl_cer' => $this->userConfig->get('ssl_cer', ''), 'ssl_key' => $this->userConfig->get('ssl_key', '')])); 153 | } else { 154 | $data = $this->fromXml($this->post($url, $this->toXml($this->config))); 155 | } 156 | if (!isset($data['return_code']) || $data['return_code'] !== 'SUCCESS' || $data['result_code'] !== 'SUCCESS') { 157 | $error = 'GetResultError:' . $data['return_msg']; 158 | $error .= isset($data['err_code_des']) ? ' - ' . $data['err_code_des'] : ''; 159 | } 160 | if (!isset($error) && $this->getSign($data) !== $data['sign']) { 161 | $error = 'GetResultError: return data sign error'; 162 | } 163 | if (isset($error)) { 164 | throw new GatewayException($error, 20000, $data); 165 | } 166 | return $data; 167 | } 168 | 169 | 170 | /** 171 | * 生成内容签名 172 | * @param $data 173 | * @return string 174 | */ 175 | protected function getSign($data) 176 | { 177 | if (is_null($this->userConfig->get('mch_key'))) { 178 | throw new InvalidArgumentException('Missing Config -- [mch_key]'); 179 | } 180 | ksort($data); 181 | $string = md5($this->getSignContent($data) . '&key=' . $this->userConfig->get('mch_key')); 182 | return strtoupper($string); 183 | } 184 | 185 | /** 186 | * 生成签名内容 187 | * @param $data 188 | * @return string 189 | */ 190 | private function getSignContent($data) 191 | { 192 | $buff = ''; 193 | foreach ($data as $k => $v) { 194 | $buff .= ($k != 'sign' && $v != '' && !is_array($v)) ? $k . '=' . $v . '&' : ''; 195 | } 196 | return trim($buff, '&'); 197 | } 198 | 199 | /** 200 | * 生成随机字符串 201 | * @param int $length 202 | * @return string 203 | */ 204 | protected function createNonceStr($length = 16) 205 | { 206 | $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 207 | $str = ''; 208 | for ($i = 0; $i < $length; $i++) { 209 | $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); 210 | } 211 | return $str; 212 | } 213 | 214 | /** 215 | * 转为XML数据 216 | * @param array $data 源数据 217 | * @return string 218 | */ 219 | protected function toXml($data) 220 | { 221 | if (!is_array($data) || count($data) <= 0) { 222 | throw new InvalidArgumentException('convert to xml error!invalid array!'); 223 | } 224 | $xml = ''; 225 | foreach ($data as $key => $val) { 226 | $xml .= is_numeric($val) ? '<' . $key . '>' . $val . '' : '<' . $key . '>'; 227 | } 228 | return $xml . ''; 229 | } 230 | 231 | /** 232 | * 解析XML数据 233 | * @param string $xml 源数据 234 | * @return mixed 235 | */ 236 | protected function fromXml($xml) 237 | { 238 | if (!$xml) { 239 | throw new InvalidArgumentException('convert to array error !invalid xml'); 240 | } 241 | libxml_disable_entity_loader(true); 242 | return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); 243 | } 244 | 245 | /** 246 | * 清理签名验证不必要的参数 247 | * @return bool 248 | */ 249 | protected function unsetTradeTypeAndNotifyUrl() 250 | { 251 | unset($this->config['notify_url']); 252 | unset($this->config['trade_type']); 253 | return true; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Pay.php: -------------------------------------------------------------------------------- 1 | config = new Config($config); 48 | } 49 | 50 | /** 51 | * 指定驱动器 52 | * @param string $driver 53 | * @return $this 54 | */ 55 | public function driver($driver) 56 | { 57 | if (is_null($this->config->get($driver))) { 58 | throw new InvalidArgumentException("Driver [$driver]'s Config is not defined."); 59 | } 60 | $this->drivers = $driver; 61 | return $this; 62 | } 63 | 64 | /** 65 | * 指定操作网关 66 | * @param string $gateway 67 | * @return GatewayInterface 68 | */ 69 | public function gateway($gateway = 'web') 70 | { 71 | if (!isset($this->drivers)) { 72 | throw new InvalidArgumentException('Driver is not defined.'); 73 | } 74 | return $this->gateways = $this->createGateway($gateway); 75 | } 76 | 77 | /** 78 | * 创建操作网关 79 | * @param string $gateway 80 | * @return mixed 81 | */ 82 | protected function createGateway($gateway) 83 | { 84 | if (!file_exists(__DIR__ . '/Gateways/' . ucfirst($this->drivers) . '/' . ucfirst($gateway) . 'Gateway.php')) { 85 | throw new InvalidArgumentException("Gateway [$gateway] is not supported."); 86 | } 87 | $gateway = __NAMESPACE__ . '\\Gateways\\' . ucfirst($this->drivers) . '\\' . ucfirst($gateway) . 'Gateway'; 88 | return new $gateway($this->config->get($this->drivers)); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /test/alipay-app.php: -------------------------------------------------------------------------------- 1 | '', // 商户订单号 21 | 'total_amount' => '1', // 支付金额 22 | 'subject' => 'test subject', // 支付订单描述 23 | ]; 24 | 25 | // 实例支付对象 26 | $pay = new \Pay\Pay($config); 27 | 28 | try { 29 | $options = $pay->driver('alipay')->gateway('app')->apply($payOrder); 30 | var_dump($options); 31 | } catch (Exception $e) { 32 | echo "创建订单失败," . $e->getMessage(); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/alipay-notify.php: -------------------------------------------------------------------------------- 1 | driver('alipay')->gateway()->verify($_POST)) { 22 | file_put_contents('notify.txt', "收到来自支付宝的异步通知\r\n", FILE_APPEND); 23 | file_put_contents('notify.txt', '订单号:' . $_POST['out_trade_no'] . "\r\n", FILE_APPEND); 24 | file_put_contents('notify.txt', '订单金额:' . $_POST['total_amount'] . "\r\n\r\n", FILE_APPEND); 25 | } else { 26 | file_put_contents('notify.txt', "收到异步通知\r\n", FILE_APPEND); 27 | } 28 | 29 | echo "success"; 30 | 31 | -------------------------------------------------------------------------------- /test/alipay-pos.php: -------------------------------------------------------------------------------- 1 | '', // 商户订单号 21 | 'total_amount' => '1', // 支付金额 22 | 'subject' => 'test subject', // 支付订单描述 23 | ]; 24 | 25 | // 实例支付对象 26 | $pay = new \Pay\Pay($config); 27 | 28 | try { 29 | $options = $pay->driver('alipay')->gateway('pos')->apply($payOrder); 30 | var_dump($options); 31 | } catch (Exception $e) { 32 | echo "创建订单失败," . $e->getMessage(); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/alipay-scan.php: -------------------------------------------------------------------------------- 1 | '', // 商户订单号 21 | 'total_amount' => '1', // 支付金额 22 | 'subject' => 'test subject', // 支付订单描述 23 | ]; 24 | 25 | // 实例支付对象 26 | $pay = new \Pay\Pay($config); 27 | 28 | try { 29 | $options = $pay->driver('alipay')->gateway('scan')->apply($payOrder); 30 | var_dump($options); 31 | } catch (Exception $e) { 32 | echo "创建订单失败," . $e->getMessage(); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/alipay-transfer.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 24 | 'payee_type' => 'ALIPAY_LOGONID', // 收款方账户类型(ALIPAY_LOGONID | ALIPAY_USERID) 25 | 'payee_account' => 'demo@sandbox.com', // 收款方账户 26 | 'amount' => '10', // 转账金额 27 | 'payer_show_name' => '未寒', // 付款方姓名 28 | 'payee_real_name' => '张三', // 收款方真实姓名 29 | 'remark' => '张三', // 转账备注 30 | ]; 31 | 32 | try { 33 | $options = $pay->driver('alipay')->gateway('transfer')->apply($payOrder); 34 | var_dump($options); 35 | } catch (Exception $e) { 36 | echo "创建订单失败," . $e->getMessage(); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test/alipay-wap.php: -------------------------------------------------------------------------------- 1 | '', // 商户订单号 21 | 'total_amount' => '1', // 支付金额 22 | 'subject' => 'test subject', // 支付订单描述 23 | ]; 24 | 25 | // 实例支付对象 26 | $pay = new \Pay\Pay($config); 27 | 28 | try { 29 | $options = $pay->driver('alipay')->gateway('wap')->apply($payOrder); 30 | var_dump($options); 31 | } catch (Exception $e) { 32 | echo "创建订单失败," . $e->getMessage(); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/alipay-web.php: -------------------------------------------------------------------------------- 1 | '', // 商户订单号 21 | 'total_amount' => '1', // 支付金额 22 | 'subject' => 'test subject', // 支付订单描述 23 | ]; 24 | 25 | // 实例支付对象 26 | $pay = new \Pay\Pay($config); 27 | 28 | try { 29 | $options = $pay->driver('alipay')->gateway('web')->apply($payOrder); 30 | var_dump($options); 31 | } catch (Exception $e) { 32 | echo "创建订单失败," . $e->getMessage(); 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/config.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'app_id' => 'wxe335431b79068046', // 应用ID 17 | 'mch_id' => '1300513101', // 微信支付商户号 18 | 'mch_key' => 'AGNq9Z6I9xQ7usWT2xPXc76pS9HUvcoq', // 微信支付密钥 19 | 'ssl_cer' => '', // 微信证书 cert 文件 20 | 'ssl_key' => '', // 微信证书 key 文件 21 | 'notify_url' => 'http://localhost/wxpay-notify.php', // 支付通知URL 22 | ], 23 | // 支付宝支付参数 24 | 'alipay' => [ 25 | 'app_id' => '', // 应用ID 26 | 'public_key' => '', // 支付宝公钥(1行填写) 27 | 'private_key' => '', // 支付宝私钥(1行填写) 28 | 'notify_url' => 'http://localhost/wxpay-notify.php', // 支付通知URL 29 | ] 30 | ]; -------------------------------------------------------------------------------- /test/wechat-refund.php: -------------------------------------------------------------------------------- 1 | '56737188841424', // 原商户订单号 24 | 'out_refund_no' => '567371888414240', // 退款订单号 25 | 'total_fee' => '1', // 原订单交易总金额 26 | 'refund_fee' => '1', // 申请退款金额 27 | ]; 28 | 29 | try { 30 | $options = $pay->driver('wechat')->gateway('transfer')->refund($payOrder); 31 | var_dump($options); 32 | } catch (Exception $e) { 33 | echo "创建订单失败," . $e->getMessage(); 34 | } -------------------------------------------------------------------------------- /test/wxpay-app.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 支付人的 IP 24 | ]; 25 | 26 | // 实例支付对象 27 | $pay = new \Pay\Pay($config); 28 | 29 | try { 30 | $options = $pay->driver('wechat')->gateway('app')->apply($payOrder); 31 | var_dump($options); 32 | } catch (Exception $e) { 33 | echo "创建订单失败," . $e->getMessage(); 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/wxpay-miniapp.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 支付人的 IP 24 | 'openid' => '', // 支付人的 openID 25 | ]; 26 | 27 | // 实例支付对象 28 | $pay = new \Pay\Pay($config); 29 | 30 | try { 31 | $options = $pay->driver('wechat')->gateway('miniapp')->apply($payOrder); 32 | var_dump($options); 33 | } catch (Exception $e) { 34 | echo "创建订单失败," . $e->getMessage(); 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/wxpay-mp.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 支付人的 IP 24 | 'openid' => '', // 支付人的 openID 25 | ]; 26 | 27 | // 实例支付对象 28 | $pay = new \Pay\Pay($config); 29 | 30 | try { 31 | $options = $pay->driver('wechat')->gateway('mp')->apply($payOrder); 32 | var_dump($options); 33 | } catch (Exception $e) { 34 | echo "创建订单失败," . $e->getMessage(); 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/wxpay-notify.php: -------------------------------------------------------------------------------- 1 | driver('wechat')->gateway('mp')->verify(file_get_contents('php://input')); 22 | 23 | if ($verify) { 24 | file_put_contents('notify.txt', "收到来自微信的异步通知\r\n", FILE_APPEND); 25 | file_put_contents('notify.txt', '订单号:' . $verify['out_trade_no'] . "\r\n", FILE_APPEND); 26 | file_put_contents('notify.txt', '订单金额:' . $verify['total_fee'] . "\r\n\r\n", FILE_APPEND); 27 | } else { 28 | file_put_contents('notify.txt', "收到异步通知\r\n", FILE_APPEND); 29 | } 30 | 31 | echo "success"; -------------------------------------------------------------------------------- /test/wxpay-pos.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 支付人的 IP 24 | 'auth_code' => '', // 授权码 25 | ]; 26 | // 实例支付对象 27 | $pay = new \Pay\Pay($config); 28 | 29 | try { 30 | $options = $pay->driver('wechat')->gateway('pos')->apply($payOrder); 31 | var_dump($options); 32 | } catch (Exception $e) { 33 | echo "创建订单失败," . $e->getMessage(); 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/wxpay-scan.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 调用 API 服务器的 IP 24 | 'product_id' => '', // 订单商品 ID 25 | ]; 26 | 27 | // 实例支付对象 28 | $pay = new \Pay\Pay($config); 29 | 30 | try { 31 | $options = $pay->driver('wechat')->gateway('scan')->apply($payOrder); 32 | var_dump($options); 33 | } catch (Exception $e) { 34 | echo "创建订单失败," . $e->getMessage(); 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/wxpay-transfer.php: -------------------------------------------------------------------------------- 1 | '', //商户订单号 21 | 'openid' => '', //收款人的openid 22 | 'check_name' => 'NO_CHECK', //NO_CHECK:不校验真实姓名\FORCE_CHECK:强校验真实姓名 23 | // 're_user_name'=>'张三', //check_name为 FORCE_CHECK 校验实名的时候必须提交 24 | 'amount' => 100, //企业付款金额,单位为分 25 | 'desc' => '帐户提现', //付款说明 26 | 'spbill_create_ip' => '192.168.0.1', //发起交易的IP地址 27 | ]; 28 | 29 | // 实例支付对象 30 | $pay = new \Pay\Pay($config); 31 | 32 | try { 33 | $options = $pay->driver('wechat')->gateway('transfer')->apply($payOrder); 34 | var_dump($options); 35 | } catch (Exception $e) { 36 | echo "创建订单失败," . $e->getMessage(); 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/wxpay-wap.php: -------------------------------------------------------------------------------- 1 | '', // 订单号 21 | 'total_fee' => '', // 订单金额,**单位:分** 22 | 'body' => '', // 订单描述 23 | 'spbill_create_ip' => '', // 支付人的 IP 24 | ]; 25 | 26 | // 实例支付对象 27 | $pay = new \Pay\Pay($config); 28 | 29 | try { 30 | $options = $pay->driver('wechat')->gateway('wap')->apply($payOrder); 31 | var_dump($options); 32 | } catch (Exception $e) { 33 | echo "创建订单失败," . $e->getMessage(); 34 | } 35 | 36 | 37 | --------------------------------------------------------------------------------