Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Webhook 异步通知

重要说明

  • 异步通知时,商户系统需正确响应参数,返回字符串 “ok”,否则视为未成功。
  • 交易完成后,系统会根据商户订阅的 webhook 地址,通过 POST,将结果以 application/json 格式发送给商户。
  • 请务必验证签名,防止数据泄漏和伪造通知。

注意事项

  1. 幂等性
    同一通知可能多次发送,商户系统需能正确处理重复通知。建议做法:收到通知后,先检查业务数据状态,仅对未处理的通知进行处理,已处理直接返回 200 和 "OK"。状态检查和数据操作前须加锁,避免并发导致状态混乱。

  2. 时效性
    极少数情况下通知可能因网络等原因未送达,请务必通过主动查询接口补偿。建议根据实际业务,合理设置补偿查单的时机,如支付1小时后未收到通知可主动查单。

  3. 防抵赖
    商户侧须对通知内容做签名校验(签名公钥见商户后台),并校验通知内容与本地数据一致,防止伪造通知导致资金风险。


通知类型

类型说明
CardApply卡状态变更(开卡、注销)
CardOperate卡充值、卡充退
Authorization卡交易
Inbound入账

CardApply

开卡结果

字段说明
partner_order_id商户请求 ID
status交易结果 Success/Failure
card_id卡 ID
card_status卡状态 Active/Failure
fail_reason失败原因
card_number卡号
available_balance可用余额
cvvCVV
expiry过期时间
card_level卡等级
merchant_fee手续费,MerchantFee
primary_card_id主卡ID(仅子卡有)
total_auth_limit子卡限额

注销结果

字段说明
partner_order_id商户请求 ID
transaction_id交易 ID
status交易结果 Success/Failure
card_id卡 ID
card_status卡状态 Closed 已注销
fail_reason失败原因
merchant_fee手续费,MerchantFee

CardOperate

字段说明
partner_order_id商户请求 ID
transaction_id交易 ID
status交易结果 Success/Failure
card_id卡 ID
operate_type交易类型:card_in 卡充值,card_out 卡充退
amount发生金额
currency发生币种
merchant_fee手续费 MerchantFee

Authorization

参见交易查询


MerchantFee

字段说明
fee_currency当前手续费币种
total_fee_amount总手续费金额
fee_detail手续费明细 List<FeeDetail>

FeeDetail

字段说明
fee_amount手续费金额
fee_type手续费类型

Inbound

| 字段说明 | 参见 InboundDetail |


签名验证参考

Go 示例

import (
    "crypto/rsa"
    "crypto/x509"
    "crypto"
    "encoding/pem"
    "encoding/base64"
    "net/http"
    "io/ioutil"
)

// 验证签名
func verifySign(content, signBase64, pubKeyPEM string) bool {
    block, _ := pem.Decode([]byte(pubKeyPEM))
    if block == nil {
        return false
    }
    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return false
    }
    rsaPub := pub.(*rsa.PublicKey)
    sig, _ := base64.StdEncoding.DecodeString(signBase64)
    h := crypto.SHA256.New()
    h.Write([]byte(content))
    return rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, h.Sum(nil), sig) == nil
}

// Webhook Handler 示例
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    appId := "1569641270953589506"
    publicKey := `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsIHIQAp4TToRZuF+/g/fr4xxJg/hGxyZGwp66l1LSS7sfsx/EnaCY0mYzEBZZ6qqEShdrtlowJa2SudemqeWbbH75vkcwvwL7cMX58pXNPny4vACaKSnIdz1wwue/tUWmubQdhb7wwJDDKB+TQoyhrZbNsW9tsASCu8WjdmxqH0i0It/FjrMw7R4pv0jrZfMmoZs0mhxnrF5HnPzRW+kFgKwDGIRzhsy32iMTk7lbOkSrBK923TuWy/mb5h3Vzw1c/PVtl9udFG2SbQsl848jXQx5TUhyh0XIVLVMRu/EAiq54bIsh/YNWx75nfCkH+h7T6d0L8qxIdQB/qdt/RZQwIDAQAB
-----END PUBLIC KEY-----`

    sign := r.Header.Get("sign")
    timestamp := r.Header.Get("x-timestamp")
    body, _ := ioutil.ReadAll(r.Body)
    content := appId + timestamp + string(body)

    if !verifySign(content, sign, publicKey) {
        // 签名有误,记录日志
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("sign error"))
        return
    }
    w.Write([]byte("ok"))
}

Rust 示例

use base64::{Engine as _, engine::general_purpose};
use ring::signature::{UnparsedPublicKey, RSA_PKCS1_2048_8192_SHA256};

fn verify_sign(content: &str, sign_b64: &str, public_key_der: &[u8]) -> bool {
    let signature = match general_purpose::STANDARD.decode(sign_b64) {
        Ok(sig) => sig,
        Err(_) => return false,
    };
    let pub_key = UnparsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, public_key_der);
    pub_key.verify(content.as_bytes(), &signature).is_ok()
}

fn main() {
    let app_id = "1569641270953589506";
    let timestamp = "1716350279000";
    let body = r#"{"partner_order_id":"xxxx","status":"Success"}"#;
    let content = format!("{}{}{}", app_id, timestamp, body);

    let public_key_pem = r#"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsIHIQAp4TToRZuF+/g/fr4xxJg/hGxyZGwp66l1LSS7sfsx/EnaCY0mYzEBZZ6qqEShdrtlowJa2SudemqeWbbH75vkcwvwL7cMX58pXNPny4vACaKSnIdz1wwue/tUWmubQdhb7wwJDDKB+TQoyhrZbNsW9tsASCu8WjdmxqH0i0It/FjrMw7R4pv0jrZfMmoZs0mhxnrF5HnPzRW+kFgKwDGIRzhsy32iMTk7lbOkSrBK923TuWy/mb5h3Vzw1c/PVtl9udFG2SbQsl848jXQx5TUhyh0XIVLVMRu/EAiq54bIsh/YNWx75nfCkH+h7T6d0L8qxIdQB/qdt/RZQwIDAQAB
-----END PUBLIC KEY-----"#;

    let public_key_der = pem::parse(public_key_pem)
        .expect("parse PEM")
        .contents();

    let sign_base64 = "xxxx";

    if verify_sign(&content, sign_base64, &public_key_der) {
        println!("签名验证通过");
    } else {
        println!("签名验证失败");
    }
}