chijintao

Luffy后台

环境

"""
为luffy项目创建一个虚拟环境
>: mkvirtualenv luffy
"""

"""
按照基础环境依赖
>: pip install django==2.2.2
>: pip install djangorestframework
>: pip install pymysql
"""

创建项目

"""
前提:在目标目录新建luffy文件夹
>: cd 建立的luffy文件夹
>: django-admin startproject luffyapi

开发:用pycharm打开项目,并选择提前备好的虚拟环境
"""

重构项目目录

"""
├── luffyapi
	├── logs/				# 项目运行时/开发时日志目录 - 包
    ├── manage.py			# 脚本文件
    ├── luffyapi/      		# 项目主应用,开发时的代码保存 - 包
     	├── apps/      		# 开发者的代码保存目录,以模块[子应用]为目录保存 - 包
        ├── libs/      		# 第三方类库的保存目录[第三方组件、模块] - 包
    	├── settings/  		# 配置目录 - 包
			├── dev.py   	# 项目开发时的本地配置
			└── prod.py  	# 项目上线时的运行配置
		├── urls.py    		# 总路由
		└── utils/     		# 多个模块[子应用]的公共函数类库[自己开发的组件]
    └── scripts/       		# 保存项目运营时的脚本文件 - 文件夹
"""

配置开发环境

# 环境变量操作:小luffyapiBASE_DIR与apps文件夹都要添加到环境变量
import sys
sys.path.insert(0, BASE_DIR)
APPS_DIR = os.path.join(BASE_DIR, \'apps\')
sys.path.insert(1, APPS_DIR)

"""
1.修改 wsgi.py 与 manage.py 两个文件:
# manage.py
os.environ.setdefault(\'DJANGO_SETTINGS_MODULE\', \'luffyapi.settings.dev\')
# wsgi.py
os.environ.setdefault(\'DJANGO_SETTINGS_MODULE\', \'luffyapi.settings.prod\')
# manage_prod.py
os.environ.setdefault(\'DJANGO_SETTINGS_MODULE\', \'luffyapi.settings.prod\')

2.将settings.py删除或改名,内容拷贝到settings/dev.py中

3.修改dev.py文件内容
LANGUAGE_CODE = \'zh-hans\'
TIME_ZONE = \'Asia/Shanghai\'
USE_TZ = False

4.修改启动配置:见插图

5.在任何一个__init__.py文件中测试默认配置文件是否是dev.py文件
from django.conf import settings
print(settings)
"""

img

Luffy后台配置

在Django中使用日志

1 复制日志配置的字典到dev.py中

LOGGING = {
    \'version\': 1,
    \'disable_existing_loggers\': False,
    \'formatters\': {
        \'verbose\': {
            \'format\': \'%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s\'
        },
        \'simple\': {
            \'format\': \'%(levelname)s %(module)s %(lineno)d %(message)s\'
        },
    },
    \'filters\': {
        \'require_debug_true\': {
            \'()\': \'django.utils.log.RequireDebugTrue\',
        },
    },
    \'handlers\': {
        \'console\': {
            # 实际开发建议使用WARNING
            \'level\': \'DEBUG\',
            \'filters\': [\'require_debug_true\'],
            \'class\': \'logging.StreamHandler\',
            \'formatter\': \'simple\'
        },
        \'file\': {
            # 实际开发建议使用ERROR
            \'level\': \'INFO\',
            \'class\': \'logging.handlers.RotatingFileHandler\',
            # 日志位置,日志文件名,日志保存目录必须手动创建,注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
            \'filename\': os.path.join(os.path.dirname(BASE_DIR), "logs", "luffy.log"),
            # 日志文件的最大值,这里我们设置300M
            \'maxBytes\': 300 * 1024 * 1024,
            # 日志文件的数量,设置最大日志数量为10
            \'backupCount\': 10,
            # 日志格式:详细格式
            \'formatter\': \'verbose\',
            # 文件内容编码
            \'encoding\': \'utf-8\'
        },
    },
    # 日志对象
    \'loggers\': {
        \'django\': {
            \'handlers\': [\'console\', \'file\'],
            \'propagate\': True, # 是否让日志信息继续冒泡给其他的日志处理系统
        },
    }
}

2 新建一个utils/loggins.py,写入
import logging
def get_logger():
    logger = logging.getLogger(\'django\')
    return logger

封装项目异常处理

1 写一个函数  utils/excepitons.py

from rest_framework.views import exception_handler
from luffy_api.utils.response import Response
from luffy_api.utils.loggins import get_logger


logger = get_logger()

def common_exception_handler(exc, context):
    # print(str(context.get(\'view\')))
    # print(context.get(\'request\').Meta.get(\'REMOTE_ADDR\'))
    # 只要出异常,就要记录日志
    logger.error(\'ip为%s的用户,访问%s视图类出错,错误信息是%s\' % (
    context.get(\'request\').META.get(\'REMOTE_ADDR\'), str(context.get(\'view\')), str(exc)))
    response = exception_handler(exc, context)
    if response:  # drf的异常
        return Response({\'code\': 999, \'msg\': response.data.get(\'detail\', \'服务器异常,请联系系统管理员\')})
    else:  # django的异常
        return Response({\'code\': 999, \'msg\': \'服务器异常,请联系系统管理员\'})
    
    
2 配置文件中配置
REST_FRAMEWORK={
\'EXCEPTION_HANDLER\': \'luffy_api.utils.exceptions.common_exception_handler\', # 只要出异常,触发这个函数执行
}

封装Response对象

1 在utils下新建response.py

2 写入

from rest_framework.response import Response


class APIResponse(Response):
    def __init__(self, code=100, msg=\'成功\', status=None, headers=None, **kwargs):
        res_data = {
            \'code\': code,
            \'msg\': msg,
        }
        if kwargs:
            res_data.update(kwargs)
        super().__init__(data=res_data, status=status, headers=headers)
        
3 使用

from luffy_api.utils.response import APIResponse
class HomeView(APIView):
    def get(self,request):
        return APIResponse()  

数据库配置

1 创建路飞数据库:luffy(使用命令,使用图形化界面,指定编码utf8)
2 创建一个luffy用户,授予权限,对luffy库的增删查改权限
	-查看有哪些用户
    select user,host,authentication_string from mysql.user;
    -创建用户(lqz)
    #授权账号命令:grant 权限(create, update) on 库.表 to \'账号\'@\'host\' identified by \'密码\'
    grant all privileges on luffy.* to \'cjt\'@\'%\' identified by \'jerry123\';
    grant all privileges on luffy.* to \'cjt\'@\'localhost\' identified by \'jerry123?\';
    flush privileges;
    
    # mysql  -h 192.168.11.47 -P 3306 -ulqz -p
3 使用lqz用户登录(只能看到路飞库,操作路飞库)
	mysql -ulqz -pLqz123?
    
    
    
    
    
4 django 项目操作mysql
	-如果使用pymysql---》配置如下两句话---》自从2.0.7版本以后,需要改源码
    import pymysql
	pymysql.install_as_MySQLdb()
    -如果使用mysqlclient----》不需要任何操作
    	-mysqlclient:有的情况下,安装不上

前台项目创建

global.css

/* 声明全局样式和项目的初始化样式 */
body, h1, h2, h3, h4, h5, h6, p, table, tr, td, ul, li, a, form, input, select, option, textarea {
    margin: 0;
    padding: 0;
    font-size: 15px;
}

a {
    text-decoration: none;
    color: #333;
}

ul {
    list-style: none;
}

table {
    border-collapse: collapse; /* 合并边框 */
}

settings.js

export default {
    base_url: \'http://127.0.0.1:8000\'
}
1 vue create luffycity  # 把项目创建,打开
2 在assets新建css和js,分别放入
    global.css
    settings.js
3 全局使用css和js,在main.js中
import \'@/assets/css/global.css\'
import settings from \'./assets/js/settings\'
Vue.prototype.$settings = settings
	
    
    
4 安装第三方依赖
	-axios
    	-cnpm install axios -S
        -配置main.js	
        import axios from \'axios\'
		Vue.prototype.$http = axios
    -vue-cookies
    	-cnpm install vue-cookies -S
        -配置main.js	
        import cookies from \'vue-cookies\'
		Vue.prototype.$cookies = cookies
    -elementui
        -npm i element-ui -S
        -配置main.js	
    	import ElementUI from \'element-ui\';
        import \'element-ui/lib/theme-chalk/index.css\';
        Vue.use(ElementUI);
    -bootstrap
    	-cnpm install jquery -S
		- cnpm install bootstrap@3 -S
         -配置main.js
        import \'bootstrap\'
        import \'bootstrap/dist/css/bootstrap.min.css\'
    	-配置,在项目根路径新建vue.config.js
        const webpack = require("webpack");
        module.exports = {
            configureWebpack: {
                plugins: [
                    new webpack.ProvidePlugin({
                        $: "jquery",
                        jQuery: "jquery",
                        "window.jQuery": "jquery",
                        "window.$": "jquery",
                        Popper: ["popper.js", "default"]
                    })
                ]
            }
        };
    

扩写user表

1 切换到apps路径下,新建user app
python ../../manage.py startapp user
2 在models.py中写入
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
    mobile = models.CharField(max_length=11, unique=True)
    icon = models.ImageField(upload_to=\'icon\', default=\'icon/default.png\')  # 必须安装pillow
    class Meta:
        db_table = \'luffy_user\'  # 表明
        verbose_name = \'用户表\'  # 后台admin中显示的名字
        verbose_name_plural = verbose_name
    def __str__(self):
        return self.username
3 在配置文件中写入
AUTH_USER_MODEL=\'user.user\'
4 media配置,在配置文件中写入
MEDIA_URL = \'/media/\'
MEDIA_ROOT = os.path.join(BASE_DIR, \'media\')

5 配置media路由
  re_path(\'^media/(?P<path>.*)\', serve, {\'document_root\': settings.MEDIA_ROOT}) # 配置media的访问路径
    # path(\'media/<path:path>\', serve, {\'document_root\': settings.MEDIA_ROOT}) # 配置media的访问路径

 6 执行迁移命令(安装pillow模块)



#### 国际化相关,配置文件中设置
LANGUAGE_CODE = \'zh-hans\'
TIME_ZONE = \'Asia/Shanghai\'
USE_I18N = True
USE_L10N = True
USE_TZ = False

主页后台模块设计

  • 轮播图表(建立基表)
  • 轮播图接口

在utils下新建models.py

from django.db import models
class BaseModel(models.Model):
    created_time = models.DateTimeField(auto_now_add=True, verbose_name=\'创建时间\')
    updated_time = models.DateTimeField(auto_now=True, verbose_name=\'最后更新时间\')
    is_delete = models.BooleanField(default=False, verbose_name=\'是否删除\')
    is_show = models.BooleanField(default=True, verbose_name=\'是否上架\')
    orders = models.IntegerField(verbose_name=\'优先级\')

    class Meta:
        abstract = True  # 不在数据库生成该表,只用来做继承

在home app下models.py新建表

# 轮播图表,继承BaseModel
from luffy_api.utils.model import BaseModel
class Banner(BaseModel):
    title = models.CharField(max_length=16, unique=True, verbose_name=\'名称\')
    image = models.ImageField(upload_to=\'banner\', verbose_name=\'图片\',help_text=\'必须传入图片大小为 100*200\')
    link = models.CharField(max_length=64, verbose_name=\'跳转链接\')
    info = models.TextField(verbose_name=\'详情\')  # 也可以用详情表,宽高出处


    class Meta:
        db_table=\'luffy_banner\'
        verbose_name = \'轮播图\'  # 后台admin中显示的名字
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.title

视图

class BannerView(Common_list_view):
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by(\'orders\')
    serializer_class = BannerSerializer
    
    
class Common_list_view(ViewSetMixin,ListAPIView):
    def list(self, request, *args, **kwargs):
        response = super().list(request, *args, **kwargs)
        return APIResponse(result = response.data)

序列化器

class BannerSerializer(serializers.ModelSerializer):
    class Meta:
        model = Banner
        fields = [\'title\', \'image\', \'link\']

路由

# 路由分发

# 总路由:
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
from django.conf import settings


urlpatterns = [
    path(\'admin/\', admin.site.urls),
    path(\'api/home/\', include(\'home.urls\')),
    path(\'api/user/\', include(\'user.urls\')),
    re_path(\'^media/(?P<path>.*)\', serve, {\'document_root\': settings.MEDIA_ROOT}) # 配置media的访问路径
    # path(\'media/<path:path>\', serve, {\'document_root\': settings.MEDIA_ROOT}) # 配置media的访问路径
]

# home.urls.py

router = SimpleRouter()
router.register(\'banner\', views.BannerView, \'banner\')


urlpatterns = [
    path(\'\', include(router.urls))
]

后台管理

  • simple-ui
# 使用simple-ui的步骤
	-下载:pip3 install django-simpleui
    -在配置文件中app注册
    INSTALLED_APPS = [
    \'simpleui\',
    ]
    -使用,跟之前使用admin一样
    
    -在admin.py中注册表
    from .models import Banner
    @admin.register(Banner)
    class BannerAdmin(admin.ModelAdmin):
        # 展示那些字段
        list_display = (\'id\', \'title\',\'link\', \'created_time\')

跨域问题详解

  • 同源策略

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现

请求的url地址,必须与浏览器上的url地址处于同域上,也就是域名,端口,协议相同.

比如:我在本地上的域名是127.0.0.1:8000,请求另外一个域名:127.0.0.1:8001一段数据

浏览器上就会报错,这个就是同源策略的保护,如果浏览器对javascript没有同源策略的保护,那么一些重要的机密网站将会很危险

  • cors(跨域资源共享)

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能
实现CORS通信的关键是服务器。只要服务器实现了CORS,就可以跨源通信

cors:分两种
    (1) 请求方法是以下三种方法之一:
        HEAD
        GET
        POST
    (2)HTTP的头信息不超出以下几种字段:
        Accept
        Accept-Language
        Content-Language
        Last-Event-ID
        Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
	# 就是简单请求,否则就是非简单请求
    # 简单请求,发送一次请求;非简单请求,发送两次,第一次是options请求,如果允许跨越,再发送真实请求
    
# 后端解决方式:

	1 自己处理跨域问题,需要在中间件里重写response方法
    
    2 使用第三方插件(django-cors-headers)
    
	-安装:pip install django-cors-headers
    -在app中注册
    	INSTALLED_APPS = [
            ...
            \'corsheaders\',
            ...
        ]
    -在中间件里注册
    	MIDDLEWARE = [
            ...,
            \'corsheaders.middleware.CorsMiddleware\',
            ...,
        ]
    -配置文件中配置
        # 允许所有的域
        CORS_ALLOW_ALL_ORIGINS=True
        # 允许的请求头
        CORS_ALLOW_HEADERS = (
            "accept",
            "accept-encoding",
            "authorization",
            "content-type",
            "dnt",
            "origin",
            "user-agent",
            "x-csrftoken",
            "x-requested-with",
            # 额外允许的请求头
        )

        # 允许的请求方式(默认就是这样)
        CORS_ALLOW_METHODS = [
            \'DELETE\',
            \'GET\',
            \'OPTIONS\',
            \'PATCH\',
            \'POST\',
            \'PUT\',
        ]

前台首页设计

Header组件

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

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

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

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

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

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

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

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

    }

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

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

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

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

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

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

    .right-part {
        float: right;
    }

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

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

Footer组件

<template>

    <div class="footer">

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

    </div>

</template>

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

<style scoped>

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

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

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

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

    .footer p {
        text-align: center;
        font-size: 12px;
    }

</style>

Banner组件

<template>
    <div class="banner">
        <el-carousel height="400px">
            <el-carousel-item v-for="item in banner_list" :key="item.id">
                <img :src="item.image" alt="">
            </el-carousel-item>
        </el-carousel>
    </div>
</template>

<script>
    export default {
        name: "Banner",
        data(){
            return {
                banner_list:[]
            }
        },
        mounted() {
            this.$http.get(this.$settings.base_url+\'/api/home/banner/\').then(res=>{
                if(res.data.code==100){
                    this.banner_list=res.data.result
                }

            })
        }
    }
</script>

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

Home组件

<template>
    <div class="home">

        <Header></Header>
        <Banner></Banner>



        <br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
        <Footer/>

    </div>
</template>

<script>
    import Footer from \'../components/Footer\'
    import Header from \'../components/Header\'
    import Banner from \'../components/Banner\'

    export default {
        name: \'Home\',
        components: {
            Footer, Header,Banner
        },
        data() {
            return {}
        },
        mounted() {
        },
        methods: {},

    }
</script>

自定义配置

# 额外创建一个配置文件(这里是banner_settings),后期修改该配置文件中的配置,可以直接在Django的配置文件中导入使用

# 在setting文件夹下新建common_setting

# 写入
BANNER_COUNT=1

# 在dev.py中,写入
from .common_settings import  *

# 在views中用切片获取指定数量的xx
class BannerView(Comment_banner):
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by(\'orders\')[:settings.BANNER_COUNT]
    serializer_class = BannerSerializer

补充

  • 前端存储数据的地方

1 存到cookie中:到了过期时间,失效
2 存到sessionStorage中:在当前页面中有效,关闭浏览器无效
3 存到localStorage中:永久有效,除非我们删除

  • vue-router路由跳转的方式
方式1(html中)

<router-link to="/about"></router-link>

方式2(js中)

this.$router.push(\'/about\')

git

介绍与安装

0 git,github,gitee,gitlab
	-git:版本管理软件
    -github:网站,全球开源代码,放在上面,我们可以看到,并下载--》本质是一个git的远程仓库
    -gitee:中国版的github,公有,私有(中小型公司的代码放在私有仓库中)
    -gitlab:公司内部的(公司内部的github)
    
1 git:软件,实现 版本控制:协同开发(多人合作开发),合并代码

2 下载:https://git-scm.com/download/win
	一路下一步

3 装完后,任意位置,鼠标右键,多出
	git gui here:图形化界面
    git bash here:命令行
    
4 git和svn
	-git的版本管理不需要服务端(远程仓库)
    -svn要进行版本管理,必须有服务端

常用命令

# 分为三个地方
	1 工作区
    2 暂存区
    3 版本库
    
    
# 命令
	git init    # 初始化仓库,在当前路径下生成一个.git文件夹,隐藏的
    git add .   # 把工作区的更该(.表示当前路径下所有更改,都提交),提交到暂存区
    git status  # 查看文件状态(绿色表示,在暂存区,不在版本库,红色表示工作区更改了,没有提交到暂存区)
    git reset . # 把提交到暂存区的,拉回到工作区(以后基本不用)
    
    git commit -m \'注释\'   # 把暂存区的所有内容,提交到版本库
    
    #在提交到版本库之前,要先配置用户信息 (--global:表示全局,在用户家路径.gitconfig)
    git config --global user.email "306334678@qq.com"
  	git config --global user.name "liuqingzheng"
    
    # 局部的用户信息,是在仓库的.git文件夹下的config中
    git config user.email "33@qq.com"
  	git config user.name "egon"
    
    
    git log   # 查看版本提交的日志(谁提交的,提交时间,提交的版本号)
	git reflog # 查看版本提交的日志
    
    git reset --hard 74c071f922  #工作区的内容回退到 74c071f922 版本

    
 # 空文件夹,不会被git管理,python中包会:包有 __init__.py

git过滤文件

在仓库目录下新建.gitignore  文件,在文件中写入要忽略的文件,文件夹,模糊匹配
## 注意:
\'\'\'
文件或文件夹名:代表所有目录下的同名文件或文件夹都被过滤
/文件或文件夹名:代表仓库根目录下的文件或文件夹被过滤

eg:
a.txt:项目中所有a.txt文件和文件夹都会被过滤
/a.txt:项目中只有根目录下a.txt文件和文件夹会被过滤
/b/a.txt:项目中只有根目录下的b文件夹下的a.txt文件和文件夹会被过滤
*x*:名字中有一个x的都会被过滤(*代表0~n个任意字符)
空文件夹不会被提交,空包会被提交,包可以被提交(包中有一个init空文件)

eg:
	.idea
	scripts
	*.pyc
	__pycache__
\'\'\'


pycharm软件
	-绿色:在暂存区,没有提交到版本库
	-红色:在工作区新增,没有提交到暂存区
	-蓝色:在工作区修改的,没有提交到暂存区
	-黄色:忽略的
	-白色:已经被git管理的

git分支操作

#创建分支
git branch 分支名
git checkout -b 分支名   #创建并切换到新分支
#切换分支
git checkout dev
#查看分支
git branch

#删除分支
git branch -d 分支名

#合并分支
git merge 分支名 #把dev分支合并到master分支:切换到master分支,执行合并dev分支的命令

# 分支合并可能会出冲突

git远程仓库

  • GitHub
  • gitee
  • gitlab
1 在远程仓库中新建空仓库

2	git init						# 初始化,让项目被git管理
	git add .						# 将所有项目提交到暂存区
    git commit -m \'注释\'	   		   # 将暂存区的项目提交到本地版本库
    git remote add 名字 远程仓库地址    #  增加一个远程仓库(指定目标远程仓库)
    git push 远程仓库名字 分支名字	  # 把本地版本库内容,提交到远程仓库(需要输入账户密码/用ssh连接)
    
3 远程仓库操作
	增加
    	git remote add 名字 远程仓库地址
    查看
    	git remote
    删除
    	git remote remove origin
    提交到远程仓库
    	git push origin master

采用ssh协议连接远程仓库

1 https方式:用户名+密码认证方式

2 ssh方式:公钥私钥方式
	本地生成公钥私钥
    	ssh-keygen -t rsa -C "1965617308@qq.com"
        生成的公钥私钥在用户家路径的.ssh文件夹下
        id_rsa		# 私钥
        id_rsa.pub	# 公钥
    把\'公钥\'配置到远程仓库上
    以后再提交代码就不用输入用户名密码
    
    ps:重新配置远程仓库
    	git remote add origin git@gitee.com:chi-jintao/luffy_api.git
        git push origin master  # 选yes

项目创始者与项目开发者

  • 开发者
需要把代码clone到本地

	1 git clone https://gitee.com/liuqingzheng/luffy_api.git
    2 修改代码
    3 提交到远程
   	4 git push origin master
  • 创始者
需要远程新建仓库,本地代码push上去

协同开发

1 管理员、开发者(10开发者以上需要充值~)
	能提交代码(增删改查)
    
2 有一个开发者提交了代码

3 另一个开发者,在提交之前,必须先拉取代码(让本地保持最新)	(有事没事pull一下)

4 冲突解决(多人在同一个分支开发,导致冲突)

"""
<<<<<<< HEAD  # 冲突的开始
# 自己的代码
=======  # 分割线
# 别人的代码
>>>>>>> b63c408abd05b87fc492d40523240561999dba50  # 冲突的结束(版本)
"""

	# 解决
    	1 该同样的功能出现的冲突,保留好代码,删掉其中一个即可
        
        2 改不同功能出现冲突,两者都留
        
    # 有事没事pull一下代码

线上分支合并

1 本地和远端分支
	本地建立分支,同步到远端
    	git branch dev
        git push origin dev
    
	远端建立分支,同步到本地
    	远端建立分支
        git pull origin dev
        git branch   		# 看不到
        git checkout dev  	# 切到dev分支,本地就能看到了
        
2 提一个pr:pull request(分支合并的请求)

3 审核通过、测试通过后,合并分支/扁平化合并
	git	变基(git rebase)	---> 扁平化合并(不保留分支的各个提交的小版本)(结果好看,但有冲突比较麻烦)
    合并分支					合并分支(保留分支的各个提交的小版本)(结果乱,有冲突比较好解决)
    
4 远端:dev分支代码和master分支代码完全一致

本地分支合并推送到远程

1 由于dev分支和master分支合并时,有冲突,就不能自动合并了,我们需要本地合并完,解决冲突,再提交

2 把dev合并到master
	git checkout master
    git merge dev
    有冲突,解决冲突
    再提交
    
3 本地的分支提交到远端分支(如果名字不一样)
	git push origin dev111:dev222  # 不要作死
        
ps:
	1 git操作出没出冲突,如何解决
    	-第一:多人在同一分支开发
        -第二:分支合并出冲突
        
    2 git 变基
    3 git fetch 和 git pull的区别
    4 git add . 与git add xxx

前端登录、注册组件

模态框登录组件

<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">登录</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);
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

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

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

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

模态框注册组件

<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">注册</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;
                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);
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

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

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

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

导航条

<template>
    <div class="nav">
        <span @click="put_login">登录</span>
        <span @click="put_register">注册</span>
        <Login v-if="is_login" @close="close_login" @go="put_register" />
        <Register v-if="is_register" @close="close_register" @go="put_login" />
    </div>
</template>

<script>
    import Login from "./Login";
    import Register from "./Register";
    export default {
        name: "Nav",
        data() {
            return {
                is_login: false,
                is_register: false,
            }
        },
        methods: {
            put_login() {
                this.is_login = true;
                this.is_register = false;
            },
            put_register() {
                this.is_login = false;
                this.is_register = true;
            },
            close_login() {
                this.is_login = false;
            },
            close_register() {
                this.is_register = false;
            }
        },
        components: {
            Login,
            Register
        }
    }
</script>

<style scoped>

</style>

多方式登录接口

序列化类

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

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

    def validate(self, attrs):
        user = self._get_user(attrs)
        token = self._get_token(user)
        self.context[\'token\'] = token
        return attrs

    def _get_user(self, attrs):
        username = attrs.get(\'username\')
        password = attrs.get(\'password\')
        if re.match(\'^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$\', username):
            user = User.objects.filter(mobile=username).first()
        elif re.match(\'^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$\', username):
            user = User.objects.filter(email=username).first()
        else:
            user = User.objects.filter(username=username).first()
        if user:
            if user.check_password(password):
                return user
            else:
                raise ValidationError({\'detail\':\'密码错误\'})
        else:
            raise ValidationError({\'detail\':\'用户不存在\'})

    def _get_token(self, user):
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

视图类

class Userinfo(ViewSet):
    @action(methods=[\'POST\'], detail=False)
    def login(self, request, *args, **kwargs):
        ser = UserModelSerializer(data=request.data)
        ser.is_valid(raise_exception=True)
        token = ser.context.get(\'token\')
        return APIResponse(token=token, data=ser.data)

校验手机号是否存在接口

 @action(methods=[\'GET\'], detail=False)
    def check_mobile(self,request):
        mobile=request.query_params.get(\'mobile\',None)
        if mobile:
            user=User.objects.filter(mobile=mobile).first()

            if user:
                return APIResponse(exisit=True)
            else:
                return APIResponse(exisit=False)

多方式登录功能前端和注销功能

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_pwd">登录</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_pwd() {
                if (this.username && this.password) {

                    this.$http.post(this.$settings.base_url + \'/api/user/login/\', {
                        username: this.username,
                        password: this.password
                    }).then(res => {
                        if (res.data.code == 100) {
                            // 把用户名和token放到cookie中(s:秒,d:天,m:月)
                            this.$cookie.set(\'token\', res.data.token, \'7d\')
                            this.$cookie.set(\'username\', res.data.username, \'7d\')
                            //关闭登录框
                            this.$emit(\'close\')

                        } else {
                            this.$message({
                                message: res.data.msg[0],
                                type: \'error\'
                            })
                        }
                    })

                } else {
                    this.$message({
                        message: \'用户名或密码没有填\',
                        type: \'warning\'
                    })
                }
            },
        }


    }
</script>


<style scoped>

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

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

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

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

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

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

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

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

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

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

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

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

</style>

Header.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(\'/course\')" :class="{active: url_path === \'/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-if="!username">
                    <span @click="put_login">登录</span>
                    <span class="line">|</span>
                    <span @click="put_register">注册</span>
                </div>
                <div v-else>
                    <span>{{username}}</span>
                    <span class="line">|</span>
                    <span @click="logout">注销</span>
                </div>
            </div>
        </div>
        <Login v-if="is_login" @close="close_login" @go="put_register"/>
        <Register v-if="is_register" @close="close_register" @go="put_login"/>
    </div>
</template>

<script>
    import Login from "./Login";
    import Register from "./Register";

    export default {
        name: "Header",
        data() {
            return {
                username:\'\',
                url_path: sessionStorage.url_path || \'/\',
                is_login: false,
                is_register: false,

            }
        },
        methods: {

            goPage(url_path) {
                // 已经是当前路由就没有必要重新跳转
                if (this.url_path !== url_path) {
                    this.$router.push(url_path);  // 路由跳转
                }
                sessionStorage.url_path = url_path;
            },
            put_login() {
                this.is_login = true;
                this.is_register = false;
            },
            put_register() {
                this.is_login = false;
                this.is_register = true;
            },
            close_login() {
                this.is_login = false;
                // 只要关闭登录框,去cookie中取username,如果在,就放到头部,不在,就不做处理
                this.username = this.$cookie.get(\'username\')

            },
            close_register() {
                this.is_register = false;
            },
            logout(){
                // 退出登录不需要跟后端交互,本地的cookie删除即可
                this.$cookie.remove(\'username\')
                this.$cookie.remove(\'token\')
                //username也要值为空
                this.username=\'\'
            }
        },
        created() {

            // localStorage.name=\'lqz\'
            // sessionStorage.name=\'lqz\'
            // this.$cookie.set(\'name\',\'lqz\',3)
            //
            // this.$router   全局vue-router的对象
            // this.$route    当次请求地址的对象,有请求地址,有携带数据
            // console.log(this.$router)
            // console.log(this.$route)
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;
            this.username=this.$cookie.get(\'username\')
        },
        components: {
            Register,
            Login
        }
    }
</script>

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

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

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

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

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

    }

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

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

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

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

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

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

    .right-part {
        float: right;
    }

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

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

腾讯云短信功能使用、发送短信接口

API接口和SDK

1 api接口,第三方提供的一个个的http的接口:腾讯会提供api接口
2 sdk,第三方基于api接口,封装的代码,发送短信,腾讯云也提供了sdk (如果有sdk,优先用sdk)
	-腾讯发送短信,python提供了两个版本的sdk
    	-3.x:不仅仅包含发送短信,还包含其他云服务业务
        -2.x:只有发送短信业务(简洁)      

短信功能封装

# 1 在libs目录下,新建sms包
    sms #包名
        sms_main.py # 核心逻辑
        settings.py # 配置
        __init__.py 
        
####### __init__.py
from .sms_main import get_code,send_sms



####### settings.py
APPID =   # SDK AppID 以1400开头
# 短信应用 SDK AppKey
APPKEY = ""

# 短信模板ID,需要在短信控制台中申请
TEMPLATE_ID =   # NOTE: 这里的模板 ID`7839` 只是示例,真实的模板 ID 需要在短信控制台中申请
# 签名
SMS_SIGN = ""



#####sms_main.py
from qcloudsms_py import SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
from . import settings
import random
from luffy_api.utils.logging import get_logger
logger=get_logger()
# 生成code函数,四位数字
def get_code():
    s=\'\'
    for i in range(4):
        num=random.randrange(0,9)
        s+=str(num)
    return s
def send_sms(mobile, code):
    ssender = SmsSingleSender(settings.APPID, settings.APPKEY)
    params = [code, ]  # 当模板没有参数时,`params = []`
    try:
        result = ssender.send_with_param(86, mobile,
                                         settings.TEMPLATE_ID, params, sign=settings.SMS_SIGN, extend="", ext="")
        if result.get(\'result\')==0:
            return True
        else:
            # 发送失败,记录日志
            logger.error(\'手机号为:%s,发送短信失败\'%mobile)
            return False
    except Exception as e:
        logger.error(\'发送短信出异常,异常手机号为:%s\' % mobile)
        return False

发送短信接口

 @action(methods=[\'GET\'], detail=False)
    def send_sms(self, request):
        mobile = request.query_params.get(\'mobile\', None)
        if mobile:
            # 手机号存在,发送短信
            code = sms.get_code()
            print(code)
            # 发送短信(同步发送:等着腾讯发送成功或失败的返回,可能等很长时间)
            #异步发送短信(不用等腾讯是否成功,直接给用户返回)
            res = sms.send_sms(mobile, code)
            if res:
                # 发送成功后,短信验证码,放到缓存中(默认放内存中,实际项目要放到redis中)
                cache.set(settings.SMS_CACHE%mobile,code,60*10)

                # # 取出验证码
                # code=cache.get(settings.SMS_CACHE%mobile)
                return APIResponse(msg=\'短信发送成功\')
            else:
                raise APIException({\'detail\': \'短信发送失败\'})
        else:
            # return APIResponse(code=101,msg=\'手机号没有传\')
            raise APIException({\'detail\': \'手机号没有传\'})
            # raise Exception(\'手机号不存在\')

公共配置文件(common_setting.py)

SMS_CACHE = \'sms_cache_%s\'

短信登录接口

视图类

    @action(methods=[\'POST\'], detail=False)
    def mobile_login(self, request):
        ser=UserMobileModelSerializer(data=request.data)
        ser.is_valid(raise_exception=True)
        token = ser.context.get(\'token\')
        username = ser.context.get(\'username\')
        icon = ser.context.get(\'icon\')
        return APIResponse(token=token, username=username, icon=icon)

序列化类

class UserMobileModelSerializer(serializers.ModelSerializer):
    mobile=serializers.CharField()
    # code不是User表的字段,所以要重写
    code=serializers.CharField()
    class Meta:
        model=User
        fields=[\'mobile\',\'code\']


    def validate(self, attrs):

        # 校验code是否正确
        self._check_code(attrs)

        # 获取用户
        user=self._get_user(attrs)
        # 签发token
        token=self._get_token(user)

        self.context[\'token\']=token
        self.context[\'username\']=user.username
        self.context[\'icon\']=str(user.icon)

        return attrs


    def _check_code(self,attrs):
        code=attrs.get(\'code\')
        mobile=attrs.get(\'mobile\')
        #获取放在缓存中的code
        old_code=cache.get(settings.SMS_CACHE%mobile)
        # 删除缓存中的code
        if not(old_code and code==old_code):
            raise ValidationError({\'detail\':\'验证码校验失败\'})


    def _get_user(self,attrs):
        mobile = attrs.get(\'mobile\')
        user = User.objects.filter(mobile=mobile).first()
        if not user:
            raise ValidationError({\'detail\':\'用户不存在\'})
        return user



    def _get_token(self,user):
        from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)
        return token

短信注册接口

  • 手机号
  • 验证码
  • 密码

序列化类

class RegisterModelSerializer(serializers.ModelSerializer):
    # code = serializers.CharField(max_length=4, min_length=4,write_only=True)
    code = serializers.CharField(max_length=4, min_length=4)

    class Meta:
        model = User
        fields = [\'mobile\', \'code\', \'password\']
        extra_kwargs = {
            \'password\': {\'max_length\': 16, \'min_length\': 3},
        }

    # def validate_mobile(self, mobile):
    #     # 手机号是否在库中了
    #     import re
    #     return mobile

    def validate(self, attrs):
        # 验证码是否正确
        self._check_code(attrs)
        # 直接返回attrs
        # code 剔除
        attrs.pop(\'code\')
        return attrs

    def _check_code(self, attrs):
        code = attrs.get(\'code\')
        mobile = attrs.get(\'mobile\')

        # 有可能有的公司,测试阶段,有万能验证码
        # 获取放在缓存中的code
        old_code = cache.get(settings.SMS_CACHE % mobile)
        # 删除缓存中的code
        if not (old_code and code == old_code):
            raise ValidationError({\'detail\': \'验证码校验失败\'})

    def create(self, validated_data):


        # username创建出来,使用手机号作为用户名
        validated_data[\'username\'] = validated_data.get(\'mobile\')
        # 使用User表的create_user方法新增用户(密码是密文的)
        user = User.objects.create_user(**validated_data)
        return user

视图类

class UserRegisterView(GenericViewSet, CreateModelMixin):
    queryset = User.objects.all()
    serializer_class = RegisterModelSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        return APIResponse(msg=\'注册成功\')

Redis

简介

Redis是一个key - value存储系统,非关系型数据库(nosql数据库),缓存数据库,数据存在于内存中

Redis支持五大数据类型

  • 字符串
  • 列表
  • hash(字典)
  • 集合
  • 有序集合

使用Redis的好处

  • 速度快:缓存数据库
  • 支持丰富数据类型(五大数据类型)
  • 支持事务(通过管道支持)(单实例上有效)
  • 丰富的特性:可用于缓存、消息队列、按key设置过期时间,过期后自动删除

Redis对比memcached

  • 都是内存数据库
  • Redis类型丰富(五大数据类型),memcached只支持字符串类型
  • Redis支持持久化,memcached不支持持久化
  • Redis速度比memcached块

Redis特点

  • 可以持久化
  • 单线程,单进程
    • Redis最新版本是6.x
    • 6.x版本之前是单线程,单进程

Redis单线程为什么这么快(官方:10w左右并发,实际:6w左右并发)

  • 纯内存操作
  • 使用了io多路复用模型(epoll、select)
  • 因为是单线程,不存在线程间切换,节省资源

服务:客户端,服务端(跟MySQL一样,是存数据的地方)

安装

启动Redis服务

  • 在任意路径下敲(前提:已经添加环境变量):redis-server
  • Windows平台已经做成服务,直接开启服务

客户端连接

  • 任意路径下敲:redis-cli -h 127.0.0.1 -p 6379
  • 使用客户端连接
  • 使用python操作(pip install redis)

普通连接与连接池

普通连接

# redis-py提供两个类Redis和StrictRedis用于实现Redis的命令,StrictRedis用于实现大部分官方的命令,并使用官方的语法和命令,Redis是StrictRedis的子类,用于向后兼容旧版本的redis-py

import redis

conn = redis.Redis(host=\'127.0.0.1\', port=6379)
conn.set(\'foo\', \'Bar\')
print(r.get(\'foo\'))

连接池

# redis-py使用connection pool来管理对一个redis server的所有连接,避免每次建立、释放连接的开销

# 默认:每个Redis实例都会维护一个自己的连接池

# 可以直接建立一个连接池,然后作为参数Redis,这样就可以实现多个Redis实例共享一个连接池

import redis

pool = redis.ConnectionPool(host=\'127.0.0.1\', port=6379)
conn = redis.Redis(connection_pool=pool)
conn.set(\'foo\', \'Bar\')
print(r.get(\'foo\'))

把POOL以单例形式使用

# pool.py

import redis

# 拿到一个连接数为3的连接池
# 把POOL做成单例:把它以模块导入方式
POOL=redis.ConnectionPool(max_connections=3,host=\'127.0.0.1\', port=6380)

========================================================================================

import redis

from pool import POOL  # 以模块形式导入,是天然单例
conn=redis.Redis(connection_pool=POOL)
# conn2=redis.Redis(connection_pool=POOL)
# conn3=redis.Redis(connection_pool=POOL)
# conn4=redis.Redis(connection_pool=POOL)
# conn5=redis.Redis(connection_pool=POOL)
# conn6=redis.Redis(connection_pool=POOL)

conn.set(\'age\',19)
conn.close()  # 不是关闭连接,把该连接,放回到连接池

Redis五大数据类型之字符串

字符串操作,Redis中的字符串再内存中按照一个name对应一个value来存储

img

\'\'\'
set(name, value, ex=None, px=None, nx=False, xx=False) 重点
setnx(name, value)
setex(name, value, time)
psetex(name, time_ms, value)
mset(*args, **kwargs) 重点
get(name) 重点
mget(keys, *args) 重点
getset(name, value)
getrange(key, start, end)
setrange(name, offset, value)
setbit(name, offset, value)
getbit(name, offset)
bitcount(key, start=None, end=None)

strlen(name) 重点
incr(self, name, amount=1) 重点
incrbyfloat(self, name, amount=1.0)
decr(self, name, amount=1)
append(key, value)
\'\'\'


import redis

conn=redis.Redis()

# set:放字符串值
\'\'\'
ex,过期时间(秒)
px,过期时间(毫秒)
nx,如果设置为True,则只有name不存在时,当前set操作才执行,值存在,就修改不了,执行没效果
xx,如果设置为True,则只有name存在时,当前set操作才执行,值存在才能修改,值不存在,不会设置新值
\'\'\'
# conn.set(\'name\',\'lqz\')
# res=conn.set(\'name\',\'lqz\',ex=3)
# res=conn.set(\'name\',\'lqz\',px=3000)
# res=conn.set(\'name\',\'lqz\')
# res=conn.set(\'name\',\'egon\')
# res=conn.set(\'name\',\'lqz\',nx=True)
# res=conn.set(\'name\',\'19\',xx=True)


# get 获取值
# res=conn.get(\'name\')

# mset 批量设置值

# conn.mset({\'name\':\'lqz\',\'age\':19,\'sex\':\'男\'})

# mget  批量获取值
# res=conn.mget([\'name\',\'age\'])
# res=conn.mget(\'name\',\'age\')

# conn.setex(name, value, time)
# conn.psetex(name, time_ms, value)
# conn.set(name, valeu, px=time)

# res=conn.getset(\'name\', \'刘亦菲\')


# res=conn.getrange(\'name\',3,5)  #前闭后闭区间  拿到 亦 这个字   拿字节
#
# print(str(res,encoding=\'utf-8\'))


# res=conn.setrange(\'name\',0,\'lqzzzzzzz\')
# res=conn.setrange(\'name\',0,\'qq\')


##获取和设置比特位
# setbit(name, offset, value)
# getbit(name, offset)


# print(conn.getbit(\'name\',3))
# conn.setbit(\'name\',0,1)
# print(conn.getbit(\'name\',0))

# print(conn.strlen(\'name\'))  # 指的是字节     获取字节长度

# conn.incr(\'age\')   # 让该键自增1
# conn.incrby(\'age\',3)   # 让该键自增3
# conn.incrbyfloat(\'age\', amount=1.1)


conn.append(\'name\', \'nb\')

conn.close()

Redis五大数据类型之hash

img

\'\'\'
hset(name, key, value)   重点
hmset(name, mapping)   重点
hget(name,key)        重点
hmget(name, keys, *args)  重点
hgetall(name)
hlen(name)              重点
hkeys(name)
hvals(name)
hexists(name, key)
hdel(name,*keys)
hincrby(name, key, amount=1)   重点
hincrbyfloat(name, key, amount=1.0)
hscan(name, cursor=0, match=None, count=None)
hscan_iter(name, match=None, count=None)
\'\'\'

import redis

conn=redis.Redis()

# conn.hset(\'users\',\'name\',\'egon\')
# conn.hset(\'users\',\'age\',\'19\')

# res=conn.hget(\'users\',\'age\')
# res=conn.hget(\'users\',\'name\')



# conn.hmset(\'h1\',{\'name\':\'lqz\',\'age\':19})


# res=conn.hmget(\'h1\',[\'name\',\'age\'])
# res=conn.hmget(\'h1\',\'name\',\'age\')


# res=conn.hlen(\'users\')  # 统计键值对的个数



# res=conn.hkeys(\'users\')
# res=conn.hvals(\'users\')
# hvals(name)
# print(str(res[2],encoding=\'utf-8\'))


# res=conn.hexists(\'users\',\'age1\')
# res=conn.hdel(\'users\',\'age\')



# conn.hincrby(\'h1\', \'age\',7)
# conn.hincrbyfloat(\'h1\', \'age\', amount=-1.01)



# for i in range(1000):
#     conn.hset(\'htest\',i,\'第%s个鸡蛋\'%i)

###hash(字典)类型是无序的

# res=conn.hgetall(\'htest\')
# print(res)



# res=conn.hscan(\'htest\', cursor=0,count=500)  # 用的少
# print(res)
# res=conn.hscan(\'htest\', cursor=254,count=499)  # 用的少
# print(res)
# res=conn.hscan(\'htest\', cursor=511,count=2)  # 用的少
# print(res)
# print(res)
# print(len(res[1]))


res=conn.hscan_iter(\'htest\', count=20)   # 把hash类型的所有数据都打印出来,比hgetall节省内存
for item in res:
    print(str(item[1],encoding=\'utf-8\'))

# print(res)



conn.close()

Redis五大数据类型之列表

img

\'\'\'

lpush(name,values)   #从左侧插入值
lpushx(name,value)
llen(name)           # 重点
linsert(name, where, refvalue, value))  # 重点
lset(name, index, value)
lrem(name, value, num)        # 删除值
lpop(name)             #从左侧弹出
lindex(name, index)
lrange(name, start, end)
ltrim(name, start, end)
rpoplpush(src, dst)
blpop(keys, timeout)          # 阻塞式弹出
brpoplpush(src, dst, timeout=0)
\'\'\'

import redis

conn=redis.Redis()

# conn.lpush(\'list1\',\'1\')  #左侧插入,上部
# conn.lpush(\'list1\',\'lqz\')
# conn.rpush(\'list1\',\'egon\')  #右侧插入,下部

# conn.rpushx(\'list1\',\'刘亦菲\')  # 只有name存在才能插入
# conn.lpushx(\'list2\',\'刘亦菲\')


# print(conn.llen(\'list1\'))


# conn.linsert(\'list1\',\'before\',\'egon\',\'迪丽热巴\')

# conn.linsert(\'list1\',\'AFTER\',\'egon\',\'古力娜扎\')
# conn.linsert(\'list1\',\'AFTER\',\'1\',\'99999\')


# conn.lset(\'list1\',3,66666) # 修改某个位置的值



# 如果count=0,表示删除所有符合条件的,删除所有66666
# conn.lrem(\'list1\',0,\'66666\')

# 如果count=1,表示从左侧删除1个符合条件的,删除1个99999
# conn.lrem(\'list1\',1,\'99999\')
# 如果count=-2,表示从底部,从右侧,删除符合条件的2个
# conn.lrem(\'list1\',-2,\'99999\')



# res=conn.lpop(\'list1\')
# res=conn.rpop(\'list1\')


# res=conn.lindex(\'list1\',1)  #返回某个索引的值  列表操作 l[0]


# res=conn.lrange(\'list1\', 0, 4)  # 前闭后闭


# res=conn.ltrim(\'list1\', 1, 3)  #修剪,不符合范围的都删除,前闭后闭
# conn.lpush(\'list2\',\'李易峰\',\'刘亦菲\',\'迪丽热巴\')

# conn.rpoplpush(\'list1\', \'list2\')

res=conn.blpop(\'list2\',timeout=2)          # 阻塞式弹出,通过它做一些分布式任务,可以用来做消息队列
print(res)


# brpoplpush(src, dst, timeout=0)




conn.close()

其他通用操作

\'\'\'

delete(*names)
# 根据删除redis中的任意数据类型

exists(name)
# 检测redis的name是否存在

keys(pattern=\'*\')
# 根据模型获取redis的name

expire(name ,time)
# 为某个redis的某个name设置超时时间

rename(src, dst)
# 对redis的name重命名为

move(name, db))
# 将redis的某个值移动到指定的db下

randomkey()
# 随机获取一个redis的name(不删除)

type(name)
#查看类型
\'\'\'



##通用操作

import redis

conn=redis.Redis()
# conn.delete(\'h1\')


# print(conn.exists(\'name\'))


# res=conn.keys(\'list*\')
# print(res)

# conn.expire(\'list1\',3)

# conn.rename(\'sex\',\'sex111\')

# conn.move(\'sex111\',2)


# res=conn.randomkey()

# res=conn.type(\'users\')
res=conn.type(\'name\')

print(res)
conn.close()

管道

# redis支持事务,借助于管道


# import redis
#
#
# conn=redis.Redis()
#
# conn.decr(\'zhangsan\',100)
#
# raise Exception(\'程序废了,出异常了\')
#
# conn.incrby(\'wo\',100)
#
# conn.close()


# 借助于管道实现

import redis
conn=redis.Redis()
pip_line=conn.pipeline(transaction=True)  #开启一个管道
pip_line.decr(\'zhangsan\',100)            #往管道中放一个命令
raise Exception(\'程序崩了\')
pip_line.incrby(\'wo\',100)                #往管道中放一个命令

pip_line.execute()                       # 把管道中的命令一次性执行

conn.close()

Django使用Redis

# 第一种:通用方式(任何web框架都可以)
	新建一个文件,其他文件导入使用

# 第二种:django提供的
	-在配置文件中配置
    	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}  # 连接池大小为100
                    # "PASSWORD": "123",
                }
            }
        }
    -安装django_redis模块
    -使用
    from django_redis import get_redis_connection
    def index(request):
        conn=get_redis_connection()
        res=conn.get(\'name\')
        print(res)
        return HttpResponse(\'okkkkk\')
    
   -一旦这样配置,django中的缓存,也用redis
	cache.set(\'key\',python的任意对象)

Celery

介绍与架构

  • 介绍
# celery:分布式的异步任务框架

# celery能做什么:
	1 异步任务
    	1.1 解决耗时任务,将耗时任务提交给celery去异步执行(比如发送短信、邮件、消息推送、音视频处理等等...)
        
    2 延迟任务
    	2.1 解决需要延迟执行的任务
    
    3 定时任务
    	3.1 解决周期任务(比如每日数据统计)
        
\'\'\'
总结:
	1 可以不依赖任何服务器,通过自身命令,启动服务
	2 celery服务是为其他项目服务提供异步解决任务需求的
		(会有两个服务同时运行,一个是项目服务,一个是celery服务,项目服务将需要异步处理的任务交给celery服务,celery就会在需要的时候通过异步完成项目的需求)
\'\'\'

举栗:
	人是一个独立运行的服务 | 医院也是一个独立运行的服务
	正常情况下,人可以完成所有健康情况的动作,不需要医院的参与;但当人生病时,就会被医院接收,解决人生病问题
	人生病的处理方案交给医院来解决,所有人不生病时,医院独立运行,人生病时,医院就来解决人生病的需求
    
# ps:
	celery官方不支持在Windows平台上使用(也可以使用)
    	1 使用Mac
        2 乌班图
        3 远程连接Linux
  • 架构
# 消息中间件(broker)
celery本身不提供消息服务,但是可以使用第三方提供的消息中间件集成(Redis...)

# 任务执行单元(worker)
worker是celery提供的任务执行的单元,worker并发的运行在分布式的系统节点中(本质:一个work就是一个进程)

# 任务结果存储(backend)
Task result store用来存储worker执行的任务的结果,celery支持以不同方式存储任务的结果(Redis...)

img

基本使用

# 安装:pip install celery

# win上启动work
# 4.x版本及之前
pip install eventlet
celery worker -A celery_task -l info -P eventlet
# 5.x版本
pip install eventlet
celery -A celery_task worker -l info -P eventlet

# linux中启动work
celery worker -A celery_task -l info
celery -A celery_task worker -l info


# 启动beat的命令(负责每隔几秒钟,向任务队列中提交任务)
celery beat -A celery_task -l info

普通使用

新建celery_task.py(任意名字)

from celery import Celery
backend=\'redis://127.0.0.1:6379/1\'  # 结果存储
broker=\'redis://127.0.0.1:6379/2\'   # 消息中间件
app=Celery(\'test\',broker=broker,backend=backend) # 传一个字符串,相当于名字


@app.task
def add(a,b):  # 很耗时的任务
    import time
    time.sleep(3)
    return a+b

新建add_task.py添加任务

from celery_task import add
# res=add(1,2)  # 同步调用
res=add.delay(3,4)  # 把任务提交到redis,返回要给uuid:b20f827d-dcf5-4fdf-a646-5abb963dc1d3
print(res)

新建get_result.py查询任务

from celery_task import app  # 自己写的app
from celery.result import AsyncResult  # celery模块下的
id = \'b20f827d-dcf5-4fdf-a646-5abb963dc1d3\'
if __name__ == \'__main__\':
    a = AsyncResult(id=id, app=app)
    if a.successful():
        result = a.get()  # task中return的数据:7
        print(result)
    elif a.failed():
        print(\'任务失败\')
    elif a.status == \'PENDING\':
        print(\'任务等待中被执行\')
    elif a.status == \'RETRY\':
        print(\'任务异常后正在重试\')
    elif a.status == \'STARTED\':
        print(\'任务已经开始被执行\')

启动worker

# win
pip install eventlet
celery -A celery_task worker -l info -P eventlet
# linux中启动work
celery -A celery_task worker -l info

包结构(推荐)

celery_task   # 包名
	__init__.py  # 任务存放路径
    user_task.py  # 任务存放路径
    order_task.py  # 任务存放路径
    home_task.py  # 任务存放路径
    celery.py  # app所在文件,必须叫celery
    
    
project
    ├── celery_task  	# celery包
    │   ├── __init__.py # 包文件
    │   ├── celery.py   # celery连接和配置相关文件,且名字必须叫celery.py
    │   └── tasks.py    # 所有任务函数
    ├── add_task.py  	# 添加任务
    └── get_result.py   # 获取结果

celery.py

### 有app的这个py文件,必须叫celery

from celery import Celery
backend = \'redis://127.0.0.1:6379/1\'  # 结果存储
broker = \'redis://127.0.0.1:6379/2\'  # 消息中间件
app = Celery(\'test\', broker=broker, backend=backend, include=[
    \'celery_task.home_task\',
    \'celery_task.order_task\',
    \'celery_task.user_task\'
])

# 定制定时任务
# 时区(修改时区)
print(app.conf)
app.conf.timezone = \'Asia/Shanghai\'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    \'send_sms_5\': {
        \'task\': \'celery_task.user_task.send_sms\',  # 要执行的任务
        \'schedule\': timedelta(seconds=5),
        # \'schedule\': crontab(hour=8, day_of_week=1),  # 每周一早八点
        \'args\': (189533333,),
    },
    \'add_3\': {
        \'task\': \'celery_task.home_task.add\',  # 要执行的任务
        \'schedule\': timedelta(seconds=3),
        # \'schedule\': crontab(hour=8, day_of_week=1),  # 每周一早八点
        \'args\': (6,8),
    }
}

home_tesk.py

from .celery import app
@app.task  # home模块的任务
def add(a,b):
    return a+b

order_task.py

from .celery import app
@app.task  # order模块的任务,写文件任务
def write_file(s):
    with open(r\'D:\lqz2\luffy_api\log\a.txt\',\'w\',encoding=\'utf-8\') as f:
        f.write(s)
    return True

user_task.py

from .celery import app
@app.task  # user模块的任务,发送带内心
def send_sms(s):
    import time
    time.sleep(3)
    return \'%s手机号,短信发送成功\'%s

celery执行异步任务,延迟任务,定时任务(5星)

异步任务

# delay

from celery_task.user_task import send_sms
res=send_sms.delay(\'18953675221\')
print(res)

延迟任务

# 延迟任务,延迟5s钟后,发送短信

from celery_task.user_task import send_sms
from datetime import datetime, timedelta
# print(datetime.utcnow()+timedelta(seconds=10))  # 打印出当前utc时间
eta=datetime.utcnow() + timedelta(seconds=10)  # 当前utc时间,往后推10s,时间对象
# # args是列表,send_sms的参数,eta是延迟时间,时间对象
res=send_sms.apply_async(args=[\'1888888\',],eta=eta)
print(res)

定时任务

# 写在celery.py中

# 定制定时任务
# 时区(修改时区)
print(app.conf)
app.conf.timezone = \'Asia/Shanghai\'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    \'send_sms_5\': {
        \'task\': \'celery_task.user_task.send_sms\',  # 要执行的任务
        \'schedule\': timedelta(seconds=5),
        # \'schedule\': crontab(hour=8, day_of_week=1),  # 每周一早八点
        \'args\': (189533333,),
    },
    \'add_3\': {
        \'task\': \'celery_task.home_task.add\',  # 要执行的任务
        \'schedule\': timedelta(seconds=3),
        # \'schedule\': crontab(hour=8, day_of_week=1),  # 每周一早八点
        \'args\': (6,8),
    }
}
# 启动beat
celery -A celery_task  beat -l info

Django使用celery

  • django-celery模块(年久失修)
  • 自己集成(和框架无关)

celery.py

"""
celery框架django项目工作流程
1)加载django配置环境
2)创建Celery框架对象app,配置broker和backend,得到的app就是worker
3)给worker对应的app添加可处理的任务函数,用include配置给worker的app
4)完成提供的任务的定时配置app.conf.beat_schedule
5)启动celery服务,运行worker,执行任务
6)启动beat服务,运行beat,添加任务

重点:由于采用了django的反射机制,使用celery.py所在的celery_task包必须放置项目的根目录下
"""

# 一、加载django配置环境
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffy_api.settings.dev")

# 二、加载celery配置环境
from celery import Celery
# broker
broker = \'redis://127.0.0.1:6379/0\'
# backend
backend = \'redis://127.0.0.1:6379/1\'
# worker
app = Celery(broker=broker, backend=backend, include=[\'celery_task.tasks\'])


# 时区
app.conf.timezone = \'Asia/Shanghai\'
# 是否使用UTC
app.conf.enable_utc = False

# 任务的定时配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    \'update-banner-list\': {
        \'task\': \'celery_task.tasks.update_banner_list\',
        \'schedule\': timedelta(seconds=10),
        \'args\': (),
    }
}

tasks.py

from .celery import app

from django.core.cache import cache
from home import models, serializers
from django.conf import settings
@app.task
def update_banner_list():
    queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by(\'-orders\')[:settings.BANNER_COUNT]
    banner_list = serializers.BannerSerializer(queryset, many=True).data
    # 拿不到request对象,所以头像的连接base_url要自己组装
    for banner in banner_list:
        banner[\'image\'] = \'http://127.0.0.1:8000%s\' % banner[\'image\']

    cache.set(\'banner_list\', banner_list, 86400)
    return True

异步发送短信

 @action(methods=[\'GET\'], detail=False, throttle_classes=[Mobile_throttle])
    def send_sms(self, request, *args, **kwargs):
        mobile = request.query_params.get(\'mobile\', None)
        if mobile:
            # 手机号存在,发送短信
            code = sms.get_code()
            print(code)
            # 发送短信(同步发送:等着腾讯发送成功或失败的返回,可能等很长时间)
            # 异步发送短信(不用等腾讯是否成功,直接给用户返回)
            from celery_task.user_task import send_sms_6
            send_sms_6.delay(mobile, code)
            cache.set(settings.SMS_CACHE % mobile, code, 600)
            return APIResponse(msg=\'短信已经发送\')
        else:
            raise APIException({\'detail\': \'手机号没有传\'})

轮播图接口加入缓存

1 如果mysql数据变化了,轮播图表数据变化了,由于缓存没有更新,就会出现问题

2 双写一致性问题(4星)
	-因为使用了缓存,mysql和redis的数据不一致了
    -如何解决:(缓存更新策略)
    	-加入过期时间?  60s? 过期时间内数据还有问题
        -定时更新缓存(每隔60s,更新一下缓存)
        
        -数据库一更改,立马改缓存
        	-先改数据库,再改缓存
            -先改缓存,再改数据库
        -数据库一改,删除缓存
        	-先删缓存,再改数据库
            -先改数据库,再删缓存(稍微好一些)
            
3 首页轮播图接口:缓存更新策略是定时更新
	-celery的定时任务
    -使用celery的定时任务,完成首页轮播图缓存的更新

课程功能表分析

模型表部分

from django.db import models

# Create your models here.
from luffy_api.utils.model import BaseModel

#### 课程分类
class CourseCategory(BaseModel):
    """分类"""
    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="课程状态")
    students = models.IntegerField(verbose_name="学习人数", default=0) # 正常来讲,没有这个字段,应该跟User表关联
    sections = models.IntegerField(verbose_name="总课时数量", default=0)
    pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
    price = models.DecimalField(max_digits=6, decimal_places=2, 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="课程名称")
    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="课程章节")
    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)

数据部分

-- 老师表

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

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

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


-- 分类表

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

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

-- 课程表

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

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

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

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

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

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

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

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

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

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

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

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

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

查询所有课程分类接口

  • 视图类
class CourseCategoryView(GenericViewSet,ListModelMixin):
    queryset = CourseCategory.objects.all().filter(is_delete=False,is_show=True).order_by(\'orders\')
    serializer_class = CourseCategorySerializer
  • 序列化类
class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model=CourseCategory
        fields=[\'id\',\'name\']

查询所有课程接口(排序,过滤)

  • 视图类
class CourseView(GenericViewSet,ListModelMixin):
    queryset = Course.objects.all().filter(is_delete=False,is_show=True).order_by(\'orders\')
    serializer_class = CourseSerializer
    # 排序
    filter_backends = [OrderingFilter,DjangoFilterBackend]
    ordering_fields=[\'students\',\'price\']

    # 过滤(使用第三方)
    # 内置过滤:search=\'1\'
    # 第三方:course_category=2
    filter_fields=[\'course_category\']

    # 分页
    pagination_class = CommonPageNumberPagination
  • 序列化类
class TeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model=Teacher
        fields = (\'name\', \'role_name\', \'title\', \'signature\', \'image\', \'brief\')
class CourseSerializer(serializers.ModelSerializer):
    class Meta:
        model=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\',]     # 课时显示,超过4个,只显示4个,不超过都显示
    ## 第二种:
    # course_type_name=serializers.CharField(source=\'get_course_type_display\')

    ## 第三种
    # course_type_name=serializers.SerializerMethodField()
    # def get_course_type_name(self,obj):
    #     return obj.get_course_type_display()


    ###序列化老师的第二种方式
    # teacher=serializers.SerializerMethodField()
    # def get_teacher(self,obj):
    #     return {\'name\':obj.teacher.name}


    ### 序列化老师的第三种方式:子序列化
    teacher=TeacherSerializer()
  • 表模型
####实战课表
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="课程状态")
    students = models.IntegerField(verbose_name="学习人数", default=0)  # 正常来讲,没有这个字段,应该跟User表关联
    sections = models.IntegerField(verbose_name="总课时数量", default=0)
    pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
    price = models.DecimalField(max_digits=6, decimal_places=2, 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

    @property
    def course_type_name(self):
        # get_字段名_display()  取到该字段用choice映射的中文
        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()

    ## 序列化teacher第一种方案
    # def teacher_detail(self):
    #     return {\'name\':self.teacher.name}

    @property
    def section_list(self):
        sections_all = []
        # 通过课程拿到所有章节(反向操作:使用表名小写_set.all())
        # 如果ForeignKey加了related_name,反向操作的时候,直接使用related_name对象的值:coursechapters

        for coursechapter in self.coursechapters.all():
            for course_section in coursechapter.coursesections.all():
                sections_all.append(
                    {
                        \'name\': course_section.name,
                        \'section_link\': course_section.section_link,
                        \'duration\': course_section.duration,
                        \'free_trail\': course_section.free_trail,
                    }
                )
                if len(sections_all)>=4:
                    return sections_all

        return sections_all


###老师表
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

    def role_name(self):
        return self.get_role_display()

补充

# related_name:反向操作时,使用的字段名,用于代替原反向查询时的’表名_set’
# related_query_name:反向查询操作时,使用的连接前缀,用于替换表名

自定义过滤器

1 写一个类CourseNameFilter,继承BaseFilterBackend
2 重写filter_queryset,在内部实现过滤逻辑,返回queryset对象
3 在视图类中配置
filter_backends = [CourseNameFilter]

借助django-filter实现过滤功能

#########方案一(弱点是只能使用课程(course表)中的字段)#######
### 1 在视图类中
filter_backends = [DjangoFilterBackend]
filter_fields=[\'course_category\',\'name\']
## 使用
http://127.0.0.1:8000/api/course/actual_course/?course_category=2
        
#########方案二:可以自定义字段,及规则实现过滤#######        
filter_backends = [DjangoFilterBackend]
filter_class = CourseFilterSet  #写一个类

#### 过滤类
class CourseFilterSet(FilterSet):
    min_price = filters.NumberFilter(field_name=\'price\', lookup_expr=\'gte\')
    max_price = filters.NumberFilter(field_name=\'price\', lookup_expr=\'lte\')
    class Meta:
        model = models.Course
        fields = [\'course_category\',\'max_price\',\'min_price\']

课程详情接口

class CourseView(GenericViewSet,ListModelMixin,RetrieveModelMixin):

获取所有章节接口

  • 视图类
from .models import CourseChapter
class CourseChapterView(GenericViewSet,ListModelMixin):
    queryset = CourseChapter.objects.filter(is_delete=False,is_show=True).order_by(\'orders\')
    serializer_class =CourseChapterSerializer

    # 加一个通过课程id过滤功能
    filter_backends = [ DjangoFilterBackend]
    filter_fields = [\'course\']
  • 序列化类
class CourseSectionSerializer(serializers.ModelSerializer):
    class Meta:
        model=CourseSection
        fields=[\'name\', \'orders\', \'section_link\', \'duration\', \'free_trail\']


class CourseChapterSerializer(serializers.ModelSerializer):
    coursesections=CourseSectionSerializer(many=True) # 子序列化,多条
    class Meta:
        model=CourseChapter
        fields=[\'course\',\'chapter\',\'name\',\'summary\',\'coursesections\']

小结

1 课程分类
2 查询所有课程:支持分页,支持排序(价格,人数),支持过滤   ---》自定义过滤类,根据django-filter自定义区间过滤
3 课程详情接口
4 所有课程章节接口(每个章节下的课时:子序列化):支持按课程id号过滤功能

课程列表前台

<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="\'/actual-course/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/Header"
    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.$http.get(`${this.$settings.base_url}/course/categories/`).then(response => {
                this.$http.get(this.$settings.base_url+\'/api/course/category/\').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.$http.get(`${this.$settings.base_url}/api/course/actual_course/`, {
                    params: filters
                }).then(response => {
                    // console.log(response.data);
                    this.course_list = response.data.results;
                    this.course_total = response.data.count;
                    // console.log(this.course_list);
                }).catch(() => {
                    this.$message({
                        message: "获取课程信息有误,请联系客服工作人员"
                    })
                })
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

    .course .ordering ul {
        float: left;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    .course-item {
        position: relative;
    }

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

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

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

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

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

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

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

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

课程详情前台

<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/Header"
    import Footer from "@/components/Footer"

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

    export default {
        name: "Detail",
        data() {
            return {
                tabIndex: 2,   // 当前选项卡显示的下标
                course_id: 0, // 当前课程信息的ID
                course_info: {
                    teacher: {},
                }, // 课程信息
                course_chapters: [], // 课程的章节课时列表
                // 播放器初始化
                playerOptions: {
                    //https://www.jianshu.com/p/e8e747e33ef0
                    playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
                    aspectRatio: \'16:9\', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
                    sources: [{ // 播放资源和资源格式
                        type: "video/mp4",
                        src: "http://qx1wkw53q.hb-bkt.clouddn.com/01%20%E6%9C%AC%E5%91%A8%E5%86%85%E5%AE%B9%E5%AE%89%E6%8E%92%E4%B8%8E%E5%AD%A6%E4%B9%A0%E6%96%B9%E6%B3%95%E4%BB%8B%E7%BB%8D.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;
                this.course_id = this.$route.params.pk
                if (this.course_id < 1) {
                    let _this = this;
                    _this.$alert("对不起,当前视频不存在!", "警告", {
                        callback() {
                            _this.$router.go(-1);  // 退到上一个地址
                        }
                    });
                }
            },
            get_course_data() {
                // ajax请求课程信息
                this.$http.get(`${this.$settings.base_url}/api/course/actual_course/${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.$http.get(`${this.$settings.base_url}/api/course/course_chapter/`, {
                    params: {
                        "course": this.course_id,
                    }
                }).then(response => {
                    this.course_chapters = response.data;
                }).catch(error => {
                    window.console.log(error.response);
                })
            },
        },
        components: {
            Header,
            // Footer,
            videoPlayer, // 注册组件
        }
    }
</script>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

视频、图片...托管

第三方

  • 七牛云
  • 阿里oss
  • ...

自己搭建文件服务器

  • fastdfs
    • 渐少、没有web界面操作
  • go-fastdfs
  • ceph
    • 专业的文件服务器
  • minio
    • 有web操作界面

补充(不同接口使用不同序列化类)

# 需求是继承class CourseView(GenericViewSet,ListModelMixin,RetrieveModelMixin):实现不同接口,使用不同的序列化类

    def get_serializer_class(self):  # 返回哪个序列化类,当前接口就以该序列化类进行序列化
        print(self.request)
        if self.action==\'list\':
            print(\'获取所有\')
            return self.serializer_class
        else:
            print(\'其他情况\')
            from .serializer import CourseDetailSerializer
            return CourseDetailSerializer

前端搜索导航栏

  • Head.vue
   <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>



# js
#data:
 // 搜索相关
is_search_tip: true,
search_placeholder: \'\',
search_word: \'\'
#methods
search_action(search_word) {
    if (!search_word) {
        this.$message(\'请输入要搜索的内容\');
        return
    }
    if (search_word !== this.$route.query.word) {
        this.$router.push(`/course/search?word=${search_word}`);
        // this.$router.push(`/course/search/${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;
},
#### 样式
  .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;
    }

搜索接口

1 全文检索,尽量不要使用mysql,(mysql性能非常低),需要用到专业的搜索引擎Elasticsearch(分布式全文检索引擎)

from rest_framework.filters import SearchFilter

from luffy_api.utils.response import APIResponse
class CourseSearchView(GenericViewSet, ListModelMixin):
    queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by(\'orders\')
    serializer_class = CourseSerializer
    filter_backends = [SearchFilter]
    search_fields = [\'name\',]
    pagination_class = CommonPageNumberPagination

    # 后期改接口,可能要查好几种课程
    # 加入全文检索,使用es实现
    # def list(self, request, *args, **kwargs):
    #     search=request.query_params.get(\'search\')
    #     # 实战课
    #     response=super().list(request, *args, **kwargs)
    #     # 免费课
    #     course_list=CourseFree.objects.all().filter(name__contains=search)
    #     # 序列化
    #
    #     return APIResponse(result={\'actual_course\':response.data,\'free_course\':ser.data})

搜索页面

  • SearchCourse.vue
<template>
    <div class="search-course course">
        <Header/>

        <!-- 课程列表 -->
        <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/Header\'

    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.filter.search = this.$route.query.word

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

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

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

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

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

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

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

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

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

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

    .course .ordering ul {
        float: left;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    .course-item {
        position: relative;
    }

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

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

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

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

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

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

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

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

支付宝支付介绍

  • 支付功能

    • 支付宝
    • 微信
    • 自有支付系统
  • 支付流程

img

  • 申请支付宝支付
    • 必须以企业申请(需要营业执照)
    • 沙箱环境(测试环境)
      • 沙箱app
  • 使用第三方sdk实现支付
  • 使用第三方sdk
    • pip install python-alipay-sdk
    • pip install pyOpenSsl

支付宝二次封装

在libs文件夹下新建alipay包

alipay
	__init__.py  #包
    settings.py   #配置文件
    pay.py        # 核心文件
    pem           # 秘钥
        alipay_public_key.pem #支付宝公钥(从支付宝网站获得)
        app_private_key.pem   #应用私钥(生成自己保存)

settings.py

import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), \'pem\', \'app_private_key.pem\')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), \'pem\', \'alipay_public_key.pem\')).read()
# 应用ID
APP_ID = \'2016092000554611\'
# 加密方式
SIGN = \'RSA2\'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = \'https://openapi.alipaydev.com/gateway.do?\' if DEBUG else \'https://openapi.alipay.com/gateway.do?\'

pay.py

from alipay import AliPay
from . import settings
alipay = AliPay(
    appid=settings.APP_ID,
    app_notify_url=None,
    app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
    # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
    alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
    sign_type=settings.SIGN,  # RSA 或者 RSA2
    debug=settings.DEBUG , # 默认 False
)

订单相关

#新建order  app,新建表,迁移
from django.db import models

# Create your models here.


# 订单表
# 订单详情表

from user.models import User
from course.models import Course


class Order(models.Model):
    """订单模型"""
    status_choices = (
        (0, \'未支付\'),
        (1, \'已支付\'),
        (2, \'已取消\'),
        (3, \'超时取消\'),
    )
    pay_choices = (
        (1, \'支付宝\'),
        (2, \'微信支付\'),
    )
    subject = models.CharField(max_length=150, verbose_name="订单标题")
    total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
    out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True) #必须唯一
    trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")  # 支付交易流水号
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
    pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
    pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
    user = models.ForeignKey(User, related_name=\'order_user\', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")
    created_time = models.DateTimeField(auto_now_add=True, verbose_name=\'创建时间\')

    class Meta:
        db_table = "luffy_order"
        verbose_name = "订单记录"
        verbose_name_plural = "订单记录"

    def __str__(self):
        return "%s - ¥%s" % (self.subject, self.total_amount)



class OrderDetail(models.Model):
    """订单详情"""
    order = models.ForeignKey(Order, related_name=\'order_courses\', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单")
    course = models.ForeignKey(Course, related_name=\'course_orders\', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程")
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
    real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")

    class Meta:
        db_table = "luffy_order_detail"
        verbose_name = "订单详情"
        verbose_name_plural = "订单详情"

    def __str__(self):
        try:
            return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
        except:
            return super().__str__()

支付接口

# 点击立即购买,向后端发送post请求,携带数据格式
	-{\'total_amount\':99,\'courses\':[1,2,3]}
# 必须登录后才能调用
	-request.user中取出当前登录用户
    
# 后端处理逻辑,生成订单
	-1 校验课程总价格
    -2 获取当前登录用户
    -3 生成支付连接
    -4 生成订单(存order和orderdetail表)

视图类

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .models import Order
from .serializer import  OrderModelSerializer
from luffy_api.utils.response import APIResponse
class PayView(GenericViewSet,CreateModelMixin):
    queryset = Order.objects.all()
    serializer_class =OrderModelSerializer
    # 认证类(可以自己写)
    authentication_classes = [JSONWebTokenAuthentication,]
    permission_classes = [IsAuthenticated,]

    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)
        pay_url=serializer.context.get(\'pay_url\')
        return APIResponse(pay_url=pay_url)

序列化类

from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
import uuid
from rest_framework.exceptions import ValidationError
from luffy_api.libs.alipay import alipay,GATEWAY
from django.conf import settings
class OrderModelSerializer(serializers.ModelSerializer):
    # courses=serializers.ListField()  # 映射列表形式
    # courses=serializers.DictField()  # 映射字典形式

    # 这个字段,会把前端传入的课程id列表,转换成课程对象列表
    courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(),many=True)

    class Meta:
        model = Order
        fields = [\'total_amount\', \'courses\']

    def validate(self, attrs):
        \'\'\'
        	-1 校验课程总价格
            -2 获取当前登录用户
            -3 生成支付连接
            -4 存库之前数据处理
            -5 生成订单(存order和orderdetail表)
        \'\'\'
        # 1 校验课程总价格
        self._check_price(attrs)
        # 2 获取当前登录用户
        user = self._get_user()
        #3 生成支付连接
        trade_no=self._get_out_trade_no()
        total_amount=float(attrs.get(\'total_amount\'))
        # 订单名:如果是一个课程,就使用课程名,如果是多个课程,就使用第一个课程名+等
        if len(attrs.get(\'courses\'))>1:
            subject=\'合并支付\'
        else:
            subject=attrs.get(\'courses\')[0].name
        pay_url = self._get_pay_url(total_amount,subject,trade_no)
        self.context[\'pay_url\'] = pay_url

        # 4 支付前数据处理
        self._perform_save(attrs,user,trade_no,subject)
        return attrs

    def create(self, validated_data):
        courses=validated_data.pop(\'courses\')
        order=Order.objects.create(**validated_data)
        # 存订单详情
        for course in courses:
            OrderDetail.objects.create(order=order,course=course,price=course.price,real_price=course.price)
        return order

    def _check_price(self, attrs):
        total_amount=attrs.get(\'total_amount\')
        courses=attrs.get(\'courses\')
        price=0
        for course in courses:
            price+=course.price
        if not total_amount==price:
            raise ValidationError({\'detail\':\'价格不匹配\'})

    def _get_user(self):
        return self.context.get(\'request\').user


    def _get_pay_url(self,total_amount,subject,trade_no):

        res=alipay.api_alipay_trade_page_pay(
            total_amount=total_amount,
            subject=subject,
            out_trade_no=trade_no,
            return_url=settings.RETURN_URL,  # 前端回调
            notify_url=settings.NOTIFY_URL  #后端回调

        )

        return GATEWAY+res


    def _get_out_trade_no(self):
        # 使用uuid生成
        # 分布式id生成方案(在分布式的节点中生成不重复的id号,性能高)
        #   -uuid
        #   -mysql 自增   当前时间+自增
        #   -redis自增    当前时间+incr
        #   -雪花算法      主流
        #   -美团的Leaf
        return str(uuid.uuid4()).replace(\'-\',\'\')


    def _perform_save(self,attrs,user,trade_no,subject):
        # 把当前登录用户放到attrs中,把订单号,放入attrs中
        attrs[\'user\']=user
        attrs[\'out_trade_no\']=trade_no
        attrs[\'subject\']=subject

支付前台,回调页面

go_buy() {
                let token = this.$cookie.get(\'token\')
                if (token) {
                    this.$http.post(`${this.$settings.base_url}/api/order/pay/`, {
                        \'total_amount\': this.course_info.price,
                        \'courses\': [this.course_info.id,]
                    }, {
                        headers: {
                            \'Authorization\': \'jwt \' + token
                        }
                    }).then(res => {
                        if (res.data.code == 100) {
                            //打开一个新连接,支付
                            open(res.data.pay_url, \'_self\')
                        } else {
                            this.$message({
                                message: res.data.msg
                            });
                        }
                    })
                } else {
                    this.$message({
                        message: "您没有登录,请先登录"
                    });
                }

            }

支付成功回调接口(两个)

from rest_framework.views import APIView
from rest_framework.response import Response
class SuccessView(APIView):
    def get(self,request):
        # vue的前端来调用的
        out_trade_no=request.query_params.get(\'out_trade_no\')
        order=Order.objects.filter(out_trade_no=out_trade_no).first()
        if order.order_status==1:
            # 订单支付成功,
            return APIResponse(msg=\'支付成功\')
        else:
            return APIResponse(msg=\'我们还没有收到该订单的付款,请稍后刷新页面再试\')

    def post(self,request):
        # 支付宝来调用,用来修改订单状态
        import json
        try:
            result_data = request.data.dict()  # 把querydic转成字典对象,否则,不允许改值
            logger.warning(json.dumps(result_data))
            out_trade_no = result_data.get(\'out_trade_no\')
            signature = result_data.pop(\'sign\')
            ###一定要验证签名,如果不验证签名,不能修改订单状态
            from libs.alipay import alipay
            result = alipay.verify(result_data, signature)
            if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
                # 完成订单修改:订单状态、流水号、支付时间
                Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
                # 完成日志记录
                logger.warning(\'%s订单支付成功\' % out_trade_no)
                return Response(\'success\')  # 支付宝要求的格式
            else:
                logger.error(\'%s订单支付失败\' % out_trade_no)
        except:
            pass
        return Response(\'failed\')

分类:

技术点:

相关文章:

  • 2022-12-23
  • 2021-05-05
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-12-21
  • 2021-06-24
  • 2021-12-20
猜你喜欢
  • 2022-12-23
  • 2022-12-23
  • 2021-11-26
  • 2022-03-04
  • 2021-12-06
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案