求 Python 网站支付异步通知回调接口示例
结论
可以使用 alipay-sdk-python 完成异步通知验签。回调接口应按以下顺序处理:
- 接收支付宝发送的
POST表单参数。 - 取出
sign,按照支付宝规则构造待验签字符串。 - 使用支付宝公钥验签。
- 校验
app_id、商户订单号、金额、收款方等业务数据。 - 通过事务和唯一约束,幂等更新订单。
- 处理成功后返回纯文本
success。
不要只根据 trade_status 修改订单。业务处理失败时,也不能返回 success。
Flask 回调接口示例
以下示例使用普通公钥模式和 RSA2 签名。不同版本 SDK 的构造参数可能略有不同,但异步通知的处理原则不变。
import logging
from decimal import Decimal, InvalidOperation
from flask import Flask, request, Response
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
app = Flask(__name__)
ALIPAY_APP_ID = "你的应用 APP_ID"
ALIPAY_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
这里填写支付宝公钥,不是应用公钥
-----END PUBLIC KEY-----"""
APP_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
这里填写应用私钥
-----END RSA PRIVATE KEY-----"""
SELLER_ID = "你的支付宝商户 UID"
config = AlipayClientConfig()
config.server_url = "https://openapi.alipay.com/gateway.do"
config.app_id = ALIPAY_APP_ID
config.app_private_key = APP_PRIVATE_KEY
config.alipay_public_key = ALIPAY_PUBLIC_KEY
config.sign_type = "RSA2"
config.charset = "utf-8"
logger = logging.getLogger("alipay")
alipay_client = DefaultAlipayClient(
alipay_client_config=config,
logger=logger
)
def build_sign_content(params):
"""
构造支付宝异步通知的待验签字符串。
规则:
1. 排除 sign 和 sign_type
2. 排除空值
3. 按参数名排序
4. 使用 key=value&key=value 的形式拼接
"""
items = []
for key in sorted(params.keys()):
if key in ("sign", "sign_type"):
continue
value = params.get(key)
if value is None or value == "":
continue
items.append("{}={}".format(key, value))
return "&".join(items)
def get_order(out_trade_no):
"""
根据商户订单号查询本地订单。
请替换为真实的数据库查询。
"""
raise NotImplementedError
def mark_order_paid(order, trade_no):
"""
在数据库事务中将订单更新为已支付。
建议:
- trade_no 建立唯一约束
- 仅允许“待支付”状态更新为“已支付”
- 同一通知重复到达时直接视为处理成功
"""
raise NotImplementedError
@app.route("/alipay/notify", methods=["POST"])
def alipay_notify():
# 使用框架解析 application/x-www-form-urlencoded 表单。
params = request.form.to_dict(flat=True)
sign = params.get("sign")
if not sign:
logger.warning("支付宝通知缺少 sign")
return Response("failure", mimetype="text/plain")
sign_content = build_sign_content(params)
try:
verified = alipay_client.verify(sign_content, sign)
except Exception:
logger.exception("支付宝异步通知验签发生异常")
return Response("failure", mimetype="text/plain")
if not verified:
logger.warning("支付宝异步通知验签失败")
return Response("failure", mimetype="text/plain")
# 验签成功后仍然必须检查业务参数。
if params.get("app_id") != ALIPAY_APP_ID:
logger.warning("支付宝通知 app_id 不匹配")
return Response("failure", mimetype="text/plain")
if SELLER_ID and params.get("seller_id") != SELLER_ID:
logger.warning("支付宝通知 seller_id 不匹配")
return Response("failure", mimetype="text/plain")
out_trade_no = params.get("out_trade_no")
trade_no = params.get("trade_no")
trade_status = params.get("trade_status")
total_amount = params.get("total_amount")
if not out_trade_no or not trade_no or not total_amount:
logger.warning("支付宝通知缺少必要业务参数")
return Response("failure", mimetype="text/plain")
try:
order = get_order(out_trade_no)
except Exception:
logger.exception("查询本地订单失败")
return Response("failure", mimetype="text/plain")
if order is None:
logger.warning("本地订单不存在:%s", out_trade_no)
return Response("failure", mimetype="text/plain")
try:
notified_amount = Decimal(total_amount)
order_amount = Decimal(str(order.amount))
except (InvalidOperation, AttributeError):
logger.warning("订单金额格式不正确")
return Response("failure", mimetype="text/plain")
if notified_amount != order_amount:
logger.warning("订单金额不匹配:%s", out_trade_no)
return Response("failure", mimetype="text/plain")
if trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED"):
try:
mark_order_paid(order, trade_no)
except Exception:
logger.exception("更新订单支付状态失败")
return Response("failure", mimetype="text/plain")
# 通知合法且已经完成对应处理,必须返回纯文本 success。
return Response("success", mimetype="text/plain")
其中,get_order() 和 mark_order_paid() 是业务占位函数,需要替换为实际的数据库代码。
Django 中如何接收参数
Django 项目的验签和业务校验逻辑相同,只需调整请求参数的获取方式和响应方式:
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def alipay_notify(request):
if request.method != "POST":
return HttpResponse("failure", content_type="text/plain")
params = request.POST.dict()
sign = params.get("sign")
if not sign:
return HttpResponse("failure", content_type="text/plain")
sign_content = build_sign_content(params)
try:
verified = alipay_client.verify(sign_content, sign)
except Exception:
verified = False
if not verified:
return HttpResponse("failure", content_type="text/plain")
# 在这里继续校验 app_id、seller_id、订单号、金额和交易状态。
# 确认数据库事务成功提交后再返回 success。
return HttpResponse("success", content_type="text/plain")
支付宝服务器无法携带 Django 的 CSRF Token,因此回调视图通常需要添加 @csrf_exempt。但这并不意味着可以省略签名验证。支付宝签名仍是该接口最重要的身份校验手段。
待验签字符串为什么容易出错
异步通知参数不能转换成 JSON 后直接验签。应使用通知中的表单参数,排除 sign 和 sign_type,再按参数名排序拼接:
app_id=...&auth_app_id=...&buyer_id=...&out_trade_no=...
常见错误有:
- 把
sign本身加入待验签字符串。 - 对参数值再次进行 URL 编码或 URL 解码。
- 把表单参数转换成 JSON 后验签。
- 修改参数中的空格、斜杠、中文或时间格式。
- 使用应用公钥代替支付宝公钥。
- 应用配置为
RSA2,但使用了不一致的密钥或签名配置。 - 中间件读取请求体后修改了原始参数。
尽量使用 Web 框架解析得到的表单值,不要对参数值做二次处理。
回调地址需要满足的条件
支付请求中的 notify_url 必须是支付宝服务器能够访问的公网 HTTPS 地址,例如:
https://www.example.com/alipay/notify
以下地址不能使用:
http://127.0.0.1:5000/alipay/notify
http://localhost:8000/alipay/notify
本地开发时,可以使用支持公网转发的调试环境。生产环境应使用稳定域名,并确认防火墙、反向代理和路由不会拦截支付宝发送的 POST 请求。
异步通知发送到哪里与 return_url 无关:
notify_url:由支付宝服务器调用,用于可靠地更新订单。return_url:由用户浏览器访问,主要用于展示支付结果。
订单支付状态应以异步通知或主动查询结果为准,不能只依赖浏览器跳转。
必须做好幂等处理
支付宝可能重复发送同一笔交易的通知,因此回调代码必须能够安全地重复执行。可以同时采取以下措施:
- 保证
out_trade_no在商户系统中唯一。 - 为
trade_no建立唯一索引或唯一约束。 - 在数据库事务中锁定订单。
- 只允许待支付订单更新为已支付。
- 已支付订单再次收到相同通知时,直接返回
success。 - 已支付订单收到不同的
trade_no或金额时,记录异常,不要覆盖原数据。
伪代码如下:
with database.transaction():
order = query_order_for_update(out_trade_no)
if order.status == "PAID":
if order.alipay_trade_no == trade_no:
return
raise ValueError("订单已绑定其他支付宝交易号")
if Decimal(str(order.amount)) != Decimal(total_amount):
raise ValueError("通知金额与订单金额不一致")
order.status = "PAID"
order.alipay_trade_no = trade_no
order.save()
返回值和重试
处理成功后,应返回内容明确的纯文本:
success
不要返回 JSON、HTML 页面或带有额外说明的字符串,例如:
{"status": "success"}
如果验签失败、金额不一致、数据库事务失败,或者业务处理尚未完成,应返回:
failure
支付宝此后可能继续发送通知。具体的重试次数和时间间隔可能因支付宝产品及当前规则而变化,应以所接入产品的最新官方文档为准。
关于 SDK 版本差异
上述代码假设当前安装的 alipay-sdk-python 中,DefaultAlipayClient.verify() 接收“待验签字符串”和 sign。如果 3.6.72 版本的实际方法签名不同,应查看该版本源码或安装包中的方法定义,不要直接套用其他版本示例的参数顺序。
无论 SDK 接口形式如何变化,完整的处理顺序都不应改变:
接收表单 → 验签 → 校验交易信息 → 幂等更新订单 → 返回 success
如果应用使用证书模式,而非普通支付宝公钥模式,还需要按照证书模式配置支付宝公钥证书、应用公钥证书和支付宝根证书,不能直接照搬普通公钥模式的验签配置。
备注:内容仅供参考。
