路飞项目

1 路飞项目头部组件

1.1 路由跳转的方式

#html中路由跳转
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>


#js中控制路由跳转
this.$router.push('/');

1.2 头部组件vue代码

# 在components内新建Head.vue
<template>
    <div class="header">
        <div class="slogan">
            <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
        </div>
        <div class="nav">
            <ul class="left-part">
                <li class="logo">
                    <router-link to="/">
                        <img src="../assets/img/head-logo.svg" alt="">
                    </router-link>
                </li>
                <li class="ele">
                    <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
                </li>
            </ul>

            <div class="right-part">
                <div>
                    <span>登录</span>
                    <span class="line">|</span>
                    <span>注册</span>
                </div>
    		</div>
        </div>
    </div>

</template>

<script>

    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
            }
        },
        methods: {
            goPage(url_path) {
                // 已经是当前路由就没有必要重新跳转
                if (this.url_path !== url_path) {
                    this.$router.push(url_path);
                }
                sessionStorage.url_path = url_path;
            },
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;
        }
    }
</script>

<style scoped>
    .header {
        background-color: white;
        box-shadow: 0 0 5px 0 #aaa;
    }

    .header:after {
        content: "";
        display: block;
        clear: both;
    }

    .slogan {
        background-color: #eee;
        height: 40px;
    }

    .slogan p {
        width: 1200px;
        margin: 0 auto;
        color: #aaa;
        font-size: 13px;
        line-height: 40px;
    }

    .nav {
        background-color: white;
        user-select: none;
        width: 1200px;
        margin: 0 auto;

    }

    .nav ul {
        padding: 15px 0;
        float: left;
    }

    .nav ul:after {
        clear: both;
        content: '';
        display: block;
    }

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

    .ele span {
        display: block;
        font: 15px/36px '微软雅黑';
        border-bottom: 2px solid transparent;
        cursor: pointer;
    }

    .ele span:hover {
        border-bottom-color: orange;
    }

    .ele span.active {
        color: orange;
        border-bottom-color: orange;
    }

    .right-part {
        float: right;
    }

    .right-part .line {
        margin: 0 10px;
    }

    .right-part span {
        line-height: 68px;
        cursor: pointer;
    }
</style>

2 路飞项目尾部组件

Footer.vue

<template>
    <div class="footer">
        <ul>
            <li>关于我们</li>
            <li>联系我们</li>
            <li>商务合作</li>
            <li>帮助中心</li>
            <li>意见反馈</li>
            <li>新手指南</li>
        </ul>
        <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p>
    </div>
</template>

<script>
    export default {
        name: "Footer"
    }
</script>

<style scoped>
    .footer {
        width: 100%;
        height: 128px;
        background: #25292e;
        color: #fff;
    }

    .footer ul {
        margin: 0 auto 16px;
        padding-top: 38px;
        width: 810px;
    }

    .footer ul li {
        float: left;
        width: 112px;
        margin: 0 10px;
        text-align: center;
        font-size: 14px;
    }

    .footer ul::after {
        content: "";
        display: block;
        clear: both;
    }

    .footer p {
        text-align: center;
        font-size: 12px;
    }
</style>

3 轮播图

3.1 轮播图接口

# 1  urls.py 
from rest_framework.routers import  SimpleRouter
router=SimpleRouter()
router.register('banner',views.BannerView,'banner')
urlpatterns = [
    # path('banner/', views.BannerView.as_view()),
    path('', include(router.urls)),
]




# 2 views.py
from django.conf import settings
class BannerView(GenericViewSet,ListModelMixin):
    # 无论有多少条待展示的数据,最多就展示3条
    queryset = models.Banner.objects.filter(is_delete=False,is_show=True).order_by('display_order')[:settings.BANNER_COUNTER]
    serializer_class = serializer.BannerModelSerilaizer
    
    
    
    
# 3 serializer.py
from rest_framework import serializers
from . import models
class BannerModelSerilaizer(serializers.ModelSerializer):
    class Meta:
        model=models.Banner
        fields=['name','link','img']
        
        
        
# 4 models.py
from utils.models import BaseModel

class Banner(BaseModel):
    name=models.CharField(max_length=32,verbose_name='图片名字')
    img=models.ImageField(upload_to='banner',verbose_name='轮播图',help_text='图片尺寸必须是:3840*800',null=True)
    link=models.CharField(max_length=32,verbose_name='跳转连接')
    info=models.TextField(verbose_name='图片简介')
    # type=models.IntegerField(choices=)

    def __str__(self):
        return self.name
    
    
# 5 utils/models.py
class BaseModel(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    update_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
    is_delete = models.BooleanField(default=False, verbose_name='是否删除')
    is_show = models.BooleanField(default=True, verbose_name='是否展示')
    display_order = models.IntegerField()
    class Meta:
        abstract=True  # 一定不要忘了
   


# 6 settings/const.py
# 首页轮播图个数
BANNER_COUNTER=3



# 7 settings/dev.py
from .const import *

3.2 轮播图组件

Banner.vue

<template>

    <div id="banner">

        <el-carousel height="400px">
            <el-carousel-item v-for="item in banner_list">
                <!--<img src="../assets/img/banner1.png" alt="">-->
                <router-link :to="item.link">
                    <img :src="item.img" :alt="item.name">
                </router-link>

            </el-carousel-item>
        </el-carousel>
    </div>

</template>

<script>
    export default {
        name: "Banner",
        // data:function(){},
        data() {
            return {
                banner_list: []
            }
        },
        created() {
            //当banner组件一创建,就向后台发请求,拿回轮播图数据
            this.$axios.get(this.$settings.base_url + '/home/banner/').then(response => {
                console.log(response.data)
                this.banner_list=response.data
            }).catch(error => {
            })
        },

    }
</script>

<style scoped>
    .el-carousel__item {
        height: 400px;
        min-width: 1200px;
    }

    .el-carousel__item img {
        height: 400px;
        /*margin-left: 20px;*/
        /*margin-left: calc(50% - 1920px / 2);*/
    }
</style>

前端路由配置(index.js)

const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/free-course',
        name: 'FreeCourse',
        component: FreeCourse
    }, {
        path: '/light-course',
        name: 'LightCourse',
        component: LightCourse
    }, {
        path: '/actual-course',
        name: 'ActualCourse',
        component: ActualCourse
    },

]

3.3 轮播图缓存

使用django-redis模块

pip install django-redis
在django项目的setting中配置

CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
# "PASSWORD": "123",
			}
		}
	}
# 首页轮播图数据缓存到redis中
# 重写轮播图接口的list方法

def list(self, request, *args, **kwargs):
    # response=super().list(request, *args, **kwargs)
    # 把data的数据加缓存,先去缓存拿数据
    banner_list=cache.get('banner_list')
    if not banner_list:
        print('走数据库了')
        # 缓存中没有,去数据库拿
        response = super().list(request, *args, **kwargs)
        # 加到缓存
        cache.set('banner_list',response.data,60*60*24)
        return response

    return Response(data=banner_list)

3.4 轮播图异步更新

pip install celery
# celery.py
from celery import Celery

# 加载django环境
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev")
django.setup()

broker='redis://127.0.0.1:6379/1'  #broker任务队列
backend='redis://127.0.0.1:6379/2'   # 结构存储,执行完的结果存在这

app=Celery(__name__,broker=broker,backend=backend,include=['celery_task.home_task',])


# 执行定时任务
# 时区
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    'add-task': {
        'task': 'celery_task.home_task.banner_update',
        'schedule': timedelta(seconds=30),
    }
}


# home_task.py
from .celery import app
@app.task
def banner_update():
    from home import serializer
    from home import models
    from django.conf import settings
    from django.core.cache import cache
    queryset_banner = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('display_order')[
               :settings.BANNER_COUNTER]
    serializer_banner=serializer.BannerModelSerilaizer(instance=queryset_banner,many=True)
    # print(serializer_banner.data)
    for banner in serializer_banner.data:
        banner['img']='http://127.0.0.1:8000'+banner['img']
    cache.set('banner_list',serializer_banner.data)
    # import time
    # time.sleep(1)
    # banner_list=cache.get('banner_list')
    # print(banner_list)
    return True

4 登录

4.1 vue登录组件雏形

# Login.vue
<template>
    <div class="login">
    <span @click="close_login">X</span>
    </div>
</template>

<script>
    export default {
        name: "Login",
        methods:{
            close_login(){
                //子传父组件 this.$emit,给父组件传递一个事件
                this.$emit("close")
            },

        },

    }
</script>

<style scoped>
.login{
    width: 100vw;
    height: 100vw;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 666;
    background-color: rgba(0,0,0,0.3);
}
    span{
        font-size: 30px;
        cursor: pointer;
    }
</style>

# Head.vue
#template中
<div class="right-part">
<div>
<span @click="pull_login">登录</span>
<span class="line">|</span>
<span>注册</span>
</div>
</div>
</div>

<Login v-if="is_login" @close="close"/>
<!--is_login是True就显示,false就不显示-->
</div>
# js中
pull_login(){
    this.is_login=true
},
close(){
    this.is_login=false
},

4.2 后端登录接口雏形

from rest_framework.viewsets import GenericViewSet
from luffyapi.utils.response import APIResponse
from rest_framework.decorators import action


class LoginView(GenericViewSet):
    queryset = models.User.objects.all()
    serializer_class = ser.LoginSerializer
    authentication_classes = []
    permission_classes = []

    @action(methods=['post'], detail=False)
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return APIResponse(code=0, msg=serializer.errors)
        token = serializer.context.get('token')
        username = serializer.context.get('username')
        return APIResponse(token=token, username=username, code=1, msg='登陆成功', status=200)

序列化器(多方式登录)

import re
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler, jwt_decode_handler, jwt_payload_handler
from user import models


class LoginSerializer(serializers.ModelSerializer):
    username = serializers.CharField()

    class Meta:
        model = models.User
        fields = ['username', 'password']

    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        if re.match('^1[3-9][0-9]{9}$', username):
            user = models.User.objects.filter(telephone=username)
        elif re.match('^.+@.+$', username):
            user = models.User.objects.filter(email=username)
        else:
            user = models.User.objects.filter(username=username)
        if user:
            user = user.first()
            if user.check_password(password):
                payload = jwt_payload_handler(user)
                token = jwt_encode_handler(payload)
                self.context['token'] = token
                self.context['username'] = user.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户名不存在')

4.3 vue登录组件第二版

Login.vue

<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>
                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary" @click="login">登录</el-button>
                </el-form>
                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary">登录</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_register">立即注册</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
            return {
                username: '',
                password: '',
                mobile: '',
                sms: '',
                login_method: 'is_pwd',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_login() {
                this.$emit('close')
            },
            go_register() {
                this.$emit('go')
            },
            change_login_method(method) {
                this.login_method = method;
            },
            check_mobile() {
                if (!this.mobile) return;
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.is_send = true;
            },
            send_sms() {
                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },
            login(){
                if(this.username&&this.password){
                    this.$axios.post(this.$settings.base_url+'/user/login/',{username:this.username,password:this.password})
                        .then(response=>{
                            if(response.data.code===0){this.$message.error(response.data.msg.non_field_errors[0])}
                            else if(response.data.code===1){
                                this.$cookies.set('token',response.data.token,'7d')
                                this.$cookies.set('username',response.data.username,'7d')
                                this.username=''
                                this.password=''
                                this.$emit('update_head')
                                this.close_login()
                            }
                        }
                    ).catch(errors=>{})
                }else {
                    this.$message.error('用户名和密码不能为空');
                }
            }
        }
    }
</script>

<style scoped>
    .login {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 420px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 210px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin: 0 20px 0 35px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>

Head.vue

<template>
    <div class="header">
        <div class="slogan">
            <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
        </div>
        <div class="nav">
            <ul class="left-part">
                <li class="logo">
                    <router-link to="/">
                        <img src="../assets/img/head-logo.svg" alt="">
                    </router-link>
                </li>
                <li class="ele">
                    <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
                </li>
            </ul>

            <div class="right-part">
                <div v-show="!username">
                    <span @click="login_show=!login_show">登录</span>
                    <span class="line">|</span>
                    <span @click="reg_show=!reg_show">注册</span>
                </div>
                <div v-show="username">
                    <span>{{username}}</span>
                    <span class="line">|</span>
                    <span @click="logout">注销</span>
                </div>
    		</div>
            <Login @close="login_show=!login_show" @go="goReg" @update_head='update_head' v-show="login_show"></Login>
            <Reg @close="reg_show=!reg_show" @go="goLogin" v-show="reg_show"></Reg>
        </div>
    </div>

</template>

<script>
    import Login from "./Login";
    import Reg from "./Reg";
    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
                login_show:false,
                reg_show:false,
                token:'',
                username:''
            }
        },
        methods: {
            goPage(url_path) {
                // 已经是当前路由就没有必要重新跳转
                if (this.url_path !== url_path) {
                    this.$router.push(url_path);
                }
                sessionStorage.url_path = url_path;
            },
            goReg(){
                this.login_show=false
                this.reg_show=true
            },
            goLogin(){
                this.reg_show=false
                this.login_show=true
            },
            logout(){
                this.$cookies.remove('username')
                this.$cookies.remove('token')
                this.username=''
                this.token=''
            },
            update_head(){
                this.token = this.$cookies.get('token');
                this.username = this.$cookies.get('username');
            }
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;
            this.token = this.$cookies.get('token');
            this.username = this.$cookies.get('username');
        },
        components:{
            Login,
            Reg
        }
    }
</script>

<style scoped>
    .header {
        background-color: white;
        box-shadow: 0 0 5px 0 #aaa;
    }

    .header:after {
        content: "";
        display: block;
        clear: both;
    }

    .slogan {
        background-color: #eee;
        height: 40px;
    }

    .slogan p {
        width: 1200px;
        margin: 0 auto;
        color: #aaa;
        font-size: 13px;
        line-height: 40px;
    }

    .nav {
        background-color: white;
        user-select: none;
        width: 1200px;
        margin: 0 auto;

    }

    .nav ul {
        padding: 15px 0;
        float: left;
    }

    .nav ul:after {
        clear: both;
        content: '';
        display: block;
    }

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

    .ele span {
        display: block;
        font: 15px/36px '微软雅黑';
        border-bottom: 2px solid transparent;
        cursor: pointer;
    }

    .ele span:hover {
        border-bottom-color: orange;
    }

    .ele span.active {
        color: orange;
        border-bottom-color: orange;
    }

    .right-part {
        float: right;
    }

    .right-part .line {
        margin: 0 10px;
    }

    .right-part span {
        line-height: 68px;
        cursor: pointer;
    }
</style>

4.4 后端登录接口第二版

from rest_framework.viewsets import GenericViewSet
from django.core.cache import cache
from user import models
from user import ser
import re
from luffyapi.utils.response import APIResponse
from rest_framework.decorators import action
from luffyapi.lib.tx_sms import send_message, get_code, expiry, PHONE_CACHE_KEY


# Create your views here.


class LoginView(GenericViewSet):
    queryset = models.User.objects.all()
    serializer_class = ser.LoginSerializer
    authentication_classes = []
    permission_classes = []

    @action(methods=['post'], detail=False)
    def login(self, request):
        serializer = self.get_serializer(data=request.data)
        if not serializer.is_valid():
            return APIResponse(code=0, msg=serializer.errors)
        token = serializer.context.get('token')
        username = serializer.context.get('username')
        return APIResponse(token=token, username=username, code=1, msg='登陆成功', status=200)

    @action(methods=['post'], detail=False)
    def check_phone(self, request):
        telephone = request.data.get('telephone')
        if not re.match('^1[3-9][0-9]{9}$', telephone):
            return APIResponse(code=999, msg='手机号不合法')
        user = models.User.objects.filter(telephone=telephone)
        if user:
            return APIResponse(code=1, msg='手机号存在')
        else:
            return APIResponse(code=0, msg='手机号未注册')

    @action(methods=['post'], detail=False)
    def send_sms(self, request):
        telephone = request.data.get('telephone')
        if not re.match('^1[3-9][0-9]{9}$', telephone):
            return APIResponse(code=999, msg='手机号不合法')
        user = models.User.objects.filter(telephone=telephone)
        if user:
            code = get_code()
            cache.set(PHONE_CACHE_KEY % telephone, code, expiry)
            print(code)
            result = send_message(telephone, code)
            if result:
                return APIResponse(code=1, msg='发送成功')
            else:
                return APIResponse(code=999, msg='未知错误')
        else:
            return APIResponse(code=0, msg='手机号未注册')

4.5 发送短信功能

pip install qcloudsms_py

send.py

from luffyapi.utils.logger import log
from .settings import *
import random
from qcloudsms_py import SmsSingleSender


# 生成一个四位随机验证码
def get_code():
    s_code = ''
    for i in range(4):
        s_code += str(random.randint(0, 9))
    return s_code


def send_message(phone, code):
    ssender = SmsSingleSender(appid, appkey)
    params = [code, expiry // 60]
    try:
        result = ssender.send_with_param(86, phone, template_id, params, sign=sms_sign, extend="", ext="")
        if result.get('result') == 0:
            return True
        else:
            return False
    except Exception as e:
        log.error(f'发送短信给{phone}失败,错误为{e}')
        return False

settings.py

# 短信应用 SDK AppID
appid = 1400403285  # SDK AppID 以1400开头
# 短信应用 SDK AppKey
appkey = "139d9d5db909d2f969fd3c533976482c"
# 短信过期时间(秒)
expiry = 300
# 短信模板ID,需要在短信控制台中申请
template_id = 669369  # NOTE: 这里的模板 ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
# 签名
sms_sign = "从零开始的py程序猿生活"  # NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云
#
PHONE_CACHE_KEY = 'sms_cache_%s'

4.6 短信接口频率限制

# throttlings.py
from rest_framework.throttling import SimpleRateThrottle

class SMSThrottle(SimpleRateThrottle):
    scope = 'sms'
    def get_cache_key(self, request, view):
        return self.cache_format % {'scope': self.scope, 'ident': request.data.get('telephone')}

    
# 将频率限制配置在视图类上

from .throttlings import SMSThrotting
class SendSmSView(ViewSet):
    throttle_classes = [SMSThrotting,]
# 可以用局部的局部:装饰器
	@action(methods=['post'], detail=False, throttle_classes=[SMSThrottle])
	def login_send_sms(self, request):
        
        
# 在setting中配置频率
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_RATES':{
        'sms':'1/m'  # key要跟类中的scop对应
    }
}

4.7 验证码登陆接口

# views.py
@action(methods=['POST'],detail=False)
def code_login(self,request,*args,**kwargs):
    ser = serializer.CodeUserSerilaizer(data=request.data)
    if ser.is_valid():
        token = ser.context['token']
        username = ser.context['user'].username
        return APIResponse(token=token, username=username)
    else:
        return APIResponse(code=0,msg=ser.errors)
    
# serializer
class CodeUserSerilaizer(serializers.ModelSerializer):
    code=serializers.CharField()
    class Meta:
        model = models.User
        fields = ['telephone', 'code']

    def validate(self, attrs):
        user=self._get_user(attrs)
        # 用户存在,签发token
        token = self._get_token(user)
        self.context['token'] = token
        self.context['user'] = user
        return attrs

    def _get_user(self, attrs):
        from django.core.cache import cache
        from django.conf import settings
        import re
        telephone = attrs.get('telephone')
        code = attrs.get('code')

        # 取出原来的code
        cache_code=cache.get(settings.PHONE_CACHE_KEY%telephone)
        if code ==cache_code:
            # 验证码通过
            if re.match('^1[3-9][0-9]{9}$', telephone):
                user = models.User.objects.filter(telephone=telephone).first()
                if user:
                    # 把使用过的验证码删除
                    cache.set(settings.PHONE_CACHE_KEY % telephone,'')
                    return user
                else:
                    raise ValidationError('用户不存在')
            else:
                raise ValidationError('手机号不合法')
        else:
            raise ValidationError('验证码错误')


    def _get_token(self, user):
        from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
        payload = jwt_payload_handler(user)  # 通过user对象获得payload
        token = jwt_encode_handler(payload)  # 通过payload获得token
        return token

4.8 前台发送验证码

<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>
                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary" @click="login_password">登录</el-button>
                </el-form>
                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary" @click="code_login">登录</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_register">立即注册</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Login",
        data() {
            return {
                username: '',
                password: '',
                mobile: '',
                sms: '',
                login_method: 'is_pwd',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_login() {
                this.$emit('close')
            },
            go_register() {
                this.$emit('go')
            },
            change_login_method(method) {
                this.login_method = method;
            },
            check_mobile() {
                if (!this.mobile) return;
                //字符串.match(/正则表达式/)
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                //发送axios请求
                // this.$axios.get(this.$settings.base_url+'/user/check_telephone/telephone='+this.mobile})
                this.$axios.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
                    if (response.data.code) {
                        //手机号存在,允许发送验证码
                        this.is_send = true;
                    } else {
                        this.$message({
                            message: '手机号不存在',
                            type: 'warning',
                            duration: 1000,
                            onClose: () => {
                                this.mobile = '';
                            }
                        });
                    }
                }).catch(error => {
                    console.log(error)
                })

            },
            send_sms() {

                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
                    .then(response => {
                        if (response.data.code) {
                            this.$message({
                                message: '发送验证码成功',
                                type: 'success',
                                duration: 1000,

                            });
                        }
                    })


                // setInterval(()=>{},100)
                //定时器:每隔一秒种,把数字减一,当减到小于1,按钮又能点了,显示获取验证码
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);  //如果小于等于1,把定时器清除
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },

            login_password() {
                if (this.username && this.password) {
                    //发送请求
                    this.$axios.post(this.$settings.base_url + '/user/login/', {
                        username: this.username,
                        password: this.password


                    }).then(response => {
                        console.log(response.data)
                        //把用户信息保存到cookie中
                        // this.$cookies.set('key','value','过期时间,按s计')
                        this.$cookies.set('token', response.data.token, '7d')
                        this.$cookies.set('username', response.data.username, '7d')
                        //关闭登录窗口(子传父)
                        this.$emit('close')
                        //给父组件,Head传递一个事件,让它从cookie中取出token和username
                        this.$emit('loginsuccess')
                    }).catch(errors => {
                    })
                } else {
                    this.$message({
                        message: '用户名或密码必须填哦',
                        type: 'warning',

                    });
                }
            },
            code_login() {

                if (this.mobile && this.sms) {
                    //发送请求
                    this.$axios.post(this.$settings.base_url + '/user/code_login/', {
                        telephone: this.mobile,
                        code: this.sms

                    }).then(response => {
                        console.log(response.data)
                        //把用户信息保存到cookie中
                        // this.$cookies.set('key','value','过期时间,按s计')
                        this.$cookies.set('token', response.data.token, '7d')
                        this.$cookies.set('username', response.data.username, '7d')
                        //关闭登录窗口(子传父)
                        this.$emit('close')
                        //给父组件,Head传递一个事件,让它从cookie中取出token和username
                        this.$emit('loginsuccess')
                    }).catch(errors => {
                    })
                } else {
                    this.$message({
                        message: '手机号或验证码必填',
                        type: 'warning',

                    });
                }

            },
        }
    }
</script>

<style scoped>
    .login {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 420px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 210px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin: 0 20px 0 35px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>

4.9 前台验证码登陆

#详细代码看第三条,前台发送验证码

5 注册

5.1 后台注册接口

# urls.py
router.register('register', views.RegisterView, 'register')  # /user/register   post请求就是新增

# views.py
class RegisterView(GenericViewSet,CreateModelMixin):
    queryset = models.User.objects.all()
    serializer_class = serializer.UserRegisterSerilaizer
    def create(self, request, *args, **kwargs):
        response=super().create(request, *args, **kwargs)
        username=response.data.get('username')
        return APIResponse(code=1,msg='注册成功',username=username)
    
# serializer.py
class UserRegisterSerilaizer(serializers.ModelSerializer):
    code=serializers.CharField(max_length=4,min_length=4,write_only=True)
    class Meta:
        model = models.User
        fields = ['telephone', 'code','password','username']
        extra_kwargs = {
            'password': {'max_length': 18,'min_length':8},
            'username': {'read_only':True}
        }



    def validate(self, attrs):
        telephone = attrs.get('telephone')
        code = attrs.get('code')
        # 取出原来的code
        cache_code = cache.get(settings.PHONE_CACHE_KEY % telephone)
        if code == cache_code:
            # 验证码通过
            if re.match('^1[3-9][0-9]{9}$', telephone):
                attrs['username']=telephone  # 把用户的名字设成手机号
                attrs.pop('code')
                return attrs
            else:
                raise ValidationError('手机号不合法')
        else:
            raise ValidationError('验证码错误')


    # 重写create方法
    def create(self, validated_data):
        user=models.User.objects.create_user(**validated_data)
        return user

5.2 vue注册组件

# Register.vue
<template>
    <div class="register">
        <div class="box">
            <i class="el-icon-close" @click="close_register"></i>
            <div class="content">
                <div class="nav">
                    <span class="active">新用户注册</span>
                </div>
                <el-form>
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button type="primary" @click="register">注册</el-button>
                </el-form>
                <div class="foot">
                    <span @click="go_login">立即登录</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        name: "Register",
        data() {
            return {
                mobile: '',
                password: '',
                sms: '',
                sms_interval: '获取验证码',
                is_send: false,
            }
        },
        methods: {
            close_register() {
                this.$emit('close', false)
            },
            go_login() {
                this.$emit('go')
            },
            check_mobile() {
                if (!this.mobile) return;
                //字符串.match(/正则表达式/)
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                this.$axios.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
                    if (response.data.code) {
                        this.$message({
                            message: '您已经注册过了,快去登录把',
                            type: 'warning',
                            duration: 1000,
                            onClose: () => {
                                this.go_login()
                            }
                        });

                    } else {
                        this.is_send = true;
                        this.$message({
                            message: '该用户没有注册过,欢迎注册我们的平台',
                            type: 'success',
                            duration: 1000,
                        });
                    }
                }).catch(error => {
                    console.log(error)
                })

            },
            send_sms() {

                if (!this.is_send) return;
                this.is_send = false;
                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
                    .then(response => {
                        if (response.data.code) {
                            this.$message({
                                message: '发送验证码成功',
                                type: 'success',
                                duration: 1000,

                            });
                        }
                    })


                // setInterval(()=>{},100)
                //定时器:每隔一秒种,把数字减一,当减到小于1,按钮又能点了,显示获取验证码
                let timer = setInterval(() => {
                    if (sms_interval_time <= 1) {
                        clearInterval(timer);  //如果小于等于1,把定时器清除
                        this.sms_interval = "获取验证码";
                        this.is_send = true; // 重新回复点击发送功能的条件
                    } else {
                        sms_interval_time -= 1;
                        this.sms_interval = `${sms_interval_time}秒后再发`;
                    }
                }, 1000);
            },
            register() {
                if (this.mobile && this.sms && this.password) {
                    this.$axios.post(this.$settings.base_url + '/user/register/', {
                        telephone: this.mobile,
                        code: this.sms,
                        password: this.password
                    }).then(response => {
                        if (response.data.code) {
                            //注册成功,来个提示,跳转到登录
                            this.$message({
                                message: '注册成功',
                                type: 'success',
                                duration: 1000,
                                onClose: () => {
                                    this.go_login()
                                }
                            });
                        } else {
                            this.$message({
                                message: '未知错误',
                                type: 'error',
                                duration: 1000,
                                onClose: () => {
                                    this.mobile = ''
                                    this.sms = ''
                                    this.password = ''
                                }
                            });
                        }
                    })
                } else {
                    this.$message({
                        message: '你有没填的信息',
                        type: 'error',
                        duration: 1000,
                    });
                }
            },
        }
    }
</script>

<style scoped>
    .register {
        width: 100vw;
        height: 100vh;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 10;
        background-color: rgba(0, 0, 0, 0.3);
    }

    .box {
        width: 400px;
        height: 480px;
        background-color: white;
        border-radius: 10px;
        position: relative;
        top: calc(50vh - 240px);
        left: calc(50vw - 200px);
    }

    .el-icon-close {
        position: absolute;
        font-weight: bold;
        font-size: 20px;
        top: 10px;
        right: 10px;
        cursor: pointer;
    }

    .el-icon-close:hover {
        color: darkred;
    }

    .content {
        position: absolute;
        top: 40px;
        width: 280px;
        left: 60px;
    }

    .nav {
        font-size: 20px;
        height: 38px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span {
        margin-left: 90px;
        color: darkgrey;
        user-select: none;
        cursor: pointer;
        padding-bottom: 10px;
        border-bottom: 2px solid darkgrey;
    }

    .nav > span.active {
        color: black;
        border-bottom: 3px solid black;
        padding-bottom: 9px;
    }

    .el-input, .el-button {
        margin-top: 40px;
    }

    .el-button {
        width: 100%;
        font-size: 18px;
    }

    .foot > span {
        float: right;
        margin-top: 20px;
        color: orange;
        cursor: pointer;
    }

    .sms {
        color: orange;
        cursor: pointer;
        display: inline-block;
        width: 70px;
        text-align: center;
        user-select: none;
    }
</style>

6 课程

6.1 课程表结构分析和优化

ForeignKey的字段:
	-related_name 反向操作时,使用的字段名,用于代替原反向查询时的'表名_set'。
    -related_query_name :反向查询操作时,使用的连接前缀,用于替换表名。
# luffyapi/utils/models.py
class BaseModel(models.Model):
    is_delete = models.BooleanField(verbose_name='是否删除',default=False)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    updated_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    orders = models.IntegerField(verbose_name='播放顺序')
    is_show = models.BooleanField(verbose_name='是否展示')

    class Meta:
        abstract = True
# 1 course/models.py

from luffyapi.utils.models import BaseModel


# 路飞课程相关表,以免费课为例
class CourseCategory(BaseModel):
    """分类
    python,linux,go, 网络安全
    跟课程是一对多的关系

    """
    name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
    class Meta:
        db_table = "luffy_course_category"
        verbose_name = "分类"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s" % self.name


class Course(BaseModel):
    """课程"""
    course_type = (
        (0, '付费'),
        (1, 'VIP专享'),
        (2, '学位课程')
    )
    level_choices = (
        (0, '初级'),
        (1, '中级'),
        (2, '高级'),
    )
    status_choices = (
        (0, '上线'),
        (1, '下线'),
        (2, '预上线'),
    )
    # 原始字段
    name = models.CharField(max_length=128, verbose_name="课程名称")
    course_img = models.ImageField(upload_to="courses", max_length=255, verbose_name="封面图片", blank=True, null=True)
    course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付费类型")
    # 使用这个字段的原因
    brief = models.TextField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
    level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="难度等级")
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
    period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
    attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="课件路径", blank=True, null=True)
    status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)

    # 优化字段
    students = models.IntegerField(verbose_name="学习人数", default=0)
    sections = models.IntegerField(verbose_name="总课时数量", default=0)
    pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)

    # 关联字段
    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师",db_constraint=False)
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True, blank=True,verbose_name="课程分类")
    class Meta:
        db_table = "luffy_course"
        verbose_name = "课程"
        verbose_name_plural = "课程"

    def __str__(self):
        return "%s" % self.name



class Teacher(BaseModel):
    """导师
    跟课程一对多,关联字段写在课程表中
    """
    role_choices = (
        (0, '讲师'),
        (1, '导师'),
        (2, '班主任'),
    )
    name = models.CharField(max_length=32, verbose_name="导师名")
    role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="导师身份")
    title = models.CharField(max_length=64, verbose_name="职位、职称")
    signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True)
    image = models.ImageField(upload_to="teacher", null=True, verbose_name="导师封面")
    brief = models.TextField(max_length=1024, verbose_name="导师描述")

    class Meta:
        db_table = "luffy_teacher"
        verbose_name = "导师"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s" % self.name



class CourseChapter(BaseModel):
    """章节
    章节跟课程是一(课程)对多(章节多)
    """
    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE, verbose_name="课程名称",db_constraint=False)
    chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
    name = models.CharField(max_length=128, verbose_name="章节标题")
    summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
    pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)

    class Meta:
        db_table = "luffy_course_chapter"
        verbose_name = "章节"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)



class CourseSection(BaseModel):
    """课时
        章节和课时是一对多的关系,关联字段写在多的一方,课时
    """
    section_type_choices = (
        (0, '文档'),
        (1, '练习'),
        (2, '视频')
    )
    chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
                                verbose_name="课程章节",db_constraint=False)
    name = models.CharField(max_length=128, verbose_name="课时标题")
    orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
    section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
    section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接",
                                    help_text="若是video,填vid,若是文档,填link")
    duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
    pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
    free_trail = models.BooleanField(verbose_name="是否可试看", default=False)

    class Meta:
        db_table = "luffy_course_Section"
        verbose_name = "课时"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s-%s" % (self.chapter, self.name)
    
    
# 2 utils/models/BaseModel
from django.db import models
# 后期课程表,轮播图表,都会用到这些字段

class BaseModel(models.Model):
    is_delete = models.BooleanField(default=False, verbose_name='是否删除')
    is_show = models.BooleanField(default=True, verbose_name='是否展示')
    # 修改成这样
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    updated_time  = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
    orders = models.IntegerField()
    class Meta:
        abstract=True  # 一定不要忘了






# 3 数据迁移
python manage.py makemigrations
python manage.py migrate

6.2 课程主页vue页面雏形

# 不要忘了注册路由

# FreeCourse.vue
<template>
    <div class="course">
        <Header></Header>
        <div class="main">
            <!-- 筛选条件 -->
            <div class="condition">
                <ul class="cate-list">
                    <li class="title">课程分类:</li>
                    <li class="this">全部</li>
                    <li>Python</li>
                    <li>Linux运维</li>
                    <li>Python进阶</li>
                    <li>开发工具</li>
                    <li>Go语言</li>
                    <li>机器学习</li>
                    <li>技术生涯</li>
                </ul>

                <div class="ordering">
                    <ul>
                        <li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
                        <li class="default this">默认</li>
                        <li class="hot">人气</li>
                        <li class="price">价格</li>
                    </ul>
                    <p class="condition-result">共21个课程</p>
                </div>

            </div>
            <!-- 课程列表 -->
            <div class="course-list">
                <div class="course-item">
                    <div class="course-image">
                        <img src="@/assets/img/course-cover.jpeg" alt="">
                    </div>
                    <div class="course-info">
                        <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
                        <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
                        <ul class="lesson-list">
                            <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
                        </ul>
                        <div class="pay-box">
                            <span class="discount-type">限时免费</span>
                            <span class="discount-price">¥0.00元</span>
                            <span class="original-price">原价:9.00元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
                <div class="course-item">
                    <div class="course-image">
                        <img src="@/assets/img/course-cover.jpeg" alt="">
                    </div>
                    <div class="course-info">
                        <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
                        <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
                        <ul class="lesson-list">
                            <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
                        </ul>
                        <div class="pay-box">
                            <span class="discount-type">限时免费</span>
                            <span class="discount-price">¥0.00元</span>
                            <span class="original-price">原价:9.00元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
                <div class="course-item">
                    <div class="course-image">
                        <img src="@/assets/img/course-cover.jpeg" alt="">
                    </div>
                    <div class="course-info">
                        <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
                        <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
                        <ul class="lesson-list">
                            <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
                        </ul>
                        <div class="pay-box">
                            <span class="discount-type">限时免费</span>
                            <span class="discount-price">¥0.00元</span>
                            <span class="original-price">原价:9.00元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
                <div class="course-item">
                    <div class="course-image">
                        <img src="@/assets/img/course-cover.jpeg" alt="">
                    </div>
                    <div class="course-info">
                        <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
                        <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
                        <ul class="lesson-list">
                            <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
                            <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
                        </ul>
                        <div class="pay-box">
                            <span class="discount-type">限时免费</span>
                            <span class="discount-price">¥0.00元</span>
                            <span class="original-price">原价:9.00元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <Footer></Footer>
    </div>
</template>

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

    export default {
        name: "Course",
        data() {
            return {
                category: 0,
            }
        },
        components: {
            Header,
            Footer,
        }
    }
</script>

<style scoped>
    .course {
        background: #f6f6f6;
    }

    .course .main {
        width: 1100px;
        margin: 35px auto 0;
    }

    .course .condition {
        margin-bottom: 35px;
        padding: 25px 30px 25px 20px;
        background: #fff;
        border-radius: 4px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .course .cate-list {
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
        padding-bottom: 18px;
        margin-bottom: 17px;
    }

    .course .cate-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .cate-list li {
        float: left;
        font-size: 16px;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
        border: 1px solid transparent; /* transparent 透明 */
    }

    .course .cate-list .title {
        color: #888;
        margin-left: 0;
        letter-spacing: .36px;
        padding: 0;
        line-height: 28px;
    }

    .course .cate-list .this {
        color: #ffc210;
        border: 1px solid #ffc210 !important;
        border-radius: 30px;
    }

    .course .ordering::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering ul {
        float: left;
    }

    .course .ordering ul::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering .condition-result {
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        line-height: 28px;
    }

    .course .ordering ul li {
        float: left;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
    }

    .course .ordering .title {
        font-size: 16px;
        color: #888;
        letter-spacing: .36px;
        margin-left: 0;
        padding: 0;
        line-height: 28px;
    }

    .course .ordering .this {
        color: #ffc210;
    }

    .course .ordering .price {
        position: relative;
    }

    .course .ordering .price::before,
    .course .ordering .price::after {
        cursor: pointer;
        content: "";
        display: block;
        width: 0px;
        height: 0px;
        border: 5px solid transparent;
        position: absolute;
        right: 0;
    }

    .course .ordering .price::before {
        border-bottom: 5px solid #aaa;
        margin-bottom: 2px;
        top: 2px;
    }

    .course .ordering .price::after {
        border-top: 5px solid #aaa;
        bottom: 2px;
    }

    .course .course-item:hover {
        box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
    }

    .course .course-item {
        width: 1100px;
        background: #fff;
        padding: 20px 30px 20px 20px;
        margin-bottom: 35px;
        border-radius: 2px;
        cursor: pointer;
        box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
        /* css3.0 过渡动画 hover 事件操作 */
        transition: all .2s ease;
    }

    .course .course-item::after {
        content: "";
        display: block;
        clear: both;
    }

    /* 顶级元素 父级元素  当前元素{} */
    .course .course-item .course-image {
        float: left;
        width: 423px;
        height: 210px;
        margin-right: 30px;
    }

    .course .course-item .course-image img {
        width: 100%;
    }

    .course .course-item .course-info {
        float: left;
        width: 596px;
    }

    .course-item .course-info h3 {
        font-size: 26px;
        color: #333;
        font-weight: normal;
        margin-bottom: 8px;
    }

    .course-item .course-info h3 span {
        font-size: 14px;
        color: #9b9b9b;
        float: right;
        margin-top: 14px;
    }

    .course-item .course-info h3 span img {
        width: 11px;
        height: auto;
        margin-right: 7px;
    }

    .course-item .course-info .teather-info {
        font-size: 14px;
        color: #9b9b9b;
        margin-bottom: 14px;
        padding-bottom: 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
    }

    .course-item .course-info .teather-info span {
        float: right;
    }

    .course-item .lesson-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .lesson-list li {
        float: left;
        width: 44%;
        font-size: 14px;
        color: #666;
        padding-left: 22px;
        /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
        background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
        margin-bottom: 15px;
    }

    .course-item .lesson-list li .lesson-title {
        /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        display: inline-block;
        max-width: 200px;
    }

    .course-item .lesson-list li:hover {
        background-image: url("/src/assets/img/play-icon-yellow.svg");
        color: #ffc210;
    }

    .course-item .lesson-list li .free {
        width: 34px;
        height: 20px;
        color: #fd7b4d;
        vertical-align: super;
        margin-left: 10px;
        border: 1px solid #fd7b4d;
        border-radius: 2px;
        text-align: center;
        font-size: 13px;
        white-space: nowrap;
    }

    .course-item .lesson-list li:hover .free {
        color: #ffc210;
        border-color: #ffc210;
    }

    .course-item .pay-box::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .pay-box .discount-type {
        padding: 6px 10px;
        font-size: 16px;
        color: #fff;
        text-align: center;
        margin-right: 8px;
        background: #fa6240;
        border: 1px solid #fa6240;
        border-radius: 10px 0 10px 0;
        float: left;
    }

    .course-item .pay-box .discount-price {
        font-size: 24px;
        color: #fa6240;
        float: left;
    }

    .course-item .pay-box .original-price {
        text-decoration: line-through;
        font-size: 14px;
        color: #9b9b9b;
        margin-left: 10px;
        float: left;
        margin-top: 10px;
    }

    .course-item .pay-box .buy-now {
        width: 120px;
        height: 38px;
        background: transparent;
        color: #fa6240;
        font-size: 16px;
        border: 1px solid #fd7b4d;
        border-radius: 3px;
        transition: all .2s ease-in-out;
        float: right;
        text-align: center;
        line-height: 38px;
    }

    .course-item .pay-box .buy-now:hover {
        color: #fff;
        background: #ffc210;
        border: 1px solid #ffc210;
    }
</style>

6.3 课程数据录入

-- 老师表
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2019-07-14 13:44:19.661327', '2019-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2019-07-14 13:45:25.092902', '2019-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2019-07-14 13:46:21.997846', '2019-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸');




-- 课程分类表
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2019-07-14 13:40:58.690413', '2019-07-14 13:40:58.690477', 'Python');

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2019-07-14 13:41:08.249735', '2019-07-14 13:41:08.249817', 'Linux');






-- 课程表数据
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2019-07-14 13:54:33.095201', '2019-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2019-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2019-07-14 13:56:05.051103', '2019-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2019-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2019-07-14 13:57:21.190053', '2019-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2019-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);





-- 章节表数据
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2019-07-14 13:58:34.867005', '2019-07-14 14:00:58.276541', 1, '计算机原理', '', '2019-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2019-07-14 13:58:48.051543', '2019-07-14 14:01:22.024206', 2, '环境搭建', '', '2019-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2019-07-14 13:59:09.878183', '2019-07-14 14:01:40.048608', 1, '项目创建', '', '2019-07-14', 2);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2019-07-14 13:59:37.448626', '2019-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2019-07-14', 3);






-- 课时表数据
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2019-07-14 14:02:33.779098', '2019-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2019-07-14 14:02:33.779193', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2019-07-14 14:02:56.657134', '2019-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2019-07-14 14:02:56.657227', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2019-07-14 14:03:20.493324', '2019-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2019-07-14 14:03:20.493420', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2019-07-14 14:03:36.472742', '2019-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2019-07-14 14:03:36.472831', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2019-07-14 14:04:19.338153', '2019-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2019-07-14 14:04:19.338252', 1, 3);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2019-07-14 14:04:52.895855', '2019-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2019-07-14 14:04:52.895942', 1, 4);


6.4 课程分类群查接口

# 路由urls.py
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
    path('', include(router.urls)),
]


# 视图views.py
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from . import models
from . import serializer
class CourseCategoryView(GenericViewSet,ListModelMixin):
    queryset = models.CourseCategory.objects.filter(is_delete=False,is_show=True).order_by('orders')
    serializer_class = serializer.CourseCategorySerializer

    
# 序列化类
from rest_framework import serializers
from . import models
class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model=models.CourseCategory
        fields=['id','name']

6.5 课程群查接口

# 路由
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
    path('', include(router.urls)),
]


# 视图 views.py
from .paginations import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter,SearchFilter
from .filters import MyFilter
class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
    queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
    serializer_class = serializer.CourseModelSerializer
    pagination_class = PageNumberPagination
    filter_backends=[DjangoFilterBackend,OrderingFilter]
    ordering_fields=['id', 'price', 'students']

    filter_fields=['course_category','students']
    
    
    
# serializer
from rest_framework import serializers
from . import models


class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.CourseCategory
        fields = ['id', 'name']


class TeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Teacher
        fields = ('name', 'role_name', 'title', 'signature', 'image', 'brief')


class CourseModelSerializer(serializers.ModelSerializer):
    # 子序列化的方式
    teacher = TeacherSerializer()

    class Meta:
        model = models.Course
        fields=[
            'id',
            'name',
            'course_img',
            'brief',
            'attachment_path',
            'pub_sections',
            'price',
            'students',
            'period',
            'sections',
            'course_type_name',
            'level_name',
            'status_name',
            'teacher',
            'section_list',
        ]
        
        
        
# models.py
class Course(BaseModel):
	...
    @property
    def course_type_name(self):
        return self.get_course_type_display()
    @property
    def level_name(self):
        return self.get_level_display()
    @property
    def status_name(self):
        return self.get_status_display()

    @property
    def section_list(self):
        ll=[]
        # 根据课程取出所有章节(正向查询,字段名.all())
        course_chapter_list=self.coursechapters.all()
        for course_chapter in course_chapter_list:
            # 通过章节对象,取到章节下所有的课时(反向查询)
            # course_chapter.表名小写_set.all() 现在变成了course_chapter.coursesections.all()
            course_sections_list=course_chapter.coursesections.all()
            for course_section in course_sections_list:
                ll.append({
                    'name': course_section.name,
                    'section_link': course_section.section_link,
                    'duration': course_section.duration,
                    'free_trail': course_section.free_trail,
                })
                if len(ll)>=4:
                    return ll

        return ll


    
class Teacher(BaseModel):
	...
    def role_name(self):
        # 返回角色的中文
        return self.get_role_display()

6.6 课程搜索和过滤

排序:
    按id正序倒叙排序,按price正序倒叙排列
    使用:
    	http://127.0.0.1:8000/course/free/?ordering=-id
    配置类:
        filter_backends=[OrderingFilter]
    配置字段:
        ordering_fields=['id','price']
    
    
内置过滤:
    使用:
    	http://127.0.0.1:8000/course/free/?search=39
    按照price过滤(表自有的字段直接过滤)
    配置类:
        filter_backends=[SearchFilter]
    配置字段:
        search_fields=['price']
    
    
扩展:django-filter
    支持自由字段的过滤还支持外键字段的过滤
    安装:
        pip3 install django-filter
    使用:
        注册,在项目settings.py的INSTALLED_APPS中注册'django_filters'
        http://127.0.0.1:8000/course/free/?course_category=1   
        # 过滤分类为1 (python的所有课程)
    配置类:
        filter_backends=[DjangoFilterBackend]
    配置字段:
        filter_fields=['course_category']

6.7 实现区间过滤

# 借助django-filter实现区间过滤

# 1 filters.py
class CourseFilterSet(FilterSet):
    # 课程的价格范围要大于min_price,小于max_price
    min_price = filters.NumberFilter(field_name='price', lookup_expr='gt')
    max_price = filters.NumberFilter(field_name='price', lookup_expr='lt')
    class Meta:
        model=models.Course
        fields=['course_category']
        
# 2 视图类中配置
     filter_backends=[DjangoFilterBackend]
	 # 配置类:(自己写的类)
     filter_class = CourseFilterSet

6.8 课程主页前端第二版

<template>
    <div class="course">
        <Header></Header>
        <div class="main">
            <!-- 筛选条件 -->
            <div class="condition">
                <ul class="cate-list">
                    <li class="title">课程分类:</li>
                    <li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
                    <li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
                        @click="filter.course_category=category.id" :key="category.name">{{category.name}}
                    </li>
                </ul>

                <div class="ordering">
                    <ul>
                        <li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
                        <li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"
                            @click="filter.ordering='-id'">默认
                        </li>
                        <li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"
                            @click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人气
                        </li>
                        <li class="price"
                            :class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"
                            @click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">价格
                        </li>
                    </ul>
                    <p class="condition-result">共{{course_total}}个课程</p>
                </div>

            </div>
            <!-- 课程列表 -->
            <div class="course-list">
                <div class="course-item" v-for="course in course_list" :key="course.name">
                    <div class="course-image">
                        <img :src="course.course_img" alt="">
                    </div>
                    <div class="course-info">
                        <h3>
                            <router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
                            <span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
                        <p class="teather-info">
                            {{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
                            <span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
                            <span v-else>共{{course.sections}}课时/更新完成</span>
                        </p>
                        <ul class="section-list">
                            <li v-for="(section, key) in course.section_list" :key="section.name"><span
                                    class="section-title">0{{key+1}}  |  {{section.name}}</span>
                                <span class="free" v-if="section.free_trail">免费</span></li>
                        </ul>
                        <div class="pay-box">
                            <div v-if="course.discount_type">
                                <span class="discount-type">{{course.discount_type}}</span>
                                <span class="discount-price">¥{{course.real_price}}元</span>
                                <span class="original-price">原价:{{course.price}}元</span>
                            </div>
                            <span v-else class="discount-price">¥{{course.price}}元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
            </div>
            <div class="course_pagination block">
                <el-pagination
                        @size-change="handleSizeChange"
                        @current-change="handleCurrentChange"
                        :current-page.sync="filter.page"
                        :page-sizes="[2, 3, 5, 10]"
                        :page-size="filter.page_size"
                        layout="sizes, prev, pager, next"
                        :total="course_total">
                </el-pagination>
            </div>
        </div>
        <Footer></Footer>
    </div>
</template>

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

    export default {
        name: "Course",
        data() {
            return {
                category_list: [], // 课程分类列表
                course_list: [],   // 课程列表
                course_total: 0,   // 当前课程的总数量
                filter: {
                    course_category: 0, // 当前用户选择的课程分类,刚进入页面默认为全部,值为0
                    ordering: "-id",    // 数据的排序方式,默认值是-id,表示对于id进行降序排列
                    page_size: 2,       // 单页数据量
                    page: 1,
                }
            }
        },
        created() {
            this.get_category();
            this.get_course();
        },
        components: {
            Header,
            Footer,
        },
        watch: {
            //当你监听的数据发生变化,就会执行函数
            "filter.course_category": function () {
                this.filter.page = 1;
                this.get_course();
            },
            "filter.ordering": function () {
                this.get_course();
            },
            "filter.page_size": function () {
                this.get_course();
            },
            "filter.page": function () {
                this.get_course();
            }
        },
        methods: {

            handleSizeChange(val) {
                // 每页数据量发生变化时执行的方法
                this.filter.page = 1;
                this.filter.page_size = val;
            },
            handleCurrentChange(val) {
                // 页码发生变化时执行的方法
                this.filter.page = val;
            },
            get_category() {
                // 获取课程分类信息
                this.$axios.get(`${this.$settings.base_url}/course/categories/`).then(response => {
                    this.category_list = response.data;
                }).catch(() => {
                    this.$message({
                        message: "获取课程分类信息有误,请联系客服工作人员",
                    })
                })
            },
            get_course() {
                // 排序
                let filters = {
                    ordering: this.filter.ordering, // 排序
                };
                // 判决是否进行分类课程的展示
                if (this.filter.course_category > 0) {
                    filters.course_category = this.filter.course_category;
                }

                // 设置单页数据量
                if (this.filter.page_size > 0) {
                    filters.page_size = this.filter.page_size;
                } else {
                    filters.page_size = 5;
                }

                // 设置当前页码
                if (this.filter.page > 1) {
                    filters.page = this.filter.page;
                } else {
                    filters.page = 1;
                }


                // 获取课程列表信息
                this.$axios.get(`${this.$settings.base_url}/course/free/`, {
                    params: filters
                }).then(response => {
                    // console.log(response.data);
                    this.course_list = response.data.results;
                    this.course_total = response.data.count;
                    // console.log(this.course_list);
                }).catch(() => {
                    this.$message({
                        message: "获取课程信息有误,请联系客服工作人员"
                    })
                })
            }
        }
    }
</script>

<style scoped>
    .course {
        background: #f6f6f6;
    }

    .course .main {
        width: 1100px;
        margin: 35px auto 0;
    }

    .course .condition {
        margin-bottom: 35px;
        padding: 25px 30px 25px 20px;
        background: #fff;
        border-radius: 4px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .course .cate-list {
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
        padding-bottom: 18px;
        margin-bottom: 17px;
    }

    .course .cate-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .cate-list li {
        float: left;
        font-size: 16px;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
        border: 1px solid transparent; /* transparent 透明 */
    }

    .course .cate-list .title {
        color: #888;
        margin-left: 0;
        letter-spacing: .36px;
        padding: 0;
        line-height: 28px;
    }

    .course .cate-list .this {
        color: #ffc210;
        border: 1px solid #ffc210 !important;
        border-radius: 30px;
    }

    .course .ordering::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering ul {
        float: left;
    }

    .course .ordering ul::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering .condition-result {
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        line-height: 28px;
    }

    .course .ordering ul li {
        float: left;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
    }

    .course .ordering .title {
        font-size: 16px;
        color: #888;
        letter-spacing: .36px;
        margin-left: 0;
        padding: 0;
        line-height: 28px;
    }

    .course .ordering .this {
        color: #ffc210;
    }

    .course .ordering .price {
        position: relative;
    }

    .course .ordering .price::before,
    .course .ordering .price::after {
        cursor: pointer;
        content: "";
        display: block;
        width: 0px;
        height: 0px;
        border: 5px solid transparent;
        position: absolute;
        right: 0;
    }

    .course .ordering .price::before {
        border-bottom: 5px solid #aaa;
        margin-bottom: 2px;
        top: 2px;
    }

    .course .ordering .price::after {
        border-top: 5px solid #aaa;
        bottom: 2px;
    }

    .course .ordering .price_up::before {
        border-bottom-color: #ffc210;
    }

    .course .ordering .price_down::after {
        border-top-color: #ffc210;
    }

    .course .course-item:hover {
        box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
    }

    .course .course-item {
        width: 1100px;
        background: #fff;
        padding: 20px 30px 20px 20px;
        margin-bottom: 35px;
        border-radius: 2px;
        cursor: pointer;
        box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
        /* css3.0 过渡动画 hover 事件操作 */
        transition: all .2s ease;
    }

    .course .course-item::after {
        content: "";
        display: block;
        clear: both;
    }

    /* 顶级元素 父级元素  当前元素{} */
    .course .course-item .course-image {
        float: left;
        width: 423px;
        height: 210px;
        margin-right: 30px;
    }

    .course .course-item .course-image img {
        max-width: 100%;
        max-height: 210px;
    }

    .course .course-item .course-info {
        float: left;
        width: 596px;
    }

    .course-item .course-info h3 a {
        font-size: 26px;
        color: #333;
        font-weight: normal;
        margin-bottom: 8px;
    }

    .course-item .course-info h3 span {
        font-size: 14px;
        color: #9b9b9b;
        float: right;
        margin-top: 14px;
    }

    .course-item .course-info h3 span img {
        width: 11px;
        height: auto;
        margin-right: 7px;
    }

    .course-item .course-info .teather-info {
        font-size: 14px;
        color: #9b9b9b;
        margin-bottom: 14px;
        padding-bottom: 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
    }

    .course-item .course-info .teather-info span {
        float: right;
    }

    .course-item .section-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .section-list li {
        float: left;
        width: 44%;
        font-size: 14px;
        color: #666;
        padding-left: 22px;
        /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
        background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
        margin-bottom: 15px;
    }

    .course-item .section-list li .section-title {
        /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        display: inline-block;
        max-width: 200px;
    }

    .course-item .section-list li:hover {
        background-image: url("/src/assets/img/play-icon-yellow.svg");
        color: #ffc210;
    }

    .course-item .section-list li .free {
        width: 34px;
        height: 20px;
        color: #fd7b4d;
        vertical-align: super;
        margin-left: 10px;
        border: 1px solid #fd7b4d;
        border-radius: 2px;
        text-align: center;
        font-size: 13px;
        white-space: nowrap;
    }

    .course-item .section-list li:hover .free {
        color: #ffc210;
        border-color: #ffc210;
    }

    .course-item {
        position: relative;
    }

    .course-item .pay-box {
        position: absolute;
        bottom: 20px;
        width: 600px;
    }

    .course-item .pay-box::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .pay-box .discount-type {
        padding: 6px 10px;
        font-size: 16px;
        color: #fff;
        text-align: center;
        margin-right: 8px;
        background: #fa6240;
        border: 1px solid #fa6240;
        border-radius: 10px 0 10px 0;
        float: left;
    }

    .course-item .pay-box .discount-price {
        font-size: 24px;
        color: #fa6240;
        float: left;
    }

    .course-item .pay-box .original-price {
        text-decoration: line-through;
        font-size: 14px;
        color: #9b9b9b;
        margin-left: 10px;
        float: left;
        margin-top: 10px;
    }

    .course-item .pay-box .buy-now {
        width: 120px;
        height: 38px;
        background: transparent;
        color: #fa6240;
        font-size: 16px;
        border: 1px solid #fd7b4d;
        border-radius: 3px;
        transition: all .2s ease-in-out;
        float: right;
        text-align: center;
        line-height: 38px;
        position: absolute;
        right: 0;
        bottom: 5px;
    }

    .course-item .pay-box .buy-now:hover {
        color: #fff;
        background: #ffc210;
        border: 1px solid #ffc210;
    }

    .course .course_pagination {
        margin-bottom: 60px;
        text-align: center;
    }
</style>

6.9 课程详情页前端页面

# 1 新建FreeCourseDetail.vue

# 2 配置路由
	{
        path: '/free/detail/:pk',
        name: 'FreeCourseDetail',
        component: FreeCourseDetail
    },
    
#3 安装 vue-video播放器
	-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);
        
#4 组件
<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 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/Head"
    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://qe5smxg12.bkt.clouddn.com/3%20%E9%A6%96%E9%A1%B5%E8%BD%AE%E6%92%AD%E5%9B%BE%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0.mp4" //你的视频地址(必填)
                    }],
                }
            }
        },
        created() {
            this.get_course_id();
            this.get_course_data();
            this.get_chapter();
        },
        methods: {
            onPlayerPlay() {
                // 当视频播放时,执行的方法
                console.log('视频开始播放')
                //加广告
            },
            onPlayerPause() {
                // 当视频暂停播放时,执行的方法
                console.log('视频暂停,可以打开广告了')
                //盖张图片
            },
            get_course_id() {
                // 获取地址栏上面的课程ID
                this.course_id = this.$route.params.pk || this.$route.query.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/free/${this.course_id}/`).then(response => {
                    // window.console.log(response.data);
                    this.course_info = response.data;
                    console.log(this.course_info)
                }).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);
                })
            },
        },
        components: {
            Header,
            Footer,
            videoPlayer, // 注册组件
        }
    }
</script>

<style scoped>
    .main {
        background: #fff;
        padding-top: 30px;
    }

    .course-info {
        width: 1200px;
        margin: 0 auto;
        overflow: hidden;
    }

    .wrap-left {
        float: left;
        width: 690px;
        height: 388px;
        background-color: #000;
    }

    .wrap-right {
        float: left;
        position: relative;
        height: 388px;
    }

    .course-name {
        font-size: 20px;
        color: #333;
        padding: 10px 23px;
        letter-spacing: .45px;
    }

    .data {
        padding-left: 23px;
        padding-right: 23px;
        padding-bottom: 16px;
        font-size: 14px;
        color: #9b9b9b;
    }

    .sale-time {
        width: 464px;
        background: #fa6240;
        font-size: 14px;
        color: #4a4a4a;
        padding: 10px 23px;
        overflow: hidden;
    }

    .sale-type {
        font-size: 16px;
        color: #fff;
        letter-spacing: .36px;
        float: left;
    }

    .sale-time .expire {
        font-size: 14px;
        color: #fff;
        float: right;
    }

    .sale-time .expire .second {
        width: 24px;
        display: inline-block;
        background: #fafafa;
        color: #5e5e5e;
        padding: 6px 0;
        text-align: center;
    }

    .course-price {
        background: #fff;
        font-size: 14px;
        color: #4a4a4a;
        padding: 5px 23px;
    }

    .discount {
        font-size: 26px;
        color: #fa6240;
        margin-left: 10px;
        display: inline-block;
        margin-bottom: -5px;
    }

    .original {
        font-size: 14px;
        color: #9b9b9b;
        margin-left: 10px;
        text-decoration: line-through;
    }

    .buy {
        width: 464px;
        padding: 0px 23px;
        position: absolute;
        left: 0;
        bottom: 20px;
        overflow: hidden;
    }

    .buy .buy-btn {
        float: left;
    }

    .buy .buy-now {
        width: 125px;
        height: 40px;
        border: 0;
        background: #ffc210;
        border-radius: 4px;
        color: #fff;
        cursor: pointer;
        margin-right: 15px;
        outline: none;
    }

    .buy .free {
        width: 125px;
        height: 40px;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 15px;
        background: #fff;
        color: #ffc210;
        border: 1px solid #ffc210;
    }

    .add-cart {
        float: right;
        font-size: 14px;
        color: #ffc210;
        text-align: center;
        cursor: pointer;
        margin-top: 10px;
    }

    .add-cart img {
        width: 20px;
        height: 18px;
        margin-right: 7px;
        vertical-align: middle;
    }

    .course-tab {
        width: 100%;
        background: #fff;
        margin-bottom: 30px;
        box-shadow: 0 2px 4px 0 #f0f0f0;

    }

    .course-tab .tab-list {
        width: 1200px;
        margin: auto;
        color: #4a4a4a;
        overflow: hidden;
    }

    .tab-list li {
        float: left;
        margin-right: 15px;
        padding: 26px 20px 16px;
        font-size: 17px;
        cursor: pointer;
    }

    .tab-list .active {
        color: #ffc210;
        border-bottom: 2px solid #ffc210;
    }

    .tab-list .free {
        color: #fb7c55;
    }

    .course-content {
        width: 1200px;
        margin: 0 auto;
        background: #FAFAFA;
        overflow: hidden;
        padding-bottom: 40px;
    }

    .course-tab-list {
        width: 880px;
        height: auto;
        padding: 20px;
        background: #fff;
        float: left;
        box-sizing: border-box;
        overflow: hidden;
        position: relative;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .tab-item {
        width: 880px;
        background: #fff;
        padding-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .tab-item-title {
        justify-content: space-between;
        padding: 25px 20px 11px;
        border-radius: 4px;
        margin-bottom: 20px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
        overflow: hidden;
    }

    .chapter {
        font-size: 17px;
        color: #4a4a4a;
        float: left;
    }

    .chapter-length {
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        letter-spacing: .19px;
    }

    .chapter-title {
        font-size: 16px;
        color: #4a4a4a;
        letter-spacing: .26px;
        padding: 12px;
        background: #eee;
        border-radius: 2px;
        display: -ms-flexbox;
        display: flex;
        -ms-flex-align: center;
        align-items: center;
    }

    .chapter-title img {
        width: 18px;
        height: 18px;
        margin-right: 7px;
        vertical-align: middle;
    }

    .section-list {
        padding: 0 20px;
    }

    .section-list .section-item {
        padding: 15px 20px 15px 36px;
        cursor: pointer;
        justify-content: space-between;
        position: relative;
        overflow: hidden;
    }

    .section-item .name {
        font-size: 14px;
        color: #666;
        float: left;
    }

    .section-item .index {
        margin-right: 5px;
    }

    .section-item .free {
        font-size: 12px;
        color: #fff;
        letter-spacing: .19px;
        background: #ffc210;
        border-radius: 100px;
        padding: 1px 9px;
        margin-left: 10px;
    }

    .section-item .time {
        font-size: 14px;
        color: #666;
        letter-spacing: .23px;
        opacity: 1;
        transition: all .15s ease-in-out;
        float: right;
    }

    .section-item .time img {
        width: 18px;
        height: 18px;
        margin-left: 15px;
        vertical-align: text-bottom;
    }

    .section-item .try {
        width: 86px;
        height: 28px;
        background: #ffc210;
        border-radius: 4px;
        font-size: 14px;
        color: #fff;
        position: absolute;
        right: 20px;
        top: 10px;
        opacity: 0;
        transition: all .2s ease-in-out;
        cursor: pointer;
        outline: none;
        border: none;
    }

    .section-item:hover {
        background: #fcf7ef;
        box-shadow: 0 0 0 0 #f3f3f3;
    }

    .section-item:hover .name {
        color: #333;
    }

    .section-item:hover .try {
        opacity: 1;
    }

    .course-side {
        width: 300px;
        height: auto;
        margin-left: 20px;
        float: right;
    }

    .teacher-info {
        background: #fff;
        margin-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .side-title {
        font-weight: normal;
        font-size: 17px;
        color: #4a4a4a;
        padding: 18px 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
    }

    .side-title span {
        display: inline-block;
        border-left: 2px solid #ffc210;
        padding-left: 12px;
    }

    .teacher-content {
        padding: 30px 20px;
        box-sizing: border-box;
    }

    .teacher-content .cont1 {
        margin-bottom: 12px;
        overflow: hidden;
    }

    .teacher-content .cont1 img {
        width: 54px;
        height: 54px;
        margin-right: 12px;
        float: left;
    }

    .teacher-content .cont1 .name {
        float: right;
    }

    .teacher-content .cont1 .teacher-name {
        width: 188px;
        font-size: 16px;
        color: #4a4a4a;
        padding-bottom: 4px;
    }

    .teacher-content .cont1 .teacher-title {
        width: 188px;
        font-size: 13px;
        color: #9b9b9b;
        white-space: nowrap;
    }

    .teacher-content .narrative {
        font-size: 14px;
        color: #666;
        line-height: 24px;
    }
</style>

6.10 课程单查接口

class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
    queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
    serializer_class = serializer.CourseModelSerializer
    pagination_class = PageNumberPagination

    # 过滤和排序
    # filter_backends=[DjangoFilterBackend,OrderingFilter,SearchFilter]
    # filter_backends=[DjangoFilterBackend,OrderingFilter,MyFilter]
    filter_backends=[DjangoFilterBackend,OrderingFilter]
    # # filter_backends=OrderingFilter
    ordering_fields=['id', 'price', 'students']
    # # search_fields=['course_category']
    filter_fields=['course_category']

6.11 章节分类接口

#1 urls.py
router.register('chapters', views.CourseChapterView, 'coursechapter')

# 2 views.py
class CourseChapterView(GenericViewSet,ListModelMixin):
    queryset = models.CourseChapter.objects.filter(is_delete=False,is_show=True)
    serializer_class = serializer.CourseChapterSerializer

    # 可以按照课程id来查
    filter_backends = [DjangoFilterBackend]
    filter_fields = ['course']
    
    
    
# 3 serializer.py
class CourseSectionSerializer(serializers.ModelSerializer):
    class Meta:
        model=models.CourseSection
        fields=['name','orders','duration','free_trail','section_link','section_type_name']
class CourseChapterSerializer(serializers.ModelSerializer):
    # 子序列化的方式
    coursesections=CourseSectionSerializer(many=True)
    class Meta:
        model=models.CourseChapter
        fields=['name','summary','chapter','coursesections']

6.12 七牛云视频托管

# 1 注册七牛云,实名认证
https://www.qiniu.com/
    
# 2 登录--->管理控制台--->对象存储

# 3 新建空间---->上传视频--->拿到视频连接

7 搜索

6.13 后台搜索接口

# urls.py
router.register('search', views.CouresSearchView, 'search')


# views.py
class CouresSearchView(GenericViewSet,ListModelMixin):
    queryset = models.Course.objects.filter(is_delete=False,is_show=True)
    serializer_class = serializer.CourseModelSerializer
    pagination_class = PageNumberPagination

    filter_backends=[SearchFilter]
    search_fields=['name']

7.2 Head组件更新

在Head.vue中增加搜索框,js,css

# template
    <form class="search">
        <div class="tips" v-if="is_search_tip">
            <span @click="search_action('Python')">Python</span>
            <span @click="search_action('Linux')">Linux</span>
        </div>
        <input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search" v-model="search_word">
        <button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)"></button>
    </form>
</template>
    
# script标签:


methods:{
search_action(search_word) {
    if (!search_word) {
        this.$message('请输入要搜索的内容');
        return
    }
    //this.$route.params  从路径中取值
    //this.$route.query   从?后面的取
    // if (search_word !== this.$route.query.word) {
        //     this.$router.push(`/search?word=${search_word}`);
        // }
    this.$router.push(`/search?word=${search_word}`);
    this.search_word = '';
},
on_search() {
    this.search_placeholder = '请输入想搜索的课程';
    this.is_search_tip = false;
},
off_search() {
    this.search_placeholder = '';
    this.is_search_tip = true;
},
}



# css:
    

.search {
    float: right;
    position: relative;
    margin-top: 22px;
    margin-right: 10px;
}

.search input, .search button {
    border: none;
    outline: none;
    background-color: white;
}

.search input {
    border-bottom: 1px solid #eeeeee;
}

.search input:focus {
    border-bottom-color: orange;
}

.search input:focus + button {
        color: orange;
    }

.search .tips {
        position: absolute;
        bottom: 3px;
        left: 0;
    }

.search .tips span {
        border-radius: 11px;
        background-color: #eee;
        line-height: 22px;
        display: inline-block;
        padding: 0 7px;
        margin-right: 3px;
        cursor: pointer;
        color: #aaa;
        font-size: 14px;

    }

.search .tips span:hover {
        color: orange;
    }

7.3 前台搜索页面

# 1  vue携带参数给另一个页面
    -路由:path: '/free/detail/:pk',this.$route.params.pk  从路径中取值
	-search?word=dsa  从this.$route.query.word取出    从?后面的取
    
    
# 2 样式
<template>
    <div class="search-course course">
        <Header/>
        <h1>总共搜索到 {{course_total}}课程</h1>
        <!-- 课程列表 -->
        <div class="main">
            <div v-if="course_list.length > 0" class="course-list">
                <div class="course-item" v-for="course in course_list" :key="course.name">
                    <div class="course-image">
                        <img :src="course.course_img" alt="">
                    </div>
                    <div class="course-info">
                        <h3>
                            <router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
                            <span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
                        <p class="teather-info">
                            {{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
                            <span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
                            <span v-else>共{{course.sections}}课时/更新完成</span>
                        </p>
                        <ul class="section-list">
                            <li v-for="(section, key) in course.section_list" :key="section.name"><span
                                    class="section-title">0{{key+1}}  |  {{section.name}}</span>
                                <span class="free" v-if="section.free_trail">免费</span></li>
                        </ul>
                        <div class="pay-box">
                            <div v-if="course.discount_type">
                                <span class="discount-type">{{course.discount_type}}</span>
                                <span class="discount-price">¥{{course.real_price}}元</span>
                                <span class="original-price">原价:{{course.price}}元</span>
                            </div>
                            <span v-else class="discount-price">¥{{course.price}}元</span>
                            <span class="buy-now">立即购买</span>
                        </div>
                    </div>
                </div>
            </div>
            <div v-else style="text-align: center; line-height: 60px">
                没有搜索结果
            </div>
            <div class="course_pagination block">
                <el-pagination
                        @size-change="handleSizeChange"
                        @current-change="handleCurrentChange"
                        :current-page.sync="filter.page"
                        :page-sizes="[2, 3, 5, 10]"
                        :page-size="filter.page_size"
                        layout="sizes, prev, pager, next"
                        :total="course_total">
                </el-pagination>
            </div>
        </div>
    </div>
</template>

<script>
    import Header from '../components/Head'

    export default {
        name: "SearchCourse",
        components: {
            Header,
        },
        data() {
            return {
                course_list: [],
                course_total: 0,
                filter: {
                    page_size: 10,
                    page: 1,
                    search: '',
                }
            }
        },
        created() {
            this.get_course()
        },
        watch: {
            '$route.query' () {
                this.get_course()
            }
        },
        methods: {
            handleSizeChange(val) {
                // 每页数据量发生变化时执行的方法
                this.filter.page = 1;
                this.filter.page_size = val;
            },
            handleCurrentChange(val) {
                // 页码发生变化时执行的方法
                this.filter.page = val;
            },
            get_course() {
                // 获取搜索的关键字
                this.filter.search = this.$route.query.word || this.$route.query.wd;

                // 获取课程列表信息
                this.$axios.get(`${this.$settings.base_url}/course/search/`, {
                    params: this.filter
                }).then(response => {
                    // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
                    this.course_list = response.data.results;
                    this.course_total = response.data.count;
                }).catch(() => {
                    this.$message({
                        message: "获取课程信息有误,请联系客服工作人员"
                    })
                })
            }
        }
    }
</script>

<style scoped>
    .course {
        background: #f6f6f6;
    }

    .course .main {
        width: 1100px;
        margin: 35px auto 0;
    }

    .course .condition {
        margin-bottom: 35px;
        padding: 25px 30px 25px 20px;
        background: #fff;
        border-radius: 4px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }

    .course .cate-list {
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
        padding-bottom: 18px;
        margin-bottom: 17px;
    }

    .course .cate-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .cate-list li {
        float: left;
        font-size: 16px;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
        border: 1px solid transparent; /* transparent 透明 */
    }

    .course .cate-list .title {
        color: #888;
        margin-left: 0;
        letter-spacing: .36px;
        padding: 0;
        line-height: 28px;
    }

    .course .cate-list .this {
        color: #ffc210;
        border: 1px solid #ffc210 !important;
        border-radius: 30px;
    }

    .course .ordering::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering ul {
        float: left;
    }

    .course .ordering ul::after {
        content: "";
        display: block;
        clear: both;
    }

    .course .ordering .condition-result {
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        line-height: 28px;
    }

    .course .ordering ul li {
        float: left;
        padding: 6px 15px;
        line-height: 16px;
        margin-left: 14px;
        position: relative;
        transition: all .3s ease;
        cursor: pointer;
        color: #4a4a4a;
    }

    .course .ordering .title {
        font-size: 16px;
        color: #888;
        letter-spacing: .36px;
        margin-left: 0;
        padding: 0;
        line-height: 28px;
    }

    .course .ordering .this {
        color: #ffc210;
    }

    .course .ordering .price {
        position: relative;
    }

    .course .ordering .price::before,
    .course .ordering .price::after {
        cursor: pointer;
        content: "";
        display: block;
        width: 0px;
        height: 0px;
        border: 5px solid transparent;
        position: absolute;
        right: 0;
    }

    .course .ordering .price::before {
        border-bottom: 5px solid #aaa;
        margin-bottom: 2px;
        top: 2px;
    }

    .course .ordering .price::after {
        border-top: 5px solid #aaa;
        bottom: 2px;
    }

    .course .ordering .price_up::before {
        border-bottom-color: #ffc210;
    }

    .course .ordering .price_down::after {
        border-top-color: #ffc210;
    }

    .course .course-item:hover {
        box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
    }

    .course .course-item {
        width: 1100px;
        background: #fff;
        padding: 20px 30px 20px 20px;
        margin-bottom: 35px;
        border-radius: 2px;
        cursor: pointer;
        box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
        /* css3.0 过渡动画 hover 事件操作 */
        transition: all .2s ease;
    }

    .course .course-item::after {
        content: "";
        display: block;
        clear: both;
    }

    /* 顶级元素 父级元素  当前元素{} */
    .course .course-item .course-image {
        float: left;
        width: 423px;
        height: 210px;
        margin-right: 30px;
    }

    .course .course-item .course-image img {
        max-width: 100%;
        max-height: 210px;
    }

    .course .course-item .course-info {
        float: left;
        width: 596px;
    }

    .course-item .course-info h3 a {
        font-size: 26px;
        color: #333;
        font-weight: normal;
        margin-bottom: 8px;
    }

    .course-item .course-info h3 span {
        font-size: 14px;
        color: #9b9b9b;
        float: right;
        margin-top: 14px;
    }

    .course-item .course-info h3 span img {
        width: 11px;
        height: auto;
        margin-right: 7px;
    }

    .course-item .course-info .teather-info {
        font-size: 14px;
        color: #9b9b9b;
        margin-bottom: 14px;
        padding-bottom: 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
    }

    .course-item .course-info .teather-info span {
        float: right;
    }

    .course-item .section-list::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .section-list li {
        float: left;
        width: 44%;
        font-size: 14px;
        color: #666;
        padding-left: 22px;
        /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
        background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
        margin-bottom: 15px;
    }

    .course-item .section-list li .section-title {
        /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
        display: inline-block;
        max-width: 200px;
    }

    .course-item .section-list li:hover {
        background-image: url("/src/assets/img/play-icon-yellow.svg");
        color: #ffc210;
    }

    .course-item .section-list li .free {
        width: 34px;
        height: 20px;
        color: #fd7b4d;
        vertical-align: super;
        margin-left: 10px;
        border: 1px solid #fd7b4d;
        border-radius: 2px;
        text-align: center;
        font-size: 13px;
        white-space: nowrap;
    }

    .course-item .section-list li:hover .free {
        color: #ffc210;
        border-color: #ffc210;
    }

    .course-item {
        position: relative;
    }

    .course-item .pay-box {
        position: absolute;
        bottom: 20px;
        width: 600px;
    }

    .course-item .pay-box::after {
        content: "";
        display: block;
        clear: both;
    }

    .course-item .pay-box .discount-type {
        padding: 6px 10px;
        font-size: 16px;
        color: #fff;
        text-align: center;
        margin-right: 8px;
        background: #fa6240;
        border: 1px solid #fa6240;
        border-radius: 10px 0 10px 0;
        float: left;
    }

    .course-item .pay-box .discount-price {
        font-size: 24px;
        color: #fa6240;
        float: left;
    }

    .course-item .pay-box .original-price {
        text-decoration: line-through;
        font-size: 14px;
        color: #9b9b9b;
        margin-left: 10px;
        float: left;
        margin-top: 10px;
    }

    .course-item .pay-box .buy-now {
        width: 120px;
        height: 38px;
        background: transparent;
        color: #fa6240;
        font-size: 16px;
        border: 1px solid #fd7b4d;
        border-radius: 3px;
        transition: all .2s ease-in-out;
        float: right;
        text-align: center;
        line-height: 38px;
        position: absolute;
        right: 0;
        bottom: 5px;
    }

    .course-item .pay-box .buy-now:hover {
        color: #fff;
        background: #ffc210;
        border: 1px solid #ffc210;
    }

    .course .course_pagination {
        margin-bottom: 60px;
        text-align: center;
    }
</style>    

8 支付宝

8.1 支付宝sdk使用方法

# 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm

# https://opendocs.alipay.com/open/291/106097/ 下载软件,生成秘钥(公钥私钥)
# 取出公钥,配置到支付宝平台,会生成一个支付宝公钥






# 安装支付宝sdk
# pip install python-alipay-sdk
from alipay import AliPay


# app_private_key_string = open("/path/to/your/private/key.pem").read()
# alipay_public_key_string = open("/path/to/alipay/public/key.pem").read()
app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApkFnp38PSslhn4wvRarL40+WtVpan6tVT55FWzAzLfQ8PfqZE2qW7mCJy1/zM07+S5L+X0pC+
...
-----END RSA PRIVATE KEY-----
"""

alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwU9mMZHHlQPE9FcxVtXOXhbWCtuDZLJRVCiofdbTVmRXr...
-----END PUBLIC KEY-----
"""

alipay = AliPay(
    appid="2016092000554611",	# 支付宝appid
    app_notify_url='http://127.0.0.1:8000/home/',  # the default notify path
    app_private_key_string=app_private_key_string,
    alipay_public_key_string=alipay_public_key_string,
    sign_type="RSA2", # RSA or RSA2
    debug=True  # False by default
)


alipay_url='https://openapi.alipaydev.com/gateway.do?'
order_string = alipay.api_alipay_trade_page_pay    (
    out_trade_no="20161112www4334",
    total_amount=9999,
    subject='韩红版充气娃娃',
    return_url="https://www.luffycity.com/free-course",
    notify_url="https://www.luffycity.com/free-course"
)
print(alipay_url+order_string)

8.2 支付宝功能二次封装

al_pay
    -pem
    -__init__.py
    -pay.py
    -setting.py
    
    
    
    
    
#__init__.py
from .pay import alipay,gateway



#pay.py
from alipay import AliPay
from . import setting
alipay = AliPay(
    appid=setting.APPID,
    app_notify_url=None,  # the default notify path
    app_private_key_string=setting.APP_PRIVATE_KEY_STRING,
    # alipay public key, do not use your own public key!
    alipay_public_key_string=setting.ALIPAY_PUBLIC_KEY_STRING,
    sign_type=setting.SIGN_TYPE, # RSA or RSA2
    debug=setting.DEBUG  # False by default
)
gateway=setting.GATEWAY




# setting.py
import os
APPID="2016092000554611"
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','private_key.pem')).read()
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','al_public_key.pem')).read()
SIGN_TYPE='RSA2'
DEBUG=True
GATEWAY='https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'

9 订单

9.1 订单模型与表分析

# 订单表分析
	-订单表
    -订单详情
    
    
from django.db import models
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="支付时间")
    # 一个用户可以下多个订单,一个订单只属于一个用户,一对多的关系,关联字段写在多个一方,写在order方
    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='创建时间')
    updated_time = models.DateTimeField(auto_now=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.SET_NULL, db_constraint=False, verbose_name="课程",null=True)
    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__()

9.2 订单模块接口分析

# 1 支付接口(生成订单,生成支付连接,返回支付连接)
	-order表和orderdetail表插入数据,重写create方法
    -生成订单号(uuid)
    -登录后才能生成订单(jwt认证)
    -当前登录用户就是下单用户,存到order表中
    -下了三个课程,总价格100,前端提交的价格是99,价格不符需要进行异常处理
        # 1)订单总价校验
        # 2)生成订单号
        # 3)支付用户:request.user
        # 4)支付链接生成
        # 5)入库(两个表)的信息准备
        
        
# 2 支付宝异步回调的post接口(验证签名,修改订单状态)

# 3 当支付宝get回调前端,vue组件一创建,立马向后端发一个请求(get)

9.3 支付接口

9.3.1 支付接口思路

# 1 前端传输的数据格式
	{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
    
    
# 2 后端接到数据要校验
	course:[1,2,3]===>course:[obj1,obj2,obj3]
	可以在序列化类中定义: 					                  	
    course = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)
    
    
# 3 需要在序列化类的validate中处理的逻辑
        # 1)订单总价校验:
        	-取出一个个课程的价格,累加是否等于传入的总价格
        # 2)生成订单号
        	-通过uuid生成
        # 3)支付用户:request.user
        	-通过视图和序列化类之间的桥梁context对象传递
        	-重新视图类中的create方法,把request对象放入context
        	-self.context.get('request').user
        # 4)支付链接生成
        	-导入封装的支付宝支付,生成
        	order_string = alipay.api_alipay_trade_page_pay    (
            out_trade_no=out_trade_no,
            total_amount=total_amout,
            subject=subject,
            return_url=settings.RETURN_URL,  # get回调,前台地址
            notify_url=settings.NOTIFY_URL   # post回调,后台地址
        )
        # 5)入库(两个表)的信息准备
        	-把user放入attrs中
        	-把订单号,放入attrs中
        	attrs['user']=user
        	attrs['out_trade_no']=out_trade_no
        	self.context['pay_url']=pay_url

    
 # 4 重写序列化类的create方法
	-把course_list弹出来
    -order=models.Order.objects.create(**validated_data)

9.3.2 支付接口代码

# urls.py
router = SimpleRouter()
router.register('pay', views.PayView, 'pay')
urlpatterns = [
    path('', include(router.urls)),
]



# views.py
class PayView(GenericViewSet,CreateModelMixin):
    authentication_classes = [JSONWebTokenAuthentication,]
    permission_classes = [IsAuthenticated,]
    queryset = models.Order.objects.all()
    serializer_class = serializer.OrderSerializer

    # 重写create方法
    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'))
    
    
    
# serializer.py
class OrderSerializer(serializers.ModelSerializer):
    # 前端传什么数据过来{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
    # user字段需要,但是不是传的,使用了jwt


    # 需要把course:[1,2,3] 处理成 course:[obj1,obj2,obj3]

    # 课时:[1,4,6,]===>课时:[obj1,obj4,obj6,]
    # course=serializers.CharField()
    course=serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)

    class Meta:
        model = models.Order
        fields = ['total_amount','subject','pay_type','course']
        extra_kwargs={
            '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_price!=total_amount:
            raise ValidationError('价格不合法')
        return total_amount

    def _gen_out_trade_no(self):
        import uuid
        return str(uuid.uuid4()).replace('-','')

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

    def _gen_pay_url(self,out_trade_no,total_amout,subject):
        # total_amout是Decimal类型,识别不了,需要转换成float类型
        from luffyapi.libs.al_pay import alipay,gateway
        order_string = alipay.api_alipay_trade_page_pay    (
            out_trade_no=out_trade_no,
            total_amount=float(total_amout),
            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
        attrs['out_trade_no']=out_trade_no

        self.context['pay_url']=pay_url
    def validate(self, attrs):
        '''
        # 1)订单总价校验
        # 2)生成订单号
        # 3)支付用户:request.user
        # 4)支付链接生成
        # 5)入库(两个表)的信息准备
        '''
        # 1)订单总价校验
        total_amout = self._check_price(attrs)
        # 2)生成订单号
        out_trade_no=self._gen_out_trade_no()
        # 3)支付用户:request.user
        user=self._get_user()
        # 4)支付链接生成
        pay_url=self._gen_pay_url(out_trade_no,total_amout,attrs.get('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

9.4 支付回调接口配置

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

9.5 前台生成订单并跳转

# FreeCourse.vue
<span class="buy-now" @click="buy_now(course)">立即购买</span>


# script
 buy_now(course) {
                let token = this.$cookies.get('token')
                if (!token) {
                    this.$message({
                        message: "您还没有登录,请先登录",
                    })
                    return false
                }
                this.$axios({
                        method: 'post',
                        url: this.$settings.base_url + '/order/pay/',
                        data: {
                            "total_amount": course.price,
                            "subject": course.name,
                            "pay_type": 1,
                            "course": [
                                course.id,
                            ]
                        },
                        headers: {Authorization: 'jwt ' + token}
                    }
                ).then(response => {
                    console.log(response.data)
                    let pay_url=response.data
                    //前端发送get请求
                    open(pay_url,'_self')
                }).catch(error => {
                })

            },

9.6 前台支付成功页面

# 路由
{
        path: '/pay/success',
        name: 'PaySuccess',
        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>
    </div>
</template>

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

    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>

9.7 支付宝get回调的参数

charset=utf-8&
out_trade_no=17fcf8cac17c442bbbc114df6004afff&
method=alipay.trade.page.pay.return&
total_amount=99.00&
sign=WfK2beWFKvVaTHXpREi8HqZtFRH3JbeIvkliReYvfuhAsqaxHguARKtW6jUqUdZinm7ZSaYE1NrBRQa3%2BLquMk6uMnxE0i%2FTXIu4%2FmNTCEqSUlG8fTRPwC2%2BuU4nN1Ym0eM4puzAc2TUnEJnXCGKP9UxMifN3cjqR5BP%2B3RRngZSS4IQeogjurpfdiIolLzed%2FHTWbc4HqvWlWn9JuLmFGTtKHvRRKFr1hqq8Pj%2Fe3Al8kieDN9Q7JhEdC6F5ROo9rLlmUJtevkjI22oRScrfJl5hb%2BeYosxNg3WktmYKlF5vsKeZKKnLayAvKGoySLvaWk90x0LijHzzf2%2F8a9s3w%3D%3D&
trade_no=2020072922001480160500851368&
auth_app_id=2016092000554611&
version=1.0&
app_id=2016092000554611&
sign_type=RSA2&
seller_id=2088102176466324&
timestamp=2020-07-29%2015%3A06%3A44

9.8 后端同步异步支付回调接口

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).first()
        if order.order_status==1:
            return Response(True)
        else:
            return Response(False)

    def post(self,request,*args,**kwargs):
        '''
        支付宝回调接口
        '''
        from luffyapi.libs.al_pay import alipay
        from luffyapi.utils.logger import log
        data = request.data
        out_trade_no=data.get('out_trade_no',None)
        gmt_payment=data.get('gmt_payment',None)
        signature = data.pop("sign")
        # 验证签名
        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)
            log.info('%s订单支付成功'%out_trade_no)
            return Response('success')
        else:
            log.info('%s订单有问题' % out_trade_no)
            return Response('error')

10 上线

详见项目上线篇

上线前准备:

# 前端执行 npm run build   把你写的vue代码编译成html,css,js

# 后端代码
	-修改setting中的pro.py
    -项目根路径新建一个manage_pro.py(把原来的manage.py复制改动上线的配置文件)
    -wsgi.py:改成线上的dev
 posted on 2020-08-03 16:03  wwwpy  阅读(208)  评论(0编辑  收藏  举报