wxpay.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. <?php
  2. declare (strict_types = 1);
  3. namespace library\utils;
  4. // +----------------------------------------------------------------------
  5. // | [ WE CAN DO IT MORE SIMPLE ]
  6. // +----------------------------------------------------------------------
  7. // | Copyright (c) 2018-2020 rights reserved.
  8. // +----------------------------------------------------------------------
  9. // | Author: TABLE ME
  10. // +----------------------------------------------------------------------
  11. // | Date: 2020-10-04 18:31
  12. // +----------------------------------------------------------------------
  13. use GuzzleHttp\Exception\RequestException;
  14. use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
  15. use WechatPay\GuzzleMiddleware\Util\PemUtil;
  16. use GuzzleHttp\HandlerStack as HandlerStackWx;
  17. use GuzzleHttp\Client as GuzzleHttpClient;
  18. class wxpay {
  19. const KEY_LENGTH_BYTE = 32;
  20. const AUTH_TAG_LENGTH_BYTE = 16;
  21. private $config;
  22. private $client;
  23. public $errorMsg = "操作失败";
  24. /**
  25. * 构造函数
  26. * @param type $config
  27. */
  28. public function __construct($config = [])
  29. {
  30. if(empty($config)) $config = config('wxpay');
  31. $this->config = $config;
  32. $merchantPrivateKey = PemUtil::loadPrivateKey($this->config["PrivateKey"]); // 商户私钥文件路径
  33. // 微信支付平台配置
  34. $wechatpayCertificate = PemUtil::loadCertificate($this->config["Certificate"]); // 微信支付平台证书文件路径
  35. // 构造一个WechatPayMiddleware
  36. $wechatpayMiddleware = WechatPayMiddleware::builder()
  37. ->withMerchant($this->config["MCHID"],$this->config["merchantSerialNumber"], $merchantPrivateKey) // 传入商户相关配置
  38. ->withWechatPay([ $wechatpayCertificate ]) // 可传入多个微信支付平台证书,参数类型为array
  39. ->build();
  40. // 将WechatPayMiddleware添加到Guzzle的HandlerStack中
  41. $stack = HandlerStackWx::create();
  42. $stack->push($wechatpayMiddleware, 'wechatpay');
  43. // 创建Guzzle HTTP Client时,将HandlerStack传入,接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
  44. $this->client = new GuzzleHttpClient(['handler' => $stack]);
  45. }
  46. /**
  47. * 微信小程序支付
  48. * @param type $post
  49. */
  50. public function wxmpPay($post=[]){
  51. $apiUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
  52. $jsonData = [];
  53. //商户信息
  54. $jsonData["appid"] = $this->config["APPID"];
  55. $jsonData["mchid"] = $this->config["MCHID"];
  56. $jsonData["notify_url"] = $this->config["NOTIFY_URL"];
  57. //必填参数
  58. $jsonData["description"] = $post["description"];//描述
  59. $jsonData["out_trade_no"] = $post["out_trade_no"];//商户订单号
  60. $jsonData["amount"] = [
  61. "total"=> floatval($post["total"])*100,//订单金额,分
  62. ];
  63. // $jsonData["scene_info"] = [//支付场景描述
  64. // "payer_client_ip"=>$post["payer_client_ip"],//客户端IP
  65. // ];
  66. $jsonData["time_expire"] = date("Y-m-d\TH:i:s+08:00",time()+30*60);//交易结束时间2018-06-08T10:34:56+08:00
  67. $jsonData["payer"]=[
  68. "openid"=>$post["openid"]
  69. ];
  70. $result = $this->clientHttp("POST", $apiUrl, $jsonData);
  71. if(empty($result)){
  72. $this->errorMsg = "支付错误001";
  73. return false;
  74. }
  75. $resuleAr = json_decode($result,true);
  76. if(empty($resuleAr)){
  77. $this->errorMsg = "支付错误002";
  78. return false;
  79. }
  80. if(empty($resuleAr["prepay_id"])){
  81. $this->errorMsg = "支付错误003";
  82. return false;
  83. }
  84. //组装支付参数
  85. $payInfo=array();
  86. $info['appId'] = $this->config["APPID"];
  87. $info['timeStamp'] = time();
  88. $info['nonceStr'] = $this->random_number(); //生成随机数,下面有生成实例,统一下单接口需要
  89. $info["package"] = "prepay_id=".$result['prepay_id'];
  90. $info['signType'] = 'MD5';
  91. $info['paySign'] = $this->MakeSign($info);
  92. return $info;
  93. }
  94. /**
  95. * h5支付
  96. */
  97. public function wapPay($post=[]){
  98. $apiUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5";
  99. $jsonData = [];
  100. //商户信息
  101. $jsonData["appid"] = $this->config["APPID"];
  102. $jsonData["mchid"] = $this->config["MCHID"];
  103. $jsonData["notify_url"] = $this->config["NOTIFY_URL"];
  104. //必填参数
  105. $jsonData["description"] = $post["description"];//描述
  106. $jsonData["out_trade_no"] = $post["out_trade_no"];//商户订单号
  107. $jsonData["amount"] = [
  108. "total"=> floatval($post["total"])*100,//订单金额,分
  109. ];
  110. $jsonData["scene_info"] = [//支付场景描述
  111. "payer_client_ip"=>$post["payer_client_ip"],//客户端IP
  112. "h5_info"=>[//h5场景信息
  113. "type"=>"Wap",//iOS, Android, Wap
  114. ]
  115. ];
  116. $jsonData["time_expire"] = date("Y-m-d\TH:i:s+08:00",time()+30*60);//交易结束时间2018-06-08T10:34:56+08:00
  117. $result = $this->clientHttp("POST", $apiUrl, $jsonData);
  118. if($result){
  119. $resuleAr = json_decode($result,true);
  120. if(empty($resuleAr) || empty($resuleAr["h5_url"])){
  121. $this->errorMsg = "h5_url不存在";
  122. return false;
  123. }
  124. return $resuleAr["h5_url"];
  125. }
  126. return false;
  127. }
  128. /**
  129. * app支付
  130. */
  131. public function appPay($post=[]){
  132. $apiUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
  133. $jsonData = [];
  134. //商户信息
  135. $jsonData["appid"] = $this->config["APPID"];
  136. $jsonData["mchid"] = $this->config["MCHID"];
  137. $jsonData["notify_url"] = $this->config["NOTIFY_URL"];
  138. //必填参数
  139. $jsonData["description"] = $post["description"];//描述
  140. $jsonData["out_trade_no"] = $post["out_trade_no"];//商户订单号
  141. $jsonData["amount"] = [
  142. "total"=> floatval($post["total"])*100,//订单金额,分
  143. ];
  144. $jsonData["scene_info"] = [//支付场景描述
  145. "payer_client_ip"=>$post["payer_client_ip"],//客户端IP
  146. ];
  147. $jsonData["time_expire"] = date("Y-m-d\TH:i:s+08:00",time()+30*60);//交易结束时间2018-06-08T10:34:56+08:00
  148. $this->clientHttp("POST", $apiUrl, $jsonData);
  149. }
  150. /**
  151. * 商户订单号
  152. * @param type $out_trade_no
  153. */
  154. public function searchNotifyOrder($out_trade_no){
  155. $apiUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{$out_trade_no}?mchid={$this->config["MCHID"]}";
  156. return $this->clientHttp("GET", $apiUrl);
  157. }
  158. /**
  159. * 关闭支付订单
  160. * @param type $out_trade_no
  161. */
  162. public function closePayOrder($out_trade_no){
  163. $apiUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{$out_trade_no}/close";
  164. $postData=array(
  165. "out_trade_no"=>$out_trade_no,
  166. "mchid"=>$this->config["MCHID"],
  167. );
  168. return $this->clientHttp("POST", $apiUrl,$postData);
  169. }
  170. /**
  171. * Decrypt AEAD_AES_256_GCM ciphertext
  172. *
  173. * @param string $associatedData AES GCM additional authentication data
  174. * @param string $nonceStr AES GCM nonce
  175. * @param string $ciphertext AES GCM cipher text
  176. *
  177. * @return string|bool Decrypted string on success or FALSE on failure
  178. */
  179. public function decryptToString($associatedData, $nonceStr, $ciphertext)
  180. {
  181. if (strlen($this->config['aesKey']) != self::KEY_LENGTH_BYTE) {
  182. $this->errorMsg = "无效的ApiV3Key,长度应为32个字节";
  183. return false;
  184. }
  185. $ciphertext = \base64_decode($ciphertext);
  186. if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
  187. $this->errorMsg = "字符长度错误";
  188. return false;
  189. }
  190. // ext-sodium (default installed on >= PHP 7.2)
  191. if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
  192. return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->config['aesKey']);
  193. }
  194. // ext-libsodium (need install libsodium-php 1.x via pecl)
  195. if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
  196. return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->config['aesKey']);
  197. }
  198. // openssl (PHP >= 7.1 support AEAD)
  199. if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
  200. $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
  201. $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
  202. return \openssl_decrypt($ctext, 'aes-256-gcm', $this->config['aesKey'], \OPENSSL_RAW_DATA, $nonceStr,$authTag, $associatedData);
  203. }
  204. $this->errorMsg = "AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php";
  205. return false;
  206. // throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
  207. }
  208. /**
  209. * http提交
  210. */
  211. public function clientHttp($type='POST',$url='',$json=[]){
  212. $httpData = [];
  213. if($type == 'POST'){
  214. $httpData["json"] = $json;
  215. }
  216. $httpData["headers"] = ['Accept' => 'application/json'];
  217. $resp = $this->client->request($type,$url,$httpData);
  218. $statusCode = $resp->getStatusCode();
  219. if ($statusCode == 200) { //处理成功
  220. return $resp->getBody()->getContents();
  221. } else if ($statusCode == 204) { //处理成功,无返回Body
  222. $this->errorMsg = "处理成功,无返回Body";
  223. return false;
  224. }else{
  225. $this->errorMsg = "未知错误";
  226. return false;
  227. }
  228. // try{
  229. // $resp = $this->client->request($type,$url,$httpData);
  230. // $statusCode = $resp->getStatusCode();
  231. // var_dump($statusCode);
  232. // if ($statusCode == 200) { //处理成功
  233. // return $resp->getBody()->getContents();
  234. // } else if ($statusCode == 204) { //处理成功,无返回Body
  235. // $this->errorMsg = "处理成功,无返回Body";
  236. // return false;
  237. // }else{
  238. // $this->errorMsg = "未知错误";
  239. // return false;
  240. // }
  241. // } catch (RequestException $e){
  242. // var_dump($e);
  243. // // 进行错误处理
  244. // $this->errorMsg = $e->getMessage();
  245. // if ($e->hasResponse()) {
  246. // $this->errorMsg = $e->getResponse()->getBody()->getContents();
  247. //// $this->errorMsg = "failed,resp code = " . $e->getResponse()->getStatusCode() . " return body = " . $e->getResponse()->getBody();
  248. // }
  249. // return false;
  250. // }
  251. }
  252. /**
  253. * 生成签名
  254. * @return 签名,本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
  255. */
  256. public function MakeSign($values) {
  257. //签名步骤一:按字典序排序参数
  258. ksort($values);
  259. $string = $this->ToUrlParams($values);
  260. //签名步骤二:在string后加入KEY
  261. $string = $string . "&key=" . \WxPayConfig::KEY;
  262. //签名步骤三:MD5加密
  263. $string = md5($string);
  264. //签名步骤四:所有字符转为大写
  265. $result = strtoupper($string);
  266. return $result;
  267. }
  268. /**
  269. * 参数数组转换为url参数
  270. * @param array $urlObj
  271. */
  272. private function ToUrlParams($urlObj) {
  273. $buff = "";
  274. foreach ($urlObj as $k => $v) {
  275. $buff .= $k . "=" . $v . "&";
  276. }
  277. $buff = trim($buff, "&");
  278. return $buff;
  279. }
  280. /**
  281. * 生成随机字符串
  282. * @param type $len
  283. * @param type $format
  284. * @return type
  285. */
  286. public function random_number($len = 21, $format = 'ALL') {
  287. $is_abc = $is_numer = 0;
  288. $password = $tmp = '';
  289. switch ($format) {
  290. case 'ALL':
  291. $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  292. break;
  293. case 'CHAR':
  294. $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  295. break;
  296. case 'NUMBER':
  297. $chars = '0123456789';
  298. break;
  299. default :
  300. $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  301. break;
  302. }
  303. mt_srand((double) microtime() * 1000000 * getmypid());
  304. while (strlen($password) < $len) {
  305. $tmp = substr($chars, (mt_rand() % strlen($chars)), 1);
  306. if (($is_numer <> 1 && is_numeric($tmp) && $tmp > 0 ) || $format == 'CHAR') {
  307. $is_numer = 1;
  308. }
  309. if (($is_abc <> 1 && preg_match('/[a-zA-Z]/', $tmp)) || $format == 'NUMBER') {
  310. $is_abc = 1;
  311. }
  312. $password .= $tmp;
  313. }
  314. if ($is_numer <> 1 || $is_abc <> 1 || empty($password)) {
  315. $password = $this->random_number($len, $format);
  316. }
  317. return $password;
  318. }
  319. }