微信官方文檔存在「JS-SDK 支付」與「商戶平台 H5 支付」兩套並存介面,參數命名不一致(timestamp vs timeStamp、package vs prepay_id),且無官方 Java Demo。本文記錄實測可行的 JSAPI 支付(公眾號內網頁支付) 全流程,適用於服務號/訂閱號(已認證)場景。
openid(靜默授權/網頁授權)prepay_idWeixinJSBridge 喚起支付notify_url)@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"));
}
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();
}
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();
}
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);
<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 為鍵防重複入帳;