├── composer.json ├── src ├── Aop │ ├── AlipayResponseException.php │ ├── AlipayCertHelper.php │ ├── AlipayRequest.php │ ├── AlipayResponse.php │ └── AopClient.php ├── AlipayCertifyService.php ├── AlipayCertdocService.php ├── AlipayBillService.php ├── AlipayService.php ├── AlipaySettleService.php ├── AlipayOauthService.php ├── AlipayComplainService.php ├── AlipayTransferService.php └── AlipayTradeService.php ├── LICENSE └── README.md /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cccyun/alipay-sdk", 3 | "description": "支付宝开放平台第三方 PHP SDK,基于官方最新版本,支持公钥和公钥证书2种模式。", 4 | "type": "library", 5 | "keywords": [ 6 | "alipay", 7 | "支付宝" 8 | ], 9 | "license": "MIT", 10 | "minimum-stability": "dev", 11 | "prefer-stable": true, 12 | "require": { 13 | "php": ">=7.1", 14 | "ext-json": "*", 15 | "ext-openssl": "*", 16 | "ext-curl": "*", 17 | "ext-mbstring": "*", 18 | "ext-bcmath": "*" 19 | }, 20 | "authors": [ 21 | { 22 | "name": "caihong", 23 | "email": "admin@cccyun.cn" 24 | } 25 | ], 26 | "autoload": { 27 | "psr-4": { 28 | "Alipay\\": "src/" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Aop/AlipayResponseException.php: -------------------------------------------------------------------------------- 1 | res = $res; 17 | $this->retCode = $res['code']; 18 | if (isset($res['sub_msg'])) { 19 | $this->errCode = $res['sub_code']; 20 | $message = '['.$res['sub_code'].']'.$res['sub_msg']; 21 | } elseif (isset($res['msg'])) { 22 | $message = '['.$res['code'].']'.$res['msg']; 23 | } else { 24 | $message = '未知错误'; 25 | } 26 | parent::__construct($message); 27 | } 28 | 29 | public function getRetCode() 30 | { 31 | return $this->retCode; 32 | } 33 | 34 | public function getErrCode() 35 | { 36 | return $this->errCode; 37 | } 38 | 39 | public function getResponse(): array 40 | { 41 | return $this->res; 42 | } 43 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 消失的彩虹海 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 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, 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alipay SDK for PHP 2 | 支付宝开放平台第三方 PHP SDK,基于官方最新版本,支持公钥和公钥证书2种模式。 3 | 4 | ### 功能特点 5 | 6 | - 根据支付宝开放平台最新API开发,相比官方SDK,功能更完善,代码更简洁 7 | - 支持支付宝服务商模式与互联网平台直付通模式 8 | - 支持Composer安装,无需加载多余组件,可应用于任何平台或框架 9 | - 符合`PSR`标准,你可以各种方便的与你的框架集成 10 | - 基本完善的PHPDoc,可以随心所欲添加本项目中没有的API接口 11 | 12 | ### 环境要求 13 | 14 | `PHP` >= 7.1 15 | 16 | ### 使用方法 17 | 18 | 1. Composer 安装。 19 | 20 | ```bash 21 | composer require cccyun/alipay-sdk 22 | ``` 23 | 24 | 2. 创建配置文件 [`config.php`](./examples/config.php),填写配置信息。 25 | 26 | 3. 引入配置文件,构造请求参数,调用AlipayTradeService中的方法发起请求,参考 [`examples/qrpay.php`](./examples/qrpay.php) 27 | 28 | 4. 更多实例,请移步 [`examples`](examples/) 目录。 29 | 30 | 5. AlipayService实现类功能说明 31 | 32 | | 类名 | 说明 | 33 | | --------------------- | -------------------------------------------------------- | 34 | | AlipayTradeService | 支付宝交易功能,基本上所有支付产品都用这个 | 35 | | AlipayOauthService | 支付宝快捷登录功能,用于JS支付快捷登录以及第三方应用授权 | 36 | | AlipaySettleService | 支付宝分账功能 | 37 | | AlipayTransferService | 支付宝转账功能 | 38 | | AlipayComplainService | 支付宝交易投诉处理 | 39 | | AlipayCertifyService | 支付宝身份认证 | 40 | | AlipayCertdocService | 支付宝实名证件信息比对验证 | 41 | | AlipayBillService | 支付宝账单功能 | 42 | 43 | 6. 要对接的API在AlipayService实现类中没有,可根据支付宝官方的文档,使用AlipayService类中的aopExecute方法直接调用接口,参考 [`examples/other.php`](./examples/other.php) 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Aop/AlipayCertHelper.php: -------------------------------------------------------------------------------- 1 | $value) { 31 | $string[] = $key . '=' . $value; 32 | } 33 | } 34 | return implode(',', $string); 35 | } 36 | 37 | /** 38 | * 提取根证书序列号 39 | * @param string $certPath 根证书 40 | * @return string|null 41 | */ 42 | public static function getRootCertSN(string $certPath): ?string 43 | { 44 | $cert = file_get_contents($certPath); 45 | $array = explode("-----END CERTIFICATE-----", $cert); 46 | $SN = null; 47 | for ($i = 0; $i < count($array) - 1; $i++) { 48 | $ssl[$i] = openssl_x509_parse($array[$i] . "-----END CERTIFICATE-----"); 49 | if(strpos($ssl[$i]['serialNumber'],'0x') === 0){ 50 | $ssl[$i]['serialNumber'] = self::hex2dec($ssl[$i]['serialNumberHex']); 51 | } 52 | if ($ssl[$i]['signatureTypeLN'] == "sha1WithRSAEncryption" || $ssl[$i]['signatureTypeLN'] == "sha256WithRSAEncryption") { 53 | if ($SN == null) { 54 | $SN = md5(self::array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']); 55 | } else { 56 | $SN = $SN . "_" . md5(self::array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']); 57 | } 58 | } 59 | } 60 | return $SN; 61 | } 62 | 63 | /** 64 | * 0x转高精度数字 65 | * @param $hex 66 | * @return int|string 67 | */ 68 | private static function hex2dec($hex) 69 | { 70 | $dec = 0; 71 | $len = strlen($hex); 72 | for ($i = 1; $i <= $len; $i++) { 73 | $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i)))); 74 | } 75 | return $dec; 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/AlipayCertifyService.php: -------------------------------------------------------------------------------- 1 | return_url = $config['return_url']; 23 | } 24 | 25 | /** 26 | * 身份认证初始化服务 27 | * @param string $outer_order_no 商户请求的唯一标识 28 | * @param string $cert_name 真实姓名 29 | * @param string $cert_no 证件号码 30 | * @param string $cert_type 证件类型 31 | * @param string $biz_code 认证场景码(FACE、SMART_FACE) 32 | * @return mixed {"code":"10000","msg":"Success","certify_id":"本次申请操作的唯一标识"} 33 | * 34 | * @throws Exception 35 | * @see https://opendocs.alipay.com/open/02ahjy 36 | */ 37 | public function initialize(string $outer_order_no, string $cert_name, string $cert_no, string $cert_type = 'IDENTITY_CARD', string $biz_code = 'SMART_FACE') 38 | { 39 | $apiName = 'alipay.user.certify.open.initialize'; 40 | $bizContent = [ 41 | 'outer_order_no' => $outer_order_no, //商户请求的唯一标识 42 | 'biz_code' => $biz_code, //认证场景码 43 | 'identity_param' => [ 44 | 'identity_type' => 'CERT_INFO', //身份信息参数类型 45 | 'cert_type' => $cert_type, //证件类型 46 | 'cert_name' => $cert_name, //真实姓名 47 | 'cert_no' => $cert_no, //证件号码 48 | ], 49 | 'merchant_config' => ['return_url'=>$this->return_url], //商户个性化配置 50 | ]; 51 | return $this->aopExecute($apiName, $bizContent); 52 | } 53 | 54 | /** 55 | * 身份认证开始认证 56 | * @param string $certify_id 本次申请操作的唯一标识 57 | * @return string html表单 58 | * @throws Exception 59 | */ 60 | public function certify(string $certify_id): string 61 | { 62 | $apiName = 'alipay.user.certify.open.certify'; 63 | $bizContent = array( 64 | 'certify_id' => $certify_id, 65 | ); 66 | return $this->aopPageExecute($apiName, $bizContent); 67 | } 68 | 69 | /** 70 | * 身份认证记录查询 71 | * @param string $certify_id 本次申请操作的唯一标识 72 | * @return mixed {"code":"10000","msg":"Success","passed":"T"} 73 | * @throws Exception 74 | */ 75 | public function query(string $certify_id) 76 | { 77 | $apiName = 'alipay.user.certify.open.query'; 78 | $bizContent = array( 79 | 'certify_id' => $certify_id, 80 | ); 81 | return $this->aopExecute($apiName, $bizContent); 82 | } 83 | } -------------------------------------------------------------------------------- /src/AlipayCertdocService.php: -------------------------------------------------------------------------------- 1 | $cert_name, //真实姓名 33 | 'cert_type' => 'IDENTITY_CARD', //证件类型 34 | 'cert_no' => $cert_no 35 | ); 36 | return $this->aopExecute($apiName, $bizContent); 37 | } 38 | 39 | /** 40 | * 实名证件信息比对验证咨询 41 | * @param string $verify_id 申请验证ID 42 | * @param string $auth_token 用户授权令牌 43 | * @return mixed {"code":"10000","msg":"Success","passed":"F","fail_reason":"姓名不一致","fail_params":"[\\\"user_name\\\"]"} 44 | * @throws Exception 45 | */ 46 | public function consult(string $verify_id, string $auth_token) 47 | { 48 | $apiName = 'alipay.user.certdoc.certverify.consult'; 49 | $bizContent = array( 50 | 'verify_id' => $verify_id, 51 | ); 52 | $params = [ 53 | 'auth_token' => $auth_token 54 | ]; 55 | return $this->aopExecute($apiName, $bizContent, $params); 56 | } 57 | 58 | /** 59 | * 跳转支付宝授权页面 60 | * @param string $redirect_uri 回调地址 61 | * @param string $verify_id 申请验证ID 62 | * @param $state 63 | * @param bool $is_get_url 是否只返回url 64 | * @return void|string 65 | */ 66 | public function oauth(string $redirect_uri, string $verify_id, $state = null, bool $is_get_url = false) 67 | { 68 | $param = [ 69 | 'app_id' => $this->appId, 70 | 'scope' => 'id_verify', 71 | 'redirect_uri' => $redirect_uri, 72 | 'cert_verify_id' => $verify_id 73 | ]; 74 | if($state) $param['state'] = $state; 75 | 76 | $url = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?'.http_build_query($param); 77 | 78 | if ($is_get_url) { 79 | return $url; 80 | } 81 | 82 | header("Location: $url"); 83 | exit(); 84 | } 85 | 86 | /** 87 | * 换取授权访问令牌 88 | * @param string $code 授权码或刷新令牌 89 | * @param string $grant_type 授权方式(authorization_code,refresh_token) 90 | * @return mixed {"user_id":"支付宝用户的唯一标识","open_id":"支付宝用户的唯一标识","access_token":"访问令牌","expires_in":"3600","refresh_token":"刷新令牌","re_expires_in":"3600"} 91 | * @throws Exception 92 | */ 93 | public function getToken(string $code, string $grant_type = 'authorization_code') 94 | { 95 | $apiName = 'alipay.system.oauth.token'; 96 | $params = []; 97 | $params['grant_type'] = $grant_type; 98 | if($grant_type == 'refresh_token'){ 99 | $params['refresh_token'] = $code; 100 | }else{ 101 | $params['code'] = $code; 102 | } 103 | return $this->aopExecute($apiName, null, $params); 104 | } 105 | } -------------------------------------------------------------------------------- /src/Aop/AlipayRequest.php: -------------------------------------------------------------------------------- 1 | $value) { 28 | $this->{$key} = $value; 29 | } 30 | } 31 | 32 | /** 33 | * 获取用于发起请求的“时间戳”. 34 | * 35 | * @return string 36 | */ 37 | public static function getTimestamp(): string 38 | { 39 | return date('Y-m-d H:i:s'); 40 | } 41 | 42 | /** 43 | * 根据类名获取 API 方法名. 44 | * 45 | * @return string 46 | */ 47 | public function getApiMethodName(): string 48 | { 49 | return $this->apiMethodName; 50 | } 51 | 52 | public function setApiMethodName($apiMethodName): AlipayRequest 53 | { 54 | $this->apiMethodName = $apiMethodName; 55 | 56 | return $this; 57 | } 58 | 59 | public function getNotifyUrl() 60 | { 61 | return $this->notifyUrl; 62 | } 63 | 64 | public function setNotifyUrl($notifyUrl): AlipayRequest 65 | { 66 | $this->notifyUrl = $notifyUrl; 67 | 68 | return $this; 69 | } 70 | 71 | public function getReturnUrl() 72 | { 73 | return $this->returnUrl; 74 | } 75 | 76 | public function setReturnUrl($returnUrl): AlipayRequest 77 | { 78 | $this->returnUrl = $returnUrl; 79 | 80 | return $this; 81 | } 82 | 83 | public function getTerminalType() 84 | { 85 | return $this->terminalType; 86 | } 87 | 88 | public function setTerminalType($terminalType): AlipayRequest 89 | { 90 | $this->terminalType = $terminalType; 91 | 92 | return $this; 93 | } 94 | 95 | public function getTerminalInfo() 96 | { 97 | return $this->terminalInfo; 98 | } 99 | 100 | public function setTerminalInfo($terminalInfo): AlipayRequest 101 | { 102 | $this->terminalInfo = $terminalInfo; 103 | 104 | return $this; 105 | } 106 | 107 | public function getProdCode() 108 | { 109 | return $this->prodCode; 110 | } 111 | 112 | public function setProdCode($prodCode): AlipayRequest 113 | { 114 | $this->prodCode = $prodCode; 115 | 116 | return $this; 117 | } 118 | 119 | public function getAuthToken() 120 | { 121 | return $this->authToken; 122 | } 123 | 124 | public function setAuthToken($authToken): AlipayRequest 125 | { 126 | $this->authToken = $authToken; 127 | 128 | return $this; 129 | } 130 | 131 | public function getAppAuthToken() 132 | { 133 | return $this->appAuthToken; 134 | } 135 | 136 | public function setAppAuthToken($appAuthToken): AlipayRequest 137 | { 138 | $this->appAuthToken = $appAuthToken; 139 | 140 | return $this; 141 | } 142 | 143 | public function getBizContent() 144 | { 145 | if (is_array($this->bizContent)) { 146 | return json_encode($this->bizContent, JSON_UNESCAPED_UNICODE); 147 | } 148 | return $this->bizContent; 149 | } 150 | 151 | public function setBizContent($bizContent = []): AlipayRequest 152 | { 153 | $this->bizContent = $bizContent; 154 | 155 | return $this; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/Aop/AlipayResponse.php: -------------------------------------------------------------------------------- 1 | raw = $raw; 60 | $this->parsed = json_decode($raw, true); 61 | if (!$this->parsed) { 62 | $error = function_exists('json_last_error_msg') ? json_last_error_msg() : json_last_error(); 63 | throw new \Exception('返回数据解析失败:'.$error); 64 | } 65 | $this->parseResponseData($apiName); 66 | } 67 | 68 | /** 69 | * 获取原始响应的被签名数据,用于验证签名. 70 | * 71 | * @param string $apiName 72 | * @throws \Exception 73 | */ 74 | protected function parseResponseData(string $apiName) 75 | { 76 | $nodeName = str_replace(".", "_", $apiName) . self::RESPONSE_SUFFIX; 77 | $nodeIndex = strpos($this->raw, $nodeName); 78 | if (!$nodeIndex) { 79 | $nodeName = self::ERROR_NODE; 80 | $nodeIndex = strpos($this->raw, $nodeName); 81 | if(!$nodeIndex){ 82 | throw new \Exception('Response data not found'); 83 | } 84 | } 85 | $this->nodeName = $nodeName; 86 | 87 | $signDataStartIndex = $nodeIndex + strlen($nodeName) + 2; 88 | $signIndex = strrpos($this->raw, '"'.static::ALIPAY_CERT_SN.'"'); 89 | if(!$signIndex) { 90 | $signIndex = strrpos($this->raw, '"'.static::SIGN_NODE.'"'); 91 | } 92 | 93 | $signDataEndIndex = $signIndex - 1; 94 | $indexLen = $signDataEndIndex - $signDataStartIndex; 95 | if ($indexLen < 0) { 96 | return; 97 | } 98 | 99 | $this->signData = substr($this->raw, $signDataStartIndex, $indexLen); 100 | } 101 | 102 | /** 103 | * 获取待验签数据 104 | * 105 | * @return string 106 | */ 107 | public function getSignData(): ?string 108 | { 109 | return $this->signData; 110 | } 111 | 112 | /** 113 | * 获取响应内的签名. 114 | * 115 | * @return string 116 | */ 117 | public function getSign(): ?string 118 | { 119 | if (isset($this->parsed[static::SIGN_NODE])) { 120 | return $this->parsed[static::SIGN_NODE]; 121 | } 122 | return null; 123 | } 124 | 125 | /** 126 | * 获取响应内的数据. 127 | * 128 | * @param bool $assoc 129 | * 130 | * @return mixed|object 131 | */ 132 | public function getData(bool $assoc = true) 133 | { 134 | if (!isset($this->parsed[$this->nodeName])){ 135 | return null; 136 | } 137 | $result = $this->parsed[$this->nodeName]; 138 | if (!$assoc) { 139 | $result = (object) ($result); 140 | } 141 | return $result; 142 | } 143 | 144 | /** 145 | * 判断响应是否成功. 146 | * 147 | * @return bool 148 | */ 149 | public function isSuccess(): bool 150 | { 151 | if (isset($this->parsed[static::ERROR_NODE])) { 152 | return false; 153 | } 154 | if (!isset($this->parsed[$this->nodeName])){ 155 | return false; 156 | } 157 | $data = $this->parsed[$this->nodeName]; 158 | return isset($data['code']) && $data['code'] == '10000'; 159 | } 160 | 161 | /** 162 | * 获取原始响应. 163 | * 164 | * @return string 165 | */ 166 | public function getRaw(): string 167 | { 168 | return $this->raw; 169 | } 170 | 171 | /** 172 | * 获取支付宝公钥证书序列号 173 | * 174 | * @return bool|string 175 | */ 176 | public function getAlipayCertSN() 177 | { 178 | if (isset($this->parsed[static::ALIPAY_CERT_SN])) { 179 | return $this->parsed[static::ALIPAY_CERT_SN]; 180 | } 181 | return false; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/AlipayBillService.php: -------------------------------------------------------------------------------- 1 | $start_time, 32 | 'end_time' => $end_time, 33 | 'page_no' => $page_no, 34 | 'page_size' => $page_size, 35 | ]; 36 | return $this->aopExecute($apiName, $bizContent); 37 | } 38 | 39 | /** 40 | * 账户买入交易查询 41 | * @param string $start_time 创建时间的起始 42 | * @param string $end_time 创建时间的结束 43 | * @param int $page_no 分页号,从1开始 44 | * @param int $page_size 分页大小1000-2000,默认2000 45 | * @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]} 46 | * @throws Exception 47 | */ 48 | public function buyQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000) 49 | { 50 | $apiName = 'alipay.data.bill.buy.query'; 51 | $bizContent = [ 52 | 'start_time' => $start_time, 53 | 'end_time' => $end_time, 54 | 'page_no' => $page_no, 55 | 'page_size' => $page_size, 56 | ]; 57 | return $this->aopExecute($apiName, $bizContent); 58 | } 59 | 60 | /** 61 | * 账户账务明细查询 62 | * @param string $start_time 创建时间的起始 63 | * @param string $end_time 创建时间的结束 64 | * @param int $page_no 分页号,从1开始 65 | * @param int $page_size 分页大小1000-2000,默认2000 66 | * @param null $bill_user_id 指定用户做账单查询 67 | * @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]} 68 | * @throws Exception 69 | */ 70 | public function accountlogQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000, $bill_user_id = null) 71 | { 72 | $apiName = 'alipay.data.bill.accountlog.query'; 73 | $bizContent = [ 74 | 'start_time' => $start_time, 75 | 'end_time' => $end_time, 76 | 'page_no' => $page_no, 77 | 'page_size' => $page_size, 78 | ]; 79 | if ($bill_user_id) $bizContent['bill_user_id'] = $bill_user_id; 80 | return $this->aopExecute($apiName, $bizContent); 81 | } 82 | 83 | /** 84 | * 账户充值,转账,提现查询 85 | * @param string $start_time 创建时间的起始 86 | * @param string $end_time 创建时间的结束 87 | * @param int $page_no 分页号,从1开始 88 | * @param int $page_size 分页大小1000-2000,默认2000 89 | * @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]} 90 | * @throws Exception 91 | */ 92 | public function transferQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000) 93 | { 94 | $apiName = 'alipay.data.bill.transfer.query'; 95 | $bizContent = [ 96 | 'start_time' => $start_time, 97 | 'end_time' => $end_time, 98 | 'page_no' => $page_no, 99 | 'page_size' => $page_size, 100 | ]; 101 | return $this->aopExecute($apiName, $bizContent); 102 | } 103 | 104 | /** 105 | * 账户当前余额查询 106 | * @return mixed {"total_amount":"支付宝账户余额","available_amount":"账户可用余额","freeze_amount":"冻结金额","settle_amount":"待结算金额"} 107 | * @throws Exception 108 | */ 109 | public function balanceQuery() 110 | { 111 | $apiName = 'alipay.data.bill.balance.query'; 112 | return $this->aopExecute($apiName); 113 | } 114 | 115 | /** 116 | * 申请电子回单 117 | * @param string $type 申请的类型 118 | * @param string $key 根据不同业务类型,传入不同参数 119 | * @return mixed {"file_id":"文件申请号"} 120 | * @throws Exception 121 | */ 122 | public function ereceiptApply(string $type, string $key) 123 | { 124 | $apiName = 'alipay.data.bill.ereceipt.apply'; 125 | $bizContent = [ 126 | 'type' => $type, 127 | 'key' => $key, 128 | ]; 129 | return $this->aopExecute($apiName, $bizContent); 130 | } 131 | 132 | /** 133 | * 查询电子回单状态 134 | * @param string $file_id 文件申请号 135 | * @return mixed {"status":"处理状态","download_url":"下载链接","error_message":"失败原因"} 136 | * @throws Exception 137 | */ 138 | public function ereceiptQuery(string $file_id) 139 | { 140 | $apiName = 'alipay.data.bill.ereceipt.query'; 141 | $bizContent = [ 142 | 'file_id' => $file_id 143 | ]; 144 | return $this->aopExecute($apiName, $bizContent); 145 | } 146 | } -------------------------------------------------------------------------------- /src/AlipayService.php: -------------------------------------------------------------------------------- 1 | appId = $config['app_id']; 54 | if (!empty($config['app_cert_path']) && !empty($config['alipay_cert_path']) && !empty($config['root_cert_path']) && (!isset($config['cert_mode']) || $config['cert_mode'] == 1)) { 55 | $this->isCertMode = true; 56 | } 57 | if (isset($config['app_auth_token'])) { 58 | $this->appAuthToken = $config['app_auth_token']; 59 | } 60 | if (isset($config['logPath'])) { 61 | $this->logPath = $config['logPath']; 62 | } 63 | if (isset($config['pageMethod'])) { 64 | $this->pageMethod = $config['pageMethod']; 65 | } 66 | 67 | $this->client = new AopClient(); 68 | $this->client->appId = $config['app_id']; 69 | if (!empty($config['gateway_url'])) { 70 | $this->client->gatewayUrl = $config['gateway_url']; 71 | } 72 | if (!empty($config['sign_type'])) { 73 | $this->client->signType = $config['sign_type']; 74 | } 75 | if (!empty($config['charset'])) { 76 | $this->client->charset = $config['charset']; 77 | } 78 | 79 | $this->client->rsaPrivateKey = $config['app_private_key']; 80 | if ($this->isCertMode) { 81 | $this->client->rsaPublicKeyFilePath = $config['alipay_cert_path']; 82 | $this->client->appCertSN = AlipayCertHelper::getCertSN($config['app_cert_path']); 83 | $this->client->alipayRootCertSN = AlipayCertHelper::getRootCertSN($config['root_cert_path']); 84 | } else { 85 | $this->client->rsaPublicKey = $config['alipay_public_key']; 86 | } 87 | 88 | } 89 | 90 | /** 91 | * 发起接口请求 92 | * 93 | * @param string $apiName 接口名称 94 | * @param array|null $bizContent 请求参数的集合 95 | * @param array|null $params 其他公共参数 96 | * @return mixed 97 | * @throws Exception 98 | */ 99 | public function aopExecute(string $apiName, array $bizContent = null, array $params = null) 100 | { 101 | $request = new AlipayRequest(); 102 | $request->setApiMethodName($apiName); 103 | $request->setNotifyUrl($this->notifyUrl); 104 | $request->setAppAuthToken($this->appAuthToken); 105 | $request->setBizContent($bizContent); 106 | if (is_array($params) && count($params) > 0) { 107 | $request->setOtherParams($params); 108 | } 109 | $result = $this->client->execute($request)->getData(); 110 | if ($apiName == 'alipay.system.oauth.token' && isset($result['access_token'])) { 111 | return $result; 112 | } elseif (isset($result['code']) && $result['code'] == '10000') { 113 | return $result; 114 | } else { 115 | throw new AlipayResponseException($result); 116 | } 117 | } 118 | 119 | /** 120 | * 页面跳转接口,返回form表单html 121 | * 122 | * @param string $apiName 接口名称 123 | * @param array|null $bizContent 请求参数的集合 124 | * @param array|null $params 其他公共参数 125 | * @return string 126 | * @throws Exception 127 | */ 128 | public function aopPageExecute(string $apiName, array $bizContent = null, array $params = null): string 129 | { 130 | $request = new AlipayRequest(); 131 | $request->setApiMethodName($apiName); 132 | $request->setNotifyUrl($this->notifyUrl); 133 | $request->setReturnUrl($this->returnUrl); 134 | $request->setAppAuthToken($this->appAuthToken); 135 | $request->setBizContent($bizContent); 136 | if (is_array($params) && count($params) > 0) { 137 | $request->setOtherParams($params); 138 | } 139 | if (!empty($this->pageMethod)) { 140 | switch ($this->pageMethod) { 141 | case '2': 142 | $httpmethod = 'REDIRECT'; 143 | break; 144 | case '1': 145 | $httpmethod = 'GET'; 146 | break; 147 | default: 148 | $httpmethod = 'POST'; 149 | break; 150 | } 151 | return $this->client->pageExecute($request, $httpmethod); 152 | } else { 153 | return $this->client->pageExecute($request); 154 | } 155 | } 156 | 157 | /** 158 | * APP接口,返回收银台SDK的字符串 159 | * 160 | * @param string $apiName 接口名称 161 | * @param array|null $bizContent 请求参数的集合 162 | * @param array|null $params 其他公共参数 163 | * @return string 164 | */ 165 | public function aopSdkExecute(string $apiName, array $bizContent = null, array $params = null): string 166 | { 167 | $request = new AlipayRequest(); 168 | $request->setApiMethodName($apiName); 169 | $request->setNotifyUrl($this->notifyUrl); 170 | $request->setAppAuthToken($this->appAuthToken); 171 | $request->setBizContent($bizContent); 172 | if (is_array($params) && count($params) > 0) { 173 | $request->setOtherParams($params); 174 | } 175 | return $this->client->sdkExecute($request); 176 | } 177 | 178 | /** 179 | * 回调验签 180 | * @param array $params 支付宝返回的信息 181 | * @return bool 182 | */ 183 | public function check(array $params): bool 184 | { 185 | return $this->client->verify($params); 186 | } 187 | 188 | /** 189 | * 记录日志 190 | */ 191 | public function writeLog($text) 192 | { 193 | if (empty($this->logPath)) return; 194 | //$text=iconv("GBK", "UTF-8//IGNORE", $text); 195 | file_put_contents($this->logPath . "log.txt", date("Y-m-d H:i:s") . " " . $text . "\r\n", FILE_APPEND); 196 | } 197 | } -------------------------------------------------------------------------------- /src/AlipaySettleService.php: -------------------------------------------------------------------------------- 1 | $type, 35 | 'account' => $account, 36 | ]; 37 | if(!empty($name)) $receiver['name'] = $name; 38 | $bizContent = array( 39 | 'receiver_list' => [ 40 | $receiver 41 | ], 42 | 'out_request_no' => $out_request_no, 43 | ); 44 | $this->aopExecute($apiName, $bizContent); 45 | return true; 46 | } 47 | 48 | /** 49 | * 分账关系解绑 50 | * @param string $type 分账接收方方类型(userId,loginName,openId) 51 | * @param string $account 分账接收方账号 52 | * @return bool 53 | * @throws Exception 54 | */ 55 | public function relation_unbind(string $type, string $account): bool 56 | { 57 | $apiName = 'alipay.trade.royalty.relation.unbind'; 58 | $out_request_no = date("YmdHis").rand(11111,99999); 59 | $receiver = [ 60 | 'type' => $type, 61 | 'account' => $account, 62 | ]; 63 | $bizContent = array( 64 | 'receiver_list' => [ 65 | $receiver 66 | ], 67 | 'out_request_no' => $out_request_no, 68 | ); 69 | $this->aopExecute($apiName, $bizContent); 70 | return true; 71 | } 72 | 73 | /** 74 | * 分账关系查询 75 | * @param int $page_num 页码 76 | * @param int $page_size 每页条数 77 | * @return array 78 | * @throws Exception 79 | */ 80 | public function relation_batchquery(int $page_num = 1, int $page_size = 20): array 81 | { 82 | $apiName = 'alipay.trade.royalty.relation.batchquery'; 83 | $out_request_no = date("YmdHis").rand(11111,99999); 84 | $bizContent = array( 85 | 'page_num' => $page_num, 86 | 'page_size' => $page_size, 87 | 'out_request_no' => $out_request_no, 88 | ); 89 | $result = $this->aopExecute($apiName, $bizContent); 90 | return $result['receiver_list']; 91 | } 92 | 93 | /** 94 | * 分账请求 95 | * @param string $trade_no 支付宝订单号 96 | * @param string $type 收入方账户类型(userId,cardAliasNo,loginName,openId) 97 | * @param string $account 收入方账户 98 | * @param numeric $money 分账的金额 99 | * @return mixed {"trade_no":"支付宝交易号","settle_no":"支付宝分账单号"} 100 | * @throws Exception 101 | */ 102 | public function order_settle(string $trade_no, string $type, string $account, $money) { 103 | $apiName = 'alipay.trade.order.settle'; 104 | $out_request_no = date("YmdHis").rand(11111,99999); 105 | $receiver = [ 106 | 'trans_in_type' => $type, 107 | 'trans_in' => $account, 108 | 'amount' => $money 109 | ]; 110 | $bizContent = array( 111 | 'out_request_no' => $out_request_no, 112 | 'trade_no' => $trade_no, 113 | 'royalty_parameters' => [ 114 | $receiver 115 | ], 116 | 'extend_params' => [ 117 | 'royalty_finish' => 'true' 118 | ] 119 | ); 120 | return $this->aopExecute($apiName, $bizContent); 121 | } 122 | 123 | /** 124 | * 解冻剩余资金 125 | * @param string $trade_no 支付宝订单号 126 | * @return mixed {"trade_no":"支付宝交易号","settle_no":"支付宝分账单号"} 127 | * @throws Exception 128 | */ 129 | public function order_settle_unfreeze(string $trade_no) { 130 | $apiName = 'alipay.trade.order.settle'; 131 | $out_request_no = date("YmdHis").rand(11111,99999); 132 | $bizContent = array( 133 | 'out_request_no' => $out_request_no, 134 | 'trade_no' => $trade_no, 135 | 'extend_params' => [ 136 | 'royalty_finish' => 'true' 137 | ], 138 | ); 139 | return $this->aopExecute($apiName, $bizContent); 140 | } 141 | 142 | /** 143 | * 分账查询 144 | * @param string $settle_no 支付宝分账单号 145 | * @return mixed {"out_request_no":"商户分账请求单号","operation_dt":"分账受理时间","royalty_detail_list":[{"operation_type":"transfer","execute_dt":"分账执行时间","trans_out":"2088111111111111","trans_out_type":"userId","trans_in":"2088111111112222","trans_in_type":"userId","amount":10,"state":"FAIL","error_code":"TXN_RESULT_ACCOUNT_BALANCE_NOT_ENOUGH","error_desc":"分账余额不足"}]} 146 | * @throws Exception 147 | */ 148 | public function order_settle_query(string $settle_no) { 149 | $apiName = 'alipay.trade.order.settle.query'; 150 | $bizContent = array( 151 | 'settle_no' => $settle_no, 152 | ); 153 | return $this->aopExecute($apiName, $bizContent); 154 | } 155 | 156 | /** 157 | * 分账比例查询 158 | * @return mixed {"user_id":"2088XXXX1234","max_ratio":80} 159 | * @throws Exception 160 | */ 161 | public function rate_query() { 162 | $apiName = 'alipay.trade.royalty.rate.query'; 163 | $out_request_no = date("YmdHis").rand(11111,99999); 164 | $bizContent = array( 165 | 'out_request_no' => $out_request_no, 166 | ); 167 | return $this->aopExecute($apiName, $bizContent); 168 | } 169 | 170 | /** 171 | * 退分账 172 | * @param string $trade_no 支付宝交易号 173 | * @param string $type 支出方账户类型(userId,loginName) 174 | * @param string $account 支出方账户 175 | * @param numeric $money 分账的金额 176 | * @return true 177 | * @throws Exception 178 | */ 179 | public function order_settle_refund(string $trade_no, string $type, string $account, $money): bool 180 | { 181 | $apiName = 'alipay.trade.refund'; 182 | $out_request_no = date("YmdHis").rand(11111,99999); 183 | $receiver = [ 184 | 'royalty_type' => 'transfer', 185 | 'trans_out_type' => $type, 186 | 'trans_out' => $account, 187 | 'amount' => $money 188 | ]; 189 | $bizContent = array( 190 | 'trade_no' => $trade_no, 191 | 'refund_amount' => '0', 192 | 'out_request_no' => $out_request_no, 193 | 'refund_royalty_parameters' => [ 194 | $receiver 195 | ], 196 | ); 197 | $this->aopExecute($apiName, $bizContent); 198 | return true; 199 | } 200 | } -------------------------------------------------------------------------------- /src/AlipayOauthService.php: -------------------------------------------------------------------------------- 1 | $this->appId, 34 | 'scope' => $scope, 35 | 'redirect_uri' => $redirect_uri, 36 | ]; 37 | if($state) $param['state'] = $state; 38 | 39 | $url = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?'.http_build_query($param); 40 | 41 | if ($is_get_url) { 42 | return $url; 43 | } 44 | 45 | header("Location: $url"); 46 | exit(); 47 | } 48 | 49 | /** 50 | * 换取授权访问令牌 51 | * @param string $code 授权码或刷新令牌 52 | * @param string $grant_type 授权方式(authorization_code,refresh_token) 53 | * @return mixed {"user_id":"支付宝用户的唯一标识","open_id":"支付宝用户的唯一标识","access_token":"访问令牌","expires_in":"3600","refresh_token":"刷新令牌","re_expires_in":"3600"} 54 | * @throws Exception 55 | */ 56 | public function getToken(string $code, string $grant_type = 'authorization_code') 57 | { 58 | $apiName = 'alipay.system.oauth.token'; 59 | $params = []; 60 | $params['grant_type'] = $grant_type; 61 | if($grant_type == 'refresh_token'){ 62 | $params['refresh_token'] = $code; 63 | }else{ 64 | $params['code'] = $code; 65 | } 66 | return $this->aopExecute($apiName, null, $params); 67 | } 68 | 69 | /** 70 | * 支付宝会员授权信息查询 71 | * @param string $accessToken 用户授权令牌 72 | * @return mixed {"code":"10000","msg":"Success","user_id":"支付宝用户的userId","avatar":"用户头像地址","city":"市名称","nick_name":"用户昵称","province":"省份名称","gender":"性别MF"} 73 | * @throws Exception 74 | */ 75 | public function userinfo(string $accessToken) 76 | { 77 | $apiName = 'alipay.user.info.share'; 78 | $params = [ 79 | 'auth_token' => $accessToken 80 | ]; 81 | 82 | return $this->aopExecute($apiName, null, $params); 83 | } 84 | 85 | 86 | /** 87 | * 跳转支付宝第三方应用授权页面 88 | * @param string $redirect_uri 回调地址 89 | * @param $state 90 | * @param bool $is_get_url 是否只返回url 91 | * @return void|string 92 | */ 93 | public function appOauth(string $redirect_uri, $state = null, bool $is_get_url = false) 94 | { 95 | $param = [ 96 | 'app_id' => $this->appId, 97 | 'redirect_uri' => $redirect_uri, 98 | ]; 99 | if($state) $param['state'] = $state; 100 | 101 | $url = 'https://openauth.alipay.com/oauth2/appToAppAuth.htm?'.http_build_query($param); 102 | 103 | if ($is_get_url) { 104 | return $url; 105 | } 106 | 107 | header("Location: $url"); 108 | exit(); 109 | } 110 | 111 | /** 112 | * 跳转支付宝指定应用授权页面 113 | * @param string $redirect_uri 回调地址 114 | * @param array $app_types 对商家应用的限制类型 115 | * @param $state 116 | * @return array [PC端url, APP端url] 117 | */ 118 | public function appOauthAssign(string $redirect_uri, array $app_types, $state = null): array 119 | { 120 | $param = [ 121 | 'platformCode' => 'O', 122 | 'taskType' => 'INTERFACE_AUTH', 123 | 'agentOpParam' => [ 124 | 'redirectUri' => $redirect_uri, 125 | 'appTypes' => $app_types, 126 | 'isvAppId' => $this->appId, 127 | 'state' => $state 128 | ], 129 | ]; 130 | 131 | $biz_data = json_encode($param, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); 132 | $pc_url = 'https://b.alipay.com/page/message/tasksDetail?bizData='.rawurlencode($biz_data); 133 | $app_url = 'alipays://platformapi/startapp?appId=2021003130652097&page=pages%2Fauthorize%2Findex%3FbizData%3D'.rawurlencode($biz_data); 134 | 135 | return [$pc_url, $app_url]; 136 | } 137 | 138 | /** 139 | * 换取授权访问令牌 140 | * @param string $code 授权码或刷新令牌 141 | * @param string $grant_type 授权方式(authorization_code,refresh_token) 142 | * @return mixed {"user_id":"授权商户的user_id","auth_app_id":"授权商户的appid","app_auth_token":"应用授权令牌","app_refresh_token":"刷新令牌","re_expires_in":"3600"} 143 | * @throws Exception 144 | */ 145 | public function getAppToken(string $code, string $grant_type = 'authorization_code') 146 | { 147 | $apiName = 'alipay.open.auth.token.app'; 148 | $bizContent = [ 149 | 'grant_type' => $grant_type, 150 | ]; 151 | if($grant_type == 'refresh_token'){ 152 | $bizContent['refresh_token'] = $code; 153 | }else{ 154 | $bizContent['code'] = $code; 155 | } 156 | return $this->aopExecute($apiName, $bizContent); 157 | } 158 | 159 | /** 160 | * 查询授权商家信息 161 | * @param string $appAuthToken 应用授权令牌 162 | * @return mixed {"user_id":"授权商户的user_id","auth_app_id":"授权商户的appid","expires_in":31536000,"auth_methods":[],"auth_start":"授权生效时间","auth_end":"授权失效时间","status":"valid/invalid","is_by_app_auth":true} 163 | * @throws Exception 164 | */ 165 | public function appQuery(string $appAuthToken) 166 | { 167 | $apiName = 'alipay.open.auth.token.app.query'; 168 | $bizContent = [ 169 | 'app_auth_token' => $appAuthToken, 170 | ]; 171 | return $this->aopExecute($apiName, $bizContent); 172 | } 173 | 174 | public function decryptMobile(array $response, string $key) 175 | { 176 | if(is_string($response['response'])){ 177 | /*if(!$this->client->rsaPubilcVerify('"'.$response['response'].'"', $response['sign'])){ 178 | throw new Exception('手机号码数据验签失败'); 179 | }*/ 180 | $data = $this->client->aesDecrypt($response['response'], $key); 181 | if(!$data) { 182 | throw new Exception('手机号码数据解密失败'); 183 | } 184 | $result = json_decode($data, true); 185 | if($result['code'] == '10000'){ 186 | return $result['mobile']; 187 | }elseif(isset($result['subMsg'])){ 188 | throw new Exception($result['subMsg']); 189 | }else{ 190 | throw new Exception('手机号码数据解密失败 '.$result['msg']); 191 | } 192 | }elseif(isset($response['response']['subCode']) && isset($response['response']['subMsg'])){ 193 | throw new Exception('['.$response['response']['subCode'].']'.$response['response']['subMsg']); 194 | }else{ 195 | throw new Exception('手机号码加密数据错误'); 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /src/AlipayComplainService.php: -------------------------------------------------------------------------------- 1 | $complain_event_id, 30 | ]; 31 | return $this->aopExecute($apiName, $bizContent); 32 | } 33 | 34 | /** 35 | * 查询交易投诉列表 36 | * @param string|null $status 状态 37 | * @param string|null $begin_time 查询开始时间 38 | * @param string|null $end_time 查询结束时间 39 | * @param int $page_num 当前页 40 | * @param int $page_size 每页条数,最多支持20条 41 | * @return mixed {"page_size":10,"page_num":1,"total_page_num":5,"total_num":55,"trade_complain_infos":[]} 42 | * @throws Exception 43 | */ 44 | public function batchQuery(string $status = null, string $begin_time = null, string $end_time = null, int $page_num = 1, int $page_size = 10) 45 | { 46 | $apiName = 'alipay.merchant.tradecomplain.batchquery'; 47 | $bizContent = [ 48 | 'page_num' => $page_num, 49 | 'page_size' => $page_size, 50 | ]; 51 | if ($status) $bizContent['status'] = $status; 52 | if ($begin_time) $bizContent['begin_time'] = $begin_time; 53 | if ($end_time) $bizContent['end_time'] = $end_time; 54 | return $this->aopExecute($apiName, $bizContent); 55 | } 56 | 57 | /** 58 | * 商户上传处理图片 59 | * @param string $file_path 文件路径 60 | * @param string $file_name 文件名 61 | * @return string 图片资源标识 62 | * @throws Exception 63 | */ 64 | public function imageUpload(string $file_path, string $file_name): string 65 | { 66 | $image_type = array_pop(explode('.',$file_name)); 67 | if (empty($image_type)) $image_type = 'png'; 68 | $apiName = 'alipay.merchant.image.upload'; 69 | $params = [ 70 | 'image_type' => $image_type, 71 | 'image_content' => new \CURLFile($file_path, '', $file_name), 72 | ]; 73 | $result = $this->aopExecute($apiName, null, $params); 74 | return $result['image_id']; 75 | } 76 | 77 | /** 78 | * 商家处理交易投诉 79 | * @param string $complain_event_id 投诉单号 80 | * @param string $feedback_code 反馈类目ID 81 | * @param string $feedback_content 反馈内容 82 | * @param string|null $feedback_images 反馈图片id列表(多个用逗号隔开) 83 | * @return bool 84 | * @throws Exception 85 | */ 86 | public function feedbackSubmit(string $complain_event_id, string $feedback_code, string $feedback_content, string $feedback_images = null): bool 87 | { 88 | $apiName = 'alipay.merchant.tradecomplain.feedback.submit'; 89 | $bizContent = [ 90 | 'complain_event_id' => $complain_event_id, 91 | 'feedback_code' => $feedback_code, 92 | 'feedback_content' => $feedback_content, 93 | ]; 94 | if ($feedback_images) $bizContent['feedback_images'] = $feedback_images; 95 | $this->aopExecute($apiName, $bizContent); 96 | return true; 97 | } 98 | 99 | /** 100 | * 商家留言回复 101 | * @param string $complain_event_id 投诉单号 102 | * @param string $reply_content 回复内容 103 | * @param string|null $reply_images 回复图片(多个用逗号隔开) 104 | * @return bool 105 | * @throws Exception 106 | */ 107 | public function replySubmit(string $complain_event_id, string $reply_content, string $reply_images = null): bool 108 | { 109 | $apiName = 'alipay.merchant.tradecomplain.reply.submit'; 110 | $bizContent = [ 111 | 'complain_event_id' => $complain_event_id, 112 | 'reply_content' => $reply_content, 113 | ]; 114 | if ($reply_images) $bizContent['reply_images'] = $reply_images; 115 | $this->aopExecute($apiName, $bizContent); 116 | return true; 117 | } 118 | 119 | /** 120 | * 商家补充凭证 121 | * @param string $complain_event_id 投诉单号 122 | * @param string $supplement_content 文字凭证 123 | * @param string|null $supplement_images 图片凭证(多个用逗号隔开) 124 | * @return bool 125 | * @throws Exception 126 | */ 127 | public function supplementSubmit(string $complain_event_id, string $supplement_content, string $supplement_images = null): bool 128 | { 129 | $apiName = 'alipay.merchant.tradecomplain.supplement.submit'; 130 | $bizContent = [ 131 | 'complain_event_id' => $complain_event_id, 132 | 'supplement_content' => $supplement_content, 133 | ]; 134 | if ($supplement_images) $bizContent['supplement_images'] = $supplement_images; 135 | $this->aopExecute($apiName, $bizContent); 136 | return true; 137 | } 138 | 139 | 140 | /** 141 | * RiskGO查询单条交易投诉详情 142 | * @param string $complain_id 支付宝侧投诉单号 143 | * @return mixed 144 | * @throws Exception 145 | */ 146 | public function riskquery(string $complain_id) 147 | { 148 | $apiName = 'alipay.security.risk.complaint.info.query'; 149 | $bizContent = [ 150 | 'complain_id' => $complain_id, 151 | ]; 152 | return $this->aopExecute($apiName, $bizContent); 153 | } 154 | 155 | /** 156 | * RiskGO查询交易投诉列表 157 | * @param string|null $status 状态 158 | * @param string|null $begin_time 查询开始时间 159 | * @param string|null $end_time 查询结束时间 160 | * @param int $page_num 当前页 161 | * @param int $page_size 每页条数,最多支持20条 162 | * @return mixed {"page_size":10,"page_num":1,"total_page_num":5,"total_num":55,"trade_complain_infos":[]} 163 | * @throws Exception 164 | */ 165 | public function riskbatchQuery(string $status = null, string $begin_time = null, string $end_time = null, int $page_num = 1, int $page_size = 10) 166 | { 167 | $apiName = 'alipay.security.risk.complaint.info.batchquery'; 168 | $bizContent = [ 169 | 'current_page_num' => $page_num, 170 | 'page_size' => $page_size, 171 | ]; 172 | if ($status) $bizContent['status_list'] = [$status]; 173 | if ($begin_time) $bizContent['begin_time'] = $begin_time; 174 | if ($end_time) $bizContent['end_time'] = $end_time; 175 | return $this->aopExecute($apiName, $bizContent); 176 | } 177 | 178 | /** 179 | * RiskGO商户上传处理图片 180 | * @param string $file_path 文件路径 181 | * @param string $file_name 文件名 182 | * @return mixed 图片资源标识 183 | * @throws Exception 184 | */ 185 | public function riskimageUpload(string $file_path, string $file_name) 186 | { 187 | $apiName = 'alipay.security.risk.complaint.file.upload'; 188 | $params = [ 189 | 'file_content' => new \CURLFile($file_path, '', $file_name), 190 | ]; 191 | return $this->aopExecute($apiName, null, $params); 192 | } 193 | 194 | /** 195 | * RiskGO商家处理交易投诉 196 | * @param string $complain_id 投诉单号 197 | * @param string $process_code 投诉处理结果码 198 | * @param string $remark 备注 199 | * @param array|null $img_file_list 图片文件列表 200 | * @return bool 201 | * @throws Exception 202 | */ 203 | public function riskfeedbackSubmit(string $complain_id, string $process_code, string $remark, array $img_file_list = null): bool 204 | { 205 | $apiName = 'alipay.security.risk.complaint.process.finish'; 206 | $bizContent = [ 207 | 'id_list' => [$complain_id], 208 | 'process_code' => $process_code, 209 | 'remark' => $remark 210 | ]; 211 | if ($img_file_list) $bizContent['img_file_list'] = $img_file_list; 212 | $result = $this->aopExecute($apiName, $bizContent); 213 | return $result['complaint_process_success']; 214 | } 215 | 216 | } -------------------------------------------------------------------------------- /src/AlipayTransferService.php: -------------------------------------------------------------------------------- 1 | isCertMode) { 35 | $apiName = 'alipay.fund.trans.uni.transfer'; 36 | switch($is_userid) { 37 | case 2:$payee_type = 'ALIPAY_OPEN_ID';break; 38 | case 1:$payee_type = 'ALIPAY_USER_ID';break; 39 | default:$payee_type = 'ALIPAY_LOGON_ID';break; 40 | } 41 | $bizContent = [ 42 | 'out_biz_no' => $out_biz_no, //商户转账唯一订单号 43 | 'trans_amount' => $amount, //转账金额 44 | 'product_code' => 'TRANS_ACCOUNT_NO_PWD', 45 | 'biz_scene' => 'DIRECT_TRANSFER', 46 | 'order_title' => $payer_show_name, //付款方显示名称 47 | 'payee_info' => array('identity' => $payee_account, 'identity_type' => $payee_type), 48 | 'business_params' => json_encode(['payer_show_name_use_alias'=>'true']), 49 | ]; 50 | if(!empty($payee_real_name))$bizContent['payee_info']['name'] = $payee_real_name; //收款方真实姓名 51 | } else { 52 | $apiName = 'alipay.fund.trans.toaccount.transfer'; 53 | $payee_type = $is_userid?'ALIPAY_USERID':'ALIPAY_LOGONID'; 54 | $bizContent = [ 55 | 'out_biz_no' => $out_biz_no, //商户转账唯一订单号 56 | 'payee_type' => $payee_type, //收款方账户类型 57 | 'payee_account' => $payee_account, //收款方账户 58 | 'amount' => $amount, //转账金额 59 | 'payer_show_name' => $payer_show_name, //付款方显示姓名 60 | ]; 61 | if(!empty($payee_real_name))$bizContent['payee_real_name'] = $payee_real_name; //收款方真实姓名 62 | } 63 | 64 | $result = $this->aopExecute($apiName, $bizContent); 65 | if(isset($result['pay_date'])) $result['trans_date'] = $result['pay_date']; 66 | return $result; 67 | } 68 | 69 | /** 70 | * 转账到银行卡账户 71 | * @param string $out_biz_no 商户转账唯一订单号 72 | * @param numeric $amount 转账金额 73 | * @param string $payee_account 收款方账户 74 | * @param string $payee_real_name 收款方姓名 75 | * @param string $payer_show_name 付款方显示姓名 76 | * @return mixed {"out_biz_no":"商户订单号","order_id":"支付宝转账订单号","pay_fund_order_id":"支付宝支付资金流水号","status":"SUCCESS","trans_date":"订单支付时间"} 77 | * @throws Exception 78 | */ 79 | public function transferToBankCard(string $out_biz_no, $amount, string $payee_account, string $payee_real_name, string $payer_show_name) 80 | { 81 | $apiName = 'alipay.fund.trans.uni.transfer'; 82 | $bizContent = [ 83 | 'out_biz_no' => $out_biz_no, //商户转账唯一订单号 84 | 'trans_amount' => $amount, //转账金额 85 | 'product_code' => 'TRANS_BANKCARD_NO_PWD', 86 | 'biz_scene' => 'DIRECT_TRANSFER', 87 | 'order_title' => $payer_show_name, //付款方显示名称 88 | 'payee_info' => array( 89 | 'identity_type' => 'BANKCARD_ACCOUNT', 90 | 'identity' => $payee_account, 91 | 'name' => $payee_real_name, 92 | 'bankcard_ext_info' => array( 93 | 'account_type' => '2' 94 | ) 95 | ), 96 | ]; 97 | return $this->aopExecute($apiName, $bizContent); 98 | } 99 | 100 | /** 101 | * 转账单据查询 102 | * @param string $order_id 订单号 103 | * @param int $type 订单号类型(0=支付宝转账单据号,1=支付宝支付资金流水号,2=商户转账唯一订单号) 104 | * @param int $code 产品类型(0=转账到支付宝账户,1=转账到银行卡) 105 | * @return mixed {"order_id":"支付宝转账单据号","pay_fund_order_id":"支付宝支付资金流水号","out_biz_no":"商户转账唯一订单号","trans_amount":1,"status":"SUCCESS","pay_date":"支付时间","error_code":"PAYEE_CARD_INFO_ERROR","fail_reason":"收款方银行卡信息有误"} 106 | * @throws Exception 107 | */ 108 | public function query(string $order_id, int $type=0, int $code = 0) 109 | { 110 | $apiName = 'alipay.fund.trans.common.query'; 111 | $bizContent = []; 112 | if($type==1){ 113 | $bizContent['pay_fund_order_id'] = $order_id; 114 | }elseif($type==2){ 115 | $bizContent['out_biz_no'] = $order_id; 116 | }else{ 117 | $bizContent['order_id'] = $order_id; 118 | } 119 | if($type==2){ 120 | $bizContent['product_code'] = $code == 1 ? 'TRANS_BANKCARD_NO_PWD' : 'TRANS_ACCOUNT_NO_PWD'; 121 | $bizContent['biz_scene'] = 'DIRECT_TRANSFER'; 122 | } 123 | return $this->aopExecute($apiName, $bizContent); 124 | } 125 | 126 | /** 127 | * 账户余额查询 128 | * @param string $alipay_user_id 支付宝用户ID 129 | * @param int $user_type 用户标识类型(0支付宝UID,1支付宝openid) 130 | * @return mixed {"available_amount":"账户可用余额","freeze_amount":"实时冻结余额"} 131 | * @throws Exception 132 | */ 133 | public function accountQuery(string $alipay_user_id = null, int $user_type = 0) 134 | { 135 | $apiName = 'alipay.fund.account.query'; 136 | if($user_type == 1){ 137 | $bizContent = [ 138 | 'alipay_open_id' => $alipay_user_id, 139 | 'account_type' => 'ACCTRANS_ACCOUNT', 140 | ]; 141 | }else{ 142 | $bizContent = [ 143 | 'alipay_user_id' => $alipay_user_id, 144 | 'account_type' => 'ACCTRANS_ACCOUNT', 145 | ]; 146 | } 147 | return $this->aopExecute($apiName, $bizContent); 148 | } 149 | 150 | /** 151 | * 现金红包转账接口 152 | * @param string $out_biz_no 商户转账唯一订单号 153 | * @param numeric $amount 转账金额 154 | * @param string $user_id 收款方账户 155 | * @param string $order_title 转账业务的标题 156 | * @param null $original_order_id 原支付宝业务单号 157 | * @return mixed {"out_biz_no":"商户订单号","order_id":"支付宝转账订单号","pay_fund_order_id":"支付宝支付资金流水号","status":"SUCCESS","trans_date":"订单支付时间"} 158 | * @throws Exception 159 | */ 160 | public function redPacketTansfer(string $out_biz_no, $amount, string $user_id, string $order_title, $original_order_id = null) 161 | { 162 | $apiName = 'alipay.fund.trans.uni.transfer'; 163 | $bizContent = [ 164 | 'out_biz_no' => $out_biz_no, 165 | 'trans_amount' => $amount, 166 | 'product_code' => 'STD_RED_PACKET', 167 | 'biz_scene' => 'PERSONAL_COLLECTION', 168 | 'order_title' => $order_title, 169 | 'payee_info' => array('identity' => $user_id, 'identity_type' => 'ALIPAY_USER_ID'), 170 | 'business_params' => json_encode(['sub_biz_scene'=>'REDPACKET'], JSON_UNESCAPED_UNICODE) 171 | ]; 172 | if($original_order_id) $bizContent['original_order_id'] = $original_order_id; 173 | 174 | return $this->aopExecute($apiName, $bizContent); 175 | } 176 | 177 | /** 178 | * 红包资金退回接口 179 | * @param string $out_request_no 标识一次资金退回请求 180 | * @param string $order_id 发红包时支付宝返回的支付宝订单号 181 | * @param numeric $refund_amount 需要退款的金额 182 | * @return mixed {"refund_order_id":"退款的支付宝系统内部单据id","order_id":"发红包时支付宝返回的支付宝订单号","out_request_no":"标识一次资金退回请求","status":"SUCCESS","refund_amount":"本次退款的金额","refund_date":"时间"} 183 | * @throws Exception 184 | */ 185 | public function redPacketRefund(string $out_request_no, string $order_id, $refund_amount) 186 | { 187 | $apiName = 'alipay.fund.trans.refund'; 188 | $bizContent = [ 189 | 'order_id' => $order_id, 190 | 'out_request_no' => $out_request_no, 191 | 'refund_amount' => $refund_amount, 192 | ]; 193 | 194 | return $this->aopExecute($apiName, $bizContent); 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /src/AlipayTradeService.php: -------------------------------------------------------------------------------- 1 | smid = $config['smid']; 23 | } 24 | if (isset($config['notify_url'])) { 25 | $this->notifyUrl = $config['notify_url']; 26 | } 27 | if (isset($config['return_url'])) { 28 | $this->returnUrl = $config['return_url']; 29 | } 30 | } 31 | 32 | /** 33 | * 付款码支付 34 | * @param array $bizContent 请求参数的集合 35 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","open_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号"} 36 | * @throws Exception 37 | * @see https://opendocs.alipay.com/open/02ekfp?ref=api&scene=32 38 | */ 39 | public function scanPay(array $bizContent) 40 | { 41 | $apiName = 'alipay.trade.pay'; 42 | return $this->aopExecute($apiName, $bizContent); 43 | } 44 | 45 | /** 46 | * 扫码支付 47 | * @param array $bizContent 请求参数的集合 48 | * @return mixed {"out_trade_no":"商户订单号","qr_code":"二维码链接"} 49 | * @throws Exception 50 | * @see https://opendocs.alipay.com/open/02ekfg?ref=api&scene=19 51 | */ 52 | public function qrPay(array $bizContent) 53 | { 54 | $apiName = 'alipay.trade.precreate'; 55 | return $this->aopExecute($apiName, $bizContent); 56 | } 57 | 58 | /** 59 | * JS支付 60 | * @param array $bizContent 请求参数的集合 61 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号"} 62 | * @throws Exception 63 | * @see https://opendocs.alipay.com/open/02ekfj?ref=api 64 | */ 65 | public function jsPay(array $bizContent) 66 | { 67 | $apiName = 'alipay.trade.create'; 68 | return $this->aopExecute($apiName, $bizContent); 69 | } 70 | 71 | /** 72 | * APP支付 73 | * @param array $bizContent 请求参数的集合 74 | * @return string SDK请求串 75 | * @see https://opendocs.alipay.com/open/02e7gq?ref=api&scene=20 76 | */ 77 | public function appPay(array $bizContent): string 78 | { 79 | $apiName = 'alipay.trade.app.pay'; 80 | return $this->aopSdkExecute($apiName, $bizContent); 81 | } 82 | 83 | /** 84 | * 电脑网站支付 85 | * @param array $bizContent 请求参数的集合 86 | * @return string html表单 87 | * @throws Exception 88 | * @see https://opendocs.alipay.com/open/028r8t?ref=api&scene=22 89 | */ 90 | public function pagePay(array $bizContent): string 91 | { 92 | $apiName = 'alipay.trade.page.pay'; 93 | $bizContent['product_code'] = 'FAST_INSTANT_TRADE_PAY'; 94 | return $this->aopPageExecute($apiName, $bizContent); 95 | } 96 | 97 | /** 98 | * 手机网站支付 99 | * @param array $bizContent 请求参数的集合 100 | * @return string html表单 101 | * @throws Exception 102 | * @see https://opendocs.alipay.com/open/02ivbs?ref=api&scene=21 103 | */ 104 | public function wapPay(array $bizContent): string 105 | { 106 | $apiName = 'alipay.trade.wap.pay'; 107 | $bizContent['product_code'] = 'QUICK_WAP_WAY'; 108 | return $this->aopPageExecute($apiName, $bizContent); 109 | } 110 | 111 | /** 112 | * 交易查询 113 | * @param string|null $trade_no 支付宝交易号 114 | * @param string|null $out_trade_no 商户订单号 115 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","open_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号","trade_status":"TRADE_SUCCESS","total_amount":88.88} 116 | * @throws Exception 117 | */ 118 | public function query(string $trade_no = null, string $out_trade_no = null) 119 | { 120 | $apiName = 'alipay.trade.query'; 121 | $bizContent = []; 122 | if ($trade_no) { 123 | $bizContent['trade_no'] = $trade_no; 124 | } 125 | if ($out_trade_no) { 126 | $bizContent['out_trade_no'] = $out_trade_no; 127 | } 128 | return $this->aopExecute($apiName, $bizContent); 129 | } 130 | 131 | /** 132 | * 交易是否成功 133 | * @param null $trade_no 支付宝交易号 134 | * @param null $out_trade_no 商户订单号 135 | * @return bool 136 | * @throws Exception 137 | */ 138 | public function queryResult($trade_no = null, $out_trade_no = null): bool 139 | { 140 | $result = $this->query($trade_no, $out_trade_no); 141 | if ($result['trade_status'] == 'TRADE_SUCCESS' || $result['trade_status'] == 'TRADE_FINISHED' || $result['trade_status'] == 'TRADE_CLOSED') { 142 | return true; 143 | } 144 | return false; 145 | } 146 | 147 | /** 148 | * 交易退款 149 | * @param array $bizContent 请求参数的集合 150 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","buyer_user_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号","fund_change":"Y","refund_fee":88.88} 151 | * @throws Exception 152 | * @see https://opendocs.alipay.com/open/02ekfk?ref=api 153 | */ 154 | public function refund(array $bizContent) 155 | { 156 | $apiName = 'alipay.trade.refund'; 157 | return $this->aopExecute($apiName, $bizContent); 158 | } 159 | 160 | /** 161 | * 交易退款查询 162 | * @param array $bizContent 请求参数的集合 163 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","out_request_no":"退款请求号","refund_status":"REFUND_SUCCESS","total_amount":88.88,"refund_amount":88.88} 164 | * @throws Exception 165 | */ 166 | public function refundQuery(array $bizContent) 167 | { 168 | $apiName = 'alipay.trade.fastpay.refund.query'; 169 | return $this->aopExecute($apiName, $bizContent); 170 | } 171 | 172 | /** 173 | * 交易撤销 174 | * @param array $bizContent 请求参数的集合 175 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","retry_flag":"N是否需要重试","action":"close本次撤销触发的交易动作"} 176 | * @throws Exception 177 | */ 178 | public function cancel(array $bizContent) 179 | { 180 | $apiName = 'alipay.trade.cancel'; 181 | return $this->aopExecute($apiName, $bizContent); 182 | } 183 | 184 | /** 185 | * 交易关闭 186 | * @param array $bizContent 请求参数的集合 187 | * @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号"} 188 | * @throws Exception 189 | */ 190 | public function close(array $bizContent) 191 | { 192 | $apiName = 'alipay.trade.close'; 193 | return $this->aopExecute($apiName, $bizContent); 194 | } 195 | 196 | /** 197 | * 查询对账单下载地址 198 | * @param array $bizContent 请求参数的集合 199 | * @return mixed {"bill_download_url":"账单下载地址"} 200 | * @throws Exception 201 | */ 202 | public function downloadurlQuery(array $bizContent) 203 | { 204 | $apiName = 'alipay.data.dataservice.bill.downloadurl.query'; 205 | return $this->aopExecute($apiName, $bizContent); 206 | } 207 | 208 | /** 209 | * 支付回调验签 210 | * @param $params array 211 | * @return bool 212 | * @throws Exception 213 | */ 214 | public function check(array $params): bool 215 | { 216 | $result = $this->client->verify($params); 217 | if($result){ 218 | $result = $this->queryResult($params['trade_no']); 219 | } 220 | return $result; 221 | } 222 | 223 | /** 224 | * 互联网直付通交易额外参数 225 | * @param array &$bizContent 请求参数的集合 226 | * @param string $settle_period_time 最晚结算周期 227 | * @throws Exception 228 | * @see https://opendocs.alipay.com/open/direct-payment/qadp9d 229 | */ 230 | public function directPayParams(array &$bizContent, string $settle_period_time = '1d') 231 | { 232 | if (empty($this->smid)) { 233 | throw new Exception("子商户SMID不能为空"); 234 | } 235 | if(strpos($this->smid, ',')){ 236 | $smids = explode(',', $this->smid); 237 | $this->smid = $smids[array_rand($smids)]; 238 | } 239 | $bizContent['sub_merchant'] = ['merchant_id' => $this->smid]; 240 | $bizContent['settle_info'] = [ 241 | 'settle_period_time' => $settle_period_time, 242 | 'settle_detail_infos' => [ 243 | [ 244 | 'trans_in_type' => 'defaultSettle', 245 | 'amount' => $bizContent['total_amount'] 246 | ] 247 | ] 248 | ]; 249 | } 250 | 251 | /** 252 | * 互联网直付通确认结算 253 | * @param string $trade_no 支付宝交易号 254 | * @param numeric $settle_amount 结算金额 255 | * @param bool $freeze 冻结标识 256 | * @return mixed {"trade_no":"支付宝交易号","out_request_no":"确认结算请求流水号","settle_amount":"结算金额"} 257 | * @throws Exception 258 | * @see https://opendocs.alipay.com/open/direct-payment/gkvknf 259 | */ 260 | public function settle_confirm(string $trade_no, $settle_amount, bool $freeze = false) 261 | { 262 | $apiName = 'alipay.trade.settle.confirm'; 263 | $out_request_no = date("YmdHis").rand(11111,99999); 264 | $bizContent = array( 265 | 'out_request_no' => $out_request_no, 266 | 'trade_no' => $trade_no, 267 | 'settle_info' => [ 268 | 'settle_detail_infos' => [ 269 | [ 270 | 'trans_in_type' => 'defaultSettle', 271 | 'amount' => $settle_amount 272 | ] 273 | ] 274 | ], 275 | ); 276 | if($freeze){ 277 | $bizContent['extend_params'] = ['royalty_freeze' => 'true']; 278 | } 279 | return $this->aopExecute($apiName, $bizContent); 280 | } 281 | 282 | /** 283 | * 合并支付预创建 284 | * @param array $bizContent 请求参数的集合 285 | * @return mixed {"out_merge_no":"合单订单号","pre_order_no":"预下单号"} 286 | * @throws Exception 287 | * @see https://opendocs.alipay.com/open/028xr9 288 | */ 289 | public function mergePrecreatePay(array $bizContent) 290 | { 291 | $apiName = 'alipay.trade.merge.precreate'; 292 | $params = null; 293 | if (!empty($this->returnUrl)) { 294 | $params['return_url'] = $this->returnUrl; 295 | } 296 | return $this->aopExecute($apiName, $bizContent, $params); 297 | } 298 | 299 | /** 300 | * 手机网站合单支付 301 | * @param array $bizContent 请求参数的集合 302 | * @return string html表单 303 | * @throws Exception 304 | * @see https://opendocs.alipay.com/open/028xra 305 | */ 306 | public function wapMergePay(array $bizContent): string 307 | { 308 | $apiName = 'alipay.trade.wap.merge.pay'; 309 | return $this->aopPageExecute($apiName, $bizContent); 310 | } 311 | 312 | /** 313 | * APP合单支付 314 | * @param array $bizContent 请求参数的集合 315 | * @return string SDK请求串 316 | * @see https://opendocs.alipay.com/open/028py8 317 | */ 318 | public function appMergePay(array $bizContent): string 319 | { 320 | $apiName = 'alipay.trade.app.merge.pay'; 321 | return $this->aopSdkExecute($apiName, $bizContent); 322 | } 323 | 324 | /** 325 | * 小程序合单支付 326 | * @param array $bizContent 请求参数的集合 327 | * @return mixed {"out_merge_no":"外部合并单号","merge_no":"合并交易号","order_detail_results":[]} 328 | * @throws Exception 329 | * @see https://opendocs.alipay.com/open/0a0yaq 330 | */ 331 | public function mergeCreate(array $bizContent) 332 | { 333 | $apiName = 'alipay.trade.merge.create'; 334 | return $this->aopExecute($apiName, $bizContent); 335 | } 336 | 337 | /** 338 | * 线上资金授权冻结 339 | * @param array $bizContent 请求参数的集合 340 | * @return string SDK请求串 341 | * @see https://opendocs.alipay.com/open/repo-0243e2 342 | */ 343 | public function preAuthFreeze(array $bizContent): string 344 | { 345 | $apiName = 'alipay.fund.auth.order.app.freeze'; 346 | return $this->aopSdkExecute($apiName, $bizContent); 347 | } 348 | 349 | /** 350 | * 资金授权解冻 351 | * @param array $bizContent 请求参数的集合 352 | * @return mixed 353 | * @throws Exception 354 | */ 355 | public function preAuthUnfreeze(array $bizContent) 356 | { 357 | $apiName = 'alipay.fund.auth.order.unfreeze'; 358 | return $this->aopExecute($apiName, $bizContent); 359 | } 360 | 361 | /** 362 | * 资金授权撤销 363 | * @param array $bizContent 请求参数的集合 364 | * @return mixed 365 | * @throws Exception 366 | */ 367 | public function preAuthCancel(array $bizContent) 368 | { 369 | $apiName = 'alipay.fund.auth.operation.cancel'; 370 | return $this->aopExecute($apiName, $bizContent); 371 | } 372 | 373 | /** 374 | * 资金授权操作查询接口 375 | * @param array $bizContent 请求参数的集合 376 | * @return mixed 377 | * @throws Exception 378 | */ 379 | public function preAuthQuery(array $bizContent) 380 | { 381 | $apiName = 'alipay.fund.auth.operation.detail.query'; 382 | return $this->aopExecute($apiName, $bizContent); 383 | } 384 | 385 | /** 386 | * 资金转账页面支付接口 387 | * @param array $bizContent 请求参数的集合 388 | * @return string 389 | * @throws Exception 390 | */ 391 | public function transPagePay(array $bizContent): string 392 | { 393 | $apiName = 'alipay.fund.trans.page.pay'; 394 | return $this->aopPageExecute($apiName, $bizContent); 395 | } 396 | 397 | /** 398 | * 现金红包无线支付接口 399 | * @param array $bizContent 请求参数的集合 400 | * @return string 401 | */ 402 | public function transAppPay(array $bizContent): string 403 | { 404 | $apiName = 'alipay.fund.trans.app.pay'; 405 | return $this->aopSdkExecute($apiName, $bizContent); 406 | } 407 | } -------------------------------------------------------------------------------- /src/Aop/AopClient.php: -------------------------------------------------------------------------------- 1 | build($request); 99 | 100 | $url = $this->gatewayUrl.'?charset='.$this->charset; 101 | $raw = $this->curl($url, $params); 102 | 103 | $response = new AlipayResponse($raw, $request->getApiMethodName()); 104 | 105 | $this->verifyResponse($response); 106 | 107 | return $response; 108 | } 109 | 110 | /** 111 | * 生成用于调用收银台SDK的字符串 112 | * 113 | * @param AlipayRequest $request 114 | * @return string 115 | * @throws Exception 116 | */ 117 | public function sdkExecute(AlipayRequest $request): string 118 | { 119 | $params = $this->build($request); 120 | 121 | return http_build_query($params); 122 | } 123 | 124 | /** 125 | * 页面提交执行方法 126 | * 127 | * @param AlipayRequest $request 128 | * @param string $httpmethod 129 | * @return string 130 | * @throws Exception 131 | */ 132 | public function pageExecute(AlipayRequest $request, string $httpmethod = 'POST'): string 133 | { 134 | $params = $this->build($request); 135 | 136 | if (strtoupper($httpmethod) == 'REDIRECT') { 137 | $requestUrl = $this->gatewayUrl.'?'.http_build_query($params); 138 | 139 | $ch = curl_init(); 140 | curl_setopt($ch, CURLOPT_URL, $requestUrl); 141 | curl_setopt($ch, CURLOPT_FAILONERROR, false); 142 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 143 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 144 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 145 | $response = curl_exec($ch); 146 | if (curl_errno($ch) > 0) { 147 | $errmsg = curl_error($ch); 148 | curl_close($ch); 149 | throw new Exception($errmsg, 0); 150 | } 151 | $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 152 | if ($httpStatusCode == 301 || $httpStatusCode == 302) { 153 | $redirect_url = curl_getinfo($ch, CURLINFO_REDIRECT_URL); 154 | curl_close($ch); 155 | return $redirect_url; 156 | } elseif ($httpStatusCode == 200) { 157 | curl_close($ch); 158 | $response = mb_convert_encoding($response, 'UTF-8', 'GB2312'); 159 | if(preg_match('/([^<]+)<\/div>/i', $response, $matchers)) { 160 | throw new Exception($matchers[1]); 161 | } 162 | } 163 | throw new Exception('返回数据解析失败', $httpStatusCode); 164 | 165 | } elseif (strtoupper($httpmethod) == 'GET') { 166 | return $this->gatewayUrl.'?'.http_build_query($params); 167 | } else { 168 | $url = $this->gatewayUrl.'?charset='.$this->charset; 169 | 170 | $html = "
"; 171 | foreach ($params as $key => $value) { 172 | if ($this->isEmpty($value)) { 173 | continue; 174 | } 175 | $value = htmlentities($value, ENT_QUOTES | ENT_HTML5); 176 | $html .= ""; 177 | } 178 | $html .= "
"; 179 | $html .= ""; 180 | 181 | return $html; 182 | } 183 | } 184 | 185 | /** 186 | * 拼接请求参数并签名. 187 | * 188 | * @param AlipayRequest $request 189 | * 190 | * @return array 191 | * @throws Exception 192 | */ 193 | protected function build(AlipayRequest $request): array 194 | { 195 | // 组装系统参数 196 | $sysParams = []; 197 | $sysParams['app_id'] = $this->appId; 198 | $sysParams['version'] = $this->apiVersion; 199 | $sysParams['alipay_sdk'] = $this->sdkVersion; 200 | 201 | $sysParams['charset'] = $this->charset; 202 | $sysParams['format'] = $this->format; 203 | $sysParams['sign_type'] = $this->signType; 204 | 205 | $sysParams['method'] = $request->getApiMethodName(); 206 | $sysParams['timestamp'] = $request->getTimestamp(); 207 | $sysParams['notify_url'] = $request->getNotifyUrl(); 208 | $sysParams['return_url'] = $request->getReturnUrl(); 209 | 210 | $sysParams['terminal_type'] = $request->getTerminalType(); 211 | $sysParams['terminal_info'] = $request->getTerminalInfo(); 212 | $sysParams['prod_code'] = $request->getProdCode(); 213 | 214 | $sysParams['auth_token'] = $request->getAuthToken(); 215 | $sysParams['app_auth_token'] = $request->getAppAuthToken(); 216 | if (!$this->isEmpty($this->appCertSN) && !$this->isEmpty($this->alipayRootCertSN)) { 217 | $sysParams["app_cert_sn"] = $this->appCertSN; 218 | $sysParams["alipay_root_cert_sn"] = $this->alipayRootCertSN; 219 | } 220 | $sysParams['biz_content'] = $request->getBizContent(); 221 | $sysParams = array_merge($sysParams, get_object_vars($request)); 222 | // 转换可能是数组的参数 223 | foreach ($sysParams as $key => &$param) { 224 | if (is_array($param) || is_object($param) && !$param instanceof \CURLFile) { 225 | $param = json_encode($param, JSON_UNESCAPED_UNICODE); 226 | } 227 | if (is_null($param)) { 228 | unset($sysParams[$key]); 229 | } 230 | } 231 | 232 | // 签名 233 | $sysParams['sign'] = $this->generateSign($sysParams, $this->signType); 234 | 235 | return $sysParams; 236 | } 237 | 238 | /** 239 | * 验证返回内容签名 240 | * 241 | * @param AlipayResponse $response 242 | * @throws Exception 243 | */ 244 | protected function verifyResponse(AlipayResponse $response) 245 | { 246 | $signData = $response->getSignData(); 247 | $sign = $response->getSign(); 248 | if ($this->isEmpty($signData) || $this->isEmpty($sign)) { 249 | throw new AlipayResponseException($response->getData()); 250 | } 251 | $checkResult = $this->rsaPubilcVerify($signData, $sign, $this->signType); 252 | if (!$checkResult) { 253 | if (strpos($signData, '\/') > 0) { 254 | $signData = str_replace('\/', '/', $signData); 255 | $checkResult = $this->rsaPubilcVerify($signData, $sign, $this->signType); 256 | } 257 | if (!$checkResult) { 258 | throw new Exception('对返回数据使用支付宝公钥验签失败'); 259 | } 260 | } 261 | } 262 | 263 | /** 264 | * 异步通知回调验签 265 | * 266 | * @param $params 267 | * 268 | * @return bool 269 | */ 270 | public function verify($params): bool 271 | { 272 | if (!$params || !isset($params['sign'])) { 273 | return false; 274 | } 275 | $sign = $params['sign']; 276 | unset($params['sign']); 277 | unset($params['sign_type']); 278 | $data = $this->getSignContent($params); 279 | try { 280 | return $this->rsaPubilcVerify($data, $sign, $this->signType); 281 | } catch (Exception $ex) { 282 | return false; 283 | } 284 | } 285 | 286 | /** 287 | * 异步通知回调验签V2 288 | * 289 | * @param $params 290 | * 291 | * @return bool 292 | */ 293 | public function verifyV2($params): bool 294 | { 295 | if (!$params || !isset($params['sign'])) { 296 | return false; 297 | } 298 | $sign = $params['sign']; 299 | unset($params['sign']); 300 | $data = $this->getSignContent($params); 301 | try { 302 | return $this->rsaPubilcVerify($data, $sign, $this->signType); 303 | } catch (Exception $ex) { 304 | return false; 305 | } 306 | } 307 | 308 | /** 309 | * 将参数数组签名(计算 Sign 值). 310 | * 311 | * @param array $params 参数数组 312 | * @param string $signType 签名类型 313 | * 314 | * @return string 315 | * @throws Exception 316 | */ 317 | protected function generateSign(array $params, string $signType = 'RSA2'): string 318 | { 319 | $data = $this->getSignContent($params); 320 | 321 | return $this->rsaPrivateSign($data, $signType); 322 | } 323 | 324 | /** 325 | * 将数组转换为待签名数据. 326 | * 327 | * @param array $params 参数数组 328 | * 329 | * @return string 330 | */ 331 | protected function getSignContent(array $params): string 332 | { 333 | ksort($params); 334 | unset($params['sign']); 335 | 336 | $stringToBeSigned = ""; 337 | foreach ($params as $k => $v) { 338 | if($v instanceof \CURLFile || $this->isEmpty($v) || substr($v, 0, 1) == '@') continue; 339 | $stringToBeSigned .= "&{$k}={$v}"; 340 | } 341 | return substr($stringToBeSigned, 1); 342 | } 343 | 344 | /** 345 | * 使用应用私钥签名 346 | * 347 | * @param string $data 待签名数据 348 | * @param string $signType 签名类型 349 | * 350 | * @return string 351 | * 352 | * @throws Exception 353 | * @see https://docs.open.alipay.com/291/106118 354 | */ 355 | protected function rsaPrivateSign(string $data, string $signType = 'RSA2'): string 356 | { 357 | if ($this->isEmpty($this->rsaPrivateKeyFilePath)) { 358 | $priKey = "-----BEGIN RSA PRIVATE KEY-----\n" . 359 | wordwrap($this->rsaPrivateKey, 64, "\n", true) . 360 | "\n-----END RSA PRIVATE KEY-----"; 361 | } else { 362 | $priKey = file_get_contents($this->rsaPrivateKeyFilePath); 363 | } 364 | $res = openssl_get_privatekey($priKey); 365 | if(!$res){ 366 | throw new Exception('签名失败,应用私钥不正确'); 367 | } 368 | 369 | if($signType == 'RSA2'){ 370 | openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256); 371 | }else{ 372 | openssl_sign($data, $sign, $res); 373 | } 374 | if(is_resource($res)){ 375 | openssl_free_key($res); 376 | } 377 | return base64_encode($sign); 378 | } 379 | 380 | /** 381 | * 使用支付宝公钥验签 382 | * 383 | * @param string $data 待验签数据 384 | * @param string $sign 签名 385 | * @param string $signType 386 | * @return bool 387 | * @throws Exception 388 | */ 389 | public function rsaPubilcVerify(string $data, string $sign, string $signType = 'RSA2'): bool 390 | { 391 | if ($this->isEmpty($this->rsaPublicKeyFilePath)) { 392 | $pubKey = "-----BEGIN PUBLIC KEY-----\n" . 393 | wordwrap($this->rsaPublicKey, 64, "\n", true) . 394 | "\n-----END PUBLIC KEY-----"; 395 | } else { 396 | $pubKey = file_get_contents($this->rsaPublicKeyFilePath); 397 | } 398 | $res = openssl_get_publickey($pubKey); 399 | if(!$res){ 400 | throw new Exception('验签失败,支付宝公钥不正确'); 401 | } 402 | 403 | if($signType == 'RSA2'){ 404 | $result = openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256); 405 | }else{ 406 | $result = openssl_verify($data, base64_decode($sign), $res); 407 | } 408 | if(is_resource($res)){ 409 | openssl_free_key($res); 410 | } 411 | return $result === 1; 412 | } 413 | 414 | /** 415 | * 校验某字符串或可被转换为字符串的数据,是否为 NULL 或均为空白字符. 416 | * 417 | * @param string|null $value 418 | * 419 | * @return bool 420 | */ 421 | protected function isEmpty(?string $value): bool 422 | { 423 | return $value === null || trim($value) === ''; 424 | } 425 | 426 | /** 427 | * 发起 GET/POST 请求. 428 | * 429 | * @param string $url 请求地址 430 | * @param array|null $postFields POST 数据 431 | * @return bool|string 432 | * @throws Exception 433 | */ 434 | protected function curl(string $url, array $postFields = null) 435 | { 436 | $ch = curl_init(); 437 | 438 | curl_setopt($ch, CURLOPT_URL, $url); 439 | curl_setopt($ch, CURLOPT_FAILONERROR, false); 440 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 441 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 442 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 443 | 444 | if (is_array($postFields) && 0 < count($postFields)) { 445 | $postMultipart = false; 446 | foreach ($postFields as &$value) { 447 | if ($value instanceof \CURLFile) { 448 | $postMultipart = true; 449 | } elseif(substr($value, 0, 1) == '@' && class_exists('CURLFile')) { 450 | $postMultipart = true; 451 | $file = substr($value, 1); 452 | if(file_exists($file)){ 453 | $value = new \CURLFile($file); 454 | } 455 | } 456 | } 457 | curl_setopt($ch, CURLOPT_POST, true); 458 | if($postMultipart){ 459 | curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); 460 | }else{ 461 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields)); 462 | } 463 | } 464 | 465 | $response = curl_exec($ch); 466 | 467 | if (curl_errno($ch) > 0) { 468 | $errmsg = curl_error($ch); 469 | curl_close($ch); 470 | throw new Exception($errmsg, 0); 471 | } 472 | 473 | $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 474 | if ($httpStatusCode != 200) { 475 | curl_close($ch); 476 | throw new Exception($response, $httpStatusCode); 477 | } 478 | 479 | curl_close($ch); 480 | 481 | return $response; 482 | } 483 | 484 | 485 | } 486 | --------------------------------------------------------------------------------