Python 接入

本页是 Python(FastAPI)商户的完整接入教程,从零到收款五步,代码都可以整段复制。 读完本页即可完成接入,其他页面作为字典备查。

第一步:确认手里有这些

东西来源
wantu_pay.py顽兔交付的单文件 SDK,复制进你的项目(仅 Python 标准库,3.9+)
fastapi_demo.py顽兔交付的完整参考实现,前后端都有,可先跑通再照抄
merchant_id / api_secret顽兔发放的商户凭据
环境地址(baseUrl)联调、生产各一个

api_secret 放环境变量或配置中心,不要写进代码,不要提交到代码仓库。

第二步:初始化

import os
from wantu_pay import WantuPayClient

pay = WantuPayClient(
    base_url=os.environ["WANTU_PAY_BASE_URL"],
    merchant_id=os.environ["WANTU_MERCHANT_ID"],
    api_secret=os.environ["WANTU_API_SECRET"],
)

第三步:服务端三个路由

充值功能在服务端就是三个路由:下单、收回调、给前端查状态。以下可直接粘贴:

import time
from fastapi import FastAPI, Request, Response
from wantu_pay import WantuPayApiError, WantuWebhookVerifyError

app = FastAPI()

# 套餐表:金额永远查服务端的表,前端只传套餐名
PLANS = {"small": ("0.01", "积分充值·体验"), "big": ("100.00", "积分充值100元")}


@app.post("/recharge")
async def create_recharge(request: Request):
    body = await request.json()
    plan = PLANS.get(body.get("plan"))
    if plan is None:
        return Response(status_code=400, content="未知套餐")
    amount, subject = plan
    out_trade_no = f"R{int(time.time() * 1000)}"   # 服务端生成,保证唯一;建议换成库表序列
    order = pay.create_order(
        out_trade_no=out_trade_no,
        amount=amount,
        subject=subject,
        pay_mode="qrcode",                          # 充值页内嵌二维码;整页跳转用 "redirect"
        attach=str(body.get("uid", "")),
    )
    # 此时应把 out_trade_no、金额、用户、状态=待支付 落到你的订单表
    return {"out_trade_no": out_trade_no, "pay_url": order["pay_url"]}


@app.post("/wantu/webhook")
async def wantu_webhook(request: Request):
    raw_body = await request.body()                 # 必须是原文,不要先 request.json()
    try:
        event = pay.verify_webhook(request.headers, raw_body)
    except WantuWebhookVerifyError:
        return Response(status_code=400)            # 验签失败:不处理,网关会重试

    if event["event_type"] == "order.paid":
        data = event["data"]
        # 入账三件套:查到本地订单 → 金额一致 → 事务内幂等入账
        # UPDATE orders SET status='paid' WHERE out_trade_no=%s AND status='created'
        # 更新行数为 1 才执行加积分;为 0 说明是重复回调,直接返回 200
        ...
    return {"ok": True}


@app.get("/orders/{out_trade_no}")
async def order_status(out_trade_no: str):
    order = pay.query_order(out_trade_no=out_trade_no)
    return {"status": order["status"]}
Warning

webhook 路由的第一行必须是 await request.body()原文。签名针对请求体原始字节, 任何先做 JSON 解析的写法都会导致验签失败。这是接入中最常踩的一个坑。

第四步:前端充值页

前端要做的只有两件事:展示 pay_url、轮询自家后端的订单状态。可直接用的最小实现:

<div id="payArea"></div>
<div id="status"></div>
<script>
async function recharge() {
  const res = await fetch('/recharge', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ plan: 'small', uid: 42 }),
  })
  const order = await res.json()

  // 内嵌二维码(用户手机支付宝扫码,不离开你的页面)
  document.getElementById('payArea').innerHTML =
    '<iframe src="' + order.pay_url + '" width="240" height="240" frameborder="0"></iframe>'

  // 轮询自家后端,到账后刷新页面状态
  const timer = setInterval(async () => {
    const s = await (await fetch('/orders/' + order.out_trade_no)).json()
    if (s.status === 'paid') { clearInterval(timer); document.getElementById('status').textContent = '已到账' }
    if (s.status === 'closed') { clearInterval(timer); document.getElementById('status').textContent = '已超时,请重新下单' }
  }, 3000)
}
</script>

要点:支付完成时 PC 页面不会收到任何事件,靠的就是这个轮询;到账的依据是你后端的 订单状态(由 webhook 驱动),不是支付页的跳转。

可选:银联商务聚合码(一张码微信/支付宝/云闪付都能扫)

顽兔侧开通银商渠道后,下单多传一个 channel 就能出聚合码,回调、查单、退款完全不变:

order = pay.create_order(
    out_trade_no=out_trade_no,
    amount=amount,
    subject=subject,
    channel="chinaums",     # 银联商务聚合码;不传就是支付宝
    pay_mode="qrcode",      # PC 出码场景用 qrcode
    attach=str(body.get("uid", "")),
)

前端有一处不同:这条渠道的 pay_url二维码内容(不能 iframe、浏览器打不开), 要用二维码库渲染成码图:

<script src="https://cdn.bootcdn.net/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
  // 替换第四步里的 iframe 那行:
  new QRCode(document.getElementById('payArea'), { text: order.pay_url, width: 220, height: 220 })
</script>

按下单返回的 channel 字段分支两种渲染即可(完整写法见 fastapi_demo.py 的演示页, 三个按钮都有)。

页面在微信里打开(公众号菜单、聊天链接)时展示二维码是没法付的——改传 pay_mode="redirect"(可带 return_url),拿到的 pay_url 直接整页跳转过去, 银商收银台会唤起微信支付,你不需要有自己的公众号。 参数细节与微信内完整示例见发起支付

第五步:联调核对

联调与上线 的用例走,每步预期如下:

动作预期
/recharge返回 pay_url,服务端日志无异常
打开支付页,用联调买家账号付款 0.01支付宝页面显示付款成功
看你的服务日志出现 webhook 入账日志(几秒内)
/orders/{单号}statuspaid
重启服务后网关重推回调不会重复入账(幂等生效)
pay.create_refund(...)返回 succeeded,随后收到 refund.succeeded 回调

本地开发收不到回调时:回调地址需公网可达(内网穿透),排查前先用查单接口确认支付 本身是否成功。遇到任何报错,直接查常见问题排查,按症状对号入座。