1. 程式人生 > >Android通過外部瀏覽器呼叫微信H5支付,Android+PHP詳解

Android通過外部瀏覽器呼叫微信H5支付,Android+PHP詳解

看了好多關於講解微信H5支付開發的文章,大多數都是通過微信內部瀏覽器來呼叫支付介面(其實就是公眾號支付),可能是因為H5支付介面剛開放不久吧。
微信官方體驗連結:http://wxpay.wxutil.com/mch/pay/h5.v2.php,請在微信外瀏覽器開啟。
看了上面的體驗連結,如果感興趣,可以接著往下看,希望對你有所幫助。

一、Android端

Android端程式碼相對來說比較簡單一些,我這邊直接呼叫系統瀏覽器開啟H5支付頁面

 Intent intent = new Intent();
 intent.setAction("android.intent.action.VIEW"
); Uri content_url = Uri.parse(url); //url裡面包含了後端需要用到的引數,例如金額,使用者ID intent.setData(content_url); startActivity(intent);

剛開始考慮過使用webview來載入支付頁面,但是除錯介面的時候發現一直報如下錯誤:

微信官方報錯示例圖
根據微信官方的報錯示例可以看出,應該是webview中沒有設定referer導致的。於是,我按照說明加上了referer,然鵝,並沒有什麼卵用,依然報錯。我只好放棄使用webview改用系統瀏覽器。使用系統瀏覽器的弊端還是挺明顯的,客戶端和後臺傳值不好處理(正在看文章的你,如果用webview呼叫成功了,還請賜教)

二、PHP端

1.獲取從Android端傳過來的引數

目前我只想到一種方法,就是通過url攜帶引數給服務端傳值,OK,獲取方式如下:

//當前頁面的URL地址為:http://www.XXXXX.com/wechatpay/h5Pay.php?money=100&uid=337828932
$string = $_SERVER['QUERY_STRING'];//通過$_SERVER['QUERY_STRING']函式獲取url地址?後面的值
$androidData = convertUrlQuery($string);//將字串$string轉化為陣列
$money = $androidData
['money '];//獲取money值 100 $uid = $androidData['uid'];//獲取使用者ID 337828932

上面用到的convertUrlQuery函式程式碼

//將字串引數變為陣列
function convertUrlQuery($query)
{
    $queryParts = explode('&', $query);
    $params = array();
    foreach ($queryParts as $param) {
        $item = explode('=', $param);
        $params[$item[0]] = $item[1];
    }
    return $params;
}
2.呼叫微信統一下單API
2.1 具體引數名稱以及各引數的用途請檢視微信官方文件 統一下單API
    //配置需要傳遞給微信的引數
    $input = new WxPayUnifiedOrder();
    $input->SetBody("xxxx-商品購買");
    $input->SetAttach("xxxx");
    $input->SetDevice_info("WEB");
    $input->SetOut_trade_no(WxPayConfig::MCHID . date("YmdHis").$uid);//把UID加在末尾主要用於識別使用者,給對應的使用者充值餘額
    $input->SetTotal_fee($money);
    $input->SetTime_start(date("YmdHis"));
    $input->SetTime_expire(date("YmdHis", time() + 600));
    $input->SetGoods_tag("t");
    $input->SetNotify_url("http://www.xxxxx.com/wechatpay/notify.php");//用於接收微信下發的支付結果通知
    $input->SetTrade_type("MWEB");
    $input->SetOpenid($openId);
    $input->SetScene_info("{\"h5_info\": \"h5_info\"{\"type\": \"Wap\",\"wap_url\": \"http://www.xxxxx.com/shop\",\"wap_name\": \"xxx\"}}");
2.2 接下來通過$order = WxPayApi::unifiedOrder($input);獲取微信返回的引數,unifiedOrder函式具體實現如下
/**
     * 
     * 統一下單,WxPayUnifiedOrder中out_trade_no、body、total_fee、trade_type必填
     * appid、mchid、spbill_create_ip、nonce_str不需要填入
     * @param WxPayUnifiedOrder $inputObj
     * @param int $timeOut
     * @throws WxPayException
     * @return 成功時返回,其他拋異常
     */
    public static function unifiedOrder($inputObj, $timeOut = 6)
    {
        $url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        //檢測必填引數
        if(!$inputObj->IsOut_trade_noSet()) {
            throw new WxPayException("缺少統一支付介面必填引數out_trade_no!");
        }else if(!$inputObj->IsBodySet()){
            throw new WxPayException("缺少統一支付介面必填引數body!");
        }else if(!$inputObj->IsTotal_feeSet()) {
            throw new WxPayException("缺少統一支付介面必填引數total_fee!");
        }else if(!$inputObj->IsTrade_typeSet()) {
            throw new WxPayException("缺少統一支付介面必填引數trade_type!");
        }


        //非同步通知url未設定,則使用配置檔案中的url
        if(!$inputObj->IsNotify_urlSet()){
            $inputObj->SetNotify_url(WxPayConfig::NOTIFY_URL);//非同步通知url
        }

        $inputObj->SetAppid(WxPayConfig::APPID);//公眾賬號ID
        $inputObj->SetMch_id(WxPayConfig::MCHID);//商戶號
        $inputObj->SetSpbill_create_ip(self::getIp());//終端ip
        //$inputObj->SetSpbill_create_ip("1.1.1.1");       
        $inputObj->SetNonce_str(self::getNonceStr());//隨機字串

        //簽名
        $inputObj->SetSign();
        $xml = $inputObj->ToXml();

        $startTimeStamp = self::getMillisecond();//請求開始時間
        $response = self::postXmlCurl($xml, $url, false, $timeOut);
        $result = WxPayResults::Init($response);
        self::reportCostTime($url, $startTimeStamp, $result);//上報請求花費時間

        return $result;
    }
2.3 上面程式碼中需要注意的有以下3點
  • 獲取客服端的真實IP地址

    使用REMOTE_ADDR只能獲取訪問者本地連線中設定的IP。經本人粗略測試,使用UC瀏覽的時候將無法直接獲取真實IP,這是如果把這個IP傳過去,微信將會返回 網路環境未能通過安全驗證,請稍後再試 的錯誤


這時可以使用以下方式獲取使用者真實IP
//獲取使用者真實IP
    public static function getIp(){
        $ip = '';
        if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
        }elseif(isset($_SERVER['HTTP_CLIENT_IP'])){
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        }else{
            $ip = $_SERVER['REMOTE_ADDR'];
        }
        $ip_arr = explode(',', $ip);
        return $ip_arr[0];
    }
  • 把引數轉換成XML格式
    在這JSON橫行的年代,還使用XML格式傳遞資料,也許這是微信的高階操作,也許是他們以前的框架就是那樣,這我們就不管了。直接上程式碼
/**
     * 輸出xml字元
     * @throws WxPayException
    **/
    public function ToXml()
    {
        if(!is_array($this->values) 
            || count($this->values) <= 0)
        {
            throw new WxPayException("陣列資料異常!");
        }

        $xml = "<xml>";
        foreach ($this->values as $key=>$val)
        {
            if (is_numeric($val)){
                $xml.="<".$key.">".$val."</".$key.">";
            }else{
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml.="</xml>";
        return $xml; 
    }
  • 簽名方法

簽名生成的通用步驟如下:

第一步,設所有傳送或者接收到的資料為集合M,將集合M內非空引數值的引數按照引數名ASCII碼從小到大排序(字典序),使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字串stringA。

特別注意以下重要規則:

◆ 引數名ASCII碼從小到大排序(字典序);
◆ 如果引數的值為空不參與簽名;
◆ 引數名區分大小寫;
◆ 驗證呼叫返回或微信主動通知簽名時,傳送的sign引數不參與簽名,將生成的簽名與該sign值作校驗。
◆ 微信介面可能增加欄位,驗證簽名時必須支援增加的擴充套件欄位

第二步,在stringA最後拼接上key得到stringSignTemp字串,並對stringSignTemp進行MD5運算,再將得到的字串所有字元轉換為大寫,得到sign值signValue。

key設定路徑:微信商戶平臺(pay.weixin.qq.com)–>賬戶設定–>API安全–>金鑰設定

/**
     * 生成簽名
     * @return 簽名
     */
    public function MakeSign()
    {
        //簽名步驟一:按字典序排序引數
        ksort($this->values);
        $string = $this->ToUrlParams();
        //簽名步驟二:在string後加入KEY
        $string = $string . "&key=".WxPayConfig::KEY;
        //簽名步驟三:MD5加密
        $string = md5($string);
        //簽名步驟四:所有字元轉為大寫
        $result = strtoupper($string);
        return $result;
    }
    /**
     * 格式化引數  --格式化成url引數
     */
    public function ToUrlParams()
    {
        $buff = "";
        foreach ($this->values as $k => $v)
        {
            if($k != "sign" && $v != "" && !is_array($v)){
                $buff .= $k . "=" . $v . "&";
            }
        }

        $buff = trim($buff, "&");
        return $buff;
    }
2.4 通過返回的mweb_url引數調起微信APP
if ($order['mweb_url']) {
        $orderId = $input->GetOut_trade_no();
        $payUrl = $order['mweb_url']."&redirect_url=http%3A%2F%2Fwww.xxxx.com%2Fwechatpay%2Forderquery.php%3Ftransaction_id%3D".$orderId;
        Log::DEBUG($payUrl);
        //通過JS開啟連結,調起微信APP支付
        echo "<script LANGUAGE='Javascript'>";
        echo "location.replace('$payUrl')";
        echo "</script>";
    }

這裡詳細解釋一下payUrl中redirect_url引數的含義

回撥頁面示例圖.png

3.處理微信支付結果通知

支付完成後,微信會把相關支付結果和使用者資訊傳送給商戶,商戶需要接收處理,並返回應答。
對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略定期重新發起通知,儘可能提高通知的成功率,但微信不保證通知最終能成功。 (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)
注意:同樣的通知可能會多次傳送給商戶系統。商戶系統必須能夠正確處理重複的通知。
推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務資料進行狀態檢查和處理之前,要採用資料鎖進行併發控制,以避免函式重入造成的資料混亂。
特別提醒:商戶系統對於支付結果通知的內容一定要做簽名驗證,並校驗返回的訂單金額是否與商戶側的訂單金額一致,防止資料洩漏導致出現“假通知”,造成資金損失。

支付回撥基礎類

/**
 * 
 * 回撥基礎類
 * @author widyhu
 *
 */
class WxPayNotify extends WxPayNotifyReply
{
    /**
     * 
     * 回撥入口
     * @param bool $needSign  是否需要簽名輸出
     */
    final public function Handle($needSign = true)
    {
        $msg = "OK";
        //當返回false的時候,表示notify中呼叫NotifyCallBack回撥失敗獲取簽名校驗失敗,此時直接回復失敗
        $result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg);
        if($result == false){
            $this->SetReturn_code("FAIL");
            $this->SetReturn_msg($msg);
            $this->ReplyNotify(false);
            return;
        } else {
            //該分支在成功回撥到NotifyCallBack方法,處理完成之後流程
            $this->SetReturn_code("SUCCESS");
            $this->SetReturn_msg("OK");
        }
        $this->ReplyNotify($needSign);
    }

    /**
     * 
     * 回撥方法入口,子類可重寫該方法
     * 注意:
     * 1、微信回撥超時時間為2s,建議使用者使用非同步處理流程,確認成功之後立刻回覆微信伺服器
     * 2、微信伺服器在呼叫失敗或者接到回包為非確認包的時候,會發起重試,需確保你的回撥是可以重入
     * @param array $data 回撥解釋出的引數
     * @param string $msg 如果回撥處理失敗,可以將錯誤資訊輸出到該方法
     * @return true回調出來完成不需要繼續回撥,false回撥處理未完成需要繼續回撥
     */
    public function NotifyProcess($data, &$msg)
    {
        //TODO 使用者基礎該類之後需要重寫該方法,成功的時候返回true,失敗返回false
        return true;
    }

    /**
     * 
     * notify回撥方法,該方法中需要賦值需要輸出的引數,不可重寫
     * @param array $data
     * @return true回調出來完成不需要繼續回撥,false回撥處理未完成需要繼續回撥
     */
    final public function NotifyCallBack($data)
    {
        $msg = "OK";
        $result = $this->NotifyProcess($data, $msg);

        if($result == true){
            $this->SetReturn_code("SUCCESS");
            $this->SetReturn_msg("OK");
        } else {
            $this->SetReturn_code("FAIL");
            $this->SetReturn_msg($msg);
        }
        return $result;
    }

    /**
     * 
     * 回覆通知
     * @param bool $needSign 是否需要簽名輸出
     */
    final private function ReplyNotify($needSign = true)
    {
        //如果需要簽名
        if($needSign == true && 
            $this->GetReturn_code($return_code) == "SUCCESS")
        {
            $this->SetSign();
        }
        WxpayApi::replyNotify($this->ToXml());
    }
}

需要注意的是,如果使用者只是開啟付款介面,而沒有執行付款操作的話,是不會觸發通知的。

當我們接收到的引數中同時含有return_code,result_code,trade_state並且每個返回值都為SUCCESS的時候,代表使用者付款成功

if(array_key_exists("return_code", $result)
        && array_key_exists("result_code", $result)
    &&array_key_exists("trade_state", $resultData)
        && $resultData["trade_state"] == "SUCCESS"
        && $result["return_code"] == "SUCCESS"
       && $result["result_code"] == "SUCCESS")
        {
            //這裡執行給使用者充值餘額的操作
        }    
4.redirect_url回撥頁面處理

如果在第2.4步驟中MWEB_URL後拼接上了redirect_url引數,使用者支付完成後將返回到這個頁面。
當用戶點選介面的已完成按鈕,將觸發查單操作
微信查單操作API
查單操作相關程式碼如下

//查詢訂單
    public function Queryorder($transaction_id)
    {
        $input = new WxPayOrderQuery();
        $input->SetTransaction_id($transaction_id);
        $result = WxPayApi::orderQuery($input);
        Log::DEBUG("query:" . json_encode($result));
        if(array_key_exists("return_code", $result)
            && array_key_exists("result_code", $result)
            && $result["return_code"] == "SUCCESS"
            && $result["result_code"] == "SUCCESS")
        {
            return $result;
        }
        return false;
    }

介面佈局以及相關的邏輯處理:

if(isset($_REQUEST["out_trade_no"]) && $_REQUEST["out_trade_no"] != ""){
    $out_trade_no = $_REQUEST["out_trade_no"];
    $input = new WxPayOrderQuery();
    $input->SetOut_trade_no($out_trade_no);
    Log::DEBUG("out_trade_no".$out_trade_no);
//  printf_info(WxPayApi::orderQuery($input));
    $result=WxPayApi::orderQuery($input);
    if(array_key_exists("return_code", $result)
        && array_key_exists("result_code", $result)
        && array_key_exists("trade_state", $result)
        && $result["trade_state"] == "SUCCESS"
        && $result["return_code"] == "SUCCESS"
        && $result["result_code"] == "SUCCESS"){
        $successUrl="success.html";
        echo "<script LANGUAGE='Javascript'>";
        echo "location.replace('$successUrl')";
        echo "</script>";
    }else{
        $failUrl="fail.html";
        echo "<script LANGUAGE='Javascript'>";
        echo "location.replace('$failUrl')";
        echo "</script>";
    }
    exit();
}
?>
<script type="text/javascript">
    ///
    window.onload=function () {
        var Request=new UrlSearch(); //例項化
        document.getElementById("out_trade_no").value=Request.transaction_id;
        document.getElementById("transaction_id").style.height = 0;
        document.getElementById("out_trade_no").style.height = 0;
        document.getElementById("btn").style.height = 0;
        $.DialogByZ.Confirm({Title: "", Content: "請確認微信支付是否已完成",FunL:confirmL,FunR:Immediate})
    }
    ///
    function confirmL(){
        $.DialogByZ.Close();
        document.getElementById('btn').click();
    }
    function Immediate(){
        //location.href="http://sc.chinaz.com/jiaoben/"
        window.location.href="nonglegou://data/?state=fail";
    }
    function UrlSearch()
    {
        var name,value;
        var str=location.href; //取得整個位址列
        var num=str.indexOf("?")
        str=str.substr(num+1); //取得所有引數   stringvar.substr(start [, length ]

        var arr=str.split("&"); //各個引數放到數組裡
        for(var i=0;i < arr.length;i++){
            num=arr[i].indexOf("=");
            if(num>0){
                name=arr[i].substring(0,num);
                value=arr[i].substr(num+1);
                this[name]=value;
            }
        }
    }
</script>

三、總結

微信H5支付的整個流程,大概就是這樣子,以下是流程圖

介面流程圖
1. 使用者在商戶側完成下單,使用微信支付進行支付
2. 由商戶後臺向微信支付發起下單請求(呼叫統一下單介面)注:交易型別trade_type=MWEB
3. 統一下單介面返回支付相關引數給商戶後臺,如支付跳轉url(引數名“mweb_url”),商戶通過mweb_url調起微信支付中間頁
4. 中間頁進行H5許可權的校驗,安全性檢查(此處常見錯誤請見下文)
5. 如支付成功,商戶後臺會接收到微信側的非同步通知
6. 使用者在微信支付收銀臺完成支付或取消支付,返回商戶頁面(預設為返回支付發起頁面)
7. 商戶在展示頁面,引導使用者主動發起支付結果的查詢
8. 商戶後臺判斷是否接到收微信側的支付結果通知,如沒有,後臺呼叫我們的訂單查詢介面確認訂單狀態
9. 展示最終的訂單支付結果給使用者

喜歡的話,可以關注下我的微信公眾號

微信公眾號