本文詳細實現微信支付 JSAPI (公眾號/小程序支付) 服務端流程:統一下單獲取 prepay_id、前端 WeixinJSBridge 唤起支付、簽名算法、HTTP 客戶端配置、XML 解析。基於官方 v2 協議 (v3 協議結構類似,簽名方式不同)。
用戶點擊支付
│
▼
前端獲取 openid (靜默授權/小程序登錄)
│
▼
前端傳 openid + 商品信息 → 後端統一下單 API
│
▼
後端調用微信「統一下單」→ 獲取 prepay_id
│
▼
後端生成前端所需支付參數 (appId, timeStamp, nonceStr, package, signType, paySign)
│
▼
前端調用 WeixinJSBridge.invoke('getBrandWCPayRequest', params)
│
▼
用戶輸入密碼/指紋/人臉確認支付
│
▼
微信異步通知後端 (notify_url) → 後端驗證簽名 → 更新訂單狀態
│
▼
前端回調 res.err_msg === 'get_brand_wcpay_request:ok' → 展示成功頁
import java.util.HashMap;
import java.util.Map;
public class UnifiedOrderRequest {
// 必填字段
private String appid;
private String mch_id;
private String nonce_str;
private String body;
private String out_trade_no;
private int total_fee; // 分為單位
private String spbill_create_ip;
private String notify_url;
private String trade_type; // JSAPI / NATIVE / APP
// JSAPI 必填
private String openid;
// NATIVE 必填
private String product_id;
// 選填
private String device_info;
private String detail;
private String attach;
private String fee_type; // CNY
private String time_start;
private String time_expire;
private String goods_tag;
private String limit_pay; // no_credit
private String sign; // 簽名,最后生成
// Builder 靜態內部類
public static class Builder {
// 必填參數構造器
public Builder(String appid, String mch_id, String body,
String out_trade_no, int total_fee,
String spbill_create_ip, String notify_url, String trade_type) {
this.appid = appid; this.mch_id = mch_id; this.body = body;
this.out_trade_no = out_trade_no; this.total_fee = total_fee;
this.spbill_create_ip = spbill_create_ip; this.notify_url = notify_url;
this.trade_type = trade_type;
}
// 選填鏈式設置
public Builder setOpenid(String openid) { this.openid = openid; return this; }
public Builder setProductId(String product_id) { this.product_id = product_id; return this; }
public Builder setDeviceInfo(String device_info) { this.device_info = device_info; return this; }
public Builder setDetail(String detail) { this.detail = detail; return this; }
public Builder setAttach(String attach) { this.attach = attach; return this; }
public Builder setFeeType(String fee_type) { this.fee_type = fee_type; return this; }
public Builder setTimeStart(String time_start) { this.time_start = time_start; return this; }
public Builder setTimeExpire(String time_expire) { this.time_expire = time_expire; return this; }
public Builder setGoodsTag(String goods_tag) { this.goods_tag = goods_tag; return this; }
public Builder setLimitPay(String limit_pay) { this.limit_pay = limit_pay; return this; }
public UnifiedOrderRequest build() {
// 業務校驗
if ("JSAPI".equals(trade_type) && openid == null)
throw new IllegalArgumentException("JSAPI 模式下 openid 必填");
if ("NATIVE".equals(trade_type) && product_id == null)
throw new IllegalArgumentException("NATIVE 模式下 product_id 必填");
UnifiedOrderRequest req = new UnifiedOrderRequest();
// 複製字段...
req.nonce_str = RandomStringUtils.randomAlphanumeric(32);
req.sign = Signature.generateSign(req.toMap()); // 簽名生成
return req;
}
}
// 轉 Map 用於簽名與 XML 序列化
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
// 反射或手動 put 所有非 null 字段
return map;
}
// Getters...
}
public class Signature {
/**
* 微信簽名算法 (v2)
* 1. 參數按 ASCII 碼排序
* 2. 拼接 key=value&key=value...&key=商戶密鑰
* 3. MD5 加密並轉大寫
*/
public static String generateSign(Map<String, Object> params, String merchantKey) {
// 1. 過濾空值與 sign 字段
Map<String, Object> filtered = new TreeMap<>(); // TreeMap 自動按 Key 排序
for (Map.Entry<String, Object> e : params.entrySet()) {
if (e.getValue() != null && !"sign".equals(e.getKey())) {
filtered.put(e.getKey(), e.getValue());
}
}
// 2. 拼接字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Object> e : filtered.entrySet()) {
sb.append(e.getKey()).append('=').append(e.getValue()).append('&');
}
sb.append("key=").append(merchantKey);
// 3. MD5 + 大寫
return MD5.encode(sb.toString()).toUpperCase();
}
// 驗證回調簽名
public static boolean verifySign(Map<String, String> params, String merchantKey) {
String sign = params.remove("sign");
String calculated = generateSign(new HashMap<>(params), merchantKey);
return calculated.equals(sign);
}
}
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
public class WeChatHttpClient {
private static CloseableHttpClient httpClient;
static {
try {
// 加載商戶證書 (PKCS12)
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream fis = new FileInputStream(new File("/path/to/apiclient_cert.p12"));
keyStore.load(fis, "商戶號".toCharArray()); // 密碼默認商戶號
fis.close();
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(keyStore, "商戶號".toCharArray())
.build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext, new String[]{"TLSv1.2"}, null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(10000)
.build();
httpClient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.setDefaultRequestConfig(config)
.build();
} catch (Exception e) {
throw new ExceptionInInitializerError("微信 HTTP 客戶端初始化失敗: " + e.getMessage());
}
}
public static String postXml(String url, String xmlData) {
HttpPost post = new HttpPost(url);
post.setHeader("Content-Type", "application/xml");
post.setEntity(new StringEntity(xmlData, "UTF-8"));
try (var response = httpClient.execute(post)) {
return EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw new RuntimeException("微信 API 調用失敗", e);
}
}
}
@Service
public class WeChatPayService {
@Value("${wechat.appid}") private String appid;
@Value("${wechat.mch_id}") private String mchId;
@Value("${wechat.key}") private String merchantKey;
@Value("${wechat.notify_url}") private String notifyUrl;
private static final String UNIFIED_ORDER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder";
public Map<String, String> unifiedOrder(UnifiedOrderRequest request) {
// 1. 請求對象轉 XML (使用 XStream)
XStream xstream = XStreamUtils.createForWeChat();
String requestXml = xstream.toXML(request);
// 2. 發送請求
String responseXml = WeChatHttpClient.postXml(UNIFIED_ORDER_URL, requestXml);
// 3. 解析響應
Map<String, String> result = XmlParser.parseToMap(responseXml);
// 4. 校驗返回
if (!"SUCCESS".equals(result.get("return_code"))) {
throw new WeChatPayException("通信失敗: " + result.get("return_msg"));
}
if (!"SUCCESS".equals(result.get("result_code"))) {
throw new WeChatPayException("業務失敗: " + result.get("err_code_des"));
}
// 簽名驗證
if (!Signature.verifySign(result, merchantKey)) {
throw new WeChatPayException("返回簽名驗證失敗");
}
return result; // 含 prepay_id
}
/**
* 生成前端 JSAPI 支付參數
*/
public Map<String, String> generateJsApiParams(String prepayId) {
Map<String, String> params = new HashMap<>();
params.put("appId", appid);
params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("nonceStr", RandomStringUtils.randomAlphanumeric(32));
params.put("package", "prepay_id=" + prepayId);
params.put("signType", "MD5");
params.put("paySign", Signature.generateSign(params, merchantKey));
return params;
}
}
function callWeChatPay(payParams) {
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": payParams.appId,
"timeStamp": payParams.timeStamp,
"nonceStr": payParams.nonceStr,
"package": payParams.package, // "prepay_id=xxx"
"signType": payParams.signType, // "MD5"
"paySign": payParams.paySign
}, function(res) {
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 支付成功
window.location.href = "/pay/success?orderNo=" + orderNo;
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
// 用戶取消
alert("支付已取消");
} else {
// 失敗
alert("支付失敗: " + res.err_msg);
}
});
}
}
@RestController
@RequestMapping("/pay/wechat")
public class WeChatNotifyController {
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
try {
// 1. 讀取 XML
String xml = IOUtils.toString(request.getInputStream(), "UTF-8");
Map<String, String> params = XmlParser.parseToMap(xml);
// 2. 簽名驗證
if (!Signature.verifySign(params, merchantKey)) {
return " ";
}
// 3. 業務處理
String outTradeNo = params.get("out_trade_no");
String transactionId = params.get("transaction_id");
String totalFee = params.get("total_fee");
// 更新訂單狀態、發送通知、發貨等...
orderService.handlePaymentSuccess(outTradeNo, transactionId, totalFee);
// 4. 返回成功
return " ";
} catch (Exception e) {
log.error("微信支付回調異常", e);
return " ";
}
}
}
wechat:
appid: wx2421b1c4370ec43b
mch_id: 10000100
key: 192006250b4c09247ec02edce69f6a2d # 商戶密鑰,微信商戶平台設置
notify_url: https://yourdomain.com/pay/wechat/notify
cert_path: /certs/apiclient_cert.p12 # 商戶證書路徑
| 錯誤碼 | 含義 | 解決 |
|---|---|---|
| NOAUTH | 商戶無此接口權限 | 微信商戶平台開通 JSAPI 支付權限 |
| NOTENOUGH | 餘額不足 | 用戶餘額不足,提示換支付方式 |
| ORDERPAID | 訂單已支付 | 幂等處理,勿重複發貨 |
| OUT_TRADE_NO_USED | 商戶訂單號重複 | 生成唯一訂單號 (時間戳+隨機數) |
| SIGNERROR | 簽名錯誤 | 檢查參數過濾、排序、Key、編碼 |
| APPID_NOT_MATCH | appid 與 mch_id 不匹配 | 確認商戶綁定的 appid |
| OPENID_MISMATCH | openid 不屬於此 appid | 重新獲取 openid (靜默授權) |
wechatpay-java微信 JSAPI 支付核心鏈路:獲取 openid → 統一下單獲取 prepay_id → 生成前端簽名參數 → JSBridge 唤起 → 異步通知確認。關鍵難點在於簽名算法正確性、雙向證書配置、異步通知幂等處理。建議使用成熟 SDK (如 wechatpay-java) 降低接入風險。