支付

08-01 支付宝支付

一. 快速连接通道

1. 支付宝

1)支付宝API:六大接口

https://opendocs.alipay.com/mini/server-api

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密钥生成

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-sandbox.dl.alipay.com/gateway.do? 沙箱:https://openapi-sandbox.dl.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. t_alipay.py

from alipay import AliPay

app_private_key_string = """-----BEGIN rsa2 PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCg2m4dB5cgqAzh4YrKphaZyGXzMbbtH1izLFNdUPPLsKaS+tfRb5PYlxq23SwH9nyUwBpSQ6rVTpa8fy2BCM93Iqpfz2Q8efzVMyve/mHNgAXFuENOb2AgRqH2+eGfmnxiF04J5WMG+cIkN7L7XUgxCl8S5pplfpxeXvuwGCoUCydqUlrLAjRiKdaqnXzJjLi2bj44KmhvWMjhTuaMRLgYUkhtp7YhGLGMrS6YVPuxNSaEJdXiR5XsFPwqz3/eriQ5txgYiDw643C1Kw0tLvaJYzaksrCuBEb6n3+KwiXi5JOoYJc3h9nRqFWgsXm6smc6Z2biaFEbPDN7j8C8evD5AgMBAAECggEAcAVO1Ea5+BMwzYpuRwz+BmEqpwBDXcYL1tQSxpUfBAblKs0oygGDnD43a4yCIpWFU26ppgrcCStvDJ0hSpChM13f+1OKgheOfcfiUK0l3aC/5F+b+B7WegPWvhJuD+Hdp86FGZ03pt7/Vou3yRjAsu5/IcGZWv+e1JYfgBaED2pObJ7Wk2ymktwbyTCDplRXH+wVVqgoDpaBHB9v6Z04BK63Hj8PRsZ8+vCzJ0jrCPXRLKoGm8pEYNbBLSExgIqhDAORn2FnU4wcrM+D0Z3QbhtCMdQWm6ctR6MIrlV+KOMrM3nyRlLCFwNj1XFqGR7kIdy4WxgxUkYXoSNhMJh90QKBgQDQeOWT5cak+pKgVyt6+segdh2tD4qMGBRJPonafKj8uHZl2xzaH6AC0Xqs7ijhS+RdbbckwKeVkrGX+68ylUQytDnV7dCJk56g6cfZN2GIohJweUv+Jf/bgI436r/Zyix3dnoImIMnKGfirVnH/Msd/CUg5BWIOVVeIxPIzUBmzQKBgQDFhlYpDpNEmxr96ibTNYVlfy3bbFbLQeCZOHQtLRmEAMbbqZD7Qo3KYZQf96eMs7xXv+guYCS+KZfhVSLg5P1d7D4Y/0Up9Pkq5tqKveNzMAyPu9kS0RTJiPl05KUt+tDYZTpIr85OIHlKmbQwbGqErW4PxEon3jso1Y2yWXb63QKBgQC7rMrIzWd6KFYN4Mu058UFMLBglwgcPKUulw1VUzpyYMG9ynb76tKLFviAa9sDj+XjMh1ZCdMUdT5J23uKZxRBLTyP2YsN/4YjyLJwW0oDzhwZgyklgCIJAn+F2WCjeT10woTz7hDMFLUJPRQBVROqR89I/+xeeXbfy2ZJNHYQ1QKBgQCoohLHBJGIHd+Cbbht0yCq0VRGI41KBFkKlp7gvsMs5jjf6jlDucMxx/LdA+MAhaZDSCyiAyT7UKlBEB0x4W3KFNnDH5RdyK49CVAE6S7Y9WmUAKzHmsbjdFR8joPS6HIKfQmwap94JdNHdEfYm4ao9SOkxFEHDnx1VTSe0jB45QKBgEMBHOKJi4pzo79FinfpWlFzQQPQuYvw3sxxuxLdXueKPUk9JNTU8U7pyy1uXJRXPea9JBHuy6ioQAzI8DFp2cQ/eWPMvZcQFdKITuqoTi/vKf7f41r5oYWfRZ8+R7rRc1HQORMre79z5WcC/DxJgtUP8e6wJEIEnFeh/ysYDSGx
-----END rsa2 PRIVATE KEY-----"""

alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvvyDeDgF3CB+lyi2t2lvP1b8UcVGhdK0a7X9a30ZDNhcAa9zOM8dHgfhbScso6w24T8UK0TUcEfPsXiECLDtDp63/z+EyisF1udCq/af2xa2T+9jNj5Vi3xDnZXdHMNCs8RgCsxizZKEmUcxIsWuBprtPBbvwo3PJMk9v4QbIpXMQW751n46ykQxpPOdfi8QymZyhI1GEfP4jaZdynxdmvvWpmOUt8oOx6KYWJ9JFNBs80Kld/6WoXEmPUpEbURoBwlzpLnGuv04HL+kbucd0BmOc4XcaiPSxwsn/Igc2cImCkAvPbmzxDv1MDEAiN5rCEPC661uLJZQz9QRY5HgAQIDAQAB
-----END PUBLIC KEY-----"""




alipay = AliPay(
    appid="9021000131667534",
    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?'
alipay_url = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?'
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="20161112",  # 订单号, 必须唯一
    total_amount=100000,  # 总金额
    subject=subject,  # 订单标题
    return_url="https://www.cnblogs.com/coderxueshan/",  # 同步回调(支付成功)
    notify_url="https://www.cnblogs.com/coderxueshan/"  # 异步回调(订单状态) 可选, 不填则使用默认notify url
)

# print(order_string)
print(alipay_url + order_string)

4. 注意事项

'''
支付宝的8次异步回调: 
    网页&移动支付 -> 支付能力 -> 电脑网站支付 -> 快速接入 -> 支付结果异步通知 8次异步回调 'success'

重要: 异步回调的验签, 一定要在验签完毕以后再修改订单状态!!!
    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. pem/al_public_key.pem

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvvyDeDgF3CB+lyi2t2lvP1b8UcVGhdK0a7X9a30ZDNhcAa9zOM8dHgfhbScso6w24T8UK0TUcEfPsXiECLDtDp63/z+EyisF1udCq/af2xa2T+9jNj5Vi3xDnZXdHMNCs8RgCsxizZKEmUcxIsWuBprtPBbvwo3PJMk9v4QbIpXMQW751n46ykQxpPOdfi8QymZyhI1GEfP4jaZdynxdmvvWpmOUt8oOx6KYWJ9JFNBs80Kld/6WoXEmPUpEbURoBwlzpLnGuv04HL+kbucd0BmOc4XcaiPSxwsn/Igc2cImCkAvPbmzxDv1MDEAiN5rCEPC661uLJZQz9QRY5HgAQIDAQAB
-----END PUBLIC KEY-----

6.pem/private_key.pem

-----BEGIN rsa2 PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCg2m4dB5cgqAzh4YrKphaZyGXzMbbtH1izLFNdUPPLsKaS+tfRb5PYlxq23SwH9nyUwBpSQ6rVTpa8fy2BCM93Iqpfz2Q8efzVMyve/mHNgAXFuENOb2AgRqH2+eGfmnxiF04J5WMG+cIkN7L7XUgxCl8S5pplfpxeXvuwGCoUCydqUlrLAjRiKdaqnXzJjLi2bj44KmhvWMjhTuaMRLgYUkhtp7YhGLGMrS6YVPuxNSaEJdXiR5XsFPwqz3/eriQ5txgYiDw643C1Kw0tLvaJYzaksrCuBEb6n3+KwiXi5JOoYJc3h9nRqFWgsXm6smc6Z2biaFEbPDN7j8C8evD5AgMBAAECggEAcAVO1Ea5+BMwzYpuRwz+BmEqpwBDXcYL1tQSxpUfBAblKs0oygGDnD43a4yCIpWFU26ppgrcCStvDJ0hSpChM13f+1OKgheOfcfiUK0l3aC/5F+b+B7WegPWvhJuD+Hdp86FGZ03pt7/Vou3yRjAsu5/IcGZWv+e1JYfgBaED2pObJ7Wk2ymktwbyTCDplRXH+wVVqgoDpaBHB9v6Z04BK63Hj8PRsZ8+vCzJ0jrCPXRLKoGm8pEYNbBLSExgIqhDAORn2FnU4wcrM+D0Z3QbhtCMdQWm6ctR6MIrlV+KOMrM3nyRlLCFwNj1XFqGR7kIdy4WxgxUkYXoSNhMJh90QKBgQDQeOWT5cak+pKgVyt6+segdh2tD4qMGBRJPonafKj8uHZl2xzaH6AC0Xqs7ijhS+RdbbckwKeVkrGX+68ylUQytDnV7dCJk56g6cfZN2GIohJweUv+Jf/bgI436r/Zyix3dnoImIMnKGfirVnH/Msd/CUg5BWIOVVeIxPIzUBmzQKBgQDFhlYpDpNEmxr96ibTNYVlfy3bbFbLQeCZOHQtLRmEAMbbqZD7Qo3KYZQf96eMs7xXv+guYCS+KZfhVSLg5P1d7D4Y/0Up9Pkq5tqKveNzMAyPu9kS0RTJiPl05KUt+tDYZTpIr85OIHlKmbQwbGqErW4PxEon3jso1Y2yWXb63QKBgQC7rMrIzWd6KFYN4Mu058UFMLBglwgcPKUulw1VUzpyYMG9ynb76tKLFviAa9sDj+XjMh1ZCdMUdT5J23uKZxRBLTyP2YsN/4YjyLJwW0oDzhwZgyklgCIJAn+F2WCjeT10woTz7hDMFLUJPRQBVROqR89I/+xeeXbfy2ZJNHYQ1QKBgQCoohLHBJGIHd+Cbbht0yCq0VRGI41KBFkKlp7gvsMs5jjf6jlDucMxx/LdA+MAhaZDSCyiAyT7UKlBEB0x4W3KFNnDH5RdyK49CVAE6S7Y9WmUAKzHmsbjdFR8joPS6HIKfQmwap94JdNHdEfYm4ao9SOkxFEHDnx1VTSe0jB45QKBgEMBHOKJi4pzo79FinfpWlFzQQPQuYvw3sxxuxLdXueKPUk9JNTU8U7pyy1uXJRXPea9JBHuy6ioQAzI8DFp2cQ/eWPMvZcQFdKITuqoTi/vKf7f41r5oYWfRZ8+R7rRc1HQORMre79z5WcC/DxJgtUP8e6wJEIEnFeh/ysYDSGx
-----END rsa2 PRIVATE KEY-----

7. __init__.py

from .pay import alipay, gateway

8. pay.py

from alipay import AliPay
from luffyapi.libs.al_pay import setting

app_private_key_string = setting.APP_PRIVATE_KEY_STRING

alipay_public_key_string = setting.ALIPAY_PUBLIC_KEY_STRING

alipay = AliPay(
    appid=setting.APPID,
    app_notify_url=None,  # 默认回调url
    app_private_key_string=app_private_key_string,
    # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
    alipay_public_key_string=alipay_public_key_string,
    sign_type=setting.SIGN_TYPE,  # rsa2 或者 RSA2
    debug=setting.DEBUG  # 默认False
)

gateway = setting.ALIPAY_GATEWAY
order_string = alipay.api_alipay_trade_page_pay(
    out_trade_no="20161112w",  # 订单号, 必须唯一
    total_amount=100000,  # 总金额
    subject='笔记本电脑',  # 订单标题
    return_url="https://www.cnblogs.com/coderxueshan/",  # 同步回调(支付成功)
    notify_url="https://www.cnblogs.com/coderxueshan/"  # 异步回调(订单状态) 可选, 不填则使用默认notify url
)

if __name__ == '__main__':
    print(gateway + order_string)

9. setting.py

import os

APPID = "9021000131667534"

# 默认回调
APP_NOTIFY_URL = None

# 阿里公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(__file__), 'pem', 'al_public_key.pem')).read()

# 自己私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(__file__), 'pem', 'private_key.pem')).read()

# 标签加密类型
SIGN_TYPE = "RSA2"

# True表示测试沙箱环境
DEBUG = True

# 阿里网关
ALIPAY_GATEWAY = 'https://openapi-sandbox.dl.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

from django.db import models

# Create your models here.

"""
class Order(models.Model):
    # 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)
    pass

class OrderDetail(models.Model):
    # 订单号(外键)、商品(外键)、实价、成交价 - 商品数量
    pass
"""
from django.db import models
from user.models import UserInfo
from course.models import Course
from luffyapi 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(UserInfo, 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.logger.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 django.shortcuts import render
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from . import models
from . import serializers
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView


# 支付页面
class PayView(GenericViewSet, CreateModelMixin):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated]
    queryset = models.Order.objects.all()
    serializer_class = serializers.OrderSerializer

    # 重写create方法,传request对象
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return Response(serializer.context.get('pay_url'))


# 不跟数据库打交道,就继承APIView
class SuccessView(APIView):
    # 支持成功后,支付宝回调给前端页面,前端再回调给后端
    def get(self, request, *args, **kwargs):
        out_trade_no = request.query_params.get('out_trade_no')
        order = models.Order.objects.filter(out_trade_no=out_trade_no)
        if order.order_status == 1:
            return Response(True)
        else:
            return Response(False)

    # 支付宝回调接口
    def post(self, request, *args, **kwargs):
        data = request.data
        # 取出支付宝回调中的订单号
        out_trade_no = data.get('out_trade_no', None)
        # 取出支付宝回调中的支付时间
        gmt_payment = data.get('gmt_payment', None)
        signature = data.pop("sign")

        # verification验证签名
        from luffyapi.libs.al_pay import alipay
        from luffyapi.utils.logging import logger
        success = alipay.verify(data, signature)
        if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
            print("trade succeed")
            # 支付成功后,改订单
            models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, pay_time=gmt_payment)
            # 记录日志
            logger.info('{}订单支付成功'.format(out_trade_no))
            return Response('success')
        else:
            logger.info('{}订单有问题'.format(out_trade_no))
            return Response('error')

3) order/serializer.py

from rest_framework import serializers
from . import models
from rest_framework.exceptions import ValidationError
from django.conf import settings


class OrderSerializer(serializers.ModelSerializer):
    # 可以再局部钩子中把course_list=[1, 2, 3]生成course_list=[obj1, obj2, obj3] 或者使用 PrimayKeyRElatedField
    course = serializers.PrimaryKeyRelatedField(write_only=True, many=True, queryset=models.Course.objects.all())

    class Meta:
        model = models.Order
        fields = ['total_amount', 'subject', 'pay_type', 'course']
        extra_kwargs = {
            # 序列化中让所有的fields中的字段必填. 有默认值的字段, 就不是必填的. required=True
            'total_amount': {'required': True},
            'pay_type': {'required': True},
        }
    def _check_price(self, attrs):
        total_amount = attrs.get('total_amount')
        course_list = attrs.get('course')
        total_price = 0
        for course in course_list:
            total_price += course.price
        if total_amount != total_price:
            raise ValidationError('价格不合法')
        return total_amount

    def _order_number(self):
        import uuid
        return str(uuid.uuid1()).replace('-', '')

    def _get_user(self):
        # 需要request对象(需要视图通过context把request对象传入,重写create方法)
        request = self.context.get('request')
        return request.user

    def _pay_url(self, out_trade_no, total_amount, subject):
        from luffyapi.libs.al_pay import gateway, alipay
        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=float(total_amount),  # 总金额
            subject=subject,  # 订单标题
            return_url=settings.RETURN_URL,  # get回调,前台地址
            notify_url=settings.NOTIFY_URL  # post回调,后台地址
        )
        return gateway + order_string

    def _before_create(self, attrs, user, pay_url,out_trade_no):
        attrs['user'] = user
        self.context['pay_url'] = pay_url
        attrs['out_trade_no'] = out_trade_no

    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._check_price(attrs)
        # 2 生成订单号
        out_trade_no = self._order_number()
        # 3 支付用户:request.user
        user = self._get_user()
        from luffyapi.utils.logging import logger
        logger.info('{}下了订单'.format(user))
        # 4. 生成支付连接
        subject = attrs.get('subject')
        pay_url = self._pay_url(out_trade_no, total_amount, subject)
        # 5. 入库(订单, 订单详情)两个表的准备
        self._before_create(attrs, user, pay_url, out_trade_no)
        return attrs

    def create(self, validated_data):
        course_list = validated_data.pop('course')
        order = models.Order.objects.create(**validated_data)
        for course in course_list:
            models.OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
        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
'''
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

 

posted @ 2023-11-26 15:36  coder雪山  阅读(212)  评论(0编辑  收藏  举报