prepay_id。prepay_id。notify_url) → 服務端驗證簽名、更新訂單狀態。
org.apache.httpcomponents
httpclient
4.5.14
org.json
json
20231013
public class WeChatPayService {
private static final String APPID = "wx0378bf81abfe3d26";
private static final String MCH_ID = "1252196606";
private static final String API_KEY = "b8b9c2bbe92d57cc38fde49745056167"; // 商戶平台設置的 Key
private static final String NOTIFY_URL = "https://your.domain.com/pay/notify";
private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
public JSONObject createAppOrder(String outTradeNo, String body, double amount) {
String nonceStr = RandomStringUtils.randomAlphanumeric(32);
String totalFee = String.valueOf((long) (amount * 100)); // 單位:分
// 1. 組裝簽名參數 (按 ASCII 排序)
Map params = new TreeMap<>();
params.put("appid", APPID);
params.put("body", body);
params.put("mch_id", MCH_ID);
params.put("nonce_str", nonceStr);
params.put("notify_url", NOTIFY_URL);
params.put("out_trade_no", outTradeNo);
params.put("spbill_create_ip", "127.0.0.1"); // 實際應獲取客戶端真實 IP
params.put("total_fee", totalFee);
params.put("trade_type", "APP");
String sign = generateSign(params, API_KEY);
params.put("sign", sign);
// 2. 轉 XML 請求體
String xml = mapToXml(params);
// 3. 發送 HTTPS POST
String responseXml = HttpClientUtil.postXml(UNIFIED_ORDER_URL, xml);
// 4. 解析響應
Map result = xmlToMap(responseXml);
if (!"SUCCESS".equals(result.get("return_code")) || !"SUCCESS".equals(result.get("result_code"))) {
throw new RuntimeException("下單失敗: " + result.get("err_code_des"));
}
// 5. 二次簽名返回給 App
String prepayId = result.get("prepay_id");
Map appParams = new TreeMap<>();
appParams.put("appid", APPID);
appParams.put("partnerid", MCH_ID);
appParams.put("prepayid", prepayId);
appParams.put("package", "Sign=WXPay");
appParams.put("noncestr", nonceStr);
appParams.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
String appSign = generateSign(appParams, API_KEY);
appParams.put("sign", appSign);
return new JSONObject(appParams);
}
// MD5 簽名 (按 key 排序拼接 key=value&...&key=API_KEY)
private String generateSign(Map params, String apiKey) {
StringBuilder sb = new StringBuilder();
params.forEach((k, v) -> { if (v != null && !v.isEmpty()) sb.append(k).append('=').append(v).append('&'); });
sb.append("key=").append(apiKey);
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
}
// mapToXml / xmlToMap / HttpClientUtil 省略,建議用成熟 XML 工具 (dom4j/jaxb)
}
@PostMapping("/pay/notify")
public String notify(@RequestBody String xmlBody) {
Map params = XmlUtil.xmlToMap(xmlBody);
// 1. 驗證 return_code & result_code 為 SUCCESS
if (!"SUCCESS".equals(params.get("return_code")) || !"SUCCESS".equals(params.get("result_code"))) {
return XmlUtil.mapToXml(Map.of("return_code", "FAIL", "return_msg", "業務失敗"));
}
// 2. 驗證簽名
String sign = params.remove("sign");
if (!sign.equals(WeChatPayService.generateSign(params, API_KEY))) {
return XmlUtil.mapToXml(Map.of("return_code", "FAIL", "return_msg", "簽名驗證失敗"));
}
// 3. 業務幂等性處理 (防重複通知)
String outTradeNo = params.get("out_trade_no");
if (orderService.isPaid(outTradeNo)) {
return XmlUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
}
// 4. 更新訂單狀態、發送 MQ 等
orderService.markPaid(outTradeNo, params.get("transaction_id"));
return XmlUtil.mapToXml(Map.of("return_code", "SUCCESS", "return_msg", "OK"));
}
sign_type=HMAC-SHA256),新接入優先使用。out_trade_no 或 transaction_id 去重。total_fee 與訂單金額一致。wechat-pay-java-sdk 封裝了簽名、加密、證書管理,強烈建議生產環境直接用 SDK。微信 App 支付核心在於「統一下單 → 二次簽名 → 回調驗證」三步。掌握 XML 組裝、MD5/HMAC-SHA256 簽名、幂等性處理,配合官方 SDK,即可構建安全可靠的支付服務端。