thinkphp3.2實現微信JSAPI支付
阿新 • • 發佈:2018-12-30
<?php namespace Mobile\Action; class WxPayAction extends BaseAction { private $wxpayConfig; private $wxpay; private $parameters; private $returnParameters; public function _initialize(){ $this->wxpayConfig = array('CURL_TIMEOUT' => 30); $this->wxpayConfig['appid'] = ""; // 微信公眾號身份的唯一標識appid $this->wxpayConfig['appsecret'] = ""; // APP金鑰 $this->wxpayConfig['mchid'] = ""; // 微信支付商戶號 $this->wxpayConfig['key'] = ""; // 商戶支付金鑰Key $this->wxpayConfig['notifyurl'] = ""; //非同步通知地址 $this->wxpayConfig['returnurl'] = ""; //同步通知地址 $this->wxpayConfig['url'] = "https://api.mch.weixin.qq.com/pay/unifiedorder"; } public function index() { } /** * 支付 */ public function pay() { $orderid = "23324321234";//商戶訂單號 $payprice = "0.01";//訂單金額 if(empty($orderid) || empty($payprice)) { die('訂單引數不完整!'); } $openid = $this->get_openid(); // 設定統一支付介面引數 // 設定必填引數 // appid已填,商戶無需重複填寫 // mch_id已填,商戶無需重複填寫 // noncestr已填,商戶無需重複填寫 // spbill_create_ip已填,商戶無需重複填寫 // sign已填,商戶無需重複填寫 $this->setParameter("openid", $openid); $this->setParameter("body", "購買商品"); // 商品描述 // 自定義訂單號,此處僅作舉例 //$timeStamp = time(); //$out_trade_no = \WxPayConf_pub::$APPID . $timeStamp; $out_trade_no = $orderid; //$out_trade_no = time(); $this->setParameter("out_trade_no", $out_trade_no); // 商戶訂單號 $this->setParameter("total_fee", $payprice * 100); // 總金額 $this->setParameter("notify_url", $this->wxpayConfig['notifyurl']); // 通知地址 $this->setParameter("trade_type", "JSAPI"); // 交易型別 // 非必填引數,商戶可根據實際情況選填 //$unifiedOrder->setParameter("sub_mch_id", "XXXX"); // 子商戶號 //$unifiedOrder->setParameter("device_info", "XXXX"); // 裝置號 //$unifiedOrder->setParameter("attach", "XXXX"); // 附加資料 //$unifiedOrder->setParameter("time_start", "XXXX"); // 交易起始時間 //$unifiedOrder->setParameter("time_expire", "XXXX"); // 交易結束時間 //$unifiedOrder->setParameter("goods_tag", "XXXX"); // 商品標記 //$unifiedOrder->setParameter("openid", "XXXX"); // 使用者標識 //$unifiedOrder->setParameter("product_id", "XXXX"); // 商品ID $prepay_id = $this->getPrepayId(); if(empty($prepay_id)) { die('引數出錯,請重試!'); } $this->getParameters($prepay_id); $this->parameters; $returnurl = $this->wxpayConfig['returnurl']; $parameters = json_decode($this->parameters); $ptimeStamp = $parameters->timeStamp; $pnonceStr = $parameters->nonceStr; $ppackage = $parameters->package; $psignType = $parameters->signType; $ppaySign = $parameters->paySign; $signPackage = $this->getSignPackage(); $button = <<<EOT <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>微信支付</title> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0"> </head> <body> <script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script> <script> wx.config({ debug: false, appId: '{$signPackage["appId"]}', timestamp: {$signPackage["timestamp"]}, nonceStr: '{$signPackage["nonceStr"]}', signature: '{$signPackage["signature"]}', jsApiList: ['chooseWXPay'] }); wx.ready(function () { wx.chooseWXPay({ timestamp: {$ptimeStamp}, nonceStr: '{$pnonceStr}', package: '{$ppackage}', signType: '{$psignType}', paySign: '{$ppaySign}', success: function (res) { //alert(JSON.stringify(res)); window.location.href='{$returnurl}'; } }); }); </script> </body> </html> EOT; echo $button; } /** * 伺服器非同步通知頁面路徑 */ public function Paynotify() { /** * 通用通知介面demo * ==================================================== * 支付完成後,微信會把相關支付和使用者資訊傳送到商戶設定的通知URL, * 商戶接收回調資訊後,根據需要設定相應的處理流程。 * * 這裡舉例使用log檔案形式記錄回撥資訊。 */ // 儲存微信的回撥 $xml = $GLOBALS['HTTP_RAW_POST_DATA']; $data = $this->xmlToArray($xml); // 驗證簽名,並回應微信。 // 對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗, // 微信會通過一定的策略(如30分鐘共8次)定期重新發起通知 // 儘可能提高通知的成功率,但微信不保證通知最終能成功。 if($this->checkSign($data) == FALSE){ $this->setReturnParameter("return_code", "FAIL"); // 返回狀態碼 $this->setReturnParameter("return_msg", "簽名失敗"); // 返回資訊 } else { $this->setReturnParameter("return_code", "SUCCESS"); // 設定返回碼 } $returnXml = $this->returnXml(); echo $returnXml; //==商戶根據實際情況設定相應的處理流程,此處僅作舉例======= // 以log檔案形式記錄回撥資訊 // $log_ = new \Log_(); // $log_name = THINK_PATH . "Library/Vendor/Wxpay/jssdk/notify_url.log"; // log檔案路徑 // $log_->log_result($log_name, "【接收到的notify通知】:\n" . $xml . "\n"); if($this->checkSign($data) == TRUE) { if ($data["return_code"] == "FAIL") { // 此處應該更新一下訂單狀態,商戶自行增刪操作 die('【通訊出錯】'.$xml); // $log_->log_result($log_name, "【通訊出錯】:\n" . $xml . "\n"); } elseif ($data["result_code"] == "FAIL"){ // 此處應該更新一下訂單狀態,商戶自行增刪操作 // $log_->log_result($log_name, "【業務出錯】:\n" . $xml . "\n"); die('【業務出錯】'.$xml); } else { // 此處應該更新一下訂單狀態,商戶自行增刪操作 } //商戶自行增加處理流程, //例如:更新訂單狀態 //例如:資料庫操作 //例如:推送支付完成資訊 } } public function ReturnNotify() { /** * 通用通知介面demo * ==================================================== * 支付完成後,微信會把相關支付和使用者資訊傳送到商戶設定的通知URL, * 商戶接收回調資訊後,根據需要設定相應的處理流程。 * * 這裡舉例使用log檔案形式記錄回撥資訊。 */ // 儲存微信的回撥 $xml = $GLOBALS['HTTP_RAW_POST_DATA']; $data = $this->xmlToArray($xml); // 驗證簽名,並回應微信。 // 對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗, // 微信會通過一定的策略(如30分鐘共8次)定期重新發起通知 // 儘可能提高通知的成功率,但微信不保證通知最終能成功。 if($this->checkSign($data) == FALSE){ $this->setReturnParameter("return_code", "FAIL"); // 返回狀態碼 $this->setReturnParameter("return_msg", "簽名失敗"); // 返回資訊 } else { $this->setReturnParameter("return_code", "SUCCESS"); // 設定返回碼 } $returnXml = $this->returnXml(); //echo $returnXml; //==商戶根據實際情況設定相應的處理流程,此處僅作舉例======= // 以log檔案形式記錄回撥資訊 // $log_ = new \Log_(); // $log_name = THINK_PATH . "Library/Vendor/Wxpay/jssdk/notify_url.log"; // log檔案路徑 // $log_->log_result($log_name, "【接收到的notify通知】:\n" . $xml . "\n"); if($this->checkSign($data) == TRUE) { if ($data["return_code"] == "FAIL") { // 此處應該更新一下訂單狀態,商戶自行增刪操作 die('【通訊出錯】'.$xml); // $log_->log_result($log_name, "【通訊出錯】:\n" . $xml . "\n"); } elseif ($data["result_code"] == "FAIL"){ // 此處應該更新一下訂單狀態,商戶自行增刪操作 // $log_->log_result($log_name, "【業務出錯】:\n" . $xml . "\n"); die('【業務出錯】'.$xml); } else { // 此處應該更新一下訂單狀態,商戶自行增刪操作 } //商戶自行增加處理流程, //例如:更新訂單狀態 //例如:資料庫操作 //例如:推送支付完成資訊 }else $this->redirect('Mobile/Orders/orderNoReceive'); } /** * 獲取openid */ private function get_openid() { $openid = $_COOKIE['apiopenid']; if(empty($openid)) { // 通過code獲得openid if (!isset($_GET['code'])) { // 觸發微信返回code碼 $url = $this->createOauthUrlForCode($this->get_url()); Header("Location: " . $url); } else { // 獲取code碼,以獲取openid $code = $_GET['code']; $openid = $this->getOpenId($code); setcookie('apiopenid', $openid, time() + 86400); } } return $openid; } /** * 獲取當前頁面完整URL地址 */ private function get_url() { $sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://'; $php_self = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['SCRIPT_NAME']; $path_info = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : ''; $relate_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $php_self.(isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : $path_info); return $sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . $relate_url; } /** * 作用:生成可以獲得code的url */ private function createOauthUrlForCode($redirectUrl) { $urlObj["appid"] = $this->wxpayConfig['appid']; $urlObj["redirect_uri"] = "$redirectUrl"; $urlObj["response_type"] = "code"; $urlObj["scope"] = "snsapi_base"; $urlObj["state"] = "STATE"."#wechat_redirect"; $bizString = $this->formatBizQueryParaMap($urlObj, false); return "https://open.weixin.qq.com/connect/oauth2/authorize?".$bizString; } /** * 作用:通過curl向微信提交code,以獲取openid */ private function getOpenid($code) { $url = $this->createOauthUrlForOpenid($code); //初始化curl $ch = curl_init(); //設定超時 curl_setopt($ch, CURLOPT_TIMEOUT, $this->wxpayConfig['CURL_TIMEOUT']); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE); curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE); curl_setopt($ch, CURLOPT_HEADER, FALSE); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); //執行curl,結果以jason形式返回 $res = curl_exec($ch); curl_close($ch); //取出openid $data = json_decode($res,true); $this->openid = $data['openid']; return $this->openid; } /** * 作用:生成可以獲得openid的url */ private function createOauthUrlForOpenid($code) { $urlObj["appid"] = $this->wxpayConfig['appid']; $urlObj["secret"] = $this->wxpayConfig['appsecret']; $urlObj["code"] = $code; $urlObj["grant_type"] = "authorization_code"; $bizString = $this->formatBizQueryParaMap($urlObj, false); return "https://api.weixin.qq.com/sns/oauth2/access_token?".$bizString; } /** * 作用:格式化引數,簽名過程需要使用 */ private function formatBizQueryParaMap($paraMap, $urlencode) { $buff = ""; ksort($paraMap); foreach ($paraMap as $k => $v) { if($urlencode) { $v = urlencode($v); } //$buff .= strtolower($k) . "=" . $v . "&"; $buff .= $k . "=" . $v . "&"; } $reqPar = ''; if (strlen($buff) > 0) { $reqPar = substr($buff, 0, strlen($buff)-1); } return $reqPar; } /** * 作用:設定請求引數 */ private function setParameter($parameter, $parameterValue) { $this->parameters[$this->trimString($parameter)] = $this->trimString($parameterValue); } private function trimString($value) { $ret = null; if (null != $value) { $ret = $value; if (strlen($ret) == 0) { $ret = null; } } return $ret; } /** * 獲取prepay_id */ private function getPrepayId() { $response = $this->postXml(); $result = $this->xmlToArray($response); $prepay_id = $result["prepay_id"]; return $prepay_id; } /** * 作用:post請求xml */ private function postXml() { $xml = $this->createXml(); $response = $this->postXmlCurl($xml,$this->wxpayConfig['url'],$this->wxpayConfig['CURL_TIMEOUT']); return $response; } /** * 生成介面引數xml */ private function createXml() { try { // 檢測必填引數 if($this->parameters["out_trade_no"] == null) { throw new \Exception("缺少統一支付介面必填引數out_trade_no!"."<br>"); }elseif($this->parameters["body"] == null){ throw new \Exception("缺少統一支付介面必填引數body!"."<br>"); }elseif ($this->parameters["total_fee"] == null ) { throw new \Exception("缺少統一支付介面必填引數total_fee!"."<br>"); }elseif ($this->parameters["notify_url"] == null) { throw new \Exception("缺少統一支付介面必填引數notify_url!"."<br>"); }elseif ($this->parameters["trade_type"] == null) { throw new \Exception("缺少統一支付介面必填引數trade_type!"."<br>"); }elseif ($this->parameters["trade_type"] == "JSAPI" && $this->parameters["openid"] == NULL){ throw new \Exception("統一支付介面中,缺少必填引數openid!trade_type為JSAPI時,openid為必填引數!"."<br>"); } $this->parameters["appid"] = $this->wxpayConfig['appid']; // 公眾賬號ID $this->parameters["mch_id"] = $this->wxpayConfig['mchid']; // 商戶號 $this->parameters["spbill_create_ip"] = $_SERVER['REMOTE_ADDR'];// 終端ip $this->parameters["nonce_str"] = $this->createNoncestr(); // 隨機字串 $this->parameters["sign"] = $this->getSign($this->parameters); // 簽名 return $this->arrayToXml($this->parameters); } catch (\Exception $e) { die($e->errorMessage()); } } /** * 作用:產生隨機字串,不長於32位 */ private function createNoncestr( $length = 32 ) { $chars = "abcdefghijklmnopqrstuvwxyz0123456789"; $str = ""; for ( $i = 0; $i < $length; $i++ ) { $str.= substr($chars, mt_rand(0, strlen($chars)-1), 1); } return $str; } /** * 作用:生成簽名 */ private function getSign($Obj) { foreach ($Obj as $k => $v) { $Parameters[$k] = $v; } //簽名步驟一:按字典序排序引數 ksort($Parameters); $String = $this->formatBizQueryParaMap($Parameters, false); //echo '【string1】'.$String.'</br>'; //簽名步驟二:在string後加入KEY $String = $String."&key=".$this->wxpayConfig['key']; //echo "【string2】".$String."</br>"; //簽名步驟三:MD5加密 $String = md5($String); //echo "【string3】 ".$String."</br>"; //簽名步驟四:所有字元轉為大寫 $result_ = strtoupper($String); //echo "【result】 ".$result_."</br>"; return $result_; } /** * 作用:array轉xml */ private function arrayToXml($arr) { $xml = "<xml>"; foreach ($arr as $key=>$val) { if (is_numeric($val)) { $xml.="<".$key.">".$val."</".$key.">"; } else { $xml.="<".$key."><![CDATA[".$val."]]></".$key.">"; } } $xml.="</xml>"; return $xml; } /** * 作用:以post方式提交xml到對應的介面url */ private function postXmlCurl($xml,$url,$second = 30) { //初始化curl $ch = curl_init(); //設定超時 curl_setopt($ch, CURLOPT_TIMEOUT, $second); //這裡設定代理,如果有的話 //curl_setopt($ch,CURLOPT_PROXY, '8.8.8.8'); //curl_setopt($ch,CURLOPT_PROXYPORT, 8080); curl_setopt($ch,CURLOPT_URL, $url); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE); curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE); //設定header curl_setopt($ch, CURLOPT_HEADER, FALSE); //要求結果為字串且輸出到螢幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); //post提交方式 curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); //執行curl $data = curl_exec($ch); curl_close($ch); //返回結果 if($data) { //curl_close($ch); return $data; } else { $error = curl_errno($ch); echo "curl出錯,錯誤碼:$error"."<br>"; echo "<a href='http://curl.haxx.se/libcurl/c/libcurl-errors.html'>錯誤原因查詢</a></br>"; curl_close($ch); return false; } } /** * 作用:將xml轉為array */ private function xmlToArray($xml) { //將XML轉為array $array_data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $array_data; } /** * 作用:設定jsapi的引數 */ private function getParameters($prepay_id) { $jsApiObj["appId"] = $this->wxpayConfig['appid']; $timeStamp = time(); $jsApiObj["timeStamp"] = "$timeStamp"; $jsApiObj["nonceStr"] = $this->createNoncestr(); $jsApiObj["package"] = "prepay_id=$prepay_id"; $jsApiObj["signType"] = "MD5"; $jsApiObj["paySign"] = $this->getSign($jsApiObj); $this->parameters = json_encode($jsApiObj); } private function checkSign($data) { $tmpData = $data; unset($tmpData['sign']); $sign = $this->getSign($tmpData);//本地簽名 if ($data['sign'] == $sign) { return TRUE; } return FALSE; } /** * 設定返回微信的xml資料 */ private function setReturnParameter($parameter, $parameterValue) { $this->returnParameters[$this->trimString($parameter)] = $this->trimString($parameterValue); } /** * 將xml資料返回微信 */ private function returnXml() { $returnXml = $this->arrayToXml($this->returnParameters); return $returnXml; } /*----以下是JSSDK的檔案----*/ private function getSignPackage() { $jsapiTicket = $this->getJsApiTicket(); // 注意 URL 一定要動態獲取,不能 hardcode. $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; $timestamp = time(); $nonceStr = $this->createNonceStr2(); // 這裡引數的順序要按照 key 值 ASCII 碼升序排序 $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url"; $signature = sha1($string); $signPackage = array( "appId" => $this->wxpayConfig['appid'], "nonceStr" => $nonceStr, "timestamp" => $timestamp, "url" => $url, "signature" => $signature, "rawString" => $string ); return $signPackage; } private function createNonceStr2($length = 16) { $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; $str = ""; for ($i = 0; $i < $length; $i++) { $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); } return $str; } private function getJsApiTicket() { // jsapi_ticket 應該全域性儲存與更新,以下程式碼以寫入到檔案中做示例 // $data = json_decode(file_get_contents("jsapi_ticket.json")); $data = json_decode($_COOKIE['jsapi_ticket_json']); if ($data->expire_time < time()) { $accessToken = $this->getAccessToken(); // 如果是企業號用以下 URL 獲取 ticket // $url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=$accessToken"; $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=$accessToken"; $res = json_decode($this->httpGet($url)); $ticket = $res->ticket; if ($ticket) { //$data->expire_time = time() + 7000; //$data->jsapi_ticket = $ticket; //$fp = fopen("jsapi_ticket.json", "w"); //fwrite($fp, json_encode($data)); //fclose($fp); $tempArr = array('jsapi_ticket' => $ticket, 'expire_time' => time() + 7000); setcookie('jsapi_ticket_json', json_encode($tempArr), $tempArr['expire_time']); } } else { $ticket = $data->jsapi_ticket; } return $ticket; } private function getAccessToken() { // access_token 應該全域性儲存與更新,以下程式碼以寫入到檔案中做示例 // $data = json_decode(file_get_contents("access_token.json")); $data = json_decode($_COOKIE["access_token_json"]); if ($data->expire_time < time()) { // 如果是企業號用以下URL獲取access_token // $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$this->appId&corpsecret=$this->appSecret"; $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" . $this->wxpayConfig['appid'] . "&secret=" . $this->wxpayConfig['appsecret']; $res = json_decode($this->httpGet($url)); $access_token = $res->access_token; if ($access_token) { //$data->expire_time = time() + 7000; //$data->access_token = $access_token; //$fp = fopen("access_token.json", "w"); //fwrite($fp, json_encode($data)); //fclose($fp); $tempArr = array('access_token' => $access_token, 'expire_time' => time() + 7000); setcookie('access_token_json', json_encode($tempArr), $tempArr['expire_time']); } } else { $access_token = $data->access_token; } return $access_token; } private function httpGet($url) { $curl = curl_init(); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_TIMEOUT, 500); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_URL, $url); $res = curl_exec($curl); curl_close($curl); return $res;; } }