• 路飞项目主体


    路飞项目

    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 {
             1200px;
            margin: 0 auto;
            color: #aaa;
            font-size: 13px;
            line-height: 40px;
        }
    
        .nav {
            background-color: white;
            user-select: none;
             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 {
             100%;
            height: 128px;
            background: #25292e;
            color: #fff;
        }
    
        .footer ul {
            margin: 0 auto 16px;
            padding-top: 38px;
             810px;
        }
    
        .footer ul li {
            float: left;
             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- 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{
         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 {
             100vw;
            height: 100vh;
            position: fixed;
            top: 0;
            left: 0;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.3);
        }
    
        .box {
             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;
             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 {
             100%;
            font-size: 18px;
        }
    
        .foot > span {
            float: right;
            margin-top: 20px;
            color: orange;
            cursor: pointer;
        }
    
        .sms {
            color: orange;
            cursor: pointer;
            display: inline-block;
             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 {
             1200px;
            margin: 0 auto;
            color: #aaa;
            font-size: 13px;
            line-height: 40px;
        }
    
        .nav {
            background-color: white;
            user-select: none;
             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 {
             100vw;
            height: 100vh;
            position: fixed;
            top: 0;
            left: 0;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.3);
        }
    
        .box {
             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;
             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 {
             100%;
            font-size: 18px;
        }
    
        .foot > span {
            float: right;
            margin-top: 20px;
            color: orange;
            cursor: pointer;
        }
    
        .sms {
            color: orange;
            cursor: pointer;
            display: inline-block;
             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 {
             100vw;
            height: 100vh;
            position: fixed;
            top: 0;
            left: 0;
            z-index: 10;
            background-color: rgba(0, 0, 0, 0.3);
        }
    
        .box {
             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;
             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 {
             100%;
            font-size: 18px;
        }
    
        .foot > span {
            float: right;
            margin-top: 20px;
            color: orange;
            cursor: pointer;
        }
    
        .sms {
            color: orange;
            cursor: pointer;
            display: inline-block;
             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 {
             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;
             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 {
             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;
             423px;
            height: 210px;
            margin-right: 30px;
        }
    
        .course .course-item .course-image img {
             100%;
        }
    
        .course .course-item .course-info {
            float: left;
             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 {
             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;
             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- 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 {
             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 {
             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学院20162017年度最受学员喜爱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 {
             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;
             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 {
             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;
             423px;
            height: 210px;
            margin-right: 30px;
        }
    
        .course .course-item .course-image img {
            max- 100%;
            max-height: 210px;
        }
    
        .course .course-item .course-info {
            float: left;
             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 {
             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;
             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- 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 {
             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;
             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 {
             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 {
             1200px;
            margin: 0 auto;
            overflow: hidden;
        }
    
        .wrap-left {
            float: left;
             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 {
             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 {
             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 {
             464px;
            padding: 0px 23px;
            position: absolute;
            left: 0;
            bottom: 20px;
            overflow: hidden;
        }
    
        .buy .buy-btn {
            float: left;
        }
    
        .buy .buy-now {
             125px;
            height: 40px;
            border: 0;
            background: #ffc210;
            border-radius: 4px;
            color: #fff;
            cursor: pointer;
            margin-right: 15px;
            outline: none;
        }
    
        .buy .free {
             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 {
             20px;
            height: 18px;
            margin-right: 7px;
            vertical-align: middle;
        }
    
        .course-tab {
             100%;
            background: #fff;
            margin-bottom: 30px;
            box-shadow: 0 2px 4px 0 #f0f0f0;
    
        }
    
        .course-tab .tab-list {
             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 {
             1200px;
            margin: 0 auto;
            background: #FAFAFA;
            overflow: hidden;
            padding-bottom: 40px;
        }
    
        .course-tab-list {
             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 {
             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 {
             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 {
             18px;
            height: 18px;
            margin-left: 15px;
            vertical-align: text-bottom;
        }
    
        .section-item .try {
             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 {
             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 {
             54px;
            height: 54px;
            margin-right: 12px;
            float: left;
        }
    
        .teacher-content .cont1 .name {
            float: right;
        }
    
        .teacher-content .cont1 .teacher-name {
             188px;
            font-size: 16px;
            color: #4a4a4a;
            padding-bottom: 4px;
        }
    
        .teacher-content .cont1 .teacher-title {
             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 {
             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;
             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 {
             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;
             423px;
            height: 210px;
            margin-right: 30px;
        }
    
        .course .course-item .course-image img {
            max- 100%;
            max-height: 210px;
        }
    
        .course .course-item .course-info {
            float: left;
             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 {
             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;
             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- 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 {
             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;
             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 {
             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;
             1200px;
            background: #fff;
        }
    
        .main .title {
            display: flex;
            -ms-flex-align: center;
            align-items: center;
            padding: 25px 40px;
            border-bottom: 1px solid #f2f2f2;
        }
    
        .main .title .success-tips {
            box-sizing: border-box;
        }
    
        .title img {
            vertical-align: middle;
             60px;
            height: 60px;
            margin-right: 40px;
        }
    
        .title .success-tips {
            box-sizing: border-box;
        }
    
        .title .tips {
            font-size: 26px;
            color: #000;
        }
    
    
        .info span {
            color: #ec6730;
        }
    
        .order-info {
            padding: 25px 48px;
            padding-bottom: 15px;
            border-bottom: 1px solid #f2f2f2;
        }
    
        .order-info p {
            display: -ms-flexbox;
            display: flex;
            margin-bottom: 10px;
            font-size: 16px;
        }
    
        .order-info p b {
            font-weight: 400;
            color: #9d9d9d;
            white-space: nowrap;
        }
    
        .study {
            padding: 25px 40px;
        }
    
        .study span {
            display: block;
             140px;
            height: 42px;
            text-align: center;
            line-height: 42px;
            cursor: pointer;
            background: #ffc210;
            border-radius: 6px;
            font-size: 16px;
            color: #fff;
        }
    </style>
    

    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
    
  • 相关阅读:
    FrameBuffer系列 之 一点资源
    FrameBuffer系列 之 显示图片
    FrameBuffer系列 之 相关结构与结构体
    FrameBuffer系列 之 介绍
    FrameBuffer系列 之 简单编程
    程序员五大层次,你属于哪一层?
    提高编程效率的14件事
    GTK简单了解记录
    __read_mostly变量含义
    [系统启动]Printk与sched_clock_init的一点分析
  • 原文地址:https://www.cnblogs.com/achai222/p/13426819.html
Copyright © 2020-2023  润新知