最新部落格更新見我的個人主頁: 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>