5.学城项目 支付宝支付

1. 支付宝API文档

* 1. 官方文档
     地址: https://opendocs.alipay.com/common/02fwvj/

* 2. 支付宝API:六大接口  
     地址: https://docs.open.alipay.com/270/105900/

* 3. 支付宝工作流程  
     地址: https://docs.open.alipay.com/270/105898/

* 4. 支付宝8次异步通知机制(支付宝对我们服务器发送POST请求, 索要 success 7个字符)
     地址: https://docs.open.alipay.com/270/105902/

2. 沙箱环境下测试支付

2.1 测试步骤
沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境.
* 1. 电脑网站支付API: https://docs.open.alipay.com/270/105900/

* 2. 在沙箱环境下实名认证
	 地址: https://openhome.alipay.com/platform/appDaily.htm?tab=info

* 3. 完成RSA密钥生成:https://docs.open.alipay.com/291/105971

* 4. 在开发中心的沙箱应用下设置应用公钥:填入生成的公钥文件中的内容

* 5. Python支付宝开源第三方框架:https://github.com/fzlee/alipay

* 6. pip install python-alipay-sdk --upgrade

* 7. 代码的密钥配置格式

    alipay_public_key.pem = """-----BEGIN PUBLIC KEY-----
    支付宝公钥
    -----END PUBLIC KEY-----"""

    app_private_key.pem = """-----BEGIN RSA PRIVATE KEY-----
    用户私钥
    -----END RSA PRIVATE KEY-----"""

* 8. 支付宝网关

    真实:https://openapi.alipay.com/gateway.do
    沙箱:https://openapi.alipaydev.com/gateway.do  # 带dev

2.2 沙箱环境配置
* 1. 认证
https://developers.alipay.com/dev/workspace/register?from=http%3A%2F%2Fopenhome.alipay.com%2Fplatform%2FappDaily.html

2022-06-03_00986

image-20220603210906653

image-20220603223019913

* 2. 配置沙箱应用
     1. 选择网页&移动应用
     2. 接口加签方式选择自定义密钥
     3. 公钥模式->设置并查看

image-20220603221823833

* 3. 下载&安装 支付宝密钥生成器
     下载地址: https://gw.alipayobjects.com/os/bmw-prod/02b946e1-9faf-4394-8004-d241443c874e.exe

image-20220603221934776

image-20220603222053414

GIF 2022-6-3 22-25-43

* 4. 生成密钥

2022-06-03_00994

* 5. 复制公钥

image-20220603223606697

* 6. 将应用公钥填写到到沙箱中

image-20220603223527596

* 7. 生成支付宝公钥

image-20220603223728884

* 8. 沙箱账户

image-20220603223829186

* 9. 下载支付宝客户端沙箱版本

image-20220603223934032

image-20220604075423063

* 10. 手机端安装支付宝沙箱版
	  买家账号 aijatb2766@sandbox.com
      登录密码 111111
      支付密码 111111

2022-06-03_00999

2.3 测试代码
* 1. 安装python-alipay-sdk模块
     pip install python-alipay-sdk
     
     如果抛ssl相关错误, 代表缺失pyopenssl模块
     pip install pyopenssl
* 2. 测试代码 (设置密钥的时候不要有多余的空格)
from alipay import AliPay

# 应用程序私钥
app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAhe3/9BI+qAaoRprc1f3liRA9v9WYL/n9Xp9Cc+PPcbkfrI/pN7q/GOaaYf521wbCEXmZYW4qQVX4mP4i2S05FwRu2B1/7Ux4oREyM+0vazHrBCNyE7erqGV2+8GsLDJRnyQXApbN79FR40+chrlc5Dt29mGCtv1DKz1l/mnNJKj0srP55joxXfLmDwfeLcNcTEblJsxpgi1TKlnEG0b/0RtkFhGGOsUrClNiLxBK5iZJBUt+QX2P97MDHj8BIqLqKITBe+aSbwHcDzl587S9cq1142y41NVhEjlC20+2iHsESA82HQo8iH7i1A6BzHvRlvk7HoUqeYb8p91ZLI0d7QIDAQABAoIBAAuppQ9RA2nIYqD7XV25JWLhKi9pXz5WS60Qu02yOd9SWqLBSXLT7U4yzqDX8utYqE+zQhsM59sWrHZOMySsXntVpH1nXDuC3EJSaAfDkMyJ5UhP+eAjr2wTod/chqy2mQr9ro9IKJjIppPf2+aTf7ZUQ1DDPwnGVjIOv7H+7qFRf/XBUVyyW1j4uwFlN/ZporDa7B6lwqWgH9+Yvo7w3/Pmjo78z2BKZzPQJa2sq3WxolcphDImflycHzno8jIn+QnB3XmlCu7yxsx4HYXMQMKY8i3gzPejdOdWsjbAuxI3mfyyELN2W8ZCpaZKWfKEhpxR4DA56byZtESWN+s1HmECgYEA7WFD1vhO2gwWyiFBzyu9yHOTpmcH6mBJ30Km7mErPHD3yHOiaCv59Ba4XTD629073m3Q2z6EXpmv7Fwz+uezac12BCBeg7AU9WfBJPiSHBRvwR4qXbZzbU91tElWDFYlwCmjd3Sp5oTGWSpWU/BW3hkxx6KYIwnYUIDIhTChCQUCgYEAkG9j4pp/XisOCUM3cQmTaUzDWZfOILDQUjrWCnLEHfPJPrX5qjtkSkZ8Z6Q4GjGNVg5m4i98VRGkt3rY0wsnsuCDFrws7TzlvDVP9fo0DgTalO4JCQ4gE9fGZypbjRYtbHobTEsGnGmeE3eRrlY5cfNfcwfrr1vKceo7slJzNckCgYAd+8kr4BVlqV0/js/XMTk5lo+x1xXC3wK1tp+LQK7LZaGGqkR7UAK0eCI1czhciSdEwy48YzspD9SO0F6odJfO52revo/xpk4faUmWN+eMsHAlPoAvchpGVmERsqmxyTffe+Lv9cZ4HZFINfbNh3ARgbEt/DWnR1kRYhLx7+CHWQKBgQCNWGTwkm9IsWu4Bs6P0WYwK04VNGklNsN3ZVqnuO5RvYxY0W71d8/KnDYMmvnIMGv3JnrqqLvM6EpAwHjF92mvNOU0b4yr0eelCqsoteURPxDFpDi1YtxjbssblKkpZeWn/csPG3DpyrZGqMGpUXpAGIJ1KPAtmO+CEU7AUM2seQKBgQDmPWXLUXx6FKRVUYLgALEY+PkIc3feRw1gdhSa7Q8/sQw8jugD92YnZ9gdBkZxIlPHqiMpvba+adbFTcTIPx6Myoy8X8mfWFTHU0su+tiwiUXGWkxAvYU/8ebSABs6HRgFGkbJEDU7TqPOvDCLFjAFdTQJ/LKGwbI+E7XfR0f4Sw==
-----END RSA PRIVATE KEY-----"""

# 支付宝公钥
alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfLBZtfSKJ3mwThLuRnLO1eEh5jf2fXDlC06VLH/lmFFJrZBfkJqzE9T41k5JoGpluI6r03vo6Vx29UYOP0bf531UUr9Bpg2TLBwmUVQYoY15plsUjKkqTqDYq55m3cYJpn0TBROMyurbGs4i0TcNq35zjC2wbl0DWmxmOhWyg3NhUu9uqWUJGbfxTuowKQxaK/3XDoOLx+mQCytz0JvNevUhBhOu6eZLCh3hNOswu8gMf3a5kCReya24uXOd26SKd5HYlwFHk4ndSSjwIYulQs9hnlSbkJn+zykjc6qdDQLMWuqkSKlb5Snu7TCh5FNUO4KsFqk2bEXgysJy23lhwIDAQAB
-----END PUBLIC KEY-----"""
alipay = AliPay(
    # 应用id
    appid="2021000120612450",
    # 默认回调
    app_notify_url=None,
    # 应用私钥字符串
    app_private_key_string=app_private_key_string,
    # 支付宝公钥字符串
    alipay_public_key_string=alipay_public_key_string,
    # 加密方式
    sign_type="RSA2",  # RSA or RSA2
    # 开启调试(上线关闭)
    debug=True
)

# 网关地址(上线之后需要修改)
alipay_url = 'https://openapi.alipaydev.com/gateway.do?'
# 生成对象(交易页面)
order_string = alipay.api_alipay_trade_page_pay(
    # 订单号唯一标识
    out_trade_no="202206041111",
    # 价格
    total_amount=9.9,
    # 商品名称
    subject='<<肾宝>>男人要肾好,就要喝肾宝。喝了之后,比刘翔快,比姚明高。一瓶提神醒脑,两瓶永不疲劳,三瓶长生不老。噢耶...',
    # 支付成功 get同步回调, 前台展示的页面, 
    # return_url="",
    # 支付成功 post异步回调, 向后端发送请求 修改订单状态(以支付)
    # notify_url=""
)

# 生成支付链接 (网关 + 交易页面)
print(alipay_url + order_string)

* 3. 运行程序, 得到一个商品支付地址:
https://openapi.alipaydev.com/gateway.do?app_id=2021000120612450&biz_content=%7B%22subject%22%3A%22%5Cu7537%5Cu4eba%5Cu8981%5Cu80be%5Cu597d%5Cuff0c%5Cu5c31%5Cu8981%5Cu559d%5Cu80be%5Cu5b9d%5Cu3002%5Cu559d%5Cu4e86%5Cu4e4b%5Cu540e%5Cuff0c%5Cu6bd4%5Cu5218%5Cu7fd4%5Cu5feb%5Cuff0c%5Cu6bd4%5Cu59da%5Cu660e%5Cu9ad8%5Cu3002%5Cu4e00%5Cu74f6%5Cu63d0%5Cu795e%5Cu9192%5Cu8111%5Cuff0c%5Cu4e24%5Cu74f6%5Cu6c38%5Cu4e0d%5Cu75b2%5Cu52b3%5Cuff0c%5Cu4e09%5Cu74f6%5Cu957f%5Cu751f%5Cu4e0d%5Cu8001%5Cu3002%5Cu5662%5Cu8036...%22%2C%22out_trade_no%22%3A%22%5Cu80be%5Cu5b9d%22%2C%22total_amount%22%3A9.9%2C%22product_code%22%3A%22FAST_INSTANT_TRADE_PAY%22%7D&charset=utf-8&method=alipay.trade.page.pay&notify_url=https%3A%2F%2Fwww.luffycity.com%2Ffree-course&return_url=https%3A%2F%2Fwww.luffycity.com%2Ffree-course&sign_type=RSA2&timestamp=2022-06-03+23%3A26%3A25&version=1.0&sign=BttMCJX7M6PGFNq1ldnaxR0HM1zLcVRbCeC7xmM3NDceOvs2U8tnRHmZb1gKlbozH2T8arQfPHrtJQ4dkjpptQHJMGWD6AU0cQzSi%2F4dI3m5GKEK4Y96w9OC1UNTjp80TXwZ1OlPCTzpHB4SAkjHeRSbxPXGJpvsvt7TjKOhEIbZ02xNAfawLa%2F2ptv6FXtplSb1ckmo1aV2k%2Fa8rQxRUVA52eeM9XNoH7GpoUiDAi47NZalgm1bcsrvrHfDizR83AU39hddA8EkqxFFAlFAOpVCRH6ZWSFgxJfTap4b6KkViYkaee74eELMgvG9qcuV9b04DB3JdFfbctl1VXyqrQ%3D%3D
* 4. 使用浏览器打开地址(如果浏览器打开的网站太多, 可能会提示钓鱼网站之类的提示.)

image-20220604074308312

* 5. 使用支付宝沙箱版本支付

image-20220603234055655

* 6. 沙箱账户中查看余额

image-20220603233804278

3. 二次封装支付宝

在项目lib目录下重新封装支付宝
配置可以写在iPay下的settings.py文件或项目的dev.py配置文件中.
lib
    ├── iPay  						  # aliapy二次封装包
       ├── __init__.py 			   # 包文件
       ├── pem						  # 公钥私钥文件夹
          ├── alipay_public_key.pem	# 支付宝公钥文件
          ├── app_private_key.pem		# 应用私钥文件
       ├── pay.py					   # 支付文件
    └── └── settings.py | dev.py         # 应用配置
* 1. 在项目lib目录下新建iPay目录, 再按照目录结构创建其他文件和目录.
* 2. 在app_private_key.pem中写入支付宝公钥
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAhe3/9BI+qAaoRprc1f3liRA9v9WYL/n9Xp9Cc+PPcbkfrI/pN7q/GOaaYf521wbCEXmZYW4qQVX4mP4i2S05FwRu2B1/7Ux4oREyM+0vazHrBCNyE7erqGV2+8GsLDJRnyQXApbN79FR40+chrlc5Dt29mGCtv1DKz1l/mnNJKj0srP55joxXfLmDwfeLcNcTEblJsxpgi1TKlnEG0b/0RtkFhGGOsUrClNiLxBK5iZJBUt+QX2P97MDHj8BIqLqKITBe+aSbwHcDzl587S9cq1142y41NVhEjlC20+2iHsESA82HQo8iH7i1A6BzHvRlvk7HoUqeYb8p91ZLI0d7QIDAQABAoIBAAuppQ9RA2nIYqD7XV25JWLhKi9pXz5WS60Qu02yOd9SWqLBSXLT7U4yzqDX8utYqE+zQhsM59sWrHZOMySsXntVpH1nXDuC3EJSaAfDkMyJ5UhP+eAjr2wTod/chqy2mQr9ro9IKJjIppPf2+aTf7ZUQ1DDPwnGVjIOv7H+7qFRf/XBUVyyW1j4uwFlN/ZporDa7B6lwqWgH9+Yvo7w3/Pmjo78z2BKZzPQJa2sq3WxolcphDImflycHzno8jIn+QnB3XmlCu7yxsx4HYXMQMKY8i3gzPejdOdWsjbAuxI3mfyyELN2W8ZCpaZKWfKEhpxR4DA56byZtESWN+s1HmECgYEA7WFD1vhO2gwWyiFBzyu9yHOTpmcH6mBJ30Km7mErPHD3yHOiaCv59Ba4XTD629073m3Q2z6EXpmv7Fwz+uezac12BCBeg7AU9WfBJPiSHBRvwR4qXbZzbU91tElWDFYlwCmjd3Sp5oTGWSpWU/BW3hkxx6KYIwnYUIDIhTChCQUCgYEAkG9j4pp/XisOCUM3cQmTaUzDWZfOILDQUjrWCnLEHfPJPrX5qjtkSkZ8Z6Q4GjGNVg5m4i98VRGkt3rY0wsnsuCDFrws7TzlvDVP9fo0DgTalO4JCQ4gE9fGZypbjRYtbHobTEsGnGmeE3eRrlY5cfNfcwfrr1vKceo7slJzNckCgYAd+8kr4BVlqV0/js/XMTk5lo+x1xXC3wK1tp+LQK7LZaGGqkR7UAK0eCI1czhciSdEwy48YzspD9SO0F6odJfO52revo/xpk4faUmWN+eMsHAlPoAvchpGVmERsqmxyTffe+Lv9cZ4HZFINfbNh3ARgbEt/DWnR1kRYhLx7+CHWQKBgQCNWGTwkm9IsWu4Bs6P0WYwK04VNGklNsN3ZVqnuO5RvYxY0W71d8/KnDYMmvnIMGv3JnrqqLvM6EpAwHjF92mvNOU0b4yr0eelCqsoteURPxDFpDi1YtxjbssblKkpZeWn/csPG3DpyrZGqMGpUXpAGIJ1KPAtmO+CEU7AUM2seQKBgQDmPWXLUXx6FKRVUYLgALEY+PkIc3feRw1gdhSa7Q8/sQw8jugD92YnZ9gdBkZxIlPHqiMpvba+adbFTcTIPx6Myoy8X8mfWFTHU0su+tiwiUXGWkxAvYU/8ebSABs6HRgFGkbJEDU7TqPOvDCLFjAFdTQJ/LKGwbI+E7XfR0f4Sw==
-----END RSA PRIVATE KEY-----
* 3. 在alipay_public_key.pem文件中写入应用程序私钥
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfLBZtfSKJ3mwThLuRnLO1eEh5jf2fXDlC06VLH/lmFFJrZBfkJqzE9T41k5JoGpluI6r03vo6Vx29UYOP0bf531UUr9Bpg2TLBwmUVQYoY15plsUjKkqTqDYq55m3cYJpn0TBROMyurbGs4i0TcNq35zjC2wbl0DWmxmOhWyg3NhUu9uqWUJGbfxTuowKQxaK/3XDoOLx+mQCytz0JvNevUhBhOu6eZLCh3hNOswu8gMf3a5kCReya24uXOd26SKd5HYlwFHk4ndSSjwIYulQs9hnlSbkJn+zykjc6qdDQLMWuqkSKlb5Snu7TCh5FNUO4KsFqk2bEXgysJy23lhwIDAQAB
-----END PUBLIC KEY-----
* 4. settings.py 配置文件中填写参数
import os

# 获取到iPay的路径
BASE_DIR = os.path.dirname(__file__)
print(BASE_DIR)
# 得到iPay下的pem目录路径
PEM_DIR = os.path.join(BASE_DIR, 'pem')
# 应用程序私钥地址
APP_PRIVATE_KEY_URL = os.path.join(PEM_DIR, 'app_private_key.pem')
# 支付宝公钥地址
ALIPAY_PUBLIC_KEY_URL = os.path.join(PEM_DIR, 'alipay_public_key.pem')

# 应用id
APPID = '2021000120612450'

# 默认回调
APP_NOTIFY_URL = None

# 应用私钥字符串
with open(APP_PRIVATE_KEY_URL, mode='r', encoding='utf8') as rf1:
    APP_PRIVATE_KEY_STRING = rf1.read()

# 支付宝公钥字符串
with open(ALIPAY_PUBLIC_KEY_URL, mode='r', encoding='utf8') as rf2:
    ALIPAY_PUBLIC_KEY_STRING = rf2.read()

# 加密方式
SIGN_TYPE = "RSA2"

# BUG模式
DEBUG = True

# 沙箱网关 ( DEBUG = True )
dev = 'https://openapi.alipaydev.com/gateway.do?'

# 真实网关 ( DEBUG = False )
master = 'https://openapi.alipay.com/gateway.do?'

# 网关
ALIPAY_URL = dev if DEBUG else master

* 5. 在pay.py生成alipay支付对象
# 导入配置参数
from .settings import *

# 导入AliPay模块
from alipay import AliPay

# 生成alipay对象
alipay = AliPay(
    # 应用id
    appid=APPID,
    # 默认回调
    app_notify_url=APP_NOTIFY_URL,
    # 应用私钥字符串
    app_private_key_string=APP_PRIVATE_KEY_STRING,
    # 支付宝公钥字符串
    alipay_public_key_string=ALIPAY_PUBLIC_KEY_STRING,
    # 加密方式
    sign_type=SIGN_TYPE,
    # 调试开关(上线设置False)
    debug=False,

)

* 6. __init__.py 中导入alipay对象  网关
     在导入iPay时会自动触发__init__.py,则导入alipay对象  网关
# 导入alipay对象 与 网关
from .pay import alipay
from .settings import ALIPAY_URL

"""
在导入iPay时则导入alipay对象 与 网关
from luffy/lib import iPay
iPay.alipay ...
iPay.ALIPAY_URL ...
"""

4. 订单接口

4.1 创建app
* 1. 新建订单app
cd luffy/apps/
python ../../manage.py startapp order
* 2. 注册app
INSTALLED_APPS = [
    ...
    'order'
* 3. 总路由配置
path('order/', include('order.urls')),
* 4. 子路由 
     先在order目录下新建urls.py
from django.urls import re_path, include

from . import views
# 导入SimpleRouter
from rest_framework.routers import SimpleRouter

# 生成对象
router = SimpleRouter()
# 注册路由
router.register('pay', views.PayView, 'pay')

# 路由列表
urlpatterns = [
    re_path('', include(router.urls)),
]

4.2 表设置
1. 订单表
2. 订单表与课程表的第三张表
from django.db import models

# 导入用户表
from user.models import User
# 导入课程表
from course.models import Course


# 订单表
class Order(models.Model):
    """
    用户表(一) 对 (多)订单表
    一个用户可以有多个订单, 一个订单只能有一个用户. 一对多关系, 关联字段写在多的一方.
    """
    # 支付状态
    status_choices = (
        (0, '未支付'),
        (1, '已支付'),
        (2, '以超时'),
        (3, '超时取消')
    )

    # 支付状态
    pay_choices = (
        (0, '支付宝'),
        (1, '微信')
    )

    # 订单标签
    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)
    # 订单状态
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name='订单状态')
    # 支付方式
    pay_type = models.SmallIntegerField(choices=pay_choices, default=0, verbose_name='支付方式')
    # 支付时间
    pay_time = models.DateTimeField(null=True, verbose_name='支付时间')
    # 外键
    user = models.ForeignKey(to=User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
                             verbose_name='下单用户')
    # 创建时间
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    # 最后更新时间
    updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')

    # 定义元类
    class Meta:
        # 表名
        db_table = 'luffy_order'
        # 定义一个变量
        verbose_name = '订单记录'
        # 后台展示的表名
        verbose_name_plural = verbose_name

    # 打印对象时展示商品的名字与价格
    def __str__(self):
        return f'{self.subject} - ¥:{self.total_amount}'


# 订单详情
class OrderDetail(models.Model):
    """
    订单表(多) 对 (多)课程表
    一个订单可以有多个课程, 一个课程可以在对个订单中
    第三张表外键关联订单表, 关联课程表, 创建额外的字段
    """
    # 外键 关联订单表
    order = models.ForeignKey(to=Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
                              verbose_name='订单外键')
    # 外键 关联关联课程表
    course = models.ForeignKey(to=Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False,
                               verbose_name='课程外键')
    # 原价
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='课程原价')
    # 实价
    real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='课程实价')

    # 定义元类
    class Meta:
        # 表名
        db_table = 'luffy_order_detail'
        # 定义一个变量
        verbose_name = '订单详情'
        # 定向详情
        verbose_name_plural = verbose_name

    # 打印对象时展示 课程名字:课程价格
    def __str__(self):
        try:
            return f'{self.course.name}的订单: {self.order.out_trade_no}'
        # 没有录数据的时候不会至于报错
        except:
            return super().__str__()

数据库迁移:
python manage.py makemigrations
python manage.py migrate
# 修改order目录下的admin.py 文件将表注册到后台
import xadmin

from . import models
# 将订单表与订单详情表上条件到后台管理
xadmin.site.register(models.Order)
xadmin.site.register(models.OrderDetail)

# 后台展示中文菜单
# 修改order下的__init__.py
default_app_config = "order.apps.OrderConfig"

# 修改order下的apps.py
from django.apps import AppConfig


class OrderConfig(AppConfig):
    name = 'order'
    verbose_name = '订单'

4.3 接口
支付接口分析
1. 支付接口
   生成订单, 生成支付连接, 返回支付链接
   
2. 支付成功时: 支付宝异步回调的post接口
   验证签名, 修改订单状态

3. 支付成功时: 支付宝同步get回调 (支付成功后展示的页面)
   vue页面创建时 生命周期钩子函数发送请求去后端查询订单状态(可有可无)
   
1. 订单总价校验
   从数据库中获取对应的课程对象, 课程的价格取出来累加, 再与前端传递的值比较是否相等.
2. 生成订单号
   使用一个uuid生成一个随机字符串
3. 支付链接生成
   使用自己封装的支付宝模块生成支付连接, 将支付链接存放在序列化对象的context属性中.
4. request.user 中获取用户对象
   视图类中重写create方法, 为context参数设置值为request, 序列化中取出用户对象
5. 入库(两张表)的信息准备
   将用户对象保存到attrs字典中
6. 重写序列化的create方法
   将课程从validated_data字典中pop出来...
   先创建支付表的数据, 在遍历创建支付详情表的数据
* 1. 模型序列化器
# 导入模型序列化
from rest_framework import serializers

# 导入模型层
from . import models

# 导入异常模块
from rest_framework.exceptions import ValidationError


class OrderModelSerializer(serializers.ModelSerializer):
    course_obj = models.Course.objects.all()
    """
    PrimaryKeyRelatedField 将 {'course': [课程id1, 课程id2, ...]} 转为 {'course': [课程1_obj, 课程2_obj, ...]}
    queryset参数对于转换表的queryset对象, 提交的值是一个列表, 设置many=True, 只写模式
    Course表与 Order可以没有关系,只要设置queryset的参数值是一张表的数据即可 
    通过id获取到表对应的数据对象	
    """
    course = serializers.PrimaryKeyRelatedField(queryset=course_obj, many=True, write_only=True)

    class Meta:
        model = models.Order

        # 转换的字段 订单标签 价格 支付方式 购买的课程
        fields = ['subject', 'total_amount', 'pay_type', 'course']
        # (设置了默认值, 不会提示价格 支付方式字段必填, 使用require提示字段必须填写)
        extra_kwargs = {
            'total_amount': {'required': True},
            'pay_type': {'required': True},
        }

    # 全局校验
    def validate(self, attrs):
        # 1. 校验价格, 检验不成功直接抛出异常, 校验成功返回价格
        total_amount = self._check_price(attrs)
        # 2. 生成订单号
        out_trade_no = self._gen_out_trade_no()
        # 3. 生成支付链接并保存到context参数中
        self._gen_pay_url(attrs, out_trade_no, total_amount)
        # 4. 获取用户对象 在视图类中重写create方法, 在生成模型序列化对象时将request通过context参数传递
        user_obj = self._get_user()
        # 5. 写入数据库前准备
        self._before_create(attrs, user_obj, out_trade_no)

        return attrs

    # 价格校验
    @staticmethod
    def _check_price(attrs):
        # 获取提交的价格
        total_amount = attrs.get('total_amount')
        # 获取课程对象
        course_list = attrs.get('course')
        # 计算数据库中的价格
        total_price = 0
        for course_obj in course_list:
            total_price += course_obj.price
        # 判断值是都相等的
        if total_amount != total_price:
            # 价格不一致, 直接抛出异常
            raise ValidationError('价格不合法')
        return total_amount

    # 生成订单号
    @staticmethod
    def _gen_out_trade_no():
        # 订单号是一个唯一字符串, 使用uuid即可
        from uuid import uuid4
        # uuid的类型值的格式 bce5eed9-8985-42ab-b37a-51bf8909c1c5  类型是  <class 'uuid.UUID'>
        # 将类类型转为str类型, 再将-去除 bce5eed9898542abb37a51bf8909c1c5
        return str(uuid4()).replace('-', '')

    # 生成支付链接
    def _gen_pay_url(self, attrs, out_trade_no, total_amount):
        # 导入支付模块
        from luffy.lib import iPay
        # 获取商品名称(前端传入)
        subject = attrs.get('subject')
        # 导入回调地址
        from django.conf import settings
        # 网关地址
        alipay_url = iPay.ALIPAY_URL

        # total_amount是Decimal类型, 支付宝无法识别, 转为float类型即可
        print(total_amount, type(total_amount))  # 138.00 <class 'decimal.Decimal'>
        total_amount = float(total_amount)
        # 生成对象
        order_string = iPay.alipay.api_alipay_trade_page_pay(
            # 订单号唯一标识
            out_trade_no=out_trade_no,
            # 价格
            total_amount=total_amount,
            # 商品名称
            subject=subject,
            # 支付成功 get同步回调, 前台展示的页面,
            return_url=settings.NOTIFY_URL,
            # 支付成功 post异步回调, 向后端发送请求 修改订单状态(以支付)
            notify_url=settings.RETURN_URL
        )
        # 生成支付地址
        pay_url = alipay_url + order_string
        # 将支付链接保存到context中
        self.context['pay_url'] = pay_url

    # 获取用户
    def _get_user(self):
        # 从context中获取request对象
        request = self.context.get('request')
        # 放回user对象(登入成功之后request.user会有用户的数据对象)
        return request.user

    # 写入数据前准备
    @staticmethod
    def _before_create(attrs, user_obj, out_trade_no):
        # 将用户对象写入到序列化字典中
        attrs['user'] = user_obj
        # 将订单号写入到序列化字典中
        attrs['out_trade_no'] = out_trade_no

    def create(self, validated_data):
        # 将课程列表虚拟字段pop掉
        course_list = validated_data.pop('course')

        # 支付表数据写入(需要的数据, 'subject', 'total_amount', 'course', 'user')
        order_obj = models.Order.objects.create(**validated_data)

        # 写入支付详情表, 需要的数据(order, course, price, real_price)
        for course_obj in course_list:
            models.OrderDetail.objects.create(order=order_obj, course=course_obj, price=course_obj.price,
                                              real_price=course_obj.price)

        # 将支付对象返回, 返回了也没用到
        return order_obj

# 支付宝回调地地址
# 后台URL
BASE_URL = 'http://127.0.0.1:8000'
# 前台URL
LUFFY_URL = 'http://127.0.0.1:8080'

# 后台异步回调地址
RETURN_URL = BASE_URL + '/order/success/'
# 前台同步回调地址(没有/结尾)
NOTIFY_URL = LUFFY_URL + '/pay/success'

* 2. 视图类
    单独使用rest_framework_jwt.authentication.JSONWebTokenAuthentication的话所有游客都可以访问, 
    request.user 是AnonymousUser 匿名用户, 使用rest_framework内置权限类, 对登入用户进行校验, 
    匿名用户禁止访问!
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from . import models
from .serializer import OrderModelSerializer
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated


# 生成支付订单
class PayView(GenericViewSet, CreateModelMixin):
    # 登入认证类 + 限制类 现在限制只有登入用户才能使用
    authentication_classes = [JSONWebTokenAuthentication]  # 没有用户登入返回
    permission_classes = [IsAuthenticated]
    # 使用的表数据
    queryset = models.Order.objects.all()
    # 使用的模型序列化类
    serializer_class = OrderModelSerializer

    # 重写视图类的 create方法 (复制CreateModelMixin的create方法进行修改)
    def create(self, request, *args, **kwargs):
        # 生成序列化器时, 将request对象创建给模型序列化器 通过context参数
        serializer = self.get_serializer(data=request.data, context=request)
        # 校验数据
        serializer.is_valid(raise_exception=True)
        # 保存数据
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.context.get('pay_url'))

4.4 接口测试
* 1. 接口地址: http://127.0.0.1:8000/order/pay/

image-20220608211000598

* 2. 需要先登入(post请求)
     登入地址: http://127.0.0.1:8000/user/login/?username=root&password=zxc123456

image-20220608211435407

* 3. 携带token访问支付接口
key: Authorization 
value: jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NTUyOTg4NTYsImVtYWlsIjoiMTM2QHFxLmNvbSJ9.7vHsFCMCM-8_Uc5-bOvEFMvUf4d5fBtCGayQG53mb2c
* token前面携带 jwt空格

image-20220608212010864

* 4. 价格不合法测试
	 三门课程都现在, 价格是138, 现在随便填写一个错误的价格

image-20220608212348631

* 5. 价格正常测试, 返回支付地址

image-20220608220622293

4.5 前台点击购买事件
为点击购买绑定事件, 免费课程页面, 页面详情, 搜索页面, 三个位置.
只有等有用户才能购买:
登入成功之后 this.$cookies.set('token', response.data.token) 将token保存到了scookies中
先从cokkies中获取token, 如果有值则可以发送请求, 如果没有值则提示用户登入.

拿到支付地址之后跳转到支付页面(新开设页面)
window.open(url) 简写 open(url) 
在当前页面打开网址
open(url, '_self')
<span class="buy-now" @click="buy_now(course)">立即购买</span>
// 购买课程
buy_now(course) {
    // 从cookies中获取token
    let token = this.$cookies.get('token')
    // 判断是否为登入状态
    if (!token) {
        // 提示用户登入
        this.$message({
            message: '请先登入!'
        })
        return false
    }
    // 用户已经登入向后台发送请求
    this.$axios({
        method: 'post', url: this.$settings.base_url + '/order/pay/',
            headers: {Authorization: 'jwt ' + token}, data: {
                // 课程的名字作为商品的标题
                "subject": course.name,
                // 课程的价格
                "total_amount": course.price,
                // 支付方式
                "pay_type": 1,
                // 课程的id
                "course": [
                course.id
                ]
        }
    }).then(response => {
        // 访问成功获取到支付链接 直接从response.data中获取数据(后端直接返回链接, 不是返回request.data)
        // 视图序列化器中报错会 需要从response.data.dat中获取错误信息
        // console.log(response.data)
        // 跳转到支付地址
        if (response.data.data) {
            this.$message({
                message: response.data.data  // 异常新 例如 { "total_amount": [ "该字段是必填项。" ] }
            })
            return false
        }
        // 返回的不是错误信息那么就是支付链接
        let pay_url = response.data

        // console.log(pay_url)
        // 跳转到支付链接
        open(pay_url, '_self')

    }).catch(error => {
        this.$message({
            message: '未知错误, 请联系管理员!'
        })
    })
}

GIF 2022-6-9 11-31-26

4.6 同步回调页面
两部分:
1. 分析支付宝回调参数展示订单的支付情况
2. 向后端get请求, 查询后端是否修改订单状态
* 1. 新建支付成功展示的页面 PayCourse.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/Head"

export default {
  name: "PayCourse",
  data() {
    return {
      result: {},
    };
  },
  created() {

    // location.search 获取 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('&') : [];
    // console.log(items)
    /*
    0: "charset=utf-8"
    1: "out_trade_no=1c6bd7ee950842249a7cb0b6956224d2"
    ...
    */
    // 将每一项逐个添加到args对象中
    for (let i = 0; i < items.length; i++) {  // 第一次循环a=1,第二次b=2
      // 按=切分得到 k = v
      let k_v = items[i].split('=');  // ['a', '1']
      // 列表元素少与2直接忽略, 有键缺值的情况
      if (k_v.length >= 2) {
        // url编码反解 解码操作,因为查询字符串经过编码的
        // 解码k
        let k = decodeURIComponent(k_v[0]);
        // 解码value 并保存到result对象中
        this.result[k] = decodeURIComponent(k_v[1]);
      }

    }
    // 解析后的结果
    // console.log(this.result);
    /*
    app_id: "2021000120612450"
    auth_app_id: "2021000120612450"
    charset: "utf-8"
    */

    // 把地址栏上面的支付结果,再get请求转发给后端
    this.$axios({
      method: 'get',
      // 携带同步回调的参数访问后端
      url: this.$settings.base_url + '/order/success/' + location.search,
    }).then(response => {
      console.log(response.data)
      if (response.data) {
        this.$message({
          message: '支付结果同步成功'
        })

      } else {
        this.$message({
          message: '支付结果同步失败'
        })
      }
    }).catch(() => {
      this.$message({
        message: '查询订单支付状态失败'
      })
    })
  },
  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>
* 2. 设置PayCourse的路由
// 导入支付成功提示页面
import PayCourse from "@/views/PayCourse"

// 支付成功提示页面 (与后台中配置的一致)
{
    path: '/pay/success',
    name: 'PayCourse',
    component: PayCourse
},
# 前台同步回调地址(没有/结尾)
RETURN_URL = LUFFY_URL + '/pay/success'
* 3. vue中url地址中多余的/#/
     不删除的话回调地址路由匹配不成功
     http://127.0.0.1:8080/pay/success 路由匹配不成功
// 路由中设置 mode: "history", 删除url中多余的/#/
const router = new VueRouter({
    mode: "history",
    routes
})

* 4. 购买课程测试, 购买成功之后
     回调地址携带参数

回调地址+参数
http://127.0.0.1:8080/pay/success?
// 编码格式
charset=utf8
// 订单号
&out_trade_no=ffb82dd39bf043808c311b4ae983d9ec&method=alipay.trade.page.pay.return
// 价格
&total_amount=39.00
// 签名
&sign=KFPDpjwAGuUa73qsBctb0m9Nj0514PoQnxcMyH%2Fzw1rPmzY%2F1%2BTwH2v4BB1hSyc5%2BRV6TIc%2Bvz1SrchxtND20HU%2BOjLuT%2BxSlGK034OFUDpxtiy4ofkYWWqcvxz84sQ8BUifdGKWDFoH1cgagbJVGl3DO1Lce5HbsvNZY2GeYQTC%2BTIWCUMKCcWYP375%2FQFCFoMUWj6LNcIQk3rrKMNJm9B%2F%2BwryrcRMJT1cNZnuLhKiYTzcIFRWnDyVu4aVfKa3xJtIEbTKAqIVHK9LuV%2BkrkBqD1GAwOy1gwIlrkB4VJ%2BvwPuyRpmVzGK7zVq6JIVzZNVawjFBhduwuoG0eUH7oA%3D%3D
// 支付宝流水号
&trade_no=2022060922001441860503213387
// 授权商户的App id
&auth_app_id=2021000120612450
// 版本
&version=1.0
// 应用id
&app_id=2021000120612450
// 签名类型
&sign_type=RSA2
// 卖方id
&seller_id=2088621959339670
// 时间戳
&timestamp=2022-06-09+11%3A50%3A44#/
location.search 获取 url后拼接的参数
先去除? 在按&切分得到元组, 元组的元素'k=v',
在遍历元素, =切分得到一个元组 ['k', 'v']
对k, v的字符进行解码保存到一个对象中

image-20220609125826707

4.7 订单状态接口
1. 查询订单状态接口 get请求  
2. 修改订饭状态接口 post请求 
* 1. 路由order.py的urls.py
# 订单状态接口
re_path('success', views.SuccessView.as_view())
# 支付状态
class SuccessView(APIView):
    @staticmethod
    def get(request):
        # 从参数中获取订单号去数据库中查询订单的状态
        out_trade_no = request.query_params.get('out_trade_no')
        order_obj = models.Order.objects.filter(out_trade_no=out_trade_no).first()
        # 订单状态
        order_status = order_obj.order_status
        # 判断订单状态
        if order_status != 1:  # 1 为支付成功状态
            return Response(False)
        return Response(True)

    # 支付宝异步回调(只能上线之后访问, 支付宝无法访问到我电脑本地的地址)
    @staticmethod
    def post(request):
        # 导入支付类
        from lib.iPay import alipay
        from utils.logger import log

        # 获取订单号
        out_trade_no = request.data.get('out_trade_no')
        # 获取支付时间
        pay_time = request.data.get('gmt_payment')

        # 验证签名 支付宝返回的urlencode格式被转为queryset_dict
        data = request.data.dict()  # 装为普通字段
        sign = data.pop('sign')
        success = alipay.verify(data, sign)
        # 验证成功, 并且 data中trade_status的值必须是TRADE_SUCCESS或TRADE_FINISHED
        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=pay_time)

            # 记录日志
            log.info(f'{out_trade_no}支付成功')

            return Response('success')  # 验证成功必须返回success

        log.info(f'{out_trade_no}支付失败')
        return Response('error')  # 验证不成功

5. 上传到远程仓库

在Terminai中输入git命令
5.1 前端
# 添加到暂存区
PS D:\vue\luffy_vue> git add .
# 提交到版本库
PS D:\vue\luffy_vue> git commit -m '完成支付页面'
# 下拉
PS F:\synchro\Project\luffy> git pull origin dev                        
# 上传到远端仓库
PS F:\synchro\Project\luffy> git push origin dev
5.2 后端
# 添加到暂存区
PS F:\synchro\Project\luffy> git add .    
# 提交到版本库
PS F:\synchro\Project\luffy> git commit -m '完成支付功能'
# 下拉
PS F:\synchro\Project\luffy> git pull origin dev                        
# 上传到远端仓库
PS F:\synchro\Project\luffy> git push origin dev
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值