一、JSAPI 支付是什么?为什么源码交易场景都在用

微信支付 JSAPI 支付(公众号支付)是指用户在微信内置浏览器中,通过公众号菜单、图文消息或 H5 页面直接拉起微信收银台完成付款的支付方式。用户不用复制链接、不用掏出手机扫码,点击即可完成支付,转化率明显高于 Native 扫码支付,是源码交易、转卡码发卡这类"虚拟商品即时交付"业务的首选收款方式。

它的完整链路是:网页授权获取 openid → 统一下单 → 前端拉起收银台 → 异步通知 → 验签解密 → 幂等自动发货。相比 Native 支付,JSAPI 多出"网页授权"和"前端拉起"两步,这也是新手最容易卡住的地方。本文用微信支付 V3 接口 + Python/JS 把每一步都跑通。

注意:JSAPI 支付要求公众号为已认证的服务号,且商户号需要与公众号完成绑定授权。订阅号无法使用 JSAPI 支付,这是第一个常见的坑。

二、准备工作:密钥、证书与域名配置

开始写代码前,先把以下配置准备好,全部在微信商户平台(pay.weixin.qq.com)和公众号后台获取:

  • APPID:公众号的 AppID(wx 开头),不是小程序 AppID
  • mchid:微信支付商户号
  • APIv3 密钥:32 位随机字符串,用于回调报文解密,务必妥善保存
  • 商户 API 证书:apiclient_cert.pem / apiclient_key.pem,用于请求签名
  • 网页授权域名:公众号后台「设置与开发 → 接口权限 → 网页授权」中配置,需与业务域名一致

另外,微信支付平台证书(用于验签)建议开启自动更新,避免证书到期后回调验签全部失败。推荐直接使用官方 wechatpay-python SDK,证书轮换和签名细节都已封装好。

三、第一步:网页授权获取 openid

JSAPI 下单必须携带用户在当前公众号下的 openid。获取方式是通过 OAuth2 网页授权:先引导用户访问授权链接,微信会带着 code 回调我们的服务器。

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=https%3A%2F%2Fyour-domain.com%2Fpay%2Foauth_callback&response_type=code&scope=snsapi_base&state=ORDER123#wechat_redirect

scope 用 snsapi_base 即可实现静默授权,用户无感知;只有需要头像昵称时才用 snsapi_userinfo。用户授权后微信回调 redirect_uri 并携带 code 参数,后端用 code 换取 openid:

# oauth_callback.py 用 code 换 openid
import requests

APPID = "wx1234567890abcdef"
SECRET = "your_app_secret"

def get_openid(code):
    url = "https://api.weixin.qq.com/sns/oauth2/access_token"
    params = {
        "appid": APPID, "secret": SECRET,
        "code": code, "grant_type": "authorization_code",
    }
    r = requests.get(url, params=params).json()
    return r["openid"]  # 存入会话,后续下单使用

openid 是用户在该公众号下的唯一标识,换一个公众号 openid 就完全不同。下单时务必使用下单公众号对应的 openid,否则统一下单会直接报 PARAM_ERROR。

四、第二步:JSAPI 统一下单

拿到 openid 后调用 V3 接口 /v3/pay/transactions/jsapi 下单。请求需要用商户私钥做 RSA-SHA256 签名,这里用官方 SDK 简化处理:

# jsapi_order.py 统一下单
from wechatpayv3 import WeChatPay, WeChatPayType

wxpay = WeChatPay(
    wechatpay_type=WeChatPayType.JSAPI,
    mchid="1900000001",
    private_key=open("apiclient_key.pem").read(),
    cert_serial_no="CERT_SERIAL_NO",
    appid="wx1234567890abcdef",
    apiv3_key="your_api_v3_key",
    notify_url="https://your-domain.com/pay/notify",
)

params = {
    "description": "源码商城-转卡码系统V3",
    "out_trade_no": "SO20260822001",    # 商户订单号,全局唯一
    "amount": {"total": 29900, "currency": "CNY"},  # 单位:分!
    "payer": {"openid": openid},
}
resp = wxpay.pay(WeChatPayType.JSAPI, params)
print(resp["prepay_id"])  # 前端拉起收银台要用

这里有两个高频错误:金额单位是分,299 元必须传 29900,传成 299 会少收 100 倍钱;payer.openid 与 appid 不匹配会报 PARAM_ERROR,下单前先确认 openid 的来源公众号。

五、第三步:前端 wx.chooseWXPay 拉起收银台

后端拿到 prepay_id 后,还需要生成一次"调起支付"的签名参数——注意这里的签名串与下单请求不同,是给前端 JS-SDK 用的,容易搞混:

# 后端生成 JSAPI 调起参数(关键!)
import time, uuid

def get_pay_params(prepay_id):
    ts = str(int(time.time()))
    nonce = uuid.uuid4().hex
    pkg = f"prepay_id={prepay_id}"
    sign_str = f"{APPID}\n{ts}\n{nonce}\n{pkg}\n"
    sign = wxpay.sign(sign_str)   # RSA-SHA256 签名
    return {
        "appId": APPID, "timeStamp": ts,
        "nonceStr": nonce, "package": pkg,
        "signType": "RSA", "paySign": sign,
    }
// 前端:调起微信收银台
wx.config({...});  // JS-SDK 权限签名,需后端 /jssdk 接口基于 jsapi_ticket 生成
wx.chooseWXPay({
    timestamp: res.timeStamp,
    nonceStr: res.nonceStr,
    package: res.package,       // "prepay_id=xxx"
    signType: 'RSA',
    paySign: res.paySign,
    success: function (r) { location.href = '/pay/done?no=' + orderNo; },
    fail: function (e) { // 用户取消或支付失败,此时绝不能发货 }
});

paySign 的签名串是 appId、timeStamp、nonceStr、package 四个值用换行符拼接后加一个尾换行,字段顺序不能乱,否则前端会报 invalid signature。另外 wx.config 需要后端用 jsapi_ticket 生成 JS-SDK 签名,别忘了单独实现 /jssdk 接口。

六、第四步:回调验签与 AES-256-GCM 解密

支付成功后微信会异步通知 notify_url。前端的 success 回调不可信,只有异步通知才是权威的支付结果。通知 body 里的 resource 字段用 AES-256-GCM 加密,需要先用 APIv3 密钥解密,再校验微信支付平台证书签名:

# notify.py 回调处理:验签 + 解密
from wechatpayv3 import WeChatPay

def handle_notify(headers, body):
    # 1. 验签:用微信支付平台证书公钥校验
    wxpay.verify(headers)   # 校验 Wechatpay-Signature
    # 2. 解密 resource 字段
    data = wxpay.decrypt(body["resource"])
    trade_state = data["trade_state"]
    if trade_state == "SUCCESS":
        deliver(data["out_trade_no"])   # 3. 幂等发货,见下一节
    return {"code": "SUCCESS"}  # 必须返回,否则微信会重试

解密失败最常见的原因:APIv3 密钥配置错误(注意不是商户私钥)、回调 body 没有按原始字符串透传。处理成功必须返回 {"code":"SUCCESS"},否则微信会按 15s/15s/30s/3m/10m/20m/30m/30m/30m/60m 的节奏重试 10 次,重复通知会反复打到你的接口。

七、第五步:幂等自动发货与转卡码系统集成

回调可能重复推送,发货逻辑必须幂等。推荐"Redis SETNX 锁 + 数据库状态条件更新"双保险,再进入发卡流程:

import redis
r = redis.Redis(decode_responses=True)

def deliver(order_no):
    # 1. 幂等锁:已处理过的订单直接跳过
    if not r.set(f"pay:{order_no}", "1", nx=True, ex=86400):
        return
    # 2. 条件更新订单状态,防止并发重复发货
    n = db.execute(
        "UP2026年8月22日 orders SET status='PAID' WHERE order_no=%s AND status='PENDING'",
        (order_no,))
    if n == 0:
        return
    # 3. 原子领取卡密并推送用户(SKIP LOCKED 防并发抢同一张卡)
    card = db.execute(
        "SELECT card_no FROM cards WHERE order_no IS NULL "
        "LIMIT 1 FOR UP2026年8月22日 SKIP LOCKED")
    send_card_to_user(order_no, card)

这套"回调验签 → 幂等 → 原子发卡"的链路,正是转卡码系统的核心能力。如果不想从零实现,源码商城在售的转卡码系统 V3 已内置 JSAPI、Native 扫码、当面付三种支付通道,以及完整的幂等自动发货、分润结算与对账模块,部署即可使用。

八、常见坑点速查表

坑点表现解决办法
金额单位少收 100 倍钱金额一律以"分"为单位
openid 不匹配下单报 PARAM_ERROR使用下单公众号对应的 openid
paySign 签名失败收银台无法拉起签名串顺序与尾换行不能丢
回调重复推送重复发货、资损SETNX 锁 + 状态条件更新
resource 解密失败回调处理报错确认用的是 APIv3 密钥而非商户私钥
前端 success 就发货掉单、对不上账只信异步通知,前端结果仅作跳转
订阅号接入拉起收银台失败JSAPI 仅支持已认证服务号

九、小结

JSAPI 支付全链路可以概括为:网页授权拿 openid → 统一下单 → 前端拉起收银台 → 异步通知验签解密 → 幂等自动发货。只要把"验签、解密、幂等"这三件事做扎实,就能稳定支撑源码交易和转卡码发卡业务,避免掉单与资损。

需要现成的支付与自动发货能力?欢迎逛逛源码商城,转卡码系统、Codex Desktop 等产品均提供完整源码,可直接部署使用。