首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 實現微信 H5 支付(JSAPI)完整攻略:從統一下單到前端喚起

     編輯:Java 相關知識

背景與坑點提示

微信官方文檔存在「JS-SDK 支付」與「商戶平台 H5 支付」兩套並存介面,參數命名不一致(timestamp vs timeStamp、package vs prepay_id),且無官方 Java Demo。本文記錄實測可行的 JSAPI 支付(公眾號內網頁支付) 全流程,適用於服務號/訂閱號(已認證)場景。

整體流程圖

  1. 用戶授權獲取 openid(靜默授權/網頁授權)
  2. 後端呼叫統一下單 API 獲得 prepay_id
  3. 後端組裝前端所需參數並進行二次簽名
  4. 前端頁面載入 WeixinJSBridge 喚起支付
  5. 用戶輸入密碼完成支付,微信異步通知商戶後台(notify_url)

後端核心代碼(Spring MVC 風格)

1. 統一下單獲取 prepay_id

@ResponseBody
public String getPrepayid(String outTradeNo, String totalFee, String openid) {
    Map<String, String> params = new HashMap<>();
    params.put("appid", Common.appid);
    params.put("mch_id", Common.mch_id);
    params.put("nonce_str", Common.randString(16));
    params.put("body", "商品訂單");
    params.put("out_trade_no", outTradeNo);
    params.put("total_fee", totalFee);           // 單位:分
    params.put("spbill_create_ip", "xxx.xxx.38.47");
    params.put("notify_url", "https://your.domain/wxpay_notify");
    params.put("trade_type", "JSAPI");
    params.put("openid", openid);

    String sign = Common.sign(params, Common.MchSecret); // 簽名
    params.put("sign", sign);

    String xml = Common.mapToXml(params);               // Map → XML
    String rsp = HttpClient.httpsPost("https://api.mch.weixin.qq.com/pay/unifiedorder", xml);
    JSONObject jo = XmlUtil.xmlToJson(rsp);

    if ("SUCCESS".equals(jo.getString("return_code")) && 
        "SUCCESS".equals(jo.getString("result_code"))) {
        return jo.getString("prepay_id");
    }
    throw new RuntimeException("統一下單失敗:" + jo.getString("return_msg"));
}

2. 微信簽名算法(MD5 + ASCII 排序 + 商戶金鑰)

public static String sign(Map<String, String> map, String key) {
    // 1. 過濾空值 + ASCII 排序拼接 stringA
    String stringA = map.entrySet().stream()
        .filter(e -> e.getValue() != null && !e.getValue().isEmpty())
        .sorted(Map.Entry.comparingByKey())
        .map(e -> e.getKey() + "=" + e.getValue())
        .collect(Collectors.joining("&"));

    // 2. 拼接商戶金鑰
    String stringSignTemp = stringA + "&key=" + key;

    // 3. MD5 → 大寫
    return MD5.MD5Encode(stringSignTemp).toUpperCase();
}

3. Map 轉 XML(CDATA 處理非數值欄位)

public static String mapToXml(Map<String, String> map) {
    StringBuilder xml = new StringBuilder("<xml>");
    map.forEach((k, v) -> {
        if (v.matches("\\d+")) xml.append("<").append(k).append(">").append(v).append("</").append(k).append(">");
        else xml.append("<").append(k).append("><![CDATA[").append(v).append("]]></").append(k).append(">");
    });
    xml.append("</xml>");
    return xml.toString();
}

4. 組裝前端喚起參數(二次簽名)

String prepayId = getPrepayid(...);
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 10 位秒級
String nonceStr = Common.randString(16);

Map<String, String> frontParams = new HashMap<>();
frontParams.put("appId", Common.appid);
frontParams.put("timeStamp", timeStamp);
frontParams.put("nonceStr", nonceStr);
frontParams.put("package", "prepay_id=" + prepayId); // 注意前綴
frontParams.put("signType", "MD5");
String paySign = Common.sign(frontParams, Common.MchSecret);

// 傳給 JSP/Thymeleaf
model.addAttribute("appId", Common.appid);
model.addAttribute("timeStamp", timeStamp);
model.addAttribute("nonceStr", nonceStr);
model.addAttribute("prepay_id", "prepay_id=" + prepayId);
model.addAttribute("signType", "MD5");
model.addAttribute("paySign", paySign);

前端喚起代碼(JSP 片段)

<script>
function onBridgeReady(){
    WeixinJSBridge.invoke('getBrandWCPayRequest', {
        "appId"     : "${appId}",
        "timeStamp" : "${timeStamp}",
        "nonceStr"  : "${nonceStr}",
        "package"   : "${prepay_id}",
        "signType"  : "MD5",
        "paySign"   : "${paySign}"
    }, function(res){
        if(res.err_msg == "get_brand_wcpay_request:ok"){
            alert("支付成功"); location.href = "/order/success";
        }else{
            alert("支付失敗:" + res.err_msg);
        }
    });
}
if (typeof WeixinJSBridge == "undefined") {
    document.addEventListener ? document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
                               : document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
} else { onBridgeReady(); }
</script>

關鍵坑點總結表

坑點症狀解決
timeStamp 大小寫簽名驗證失敗後端生成 10 位秒級字串,前端參數名 timeStamp(S 大寫)
package 參數值「參數錯誤」必須為 prepay_id=xxxx 格式,非直接傳 prepay_id
金額單位金額錯誤統一下單 total_fee 單位為分,前端不傳金額
商戶金鑰 vs AppSecret簽名不匹配統一下單與二次簽名均用商戶平台金鑰 (MchSecret),非公眾號 AppSecret
回調地址需外網可達收不到通知使用內網穿透或正式域名,不可用 localhost

安全與合規建議

  • 簽名金鑰絕不前端暴露,所有簽名在後端完成;
  • 驗證回調簽名、金額、訂單號一致性,防止偽造通知;
  • 冪等處理回調:以 out_trade_no 為鍵防重複入帳;
  • 敏感資訊(openid、訂單號)傳輸全程 HTTPS。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved