接口响应的 Self-Verification:别把“第三方说成功”当成功
做过支付、短信、风控、物流这类外部接口对接的,几乎都踩过同一个坑:第三方返回了“成功”,但业务却出错了。
微信回调返回 success,不代表钱到了账;短信平台返回“已下发”,不代表对方收到了;风控返回“通过”,不代表请求真的通过。这些接口的共同点是:返回结果只代表“对方收到并处理了你的请求”,不代表“你想要的结果真的发生了”。
所以成熟的对接代码里,都有一层 Self-Verification——自己再验一遍,而不是把第三方返回直接当真。
一、验签:先证明这条响应是真的
回调/响应体本身可能被伪造、重放、被中间人篡改。第一道校验永远是验签。
支付类平台回调通常带 sign 字段,用商户密钥做 HMAC/MD5 签名。验签要自己实现,不能只比对字段值。
以微信支付 v3 为例,回调头里有 Wechatpay-Signature,用平台证书公钥做 RSA 验签,再验平台证书本身(防证书被换)。
// 伪代码,仅示意核心逻辑
func verifyWechatpaySignature(r *http.Request, body []byte, platformPublicKey *rsa.PublicKey) bool {
signature := r.Header.Get("Wechatpay-Signature")
timestamp := r.Header.Get("Wechatpay-Timestamp")
nonce := r.Header.Get("Wechatpay-Nonce")
message := timestamp + "\n" + nonce + "\n" + string(body) + "\n"
decodedSig, _ := base64.StdEncoding.DecodeString(signature)
hashed := sha256.Sum256([]byte(message))
err := rsa.VerifyPKCS1v15(platformPublicKey, crypto.SHA256, hashed[:], decodedSig)
return err == nil
}
注意:还要校验微信支付平台证书本身,防止中间人换证书。这一步不要省。
二、幂等:同一结果只能生效一次
第三方回调可能重发(网络抖动、对方超时重试),所以接口必须幂等。用幂等键(订单号 + 事件类型)判断是否已处理过。
处理前先查库,已处理直接返回成功,别重复加积分、别重复扣款。
func handleCallback(db *sql.DB, orderID, eventType string) error {
key := orderID + ":" + eventType
// 使用数据库唯一约束做幂等,或者 Redis SETNX 也行
var affected int64
err := db.QueryRow(
"INSERT INTO processed_events(event_key, created_at) VALUES($1, now()) ON CONFLICT DO NOTHING RETURNING 1",
key,
).Scan(&affected)
if err == sql.ErrNoRows {
// 已处理过,直接返回成功
return nil
}
if err != nil {
return err
}
// 继续实际的业务处理
return applyBizLogic(orderID, eventType)
}
三、二次查询:成功之后再做一次确认
这是 Self-Verification 的核心。回调返回成功,别急着落库业务,先反过来向第三方发起一次“主动查询”接口,用订单号确认最终状态。
支付场景很典型:回调说“已支付”,你再去查一下订单状态接口,确认订单状态确实是 SUCCESS 才更新本地状态。因为回调是异步推送,可能和主动查询存在时序差。
func selfVerifyBeforeUpdate(orderID string) error {
status, err := queryPaymentOrder(orderID)
if err != nil {
return fmt.Errorf("query payment order failed: %w", err)
}
if status != "SUCCESS" {
return fmt.Errorf("callback says success, but query status is %s", status)
}
// 状态一致,才更新本地订单
return updateLocalOrderToPaid(orderID)
}
如果查询结果和回调不一致,不要直接拒绝或结束。间隔几秒重查几次,必要时走人工告警。
四、对账:定时任务兜底
最稳的 Self-Verification 是对账。每天跑一次账单下载,把本地流水和第三方账单逐笔核对,能发现漏单、多单、金额不符。
对账的粒度要自己定义:是只对“成功”单,还是连“退款”也一起对。
func dailyReconcile(date string) error {
bills, err := downloadThirdPartyBills(date)
if err != nil {
return err
}
for _, bill := range bills {
local := loadLocalOrder(bill.OrderID)
if local == nil || !isAmountEqual(local.Amount, bill.Amount) {
reportMismatch(bill.OrderID, "local", local, "bill", bill)
}
}
return nil
}
对账任务要分批处理,控制并发,避免大促期间把主库读垮。
什么情况别这么做
- 实时性要求极高的场景:二次查询的延迟可能接受不了,可以只做验签+幂等,把二次查询改成异步。
- 第三方没有提供查询/对账接口:那你就只能信回调,但要在文档里明确这个风险。
- 别对每个回调都做完整对账:量大的时候对账要分批、限流,别把对账任务做成拖垮主库的事。
一句话总结:Self-Verification 不是不信任,是把“对方说成功”变成“我确认成功”。