一.课程详情页面CourseDetail.vue

<template>
    <div class="detail">
        <Header/>
        <div class="main">
            <div class="course-info">
                <div class="wrap-left">
                    <videoPlayer class="video-player vjs-custom-skin"
                                 ref="videoPlayer"
                                 :playsinline="true"
                                 :options="playerOptions"
                                 @play="onPlayerPlay($event)"
                                 @pause="onPlayerPause($event)">
                    </videoPlayer>
                </div>
                <div class="wrap-right">
                    <h3 class="course-name">{{course_info.name}}</h3>
                    <p class="data">{{course_info.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_info.sections}}课时/{{course_info.pub_sections}}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course_info.level_name}}</p>
                    <div v-if="course_info.active_time>0">
                        <div class="sale-time">
                            <p class="sale-type">{{course_info.discount_type}}</p>
                            <p class="expire">距离结束:仅剩{{day}}天 {{hour}}小时 {{minute}}分 <span
                                    class="second">{{second}}</span> 秒</p>
                        </div>
                        <p class="course-price">
                            <span>活动价</span>
                            <span class="discount">¥{{course_info.real_price}}</span>
                            <span class="original">¥{{course_info.price}}</span>
                        </p>
                    </div>
                    <div v-else class="sale-time">
                        <p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
                        <p class="expire"></p>
                    </div>
                    <div class="buy">
                        <div class="buy-btn">
                            <button class="buy-now">立即购买</button>
                            <button class="free">免费试学</button>
                        </div>
                        <!--<div class="add-cart" @click="add_cart(course_info.id)"><img src="@/assets/img/cart-yellow.svg"-->
                                                                                     <!--alt="">加入购物车-->
                        <!--</div>-->
                    </div>
                </div>
            </div>
            <div class="course-tab">
                <ul class="tab-list">
                    <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
                    <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
                    </li>
                    <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
                    <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
                </ul>
            </div>
            <div class="course-content">
                <div class="course-tab-list">
                    <div class="tab-item" v-if="tabIndex==1">
                        <div class="course-brief" v-html="course_info.brief_text"></div>
                    </div>
                    <div class="tab-item" v-if="tabIndex==2">
                        <div class="tab-item-title">
                            <p class="chapter">课程章节</p>
                            <p class="chapter-length">共{{course_chapters.length}}章 {{course_info.sections}}个课时</p>
                        </div>
                        <div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
                            <p class="chapter-title"><img src="@/assets/img/enum.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}
                            </p>
                            <ul class="section-list">
                                <li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
                                    <p class="name"><span class="index">{{chapter.chapter}}-{{section.orders}}</span>
                                        {{section.name}}<span class="free" v-if="section.free_trail">免费</span></p>
                                    <p class="time">{{section.duration}} <img src="@/assets/img/chapter-player.svg"></p>
                                    <button class="try" v-if="section.free_trail">立即试学</button>
                                    <button class="try" v-else>立即购买</button>
                                </li>
                            </ul>
                        </div>
                    </div>
                    <div class="tab-item" v-if="tabIndex==3">
                        用户评论
                    </div>
                    <div class="tab-item" v-if="tabIndex==4">
                        常见问题
                    </div>
                </div>
                <div class="course-side">
                    <div class="teacher-info">
                        <h4 class="side-title"><span>授课老师</span></h4>
                        <div class="teacher-content">
                            <div class="cont1">
                                <img :src="course_info.teacher.image">
                                <div class="name">
                                    <p class="teacher-name">{{course_info.teacher.name}}
                                        {{course_info.teacher.title}}</p>
                                    <p class="teacher-title">{{course_info.teacher.signature}}</p>
                                </div>
                            </div>
                            <p class="narrative">{{course_info.teacher.brief}}</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <Footer/>
    </div>
</template>

<script>
    import Header from "@/components/Header"
    import Footer from "@/components/Footer"

    // 加载组件
    import {videoPlayer} from 'vue-video-player';

    export default {
        name: "Detail",
        data() {
            return {
                tabIndex: 2,   // 当前选项卡显示的下标
                course_id: 0, // 当前课程信息的ID
                course_info: {
                    teacher: {},
                }, // 课程信息
                course_chapters: [], // 课程的章节课时列表
                playerOptions: {
                    aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9""4:3")
                    sources: [{ // 播放资源和资源格式
                        type: "video/mp4",
                        src: "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4" //你的视频地址(必填)
                    }],
                }
            }
        },
        computed: {
            day() {
                let day = parseInt(this.course_info.active_time / (24 * 3600));
                if (day < 10) {
                    return '0' + day;
                } else {
                    return day;
                }
            },
            hour() {
                let rest = parseInt(this.course_info.active_time % (24 * 3600));
                let hours = parseInt(rest / 3600);
                if (hours < 10) {
                    return '0' + hours;
                } else {
                    return hours;
                }
            },
            minute() {
                let rest = parseInt(this.course_info.active_time % 3600);
                let minute = parseInt(rest / 60);
                if (minute < 10) {
                    return '0' + minute;
                } else {
                    return minute;
                }
            },
            second() {
                let second = this.course_info.active_time % 60;
                if (second < 10) {
                    return '0' + second;
                } else {
                    return second;
                }
            }
        },
        created() {
            this.get_course_id();
            this.get_course_data();
            this.get_chapter();
        },
        methods: {
            onPlayerPlay() {
                // 当视频播放时,执行的方法
            },
            onPlayerPause() {
                // 当视频暂停播放时,执行的方法
            },
            get_course_id() {
                // 获取地址栏上面的课程ID
                this.course_id = this.$route.params.pk;
                if (this.course_id < 1) {
                    let _this = this;
                    _this.$alert("对不起,当前视频不存在!", "警告", {
                        callback() {
                            _this.$router.go(-1);
                        }
                    });
                }
            },
            get_course_data() {
                // ajax请求课程信息
                this.$axios.get(`${this.$settings.base_url}/course/${this.course_id}/`).then(response => {
                    // window.console.log(response.data);
                    this.course_info = response.data;
                }).catch(() => {
                    this.$message({
                        message: "对不起,访问页面出错!请联系客服工作人员!"
                    });
                })
            },

            get_chapter() {
                // 获取当前课程对应的章节课时信息
                // http://127.0.0.1:8000/course/chapters/?course=(pk)
                this.$axios.get(`${this.$settings.base_url}/course/chapters/`, {
                    params: {
                        "course": this.course_id,
                    }
                }).then(response => {
                    this.course_chapters = response.data;
                }).catch(error => {
                    window.console.log(error.response);
                })
            },
            // add_cart(course_id) {
            //     // 添加商品到购物车
            //     // 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车
            //     let token = localStorage.token || sessionStorage.token;
            //     if (!token) {
            //         this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(() => {
            //             this.$router.push("/login/");
            //         });
            //         return false; // 阻止代码往下执行
            //     }
            //
            //     // 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
            //     this.$axios.post(`${this.$settings.Host}/cart/`, {
            //         "course_id": course_id,
            //     }, {
            //         headers: {
            //             "Authorization": "jwt " + token,
            //         }
            //     }).then(response => {
            //         this.$message({
            //             message: response.data.message,
            //         });
            //         // 购物车中的商品数量
            //         let total = response.data.total;
            //         this.$store.commit("change_total", total)
            //     }).catch(error => {
            //         this.$message({
            //             message: error.response.data
            //         })
            //     })
            // }
        },
        components: {
            Header,
            Footer,
            videoPlayer, // 注册组件
        }
    }
</script>

路由router.js

import CourseDetail from './views/CourseDetail.vue'

{
            path: '/course/detail/:pk',
            name: 'course-detail',
            component: CourseDetail
        },
 
依赖:在luffycity目录下的命令
>: cnpm install vue-video-player
配置:main.js
// vue-video播放器
require('video.js/dist/video-js.css');
require('vue-video-player/src/custom-theme.css');
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer);
资源:图片放置assrts/img文件夹
"""
enum.svg
chapter-player.svg
cart-yellow.svg
"""
Course.vue中的转跳链接:
 <router-link :to="'/course/detail/'+course.id">{{course.name}}</router-link>

二.课程详情接口

路由course/urls.py:

    from django.urls import path, re_path

    from . import views

    re_path('(?P<pk>\d+)/', views.CourseRetrieveAPIView.as_view()),
    path('chapters/', views.ChapterListAPIView.as_view()),

视图views.py:

from rest_framework.generics import RetrieveAPIView
class CourseRetrieveAPIView(RetrieveAPIView):
    queryset = models.Course.objects.filter(is_delete=False, is_show=True)
    serializer_class = serializers.CourseModelSerializer


from .filters import ChapterFilterSet
class ChapterListAPIView(ListAPIView):
    queryset = models.CourseChapter.objects.filter(is_delete=False, is_show=True)
    serializer_class = serializers.CourseChapterModelSerializer

    filter_backends = [DjangoFilterBackend]
    # filter_fields = ('course',)
    filter_class = ChapterFilterSet

序列化类serializers.py:

class CourseModelSerializer(ModelSerializer):
    teacher = TeacherModelSerializer()
    class Meta:
        model = models.Course
        fields = (
            'id',
            'name',
            'course_img',
            'brief',
            'period',
            'attachment_path',
            'students',
            'sections',
            'pub_sections',
            'price',
            'teacher',
            'section_list',
            'level_name',
        )

class CourseSectionModelSerializer(ModelSerializer):
    class Meta:
        model = models.CourseSection
        fields = ('name', 'section_link', 'name', 'free_trail', 'orders')

class CourseChapterModelSerializer(ModelSerializer):
    coursesections = CourseSectionModelSerializer(many=True)
    class Meta:
        model = models.CourseChapter
        fields = ('course', 'chapter', 'name', 'summary', 'coursesections')

添加难度字段level_name   =>  models.py/class Course(BaseModel):

 

    @property
    def level_name(self):
        return self.get_level_display()

三.订单模块

 创建apps/order:

cd luffyapi/apps
python ../../manage.py startapp order

路由:

主:
path('order/', include('order.urls')),
子:
from django.urls import path
from . import views
urlpatterns = [
    path('pay/', views.PayAPIView.as_view()),
    path('success/', views.SuccessAPIView.as_view()),
]

models.py:

"""
订单:订单号、流水号、价格、用户
订单详情(自定义关系表):订单、课程
"""

from django.db import models
from utils.model import BaseModel
from user.models import User
from course.models import Course


class Order(BaseModel):
    """订单模型"""
    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='user_orders', on_delete=models.DO_NOTHING, db_constraint=False,
                             verbose_name="下单用户")

    # 多余字段
    orders = models.IntegerField(verbose_name='显示顺序', default=0)

    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(BaseModel):
    """订单详情"""
    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):
        return "%s订单(%s)" % (self.course.name, self.order.order_number)

注:数据库迁移

四.支付宝应用开发

# 1、在沙箱环境下实名认证:https://openhome.alipay.com/platform/appDaily.htm?tab=info

# 2、电脑网站支付API:https://docs.open.alipay.com/270/105898/

# 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
"""

RSA:

支付宝公钥:

前台后台支付宝交互原理图:

沙箱测试账号:

五.alipay二次封装包

依赖
>: pip install python-alipay-sdk --upgrade
结构
libs
   ├── iPay # aliapy二次封装包
   │   ├── __init__.py # 包文件
   │   ├── keys # 密钥文件夹
   │   │   ├── alipay_public_key.pem # 支付宝公钥
   │   │   └── app_private_key.pem # 应用私钥
   └── └── settings.py # 应用配置  
setting.py
import os
# 支付宝应用id
APP_ID = '2016093000631831'
# 默认异步回调的地址,通常设置None就行
APP_NOTIFY_URL = None
# 应用私钥文件路径
APP_PRIVATE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'keys', 'app_private_key.pem')
# 支付宝公钥文件路径
ALIPAY_PUBLIC_KEY_PATH = os.path.join(os.path.dirname(__file__), 'keys', 'alipay_public_key.pem')
# 签名方式
SIGN_TYPE = 'RSA2'
# 是否是测试环境
DEBUG = True
__init__.py
from alipay import AliPay
from .settings import *
# 对外提供,放到自己的dev配置文件中
# from .settings import RETURN_URL, NOTIFY_URL
# 对外提供支付对象
alipay = AliPay(
    appid=APP_ID,
    app_notify_url=APP_NOTIFY_URL,
    app_private_key_path=APP_PRIVATE_KEY_PATH,
    alipay_public_key_path=ALIPAY_PUBLIC_KEY_PATH,
    sign_type=SIGN_TYPE,
    debug=DEBUG
)
alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----
app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
应用私钥
-----END RSA PRIVATE KEY-----
补充:dev.py
# 上线后必须换成官网地址
# 同步回调的接口(get),前后台分离时一般设置前台页面url
RETURN_URL = 'http://127.0.0.1:8080/pay/success'
# 异步回调的接口(post),一定设置为后台服务器接口
NOTIFY_URL = 'http://127.0.0.1:8000/order/success/'

六.订单接口

订单视图views.py:

# 1)生成订单
# 2)生成支付链接
# 3)第三方支付
# 4)修改订单状态

import time
from rest_framework.views import APIView
from utils.response import APIResponse
from libs.iPay import alipay
from . import authentications, serializers
from rest_framework.permissions import IsAuthenticated
from django.conf import settings
# 获取前台 商品名、价格,产生 订单、支付链接
class PayAPIView(APIView):
    authentication_classes = [authentications.JWTAuthentication]
    permission_classes = [IsAuthenticated]
    def post(self, request, *args, **kwargs):
        # 前台提供:商品名、总价、支付方式
        request_data = request.data
        # 后台产生:订单号、用户
        out_trade_no = '%d' % time.time() * 2
        request_data['out_trade_no'] = out_trade_no
        request_data['user'] = request.user.id

        # 反序列化数据,用于订单生成前的校验
        order_ser = serializers.OrderModelSerializer(data=request_data)
        if order_ser.is_valid():
            # 生成订单,订单默认状态为:未支付
            order = order_ser.save()
            # 支付链接的参数
            order_string = alipay.api_alipay_trade_page_pay(
                subject=order.subject,
                out_trade_no=order.out_trade_no,
                total_amount='%.2f' % order.total_amount,
                return_url=settings.RETURN_URL,
                notify_url=settings.NOTIFY_URL
            )
            # 形成支付链接:alipay._gateway根据字符环境DEBUG配置信息,决定是沙箱还是真实支付环境
            pay_url = '%s?%s' % (alipay._gateway, order_string)
            return APIResponse(0, 'ok', pay_url=pay_url)


        return APIResponse(1, 'no ok', results=order_ser.errors)

用户校验需要认证authentications.py:

import jwt
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.authentication import get_authorization_header
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication

class JWTAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # jwt_value = get_authorization_header(request)
        jwt_value = request.META.get('HTTP_AUTHORIZATION', b'')

        if not jwt_value:
            raise AuthenticationFailed('Authorization 字段是必须的')
        try:
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('签名过期')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('非法用户')
        user = self.authenticate_credentials(payload)

        return user, jwt_value

序列化类serializers.py:

from rest_framework import serializers
from . import models
class OrderModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Order
        fields = ('subject', 'total_amount', 'out_trade_no', 'pay_type', 'user')
        extra_kwargs = {
            'pay_type': {
                'required': True
            },
            'total_amount': {
                'required': True
            },
        }
        # 如果需要处理订单详情,前台一定要提供 课程主键(一个或多个)
        # 需要重写create方法:1)产生Order表对象  2)产生OrderDetail表对象 => 购物车逻辑
        # 需求可拓展:UserCourse user course

七.前台生成订单

Course.vue链接跳转支付:

  <span class="buy-now" @click="pay_course(course)">立即购买</span>


......

methods: {
            // 购买课程
            pay_course(course) {
                // 判断登录状态
                let token = this.$cookies.get('token');
                if (!token) {
                    this.$message.error('请先登录');
                    return
                }
                this.$axios({
                    url: this.$settings.base_url + '/order/pay/',
                    method: 'post',
                    data: {
                        'subject': course.name,
                        'total_amount': course.price,
                        // 如果有支付页面:1 支付宝  2 微信
                        'pay_type': 1,
                    },
                    headers: {
                        Authorization: token
                    }
                }).then(response => {
                    // console.log(response.data)
                    if (response.data.status == 0) {
                        location.href = response.data.pay_url;
                    } else {
                        this.$message({
                            message: '生成订单失败'
                        })
                    }
                }).catch(() => {
                    this.$message({
                        message: '生成订单失败'
                    })
                })
            },
......

八.支付完成后同步回调链接给前台渲染

 router.js:

import PaySuccess from './views/PaySuccess.vue'

Vue.use(Router);

 {
            path: '/pay/success',
            name: 'pay-success',
            component: PaySuccess
        },
  

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>
        <Footer/>
    </div>
</template>

<script>
    import Header from "@/components/Header"
    import Footer from "@/components/Footer"

    export default {
        name: "Success",
        data() {
            return {
                result: {},
            };
        },
        created() {
            // 判断登录状态
            let token = this.$cookies.get('token');
            if (!token) {
                this.$message.error('非法请求');
                this.$router.go(-1)
            }


            localStorage.this_nav = '/';
            if (!location.search.length) return;
            let params = location.search.substring(1);
            let items = params.length ? params.split('&') : [];
            //逐个将每一项添加到args对象中
            for (let i = 0; i < items.length; i++) {
                let k_v = items[i].split('=');
                //解码操作,因为查询字符串经过编码的
                let k = decodeURIComponent(k_v[0]);
                let v = decodeURIComponent(k_v[1]);
                this.result[k] = v;
                // this.result[k_v[0]] = k_v[1];
            }
            // console.log(this.result);

            // 把地址栏上面的支付结果,转发给后端
            this.$axios({
                url: this.$settings.base_url + '/order/success/' + location.search,
                method: 'patch',
                headers: {
                    Authorization: token
                }
            }).then(response => {
                console.log(response.data);
            }).catch(() => {
                console.log('支付结果同步失败');
            })
        },
        components: {
            Header,
            Footer,
        }
    }
</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>

页面如图:

九.同步回调到后端

视图order/views.py:

from . import models
from utils.logging import logger
from rest_framework.response import Response
class SuccessAPIView(APIView):
    # 不能认证,别人支付宝异步回调就进不来了
    # authentication_classes = [authentications.JWTAuthentication]
    # permission_classes = [IsAuthenticated]
    def patch(self, request, *args, **kwargs):
        # 默认是QueryDict类型,不能使用pop方法
        request_data = request.query_params.dict()
        # 必须将 sign、sign_type(内部有安全处理) 从数据中取出,拿sign与剩下的数据进行校验
        sign = request_data.pop('sign')
        result = alipay.verify(request_data, sign)
        if result:  # 同步回调:修改订单状态
            try:
                out_trade_no = request_data.get('out_trade_no')
                order = models.Order.objects.get(out_trade_no=out_trade_no)
                if order.order_status != 1:
                    order.order_status = 1
                    order.save()
            except:
                pass
            return APIResponse(0, '支付成功')
        return APIResponse(1, '支付失败')

    # 支付宝异步回调
    def post(self, request, *args, **kwargs):
        # 默认是QueryDict类型,不能使用pop方法
        request_data = request.data.dict()
        # 必须将 sign、sign_type(内部有安全处理) 从数据中取出,拿sign与剩下的数据进行校验
        sign = request_data.pop('sign')
        result = alipay.verify(request_data, sign)
        # 异步回调:修改订单状态
        if result and request_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED" ):
            out_trade_no = request_data.get('out_trade_no')
            logger.critical('%s支付成功' % out_trade_no)
            try:
                order = models.Order.objects.get(out_trade_no=out_trade_no)
                if order.order_status != 1:
                    order.order_status = 1
                    order.save()
            except:
                pass
            # 支付宝八次异步通知,订单成功一定要返回 success
            return Response('success')
        return Response('failed')
02-11 16:09