08-01 支付寶支付


一. 快速連接通道

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

二. 支付流程圖

![img](01 支付寶支付.assets/007S8ZIlgy1ggqw1m8jhaj31jy0u0gtz.jpg)

三. 支付寶接入入門

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. 測試目錄結構

image-20200802195750274

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. 目錄結構

image-20200802195432002

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM