一. 快速連接通道
1. 支付寶
1)支付寶API:六大接口
https://docs.open.alipay.com/270/105900/
2)支付寶工作流程(見下圖):
https://docs.open.alipay.com/270/105898/
3)支付寶8次異步通知機制(支付寶對我們服務器發送POST請求,索要 success 7個字符)
https://docs.open.alipay.com/270/105902/
2. 沙箱環境
1) 在沙箱環境下實名認證
https://openhome.alipay.com/platform/appDaily.htm?tab=info
2) 電腦網站支付API
https://docs.open.alipay.com/270/105900/
3) 完成RSA密鑰生成
https://docs.open.alipay.com/291/105971
4) 在開發中心的沙箱應用下設置應用公鑰
填入生成的公鑰文件中的內容
5) Python支付寶開源框架
https://github.com/fzlee/alipay
pip install python-alipay-sdk --upgrade
6) 公鑰私鑰設置
"""
# alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
支付寶公鑰
-----END PUBLIC KEY-----
# app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
用戶私鑰
-----END RSA PRIVATE KEY-----
"""
7) 支付寶鏈接
開發:https://openapi.alipay.com/gateway.do
沙箱:https://openapi.alipaydev.com/gateway.do
二. 支付流程圖

三. 支付寶接入入門
1. 流程
'''
# 支付寶開放平台
1. 服務范圍(自研開發服務) -> 實名認證
2. 控制台 -> 我的應用 -> 創建應用 -> 網頁&移動應用 -> 支付接入 -> 應用名稱 -> 應用圖標 ->
1) 移動應用 -> 應用平台 -> Bundle ID ...
2) 網頁應用 (不成功. 需要使用營業執照) -> 網址url -> 簡介
注意: 先選擇功能再審核
能力列表:添加能力 -> 支付能力 -> 電腦網站支付
開發設置:
加簽管理 -> 公鑰 -
支付寶網關
應用網關
授權回調地址
3. 文檔 -> 網頁 & 移動應用 接口文檔能力列表
1) 開放能力:
支付能力 -> 電腦網站支付
2) 產品介紹:
注意: 會跳到支付寶的頁面, 支付寶會有一個get頁面回調, post數據返回后端回調
費率: 0.6%
3) 快速接入:
SDK快速接入: python沒有, 只能使用API開發
支付流程: 下單 -> 商戶系統 -> 支付寶 -> 回調(get顯示訂單結果, post修改訂單狀態)
4) 支付API:
公共請求參數
請求參數
訂單號 out_trade_no
總金額 total_amount
訂單標題 subjet
公共響應參數
支付寶交易號 trade_no
我們的訂單號 out_trade_no
5) GitHub開源SDK
pip install python-alipay-sdk
# 支付寶沙箱環境
1. 沙箱環境地址: https://openhome.alipay.com/platform/appDaily.htm
2. 沙箱應用:
APPID
支付寶網關: 地址中帶dev表示沙箱環境, 不帶表示正式環境
加密方式: 使用支付寶提供的密鑰生成(支付寶開放平台組助手).
之前是xx.jar包, 現在變成xx.exe軟件. 需要生成公鑰和私鑰
將自己的公鑰配置在支付寶中, 支付寶會生成一個支付寶的公鑰.
3. 項目中使用:
注釋 .read這里是操作文件的
app_private_key_string 配置自己的私鑰
alipay_public_key_string 配置支付寶的公鑰
注意: 不能有空格
AliPay類中的參數配置:
APPID配置 沙箱環境的APPID
sign_type 配置自己的 RSA2
debug=False測試環境, True正式環境
alipay.api_alipay_trade_page_pay中的參數配置:
out_trade_no 配置自己的商品訂單號
total_amount 總金額
subject 訂單標題
return_url 回調地址 (注意: 需要使用公網地址)
notify_url 回調地址
支付寶網關 + order_string => 生成連接地址
提示: 生成連接地址打開會出現釣魚網站異常
4. 解決提示釣魚問題: 瀏覽器里面有多個窗口
沙箱環境存在的問題, 如果出現問題, 開無痕窗口即可, 付完之后會回調到之前配置的return_url中配置的網頁
支付寶沙箱環境充值:
控制台 -> 沙箱賬號 -> 賬戶余額
# 支付寶公私密鑰生成, sdk使用
支付寶開放平台組助手使用: 生成公私鑰
支付寶開放平台下載:https://ideservice.alipay.com/ide/getPluginUrl.htm?clientType=assistant&platform=win&channelType=WEB
密鑰長度: RSA2
密鑰格式: PKCS1
生成即可
GitHub開源SDK:
支付寶開源框架地址: https://github.com/fzlee/alipay
pip install python-alipay-sdk
# 拓展:
xx.apk 如果apk使用QQ 或者 微信傳送, 它會改名, 再后面加個.1 -> xx.apk.1. 目的就是防止惡意軟件.
如果你需要安裝, 只需要將后綴名修改過來即可
'''
2. 測試目錄結構
3. test_alipay.py
from alipay import AliPay
app_private_key_string = """-----BEGIN rsa2 PRIVATE KEY-----
MIIEowIBAAKCAQEAr6my/KRUtoPcQzuBt8TZtxLvLtwI8Rf/ETubH6dfi143yuiHd0SnfTctD+ZTmGyRHxuqNwwTNV4CN0d58wuI2F3hky4Tm8ocp8n0tzjlYxDvoh1b4d4ksxXCM0yhSzywdIK+K+Y9VP74uU4mlT47oBFUs6TBK9AAlMfZfoPTUAUjSDF3usUE0IvkbKyv4Yd/cD0Stnqrl5qzplBjrA7H0HSRbw5nrk8Pj8aWnZYAayq3aGCJZxc+UfLKo8rfhV3GY6Tu29cTvTX2K69TZLQYPHkH1+8nLtwQywuWioXHOWwac6fE3270XR41xaUHo9avS48Gr4HNdkTAUtvq6YmfAwIDAQABAoIBAC8AuWPglMpBfi5/PbZuddMGvflL5xib0yRJTripkGc6TrN8hMLlG+vlV6lpd/TRGAO641DXakxdWzpvZbIi4/sBI9q9+YE2E3TSFSjxkG9xmK1ILc3CIw/IQq53UrFPC+ghE8GrWb3ke6kZwDku7cVm3cMz0nxmq8EjuI6ht2kxhUKzdsE9Z1DMOHWiEZSM6dvNMW/axToM2UawwPsermbTa++IJujvzjKH0UJORBqVnC6Ar2tPbEOJ9wbybVihnpHTyaCCWq2XDx/nEkOWeW8oeRp41uXrJf7iaHbYFYA2GlvQT2vMkFU5qKPzKYJDdBZdvlN0dEpEUstx4WYcSAECgYEA1QP1Vr54PFuh7KWxkV7WJz1iZyNcyPHWG4XjzTifK4uTPyciabDhUO1aPmnd7I2GvaRwyCWAZQSn7bIV9rumebEWzMWbcttm2e/iL7lzE1Aj8Ank8pXXaIXQHE+4yUfdE3jsHcTq99EzIY+K7Js4LckvR9sCWiM9J6Za1TuO9WkCgYEA0xwqePmrfYE965quZpbNZZ5uGO6AJZ0YZlK69RwVT5NV1fmdLsqk/4jgJKrZnnspt16cKjDJ5DyeGH4uikL1F6AReDbCPSVJjWqo50Al98K6WDqf6HhJnz+A+dxtgzHbQKUWlWiWuqbeB6ITXnVG0mrBi6jHR247fkh3FkOAh4sCgYEAriN2RVugX3dpgFRUPUsSNzHvZ/F4wK0zI3zpJbPMK4UG8vHDKDP5fncK90sEqYVpSU9NA9HkjLCpt5+GZRYymfkzcmN5GQRTqIZ6mhk5AejZ+DmeeNIWLtR1tS9XGPUuveR04kFA9SaIbj8yiwq5enSlulBIM/fq3qcYSolN7UECgYA3MiMMtEKZMuRsqGm22vDjA9RHYnxQ2U0a28CT+3666ovDwVrOdB9FzJTGIYF6hTs3/V2ZTl5K9WpkfwFOFwmb3rcSlkac1BXyCpQUulny+I/eJ53Nmz2sjF79dRuQ9MUdlsxbzheyv5RHrKGhzcnxlAX8rOlFjNWzQ+EXChkd1wKBgG/B0o8VeajSxcOqlAJZbbcnKzX3V/Fea6TPxUpzG7HCBL0y7xMSt+KtBBQzD+0GRIkwZqhsHw1tCzvTTI74z3DiSMy95atflE9PIyRriKaMiUfSALw6UA5okEis56IGmY5mSZ60O3z+mONjRXXBCeRIjJAd0UByVBjcUoNxABe3
-----END rsa2 PRIVATE KEY-----"""
alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgvXw19HTUH0t1thzkoq8KBhDBwFCoDqRJyBYnpN/KOTxTuSoUR0+pLK3vJbeQ0w5GJ/tiHpLh38hc88LNSR5nk26IBXX8WuNmxxC56d/A4/AaqiO3xgs9jKZjvYs0xuaFkwLswMuD8vm3xh6/YCD97EPkDqMY6aqbBdjHv8wOZ2Y/X6uFkANValvx2x+Lf8vSO+I2Iyq0sDmdCFS8LdnKgN5L8GoR1WkorgY6sTs2eV86acb95iAZC+7fVpVWzpX6yxBOL7hDIFDNDXCXhhWnsR3HfILNOMw84/jXdUKnXQIYYGCkSOQlK2hSB8/DtJaOoBTMrvpa29SCeIGvxFJRQIDAQAB
-----END PUBLIC KEY-----"""
alipay = AliPay(
appid="2021000117620642",
app_notify_url=None, # 默認回調url
app_private_key_string=app_private_key_string,
# 支付寶的公鑰,驗證支付寶回傳消息使用,不是你自己的公鑰,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # rsa2 或者 RSA2
debug=False # 默認False
)
# 如果你是 Python 3的用戶,使用默認的字符串即可
subject = "測試訂單"
# 電腦網站支付,需要跳轉到https://openapi.alipay.com/gateway.do? + order_string
alipay_url = 'https://openapi.alipaydev.com/gateway.do?'
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no="20161112", # 訂單號, 必須唯一
total_amount=10, # 總金額
subject=subject, # 訂單標題
return_url="http://139.196.184.91/", # 同步回調(支付成功)
notify_url="http://139.196.184.91/" # 異步回調(訂單狀態) 可選, 不填則使用默認notify url
)
print(alipay_url + order_string)
4. 注意事項
'''
支付寶的8次異步回調:
網頁&移動支付 -> 支付能力 -> 電腦網站支付 -> 快速接入 -> 支付結果異步通知 8次異步回調 'succes s'
重要: 異步回調的驗簽, 一定要在驗簽完畢以后再修改訂單狀態!!!
seller_id 賣家id號
biyder_id 賣家的id號
receipt
提示: APPID號可以再支付界面查出訂單的名字
'''
四. 支付寶二次封裝
1. GitHub開源框架參考
https://github.com/fzlee/alipay
2. 調用支付寶支付SDK依賴包下載
pip install python-alipay-sdk --upgrade
# 課程出現的錯誤解決: 拋ssl相關錯誤,代表缺失該包
pip install pyopenssl
3. 流程
'''
1. libs中新建文件, 文件中新建__init__.py, 新建.py文件
2. 將之前寫死的 app...string 等, 修改成從文件中讀取 open().read()
3. 新建文件夾存放支付寶公鑰和自己的私鑰用於被第二步讀取
公鑰私鑰存放的文件格式是:
-----xxx-----
公鑰 或者 私鑰
-----xxx-----
4. 新建settings.py文件存放一些常量
5. debug 配置成和 setting.py中的debug一直性
6. 使用三元運算配置支付寶的支付網關
7. 使用__init__.py優化導入的層級
注意: 網站支付alipay.api_alipay_trade_page_pay放到外面書寫和訂單一起.
'''
4. 目錄結構
libs
├── iPay # aliapy二次封裝包
│ ├── __init__.py # 包文件
│ ├── pem # 公鑰私鑰文件夾
│ │ ├── alipay_public_key.pem # 支付寶公鑰文件
│ │ ├── app_private_key.pem # 應用私鑰文件
│ ├── pay.py # 支付文件
└── └── settings.py # 應用配置
5. rsa2/alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgvXw19HTUH0t1thzkoq8KBhDBwFCoDqRJyBYnpN/KOTxTuSoUR0+pLK3vJbeQ0w5GJ/tiHpLh38hc88LNSR5nk26IBXX8WuNmxxC56d/A4/AaqiO3xgs9jKZjvYs0xuaFkwLswMuD8vm3xh6/YCD97EPkDqMY6aqbBdjHv8wOZ2Y/X6uFkANValvx2x+Lf8vSO+I2Iyq0sDmdCFS8LdnKgN5L8GoR1WkorgY6sTs2eV86acb95iAZC+7fVpVWzpX6yxBOL7hDIFDNDXCXhhWnsR3HfILNOMw84/jXdUKnXQIYYGCkSOQlK2hSB8/DtJaOoBTMrvpa29SCeIGvxFJRQIDAQAB
-----END PUBLIC KEY-----
6. rsa2/app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAr6my/KRUtoPcQzuBt8TZtxLvLtwI8Rf/ETubH6dfi143yuiHd0SnfTctD+ZTmGyRHxuqNwwTNV4CN0d58wuI2F3hky4Tm8ocp8n0tzjlYxDvoh1b4d4ksxXCM0yhSzywdIK+K+Y9VP74uU4mlT47oBFUs6TBK9AAlMfZfoPTUAUjSDF3usUE0IvkbKyv4Yd/cD0Stnqrl5qzplBjrA7H0HSRbw5nrk8Pj8aWnZYAayq3aGCJZxc+UfLKo8rfhV3GY6Tu29cTvTX2K69TZLQYPHkH1+8nLtwQywuWioXHOWwac6fE3270XR41xaUHo9avS48Gr4HNdkTAUtvq6YmfAwIDAQABAoIBAC8AuWPglMpBfi5/PbZuddMGvflL5xib0yRJTripkGc6TrN8hMLlG+vlV6lpd/TRGAO641DXakxdWzpvZbIi4/sBI9q9+YE2E3TSFSjxkG9xmK1ILc3CIw/IQq53UrFPC+ghE8GrWb3ke6kZwDku7cVm3cMz0nxmq8EjuI6ht2kxhUKzdsE9Z1DMOHWiEZSM6dvNMW/axToM2UawwPsermbTa++IJujvzjKH0UJORBqVnC6Ar2tPbEOJ9wbybVihnpHTyaCCWq2XDx/nEkOWeW8oeRp41uXrJf7iaHbYFYA2GlvQT2vMkFU5qKPzKYJDdBZdvlN0dEpEUstx4WYcSAECgYEA1QP1Vr54PFuh7KWxkV7WJz1iZyNcyPHWG4XjzTifK4uTPyciabDhUO1aPmnd7I2GvaRwyCWAZQSn7bIV9rumebEWzMWbcttm2e/iL7lzE1Aj8Ank8pXXaIXQHE+4yUfdE3jsHcTq99EzIY+K7Js4LckvR9sCWiM9J6Za1TuO9WkCgYEA0xwqePmrfYE965quZpbNZZ5uGO6AJZ0YZlK69RwVT5NV1fmdLsqk/4jgJKrZnnspt16cKjDJ5DyeGH4uikL1F6AReDbCPSVJjWqo50Al98K6WDqf6HhJnz+A+dxtgzHbQKUWlWiWuqbeB6ITXnVG0mrBi6jHR247fkh3FkOAh4sCgYEAriN2RVugX3dpgFRUPUsSNzHvZ/F4wK0zI3zpJbPMK4UG8vHDKDP5fncK90sEqYVpSU9NA9HkjLCpt5+GZRYymfkzcmN5GQRTqIZ6mhk5AejZ+DmeeNIWLtR1tS9XGPUuveR04kFA9SaIbj8yiwq5enSlulBIM/fq3qcYSolN7UECgYA3MiMMtEKZMuRsqGm22vDjA9RHYnxQ2U0a28CT+3666ovDwVrOdB9FzJTGIYF6hTs3/V2ZTl5K9WpkfwFOFwmb3rcSlkac1BXyCpQUulny+I/eJ53Nmz2sjF79dRuQ9MUdlsxbzheyv5RHrKGhzcnxlAX8rOlFjNWzQ+EXChkd1wKBgG/B0o8VeajSxcOqlAJZbbcnKzX3V/Fea6TPxUpzG7HCBL0y7xMSt+KtBBQzD+0GRIkwZqhsHw1tCzvTTI74z3DiSMy95atflE9PIyRriKaMiUfSALw6UA5okEis56IGmY5mSZ60O3z+mONjRXXBCeRIjJAd0UByVBjcUoNxABe3
-----END RSA PRIVATE KEY-----
7. __init__.py
from .alipay_task import (
alipay, alipay_gateway
)
8. alipay_task.py
from alipay import AliPay
from . import settings
alipay = AliPay(
appid=settings.APPID,
app_notify_url=settings.APP_NOTIFY_URL, # 默認回調url
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
# 支付寶的公鑰,驗證支付寶回傳消息使用,不是你自己的公鑰,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN_TYPE, # rsa2 或者 RSA2
debug=settings.DEBUG # 默認False
)
alipay_gateway = settings.ALIPAY_GATEWAY
9. settings.py
import os
APPID = '2021000117620642'
# 默認回調
APP_NOTIFY_URL = None
# 阿里公鑰
ALIPAY_PUBLIC_KEY_FILE_PATH = os.path.join(os.path.dirname(__file__), 'rsa2', 'alipay_public_key.pem')
ALIPAY_PUBLIC_KEY_STRING = open(ALIPAY_PUBLIC_KEY_FILE_PATH).read()
# 自己私鑰
APP_PRIVATE_KEY_FILE_PATH = os.path.join(os.path.dirname(__file__), 'rsa2', 'app_private_key.pem')
APP_PRIVATE_KEY_STRING = open(APP_PRIVATE_KEY_FILE_PATH).read()
# 標簽加密類型
SIGN_TYPE = "RSA2"
# True表示測試沙箱環境
DEBUG = True
# 阿里網關
ALIPAY_GATEWAY = "https://openapi.alipaydev.com/gateway.do" if DEBUG else "https://openapi.alipay.com/gateway.do"
10. 配置文件中配置支付寶替換接口:settings.py | 開發人員
# 后台基URL
BASE_URL = 'http://139.196.184.91:8000' # 注意: 這里的8000上線以后指定的nginx的8000端口, 由nginx的8000端口發送到nginx配置內部的uwsgi的端口中
# 前台基URL
LUFFY_URL = 'http://139.196.184.91' # 注意: 這里沒有寫端口默認就是80端口.
# 支付寶同步異步回調接口配置
# 后台: 支付寶異步回調的接口
NOTIFY_URL = BASE_URL + "/order/success/"
# 前台: 支付寶同步回調接口,沒有 / 結尾
RETURN_URL = LUFFY_URL + "/pay/success"
五. 后台-支付接口
1. 訂單模塊表
1) 流程
'''
1. 新建訂單app, 注冊, 子路由urls, 總路由分發,
2. 表分析
訂單表:
訂單標題, 總價格, 訂單id(自己的), 流水號(支付寶), 訂單狀態, 支付方式, 支付時間, 訂單用戶(注意: 導入用戶表路徑盡量小), 創建時間, 更新時間
訂單詳情表:
訂單一對多外鍵, 課程一對多外鍵(級聯刪除改為Set_NULL, null=True), 原價格, 實價
str的健壯性校驗
訂單和訂單詳情表關系分析: 一對多 訂單詳情是多的一方 一個訂單可以有多個訂單詳情, 一個訂單詳情不可以同時屬於多個訂單.
訂單表和課程表關系分析: 多對多 一個訂單可以包含多個課程, 一個課程可以屬於多個訂單
重點: 但是我們這里不着不過對訂單表與課程表建立多對多的關系,而是通過訂單詳情表與課程表建立關系.
訂單詳情表和課程表關系分析: 一對多 訂單詳情是多的一方 訂單詳情多的一方 一個訂單詳情不可以屬於多個課程, 而一個課程可以屬於多個訂單詳情
訂單表和用戶表關系分析: 一對多 訂單是多的一方 一個用戶可以下多個訂單, 一個訂單不能屬於多個用戶
on_delete -> DO_NOTHING
db_constraint=False
提示: 不繼承BaseModel表. is_show, orders沒有必要存在
3. 數據遷移
'''
2) order/models.py
"""
class Order(models.Model):
# 主鍵、總金額、訂單名、訂單號、訂單狀態、創建時間、支付時間、流水號、支付方式、支付人(外鍵) - 優惠劵(外鍵,可為空)
pass
class OrderDetail(models.Model):
# 訂單號(外鍵)、商品(外鍵)、實價、成交價 - 商品數量
pass
"""
from django.db import models
from user.models import User
from course.models import Course
import utils
class Order(models.Model):
"""訂單模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超時取消'),
)
pay_choices = (
(1, '支付寶'),
(2, '微信支付'),
)
subject = models.CharField(max_length=150, verbose_name="訂單標題")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="訂單總價", default=0)
out_trade_no = models.CharField(max_length=64, verbose_name="訂單號", unique=True)
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水號")
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="訂單狀態")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
pay_time = models.DateTimeField(null=True, verbose_name="支付時間")
created_time = models.DateTimeField(auto_now_add=True, verbose_name='創建時間')
updated_time = models.DateTimeField(auto_now=True, verbose_name='更新時間')
# 訂單表和用戶表關系分析: 一對多 訂單是多的一方 一個用戶可以下多個訂單, 一個訂單不能屬於多個用戶
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
verbose_name="下單用戶")
class Meta:
db_table = "luffy_order"
verbose_name = "訂單記錄"
verbose_name_plural = "訂單記錄"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
@property
def courses(self):
data_list = []
for item in self.order_courses.all():
data_list.append({
"id": item.id,
"course_name": item.course.name,
"real_price": item.real_price,
})
return data_list
class OrderDetail(models.Model):
"""訂單詳情"""
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程原價")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程實價")
# 訂單和訂單詳情表關系分析: 一對多 訂單詳情是多的一方 一個訂單可以有多個訂單詳情, 一個訂單詳情不可以同時屬於多個訂單.
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
verbose_name="訂單")
# 訂單詳情表和課程表關系分析: 一對多 訂單詳情是多的一方 訂單詳情多的一方 一個訂單詳情不可以屬於多個課程, 而一個課程可以屬於多個訂單詳情
'''
訂單表和課程表關系分析: 多對多 一個訂單可以包含多個課程, 一個課程可以屬於多個訂單
!!!重點!!!: 但是我們這里不着不過對訂單表與課程表建立多對多的關系,而是通過訂單詳情表與課程表建立關系.
'''
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.SET_NULL, null=True,
db_constraint=False,
verbose_name="課程")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "訂單詳情"
verbose_name_plural = "訂單詳情"
def __str__(self):
"""str的健壯性校驗"""
try:
return "%s的訂單:%s" % (self.course.name, self.order.out_trade_no)
except Exception as e:
utils.log.error(str(e))
return super().__str__()
2. 訂單模塊接口之支付接口
1) 流程
'''
1. 支付接口: 生成訂單, 生成支付連接, 返回支付連接
1) 新建路由pay, payView
2) 新建視圖payView
# 思路
order表和orderdetail表插入數據, 重寫create方法.
生成訂單號 uuid
登錄后才能支付 jwt認證
當前登錄用戶就是下單用戶, 存到order表中
訂單價格校驗. 如: 下了三個課程, 總價格100, 前端提交的價格是99
# 實現
繼承 C, G
新建序列化類 OrderModelSeriailzer
注意: 這是一個反序列化的表
# 傳輸的數據格式
{course: [1, 2, 3], total_amount: 100, subject: 商品名, pay_type: 1}
# 控制字段
fields=['total_amount', 'subject', 'pay_type', 'course_list']
# 可以再局部鈎子中把course=[1, 2, 3]生成course=[obj1, obj2, obj3] 或者使用 PrimayKeyRElatedField
course=serialisers.CharField()
# 校驗
1. 校驗訂單總價格: 獲取總價格, 獲取課程對象列表從總價格列表中獲取每個價格疊加與總價格對比 (注意: 需要返回總價格)
2. 生成訂單號: str(uuid).replace('-', '')
3. 獲取支付用戶: 視圖中重寫create方法借助self.context傳將request對象傳給序列化類
4. 生成支付連接: 導入alipay, alipay_gateway. 拷貝, 將post, get2個回調的地址存放到配置文件中(配置到django的配置文件中), 拼接地址返回即可!
5. 入庫(訂單, 訂單詳情): 將user對象存入attrs中, 把訂單號存入attrs中, 將pay_url存入self.context中
6. create方法. 先pop出課程列表對象, 存order表. for循環存入課程詳情
視圖中: Response返回給前端的, 前端只需要一個連接, 那么序列化校驗的第五步, 在self.context中將它存入, 將它返回給前端
3) 配置jwt認證
對PayView類進行限制. 使用內置限制(認證 + 權限)
內置認證類: JSONWebTokenAUthentication
內置權限類: isAuthenticated
4) 序列化中讓所有的fields中的字段必填. 有默認值的字段, 就不是必填的. required=True
5) 出現錯誤: 支付寶支付的時候pay_total_amount是一個decimal類型, 需要轉換成float類型. (提示: decimal累加可以)
提示: 支付方式目前只寫了支付寶的支付方式因此pay_type=1, 3個課程一起買一共138
2. 支付寶異步回調的post接口: 驗簽, 修改訂單狀態
3. 當支付寶get回調前端, vue組件一創建, 立馬向后端你發一個get請求.(比較繞)
'''
2) order/views.py
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework import status
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
import utils
from . import models
from . import serializer
class PayView(CreateModelMixin, GenericViewSet):
# 對PayView類進行限制. 使用內置限制(認證 + 權限)
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated]
queryset = models.Order.objects.all()
serializer_class = serializer.OrderModelSeriailzer
def create(self, request, *args, **kwargs):
# 視圖中重寫create方法借助self.context傳將request對象傳給序列化類
serializer = self.get_serializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
# 視圖中: Response返回給前端的, 前端只需要一個連接, 那么序列化校驗的第五步, 在self.context中將它存入, 將它返回給前端
return utils.APIResponse(serializer.context['pay_link'], status=status.HTTP_201_CREATED, headers=headers)
3) order/serializer.py
import uuid
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from django.conf import settings
from . import models
from libs.alipay_sdk import alipay, alipay_gateway
class OrderModelSeriailzer(serializers.ModelSerializer):
# 可以再局部鈎子中把course_list=[1, 2, 3]生成course_list=[obj1, obj2, obj3] 或者使用 PrimayKeyRElatedField
course_list = serializers.PrimaryKeyRelatedField(write_only=True, many=True, queryset=models.Course.objects.all())
class Meta:
model = models.Order
fields = ['subject', 'total_amount', 'pay_type', 'course_list']
extra_kwargs = {
# 序列化中讓所有的fields中的字段必填. 有默認值的字段, 就不是必填的. required=True
'total_amount': {'required': True},
'pay_type': {'required': True},
}
@staticmethod
def _verify_amount(attrs):
total_amount = attrs.get('total_amount')
course_list = attrs.get('course_list')
course_amount = 0
for course in course_list:
course_amount += course.price
if course_amount == total_amount:
return total_amount
raise ValidationError("訂單總價錯誤!")
@staticmethod
def _order_number():
return str(uuid.uuid1()).replace('-', '')
def _pay_user(self):
return self.context['request'].user
def _pay_link(self, out_trade_no, total_amount, subject):
# print('total_amount:', total_amount, type(total_amount)) # total_amount: 138.00 <class 'decimal.Decimal'>
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=out_trade_no, # 訂單號, 必須唯一
# 支付寶支付的時候pay_total_amount是一個decimal類型, 需要轉換成float類型. (提示: decimal累加可以)
# 如果不轉換成float那么格式就會拋出異常: Object of type 'Decimal' is not JSON serializable
# total_amount=total_amount, # 總金額
total_amount=float(total_amount), # 總金額
subject=subject, # 訂單標題
return_url=settings.RETURN_URL, # 同步回調(支付成功)
notify_url=settings.NOTIFY_URL # 異步回調(訂單狀態) 可選, 不填則使用默認notify url
)
return alipay_gateway + order_string
def _before_create(self, attrs, out_trade_no, user, pay_link):
attrs['out_trade_no'] = out_trade_no
attrs['user'] = user
self.context['pay_link'] = pay_link
def validate(self, attrs):
"""
1. 校驗訂單總價格: 獲取總價格, 獲取課程對象列表從總價格列表中獲取每個價格疊加與總價格對比 (注意: 需要返回總價格)
2. 生成訂單號: str(uuid).replace('-', '')
3. 獲取支付用戶: 視圖中重寫create方法借助self.context傳將request對象傳給序列化類
4. 生成支付連接: 導入alipay, alipay_gateway. 拷貝, 將post, get2個回調的地址存放到配置文件中(配置到django的配置文件中), 拼接地址返回即可!
5. 入庫(訂單, 訂單詳情): 將user對象存入attrs中, 將pay_link存入self.context中
"""
# 1. 校驗訂單總價格
total_amount = self._verify_amount(attrs)
# 2. 生成訂單號
order_number = self._order_number()
# 3. 獲取支付用戶
user = self._pay_user()
# 4. 生成支付連接
pay_link = self._pay_link(out_trade_no=order_number, total_amount=total_amount, subject=attrs.get('subject'))
# 5. 入庫(訂單, 訂單詳情)
self._before_create(attrs=attrs, out_trade_no=order_number, user=user, pay_link=pay_link)
return attrs
def create(self, validated_data):
course_list = validated_data.pop('course_list')
order = models.Order.objects.create(**validated_data)
for course in course_list:
models.OrderDetail.objects.create(course=course, price=course.price, real_price=course.price, order=order)
return order
4) settings/dev.py
# 后台基URL
BASE_URL = 'http://139.196.184.91'
# 前台基URL
LUFFY_URL = 'http://139.196.184.91'
# 支付寶同步異步回調接口配置
# 后台異步回調接口
NOTIFY_URL = BASE_URL + "/order/success/"
# 前台同步回調接口,沒有 / 結尾
RETURN_URL = LUFFY_URL + "/pay/success"
5) luffyapi/urls.py 總路由
path('order/', include('order.urls')),
6) order/urls.py 子路由
from django.urls import path, re_path, include
from . import views
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('pay', views.PayView, 'pay')
urlpatterns = [
path('', include(router.urls)),
]
六. 前台-支付生成頁面
1. 前端跳轉到支付寶支付
1) 流程
'''
提示: 一共三個地方都有立即購買操作
1. FreeCourse.vue
1) 定義buy_now()點擊觸發事件的方法
從this.$cookies中獲取token
判斷如果沒有token那么觸發this.$message
發送ajax的post請求, this.$settings.base_url + /order/pay/, headers需要攜帶認證 Authorization, data需要攜帶對着數據. 使用另一種用法{}
獲取到pay_link, 前端發送get請求
window.open(pay_link, '_self')
2) 付款成功以后需要跳轉到/order/success頁面, 前端需要success組件. 后端需要success接口
'''
2) FreeCourse.vue
# template
<span class="buy-now" @click="buy_now(course)">立即購買</span>
# script
methods: {
buy_now(course) {
// 獲取token, 校驗用戶是否登錄
let token = this.$cookies.get('token');
if (!token) {
this.$message({
message: "請先登錄!",
type: 'warning',
});
return false;
}
// 發送axios
this.$axios({
method: 'post',
url: `${this.$settings.base_url}/order/pay/`,
data: {
"subject": course.name,
// "total_amount": 11,
"total_amount": course.price,
"pay_type": 1,
"course_list": [
course.id,
]
},
headers: {
Authorization: `jwt ${this.$cookies.get('token')}`
},
}).then(response => {
console.log(response.data);
if (response.data.code) {
open(response.data.data, '_self');
} else {
this.$message({
message: '訂單處理失敗!',
type: 'warning',
})
}
}).catch(error => {
this.$message({
message: "未知錯誤!",
type: 'warning',
})
})
},
...
}
2. 支付成功前端頁面
1) 流程
'''
1. 新建PaySuccess.vue組件
2. 配置路由 path: '/pay/success'
注意: 回調以后會在你的url地址中, 攜帶者很多東西
3. 拷貝PaySuccess頁面
提示: 頁面只有支付寶回調回來才有數據, 直接查看是沒有的
4. create里面有一種特殊用法
5. 同步回調參數
trade_no 支付寶的流水號
auth_app_id 商家流水號
app_id 我們的id號
頁面需要的參數: 訂單號, 交易號, 付款時間
'''
2) router/index.js
import PaySuccess from '../views/PaySuccess.vue'
const routes = [
...
{
path: '/pay/success',
name: 'PaySuccess',
component: PaySuccess
},
];
3) 同步理論的參數
charset=utf-8&
out_trade_no=7f7c7d12d57d45b693e1b49a6b01e1dd& # 自己的訂單號
method=alipay.trade.page.pay.return&
total_amount=39.00&
sign=FUmceqiNMWvxcD%2BUPCHiOTaEwlJ%2FXIXL5UwZWOSI1TwRjPIZVzjRLB4j2G5CQpn472JO8X%2BwMx04dHqjLxqLcY3TRu0XurQ%2FwKTNpyfDrtNuNv0rfGPuVHw52y3blbS7%2FKFVsWryw4%2BBuF2fCrJ4qWH8Zg14Rct7qoMbu73N74WkQtDyzXefiKDbkMMRMfLbelE9TFyeIeygeMId8%2B58mcJMUOh6aQqwpr9bzuBbfJ17fkqU%2F0ys9zGr%2FlDtLL7aAh6BPViqZN%2F9T7byCoferD1BhcSzJNR6V6VuhOdTq8iEaH2XgJT9aIiyHgg3GT1taBBvZX2gK41FSmkguk%2BfsA%3D%3D&
trade_no=2020030722001464020500585462& # 支付寶的流水號
auth_app_id=2016093000631831&
version=1.0&
app_id=2016093000631831&
sign_type=RSA2&
seller_id=2088102177958114&
timestamp=2020-03-07%2014%3A47%3A48 # 付款時間
`
// 同步回調沒與訂單狀態
4) views/PaySuccess.vue
<template>
<div class="pay-success">
<!--如果是單獨的頁面,就沒必要展示導航欄(帶有登錄的用戶)-->
<Header/>
<div class="main">
<div class="title">
<div class="success-tips">
<p class="tips">您已成功購買 1 門課程!</p>
</div>
</div>
<div class="order-info">
<p class="info"><b>訂單號:</b><span>{{ result.out_trade_no }}</span></p>
<p class="info"><b>交易號:</b><span>{{ result.trade_no }}</span></p>
<p class="info"><b>付款時間:</b><span><span>{{ result.timestamp }}</span></span></p>
</div>
<div class="study">
<span>立即學習</span>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Header"
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// url后拼接的參數:?及后面的所有參數 => ?a=1&b=2
// console.log(location.search);
// 解析支付寶回調的url參數
let params = location.search.substring(1); // 去除? => a=1&b=2
let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
//逐個將每一項添加到args對象中
for (let i = 0; i < items.length; i++) { // 第一次循環a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解碼操作,因為查詢字符串經過編碼的
if (k_v.length >= 2) {
// url編碼反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 沒有url編碼反解
// this.result[k_v[0]] = k_v[1];
}
}
// 解析后的結果
// console.log(this.result);
// 把地址欄上面的支付結果,再get請求轉發給后端
this.$axios({
url: this.$settings.base_url + '/order/success/' + location.search,
method: 'get',
}).then(response => {
console.log(response.data);
}).catch(() => {
console.log('支付結果同步失敗');
})
},
components: {
Header,
}
}
</script>
<style scoped>
.main {
padding: 60px 0;
margin: 0 auto;
width: 1200px;
background: #fff;
}
.main .title {
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 25px 40px;
border-bottom: 1px solid #f2f2f2;
}
.main .title .success-tips {
box-sizing: border-box;
}
.title img {
vertical-align: middle;
width: 60px;
height: 60px;
margin-right: 40px;
}
.title .success-tips {
box-sizing: border-box;
}
.title .tips {
font-size: 26px;
color: #000;
}
.info span {
color: #ec6730;
}
.order-info {
padding: 25px 48px;
padding-bottom: 15px;
border-bottom: 1px solid #f2f2f2;
}
.order-info p {
display: -ms-flexbox;
display: flex;
margin-bottom: 10px;
font-size: 16px;
}
.order-info p b {
font-weight: 400;
color: #9d9d9d;
white-space: nowrap;
}
.study {
padding: 25px 40px;
}
.study span {
display: block;
width: 140px;
height: 42px;
text-align: center;
line-height: 42px;
cursor: pointer;
background: #ffc210;
border-radius: 6px;
font-size: 16px;
color: #fff;
}
</style>
七. 后台-支付成功的備選接口
1. 流程
'''
優化: 后端序列化中判斷用戶支付金額是否是0, 是0那么就直接修改訂單狀態, 也不用發送pay_link了
# 前端: created分析
1. localtion.search就可以獲取支付好?號后面的參數獲取到(包括問號), 使用.substring(1), 取出左邊的?號
2. 使用三元表達式, 對params進行split. 以及后面將這種參數進行處理
3. decodeURICompontent,
4. 把地址欄上面的支付結果, 再get請求發給后端
this.$settings.base_url + '/order/success/' + localtion.search
# 后端
1. 路由: success/ SuccessView
2. 視圖: 繼承APIView 因為不和序列化類有關系, 和數據庫有點關系
# get:
獲取前端傳遞過來的 out_trade_no, 去數據庫中查取, 判斷訂單 order_status 的訂單狀態是否成功.
最后返回響應中通過code=0或者code=1返回給前端即可
# post: 支付寶回調
回調地址: https://github.com/fzlee/alipay/blob/master/README.zh-hans.md#alipay.fund.trans.toaccount.transfer
回調參數: https://opendocs.alipay.com/open/270/105902/
注意: 必須data內容返回 success
request.data可能有2種情況. 如果是json格式是字典, 如果是QuseryDict需要注意
失敗了之后需要記錄日志
成功了之后需要記錄日志, 並且修改訂單狀態, 使用 out_trade_no 作為過來標志, order_status 修改為1, 交易支付時間pay_time=gmt_payment
'''
2. 同步理論的參數
charset=utf-8&
out_trade_no=7f7c7d12d57d45b693e1b49a6b01e1dd&
method=alipay.trade.page.pay.return&
total_amount=39.00&
sign=FUmceqiNMWvxcD%2BUPCHiOTaEwlJ%2FXIXL5UwZWOSI1TwRjPIZVzjRLB4j2G5CQpn472JO8X%2BwMx04dHqjLxqLcY3TRu0XurQ%2FwKTNpyfDrtNuNv0rfGPuVHw52y3blbS7%2FKFVsWryw4%2BBuF2fCrJ4qWH8Zg14Rct7qoMbu73N74WkQtDyzXefiKDbkMMRMfLbelE9TFyeIeygeMId8%2B58mcJMUOh6aQqwpr9bzuBbfJ17fkqU%2F0ys9zGr%2FlDtLL7aAh6BPViqZN%2F9T7byCoferD1BhcSzJNR6V6VuhOdTq8iEaH2XgJT9aIiyHgg3GT1taBBvZX2gK41FSmkguk%2BfsA%3D%3D&
trade_no=2020030722001464020500585462&
auth_app_id=2016093000631831&
version=1.0&
app_id=2016093000631831&
sign_type=RSA2&
seller_id=2088102177958114&
timestamp=2020-03-07%2014%3A47%3A48
`
// 同步回調沒與訂單狀態
3. order/urls.py
path('success/', views.SuccessView.as_view())
4. order/views.py
from rest_framework.views import APIView
from libs.alipay_sdk import alipay
class SuccessView(APIView):
def get(self, request, *args, **kwargs):
"""
獲取前端傳遞過來的 out_trade_no, 去數據庫中查取, 判斷訂單 order_status 的訂單狀態是否成功.
最后返回響應中通過code=0或者code=1返回給前端即可
"""
out_trade_no = request.query_params.get('out_trade_no')
order = models.Order.objects.filter(out_trade_no=out_trade_no).first()
# order.order_status值為1表示訂單成功
if order.order_status == 1:
return utils.APIResponse()
return utils.APIResponse(code=0, msg='失敗')
def post(self, request, *args, **kwargs):
"""
回調地址: https://github.com/fzlee/alipay/blob/master/README.zh-hans.md#alipay.fund.trans.toaccount.transfer
回調參數: https://opendocs.alipay.com/open/270/105902/
注意: 必須data內容返回 success
request.data可能有2種情況. 如果是json格式是字典, 如果是QuseryDict需要注意
失敗了之后需要記錄日志
成功了之后需要記錄日志, 並且修改訂單狀態, 使用 out_trade_no 作為過來標志, order_status修改為1, 交易支付時間pay_time=gmt_payment
"""
# request.data類型判斷
data = request.data.dict()
utils.log(f'data: {data}')
signature = data.pop("sign")
out_trade_no = data.get('out_trade_no')
gmt_payment = data.get('gmt_payment')
# 校驗
success = alipay.verify(data, signature)
if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# 修改訂單狀態
models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, pay_time=gmt_payment)
utils.log.info(f'{out_trade_no}訂單支付成功!')
# !!!注意!!!: 服務器異步通知頁面特性
'''
當商戶收到服務器異步通知並打印出 success 時,服務器異步通知參數 notify_id 才會失效。
也就是說在支付寶發送同一條異步通知時(包含商戶並未成功打印出 success 導致支付寶重發數次通知),服務器異步通知參數 notify_id 是不變的。
'''
return utils.APIResponse(data='success')
utils.log.error(f'{out_trade_no}訂單支付失敗!')
return utils.APIResponse(code=0, msg='失敗')
八. 上線前准備
1. 后端
# pro.py
'''
DEBUG = False
ALLOWED_HOSTS = ["*"] # 服務器的公網IP
# 后台基URL
BASE_URL = 'http://139.196.184.91:8000' # 注意: 這里的8000上線以后指定的nginx的8000端口, 由nginx的8000端口發送到nginx配置內部的uwsgi的端口中
# 前台基URL
LUFFY_URL = 'http://139.196.184.91' # 注意: 這里沒有寫端口默認就是80端口.
# 支付寶同步異步回調接口配置
# 后台: 支付寶異步回調的接口
NOTIFY_URL = BASE_URL + "/order/success/"
# 前台: 支付寶同步回調接口,沒有 / 結尾
RETURN_URL = LUFFY_URL + "/pay/success"
# 注意: 檢查mysql配置, 如果mysql配置的HOST是127.0.0.1, 那么需要檢查遠端服務器上的mysql本地密碼是否正確.
'''
# wsgi.py
'''
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.pro')
'''
# manage.py
'''
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev')
'''
# 拷貝manage.py改名manage_pro.py(在項目根路徑)
'''
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.pro')
'''
2. 前端
# 配置src/assets/js/settings.py文件
export default {
// 注意: 這里的8000的端口是nginx的監聽端口
base_url: 'http://139.196.184.91:8000'
}
# 將vue代碼打包成html, css, js
cmpn run build