支付
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
""" # 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. 目的就是防止恶意软件. 如果你需要安装, 只需要将后缀名修改过来即可 '''
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)
''' 支付宝的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
''' 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放到外面书写和订单一起. '''
libs ├── iPay # aliapy二次封装包 │ ├── __init__.py # 包文件 │ ├── pem # 公钥私钥文件夹 │ │ ├── alipay_public_key.pem # 支付宝公钥文件 │ │ ├── app_private_key.pem # 应用私钥文件 │ ├── pay.py # 支付文件 └── └── settings.py # 应用配置
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvvyDeDgF3CB+lyi2t2lvP1b8UcVGhdK0a7X9a30ZDNhcAa9zOM8dHgfhbScso6w24T8UK0TUcEfPsXiECLDtDp63/z+EyisF1udCq/af2xa2T+9jNj5Vi3xDnZXdHMNCs8RgCsxizZKEmUcxIsWuBprtPBbvwo3PJMk9v4QbIpXMQW751n46ykQxpPOdfi8QymZyhI1GEfP4jaZdynxdmvvWpmOUt8oOx6KYWJ9JFNBs80Kld/6WoXEmPUpEbURoBwlzpLnGuv04HL+kbucd0BmOc4XcaiPSxwsn/Igc2cImCkAvPbmzxDv1MDEAiN5rCEPC661uLJZQz9QRY5HgAQIDAQAB -----END PUBLIC KEY-----
-----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-----
from .pay import alipay, gateway
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)
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?"
# 本地的地址,支付宝回调不了,必须是公网地址,才能回调 # 后台基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. 数据迁移 '''
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请求.(比较绕) '''
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')
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
# 后台基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"
path('order/', include('order.urls')),
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接口 '''
# 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号 页面需要的参数: 订单号, 交易号, 付款时间 '''
import PaySuccess from '../views/PaySuccess.vue' const routes = [ ... { path: '/pay/success', name: 'PaySuccess', component: PaySuccess }, ];
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 # 付款时间 ` // 同步回调没与订单状态
<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 // 同步回调没与订单状态
path('success/', views.SuccessView.as_view())
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='失败')
# 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') '''
# 配置src/assets/js/settings.py文件 export default { // 注意: 这里的8000的端口是nginx的监听端口 base_url: 'http://139.196.184.91:8000' } # 将vue代码打包成html, css, js cmpn run build