微信支付統一下單是 JSAPI/小程序/APP 支付的核心前置步驟。本文基於微信支付 V3 版介面,詳細講解商戶號配置、請求參數構建、簽名算法 (RSA-SHA256)、XML/JSON 轉換、HTTP 客戶端調用、應答解析與錯誤處理,附生產級 Java 代碼結構。
AppID (appid)、商戶號 (mch_id)、APIv3 密鑰 (api_v3_key)、商戶私鑰 (PKCS#8 PEM)、微信支付平台公鑰 (用於驗簽)。openid (靜默授權 snsapi_base)。/v3/pay/transactions/jsapi。prepay_id (預支付交易會話標識)。paySign 返回前端 (小程序 wx.requestPayment / JSAPI WeixinJSBridge)。notify_url → 後端驗簽、更新訂單狀態。
public class UnifiedOrderRequest {
private String appid; // 公眾號/小程序 AppID
private String mchid; // 商戶號
private String description; // 商品描述 (body)
private String outTradeNo; // 商戶訂單號 (唯一)
private String notifyUrl; // 回調地址
private Amount amount; // 金額對象
private Payer payer; // 支付者 openid
// getter/setter 省略
}
public class Amount {
private int total; // 總金額,**分** 為單位
private String currency = "CNY";
// ...
}
public class Payer {
private String openid; // 用戶 openid
}
public class WeChatPaySigner {
private final PrivateKey merchantPrivateKey; // PKCS#8 載入
private final String merchantId; // mch_id
private final String serialNo; // 商戶證書序列號
/**
* 構建待簽名字串:
* HTTP_METHOD\n
* URL_PATH\n
* TIMESTAMP\n
* NONCE_STR\n
* REQUEST_BODY_JSON\n
*/
public String sign(String httpMethod, String urlPath, String requestBody) {
long timestamp = System.currentTimeMillis() / 1000;
String nonce = UUID.randomUUID().toString().replace("-", "");
String message = String.join("\n", httpMethod, urlPath,
String.valueOf(timestamp), nonce, requestBody);
try {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(merchantPrivateKey);
sign.update(message.getBytes(StandardCharsets.UTF_8));
byte[] signature = sign.sign();
return Base64.getEncoder().encodeToString(signature);
} catch (Exception e) {
throw new RuntimeException("簽名失敗", e);
}
}
/** 生成 Authorization 標頭 */
public String buildAuthorization(String httpMethod, String urlPath, String requestBody) {
String signature = sign(httpMethod, urlPath, requestBody);
long timestamp = System.currentTimeMillis() / 1000;
String nonce = UUID.randomUUID().toString().replace("-", "");
return String.format("WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\"," +
"timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"",
merchantId, nonce, timestamp, serialNo, signature);
}
}
public class WeChatPayClient {
private final OkHttpClient httpClient; // 預配置雙向 TLS: 憑證 + 私鑰
private final WeChatPaySigner signer;
private final String baseUrl = "https://api.mch.weixin.qq.com";
public UnifiedOrderResponse unifiedOrder(UnifiedOrderRequest req) {
String urlPath = "/v3/pay/transactions/jsapi";
String requestBody = toJson(req); // Jackson 序列化
String authHeader = signer.buildAuthorization("POST", urlPath, requestBody);
Request request = new Request.Builder()
.url(baseUrl + urlPath)
.addHeader("Authorization", authHeader)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.post(RequestBody.create(requestBody, MediaType.get("application/json")))
.build();
try (Response response = httpClient.newCall(request).execute()) {
String respBody = response.body().string();
if (!response.isSuccessful()) {
throw new WeChatPayException("HTTP " + response.code() + ": " + respBody);
}
// 驗證回應簽名 (略)
return fromJson(respBody, UnifiedOrderResponse.class);
} catch (IOException e) {
throw new WeChatPayException("網路異常", e);
}
}
}
public class UnifiedOrderResponse {
private String prepayId; // 關鍵: 預支付 ID
// ...
}
// 後端返回給前端的支付參數 (小程序範例)
public class JsapiPayParams {
private String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
private String nonceStr = UUID.randomUUID().toString().replace("-", "");
private String packageValue = "prepay_id=" + prepayId;
private String signType = "RSA";
private String paySign; // 二次簽名
// 待簽字串: timeStamp\nnonceStr\npackageValue\n
// 使用同一套簽名工具 signer.signForPay(timeStamp, nonceStr, packageValue)
}
/pay/unifiedorder | /v3/pay/transactions/jsapi (按場景分) |
| 回調驗簽 | MD5 | **RSA-SHA256 + 平台公鑰** |
| 錯誤碼 / 現象 | 原因 | 解決 |
|---|---|---|
SYSTEM_ERROR / 500 | 微信端暫時故障 | 指數退避重試 (冪等鍵 out_trade_no 保證不重複扣款) |
INVALID_REQUEST 簽名驗證失敗 | 待簽字串組裝順序/換行/編碼錯誤、私鑰格式不符 (PKCS#1 vs PKCS#8) | 對照官方示例逐字節核對、用 OpenSSL 驗證私鑰 |
PARAM_ERROR 金額不合法 | 單位不是「分」、或超過限額 | 金額轉換: int totalFen = yuan * 100 |
| 回調收不到 / 重複回調 | 防火牆攔截、回調未在 2s 內返回 SUCCESS | 回調邏輯冪等、異步處理、快速返回 SUCCESS |
| 雙向 TLS 握手失敗 | 證書路徑錯誤、密碼錯誤、未載入平台公鑰 | 用 openssl s_client -connect api.mch.weixin.qq.com:443 -cert ... -key ... 測試 |
out_trade_no 業務唯一,重試不變,防重複扣款。SUCCESS,全程無阻塞。openid、prepay_id、簽名值脫敏記錄。wechatpay-java SDK (封裝簽名/雙向 TLS/重試),減少造輪子風險。微信支付 V3 統一下單雖涉及雙向 TLS、RSA 簽名、JSON 協議等較複雜環節,但只要掌握 **「請求體 → 待簽字串 → 私鑰簽名 → Authorization 標頭 → HTTPS POST」** 核心鏈路,配合官方 SDK 與完善的異常處理,即可穩健接入生產環境。