本帖 部分爲轉貼 順便吐槽一下 微信的開發者文檔真是坑啊php
首先做爲服務端 你要了解 整個APP 支付流程 看文檔前端
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3 json
而後 下載官方給的SDK https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=11_1api
咦? 沒有服務端的SDK 傻逼了吧? %$#^&騰訊 不用着急 等會我給你 - -!數組
秋高氣爽,天氣轉涼,正是學習工做作的好時候。(~ ̄▽ ̄)~~(~ ̄▽ ̄)~安全
我是個phper最近在寫微信支付(APP支付),微信給的官方文檔並非很詳細也沒有dome之類的代碼啥的(對於新手來講比較麻煩),本人是新手之前也沒寫過支付,踩了好多坑,因此想寫篇文章給沒寫過支付的新手幾個建議。服務器
這首先呢你得註冊個開放平臺以及商戶平臺的帳號吧,註冊完成後呢你會收到一封微信裏郵件裏面有你的商戶號等信息,註冊這倆帳號完你會擁有商戶號,appid,appkey等須要的東西。微信
準備完成後咱們來看一下支付的大致流程併發
商戶APP應用與微信支付主要的交互說明:app
用戶在商戶APP應用(移動端)中選擇商品提交訂單,支付方式選擇微信支付。
商戶APP應用(後臺)收到用戶支付訂單,調用微信支付中的統一下單接口。
商戶APP應用(後臺)統一下單接口調用成功後,返回的數據中有咱們須要的prepay_id,按照簽名規範從新生成一個簽名,而後把這個從新生成的簽名及app須要的數據返回給商戶APP應用(移動端)。
商戶APP應用(移動端)收到商戶APP應用(後臺)的數據調起微信支付,用戶進行支付
商戶APP應用(後臺)的回調接口會收到微信發來的支付結果通知
商戶APP應用(後臺)查詢支付結果通知
附:1,4是移動端所要作的事情,2,3,5是咱們PHP服務端後臺要作的6也是,但我沒作,這個根據狀況而定若是須要的話就作。
步驟1由移動端完成
步驟2. 調用同一下單接口:
先要作的是流程中的第二步,調用同一下單接口。官方文檔裏說了請求的地址與參數,其中有一些是必填參數,有
appid
應用ID 固定值,你申請帳號時就給你了
mch_id
商戶號 固定值,你申請帳號時就給你了
nonce_str
隨機字符串 這個是本身寫的要求不能長於32位,參見官方給的[標準][8]
sign
簽名 咱們把這個簽名叫作第一次簽名,注意這個是個坑,得本身寫了,官方只給瞭如何寫的[標準][9]沒有代碼,這個就比較蛋疼了。好多人掉進這個坑裏,寫的簽名函數不對,總是出錯。但不用擔憂我在文章的最後會貼出代碼裏面有簽名函數直接調用就能夠了。(注意看我寫的函數使用規則)
body
商品描述 固定值 商品描述交易字段格式根據不一樣的應用場景按照如下格式:APP——需傳入應用市場上的APP名字-實際商品名稱,每天愛消除-遊戲充值。
out_trade_no
商戶訂單號 咱們本身定義的訂單號,32個字符內、可包含字母。
total_fee
總金額 這個就是你要支付的錢數了,由前端返回。注意一下這裏的貨幣單位是分!
spbill_create_ip
終端IP 這個用戶的IP地址,寫個取IP地址的函數一調用就行
notify_url
通知地址 這又是一個坑,好多人不理解是幹嗎的,這是接收微信支付異步通知回調地址用的,通知url必須爲直接可訪問的url,不能攜帶參數! 也能夠這樣理解,這個是給微信支付的接口,微信來調用的接口,微信調這接口乾嗎用呢?就是告訴你用戶付款成功啦或者用戶付款失敗了,而後你就能夠在這個接口裏經過微信給你返回的信息來作邏輯處理了。
trade_type
固定值 寫 「APP」 由於咱寫的是APP支付嘛,因此就填APP。
好了就是這些必選參數了,剩下就能夠本身選擇是否要用的參數了根據本身狀況而定。
參數選完了就要發送參數了唄,如何發呢?
咱們來調用wechatAppPay類中的unifiedOrder()函數。
啊哈啥!!!!!!??????
(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?(⊙o⊙)?
wechatAppPay類???unifiedOrder()函數???
對就這這倆東西,不要驚訝,不要着急看最後有代碼,有這個類,有代碼的O(∩_∩)O哈哈~,
你只需在你的項目中加載這個類就能夠調用這個方法了!不要崇拜我( ╯▽╰)(由於這個類不是我寫的我也忘了從哪找的了,我從百度搜的而後整理的作了些改動╮(╯▽╰)╭ -_-|||-_-|||-_-!好吧好吧好吧沒作改動,只是加了點註釋而已,感謝寫這個類的大神謝謝O(∩_∩)O謝謝O(∩_∩)O謝謝)
好了抽完瘋了,開是幹正事!
咱們先來new下wechatAppPay類
$wxappid = 'wx0000000000000';//應用ID 字符串 $mch_id = '1000000000';//商戶號 字符串 $notify_url = 'http://www.xxx.com/xxxx.php/xxxx/xxxx';//接收微信支付異步通知回調地址 字符串 $wxkey = '00000000000000000000000';//這個是在商戶中心設置的那個值用來生成簽名時保證安全的 字符串 $this->wechatAppPay = new wechatAppPay($wxappid, $mch_id, $notify_url, $wxkey);
調用wechatAppPay類中的unifiedOrder()函數。unifiedOrder()須要的參數是個數組咱們定義爲$params
$params = array(); $params['body'] = 'APP-在線支付'; //必填項 商品描述 $params['out_trade_no'] = time()."$member"; //必填項 自定義的訂單號 $params['total_fee'] = ($money*100); //必填項 訂單金額 單位爲分因此要*100 $params['trade_type'] = 'APP'; //必填項 交易類型固定寫 APP $params['根據本身狀況定的值'] = "根據本身狀況定的值" //非必填項 根據本身狀況定的值 這個可有好多個能夠參看開發文檔中的參數 $result = $this->wechatAppPay->unifiedOrder( $params );
注:若是你加了$params['根據本身狀況定的值'] wechatAppPay類裏要作相應的改動,文章的最後有代碼,你一看代碼就明白了
如今$result就是咱們調用統一下單接口返回的數據了,這個$resutl經過unifiedOrder()函數的處理已經把xml格式變成數組了。$result 裏有return_code,return_msg,appid,mch_id,nonce_str,sign,result_code,prepay_id,trade_type。這裏面就用一個prepay_id(預支付交易會話ID),其餘都不重要了
步驟2完畢
步驟3 把數據返回給商戶APP應用(移動端)調起支付接口
如今咱們要把調用統一下單接口返回的數據$resutl裏的幾個值返回給移動端那幾個值呢?這幾個:
appid
應用ID 這個是固定的 能夠本身寫也能夠從$resutl裏拿 可讓移動端寫死 就不用每次返回了
partnerid
商戶號 這個也是固定的 能夠本身寫也能夠從$resutl裏拿 可讓移動端寫死 就不用每次返回了
prepayid
預支付交易會話ID 這個很重要必須返回給移動端 是必須從$resutl裏拿的
package
擴展字段 能夠本身寫也能夠從$resutl裏拿 暫填寫固定值"Sign=WXPay" 可讓移動端寫死 就不用每次返回了
noncestr
隨機字符串 這個能夠本身寫也能夠從$resutl裏拿
timestamp
時間戳 本身生成 標準北京時間,時區爲東八區注意:部分系統取到的值爲毫秒級,須要轉換成秒(10位數字),這裏有個坑,ISO端接收的時候好像得強行轉化一下,由於返回的是字符串不是數字,還有什麼幾位的數字之類的,我也不太懂,反正就是寫的時候提醒下iOS工程師就行。安卓不清楚。
sign
簽名 又來一個坑,咱們把這個簽名叫作二次簽名,可是這個簽名不是從$resutl裏拿的,而是本身寫的,如何寫呢,又有坑!由於參與簽名的參數值是那幾個不清楚,參數名寫不對!不怕我有代碼!貼給你看!須要參與簽名的值有六個! $sign_array = array(); $sign_array['appid'] = $wx_result['appid']; //注意 $sign_array['appid'] 裏的參數名必須是appid $sign_array['partnerid'] = $wx_result['mch_id']; //注意 $sign_array['partnerid'] 裏的參數名必須是partnerid $sign_array['prepayid'] = $wx_result['prepay_id'];//注意 $sign_array['prepayid'] 裏的參數名必須是prepayid $sign_array['package'] = 'Sign=WXPay'; //注意 $sign_array['package'] 裏的參數名必須是package $sign_array['noncestr'] = $wx_result['nonce_str'];//注意 $sign_array['noncestr'] 裏的參數名必須是noncestr $sign_array['timestamp'] = time(); //注意 $sign_array['timestamp'] 裏的參數名必須是timestamp $sign_two = $this->wechatAppPay->MakeSign($sign_array);//調用wechatAppPay類裏的MakeSign()函數生成sign
如今就能夠把從新生成的sign($sign_two)以及其餘參數返回給移動端了,一共返回七個值,有三個之可讓前端寫死(appid,partnerid,package),其他四個必須由服務器返回給移動端。
步驟3完畢
步驟4由移動端完成
步驟5 回調接口 支付結果通用通知
還記得步驟2中咱們設置的$notify_url嗎,對如今就要對這個微信返回到這個接口的數據進行一系列的邏輯處理了官方是這樣寫的:
支付完成後,微信會把相關支付結果和用戶信息發送給商戶,商戶須要接收處理,並返回應答。 對後臺通知交互時,若是微信收到商戶的應答不是成功或超時,微信認爲通知失敗,微信會經過必定的策略按期從新發起通知,儘量提升通知的成功率,但微信不保證通知最終能成功。 (通知頻率爲15/15/30/180/1800/1800/1800/1800/3600,單位:秒) 注意:一樣的通知可能會屢次發送給商戶系統。商戶系統必須可以正確處理重複的通知。 推薦的作法是,當收到通知進行處理時,首先檢查對應業務數據的狀態,判斷該通知是否已經處理過,若是沒有處理過再進行處理,若是處理過直接返回結果成功。在對業務數據進行狀態檢查和處理以前,要採用數據鎖進行併發控制,以免函數重入形成的數據混亂。 特別提醒:商戶系統對於支付結果通知的內容必定要作簽名驗證,防止數據泄漏致使出現「假通知」,形成資金損失。
首先來接收數據
$data = $this->wechatAppPay->getNotifyData();//獲取數據 用wechatAppPay類裏的getNotifyData()方法,這裏數據也被getNotifyData()由xml轉化成了數組。
而後官方說要採用數據鎖進行併發控制,這個我不懂因此沒寫(若是你懂你會的話請給我留言私信告訴我,在這謝謝了),對數據進行狀態檢查這個寫了,如何寫的呢?很簡單微信返回的值有好多其中就能夠判斷result_code(業務結果)和return_code(返回狀態碼)是否爲SUCCESS就能夠了代碼就不寫了。
而後驗籤,這個很重要由於這是保證數據沒有被第三方人爲篡改的標準!
如何驗籤呢?
把返回的數據$data裏除去sign剩下的值都參與從新簽名咱們把此次簽名叫作驗籤簽名,驗籤簽名生成後再與$data裏的sign對比,若是相同驗籤經過,不然不經過。此次簽名的參數名與二次簽名時的參數名不一樣,$data數組裏叫什麼參數名就驗籤時叫什麼參數名。聽亂了吧?(~ ̄▽ ̄)~(~ ̄▽ ̄)~不要緊請看代碼
//假如$data裏有以下參數 $w_sign = array(); //參加驗籤簽名的參數數組 $w_sign['appid'] = $data['appid']; $w_sign['bank_type'] = $data['bank_type']; $w_sign['cash_fee'] = $data['cash_fee']; $w_sign['fee_type'] = $data['fee_type']; $w_sign['is_subscribe'] = $data['is_subscribe']; $w_sign['mch_id'] = $data['mch_id']; $w_sign['nonce_str'] = $data['nonce_str']; $w_sign['openid'] = $data['openid']; $w_sign['out_trade_no'] = $data['out_trade_no']; $w_sign['result_code'] = $data['result_code']; $w_sign['return_code'] = $data['return_code']; $w_sign['time_end'] = $data['time_end']; $w_sign['total_fee'] = $data['total_fee']; $w_sign['trade_type'] = $data['trade_type']; $w_sign['transaction_id'] = $data['transaction_id']; $verify_sign = $this->wechatAppPay->MakeSign($w_sign);//生成驗籤簽名
好了如今假設你的驗籤已經經過了接下里就是你本身的邏輯處理了
/////////////////////////////////////////////////////// 商戶APP應用(後臺)處理邏輯代碼 //////////////////////////////////////////////////////
本身的邏輯處理已經處理完以後,還得告訴微信一下,別再一直髮結果通用通知啦,我已經收到通知並處理完啦!
$this->wechatAppPay->replyNotify();//商戶處理後同步返回給微信參數
步驟5結束步驟6根據本身狀況而定
至此微信支付處理完成~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦
寫的有不對的方還請你們多多指導指教!!!給我留言!!b( ̄▽ ̄)db( ̄▽ ̄)db( ̄▽ ̄)d
還有感謝在我寫微信支付地時候 那些被我問煩了的大神們! !謝謝啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦~(≧▽≦)/~啦啦啦O(∩_∩)O哈哈~O(∩_∩)O哈哈~O(∩_∩)O哈哈~<( ̄︶ ̄)><( ̄︶ ̄)><( ̄︶ ̄)>
wechatAppPay類 重頭戲
class wechatAppPay { //接口API URL前綴 const API_URL_PREFIX = 'https://api.mch.weixin.qq.com'; //下單地址URL const UNIFIEDORDER_URL = "/pay/unifiedorder"; //查詢訂單URL const ORDERQUERY_URL = "/pay/orderquery"; //關閉訂單URL const CLOSEORDER_URL = "/pay/closeorder"; //公衆帳號ID private $wxappid; //商戶號 private $mch_id; //隨機字符串 private $nonce_str; //簽名 private $sign; //商品描述 private $body; //商戶訂單號 private $out_trade_no; //支付總金額 private $total_fee; //終端IP private $spbill_create_ip; //支付結果回調通知地址 private $notify_url; //交易類型 private $trade_type; //支付密鑰 private $key; //證書路徑 private $SSLCERT_PATH; private $SSLKEY_PATH; //全部參數 private $params = array(); public function __construct($wxappid, $mch_id, $notify_url, $key) { $this->appid = $wxappid; $this->mch_id = $mch_id; $this->notify_url = $notify_url; $this->key = $key; } /** * 下單方法 * @param $params 下單參數 */ public function unifiedOrder( $params ){ $this->body = $params['body']; $this->out_trade_no = $params['out_trade_no']; $this->total_fee = $params['total_fee']; $this->trade_type = $params['trade_type']; $this->nonce_str = $this->genRandomString(); $this->spbill_create_ip = $_SERVER['REMOTE_ADDR']; $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->nonce_str; $this->params['body'] = $this->body; $this->params['out_trade_no'] = $this->out_trade_no; $this->params['total_fee'] = $this->total_fee; $this->params['spbill_create_ip'] = $this->spbill_create_ip; $this->params['notify_url'] = $this->notify_url; $this->params['trade_type'] = $this->trade_type; //獲取簽名數據 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::UNIFIEDORDER_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); if( !empty($result['result_code']) && !empty($result['err_code']) ){ $result['err_msg'] = $this->error_code( $result['err_code'] ); } return $result; } /** * 查詢訂單信息 * @param $out_trade_no 訂單號 * @return array */ public function orderQuery( $out_trade_no ){ $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->genRandomString(); $this->params['out_trade_no'] = $out_trade_no; //獲取簽名數據 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::ORDERQUERY_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); if( !empty($result['result_code']) && !empty($result['err_code']) ){ $result['err_msg'] = $this->error_code( $result['err_code'] ); } return $result; } /** * 關閉訂單 * @param $out_trade_no 訂單號 * @return array */ public function closeOrder( $out_trade_no ){ $this->params['appid'] = $this->appid; $this->params['mch_id'] = $this->mch_id; $this->params['nonce_str'] = $this->genRandomString(); $this->params['out_trade_no'] = $out_trade_no; //獲取簽名數據 $this->sign = $this->MakeSign( $this->params ); $this->params['sign'] = $this->sign; $xml = $this->data_to_xml($this->params); $response = $this->postXmlCurl($xml, self::API_URL_PREFIX.self::CLOSEORDER_URL); if( !$response ){ return false; } $result = $this->xml_to_data( $response ); return $result; } /** * * 獲取支付結果通知數據 * return array */ public function getNotifyData(){ //獲取通知的數據 $xml = $GLOBALS['HTTP_RAW_POST_DATA']; $data = array(); if( empty($xml) ){ return false; } $data = $this->xml_to_data( $xml ); if( !empty($data['return_code']) ){ if( $data['return_code'] == 'FAIL' ){ return false; } } return $data; } /** * 接收通知成功後應答輸出XML數據 * @param string $xml */ public function replyNotify(){ $data['return_code'] = 'SUCCESS'; $data['return_msg'] = 'OK'; $xml = $this->data_to_xml( $data ); echo $xml; die(); } /** * 生成APP端支付參數 * @param $prepayid 預支付id */ public function getAppPayParams( $prepayid ){ $data['appid'] = $this->appid; $data['partnerid'] = $this->mch_id; $data['prepayid'] = $prepayid; $data['package'] = 'Sign=WXPay'; $data['noncestr'] = $this->genRandomString(); $data['timestamp'] = time(); $data['sign'] = $this->MakeSign( $data ); return $data; } /** * 生成簽名 * @return 簽名 */ public function MakeSign( $params ){ //簽名步驟一:按字典序排序數組參數 ksort($params); $string = $this->ToUrlParams($params); //簽名步驟二:在string後加入KEY $string = $string . "&key=".$this->key; //簽名步驟三:MD5加密 $string = md5($string); //簽名步驟四:全部字符轉爲大寫 $result = strtoupper($string); return $result; } /** * 將參數拼接爲url: key=value&key=value * @param $params * @return string */ public function ToUrlParams( $params ){ $string = ''; if( !empty($params) ){ $array = array(); foreach( $params as $key => $value ){ $array[] = $key.'='.$value; } $string = implode("&",$array); } return $string; } /** * 輸出xml字符 * @param $params 參數名稱 * return string 返回組裝的xml **/ public function data_to_xml( $params ){ if(!is_array($params)|| count($params) <= 0) { return false; } $xml = "<xml>"; foreach ($params as $key=>$val) { if (is_numeric($val)){ $xml.="<".$key.">".$val."</".$key.">"; }else{ $xml.="<".$key."><![CDATA[".$val."]]></".$key.">"; } } $xml.="</xml>"; return $xml; } /** * 將xml轉爲array * @param string $xml * return array */ public function xml_to_data($xml){ if(!$xml){ return false; } //將XML轉爲array //禁止引用外部xml實體 libxml_disable_entity_loader(true); $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $data; } /** * 獲取毫秒級別的時間戳 */ private static function getMillisecond(){ //獲取毫秒的時間戳 $time = explode ( " ", microtime () ); $time = $time[1] . ($time[0] * 1000); $time2 = explode( ".", $time ); $time = $time2[0]; return $time; } /** * 產生一個指定長度的隨機字符串,並返回給用戶 * @param type $len 產生字符串的長度 * @return string 隨機字符串 */ private function genRandomString($len = 32) { $chars = array( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ); $charsLen = count($chars) - 1; // 將數組打亂 shuffle($chars); $output = ""; for ($i = 0; $i < $len; $i++) { $output .= $chars[mt_rand(0, $charsLen)]; } return $output; } /** * 以post方式提交xml到對應的接口url * * @param string $xml 須要post的xml數據 * @param string $url url * @param bool $useCert 是否須要證書,默認不須要 * @param int $second url執行超時時間,默認30s * @throws WxPayException */ private function postXmlCurl($xml, $url, $useCert = false, $second = 30){ $ch = curl_init(); //設置超時 curl_setopt($ch, CURLOPT_TIMEOUT, $second); curl_setopt($ch,CURLOPT_URL, $url); curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE); curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2); //設置header curl_setopt($ch, CURLOPT_HEADER, FALSE); //要求結果爲字符串且輸出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); if($useCert == true){ //設置證書 //使用證書:cert 與 key 分別屬於兩個.pem文件 curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM'); //curl_setopt($ch,CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH); curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM'); //curl_setopt($ch,CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH); } //post提交方式 curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); //運行curl $data = curl_exec($ch); //返回結果 if($data){ curl_close($ch); return $data; } else { $error = curl_errno($ch); curl_close($ch); return false; } } /** * 錯誤代碼 * @param $code 服務器輸出的錯誤代碼 * return string */ public function error_code( $code ){ $errList = array( 'NOAUTH' => '商戶未開通此接口權限', 'NOTENOUGH' => '用戶賬號餘額不足', 'ORDERNOTEXIST' => '訂單號不存在', 'ORDERPAID' => '商戶訂單已支付,無需重複操做', 'ORDERCLOSED' => '當前訂單已關閉,沒法支付', 'SYSTEMERROR' => '系統錯誤!系統超時', 'APPID_NOT_EXIST' => '參數中缺乏APPID', 'MCHID_NOT_EXIST' => '參數中缺乏MCHID', 'APPID_MCHID_NOT_MATCH' => 'appid和mch_id不匹配', 'LACK_PARAMS' => '缺乏必要的請求參數', 'OUT_TRADE_NO_USED' => '同一筆交易不能屢次提交', 'SIGNERROR' => '參數簽名結果不正確', 'XML_FORMAT_ERROR' => 'XML格式錯誤', 'REQUIRE_POST_METHOD' => '未使用post傳遞參數 ', 'POST_DATA_EMPTY' => 'post數據不能爲空', 'NOT_UTF8' => '未使用指定編碼格式', ); if( array_key_exists( $code , $errList ) ){ return $errList[$code]; } } }