1. 程式人生 > >Java微信掃碼支付

Java微信掃碼支付

Java微信掃碼支付

以下內容是基於模式二開發
在開發之前需要先到微信支付官網註冊賬號,並獲取到以下資訊
appid:wx1137939101111111公眾賬號id
mch_id:1438111111 商戶號
key:4Inn0va1eSxOnl1neqsxwuhan1111111金鑰
send_url:https://api.mch.weixin.qq.com/pay/unifiedorder統一下單API
notify_url:http://127.0.0.1:8080/sop/order/notify/wechat支付成功回撥地址

更多精彩

官網

  1. 微信支付官網
  2. 掃碼支付開發者文件

模式

  1. 需要在公眾平臺後臺設定支付回撥URL ,用於接收使用者掃碼後微信支付系統回撥的productid和openid
  2. 直接呼叫統一下單API 即可,相對於模式一更為簡潔

定義介面物件

  1. 根據 統一下單介面API 定義四個物件,用於傳送和接收資料
    UnifiedOrderRequest.java 統一下單請求引數-必填項
public class UnifiedOrderRequest {

    // 公眾賬號id
    private
String appid; // 商戶號 private String mch_id; // 隨機字串,32位以內 private String nonce_str; // 簽名,遵循簽名演算法 private String sign; // 商品描述,瀏覽器開啟的網站主頁title名稱-商品概述 private String body; // 商戶訂單號,32位以內,不重複 private String out_trade_no; // 標價金額,單位分 private Integer total_fee; // 終端ip,填寫呼叫端的ip
private String spbill_create_ip; // 通知地址,接收支付結果的會掉地址,必須外網可訪問 private String notify_url; // 交易型別,JSAPI--公眾號支付、NATIVE--原生掃碼支付、APP--app支付 private String trade_type; }

UnifiedOrderRequestExt.java 統一下單請求引數-非必填項

public class UnifiedOrderRequestExt extends UnifiedOrderRequest {

    // 裝置號,網頁端填寫WEB
    private String device_info;

    // 簽名型別,預設MD5
    private String sign_type;

    // 商品詳情,JSON格式
    private String detail;

    // 附加資料,可作為自定義引數使用
    private String attach;

    // 標價幣種,預設CNY
    private String fee_type;

    // 交易起始時間,格式為yyyyMMddHHmmss
    private String time_start;

    // 交易結束時間,最短失效時間必須間隔5分鐘
    private String time_expire;

    // 商品id,trade_type=NATIVE時,必填
    private String product_id;

    // 指定支付方式,no_credit可限制使用信用卡
    private String limit_pay;

    // 使用者標識,trade_type=JSAPI時,必填
    private String openid;
}

UnifiedOrderResponse.java 統一下單返回引數-必填項

public class UnifiedOrderResponse {

    // 返回狀態碼,通訊標識,SUCCESS/FAIL
    private String return_code;

    // 公眾賬號id
    private String appid;

    // 商戶號
    private String mch_id;

    // 隨機字串
    private String nonce_str;

    // 簽名
    private String sign;

    // 業務結果,交易標識,SUCCESS/FAIL
    private String result_code;

    // 交易型別,JSAPI,NATIVE,APP
    private String trade_type;

    // 預支付交易會話標識,有效值2小時
    private String prepay_id;
}

UnifiedOrderResponseExt.java 統一下單返回引數-非必填項

public class UnifiedOrderResponseExt extends UnifiedOrderResponse {

    // 返回資訊,非空則表示返回了錯誤資訊
    private String return_msg;

    // 裝置號
    private String device_info;

    // 錯誤程式碼
    private String err_code;

    // 錯誤程式碼描述
    private String err_code_des;

    // 二維碼連線,trade_type=NATIVE時返回
    private String code_url;
}

定義一個標籤用於顯示二維碼

  • 呼叫統一下單API成功後,會返回一系列XML資料,其中code_url表示返回的預支付交易連結,可將其生成二維碼圖片
<div class="order-pay-panel order-wechat-panel">
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
        <h4 class="modal-title">
            <i class="icon-th-large"></i> 微信支付
        </h4>
    </div>
    <div class="modal-body">
        <div class="wechat-qrcode-panel margin-bottom-10">
            <img src="${ctx}/api/order/pay/wechat/qrcode?orderId=${order.hexId}">
        </div>
        <div class="wechat-description-panel">
            <p class="text-muted">使用微信掃描二維碼完成支付</p>
            <p class="text-danger">¥${order.price}</p>
        </div>
    </div>
</div>

根據統一下單API的要求生成訂單

  • 將系統內部訂單號傳入請求引數的out_trade_no中,用於後續操作的唯一識別符號
  • 請求引數中的sign是驗證引數合法性的唯一標識,需要根據 微信支付簽名演算法 來生成
  • 使用XStream將物件轉換為XML,由於微信的請求引數中大量使用下劃線,但下劃線在XStream中是關鍵字,因此需要把下劃線轉換為雙下劃線,避免報錯
private String generateOrderInfo(Long orderId) throws Exception {
    // 獲取訂單資訊
    OrderDTO order = orderManageService.getOrder(orderId);

    // 生成訂單
    UnifiedOrderRequestExt ext = new UnifiedOrderRequestExt();
    ext.setAppid(SOPConstants.WECHAT_PAY_APP_ID);
    ext.setMch_id(SOPConstants.WECHAT_PAY_MCH_ID);
    ext.setBody("輕實訓-" + order.getName());
    ext.setOut_trade_no(order.getCode());
    ext.setTotal_fee(order.getPrice() * 100);
    ext.setSpbill_create_ip(super.getClientIP());
    ext.setNotify_url(SOPConstants.WECHAT_PAY_NOTIFY_URL);
    ext.setTrade_type("NATIVE");
    ext.setProduct_id(order.getHexId());

  // 生成32位隨機數
    ext.setNonce_str(makeNonceStr());

  // 簽名,按照指定簽名演算法生成
    ext.setSign(makeSign(ext));

    // 格式轉換為XML
    XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
    xStream.alias("xml", UnifiedOrderRequestExt.class);

    return xStream.toXML(ext);
}
  • 生成32位隨機數,方式為當前時間加隨機數
private String makeNonceStr() {
    StringBuffer str = new StringBuffer(DateUtil.getSysDateString("yyyyMMddHHmmssS"));
    str.append((new Random().nextInt(900) + 100));

    return str.toString();
}
  • 拼接簽名資料
private String makeSign(UnifiedOrderRequestExt ext) throws Exception {
    // 根據規則建立可排序的map集合
    SortedMap<String, String> signMaps = Maps.newTreeMap();
    signMaps.put("appid", ext.getAppid());
    signMaps.put("body", ext.getBody());
    signMaps.put("mch_id", ext.getMch_id());
    signMaps.put("nonce_str", ext.getNonce_str());
    signMaps.put("notify_url", ext.getNotify_url());
    signMaps.put("out_trade_no", ext.getOut_trade_no());
    signMaps.put("spbill_create_ip", ext.getSpbill_create_ip());
    signMaps.put("trade_type", ext.getTrade_type());
    signMaps.put("total_fee", ext.getTotal_fee().toString());
    signMaps.put("product_id", ext.getProduct_id());

    // 生成簽名
    return generateSign(signMaps);
}
  • 按照簽名演算法生成簽名
private String generateSign(SortedMap<String, String> signMaps) throws Exception {
    StringBuffer sb = new StringBuffer();

    // 字典序
    for (Map.Entry signMap : signMaps.entrySet()) {
        String key = (String) signMap.getKey();
        String value = (String) signMap.getValue();

        // 為空不參與簽名、引數名區分大小寫
        if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
            sb.append(key + "=" + value + "&");
        }
    }

    // 拼接key
    sb.append("key=" + SOPConstants.WECHAT_PAY_KEY);

    // MD5加密
    return encoderByMd5(sb.toString()).toUpperCase();
}

呼叫統一下單API

  • 將生成的訂單傳送給微信,同時接收微信的返回引數,讀取其中的code_url
  • 如果傳送的訂單資訊不符合要求,則會在返回引數中告知問題
  • 訂單合法,返回引數中return_code=SUCCESS return_msg=OK result_code=SUCCESS
  • 訂單不合法,返回引數中return_code=FAIL return_msg=具體錯誤原因
private String sendHttpRequest(String orderInfo) throws IOException {
    // 建立連線
    HttpURLConnection conn = (HttpURLConnection) new URL(SOPConstants.WECHAT_PAY_SEND_URL).openConnection();
    conn.setRequestMethod("POST");
    conn.setDoOutput(true);

    // 傳送資料
    BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream());
    bos.write(orderInfo.getBytes());
    bos.flush();
    bos.close();

    // 獲取資料
    BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

    // 接收資料
    String line;
    StringBuffer str = new StringBuffer();
    while ((line = reader.readLine()) != null) {
        str.append(line);
    }

    // XML資料轉換為物件
    XStream xStream = new XStream(new XppDriver(new XmlFriendlyReplacer("_-", "_")));
    xStream.alias("xml", UnifiedOrderResponseExt.class);

    UnifiedOrderResponseExt ext = (UnifiedOrderResponseExt) xStream.fromXML(str.toString());

    // 判斷資料有效性
    if (null != ext && "SUCCESS".equals(ext.getReturn_code()) && "SUCCESS".equals(ext.getResult_code())) {
        return ext.getCode_url();
    }

    return null;
}

根據返回的code_url生成二維碼圖片

@RequestMapping(value = “/wechat/qrcode”, method = RequestMethod.GET)
public void wechatQRCode(HttpServletResponse response, @RequestParam("orderId") String orderId) {
    try {
        // 初始化資料
        int width = 240;
        int height = 240;
        String format = "png";
  // 獲取二維碼連結
        String codeUrl = orderPayService.getQRCodeUrl(IdEncoder.decodeId(orderId));

        Hashtable htable = new Hashtable();
        htable.put(EncodeHintType.CHARACTER_SET, "UTF-8");

        // 生成圖片
        BitMatrix matrix = new MultiFormatWriter().encode(codeUrl, BarcodeFormat.QR_CODE, width, height, htable);

        OutputStream out = response.getOutputStream();

        // 輸出圖片
        MatrixToImageWriter.writeToStream(matrix, format, out);

        out.flush();
        out.close();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}
  • 需要準備的包資訊
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.3.0</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.3.0</version>
</dependency>

接收回調

  • 使用者通過微信掃描二維碼並支付成功後,微信會根據之前訂單中的notify_url回撥地址進行回執
  • 此處提供給微信的回撥地址必須是外網可訪問的,否則無法正常接收回執資訊
  • 由於回執時並沒有攜帶使用者資訊,所以如果使用了諸如shiro等安全框架的,需要給予該回執地址一個訪問許可,否則會被安全框架遮蔽
  • 傳送回執是非同步進行,由於網路等不確定因素,微信不保證回執一定成功
  • 微信會通過一定的策略定期重啟發送通知,通知頻率為15/15/30/180/1800/1800/1800/1800/3600,單位:秒
  • 雖然是非同步回執,但並不需要採用ajax非同步接收的方式來接收資料
@RequestMapping(value = "/wechat")
public String wechatNotify(HttpServletResponse response, HttpServletRequest request) throws Exception {
    orderPayService.notify(response, request);

  // 此處的返回值無效,需要在支付頁面通過輪詢獲取支付結果,微信支付本身無法實現自動跳轉
    return "/redirect:/login";
}

處理回執內容

  • 資料是通過IO流傳送,所以也需要通過IO流接收
  • 微信傳送回執使用者接收後,需要通過IO流的方式告知微信接收成功,否則微信認為回執失敗
  • 接收到回執資訊後,最關鍵是驗證簽名來確保資訊的有效性和安全性,驗籤的方式和傳送訂單簽名的方式一致
  • 驗籤成功,且回執資訊中result_code=SUCCESS,則表示回執資訊有效
  • 從回執資訊中可獲取到out_trade_no,這是之前傳送的使用者訂單唯一識別符號,通過該資訊可以繼續處理使用者訂單
  • 所有流程處理完畢後,必須以XML格式編寫回執資訊,並通過IO流的方式告知微信回執接收成功
public void notify(HttpServletResponse response, HttpServletRequest request) throws Exception {
    // 讀取回執資料
    HashMap<String, String> notifyMaps = readNotify(request);

    // 回執資料驗證
    if (notifyMaps == null || notifyMaps.isEmpty()) {
        logger.error("未收到回執資料!");
        throw new TSharkException("未收到回執資料!");
    }

    // 挑選資料
    SortedMap<String, String> notifySorts = sortNotify(notifyMaps);

    // 重新簽名
    String sign = generateSign(notifySorts);
    // 獲取回執簽名
    String notifySign = notifySorts.get("sign").toUpperCase();

    // 驗證簽名
    if (!sign.equals(notifySign)) {
        logger.error("簽名驗證失敗!");
        throw new TSharkException("簽名驗證失敗!");
    }

    String resXml;

    // 驗證回執
    if ("SUCCESS".equals(notifySorts.get("result_code"))) {
        // 更新訂單資訊
        updateOrderInfo(notifySorts.get("out_trade_no"));

        // 微信回執
        resXml = "<xml>" +
                "<return_code><![CDATA[SUCCESS]]></return_code>" +
                "<return_msg><![CDATA[OK]]></return_msg>" +
                "</xml> ";
    } else {
        resXml = "<xml>" +
                "<return_code><![CDATA[FAIL]]></return_code>" +
                "<return_msg><![CDATA[報文為空]]></return_msg>" +
                "</xml> ";
    }

    BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());
    out.write(resXml.getBytes());
    out.flush();
    out.close();
}
  • 從IO流中讀取回執資訊
private HashMap<String, String> readNotify(HttpServletRequest request) throws Exception {
    // 讀取引數
    InputStream inputStream = request.getInputStream();
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

    // 接收資料
    String line;
    StringBuffer str = new StringBuffer();
    while ((line = reader.readLine()) != null) {
        str.append(line);
    }

    reader.close();
    inputStream.close();

    // XML轉換Map
    return fromXml(str.toString());
}
  • 讀取的回執資訊時XML格式,需要通過jDom的SAXBuilder解析為Map
private HashMap<String, String> fromXml(String xml) throws Exception {
    xml = xml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");

    if (null == xml || "".equals(xml)) {
        return null;
    }

    HashMap<String, String> m = Maps.newHashMap();

    InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));

    SAXBuilder builder = new SAXBuilder();

    Document doc = builder.build(in);

    Element root = doc.getRootElement();

    List list = root.getChildren();

    Iterator it = list.iterator();

    while (it.hasNext()) {
        Element e = (Element) it.next();

        String k = e.getName();
        String v = "";
        List children = e.getChildren();

        if (children.isEmpty()) {
            v = e.getTextNormalize();
        } else {
            v = getXmlChildren(children);
        }

        m.put(k, v);
    }

    //關閉流
    in.close();

    return m;
}

private String getXmlChildren(List children) {
    StringBuffer sb = new StringBuffer();

    if (!children.isEmpty()) {
        Iterator it = children.iterator();

        while (it.hasNext()) {
            Element e = (Element) it.next();

            String name = e.getName();
            String value = e.getTextNormalize();

            List list = e.getChildren();

            sb.append("<" + name + ">");

            if (!list.isEmpty()) {
                sb.append(getXmlChildren(list));
            }

            sb.append(value);
            sb.append("</" + name + ">");
        }
    }

    return sb.toString();
}

輪詢訂單狀態,實現支付完成後頁面自動跳轉

  • 由於支付回執是非同步的,所以即使捕獲到非同步回執也無法實現支付頁面的自動跳轉
  • 所以需要在支付頁面開啟時設定一個ajax輪詢訂單狀態,一旦訂單狀態更新,則進行頁面跳轉
// 頁面關閉
$(".modal-header button.close:last").click(function () {
    // 停止輪詢
    clearInterval(checkTimer);
});

// 輪詢訂單狀態
checkTimer = setInterval(function () {
    // 視窗是否開啟
    if ($(".order-pay-panel").length <= 0) {
        clearInterval(checkTimer);
    }

    // 獲取訂單狀態
    $.ts.doAction("/api/order/review/check/", {
        orderId: orderId
    }, function () {
        // 訂單已支付
        if (!this.data) {
            $.ts.closeWindow();

            g_index.loadMainContentWithState("/order/manage");

      $.ts.toastr.success("訂單已支付成功!");
        }
    }, "", "", "");
}, 3000);