- 异步通知时,商户系统需正确响应参数,返回字符串 “ok”,否则视为未成功。
- 交易完成后,系统会根据商户订阅的 webhook 地址,通过
POST,将结果以 application/json 格式发送给商户。
- 请务必验证签名,防止数据泄漏和伪造通知。
-
幂等性
同一通知可能多次发送,商户系统需能正确处理重复通知。建议做法:收到通知后,先检查业务数据状态,仅对未处理的通知进行处理,已处理直接返回 200 和 "OK"。状态检查和数据操作前须加锁,避免并发导致状态混乱。
-
时效性
极少数情况下通知可能因网络等原因未送达,请务必通过主动查询接口补偿。建议根据实际业务,合理设置补偿查单的时机,如支付1小时后未收到通知可主动查单。
-
防抵赖
商户侧须对通知内容做签名校验(签名公钥见商户后台),并校验通知内容与本地数据一致,防止伪造通知导致资金风险。
| 类型 | 说明 |
| CardApply | 卡状态变更(开卡、注销) |
| CardOperate | 卡充值、卡充退 |
| Authorization | 卡交易 |
| Inbound | 入账 |
| 字段 | 说明 |
| partner_order_id | 商户请求 ID |
| status | 交易结果 Success/Failure |
| card_id | 卡 ID |
| card_status | 卡状态 Active/Failure |
| fail_reason | 失败原因 |
| card_number | 卡号 |
| available_balance | 可用余额 |
| cvv | CVV |
| 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 |
| 字段 | 说明 |
| partner_order_id | 商户请求 ID |
| transaction_id | 交易 ID |
| status | 交易结果 Success/Failure |
| card_id | 卡 ID |
| operate_type | 交易类型:card_in 卡充值,card_out 卡充退 |
| amount | 发生金额 |
| currency | 发生币种 |
| merchant_fee | 手续费 MerchantFee |
参见交易查询
| 字段 | 说明 |
| fee_currency | 当前手续费币种 |
| total_fee_amount | 总手续费金额 |
| fee_detail | 手续费明细 List<FeeDetail> |
| 字段 | 说明 |
| fee_amount | 手续费金额 |
| fee_type | 手续费类型 |
| 字段说明 | 参见 InboundDetail |
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"))
}
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!("签名验证失败");
}
}