最新博客更新见我的个人主页: https://xzajyjs.cn
我们在使用Django构建网站时常需要对接第三方支付平台的支付接口,这里就以支付宝为例(其他平台大同小异),使用支付宝开放平台的沙箱环境进行实验。
我们这里使用一个第三方的AliPay Python SDK
(github)
下面看一下它的基本使用
调用流程
事实上需要我们网站服务端做的事并不多,只需要生成一个订单向支付宝发出支付请求,等用户支付完毕后向支付宝(通过同步和异步的方式)查询订单、交易信息即可。
在实际生产环境中,需要注意如下各种安全性问题:
由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
商户系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。
接收到异步通知并验签通过后,请务必核对通知中的 app_id、out_trade_no、total_amount 等参数值是否与请求中的一致,并根据 trade_status 进行后续业务处理。
在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商户端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。
具体实践
1.准备工作
由于使用真实环境需要商户支付宝账号、上线应用需要审批等流程,我们这里使用支付宝开放平台的沙箱环境
沙箱环境中提供了后面需要的参数如APPID
、APP_PRIVATE_KEY
、ALIPAY_PUBLIC_KEY
、支付宝网关
等。
接下来安装AliPay Python SDK
pip3 install python-alipay-sdk --upgrade
由于是沙箱环境,平台已经提供给我们需要的公钥和私钥,如果是生产环境,则需要通过openssl生成
openssl OpenSSL> genrsa -out app_private_key.pem 2048 # 私钥 OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem # 导出公钥 OpenSSL> exit
在支付宝上下载的公钥是一个字符串,你需要在文本的首尾添加标记位:
-----BEGIN PUBLIC KEY----- 和 -----END PUBLIC KEY-----
2.创建订单
先在settings.py
中设定一些关键参数
# 读取公钥和私钥为字符串 app_private_key_string = open("/path/to/your/private/key.pem").read() alipay_public_key_string = open("/path/to/alipay/public/key.pem").read() # 沙箱环境提供的APPID ALIPAY_APP_ID = "2021000120607609" # 同步回调url(这里需要一个公网ip) RETURN_URL = "http://xxx.xxx.xxx.xxx/" # 支付宝网关地址。注意:正式环境和沙箱环境的网关地址不同 GATEWAY = "https://openapi.alipaydev.com/gateway.do?"
from alipay import AliPay, AliPayConfig from .settings import APP_PRIVATE_KEY, ALIPAY_PUBLIC_KEY, ALIPAY_APP_ID, RETURN_URL, GATEWAY def create_alipay(): # 使用应用公钥进行报文验签 alipay = AliPay( appid=ALIPAY_APP_ID, app_notify_url=None, # 默认异步回调 url app_private_key_string=APP_PRIVATE_KEY, alipay_public_key_string=ALIPAY_PUBLIC_KEY, sign_type="RSA2", debug=False, # 默认 False verbose=False, # 输出调试数据 config=AliPayConfig(timeout=15) # 可选,请求超时时间 ) return alipay
下面可以创建支付订单了(官方文档)
# 向支付宝提交订单信息 def alipay_pay(subject, total_amount, out_trade_no, return_url_view): alipay = create_alipay() # 先实例化alipay return_url = RETURN_URL + return_url_view # 同步回调url,用于支付完后跳转回网站并对支付状态进行即时检验。这里的return_url_view是用于接收支付宝回调的状态检验的视图函数 order_string = alipay.api_alipay_trade_page_pay( out_trade_no=out_trade_no, # 商户订单号,这个需要商户自定义 total_amount=total_amount, # 支付总金额 subject=subject, # 订单标题 return_url=return_url, # 同步回调url,用于支付完后跳转回网站并对支付状态进行即时检验 notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url ) return order_string # 返回订单字符串
调用
import string import random from .settings import GATEWAY # 随机生成32位商户交易号 out_trade_no = "".join(random.sample(string.ascii_letters+string.digits, 32)) # 在视图函数对alipay_return进行绑定 # 同步回调url为: http://xxx.xxx.xxx.xxx/alipay_return order_string = alipay_pay(subject="测试商品",total_amount=100,out_trade_no=out_trade_no,return_url_view='alipay_return') return HttpResponseRedirect(GATEWAY+order_string)
调用后会跳转到支付宝平台,使用沙箱环境提供的买家账号即可完成支付
但是此时我们还不能回调跳转到我们自己的网站,也不能获得订单支付信息,下面还有最后一步。
3.同步回调
我们刚刚创建的订单信息中填写了return_url
,我们需要一个视图函数来接收,并对其返回值进行分析
def alipay_return(request): processed_dict = {} # 回调时alipay会把一些公用信息通过GET方式传参回来,这里用字典去接收存储 for key, value in request.GET.items(): processed_dict[key] = value """ processed_dict = { 'charset': 'utf-8', 'out_trade_no': 'xxxxxxx', # 这个是我们之前创建订单时生成的商户交易号 'method': 'alipay.trade.page.pay.return', 'total_amount': '100.00', # 交易金额 'trade_no': '20220xxxxxxxx24353', # 支付宝交易号 'auth_app_id': '2021xxxxxx609', # 用户appid 'version': '1.0', 'app_id': '2021xxxxxx7609', # 沙箱提供的APPID 应用ID 'sign_type': 'RSA2', 'seller_id': '2088xxxxx844', # 收款支付宝账号对应的支付宝唯一用户号。 以2088开头的纯16位数字 'timestamp': '2022-05-28 23:40:55' } """ sign = processed_dict.pop("sign", None) new_alipay = create_alipay() verify_re = new_alipay.verify(processed_dict, sign) if verify_re is True: print("支付成功") else: print("支付失败")
注意:同步回调往往不可靠,因此需要增加一个异步回调检验
另外,在订单创建后需要向数据库存储订单信息,包括订单金额、商户订单号、appid等,等待回调后与参数校验一致无误后再将订单支付信息进行更新。下面的完整示例不会包括该部分,请自行完成
完整示例
项目结构
# alipay.py from alipay import AliPay, AliPayConfig from .settings import APP_PRIVATE_KEY, ALIPAY_PUBLIC_KEY, ALIPAY_APP_ID, RETURN_URL def create_alipay(): alipay = AliPay( appid=ALIPAY_APP_ID, app_notify_url=None, # 默认回调 url app_private_key_string=APP_PRIVATE_KEY, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥, alipay_public_key_string=ALIPAY_PUBLIC_KEY, sign_type="RSA2", # RSA 或者 RSA2 debug=False, # 默认 False verbose=False, # 输出调试数据 config=AliPayConfig(timeout=15) # 可选,请求超时时间 ) return alipay def alipay_pay(subject, total_amount, out_trade_no, return_url_view): alipay = create_alipay() return_url = RETURN_URL + return_url_view order_string = alipay.api_alipay_trade_page_pay( out_trade_no=out_trade_no, total_amount=total_amount, subject=subject, return_url=return_url, notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url ) return order_string
# settings.py ... ... ALIPAY_APP_ID = "xxxxxx" APP_PRIVATE_KEY = open(os.path.join(BASE_DIR, 'alipay/app_private_key.pem'), 'r').read() ALIPAY_PUBLIC_KEY = open(os.path.join(BASE_DIR, 'alipay/alipay_public_key.pem'), 'r').read() RETURN_URL = "http://xxxxxx/" GATEWAY = "https://openapi.alipaydev.com/gateway.do?"
# urls.py from django.contrib import admin from django.urls import path from . import views urlpatterns = [ path('admin/', admin.site.urls), path('', views.index), path('alipay_return/', views.alipay_return) ]
# views.py import random import string from django.http import HttpResponseRedirect from django.shortcuts import render from ali_django.alipay import alipay_pay, create_alipay from django.conf import settings def index(request): if request.method == "GET": return render(request, 'index.html') elif request.method == "POST": # 随机生成32位商户交易号 out_trade_no = "".join(random.sample(string.ascii_letters + string.digits, 32)) order_string = alipay_pay(subject="测试商品", total_amount=100, out_trade_no=out_trade_no,return_url_view='alipay_return') return HttpResponseRedirect(settings.GATEWAY + order_string) def alipay_return(request): processed_dict = {} # 回调时alipay会把一些公用信息通过GET方式传参回来,这里用字典去接收存储 for key, value in request.GET.items(): processed_dict[key] = value sign = processed_dict.pop("sign", None) new_alipay = create_alipay() verify_re = new_alipay.verify(processed_dict, sign) if verify_re is True: print("支付成功") else: print("支付失败")
<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>支付宝支付接口测试</title> </head> <body> <form action="" method="post"> <input type="submit" value="提交"> </form> </body> </html>