• 支付


    支付宝支付

    入门

    """
    1)支付宝API:六大接口
    https://docs.open.alipay.com/270/105900/
    
    2)支付宝工作流程(见下图):
    https://docs.open.alipay.com/270/105898/
    
    3)支付宝8次异步通知机制(支付宝对我们服务器发送POST请求,索要 success 7个字符)
    https://docs.open.alipay.com/270/105902/
    """
    # 1、在沙箱环境下实名认证:https://openhome.alipay.com/platform/appDaily.htm?tab=info
    
    # 2、电脑网站支付API:https://docs.open.alipay.com/270/105900/
    
    # 3、完成RSA密钥生成:https://docs.open.alipay.com/291/105971
    
    # 4、在开发中心的沙箱应用下设置应用公钥:填入生成的公钥文件中的内容
    
    # 5、Python支付宝开源框架:https://github.com/fzlee/alipay
    # >: pip install python-alipay-sdk --upgrade
    
    # 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
    """

    支付流程

    aliapy二次封装包

    ##GitHub开源框架
    https://github.com/fzlee/alipay
    
    
    ##依赖
    >: pip install python-alipay-sdk --upgrade
    # 如果抛ssl相关错误,代表缺失该包
    >: pip install pyopenssl
    
    
    ##结构
    libs
        ├── iPay                              # aliapy二次封装包
        │   ├── __init__.py                 # 包文件
        │   ├── pem                            # 公钥私钥文件夹
        │   │   ├── alipay_public_key.pem    # 支付宝公钥文件
        │   │   ├── app_private_key.pem        # 应用私钥文件
        │   ├── pay.py                        # 支付文件
        └── └── settings.py                  # 应用配置
    
    
    ##alipay_public_key.pem
    -----BEGIN PUBLIC KEY-----
    拿应用公钥跟支付宝换来的支付宝公钥
    -----END PUBLIC KEY-----
    
    
    ##app_private_key.pem
    -----BEGIN RSA PRIVATE KEY-----
    通过支付宝公钥私钥签发软件签发的应用私钥
    -----END RSA PRIVATE KEY-----
    
    ##setting.py
    import os
    # 应用私钥
    APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
    
    # 支付宝公钥
    ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
    
    # 应用ID
    APP_ID = '2016093000631831'
    
    # 加密方式
    SIGN = 'RSA2'
    
    # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
    DEBUG = True
    
    # 支付网关
    GATEWAY = 'https://openapi.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
    
    
    ##pay.py
    from alipay import AliPay
    from . import settings
    
    # 支付对象
    alipay = AliPay(
        appid=settings.APP_ID,
        app_notify_url=None,
        app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
        alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
        sign_type=settings.SIGN,
        debug=settings.DEBUG
    )
    
    # 支付网关
    gateway = settings.GATEWAY
    
    
    ##_init_.py
    # 包对外提供的变量
    from .pay import gateway, alipay
    
    
    
    ##补充:在自己项目的配置文件中配置支付宝回调接口:settings.py | dev.py
    
    # 上线后必须换成公网地址
    # 后台基URL
    BASE_URL = 'http://127.0.0.1:8000'
    # 前台基URL
    LUFFY_URL = 'http://127.0.0.1:8080'
    # 支付宝同步异步回调接口配置
    # 后台异步回调接口
    NOTIFY_URL = BASE_URL + "/order/success/"
    # 前台同步回调接口,没有 / 结尾
    RETURN_URL = LUFFY_URL + "/pay/success"
    View Code

    后台 - 支付接口

    路由:order/urls.py
    from django.urls import path, include
    from utils.router import router
    from . import views
    
    """
    1)支付接口(需要登录认证:是谁):前台提交商品等信息,得到支付链接
        post方法
    
    分析:支付宝回调
        同步:get给前台 => 前台可以在收到支付宝同步get回调时,ajax异步在给消息同步给后台,也采用get,后台处理前台的get请求
        异步:post给后台 => 后台直接处理支付宝的post请求
    2)支付回调接口(不需要登录认证:哪个订单(订单信息中有非对称加密)、支付宝压根不可能有你的token):
        get方法:处理前台来的同步回调(不一定能收得到,所有不能在该方法完成后台订单状态等信息操作)
        post方法:处理支付宝来的异步回调
        
    3)订单状态确认接口:随你前台任何时候来校验订单状态的接口
    """
    # 支付接口(生成订单)
    router.register('pay', views.PayViewSet, 'pay')
    
    
    urlpatterns = [
        path('', include(router.urls)),
    
        path('success/', views.SuccessViewSet.as_view({'get': 'get', 'post': 'post'}))
    ]
    View Code
    模型表:order/models.py
    """
    class Order(models.Model):
        # 主键、总金额、订单名、订单号、订单状态、创建时间、支付时间、流水号、支付方式、支付人(外键) - 优惠劵(外键,可为空)
        pass
    
    class OrderDetail(models.Model):
        # 订单号(外键)、商品(外键)、实价、成交价 - 商品数量
        pass
    """
    
    
    
    from django.db import models
    from user.models import User
    from course.models import Course
    
    
    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="支付时间")
        user = models.ForeignKey(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='创建时间')
    
        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):
        """订单详情"""
        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.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 = "订单详情"
    
        def __str__(self):
            try:
                return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
            except:
                return super().__str__()
    View Code
    支付接口类:order/views.py
    from rest_framework.viewsets import GenericViewSet, ViewSet
    from rest_framework.mixins import CreateModelMixin
    from rest_framework.permissions import IsAuthenticated
    from rest_framework.response import Response
    from . import models, serializers
    
    
    # 支付接口
    class PayViewSet(GenericViewSet, CreateModelMixin):
        permission_classes = [IsAuthenticated]
        queryset = models.Order.objects.all()
        serializer_class = serializers.PaySerializer
    
        # 重写create方法,返回pay_url,pay_url是在serializer对象中,所以要知道serializer
        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['pay_url'])
    View Code
    支付接口序列化类:model/serializers
    from rest_framework import serializers
    from . import models
    from course.models import Course
    from rest_framework.exceptions import ValidationError
    from django.conf import settings
    
    
    class PaySerializer(serializers.ModelSerializer):
        # 要支持单购物和群购物(购物车),前台要提交 课程主键(们)
        courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)
    
        class Meta:
            model = models.Order
            fields = ('subject', 'total_amount', 'pay_type', 'courses')
            extra_kwargs = {
                'total_amount': {
                    'required': True
                },
                'pay_type': {
                    'required': True
                },
            }
    
        # 订单总结校验
        def _check_total_amount(self, attrs):
            courses = attrs.get('courses')
            total_amount = attrs.get('total_amount')
            total_price = 0
            for course in courses:
                total_price += course.price
            if total_price != total_amount:
                raise ValidationError('total_amount error')
            return total_amount
    
        # 生成订单号
        def _get_out_trade_no(self):
            import uuid
            code = '%s' % uuid.uuid4()
            return code.replace('-', '')
    
        # 获取支付人
        def _get_user(self):
            return self.context.get('request').user
    
        # 获取支付链接
        def _get_pay_url(self, out_trade_no, total_amount, subject):
            from libs import iPay
            order_string = iPay.alipay.api_alipay_trade_page_pay(
                out_trade_no=out_trade_no,
                total_amount=float(total_amount),  # 只有生成支付宝链接时,不能用Decimal
                subject=subject,
                return_url=settings.RETURN_URL,
                notify_url=settings.NOTIFY_URL,
            )
            pay_url = iPay.gateway + '?' + order_string
            # 将支付链接存入,传递给views
            self.context['pay_url'] = pay_url
    
        # 入库(两个表)的信息准备
        def _before_create(self, attrs, user, out_trade_no):
            attrs['user'] = user
            attrs['out_trade_no'] = out_trade_no
    
        def validate(self, attrs):
            # 1)订单总价校验
            total_amount = self._check_total_amount(attrs)
            # 2)生成订单号
            out_trade_no = self._get_out_trade_no()
            # 3)支付用户:request.user
            user = self._get_user()
            # 4)支付链接生成
            self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
            # 5)入库(两个表)的信息准备
            self._before_create(attrs, user, out_trade_no)
    
            # 代表该校验方法通过,进入入库操作
            return attrs
    
    
    
        # 重写入库方法的目的:完成订单与订单详情两个表入库操作
        def create(self, validated_data):
            courses = validated_data.pop('courses')
            # 订单表入库,不需要courses
            order = models.Order.objects.create(**validated_data)
    
            # 订单详情表入库:只需要订单对象,课程对象(courses要拆成一个个course)
            for course in courses:
                models.OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
    
            # 先循环制造数据列表[{}, ..., {}],用群增完成入库 bulk_create(),效率高
    
            return order
    View Code

    前台 - 支付生成页面

    课程主页或是详情页或者搜索页
    <template>
        ...
        <span class="buy-now" @click="buy_course(course)">立即购买</span>
    </template>
    <script>
         export default {
            methods: {
                // 购买课程
                buy_course(course) {
                    // es6语法下,变量必须定义才能使用
                    let token = this.$cookies.get('token');
                    if (!token) {
                        this.$message({
                            message: '请先登录',
                            type: 'warning'
                        });
                        return false
                    }
                    this.$axios({
                        url: this.$settings.base_url + '/order/pay/',
                        method: 'post',
                        headers: {
                            authorization: 'luffy ' + token,
                        },
                        data: {
                            subject: course.name,
                            total_amount: course.price,
                            pay_type: 1,  // 现在只能默认1,为支付宝
                            courses: [course.id]  // 后台完成的是基于购物车情况下的接口,所以单购物为群购1个
                        }
                    }).then(response => {
                        let pay_url = response.data;
                        // console.log(pay_url)
                        // 本页面标签调整:可以选择 _self 或 _blank
                        open(pay_url, '_self');
                    }).catch(error => {
                        console.log(error.response.data)
                    })
                },
            }
        }
    </script>
    View Code

    前台 - 支付成功的回调页面

    router/index.js
    import PaySuccess from '../views/PaySuccess.vue'
    // ...
    const routes = [
        // ...
        {
            path: '/pay/success',
            name: 'pay-success',
            component: PaySuccess
        },
    ];
    View Code
    同步回调的参数
    `
    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
    `
    // 同步回调没与订单状态
    View Code
    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;
             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;
             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;
     140px;
    height: 42px;
    text-align: center;
    line-height: 42px;
    cursor: pointer;
    background: #ffc210;
    border-radius: 6px;
    font-size: 16px;
    color: #fff;
        }
    </style>
    View Code

    后台 - 支付成功的回调接口

    from utils.logging import logger
    # 支付回调接口
    class SuccessViewSet(ViewSet):
        authentication_classes = ()
        permission_classes = ()
    
        # 支付宝同步回调给前台,在同步通知给后台处理
        def get(self, request, *args, **kwargs):
            # return Response('后台已知晓,Over!!!')
    
            # 不能在该接口完成订单修改操作
            # 但是可以在该接口中校验订单状态(已经收到支付宝post异步通知,订单已修改),告诉前台
            # print(type(request.query_params))  # django.http.request.QueryDict
            # print(type(request.query_params.dict()))  # dict
    
            out_trade_no = request.query_params.get('out_trade_no')
            try:
                models.Order.objects.get(out_trade_no=out_trade_no, order_status=1)
                return APIResponse(result=True)
            except:
                return APIResponse(1, 'error', result=False)
    
    
        # 支付宝异步回调处理
        def post(self, request, *args, **kwargs):
            try:
                result_data = request.data.dict()
                out_trade_no = result_data.get('out_trade_no')
                signature = result_data.pop('sign')
                from libs import iPay
                result = iPay.alipay.verify(result_data, signature)
                if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
                    # 完成订单修改:订单状态、流水号、支付时间
                    models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
                    # 完成日志记录
                    logger.warning('%s订单支付成功' % out_trade_no)
                    return Response('success')
                else:
                    logger.error('%s订单支付失败' % out_trade_no)
            except:
                pass
            return Response('failed')
    View Code
  • 相关阅读:
    十大排序算法
    SQL优化指南
    Python基础-类与对象
    Python基础-函数
    Python基础-字典
    Python基础-字符串
    Python基础-列表
    以太坊智能合约开发框架Truffle
    比特币钱包搭建与使用
    矩阵的压缩存储
  • 原文地址:https://www.cnblogs.com/bubu99/p/13767874.html
Copyright © 2020-2023  润新知