微信JSAPI支付(一)統一下單

開發準備

參考文檔 JSAPI支付開發文檔javascript

支付方式

目前微信主流的支付方式有如下6種php

方式 說明
付款碼支付 付款碼支付是用戶展現微信錢包內的「刷卡條碼/二維碼」給商戶系統掃描後直接完成支付的模式。主要應用線下面對面收銀的場景。
Native支付 Native支付是商戶系統按微信支付協議生成支付二維碼,用戶再用微信「掃一掃」完成支付的模式。該模式適用於PC網站支付、實體店單品或訂單支付、媒體廣告支付等場景。
JSAPI支付 JSAPI支付是用戶在微信中打開商戶的H5頁面,商戶在H5頁面經過調用微信支付提供的JSAPI接口調起微信支付模塊完成支付。
APP支付 APP支付又稱移動端支付,是商戶經過在移動端應用APP中集成開放SDK調起微信支付模塊完成支付的模式。
H5支付 H5支付主要是在手機、ipad等移動設備中經過瀏覽器來喚起微信支付的支付產品。
小程序支付 小程序支付是專門被定義使用在小程序中的支付產品。目前在小程序中能且只能使用小程序支付的方式來喚起微信支付。

由於前面作過關於公衆號的文章,所以這裏主要介紹JSAPI支付,後面的開發等也圍繞於此。html

JSAPI應用場景有:前端

  • 用戶在微信公衆帳號內進入商家公衆號,打開某個主頁面,完成支付
  • 用戶的好友在朋友圈、聊天窗口等分享商家頁面鏈接,用戶點擊連接打開商家頁面,完成支付
  • 將商戶頁面轉換成二維碼,用戶掃描二維碼後在微信瀏覽器中打開頁面後完成支付

核心名詞

不一樣於微信公衆號的測試開發,可使用內網穿透,和普通的測試帳號等。微信支付要求開發者,必需要有一個已經過驗證的真實商戶號,且該商戶號開通支付功能,以及該商戶下有真實的公衆號等。java

  1. 【微信商戶平臺】 微信商戶平臺是微信支付相關的商戶功能集合,包括參數配置、支付數據查詢與統計、在線退款、代金券或立減優惠運營等功能 平臺入口:pay.weixin.qq.comjquery

  2. 【微信公衆平臺】 微信公衆平臺是微信公衆帳號申請入口和管理後臺。商戶能夠在公衆平臺提交基本資料、業務資料、財務資料申請開通微信支付功能。 平臺入口:mp.weixin.qq.comgit

  3. 【微信支付系統】 微信支付系統是指完成微信支付流程中涉及的API接口、後臺業務處理系統、帳務系統、回調通知等系統的總稱。github

  4. 【商戶證書】 商戶證書是微信提供的二進制文件,商戶系統發起與微信支付後臺服務器通訊請求的時候,做爲微信支付後臺識別商戶真實身份的憑據。web

  5. 【商戶後臺系統】 商戶後臺系統是商戶後臺處理業務系統的總稱,例如:商戶網站、收銀系統、進銷存系統、發貨系統、客服系統等,通常關聯開發者本身的數據庫。算法

  6. 【簽名】 商戶後臺和微信支付後臺根據相同的密鑰和算法生成一個結果,用於校驗雙方身份合法性。簽名的算法由微信支付制定並公開,經常使用的簽名方式有:MD五、SHA一、SHA25六、HMAC等。

  7. 【支付密碼】 支付密碼是用戶開通微信支付時單獨設置的密碼,用於確認支付完成交易受權。該密碼與微信登陸密碼不一樣。

  8. 【Openid】 用戶在公衆號內的身份標識,不一樣公衆號擁有不一樣的openid。商戶後臺系統經過登陸受權、支付通知、查詢訂單等API可獲取到用戶的openid。主要用途是判斷同一個用戶,對用戶發送客服消息、模版消息等。

申請的核心帳戶參數:

帳戶參數說明

郵件中參數 API參數名 詳細說明
APPID appid appid是微信公衆帳號或開放平臺APP的惟一標識,在公衆平臺申請公衆帳號或者在開放平臺申請APP帳號後,微信會自動分配對應的appid,用於標識該應用。可在微信公衆平臺-->開發-->基本配置裏面查看,商戶的微信支付審覈經過郵件中也會包含該字段值。
微信支付商戶號 mch_id 商戶申請微信支付後,由微信支付分配的商戶收款帳號。
API密鑰 key 交易過程生成簽名的密鑰,僅保留在商戶系統和微信支付後臺,不會在網絡中傳播。商戶妥善保管該Key,切勿在網絡中傳輸,不能在其餘客戶端中存儲,保證key不會被泄漏。商戶可根據郵件提示登陸微信商戶平臺進行設置。也可按如下路徑設置:微信商戶平臺(pay.weixin.qq.com)-->帳戶中心-->帳戶設置-->API安全-->密鑰設置
Appsecret secret AppSecret是APPID對應的接口密碼,用於獲取接口調用憑證access_token時使用。在微信支付中,先經過OAuth2.0接口獲取用戶openid,此openid用於微信內網頁支付模式下單接口使用。可登陸公衆平臺-->微信支付,獲取AppSecret(需成爲開發者且賬號沒有異常狀態)。

協議規則

商戶接入微信支付,調用API必須遵循如下規則:

傳輸方式 爲保證交易安全性,採用HTTPS傳輸
提交方式 採用POST方法提交
數據格式 提交和返回數據都爲XML格式,根節點名爲xml
字符編碼 統一採用UTF-8字符編碼
簽名算法 MD5/HMAC-SHA256
簽名要求 請求和接收數據均須要校驗簽名,詳細方法請參考安全規範-簽名算法
證書要求 調用申請退款、撤銷訂單、紅包接口等須要商戶api證書,各api接口文檔均有說明。
判斷邏輯 先判斷協議字段返回,再判斷業務返回,最後判斷交易狀態

開發中代碼配置的參數(實際開發中建議直接在屬性文件中配置,便於環境切換)

// 公衆號、小程序appid
public static String APP_ID = "xxxxxxxxx"; 
// AppSecret
public static String SECRET = "xxxxxxxxx";
// 商戶號
public static final String MCH_ID = "xxxxxxxxx";
// API密鑰
public static final String API_KEY = "xxxxxxxxx";
// 網頁受權域名,JSAPI支付受權目錄,JS接口安全域名
public static final String AUTH_URL = "xxxxxxxxx";
複製代碼

以上參數不便公開。若是公司有現成的支付帳戶最好,沒有的話恐怕只能在某寶租用一下了,但沒有這些不影響前期的業務開發。

業務梳理

業務流程時序圖

對於開發者來講,發起支付的過程當中,

後端:主要調用了JSAPI支付中的三個接口:【統一下單API】、【支付結果通知API】、【查詢訂單API

前端:

前端微信內H5調起支付,提供用戶觸發微信支付的button和JSON數據傳輸。

開始開發

項目搭建

1、採用SpringBoot+Thymeleaf結構,參考微信公衆號快速開發(二)項目搭建與被動回覆

2、引入官方SDK工具包

閱讀文檔後發現,對於xml解析,加密算法等其實都時經常使用的方法,微信爲咱們直接提供了經常使用工具類方法的半成品,注意,這些只能是半成品,使用時須要作適當的更改。

連接:SDK與DEMO下載,選擇JAVA版本下載後解壓便可

代碼開發

公衆號配置

1、將公衆號和商戶的信息注入到Bean中

@Component
public class WXPayConfigExtend extends WXPayConfig {

    private byte[] certData;

    private WXPayConfigExtend() throws Exception {
// String certPath = WXPayConstants.APICLIENT_CERT;
// File file = new File(certPath);
// InputStream certStream = new FileInputStream(file);
// this.certData = new byte[(int) file.length()];
// certStream.read(this.certData);
// certStream.close();
    }

    @Override
    public String getAppID() {
        return WXPayConstants.APP_ID;
    }
    @Override
    public String getMchID() {
        return WXPayConstants.MCH_ID;
    }
    @Override
    public String getKey() {
        return WXPayConstants.API_KEY;
    }
    @Override
    public InputStream getCertStream() {
        ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
        return certBis;
    }
    @Override
    public int getHttpConnectTimeoutMs() {
        return 2000;
    }
    @Override
    public int getHttpReadTimeoutMs() {
        return 10000;
    }
    @Override
    public IWXPayDomain getWXPayDomain() {
        return WXPayDomainSimpleImpl.instance();
    }
    public String getPrimaryDomain() {
        return "api.mch.weixin.qq.com";
    }
    public String getAlternateDomain() {
        return "api2.mch.weixin.qq.com";
    }
    @Override
    public int getReportWorkerNum() {
        return 1;
    }
    @Override
    public int getReportBatchSize() {
        return 2;
    }
}
複製代碼

獲取openid

需頁面提供網頁受權,以獲取openid,關於微信網頁受權可參考:微信公衆號快速開發(四)微信網頁受權

頁面:

頁面這裏直接設計了一個能夠發起預支付的按鈕的靜態頁面:templates/preOrder.html

裏面包含了跳轉到後端支付接口的表單:

<form name=wexinpayment action='http://chety.mynatapp.cc/api/v1/wechat1/placeOrder' method=post target="_blank">
    ...
複製代碼

Thymeleaf下頁面轉發的控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {

    // 用於thymeleaf環境下,跳轉到字符串相應的html頁面
    @RequestMapping("/{path}")
    public String webPath(@PathVariable String path) {
        return path;
    }
}   
複製代碼

網頁受權的入口控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class IndexController {

    ...

    @RequestMapping("/index")
    public void index(String code, Model model, HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 顯式受權,得到code
        if (code != null) {
            JSONObject json = WeChatUtil.getWebAccessToken(code);
            WXPayUtil.getLogger().info("code: ",json.toJSONString());
            String openid = json.getString(("openid"));
            request.getSession().setAttribute("openid", openid);
            WXPayUtil.getLogger().info("index openid={}",openid);
            // 重定向到預下單頁面
            response.sendRedirect("preOrder"); // 重定向到預支付頁面
        } else {
            StringBuffer url = RequestUtil.getRequestURL(request);
            WXPayUtil.getLogger().info("index 請求路徑:{}"+url);
            String path = WeChatUtil.WEB_REDIRECT_URL.replace("APPID", WeChatConstants.APP_ID).replace("REDIRECT_URI", url).replace("SCOPE", "snsapi_userinfo");
            WXPayUtil.getLogger().info("index 重定向:{}",path);
            // 重定向到受權獲取code的頁面
            response.sendRedirect(path);
        }
    }
}    
複製代碼

啓動項目,請求接口:

1、 微信開發者工具的地址欄輸入:{網頁受權域名}//api/v1/wechat1/index

2、確認【贊成】受權,(這裏目的是爲了獲取openid,也可使用base靜默受權的模式,不用顯示的提示受權),跳轉到預支付頁面,如圖:

發起支付

當用戶確認預支付頁面的訂單時,將請求【/placeOrder】接口,該業務將調用微信的【統一下單】接口:

1、微信統一下單實體類

@Setter
@Getter
@ToString
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class WxOrderEntity {
    private String appid;
    private String mchId;
    private String deviceInfo;
    private String nonceStr;
    private String sign;
    private String body;
    private String outTradeNo;
    private int totalFee;
    private String spbillCreateIp;
    private String notifyUrl;
    private String tradeType;
    private String openid;
}
複製代碼

2、微信支付的業務層

@Service
public class WxBackendServiceImpl {

    @Autowired
    WXPayConfigExtend wxPayConfigExtend;

    // 統一下單
    public Map<String, Object> unifiedorder(Model model, HttpServletRequest request) throws Exception {
        WXPayUtil.getLogger().info("進入下單控制器...");
        Map<String,Object> data = null;
        try {
            //生成訂單編號
            WXPay wxpay = new WXPay(wxPayConfigExtend);
            WxOrderEntity order = new WxOrderEntity();

            double price = 0.01;
            String orderName = "xxx--微信支付";
            int number = (int)((Math.random()*9)*1000);//隨機數
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");//時間
            String orderNumber = dateFormat.format(new Date()) + number;
            String nonceStr = WXPayUtil.generateNonceStr();
            String openId = (String) request.getSession().getAttribute("openid");
            openId = openId == null ? "o4036jqo2PN9isV6N2FHGRsGRVqg" : openId; // 前一個openid,是chet在xxx公衆號下的openid

            order.setBody(orderName);
            order.setOutTradeNo(orderNumber);
            order.setTotalFee(MoneyUtil.Yuan2Fen(price));
            order.setSpbillCreateIp(IpUtils.getIpAddr(request));
            order.setOpenid(openId);
            order.setNotifyUrl(WXPayConstants.NOTIFY_URL);
            order.setTradeType(WXPayConstants.TRADE_TYPE_JSAPI);
            order.setNonceStr(nonceStr);

            WXPayUtil.getLogger().info("save 統一下單接口調用,order:{}",order);
            // 利用sdk統一下單,已自動調用wxpay.fillRequestData(data);
            Map<String, String> response = wxpay.doWxPayApi(order,WXPayConstants.UNIFIEDORDER);
            WXPayUtil.getLogger().info("save 下單結果,response:{}",response);

            if(response.get(WXPayConstants.RETURN_CODE).equals("SUCCESS")&&response.get(WXPayConstants.RESULT_CODE).equals("SUCCESS")){
                String url = request.getQueryString() == null?request.getRequestURL().toString():request.getRequestURL()+"?"+request.getQueryString();
                String prepayId = response.get(WXPayConstants.PREPAY_ID);
                data = wxpay.permissionValidate(nonceStr,url,prepayId,wxPayConfigExtend.getKey());
                return data;
            }
        } catch (Exception e) {
            WXPayUtil.getLogger().error("doUnifiedOrder--下單失敗:{}" , e.getMessage());
        }
        return null;
    }
}    
複製代碼

wxpay.doWxPayApi(...)封裝了對下單接口的調用:

public Map<String, String> doWxPayApi(WxOrderEntity order,String apiType) {
    Map<String, String> resp = null;
    try {
        Map<String,String> map = new HashMap<>();
        map.put("out_trade_no", order.getOutTradeNo());
        map.put("nonce_str", order.getNonceStr());
        map.put("trade_type", order.getTradeType());

        if ("unifiedorder".equalsIgnoreCase(apiType)) {
            map.put("spbill_create_ip", order.getSpbillCreateIp());
            map.put("openid", order.getOpenid());
            map.put("notify_url", order.getNotifyUrl());
            map.put("total_fee", String.valueOf(order.getTotalFee()));
            map.put("body", order.getBody());

            resp = unifiedOrder(map);
        } else if ("orderquery".equalsIgnoreCase(apiType)) {
            resp = orderQuery(map);
        } else if ("closeorder".equalsIgnoreCase(apiType)) {
            resp = orderQuery(map);
        }
    } catch (Exception e) {
        WXPayUtil.getLogger().error(order.getOutTradeNo()+" -- 調用接口失敗 {}",e.getMessage());
    }
    return resp;
}
複製代碼

wxPay.doWxPayApi(...)封裝了對簽名的二次校驗:

public Map<String, Object> permissionValidate(String nonceStr, String url, String prepayId, String key) throws Exception {
    //jssdk權限驗證參數
    TreeMap<Object, Object> param = new TreeMap<>();
    Map<String, Object> data = new HashMap<>();
    param.put("appId", WeChatConstants.APP_ID);
    String timestamp = String.valueOf(WXPayUtil.getCurrentTimestamp());
    param.put("timestamp", timestamp);//全小寫
    param.put("nonceStr", nonceStr);
    //map.put("signature",WeChatUtil.getSignature(timestamp,uuid,RequestUtil.getUrl(request)));
    param.put("signature", WeChatUtil.getSignature(timestamp, nonceStr, url));
    data.put("configMap", param);

    //微信支付權限驗證參數
    Map<String, String> payMap = new HashMap<>();
    payMap.put("appId", WeChatConstants.APP_ID);
    payMap.put("timeStamp", timestamp);//駝峯
    payMap.put("nonceStr", nonceStr);
    payMap.put("package", "prepay_id=" + prepayId);
    payMap.put("signType", "MD5");
    payMap.put("paySign", WXPayUtil.generateSignature(payMap, key));
    payMap.put("packageStr", "prepay_id=" + prepayId);
    data.put("payMap", payMap);

    return data;
}
複製代碼

支付結果通知與回調

配置回調接口的控制器:

@Controller
@RequestMapping("/api/v1/wechat1")
public class NotifyController {

    WxBackendServiceImpl wxBackendService;

    /** * 在調用下單接口時,咱們會傳入 異步接收微信支付結果通知的回調地址,顧名思義這個地址做用就是用來接收支付結果通知, * 當用戶在前端支付成功後,微信服務器會自動調用此地址,而後商戶再進行處理 * @param request * @param response * @return */
    @RequestMapping("/wxnotify")
    public String wxNotify(HttpServletRequest request, HttpServletResponse response) {
        String respXml = "";
        try (InputStream in = request.getInputStream();
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            // 獲取微信調用咱們notify_url的返回信息
            String notifyData = new String(baos.toByteArray(), "utf-8");
            // 回調處理
            respXml = wxBackendService.payCallBack(notifyData);
        } catch (Exception e) {
            WXPayUtil.getLogger().error("wxnotify:支付回調發布異常:", e.getMessage());
        } finally {
            try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())){
                // 處理業務完畢
                bos.write(respXml.getBytes());
            } catch (IOException e) {
                WXPayUtil.getLogger().error("wxnotify:支付回調發布異常:out:", e.getMessage());
            }
        }
        return respXml;
    }
}
複製代碼

回調業務:

public String payCallBack(String notifyData) throws Exception{
    // String respXml = WXPayConstants.RESP_FAIL_XML;
    Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyData);

    if (WXPayConstants.SUCCESS.equalsIgnoreCase(notifyMap.get(WXPayConstants.RESULT_CODE))) {
        WXPayUtil.getLogger().info("payCallBack:微信支付----返回成功");
        if (WXPayUtil.isSignatureValid(notifyMap, WXPayConstants.API_KEY)) {
            // TODO 數據庫操做,付款記錄修改 & 記錄付款日誌
            WXPayUtil.getLogger().info("payCallBack:微信支付----驗證簽名成功,更新數據庫");
            /*String outTradeNo = notifyMap.get("out_trade_no"); OrderTrading dbOrder = transactionService.findByOutTradeNo(outTradeNo); // 將未支付狀態改成已支付 if (dbOrder != null && dbOrder.getState() == 1) { // 處理業務 - 修改訂單狀態 OrderTrading order = new OrderTrading(); order.setOutTradeNo(outTradeNo); order.setNotifyTime(new Date()); order.setState(1); transactionService.updateTransOrderByWxnotify(order); // TODO 數據庫更新異常,補償措施 }*/
            // 通知微信.異步確認成功.必寫.否則會一直通知後臺.八次以後就認爲交易失敗了.
            return WXPayConstants.RESP_SUCCESS_XML;
        } else {
            WXPayUtil.getLogger().error("payCallBack:微信支付----判斷簽名錯誤");
        }
    } else {
        WXPayUtil.getLogger().error("payCallBack:支付失敗,錯誤信息:" + notifyMap.get(WXPayConstants.ERR_CODE_DES));
    }
    return WXPayConstants.RESP_FAIL_XML;
}
複製代碼

靜態頁面

預下單頁面:templates/preOrder.html

確認下單頁面:templates/toOrder.html

該頁面用於簽名校驗和參數傳遞,爲便於觀察,開啓了調試模式

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>當即支付:123</h1> <button type="submit" id="payBtn">支付</button> <script th:src="@{/static/js/jquery-1.8.3.min.js}" type="text/javascript" charset="utf-8" rel="stylesheet"></script> <script type="text/javascript" th:src="@{/static/js/jquery.rotate.min.js}" rel="stylesheet"></script> <!--微信的JSSDK--> <script th:src="@{http://res.wx.qq.com/open/js/jweixin-1.2.0.js}"></script> <script> $(function() { <!--經過config接口注入權限驗證配置--> alert('[[${configMap}]]'); alert('[[${payMap}]]'); wx.config({ debug: true, // 開啓調試模式 appId: '[[${configMap.appId}]]', // 公衆號的惟一標識 timestamp: '[[${configMap.timestamp}]]', // 生成簽名的時間戳 nonceStr: '[[${configMap.nonceStr}]]', // 生成簽名的隨機串 signature: '[[${configMap.signature}]]',// 簽名 jsApiList: ['chooseWXPay'] // 填入須要使用的JS接口列表,這裏是先聲明咱們要用到支付的JS接口 }); <!-- config驗證成功後會調用ready中的代碼 --> wx.ready(function(){ //點擊立刻付款按鈕 $("#payBtn").click(function(){ //彈出支付窗口 wx.chooseWXPay({ timestamp: '[[${payMap.timeStamp}]]', // 支付簽名時間戳, nonceStr: '[[${payMap.nonceStr}]]', // 支付簽名隨機串,不長於 32 位 package: '[[${payMap.packageStr}]]', // 統一支付接口返回的prepay_id參數值,提交格式如:prepay_id=xxxx) signType: '[[${payMap.signType}]]', // 簽名方式,默認爲'SHA1',使用新版支付需傳入'MD5' paySign: '[[${payMap.paySign}]]', // 支付簽名 success: function (res) { // 支付成功後的回調函數 alert("支付成功!"); } }); }) }); }); </script> </body> </html> 複製代碼

效果演示

項目啓動後,點擊確認支付就能夠看看到debug模式下參數的顯示了。最後的支付效果如圖:

注:

  1. 支付回調的端口必須是80,應該是出於安全考慮
  2. web開發工具只能用於調試,測試支付功能時,須要用手機打開。
  3. 細心的朋友可能看出來,訂單的時間早了一個多月。這個是我以前用公司帳號和域名開發的,用的當時的截圖。

本文的代碼是爲了展現統一下單的流程,卻不利於移植,目前代碼已重構。

源代碼請查看:github.com/chetwhy/wpp

相關文章
相關標籤/搜索