编程 接口响应的 Self-Verification:别把"第三方说成功"当成功

2026-08-30 15:18:32 views 5

接口响应的 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 不是不信任,是把“对方说成功”变成“我确认成功”。

推荐文章

程序员茄子在线接单