1. 程式人生 > >Android應用整合微信支付

Android應用整合微信支付

前言

最近的專案用到了移動支付功能,客戶要求同時支援“支付寶”和“微信支付”;個人感覺相對來說支付寶較簡單一些,以前也在Android應用中整合過,因此沒有花費過多時間便完成了。但微信支付我是第一次接觸,著實費了不少功夫,花了幾天才折騰出來,便想著寫篇日誌記一下這個過程,後面再用到的時候也不至於再糾結一次。

微信支付

支付模式

APP支付又稱移動端支付,是商戶通過在移動端應用APP中整合開放SDK調起微信支付模組完成支付的模式。

另外還有“刷卡支付”、“掃碼支付”和“公眾號支付”三種,有需要的朋友請到文件中檢視,我暫時沒有去研究。

協議規則

總地來說,商戶接入微信支付,呼叫API必須遵循以下規則:

  • POST方式提交
  • 簽名演算法使用MD5
  • 提交和返回資料都為XML格式,根節點名為xml

引數規定

  • 交易金額
    交易金額預設為人民幣交易,介面中引數支付金額單位為【分】,引數值不能帶小數。
  • 交易型別
    APP代表App支付,統一下單介面trade_type的傳參需要使用。
  • 時間戳
    標準北京時間,時區為東八區,自1970年1月1日 0點0分0秒以來的秒數。注意:Java平臺取到的值為毫秒級,需要轉換成秒(10位數字)。
  • 商戶訂單號
    商戶支付的訂單號由商戶自定義生成,微信支付要求商戶訂單號保持唯一性。

安全規範

微信支付過程中兩步(統一下單和發起支付)都要採用本部分描述的方法進行簽名,這兩步簽名過程是一樣的,只有簽名時的引數不同,後面貼出程式碼時再作進一步的介紹。

簽名規則:

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

準備工作

在進行程式碼開發前我們需要做一些準備工作,主要是在微信開放平臺上建立移動應用,然後為其申請微信支付許可權,申請通過後還要作一些微信支付環境的配置。

下面分別記錄一下這個過程。

微信開放平臺

首先要在“微信開放平臺”註冊我們的應用。註冊並登入微信開放平臺,到“管理中心”可看到如下介面(我登入的帳號已經建立了一個App應用,如果是新帳號這個列表應該是空的):

這裡寫圖片描述

點選左上角的“建立移動應用”按鈕即可建立一個新的移動應用,過程不復雜,這裡不再贅述。建立移動應用後還需要設定App的包名和應用簽名信息,引用文件中的話如下:

商戶在微信開放平臺申請開發應用後,微信開放平臺會生成APP的唯一標識APPID。由於需要保證支付安全,需要在開放平臺繫結商戶應用包名和應用簽名,設定好後才能正常發起支付。設定介面在【開放平臺】中的欄目【管理中心 / 修改應用 / 修改開發資訊】裡面。

接下來還需要為建立的移動應用申請微信支付能力,申請支付能力時好像還需要向微信繳納300元錢費用,具體申請步驟我也不是很清楚,好像是提交相關資料由微信工作人員稽核,一般幾天內可以完成。

最終結果如下圖所示:

這裡寫圖片描述

從這裡我們獲取了AppID,記下來這個字串,後面程式碼中要用。

微信商戶平臺

下面是微信官方文件中的一段話:

商戶在微信公眾平臺(申請掃碼支付、公眾號支付)或開放平臺(申請APP支付)按照相應提示,申請相應微信支付模式。微信支付工作人員稽核資料無誤後開通相應的微信支付許可權。微信支付申請稽核通過後,商戶在申請資料填寫的郵箱中收取到由微信支付小助手傳送的郵件,此郵件包含開發時需要使用的支付賬戶資訊。

我們在微信開放平臺(我只關注App支付)建立註冊移動應用並申請微信支付能力通過後,會收到如下圖所示的郵件:

此郵件基本上包括我們呼叫微信支付的所有資訊(還差一個App Key馬上就提到),我們記下來這些資訊,接著做下一步。

使用上面收到的郵件中提到和使用者名稱和密碼登入“微信商戶平臺”,找到“帳戶設定 》API安全“頁面,在”API金鑰“部分設定一下金鑰,金鑰字串可自行設計,我這裡是用的MD5加密隨機數字。在設定API金鑰時還需要安裝操作證書,這些內容在登入商戶平臺後設置API金鑰時都有提示的,這裡也不再贅述了,描述多了反而更亂,過程其實挺簡單的。

引數 API引數名 詳細說明
AppID appid appid是微信公眾賬號或開放平臺APP的唯一標識,在公眾平臺申請公眾賬號或者在開放平臺申請APP賬號後,微信會自動分配對應的appid,用於標識該應用。可在微信公眾平臺–>開發者中心檢視,商戶的微信支付稽核通過郵件中也會包含該欄位值。
微信支付商戶號 mch_id 商戶申請微信支付後,由微信支付分配的商戶收款賬號。
API金鑰 key 交易過程生成簽名的金鑰,僅保留在商戶系統和微信支付後臺,不會在網路中傳播。商戶妥善保管該Key,切勿在網路中傳輸,不能在其他客戶端中儲存,保證key不會被洩漏。商戶可根據郵件提示登入微信商戶平臺進行設定。也可按一下路徑設定:微信商戶平臺(pay.weixin.qq.com)–>賬戶設定–>API安全–>金鑰設定

支付步驟

微信支付主要分兩步:統一下單和呼叫App支付。在呼叫微信支付之前根據業務還有可能存在諸如加入購物車、提交訂單等步驟,這些跟微信支付是沒有任何關係的,這些都由我們應用的後臺服務來完成,最終要生成訂單號及商品詳情等資料,除此之外,還要有時間戳、簽名及隨機字串等資料,這些資料在統一下單或呼叫App支付時會用到。

統一下單

除被掃支付場景以外,商戶系統先呼叫該介面在微信支付服務後臺生成預支付交易單,返回正確的預支付交易回話標識後再按掃碼、JSAPI、APP等不同場景生成交易串調起支付。

統一下單是通過HTTP協議來完成的,也就是說在呼叫微信App支付之前需要向微信支付後臺服務發一個HTTP請求,這個HTTP請求會攜帶一些重要的引數資料以及這些引數的簽名,微信支付後臺在簽名驗證通過後生成預支付交易單並返回交易單ID,然後我們才能以交易單ID為引數發起App支付請求。

本步驟需要以XML形式傳遞引數,具體過程在微信支付示例中都有,相關引數及說明也可以去相關文件中查閱,下面的程式碼部分也會說明這些。

呼叫App支付

統一下單請求完成後我們會得到一個XML字串,分析可得到預支付交易單ID(prepare_id),此ID就是本步驟請求中要攜帶的引數。

使用預支付交易單ID以及其它一些引數(應用appId、商戶號、隨機字串及時間戮等)經過與上一步樣的步驟生成簽名,然後將被簽名的引數以及簽名結果一起建立App支付請求物件,最後呼叫微信支付介面將該請求傳送即可,再完成回撥就可以了。

程式碼編寫

下面是微信支付的基本程式碼,網路請求我使用的是okhttp框架並作了一些封裝,但本文重點是微信支付,請使用其它網路框架的的同學不要介意。在這之前我假設已經通過應用伺服器介面拿到了訂單號及商品詳情等業務資料,以引數形式傳到微信支付的入口方法。

// 呼叫伺服器介面獲取相關資料後發起“統一下單”微信介面
private void payByWechat(String tradeNo, String desp, String notifyUrl, String amount) {
    // 構建產品引數, 結果為XML字串
    String xmlParam = buildProductArgs(tradeNo, desp, notifyUrl, amount);
    // 呼叫“統一下單”介面獲取預支付ID
    OkHttpClientManager.postAsyncXml(ApiHelper.URL_WECHAT_PAY_UNIFIEDORDER, xmlParam, new Listener<String>() {
        @Override
        public void onSuccess(String response) {
            try {
                Map<String, String> resultMap = decodeXml(response);
                String prePayOrderId = resultMap.get("prepay_id");
                mIWXAPI.sendReq(buildWechatPayReq(prePayOrderId));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

上面這個方法包含了微信支付的整個過程,首先是構建統一下單介面所需要的XML資料;接著使用該XML資料為引數呼叫統一下單介面;然後分析統一下單介面響應資料得到預支付交易單ID;然後再使用獲取得的預支付交易單ID及其它資料構建App支付引數物件;最後發起App支付請求。

下面分開來看這幾步的程式碼。

構建統一下單介面引數XML

/**
 * 構建產品引數, 結果為符合微信“統一下單”介面的XML串
 *
 * @param tradeNo   系統訂單號
 * @param desp      描述
 * @param notifyUrl 回撥URL
 * @param amount    金額
 * @return XML結構字串
 */
private String buildProductArgs(String tradeNo, String desp, String notifyUrl, String amount) {
    // 構建基本的引數列表
    List<TwoTuple<String, String>> paramList = new ArrayList<>();
    paramList.add(new TwoTuple<>("appid", Constants.WX_APP_ID));
    paramList.add(new TwoTuple<>("body", desp));
    paramList.add(new TwoTuple<>("mch_id", Constants.WX_PARTNER_ID));
    paramList.add(new TwoTuple<>("nonce_str", UUID.randomUUID().toString().replace("-", "")));
    paramList.add(new TwoTuple<>("notify_url", notifyUrl));
    paramList.add(new TwoTuple<>("out_trade_no", tradeNo));
    paramList.add(new TwoTuple<>("spbill_create_ip", "127.0.0.1"));
    paramList.add(new TwoTuple<>("total_fee", amount));
    paramList.add(new TwoTuple<>("trade_type", "APP"));

    // 獲取MD5簽名並追加到引數列表中
    String sign = generateWechatMD5Signature(paramList);
    paramList.add(new TwoTuple<>("sign", sign));

    // 構建XML引數
    StringBuilder xmlBuilder = new StringBuilder();
    xmlBuilder.append("<xml>");
    for (TwoTuple<String, String> paramTuple : paramList) {
        xmlBuilder.append("<").append(paramTuple.first).append(">");
        xmlBuilder.append(paramTuple.second);
        xmlBuilder.append("</").append(paramTuple.first).append(">");
    }
    xmlBuilder.append("</xml>");

    return xmlBuilder.toString();
}

從上面的程式碼可以看出,首先按照要求將所需要的引數組織好,然後生成簽名並將簽名也加入引數中,最後將所有引數(連帶簽名)組織成規定格式的XML字串即可。這裡只是大體說一下步驟,至於具體要求請引數微信支付官方文件,上面的說明很是詳細。

其中TwoTuple是我自定義的一個工具類(二元組)。

程式碼中還有一處要注意的是簽名,這裡我抽出來了個方法,因為發起App支付的時候也需要用到簽名,而且步驟是一樣的,下面是簽名方法程式碼

// 根據引數列表生成MD5簽名
private String generateWechatMD5Signature(List<TwoTuple<String, String>> paramList) {
    StringBuilder sb = new StringBuilder();
    for (TwoTuple<String, String> paramTuple : paramList) {
        sb.append(paramTuple.first);
        sb.append('=');
        sb.append(paramTuple.second);
        sb.append('&');
    }
    sb.append("key=").append(Constants.WX_SECURITY_KEY);

    return MD5Utils.getMD5Code(sb.toString()).toUpperCase();
}

很簡單的程式碼,無非是將組織的引數以&符號連線起來,最後呼叫MD5演算法生成簽名並轉換成大寫形式,至於MD5演算法就不放程式碼了,網上隨便一找即可。

通過上面的程式碼我們拿到了目標XML資料,這個XML字串將用於發起統一下單請求。統一下單使用HTTP協議,這裡就不放這部分程式碼了,要不會顯得特別亂;我在應用中使用的是okhttp框架,發起XML請求還是比較簡單的。

假設我們已經成功發起統一下單請求,程式碼回撥到我們的監聽器中,接下來需要我們分析響應獲取預支付交易單ID

解析統一下單響應XML

統一下單響應也是XML資料,響應示例:

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

解析程式碼如下

// 解析“統一下單”介面返回的XML獲取預支付訂單ID
private Map<String, String> decodeXml(String xml) throws Exception {
    Map<String, String> resultMap = new HashMap<>();
    XmlPullParser parser = Xml.newPullParser();
    parser.setInput(new StringReader(xml));
    int event = parser.getEventType();
    while (event != XmlPullParser.END_DOCUMENT) {
        String nodeName = parser.getName();
        switch (event) {
            case XmlPullParser.START_DOCUMENT:
                break;
            case XmlPullParser.START_TAG:
                if (!"xml".equals(nodeName)) {
                    resultMap.put(nodeName, parser.nextText());
                }
                break;
            case XmlPullParser.END_TAG:
                break;
        }
        event = parser.next();
    }
    return resultMap;
}

這部分是常規的XML解析,邏輯很簡單,就是將響應資料放入Map中返回;至於返回的詳細資料在官方文件中有詳細說明,也有示例,這裡不再贅述了。

正常情況下,為了安全,在解析獲取的響應後我們可以拿到微信的簽名,還需要驗證簽名,這一步在這裡我沒有做。

通過這一步我們拿到了預支付交易單ID(在上面程式碼返回的Map中,key為prepare_id),接下來可以構建引數併發起App支付請求了。

構建微信App支付請求物件

// 構建微信支付請求物件
private PayReq buildWechatPayReq(String preOrderId) {
    PayReq payReq = new PayReq();
    payReq.appId = Constants.WX_APP_ID;
    payReq.partnerId = Constants.WX_PARTNER_ID;
    payReq.prepayId = preOrderId;
    payReq.packageValue = "Sign=WXPay";
    payReq.nonceStr = UUID.randomUUID().toString().replace("-", "");
    payReq.timeStamp = String.valueOf(System.currentTimeMillis() / 1000);

    // 構建基本的引數列表
    List<TwoTuple<String, String>> paramList = new ArrayList<>();
    paramList.add(new TwoTuple<>("appid", payReq.appId));
    paramList.add(new TwoTuple<>("noncestr", payReq.nonceStr));
    paramList.add(new TwoTuple<>("package", payReq.packageValue));
    paramList.add(new TwoTuple<>("partnerid", payReq.partnerId));
    paramList.add(new TwoTuple<>("prepayid", payReq.prepayId));
    paramList.add(new TwoTuple<>("timestamp", payReq.timeStamp));

    payReq.sign = generateWechatMD5Signature(paramList);

    return payReq;
}

建立支付請求物件PayReq,並將需要的各引數組織好返回即可。本步的簽名與統一下單引數一樣,唯一不同的就是具體引數項不同,但同樣要注意引數名ASCII碼從小到大排序(字典序)

發起微信App支付請求

發起App支付請求使用 IWXAPI 類的 sendReq 方法, IWXAPI 例項建立註冊程式碼如下所示:

mIWXAPI = WXAPIFactory.createWXAPI(getActivity(), null);
mIWXAPI.registerApp(Constants.WX_APP_ID);

支付結果回撥

通過以上幾步,如果所有引數都正確,在發起App支付請求後會啟動微信支付,要求使用者輸入支付密碼並確認支付,正常情況下支付會成功,然後微信支付會呼叫我們程式。

至於微信支付的響應接收要嚴格按照規範來做,要不無法成功呼叫。我們需要在應用包中新建一個wxapi包(在AndroidManifest.xml檔案中指定的包下)並建立一個 WXPayEntryActivity 類(包名或類名不一致會造成無法回撥)。

在WXPayEntryActivity類中實現onResp函式,支付完成後,微信APP會返回到商戶APP並回調onResp函式,開發者需要在該函式中接收通知,判斷返回錯誤碼,如果支付成功則去後臺查詢支付結果再展示使用者實際支付結果。注意一定不能以客戶端返回作為使用者支付的結果,應以伺服器端的接收的支付通知或查詢API返回的結果為準。

示例程式

package com.witmoon.eab.wxapi;

import android.content.Intent;
import android.os.Bundle;

import com.tencent.mm.sdk.constants.ConstantsAPI;
import com.tencent.mm.sdk.modelbase.BaseReq;
import com.tencent.mm.sdk.modelbase.BaseResp;
import com.tencent.mm.sdk.openapi.IWXAPI;
import com.tencent.mm.sdk.openapi.IWXAPIEventHandler;
import com.tencent.mm.sdk.openapi.WXAPIFactory;
import com.witmoon.eab.ui.base.BaseActivity;
import com.witmoon.eab.util.Constants;
import com.witmoon.svprogresshud.SVProgressHUD;

/**
 * 微信支付回撥Activity
 * Created by zhyh on 2015/11/28.
 */
public class WXPayEntryActivity extends BaseActivity implements IWXAPIEventHandler {

    private IWXAPI api;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        api = WXAPIFactory.createWXAPI(this, Constants.WX_APP_ID);
        api.handleIntent(getIntent(), this);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        api.handleIntent(intent, this);
    }

    @Override
    public void onReq(BaseReq baseReq) {
    }

    @Override
    public void onResp(BaseResp resp) {
        if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {
            int errCode = resp.errCode;
            if (errCode == -1) {
                SVProgressHUD.showErrorWithStatus(this, "支付出現錯誤");
            } else if (errCode == 0) {
                SVProgressHUD.showSuccessWithStatus(this, "支付完成");
            }
            finish();
        }
    }
}

另外一種回撥方式是在引數中提供notify_url引數,該引數指定一個伺服器地址即可,微信在支付完成後會呼叫並傳遞相關引數。

支付完成後,微信會把相關支付結果和使用者資訊傳送給商戶,商戶需要接收處理,並返回應答。

對後臺通知互動時,如果微信收到商戶的應答不是成功或超時,微信認為通知失敗,微信會通過一定的策略(如30分鐘共8次)定期重新發起通知,儘可能提高通知的成功率,但微信不保證通知最終能成功。 (通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒)

注意:同樣的通知可能會多次傳送給商戶系統。商戶系統必須能夠正確處理重複的通知。

推薦的做法是,當收到通知進行處理時,首先檢查對應業務資料的狀態,判斷該通知是否已經處理過,如果沒有處理過再進行處理,如果處理過直接返回結果成功。在對業務資料進行狀態檢查和處理之前,要採用資料鎖進行併發控制,以避免函式重入造成的資料混亂。

特別提醒:商戶系統對於支付結果通知的內容一定要做簽名驗證,防止資料洩漏導致出現“假通知”,造成資金損失。

總結

總地來說,微信支付相對於支付寶支付還是顯得複雜一些,尤其是統一下單要求以XML形式傳遞引數;但是如果能理順其步驟也不是特別難。下面總結一下需要注意的內容,包括上面描述和程式碼沒有提到的或比較重要的。

  • 引數簽名時一定要保證有序(按引數名ASCII碼從小到大排序),簽名要轉換成大寫形式
  • 引數名區分大小寫
  • 驗證呼叫返回或微信主動通知簽名時,傳送的sign引數不參與簽名,將生成的簽名與該sign值作校驗
  • 時間戮以秒為單位,Java中獲取當前時間是以毫秒為單位的,因此需要轉換一下
  • 金額以分為單位,其值不能帶有小數
  • 隨機字串不能超過32位,如果使用UUID生成,需要擷取(我在程式碼中將-去掉)

補充

在上面的程式碼中我使用了一個二元組的工具類TwoTuple,非常簡單,就是用來攜帶兩個元素的,程式碼如下:

/**
 * 兩個元素的元組,用於在一個方法裡返回兩種型別的值
 * Created by zhyh on 2015/5/5.
 */
public class TwoTuple<A, B> {

    public final A first;
    public final B second;

    public TwoTuple(A a, B b) {
        first = a;
        second = b;
    }

    /**
     * 建立一個二元組, 用所給引數初始化
     * @param a 第一個引數
     * @param b 第二個引數
     * @param <A>   第一個引數的型別
     * @param <B>   第二引數的型別
     * @return  包含所給引數的二元組
     */
    public static <A, B> TwoTuple<A, B> tuple(A a, B b) {
        return new TwoTuple<>(a, b);
    }
}