setWechat($wechat); } public function setWechat($wechat) { $this->wechat = $wechat; $data = $this->wechat->getConfig(); $this->service_app_id = $data['app_id']; $this->mch_private_key = $data['key_path']; $this->public_key_path = $data['cert_path']; $this->mch_id = $data['mch_id']; $this->mch_api_key = $data['key']; // $this->serial_no = $this->parseSerialNo($this->getCertFicates()); \Yii::error([$this->mch_id, $this->mch_api_key, $this->serial_no, $this->service_app_id]); } /** * 获取微信支付平台证书 */ public function certFicates() { $url = $this->certificates_url; // 获取认证信息 $authorization = $this->getAuthorization($url); $header = [ 'Content-Type:application/json', 'Accept:application/json', 'User-Agent:' . $this->mch_id, 'Authorization:' . $authorization ]; $json = $this->getCurl('GET', $url, '', $header); $data = json_decode($json, true); if (isset($data['code']) && isset($data['message'])) { return [ 'code' => 1, 'msg' => $data['message'] ]; } if (empty($cfdata = $data['data'][0])) { return [ 'code' => 1, 'msg' => '返回错误' ]; } return $cfdata; } /** * 获取认证信息 * @param string $url * @param string $http_method * @param string $body * @return string * @throws Exception */ private function getAuthorization($url, $http_method = 'GET', $body = '') { if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \Exception("当前PHP环境不支持SHA256withRSA"); } //私钥地址 $mch_private_key = $this->mch_private_key; //商户号 $merchant_id = $this->mch_id; //当前时间戳 $timestamp = time(); //随机字符串 $nonce = $this->getNonceStr(); //证书编号 $serial_no = $this->serial_no; $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $message = $http_method . "\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; openssl_sign($message, $raw_sign, \openssl_get_privatekey(\file_get_contents($mch_private_key)), 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $schema = 'WECHATPAY2-SHA256-RSA2048'; $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $merchant_id, $nonce, $timestamp, $serial_no, $sign); return $schema . ' ' . $token; } /** * 敏感字符加密 * @param $str * @return string * @throws Exception */ private function getEncrypt($str) { static $content; if (empty($content)) { $content = $this->getCertFicates(); } // 去除内容中的空格 $str = preg_replace("/\s| /", '', $str); $encrypted = ''; if (openssl_public_encrypt($str, $encrypted, $content, OPENSSL_PKCS1_OAEP_PADDING)) { //base64编码 $sign = base64_encode($encrypted); } else { throw new \Exception('encrypt failed'); } return $sign; } /** * 获取支付平台证书 * @return false|string */ private function getCertFicates() { if($this->pay_public_key){ return $this->pay_public_key; } if(!$this->public_key_path){ $Merchant = new Merchant(); return $Merchant->getCertFicates(); } $public_key_path = $this->public_key_path; if (!file_exists($public_key_path)) { \Yii::error([__METHOD__]); $cfData = $this->certFicates(); $content = $this->decryptToString($cfData['encrypt_certificate']['associated_data'], $cfData['encrypt_certificate']['nonce'], $cfData['encrypt_certificate']['ciphertext'], $this->mch_api_key); file_put_contents($public_key_path, $content); } else { $content = file_get_contents($public_key_path); } return $content; } /** * 业务编号 * @return string */ public function getBusinessCode() { return date('Ymd') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99)); } /** * 随机字符串 * @param int $length * @return string */ private function getNonceStr($length = 16) { // 密码字符集,可任意添加你需要的字符 $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $str = ""; for ($i = 0; $i < $length; $i++) { $str .= $chars[mt_rand(0, strlen($chars) - 1)]; } return $str; } /** * @param string $method * @param string $url * @param array|string $data * @param array $headers * @param int $timeout * @return bool|string */ private function getCurl($method = 'GET', $url, $data, $headers = [], $timeout = 30) { $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_SSLVERSION, false); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); if (!empty($headers)) { curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); } if ($method == 'POST') { curl_setopt($curl, CURLOPT_POST, TRUE); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } $result = curl_exec($curl); curl_close($curl); return $result; } /** * Decrypt AEAD_AES_256_GCM ciphertext(官方案例-已改造) * * @param string $associatedData AES GCM additional authentication data * @param string $nonceStr AES GCM nonce * @param string $ciphertext AES GCM cipher text * * @return string|bool Decrypted string on success or FALSE on failure */ public function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey) { $auth_tag_length_byte = 16; $ciphertext = \base64_decode($ciphertext); if (strlen($ciphertext) <= $auth_tag_length_byte) { return false; } // ext-sodium (default installed on >= PHP 7.2) if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // ext-libsodium (need install libsodium-php 1.x via pecl) if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); } // openssl (PHP >= 7.1 support AEAD) if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { $ctext = substr($ciphertext, 0, -$auth_tag_length_byte); $authTag = substr($ciphertext, -$auth_tag_length_byte); return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData); } throw new \Exception('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php'); } /** * 获取证书编号(官方案例-已改造) * @param $certificate * @return string */ private function parseSerialNo($certificate) { $info = \openssl_x509_parse($certificate); if (!isset($info['serialNumber']) && !isset($info['serialNumberHex'])) { throw new \InvalidArgumentException('证书格式错误'); } $serialNo = ''; // PHP 7.0+ provides serialNumberHex field if (isset($info['serialNumberHex'])) { $serialNo = $info['serialNumberHex']; } else { // PHP use i2s_ASN1_INTEGER in openssl to convert serial number to string, // i2s_ASN1_INTEGER may produce decimal or hexadecimal format, // depending on the version of openssl and length of data. if (\strtolower(\substr($info['serialNumber'], 0, 2)) == '0x') { // HEX format $serialNo = \substr($info['serialNumber'], 2); } else { // DEC format $value = $info['serialNumber']; $hexvalues = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; while ($value != '0') { $serialNo = $hexvalues[\bcmod($value, '16')] . $serialNo; $value = \bcdiv($value, '16', 0); } } } return \strtoupper($serialNo); } //商家转账 public function transferBatches($store_id, $data) { $url = 'https://api.mch.weixin.qq.com/v3/transfer/batches'; $_data = [ 'appid' => $this->service_app_id, 'out_batch_no' => $data['partner_trade_no'], 'batch_name' => $data['partner_trade_no'], 'batch_remark' => $data['desc'], 'total_amount' => (int)$data['amount'], 'total_num' => 1, 'transfer_detail_list' => [[ 'out_detail_no' => $data['partner_trade_no'], 'transfer_amount' => (int)$data['amount'], 'transfer_remark' => $data['desc'], 'openid' => $data['openid'], ]], 'notify_url' => 'https://' . \Yii::$app->request->hostName . '/index.php/wechat/transfer-batches-notify', ]; $serial_no = $this->serial_no; $store = Store::findOne($store_id); $wechat_cash = Option::get('wechat_cash', $store_id, 'store')['value']; if($store->is_platform_transfers == 1){ $wechat_cash = Option::get('store_wechat_cash', $store_id, 'store')['value']; } if ($wechat_cash) { $wechat_cash = json_decode($wechat_cash, true); } if ($wechat_cash && $wechat_cash['appid']) { $this->serial_no = $wechat_cash['serial_no']; $this->pay_public_key = $wechat_cash['pay_public_key']; $this->pay_public_key_id = $wechat_cash['pay_public_key_id']; if (intval($wechat_cash['is_open']) === 2) { $url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'; $_data = [ 'appid' => $wechat_cash['appid'], 'out_bill_no' => $data['partner_trade_no'], 'transfer_scene_id' => '1000',//转账场景ID 'openid' => $data['openid'], 'user_name' => $this->getEncrypt($data['user_name']),//收款方真实姓名 'transfer_amount' => (int)$data['amount'], 'transfer_remark' => $data['desc'], 'notify_url' => pay_notify_url('wechat/transfer-batches-notify', $store_id), // 'user_recv_perception' => '劳务报酬', 'transfer_scene_report_infos' => [ [ "info_type" =>"活动名称", "info_content" => "账户提现" ], [ "info_type" =>"奖励说明", "info_content" => "账户金额提现" ] ] ]; if ((int)$data['amount'] < 200000) { $_data['user_name'] = ''; } } else { $_data['appid'] = $wechat_cash['appid']; } if($this->pay_public_key){ $serial_no = $this->pay_public_key_id; }else{ $serial_no = $this->serial_no; } $bodyData = json_encode($_data,JSON_UNESCAPED_UNICODE); debug_log(['data' => $bodyData], 'wechatTransferBatchesNotify.log'); $authorization = $this->getWechatAuthorization($url, 'POST', $bodyData, $store_id); // $wechat_cash = json_decode($wechat_cash, true); // $_data['appid'] = $wechat_cash['appid']; // $_data['mch_id'] = $wechat_cash['mch_id']; // // $config = $_data; //// $cert_pem_file = null; //// if ($this->wechat_config->cert_pem) { //// $cert_pem_file = \Yii::$app->runtimePath . '/pem/' . md5($this->wechat_config->cert_pem); //// if (!file_exists($cert_pem_file)) { //// file_put_contents($cert_pem_file, $this->wechat_config->cert_pem); //// } //// } // // $wechat = Factory::payment($config); } else { $bodyData = json_encode($_data); // 获取认证信息 $hasIncoming = \app\models\Store::hasIncoming($store_id); if($hasIncoming){ return $this->res([ 'code' => 1, 'msg' => '店铺内未配置提现参数', ]); // $Merchant = new Merchant(); // $authorization = $Merchant->getAuthorization($url, 'POST', $bodyData); }else{ $this->serial_no = Option::get('platform_mch_api_serial_no', 0, 'saas')['value']; $this->pay_public_key = Option::get('platform_mch_public_key', 0, 'saas')['value']; $this->pay_public_key_id = Option::get('platform_mch_public_key_id', 0, 'saas')['value']; if($this->pay_public_key){ $serial_no = $this->pay_public_key_id; }else{ $serial_no = $this->serial_no; } //商城提现 $url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'; $_data = [ 'appid' => $this->service_app_id, 'out_bill_no' => $data['partner_trade_no'], 'transfer_scene_id' => '1000',//转账场景ID 'openid' => $data['openid'], 'user_name' => '',//收款方真实姓名 $this->getEncrypt($data['user_name'])这块先给空 因为前端没有设置真实姓名的地方 'transfer_amount' => (int)$data['amount'], 'transfer_remark' => $data['desc'], 'notify_url' => pay_notify_url('wechat/transfer-batches-notify', $store_id, 1), // 'user_recv_perception' => '劳务报酬', 'transfer_scene_report_infos' => [ [ "info_type" =>"活动名称", "info_content" => "账户提现" ], [ "info_type" =>"奖励说明", "info_content" => "账户金额提现" ] ] ]; if ((int)$data['amount'] < 200000) { $_data['user_name'] = ''; } $bodyData = json_encode($_data,JSON_UNESCAPED_UNICODE); $authorization = $this->getAuthorization($url, 'POST', $bodyData); } } $header = [ 'Content-Type:application/json', 'Accept:application/json', 'User-Agent:*/*', 'Authorization:' . $authorization, 'Wechatpay-Serial:' . $serial_no ]; $json = $this->getCurl('POST', $url, $bodyData, $header); \Yii::error([__METHOD__, $store_id, $_data, $header, $json]); $res = json_decode($json, true); debug_log([$store_id, $_data, $header, $res], 'wechatTransferBatchesNotify.log'); if (isset($res['code']) && isset($res['message'])) { return $this->res([ 'code' => 1, 'msg' => $res['code'] . $res['message'] ]); } if (!($wechat_cash && intval($wechat_cash['is_open']) === 2)) { if (empty($res['batch_id'])) { // return $this->res([ // 'code' => 1, // 'msg' => '返回错误' // ]); } } return $this->res(['code' => 0, 'data' => $res]); } /** * 获取认证信息 * @param string $url * @param string $http_method * @param string $body * @return string * @throws Exception */ private function getWechatAuthorization($url, $http_method = 'GET', $body = '', $store_id = 0) { if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) { throw new \Exception("当前PHP环境不支持SHA256withRSA"); } $store = Store::findOne($store_id); $wechat_cash = Option::get('wechat_cash', $store_id, 'store')['value']; if($store->is_platform_transfers == 1){ $wechat_cash = Option::get('store_wechat_cash', $store_id, 'store')['value']; } if ($wechat_cash) { $wechat_cash = json_decode($wechat_cash, true); $mch_private_key = $wechat_cash['mch_private_key']; $merchant_id = $wechat_cash['mch_id']; $serial_no = $wechat_cash['serial_no']; $timestamp = time(); $nonce = $this->getNonceStr(); $url_parts = parse_url($url); $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")); $message = $http_method . "\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n"; openssl_sign($message, $raw_sign, \openssl_get_privatekey($mch_private_key), 'sha256WithRSAEncryption'); $sign = base64_encode($raw_sign); $schema = 'WECHATPAY2-SHA256-RSA2048'; $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $merchant_id, $nonce, $timestamp, $serial_no, $sign); return $schema . ' ' . $token; } } public function res($res){ if($res['code'] == 0){ $res['result_code'] = 'SUCCESS'; }else{ $res['err_code_des'] = $res['msg']; } return $res; } public function getBatchDetail($store_id, $data) { $out_batch_no = $data['out_batch_no']; $url = "https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no/". $out_batch_no . "/details/out-detail-no/". $out_batch_no; $serial_no = $this->serial_no; $wechat_cash = Option::get('wechat_cash', $store_id, 'store')['value']; if ($wechat_cash) { $wechat_cash = json_decode($wechat_cash, true); $serial_no = $wechat_cash['serial_no']; $bodyData = ''; $authorization = $this->getWechatAuthorization($url, 'GET', $bodyData, $store_id); // $wechat_cash = json_decode($wechat_cash, true); // $_data['appid'] = $wechat_cash['appid']; // $_data['mch_id'] = $wechat_cash['mch_id']; // // $config = $_data; //// $cert_pem_file = null; //// if ($this->wechat_config->cert_pem) { //// $cert_pem_file = \Yii::$app->runtimePath . '/pem/' . md5($this->wechat_config->cert_pem); //// if (!file_exists($cert_pem_file)) { //// file_put_contents($cert_pem_file, $this->wechat_config->cert_pem); //// } //// } // // $wechat = Factory::payment($config); } else { $bodyData = ''; // 获取认证信息 $hasIncoming = \app\models\Store::hasIncoming($store_id); if($hasIncoming){ $Merchant = new Merchant(); $authorization = $Merchant->getAuthorization($url, 'GET', $bodyData); }else{ $authorization = $this->getAuthorization($url, 'GET', $bodyData); } } $header = [ 'Content-Type:application/json', 'Accept:application/json', 'User-Agent:*/*', 'Authorization:' . $authorization, 'Wechatpay-Serial:' . $serial_no ]; $json = $this->getCurl('GET', $url, $bodyData, $header); $res = json_decode($json, true); if (isset($res['code']) && isset($res['message'])) { return $this->res([ 'code' => 1, 'msg' => $res['code'] . $res['message'] ]); } // if ($res['detail_status'] === "WAIT_PAY") { // $res['fail_reason_text'] = "待确认"; // } // if ($res['detail_status'] === "PROCESSING") { // $res['fail_reason_text'] = "转账中"; // } // if ($res['detail_status'] === "SUCCESS") { // $res['fail_reason_text'] = "转账成功"; // } // if ($res['detail_status'] === "FAIL") { // $res['fail_reason_text'] = "转账失败:" . Cash::WX_CASH_FAIL_REASON[$res['fail_reason']]; // } return $this->res(['code' => 0, 'msg' => $res['fail_reason_text'], 'data' => $res]); } }