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

Java 微信支付 JSAPI 接口完整實現流程

     編輯:Java 相關知識

本文詳細實現微信支付 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' → 展示成功頁

核心實體:統一下單請求數據 (Builder 模式)


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...
}

簽名算法 (微信 v2: MD5)


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);
    }
}

HTTP 客戶端 (支援雙向證書)


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;
    }
}

前端調用代碼 (微信 JSBridge)


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);
            }
        });
    }
}

異步通知處理 (notify_url)


@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 "";
        }
    }
}

關鍵配置 (application.yml)


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_MATCHappid 與 mch_id 不匹配確認商戶綁定的 appid
OPENID_MISMATCHopenid 不屬於此 appid重新獲取 openid (靜默授權)

微信支付 v3 遷移提示

  • 簽名算法:MD5 → RSA-SHA256 (需私鑰簽名、公鑰驗證)
  • 通信:HTTP + XML → HTTPS + JSON
  • 證書:PKCS12 → 平台證書下載 API 定期輪換
  • 推薦使用官方 SDK:wechatpay-java

總結

微信 JSAPI 支付核心鏈路:獲取 openid → 統一下單獲取 prepay_id → 生成前端簽名參數 → JSBridge 唤起 → 異步通知確認。關鍵難點在於簽名算法正確性、雙向證書配置、異步通知幂等處理。建議使用成熟 SDK (如 wechatpay-java) 降低接入風險。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved