在源码商城这类自动发卡站点买东西,体验是这样的:扫码 → 付款 → 两三秒后手机弹出卡密,全程没有人工参与。很多开发者好奇:支付成功之后,系统到底做了什么,能在几百毫秒内把卡密准确送到买家手里?这篇文章把这条"自动发卡流水线"从支付回调到买家通知完整拆开,每一环都给出可运行的 Python 代码。

整条流水线可以压缩成一句话:验签 → 幂等 → 锁卡 → 发货 → 通知。听起来简单,但每一步都有坑:回调不验签会被白嫖,不幂等会重复发货,扣库存不原子会超卖,发货失败不补偿会吞单。下面逐个击破。

一、整条流水线:从"已支付"到"已发货"的八个环节

把一次自动发货放大看,一共八个环节:

  1. 支付平台(支付宝/微信)向 notify_url 发起回调,携带订单号与支付结果;
  2. 服务端验签,确认回调确实来自支付平台;
  3. 查订单状态,已发货的直接返回 success,幂等短路;
  4. Redis 加幂等锁,防止并发回调同时进入发货逻辑;
  5. 事务内"领取"一张状态为待售的卡密,并把订单标记为已支付;
  6. 提交事务,卡密正式归属这笔订单;
  7. 异步通知买家(站内信 / 短信 / 邮件);
  8. 返回 success 给支付平台,关闭本次回调。

其中 2、3、4、5 是核心,任何一环做错,轻则重复发货亏卡密,重则被伪造回调拖垮库存。下面逐个展开。

二、验签:回调的第一道门,不验签等于开门揖盗

支付宝的回调用 RSA2 签名(SHA256withRSA),验签代码很短:

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64

# 支付宝公钥:商户后台可下载,注意不是应用私钥
with open("alipay_public_key.pem", "rb") as f:
    pub = serialization.load_pem_public_key(f.read())

# 把回调参数按 key 排序拼接(剔除 sign、sign_type),再验 RSA2 签名
def verify_alipay(params, sign):
    content = "&".join(
        f"{k}={params[k]}" for k in sorted(params)
        if k not in ("sign", "sign_type"))
    try:
        pub.verify(base64.b64decode(sign), content.encode(),
                   padding.PKCS1v15(), hashes.SHA256())
        return True
    except Exception:
        return False

微信 V3 的回调是 AES-256-GCM 加密 + 平台证书签名双重防护,解密验签逻辑之前的文章讲过,这里不重复。记住结论:任何回调先验签再谈业务,验签失败的请求直接丢弃并记日志。我们的转卡码系统里,验签失败的 IP 会进限流黑名单,连续失败直接封禁。

三、幂等:同一笔回调可能来三次

支付平台的回调是"不保证一次"的:网络抖动会重试,通常 15 分钟、4 小时、次日还会各补一次。如果每次回调都执行发货,买家会收到三张卡密。幂等靠两道锁:数据库订单状态 + Redis 分布式锁

import redis
r = redis.Redis.from_url("redis://127.0.0.1:6379/0")

# 1. Redis 幂等锁:同一订单同时只允许一个发货任务
lock_key = f"deliver:lock:{order_no}"
if not r.set(lock_key, "1", nx=True, ex=60):
    # 拿不到锁说明已有任务在处理,直接返回成功
    return "success"

try:
    # 2. 数据库状态检查:已发货的直接短路,幂等返回
    order = db.query_one(
        "SELECT status FROM orders WHERE order_no=%s", order_no)
    if order["status"] != "pending":
        return "success"
    # 3. 只有 status='pending' 才继续走发货逻辑
    deliver_card(order_no)
finally:
    r.delete(lock_key)

锁的过期时间要覆盖整个发货事务(30~60 秒足够),宁可锁过期后重试,也不能让两个任务并发发货。

四、原子领取卡密:一个 UPDATE 解决超卖

发货的核心动作是"从卡密池里领一张"。如果先 SELECT 再 UPDATE,两个并发请求可能领到同一张卡。正确姿势是让数据库完成原子操作:

# PostgreSQL:子查询锁定目标行,一次更新且只更新一行
card = db.query_one("""
    UPDATE cards
    SET status='locked', order_no=%s, locked_at=NOW()
    WHERE id = (
        SELECT id FROM cards
        WHERE status='pending'
        ORDER BY id ASC
        LIMIT 1
    )
    RETURNING id, card_no
""", order_no)

并发下数据库的行锁保证只有一条 UPDATE 成功,天然防超卖。拿到卡密后,在同一事务里把订单状态改成 paid

db.execute("""
    UPDATE orders SET status='paid', card_id=%s, paid_at=NOW()
    WHERE order_no=%s AND status='pending'
""", card["id"], order_no)

这里 WHERE status='pending' 是第二道幂等防线:即使 Redis 锁意外失效,数据库层面也不会重复发货。事务提交后,卡密才真正属于这笔订单。

五、发货失败怎么办:本地消息表 + 定时补偿

事务提交成功但通知服务挂了,买家就收不到卡密——这是最坑的"吞单"。业界标准解法是本地消息表(outbox):发货动作落库时顺带写一条待发送消息,由后台任务定时扫描补偿,保证最终一致。

# 发货事务里顺带写一条消息记录(同库同事务,保证不丢)
db.execute("""
    INSERT INTO outbox (order_no, card_no, status, retry_count)
    VALUES (%s, %s, 'pending', 0)
""", order_no, card["card_no"])

# 补偿任务:每 30 秒扫一次,未发出的重试,超过 5 次转人工
def compensate():
    for msg in db.query(
            "SELECT * FROM outbox WHERE status='pending' AND retry_count<5"):
        try:
            notify_buyer(msg)   # 发站内信/短信/邮件
            db.execute("UPDATE outbox SET status='done' WHERE id=%s", msg["id"])
        except Exception:
            db.execute(
                "UPDATE outbox SET retry_count=retry_count+1 WHERE id=%s",
                msg["id"])

消息表和订单在同一个数据库事务里写入,要么都成功要么都回滚,不会出现"钱扣了卡没发";定时补偿兜底,极端情况还能人工介入补发。

六、通知买家:异步、多渠道、不阻塞主流程

卡密发货后要尽快通知买家。注意通知绝不能放在回调的同步链路里:短信服务超时 3 秒,回调就晚确认 3 秒,支付平台会认为你没处理成功而反复重试。正确做法是异步发送:

from concurrent.futures import ThreadPoolExecutor
notify_pool = ThreadPoolExecutor(max_workers=8)

def deliver_card(order_no):
    # ... 上面的验签、幂等、锁卡发货代码 ...
    # 异步通知,主流程立刻返回 success
    notify_pool.submit(notify_buyer, order_no, card["card_no"])

# 通知渠道按优先级降级:站内信 → 短信 → 邮件
def notify_buyer(order_no, card_no):
    for channel in (send_inbox, send_sms, send_email):
        if channel(order_no, card_no):
            break

七、端到端延迟:那 500ms 花在哪了

流水线跑通后,用日志把每个环节的耗时打出来,看看时间都花在哪:

t0 = time.time()
# 验签 ~1ms → 幂等检查 ~1ms → 锁卡事务 ~5ms → 通知异步不阻塞
logger.info("deliver pipeline: verify=%.1fms lock=%.1fms "
            "tx=%.1fms total=%.1fms",
            (t1-t0)*1000, (t2-t1)*1000, (t3-t2)*1000, (t3-t0)*1000)

实测下来,从支付平台回调到订单标记已发货,通常只要 10~30ms;剩下的时间花在通知渠道和支付平台自身的回调延迟上。如果发现耗时异常,优先查数据库慢查询和 Redis 连接池。

八、写在最后

自动发卡的本质,就是一条验签 → 幂等 → 锁卡 → 发货 → 通知的可靠流水线。把回调当不可信输入、把发货当必须恰好一次、把通知当可以异步,这三条原则想透了,转卡码系统就立住了一半。如果你不想从零实现这些细节,我们商城的转卡码系统 V3 已内置支付宝/微信回调验签、Redis 幂等锁、原子锁卡发货、本地消息表补偿和异步通知,部署即可商用;写这套逻辑时配合Codex Desktop 让 AI 帮你审查并发边界,能少踩一半的坑。

浏览源码商城 →