

























周一早上 8:30,你走进公司楼下的咖啡店,扫了吧台上的二维码,选了一杯冰美式,¥28。
打开微信,扫一下,输入指纹,手机震动——"支付成功"。整个过程不到 2 秒。
你可能觉得这没什么。但如果你是这家咖啡店的老板,刚请了个开发者帮你搭线上点单系统——你的开发者正在跟支付宝和微信支付的签名机制较劲。
这篇文章,就从这杯 ¥28 的拿铁出发,带你走通支付宝和微信支付的完整对接流程。我们不会一上来就甩代码,而是先搞懂每一步"为什么要这么做",然后再写代码。
让我们先跟着这笔 ¥28,看看它完整的旅程。
你用支付宝扫码支付时,背后发生的事情是这样的:
你的手机App 咖啡店服务器 支付宝服务器
│ │ │
│ │ ① 创建订单 │
│ │ "冰美式 ¥28" │
│ │─────────────────→│
│ │ ② 返回二维码链接 │
│ │←─────────────────│
│ ③ 吧台展示二维码 │ │
│ │ │
│ ④ 你扫码 + 输入指纹 │
│───────────────────────────────────→│
│ │ │ ⑤ 支付宝扣款
│ │ ⑥ 异步通知:"¥28 已收到" │
│ │←─────────────────│
│ │ ⑦ 更新订单状态 │
│ ⑧ 手机显示"支付成功" │
│←──────────────────────────────────│
注意第 ⑥ 步:支付宝不是直接把结果告诉你的手机,而是通过一个异步通知告诉咖啡店的服务器。这个异步通知机制,是整个支付对接中最核心、也最容易出问题的部分。
微信支付的流程几乎一样,但在细节上有重要差异——最大的不同是:微信支付的异步通知是加密的。
你的手机App 咖啡店服务器 微信支付服务器
│ │ │
│ │ ① 创建订单 │
│ │ "冰美式 2800分" │ ← 注意:微信用"分"!
│ │─────────────────→│
│ │ ② 返回二维码链接 │
│ │←─────────────────│
│ │ │
│ ③ 你扫码 + 输入密码 │
│───────────────────────────────────→│
│ │ ④ 异步通知(AES-GCM 加密)│
│ │←─────────────────│
│ │ ⑤ 解密 → 验签 │
│ │ → 更新订单 │
两个关键差异先记住,后面写代码时会反复用到:
"28.00"),微信用分(整数 2800)在我们动手写代码之前,必须先搞懂一个问题:为什么每个请求都需要"签名"?
设想这个场景:你的咖啡店服务器向支付宝发了一个请求——"帮我收 ¥28"。如果有人在中间截获了这个请求,把 ¥28 改成 ¥0.01,那不是白喝咖啡了?
数字签名就是为了解决这个问题。它保证三件事:
签名的原理很简单:
你的请求数据:"冰美式,¥28,订单号 20260404001"
│
▼ 用你的私钥加密
签名值:a3f8b2c1d4e5f6...
│
▼ 支付宝用你的公钥解密验证
结果:数据没被篡改 ✅ 确实是你发的 ✅
支付宝用的是 RSA-SHA256 非对称加密。你在支付宝开放平台配置应用时,会生成一对密钥:
待签名字符串的拼接方式:
HTTP方法\n
请求路径\n
时间戳\n
随机字符串\n
请求体\n
微信支付同样用 RSA-SHA256,但密钥管理方式不同:
待签名字符串几乎一样:
HTTP方法\n
URL(含查询参数)\n
时间戳\n
随机字符串\n
请求体\n
虽然两者都是 RSA-SHA256,但有几个细节差异会让你踩坑:
| 支付宝 V3 | 微信支付 V3 | |
|---|---|---|
| RSA 填充方式 | PSS 填充 | PKCS#1 v1.5 填充 |
| 密钥管理 | 应用公私钥对 | API 证书(含私钥) |
| 回调内容 | 明文 + RSA 签名 | AES-GCM 加密 + RSA 签名 |
| 证书获取 | 手动下载支付宝根证书 | API 自动获取(需定期轮换) |
| 金额单位 | 元(字符串 "28.00") |
分(整数 2800) |
| 回调响应 | 返回字符串 "success" |
返回 JSON {"code":"SUCCESS"} |
经验之谈:金额单位不一致是最常见的生产事故之一。建议在你的系统内部统一用"分"存储,展示时再转换成元。
理论够了,让我们开始写代码。我们用 Python + FastAPI 实现,因为它简洁易懂。
在写代码之前,你需要准备这些东西:
第一步:注册企业支付宝账号
▼
第二步:登录开放平台(open.alipay.com)创建应用
▼
第三步:配置应用——生成 RSA2 密钥对,上传公钥(推荐证书模式)
▼
第四步:提交审核,获取 AppID
▼
第五步:签约「电脑网站支付」/「当面付」能力
你需要拿到这些配置:
| 配置项 | 说明 | 安全级别 |
|---|---|---|
| AppID | 应用唯一标识 | 可公开 |
| 应用私钥 | 给请求签名,绝对不能泄露 | 🔴 绝密 |
| 支付宝公钥证书 | 验证支付宝的响应 | 可公开 |
| 回调地址(Notify URL) | 接收异步支付通知,必须 HTTPS | — |
当咖啡店的点单系统收到一笔 ¥28 的订单后,它需要调用支付宝 API 生成一个二维码。顾客扫这个码就能付款。
import json
import os
import uuid
from fastapi import FastAPI, Request, Response
from alipay_sdk import AliPay # alipay-sdk-python
app = FastAPI()
# 初始化支付宝客户端(证书模式,推荐)
alipay = AliPay(
appid=os.getenv("ALIPAY_APP_ID"),
app_private_key_path=os.getenv("ALIPAY_PRIVATE_KEY_PATH"),
alipay_public_key_cert_path=os.getenv("ALIPAY_PUBLIC_KEY_CERT_PATH"),
app_public_key_cert_path=os.getenv("APP_PUBLIC_KEY_CERT_PATH"),
root_cert_path=os.getenv("ALIPAY_ROOT_CERT_PATH"),
sign_type="RSA2",
)
@app.post("/api/pay/alipay/native")
async def create_alipay_native(request: Request):
"""顾客下单后,生成支付宝二维码"""
body = await request.json()
biz_content = {
"out_trade_no": f"ORDER_{uuid.uuid4().hex[:16]}", # 商户订单号
"total_amount": body["amount"], # 金额(元,字符串)
"subject": body["subject"], # 商品标题
"product_code": "FACE_TO_FACE_PAYMENT", # 当面付
}
# 这一步,SDK 会自动用你的私钥签名
response = alipay.api_alipay_trade_precreate(biz_content=biz_content)
if response["code"] == "10000":
return {
"qr_code": response["qr_code"], # 二维码链接
"out_trade_no": biz_content["out_trade_no"],
}
else:
return {"error": response["msg"]}
吧台收到 qr_code 后,把它渲染成二维码图片展示出来。顾客用支付宝 App 扫这个码,就会进入支付流程。
顾客扫码支付成功后,支付宝会向你预设的回调地址发送一个 POST 请求,告诉你"¥28 收到了"。
这是整个对接过程中最关键的一段代码。写错了,要么钱收到了但订单没更新(用户投诉),要么被人伪造通知白嫖你的商品。
@app.post("/api/pay/alipay/notify")
async def alipay_notify(request: Request):
"""支付宝异步通知回调——每一行都很重要"""
# 1. 获取所有 POST 参数(支付宝用 Form 表单格式)
form_data = await request.form()
params = dict(form_data)
# 2. 验签(最关键的一步!不验签 = 裸奔)
sign = params.pop("sign")
sign_type = params.pop("sign_type")
if not alipay.verify(params, sign):
return Response(content="fail", status_code=400)
# 3. 检查交易状态
if params["trade_status"] not in ("TRADE_SUCCESS", "TRADE_FINISHED"):
return Response(content="success") # 非成功状态,直接确认收到
out_trade_no = params["out_trade_no"]
total_amount = params["total_amount"]
# 4. 幂等检查——支付宝可能重复推送!
# 25 小时内最多推 8 次,你必须保证重复处理不会出问题
order = await get_order(out_trade_no)
if order.status == "PAID":
return Response(content="success") # 已处理过,直接返回
# 5. 金额校验——防止有人篡改金额
if float(total_amount) != float(order.amount):
logger.error(f"金额不匹配: {total_amount} vs {order.amount}")
return Response(content="fail")
# 6. 一切正常,更新订单状态
await update_order_status(out_trade_no, "PAID",
trade_no=params["trade_no"])
# 7. 返回 "success" 告知支付宝不再重试
return Response(content="success")
让我解释一下为什么每一步都不能少:
搞定了支付宝,接下来是微信支付。流程类似,但有几个"坑"需要特别注意。
第一步:注册微信支付商户号(pay.weixin.qq.com)
▼
第二步:完成实名认证和签约
▼
第三步:商户平台 → API安全 → 申请 API v3 证书
▼
第四步:设置 APIv3 密钥(32 位字符串,用于解密回调通知)
▼
第五步:关联 AppID(公众号/小程序/移动应用)
| 配置项 | 说明 | 安全级别 |
|---|---|---|
| 商户号(mchid) | 商户唯一标识 | 可公开 |
| API V3 密钥 | 解密回调通知 | 🔴 绝密 |
| 商户 API 证书 | 包含私钥,用于签名 | 🔴 绝密 |
| 微信支付平台证书 | 验证微信支付的响应 | 可公开 |
⚠️ 安全警告:私钥和 API V3 密钥绝对不能硬编码在代码中。用环境变量,或者更好——用密钥管理服务(如 HashiCorp Vault)。
微信支付的 API V3 需要你手动构造签名(不像支付宝有比较完善的官方 SDK 一步到位),所以代码会稍微复杂一些。
import hashlib
import json
import os
import time
import uuid
import requests
from fastapi import FastAPI, Request
app = FastAPI()
MCHID = os.getenv("WECHAT_MCHID") # 商户号
APPID = os.getenv("WECHAT_APPID") # 应用 ID
API_V3_KEY = os.getenv("WECHAT_API_V3_KEY") # APIv3 密钥
SERIAL_NO = os.getenv("WECHAT_CERT_SERIAL") # 证书序列号
@app.post("/api/pay/wechat/native")
async def create_wechat_native(request: Request):
"""生成微信支付二维码"""
body = await request.json()
out_trade_no = f"WX_{uuid.uuid4().hex[:16]}"
# 构造请求体——注意:金额单位是分!¥28 = 2800 分
pay_body = {
"appid": APPID,
"mchid": MCHID,
"description": body["subject"],
"out_trade_no": out_trade_no,
"notify_url": "https://yourdomain.com/api/pay/wechat/notify",
"amount": {
"total": int(float(body["amount"]) * 100), # 元 → 分
"currency": "CNY"
}
}
# 构造 API v3 签名
timestamp = str(int(time.time()))
nonce_str = uuid.uuid4().hex
sign_str = build_sign_string("POST",
"/v3/pay/transactions/native",
timestamp, nonce_str,
json.dumps(pay_body))
signature = rsa_sign(sign_str) # 用商户私钥签名
# 发送请求
headers = {
"Authorization": f'WECHATPAY2-SHA256-RSA2048 '
f'mchid="{MCHID}",'
f'nonce_str="{nonce_str}",'
f'signature="{signature}",'
f'timestamp="{timestamp}",'
f'serial_no="{SERIAL_NO}"',
"Content-Type": "application/json",
}
resp = requests.post(
"https://api.mch.weixin.qq.com/v3/pay/transactions/native",
headers=headers,
json=pay_body
)
result = resp.json()
return {
"code_url": result.get("code_url"), # 二维码链接
"out_trade_no": out_trade_no,
}
def build_sign_string(method, url, timestamp, nonce_str, body):
"""构造 API v3 待签名字符串"""
return f"{method}\n{url}\n{timestamp}\n{nonce_str}\n{body}\n"
这是微信支付和支付宝最大的区别:微信的回调通知是加密的。
为什么要加密?因为微信更进一步地保护了交易数据——即使有人截获了回调请求,也看不到里面的订单信息。
处理流程是:先验签(确认是微信发的)→ 再解密(用 API V3 密钥)→ 最后处理业务。
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import base64
@app.post("/api/pay/wechat/notify")
async def wechat_notify(request: Request):
"""微信支付异步通知回调——需要先解密!"""
body = await request.json()
# 1. 验证请求签名(用微信支付平台证书的公钥)
timestamp = request.headers.get("Wechatpay-Timestamp")
nonce = request.headers.get("Wechatpay-Nonce")
signature = request.headers.get("Wechatpay-Signature")
serial = request.headers.get("Wechatpay-Serial")
raw_body = await request.body()
if not verify_wechat_signature(timestamp, nonce,
raw_body.decode(), signature, serial):
return {"code": "FAIL", "message": "验签失败"}
# 2. 解密通知内容(AEAD_AES_256_GCM)
# 这一步是微信支付独有的!
resource = body["resource"]
plaintext = decrypt_aes_gcm(
nonce=resource["nonce"],
ciphertext=resource["ciphertext"],
associated_data=resource["associated_data"],
api_v3_key=API_V3_KEY
)
notification = json.loads(plaintext)
# 3. 和支付宝一样:幂等检查 + 金额校验 + 更新状态
out_trade_no = notification["out_trade_no"]
total = notification["amount"]["total"] # 单位:分!
order = await get_order(out_trade_no)
if order.status == "PAID":
return {"code": "SUCCESS", "message": "OK"}
# 金额对比时记得统一单位
if total != int(order.amount * 100): # 如果 order.amount 存的是元
return {"code": "FAIL", "message": "金额不匹配"}
await update_order_status(out_trade_no, "PAID",
trade_no=notification["transaction_id"])
# 微信支付要求返回 JSON 格式(不是字符串 "success"!)
return {"code": "SUCCESS", "message": "OK"}
def decrypt_aes_gcm(nonce, ciphertext, associated_data, api_v3_key):
"""解密微信支付回调通知——AES-256-GCM"""
key = api_v3_key.encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ad = associated_data.encode('utf-8') if associated_data else b''
data = base64.b64decode(ciphertext)
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce_bytes, data, ad).decode('utf-8')
走到这里,你已经成功对接了支付宝和微信支付。让我们停下来,对比一下两者回调机制的关键差异,帮你避开生产环境中最常见的坑。
当你的服务器没有正确响应时,支付平台不会就此罢休——它们会反复重试:
支付宝:在 25 小时内最多重试 8 次。间隔大约是 4m、10m、10m、1h、2h、6h、15h。
微信支付:最多重试 15 次,间隔从 15 秒递增到 6 小时(15s、15s、30s、3m、10m、20m、30m、30m、30m、60m、3h、3h、3h、6h、6h)。
这意味着微信支付的重试更频繁、更执着。所以你的回调接口必须做好幂等处理——同一笔订单被推送 15 次,你只能处理一次。
| 维度 | 支付宝 | 微信支付 |
|---|---|---|
| 数据格式 | Form 表单(key=value) | JSON |
| 加密方式 | 明文 + RSA 签名 | AES-GCM 加密 + RSA 签名 |
| 成功响应 | 返回字符串 "success" |
返回 JSON {"code":"SUCCESS"} |
| 金额字段 | total_amount(元,字符串) |
amount.total(分,整数) |
| 订单号字段 | out_trade_no |
out_trade_no |
| 渠道交易号 | trade_no |
transaction_id |
坑中之坑:支付宝返回纯字符串
success,微信返回 JSON。搞反了的话,支付平台会认为你没收到通知,一直重试。
对接了几十家商户的支付宝和微信支付后,我总结了最常见的踩坑清单:
症状:调用支付宝/微信 API 时返回签名错误。
常见原因:
\n 不能少)解决方案:别自己拼签名串,用官方 SDK。如果必须手动实现,用官方提供的在线验签工具逐字段比对。
症状:用户明明支付成功了,但服务器没收到通知,订单状态卡在"待支付"。
常见原因:
解决方案:
症状:用户付了 ¥28,你的系统显示收到 ¥2800(或 ¥0.28)。
原因:微信用分,支付宝用元。存储时没统一。
解决方案:系统内部一律用最小单位(分)存储。amount_cents = 2800 而不是 amount = 28.00。展示时再除以 100。
症状:同一笔订单被处理了多次。比如多次发了优惠券、多次通知了业务系统。
原因:回调接口没做幂等。
解决方案:
症状:某天凌晨你的支付突然全部失败。
原因:商户证书或平台证书过期了,没有及时更新。
解决方案:
扫码支付只是冰山一角。实际业务中,你可能还需要对接这些场景:
H5 支付:用户在手机浏览器里打开你的网站,点击支付后唤起支付宝/微信 App 完成支付。适合移动端网页。
JSAPI 支付(微信独有):用户在微信内置浏览器中打开你的公众号/小程序页面,直接弹出支付弹窗。这是微信生态内最流畅的支付方式——需要获取用户的 openid。
App 支付:用户在你的原生 App 中发起支付,唤起支付宝/微信 App 完成支付。
这些场景的核心对接逻辑(签名、回调、幂等)和本文讲的 Native 扫码完全一样,只是请求参数和前端调用方式略有不同。掌握了本文的内容,切换到其他场景会非常顺畅。
有收款就有退款。支付宝和微信的退款逻辑相对简单:
支付宝退款:调用 alipay.trade.refund 接口,传入原交易号和退款金额即可。支持部分退款。退款通常即时到账,最慢 5 个工作日。注意:支付宝没有退款异步通知,你需要主动调用查询接口确认退款状态。
微信支付退款:调用退款接口,同样支持部分退款。到账时间 1~3 个工作日。微信有退款异步通知——同样是 AES-GCM 加密的。
两个好消息:支付宝和微信支付的退款手续费免费(不像 PayPal 退款不退手续费),而且退款时限都比较长(支付宝 3 个月,微信 1 年)。
在结束之前,给你一份在生产环境中对接支付宝和微信支付的最佳实践清单:
✅ 用官方 SDK,不要手动拼签名(除非你有特殊需求)
✅ 密钥通过环境变量或密钥管理服务存储,绝不硬编码
✅ 回调接口做幂等(唯一约束 + 状态检查)
✅ 同时实现异步通知 + 主动查询(双保险)
✅ 金额统一用最小单位(分)存储
✅ 回调处理控制在 5 秒内,复杂逻辑异步化
✅ 所有支付操作记录详细日志(排查问题的救命稻草)
✅ 监控证书有效期,设置到期告警
✅ 先在沙盒环境调通,再切换到生产环境
✅ 建立每日自动对账机制
| 篇目 | 标题 | 你将学到 |
|---|---|---|
| 第 1 篇 | 概览篇——一笔订单触发的支付之旅 | 四方模型、支付生命周期、市场格局、成本分析、选型决策 |
| 第 2 篇 | 支付宝 & 微信支付——一杯咖啡的扫码之旅(本文) | 签名机制、回调处理、AES-GCM 解密、完整对接代码 |
| 第 3 篇 | Stripe & 信用卡——一件跨境商品的卡支付之旅 | Payment Intents、3D Secure、PCI DSS、前后端代码 |
| 第 4 篇 | PayPal——一位海外买家的安全支付之旅 | OAuth 认证、Smart Buttons、争议保护、Webhook |
| 第 5 篇 | 统一支付网关——当四条河流汇入一片海 | 三层架构、策略模式、幂等设计、对账补偿 |
欢迎关注公众号 coft,获取更多深度技术文章。下一篇,我们跟着一件 $149.99 的限量球鞋,走进 Stripe 和信用卡的世界——3D Secure、PCI DSS、授权与捕获,这些听起来吓人的概念,其实并没有那么难。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。