一、为什么说当面付是转卡码系统的最佳搭档

转卡码系统的核心链路只有一句话:用户扫码付款,系统自动发卡。而"扫码付款"这一步,最适合的支付宝产品就是当面付(Face to Face,F2F)。它专为扫码场景设计,商家调用 alipay.trade.precreate 预下单接口拿到一个二维码字符串,用户用支付宝扫码即付,全程无需 H5 跳转、无需复杂的 JSAPI 授权流程。

和微信 Native 支付、支付宝电脑网站支付相比,当面付优势明显:申请门槛低(个体工商户即可开通)、接入成本低(一个接口打天下)、查询与退款接口齐全。对做源码交易、虚拟卡密生意的站长来说,这是性价比最高的支付方案。我们商城在售的转卡码系统 V3 版就内置了当面付全套对接,本文把底层实现完整拆开讲,你可以照着自建。

二、准备工作:应用创建与密钥配置

在支付宝开放平台创建应用,签约"当面付"产品,然后做两件事:生成应用密钥对,上传应用公钥,换取支付宝公钥。密钥是整套系统的安全地基,务必妥善保管。

# 生成 RSA2 密钥对(2048 位)
openssl genrsa -out app_private_key.pem 2048
openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem

上传应用公钥后,开放平台会返回支付宝公钥,保存为 alipay_public_key.pem。然后安装 Python SDK(推荐 python-alipay-sdk)并初始化客户端:

pip install python-alipay-sdk qrcode
# alipay_client.py - 支付客户端单例
from alipay import AliPay

alipay = AliPay(
    appid="20210031xxxxxxxx",                 # 你的应用 APPID
    app_notify_url="https://pay.example.com/notify/alipay",  # 异步通知地址
    app_private_key_string=open("keys/app_private_key.pem").read(),
    alipay_public_key_string=open("keys/alipay_public_key.pem").read(),
    sign_type="RSA2",                         # 必须 RSA2(SHA256)
    debug=False,                              # 沙箱环境设为 True
)

三、核心流程一:预下单与二维码生成

用户提交订单后,后端调用 alipay.trade.precreate,支付宝返回二维码字符串 qr_code,前端把它渲染成二维码图片展示给用户。关键参数:out_trade_no(商户订单号,全局唯一)、total_amount(金额,单位元)、timeout_express(二维码有效期,建议 15 分钟)。

# 预下单:返回二维码内容
def create_qr_payment(order):
    result = alipay.api_alipay_trade_precreate(
        out_trade_no=order.order_no,
        total_amount=f"{order.amount:.2f}",
        subject=order.product_name,
        timeout_express="15m",        # 15 分钟后二维码失效
    )
    if result.get("code") != "10000": # 10000 = 调用成功
        raise PaymentError(result.get("sub_msg"))
    return result["qr_code"]          # 形如 https://qr.alipay.com/xxx

# 生成二维码图片,转 base64 返回给前端展示
import qrcode, io, base64
img = qrcode.make(qr_code)
buf = io.BytesIO(); img.save(buf, format="PNG")
qr_data_uri = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()

注意:同一订单号不能重复预下单。用户刷新页面时,先查订单状态:已支付就直接跳发货;未支付且二维码已生成就复用原二维码,否则才重新预下单。

四、核心流程二:主动轮询 + 异步通知双通道

支付结果通知有两条通道:主动轮询(前端定时调 alipay.trade.query 查状态)和异步通知(支付宝 POST 到 notify_url)。生产环境两条都要接:轮询保证用户侧体验实时,异步通知保证服务端最终确认,两者互为兜底。

# 主动轮询:前端每 2 秒调一次,最多查 15 分钟
def query_order(order_no):
    result = alipay.api_alipay_trade_query(out_trade_no=order_no)
    return result.get("trade_status")
    # WAIT_BUYER_PAY 等待付款 / TRADE_SUCCESS 支付成功
    # TRADE_CLOSED 超时关闭 / TRADE_FINISHED 交易完成
# 异步通知:Flask 回调入口,验签 + 校验业务字段
@app.post("/notify/alipay")
def alipay_notify():
    data = request.form.to_dict()
    sign = data.pop("sign", "")
    if not alipay.verify(data, sign):      # 1. 验签,防伪造通知
        return "failure"
    if data["app_id"] != APP_ID:           # 2. app_id 必须匹配
        return "failure"
    if data["trade_status"] != "TRADE_SUCCESS":
        return "success"                   # 其他状态直接确认,不处理
    order = get_order(data["out_trade_no"])
    # 3. 金额一致性校验:防止金额被篡改
    if not order or abs(float(data["total_amount"]) - order.amount) > 0.01:
        return "failure"
    deliver_card(order)                    # 4. 幂等发货(见下节)
    return "success"                       # 必须回 success,否则支付宝重试

异步通知有个经典坑:必须返回纯文本 "success",返回其他任何内容支付宝都按失败处理并重试(共 8 次,间隔递增)。另外通知可能重复到达,所以发货逻辑必须幂等。

五、自动发货:回调里发卡密的正确姿势

支付确认后要做的唯一一件事:从库存里原子地取一张卡密,标记已售,返回给用户。这里有两个并发安全点:同一订单不能发两次同一张卡密不能卖两次。用"订单状态机 + 数据库条件更新"解决:

# 幂等发货:订单状态机保证只发一次
def deliver_card(order):
    updated = db.execute(text(
        "UPDATE orders SET status='PAID', paid_at=NOW() "
        "WHERE order_no=:no AND status='PENDING'"), {"no": order.order_no}).rowcount
    if updated == 0:
        return                               # 已处理过,直接返回

    # 原子出库:行锁 + 条件更新,卡密不会重复售出
    card = db.execute(text(
        "SELECT id FROM card_stock WHERE product_id=:pid AND status='UNSOLD' "
        "ORDER BY id LIMIT 1 FOR UPDATE"), {"pid": order.product_id}).fetchone()
    if card is None:
        alert_stock_empty(order.product_id)  # 库存告警,转人工
        return
    db.execute(text(
        "UPDATE card_stock SET status='SOLD', order_no=:no WHERE id=:id"),
        {"no": order.order_no, "id": card.id})
    db.session.commit()
    send_card_to_user(order, card)           # 页面展示 / 短信 / 订阅消息推送

注意 FOR UPDATE 行锁会阻塞并发请求,出库操作必须短平快;订单量特别大时,可以换成"条件 UPDATE 判断受影响行数"的方式,让数据库替你保证唯一性。

六、退款与对账

虚拟商品也有售后。退款调用 alipay.trade.refund,注意 refund_amount 不能大于实付金额,且同一订单可多次部分退款。退款成功后要把对应卡密回滚为 RECYCLED 状态,防止二次售卖引发纠纷:

# 退款并回收卡密
def refund_order(order_no, amount):
    result = alipay.api_alipay_trade_refund(
        out_trade_no=order_no, refund_amount=f"{amount:.2f}")
    if result.get("code") == "10000":
        db.execute(text(
            "UPDATE card_stock SET status='RECYCLED' WHERE order_no=:no"),
            {"no": order_no})
        db.session.commit()

每天凌晨再跑一遍对账:拉取支付宝账单文件,与本地订单逐笔核对金额与状态,差异告警,确保每一分钱都对得上。

七、避坑清单

  • 金额校验不能省:异步通知里的 total_amount 必须与本地订单比对,差 0.01 元都拒绝。
  • 订单号唯一:out_trade_no 用「日期 + 随机数 + 用户ID」生成,绝不裸用自增 ID。
  • 二维码有效期:timeout_express 设 15 分钟,后端轮询超时后主动调 alipay.trade.close 关单。
  • 验签失败先查时间:服务器时间偏差超过 5 分钟会导致验签失败,记得同步 NTP。
  • 沙箱调试:debug=True + 沙箱环境 AppID,用支付宝沙箱 App 扫码测试,别拿真实订单试。
  • 回调要快:notify 里只做验签、落库、发卡,其他重活丢消息队列异步处理。

八、小结

当面付对接转卡码系统,核心就四步:预下单出码 → 轮询 + 通知确认 → 幂等发货 → 退款回收。把并发安全和幂等做扎实,一个稳定赚钱的扫码发卡系统就立起来了。想省去从零开发的成本,可以直接用我们商城的转卡码系统 V3(内置当面付 / 微信 Native 双通道与自动发货)或V2 版;写代码、调接口时配一个Codex Desktop当 AI 结对编程搭档,效率直接翻倍。