1. 程式人生 > 實用技巧 >python | 支付寶支付

python | 支付寶支付

一、支付寶支付

沙箱環境介紹

螞蟻沙箱環境(Beta)是協助開發者進行介面功能開發及主要功能聯調的輔助環境。沙箱環境模擬了開放平臺部分產品的主要功能和主要邏輯(當前沙箱支援產品請參考“沙箱支援產品列表”)。
在開發者應用上線稽核前,開發者可以根據自身需求,先在沙箱環境中瞭解、組合和除錯各種開放介面,進行開發調通工作,從而幫助開發者在應用上線稽核完成後,能更快速、更順利的進行線上除錯和驗收工作。

詳情,請參考連結:

https://docs.open.alipay.com/200/105311/

商家要使用支付寶支付功能,需要申請一個支付介面。需要提交企業的營業執照等相關資訊!

但是對於個人開發者來說,並沒有這些資料。所以,為了幫助開發者進行介面測試,個人可以使用沙箱環境測試。只要你有支付寶賬號就行。裡面提供了測試賬號,錢可以隨便充值。注意:資料是假的,金額可能會被清洗!

申請賬號

開啟連結

https://openhome.alipay.com/platform/appDaily.htm?tab=info

開啟手機支付寶,掃描二維碼,就可以登入了

登入成功之後,點選自研開發者

點選導航的開發者中心-->研發服務

填寫相關資訊,完成實名認證

再次進入沙箱應用,點選檢視應用公鑰

點選設定應用公鑰

點選檢視祕鑰生成方法

下載windows版本的壓縮包,解壓一下,效果如下:

點選RSA簽名驗籤工具,直接點選生成祕鑰

不要管JAVA和非JAVA,預設的就可以了

效果如下:

它會在當前壓縮包裡面的RSA祕鑰,生成2個txt檔案

開啟應用公鑰2048.txt,將裡面的內容複製一下

進入到剛才的頁面,輸入公鑰,點選儲存。

如果儲存時,提示公鑰格式不對。再生成一次公鑰,再次輸入,就可以了!

SDK

SDK的英文全名是:software development kit,翻譯成中文的意思就是“軟體開發工具包”


通俗一點的理解,是指由第三方服務商提供的實現軟體產品某項功能的工具包。一般以集合api和文件、範例、工具的形式出現。


通常SDK是由專業性質的公司提供專業服務的集合,比如提供安卓開發工具、或者基於硬體開發的服務等。也有針對某項軟體功能的SDK,如推送技術、影象識別技術、移動支付技術等,同時資源優勢類的公司也提供資源共享的SDK,如一些廣告SDK提供盈利渠道,分發SDK提供產品下載渠道。

注意:支付寶的SDK,採用了rsa加密演算法。對於傳送,返回的資料,它有一定的加密演算法以及解密演算法。

所以,我們要使用支付寶服務,傳送的資料,必須符合它的加密要求才行。有了SDK之後,我們不需要關心加密演算法,只需要關係業務邏輯就行!

開啟支付寶提供的SDK

https://docs.open.alipay.com/54/103419/

下載連結:

https://pypi.org/project/alipay-sdk-python/

不需要下載SDK!

不需要下載SDK!

不需要下載SDK!

這裡使用的是,根據官方SDK封裝好的一個python檔案,需要安裝依賴包pycryptodome

pip3 install pycryptodome

pay.py程式碼如下

from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from urllib.parse import quote_plus
from urllib.parse import urlparse, parse_qs
from base64 import decodebytes, encodebytes
import json


class AliPay(object):
"""
支付寶支付介面(PC端支付介面)
"""

def __init__(self, appid, app_notify_url, app_private_key_path,
alipay_public_key_path, return_url, debug=False):
self.appid = appid
self.app_notify_url = app_notify_url
self.app_private_key_path = app_private_key_path
self.app_private_key = None
self.return_url = return_url
with open(self.app_private_key_path) as fp:
self.app_private_key = RSA.importKey(fp.read())
self.alipay_public_key_path = alipay_public_key_path
with open(self.alipay_public_key_path) as fp:
self.alipay_public_key = RSA.importKey(fp.read())

if debug is True:
self.__gateway = "https://openapi.alipaydev.com/gateway.do"
else:
self.__gateway = "https://openapi.alipay.com/gateway.do"

def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
biz_content = {
"subject": subject,
"out_trade_no": out_trade_no,
"total_amount": total_amount,
"product_code": "FAST_INSTANT_TRADE_PAY",
# "qr_pay_mode":4
}

biz_content.update(kwargs)
data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)
return self.sign_data(data)

def build_body(self, method, biz_content, return_url=None):
data = {
"app_id": self.appid,
"method": method,
"charset": "utf-8",
"sign_type": "RSA2",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"version": "1.0",
"biz_content": biz_content
}

if return_url is not None:
data["notify_url"] = self.app_notify_url
data["return_url"] = self.return_url

return data

def sign_data(self, data):
data.pop("sign", None)
# 排序後的字串
unsigned_items = self.ordered_data(data)
unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)
sign = self.sign(unsigned_string.encode("utf-8"))
# ordered_items = self.ordered_data(data)
quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)

# 獲得最終的訂單資訊字串
signed_string = quoted_string + "&sign=" + quote_plus(sign)
return signed_string

def ordered_data(self, data):
complex_keys = []
for key, value in data.items():
if isinstance(value, dict):
complex_keys.append(key)

# 將字典型別的資料dump出來
for key in complex_keys:
data[key] = json.dumps(data[key], separators=(',', ':'))

return sorted([(k, v) for k, v in data.items()])

def sign(self, unsigned_string):
# 開始計算簽名
key = self.app_private_key
signer = PKCS1_v1_5.new(key)
signature = signer.sign(SHA256.new(unsigned_string))
# base64 編碼,轉換為unicode表示並移除回車
sign = encodebytes(signature).decode("utf8").replace("\n", "")
return sign

def _verify(self, raw_content, signature):
# 開始計算簽名
key = self.alipay_public_key
signer = PKCS1_v1_5.new(key)
digest = SHA256.new()
digest.update(raw_content.encode("utf8"))
if signer.verify(digest, decodebytes(signature.encode("utf8"))):
return True
return False

def verify(self, data, signature):
if "sign_type" in data:
sign_type = data.pop("sign_type")
# 排序後的字串
unsigned_items = self.ordered_data(data)
message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
return self._verify(message, signature)


這個pay.py就是sdk

支付寶金鑰處理體系

為了保證交易雙方(商戶和支付寶)的身份和資料安全,開發者在呼叫介面前,需要配置雙方金鑰,對交易資料進行雙方校驗。

整合和開發

在開始整合和開發前,首先了解一下常用的接入方式和架構建議:

其次,為了保證交易安全,支付寶採用了一系列的安全手段:

  1. 採用HTTPS協議傳輸交易資料,防止資料被截獲,解密。

  2. 採用RSA非對稱金鑰,明確交易雙方的身份,保證交易主體的正確性和唯一性

專案部署

新建一個django專案alipay,版本為django 2.x

在專案根目錄建立utils,將上面的pay.py複製到此目錄下

在專案根目錄建立keys

進入目錄secret_key_tools_RSA_win\RSA簽名驗籤工具windows_V1.4\RSA金鑰

將裡面的應用公鑰2048.txt,應用私鑰2048.txt這2個檔案,複製到keys目錄

python程式碼裡面的檔名,不要出現中文!

重新命名為英文!!!!!!!!

將這2個txt重新命名為alipay_public_2048.txt和app_private_2048.txt

注意:!!!!!!!!!!!!!!!!!!!!!!!!

alipay_public_2048.txt這個是公鑰,不能用本地的。要用網頁的!

點選檢視支付寶公鑰,不是應用公鑰!

複製裡面的公鑰

開啟alipay_public_2048.txt,必須增加頭部和尾部,將網頁複製的公鑰貼上到裡面!

-----BEGIN PUBLIC KEY-----
MIIBI...
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBI...
-----END PUBLIC KEY-----

效果如下:

一定要覆蓋公鑰!!!!!!!!!!!!!!!!!!!

開啟app_private_2048.txt,必須增加頭部和尾部

-----BEGIN RSA PRIVATE KEY-----
MIIE...
-----END RSA PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
MIIE...
-----END RSA PRIVATE KEY-----

效果如下:

注意:這2個txt檔案,必須為utf-8編碼,否則啟動django專案會報錯!

修改urls.py,增加路徑

from django.contrib import admin
from django.urls import path,re_path

from app01 import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.index),
path('index/', views.index),
re_path('buy/(?P<gid>\d+)/', views.buy),
path('check_order/', views.check_order),
re_path('show', views.show),
path('order_list/', views.order_list),
]

from django.contrib import admin
from django.urls import path,re_path

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
    path('index/', views.index),
    re_path('buy/(?P<gid>\d+)/', views.buy),
    path('check_order/', views.check_order),
    re_path('show', views.show),
    path('order_list/', views.order_list),
]

修改models.py,增加2個表

from django.db import models

# Create your models here.
class Goods(models.Model): # 商品
name = models.CharField(max_length=32,verbose_name="名稱")
price = models.FloatField(verbose_name="價格")


class Order(models.Model): # 訂單
no = models.CharField(max_length=64,verbose_name="訂單號")
goods = models.ForeignKey(to='Goods',on_delete=models.CASCADE,verbose_name="商品ID")
status_choices = (
(1,'未支付'),
(2,'已支付'),
)
status = models.SmallIntegerField(choices=status_choices,default=1,verbose_name="支付狀態")

from django.db import models

# Create your models here.
class Goods(models.Model):  # 商品
    name = models.CharField(max_length=32,verbose_name="名稱")
    price = models.FloatField(verbose_name="價格")


class Order(models.Model):  # 訂單
    no = models.CharField(max_length=64,verbose_name="訂單號")
    goods = models.ForeignKey(to='Goods',on_delete=models.CASCADE,verbose_name="商品ID")
    status_choices = (
        (1,'未支付'),
        (2,'已支付'),
    )
    status = models.SmallIntegerField(choices=status_choices,default=1,verbose_name="支付狀態")

修改views.py,增加檢視函式

注意:appid改成自己的,後6位我改成xx了

from django.shortcuts import render, HttpResponse, redirect
from app01 import models
import uuid
from utils.pay import AliPay # 匯入sdk


# Create your views here.
def index(request):
goods_list = models.Goods.objects.all()

return render(request, 'index.html', {'goods_list': goods_list})


# 全域性變數
alipay = AliPay(
appid="2016091700xxxxxx", # 注意,改成自己的!
app_notify_url="http://127.0.0.1:8000/check_order/", # POST,傳送支付狀態資訊
return_url="http://127.0.0.1:8000/show/", # GET,將使用者瀏覽器地址重定向回原網站
app_private_key_path="keys/app_private_2048.txt",
alipay_public_key_path="keys/alipay_public_2048.txt",
debug=True, # 預設True測試環境、False正式環境
)


def buy(request, gid):
"""
去購買並支付
:param request:
:param gid:
:return:
"""
obj = models.Goods.objects.get(pk=gid)

# 生成訂單(未支付)
no = str(uuid.uuid4())
models.Order.objects.create(no=no, goods_id=obj.id)

# 根據
# APPID
# 支付寶閘道器
# 公鑰和私鑰
# 生成要跳轉的地址
# 沙箱環境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info

query_params = alipay.direct_pay(
subject=obj.name, # 商品簡單描述
out_trade_no=no, # 商戶訂單號
total_amount=obj.price, # 交易金額(單位: 元 保留倆位小數)
)

pay_url = "https://openapi.alipaydev.com/gateway.do?{0}".format(query_params)

return redirect(pay_url)


def check_order(request):
"""
POST請求,支付寶通知支付資訊,我們修改訂單狀態
:param request:
:return:
"""
if request.method == 'POST':

from urllib.parse import parse_qs
body_str = request.body.decode('utf-8')
post_data = parse_qs(body_str)

post_dict = {}
for k, v in post_data.items():
post_dict[k] = v[0]
sign = post_dict.pop('sign', None)
status = alipay.verify(post_dict, sign)
if status:
# 支付成功,獲取訂單號將訂單狀態更新
out_trade_no = post_dict['out_trade_no']
models.Order.objects.filter(no=out_trade_no).update(status=2)
return HttpResponse('success')
else:
return HttpResponse('支援失敗')
else:
return HttpResponse('只支援POST請求')


def show(request):
"""
回到我們頁面
:param request:
:return:
"""

if request.method == "GET":
params = request.GET.dict()
sign = params.pop('sign', None)
status = alipay.verify(params, sign)
if status:
return HttpResponse('支付成功')
else:
return HttpResponse('失敗')
else:
return HttpResponse('只支援GET請求')


def order_list(request):
"""
檢視所有訂單狀態
:param request:
:return:
"""
orders = models.Order.objects.all()
return render(request, 'order_list.html', {'orders': orders})

from django.shortcuts import render, HttpResponse, redirect
from app01 import models
import uuid
from utils.pay import AliPay  # 匯入sdk


# Create your views here.
def index(request):
    goods_list = models.Goods.objects.all()

    return render(request, 'index.html', {'goods_list': goods_list})


# 全域性變數
alipay = AliPay(
    appid="2016091700xxxxxx",  # 注意,改成自己的!
    app_notify_url="http://127.0.0.1:8000/check_order/",  # POST,傳送支付狀態資訊
    return_url="http://127.0.0.1:8000/show/",  # GET,將使用者瀏覽器地址重定向回原網站
    app_private_key_path="keys/app_private_2048.txt",
    alipay_public_key_path="keys/alipay_public_2048.txt",
    debug=True,  # 預設True測試環境、False正式環境
)


def buy(request, gid):
    """
    去購買並支付
    :param request:
    :param gid:
    :return:
    """
    obj = models.Goods.objects.get(pk=gid)

    # 生成訂單(未支付)
    no = str(uuid.uuid4())
    models.Order.objects.create(no=no, goods_id=obj.id)

    # 根據
    #   APPID
    #   支付寶閘道器
    #   公鑰和私鑰
    # 生成要跳轉的地址
    # 沙箱環境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info

    query_params = alipay.direct_pay(
        subject=obj.name,  # 商品簡單描述
        out_trade_no=no,  # 商戶訂單號
        total_amount=obj.price,  # 交易金額(單位: 元 保留倆位小數)
    )

    pay_url = "https://openapi.alipaydev.com/gateway.do?{0}".format(query_params)

    return redirect(pay_url)


def check_order(request):
    """
    POST請求,支付寶通知支付資訊,我們修改訂單狀態
    :param request:
    :return:
    """
    if request.method == 'POST':

        from urllib.parse import parse_qs
        body_str = request.body.decode('utf-8')
        post_data = parse_qs(body_str)

        post_dict = {}
        for k, v in post_data.items():
            post_dict[k] = v[0]
        sign = post_dict.pop('sign', None)
        status = alipay.verify(post_dict, sign)
        if status:
            # 支付成功,獲取訂單號將訂單狀態更新
            out_trade_no = post_dict['out_trade_no']
            models.Order.objects.filter(no=out_trade_no).update(status=2)
            return HttpResponse('success')
        else:
            return HttpResponse('支援失敗')
    else:
        return HttpResponse('只支援POST請求')


def show(request):
    """
    回到我們頁面
    :param request:
    :return:
    """

    if request.method == "GET":
        params = request.GET.dict()
        sign = params.pop('sign', None)
        status = alipay.verify(params, sign)
        if status:
            return HttpResponse('支付成功')
        else:
            return HttpResponse('失敗')
    else:
        return HttpResponse('只支援GET請求')


def order_list(request):
    """
    檢視所有訂單狀態
    :param request:
    :return:
    """
    orders = models.Order.objects.all()
    return render(request, 'order_list.html', {'orders': orders})

增加index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
{% for row in goods_list %}
<li>{{ row.name }},價格:{{ row.price }} <a href="/buy/{{ row.id }}/">購買</a></li>
{% endfor %}
</ul>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <ul>
        {% for row in goods_list %}
            <li>{{ row.name }},價格:{{ row.price }}   <a href="/buy/{{ row.id }}/">購買</a></li>
        {% endfor %}
    </ul>
</body>
</html>

增加order_list.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
{% for item in orders %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.no }}</td>
<td>{{ item.goods.name }}</td>
<td>{{ item.get_status_display }}</td>
</tr>
{% endfor %}
</table>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <table border="1">
        {% for item in orders %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.no }}</td>
            <td>{{ item.goods.name }}</td>
            <td>{{ item.get_status_display }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>

修改settings.py,關閉csrf

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

專案結構如下:

alipay/
├── alipay
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── app01
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20180810_1623.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── db.sqlite3
├── keys
│ ├── alipay_public_2048.txt
│ └── app_private_2048.txt
├── manage.py
├── templates
│ ├── index.html
│ └── order_list.html
└── utils
└── pay.py

alipay/
├── alipay
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── app01
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_auto_20180810_1623.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── db.sqlite3
├── keys
│ ├── alipay_public_2048.txt
│ └── app_private_2048.txt
├── manage.py
├── templates
│ ├── index.html
│ └── order_list.html
└── utils
    └── pay.py

使用2個命令,生成表

python manage.py makemigrations
python manage.py migrate

使用navicat開啟sqlite3資料庫,增加幾條資料

INSERT INTO app01_goods ("id", "name", "price") VALUES (1, 'Apple iPad Pro', 5588.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (2, 'i5 8500', 1599.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (3, '拯救者Y7000', 7299.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (4, '大白兔奶糖', 1.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (1, 'Apple iPad Pro', 5588.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (2, 'i5 8500', 1599.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (3, '拯救者Y7000', 7299.0);
INSERT INTO app01_goods ("id", "name", "price") VALUES (4, '大白兔奶糖', 1.0);

啟動django專案,訪問首頁

本地測試支付

點選購買大白兔奶糖,它會自動跳轉到支付寶的支付頁面

注意:要沙箱環境的APP

它只提供了安卓版本,下載之後,進行安裝

點選沙箱賬號

它提供了商家資訊和買家資訊

預設的買家資訊,賬號有9萬多塊。注意:這個錢是虛擬的,不能體現!

如果錢不夠了,可以輸入任意金額進行充值!

手機登入買家賬號,密碼為111111

開啟掃一掃,進行支付

提示支付成功

注意:如果出現失敗,可能是公鑰錯了!一定是網頁的支付寶公鑰,不是本地的公鑰!

檢視買家資訊,少了一塊錢

檢視商家資訊,多了一塊錢。那一分錢,是扣了手續費!

注意:這個是本地環境測試的。

如果需要伺服器測試,請修改views.py中的全域性變數,將127.0.0.1改為伺服器的公網IP即可!

檢視訂單狀態,這裡的狀態是未支付。為什麼呢?因為支付寶要傳送一個POST請求到

http://127.0.0.1:8000/check_order/ 才能修改狀態。由於在本地,支付寶無法訪問此地址!

注意:商品價格,必須保證最多為小數點2位。這個是支付寶規定的!

線上測試支付

將程式碼上傳到公網伺服器,修改views.py,將裡面的127.0.0.1改成公網IP。

再次支付一次,檢視訂單狀態。狀態為已支付

完整程式碼,請引數github

https://github.com/987334176/alipay

總結:

1. 什麼是SDK?
    - 第三方服務商提供的實現軟體產品某項功能的工具包
    
2. 沙箱環境是什麼?
    - 是測試環境

3. 簡述支付寶支付流程:
    - 使用者點選支援,根據支付寶的AppID/閘道器/公鑰私鑰/SDK生成地址
    - 使用者跳轉到支付寶
    - 支付成功之後,支付寶會給我傳送兩個請求:
        - GET, 從支付寶網站跳轉回自己網站。
        - POST,支付傳送支付資訊(修改訂單狀態)
    
4. 支付寶金額的精度?
    - 小數點後兩位

5. 支付寶用的什麼加密?
    - RSA

6. 支付完成之後,伺服器宕機了,怎麼辦?
    - 24小時內,支付依然傳送通知。