路飞学城之 luffy(1)

目录

路飞学城之 luffy(1)

一、内容

"""
1)聊一聊为什么要集体管理django项目的应用app,如果用子文件夹集中管理了应用app,如何实现app可以直接在settings中注册
	一个成型的Django项目,会有很多功能模块,每一个功能模块对应一个app管理,所以导致项目由众多app,集中管理,结构更清晰,代码更规范。
	例如用apps文件夹集中管理app,要将apps文件夹在settings配置中添加到环境变量,apps下的应用都是对Django项目直接可见,所以可以直接注册

2)谈谈项目为什么要记录日志文件,记录的级别有什么要求
	开发中有控制台辅助开发,但是项目上线后,没有控制台辅助查看程序实时运行信息,但难免运行中出现了问题,且这些问题需要被记录下来,以便优化程序或修改bug。所以可以将重要的信息或错误的信息记录到日志文件中进行永久保存(会定期定量清楚过老的日志)

3)就存在的删库跑路,如何在数据库层面对程序员数据库开发这块进行管控
	为开发者创建满足开发需要的低权限数据库账号(只能操作某些数据库、只能操作某个数据库的某些表)

4)前后台分离项目交互时,如果出现了CORS,代表什么?如何解决
	1)服务器已经接收到了客户端请求
	2)服务器发现客户端非同域客户端(不是自家的)(ip、port、协议存在不一致)
	3)服务器不对客户端做需要响应,而是一种CORS错误响应(就是在响应头中做文章)
	处理CORS错误响应的本质:处理响应头
	
5)日志logging
	i)import logging 是Python解释器自带的(os、sys)
	ii)django项目加载settings配置,加载了LOGGING的配置字典,加载了配置中的所有logger
		logger(打印者)会有handler(输出位置),handler会有formatter(打印格式)和filter(信息过滤器)
	iii)提供logging模块的getLogger('logger名')方法来获取打印者,调用日志打印功能
	iv)打印级别有5种:debug、info、warning、error、critical,自己根据需求选择,你将要打印的信息定义为什么级别,你就采用什么级别进行打印
	v)handler可以设置收纳的级别信息,如果file handler设置了收纳级别为WARNING,那用logger完成五种几个的各一个信息打印,只有warning及以上级别会被记录到file中
"""

二、内容大纲

"""
1)前台三个页面(CV大法)
2)轮播图接口(重点)
3)git的基础操作
	i)三个区的命令:init、status、add、commit、reset、log
	ii)仓库账号管理
	iii)仓库过滤文件
"""

小结

"""
1)前台三个组件:导航栏、页脚、轮播图 => 主页
	i)组件文件如何编写代码
	ii)组件的注册和使用
	iii)路由配置与跳转(数据库保存当前页面路由)
	iv)前后台的axios交互
		this.$axios({
			url: '',
			method: 'get',
			headers: {},
			params: {},
			data: {}
		}).then(response => {}).catch(error => {})
		this.$axios({
			url: '',
		})
		this.$axios.get({
			url: ''
		})
		
2)轮播图接口:
	i)编写接口开发流程:
		根据业务数据分析创建Model表 => 为该Model配置满足需求的序列化类 => 实现具有Model及序列化类的视图类(页面逻辑处理) => 配置路由
		
3)版本控制器:svn、git
	i)优势:集群、多分支
	ii)当前文件夹为仓库:init;当前文件夹下创建子文件夹作为仓库:init 子文件夹
	iii)三个区:工作区(checkout .) =add .=> 暂存区(reset .) =commit -m ''=> 版本库(reset --hard 版本号)
	iv)基础命令:status、log、reflog
	v)账号:仓库内部配置了,走自己的;没有配置走全局的;如果都没配置,不能提交代码到版本库
		没有提交到版本库的代码,是不被git监听管理的
	vi)过滤 .gitignore 文件,过滤文件的内容都是自定义的,且随开发时间,内容会有所增加
		a:全文a文件|文件夹
		/a:从根目录下索引具体的a文件|文件夹
		*a*:名字中带a的文件|文件夹
		空文件夹:git无法检测,但是可以检测空包(空包下有一个__init__.py文件)
"""

三、内容大纲

"""
1)git的分支管理

2)gitee线上仓库与线下仓库交互

3)协调开发

4)git的冲突解决

5)线上版本回滚
"""

"""
6)登录注册页面

7)后台多方式登录接口

8)腾讯云短信服务(第三方)

9)短信验证码登录、短信验证码注册

10)前台登录状态的记录、前台注销登录
"""

小结

"""
1)简述一下,如何把dev分支上最新的功能推到prod分支上用于上线
	i)创建分支:git branch 分支,该分支是拷贝当前所在分支的所有版本
	ii)切换分支:git checkout prod
	iii)合并分支:git merge dev

2)项目仓库在本地初始化 和 在远程仓库完成初始化,两者的操作流程是如何
	i)项目在本地初始化,同步给远程仓库(空的),将本地项目初始化为仓库连接远程,push同步给远程仓库
	ii)远程仓库已经准备就绪,本地克隆远程仓库(连接远程),进行pull、push开发

3)如何加入公司已有的项目开发,公司项目采用的是自建git服务器,采用ssh协议
	i)生成电脑的ssh公钥私钥,提供公钥给管理员,管理员添加你为开发者
	ii)拿到管理员提供的仓库地址,克隆远程仓库到本地
	iii)进入本地仓库进行开发,与连接的远程仓库进行代码同步

4)在多名开发者协同开发项目过程中,从新建文件文件到提交成功到远程仓库,整个流程(已经是该项目合理开发者了)
	i)新建新功能文件,编写代码
	ii)将新功能 add、commit 到本地版本库
	iii)拉取远程仓库,在本地进行本版合并(别的开发者可能已经更新了远程仓库的版本)
	iv)如果出现冲突,线下解决冲突,并重复2、3步,知道没有任何冲突
	v)将本地版本同步给远程仓库
"""

四、内容大纲

"""
1)git冲突、线上分支合并、线上版本回滚

2)登录、注册、短信第三方、注销
"""

自定义模态框

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

<script>
    export default {
        name: "Login",
        methods: {
            close_login() {
                this.$emit('close')
            }
        }
    }
</script>

<style scoped>
    .login {
         100vw;
        height: 100vh;
        position: fixed;
        left: 0;
        top: 0;
        z-index: 666;
        background-color: rgba(0, 0, 0, 0.3);
    }
    span {
        font-size: 30px;
        cursor: pointer;
    }
</style>
componses/Header.vue
<template>
    <div class="header">
        <!-- ... -->
        <span @click="pull_login">登录</span>

        <Login v-if="is_login" @close="close_login" />
    </div>
</template>

<script>
    import Login from './Login'

    export default {
        name: "Header",
        data() {
            return {
                is_login: false,
            }
        },
        methods: {
            pull_login() {
                this.is_login = true;
            },
            close_login() {
                this.is_login = false;
            }
        },
        components: {
            Login,
        }
    }
</script>

vue-cookies操作浏览器cookies

"""
1)vue项目安装插件:
>: cnpm install vue-cookies

2)vue项目在main.js中配置
import cookies from 'vue-cookies'
Vue.prototype.$cookies = cookies;


3)vue项目逻辑中使用 vue-cookies 插件
增改:this.$cookies.set(key, value, exp)
查:this.$cookie.get(key)
删:this.$cookie.remove(key)

exp参数:
1)直接给数字,单位是s
2)默认是1d,代表一天
3)其他单位 1d 1m 1y,通常后后台统一 7d
"""

腾讯云短信开发

短信服务应用申请
""" 准备工作
1)创建短信应用 - 应用管理
2)申请短信签名 - 国内短信 > 签名管理
3)申请短信模块 - 国内短信 > 正文模板管理
"""

python中开发腾讯云短信服务
"""
1)API文档,接口的使用说吧
2)SDK,基于开发语言封装的可以直接调用的功能(工具)集合
	官网sdk使用文档中找到安装命令:pip install qcloudsms_py
	按照sdk使用说明进行开发:https://cloud.tencent.com/document/product/382/11672
"""

小结

"""
1)在git线上下线仓库合并时,怎样操作或导致合并冲突,如何来定位冲突位置,如何解决冲突
	i)线上线下遵循同分支操作
	ii)必须先拉取后提交,有冲突解决冲突,再将代码提交到本地版本库,再先拉后提
	iii)在多个开发者开发同一文件同行代码时,会出现冲突标识,删除所有标识,线下交流解决冲突
	
	iv)线上分支合并
	v)线上版本回滚:线上版本回滚、强行提交给线上(-f)

2)不用页面跳转,而采用自定义模态框方式,父子组件如何操作,可以实现子组件模态框的显示与隐藏
	i)在父组件中加载子组件,用一个变量控制子组件的显隐
	ii)父组件中的按钮点击事件激活可以修改显隐变量,控制子组件模态框的显示
	iii)子组件的按钮点击事件激活可以对父组件发送自定义事件,在父组件中接收事件完成显隐变量的值修改,隐藏模态框

3)前后台分离项目,cookies应该由哪一方独立完成?怎么实现vue项目cookies的增删改查操作
	i)vue-cookies插件
	ii)增改:this.$cookies.set(key, value, exp)
	iii)查:this.$cookies.get(key)
	iv)改:this.$cookies.remove(key)

4)要开通腾讯云的短信服务,将短信功能嵌入到Django项目,如何一步步操作
	i)开通腾讯云短信服务
	ii)创建短信服务应用、申请签名、申请模板
	iii)基于腾讯云的 Python SDK 进行二次开发
"""

五、内容大纲

Django缓存

# 1)导入缓存功能
from django.core.cache import 

# 2)设置,如果将exp过期时间设置0或负值,就是删除缓存
cache.set(key, value, exp)

# 3)获取
cache.get(key)

redis介绍

redis安装
"""
1、官网下载:安装包或是绿色面安装
2、安装并配置环境变量
"""

redis VS mysql
"""
redis: 内存数据库(读写快)、非关系型(操作数据方便、数据固定)
mysql: 硬盘数据库(数据持久化)、关系型(操作数据间关系、可以不同组合)

大量访问的临时数据,才有redis数据库更优
"""

redis VS memcache
"""
redis: 操作字符串、列表、字典、无序集合、有序集合 | 支持数据持久化(数据丢失可以找回(默认持久化,主动持久化save)、可以将数据同步给mysql) | 高并发支持
memcache: 操作字符串 | 不支持数据持久化 | 并发量小
"""

Redis操作

启动服务
"""
前提:前往一个方便管理redis持久化文件的逻辑再启动服务:dump.rdb
1)前台启动服务
>: redis-server

2)后台启动服务
>: redis-server --service-start
注)Linux系统后台启动(或是修改配置文件,建议采用方式)
>: redis-server &

3)配置文件启动前台服务
>: redis-server 配置文件的绝对路径

4)配置文件启动后台服务
注)windows系统默认按Redis安装包下的redis.windows-service.conf配置文件启动
>: redis-server --service-start
注)Linux系统可以完全自定义配置文件(redis.conf)后台启动
>: redis-server 配置文件的绝对路径 &
"""

密码管理
"""
1)提倡在配置文件中配置,采用配置文件启动
requirepass 密码

2)当服务启动后,并且连入数据库(redis数据库不能轻易重启),可以再改当前服务的密码(服务重启,密码重置)
config set requirepass 新密码

3)已连入数据库,可以查看当前数据库服务密码
config get requirepass
"""

连接数据库
"""
1)默认连接:-h默认127.0.0.1,-p默认6379,-n默认0,-a默认无
>: redis-cli

2)完整连接:
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码

3)先连接,后输入密码
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号
>: auth 密码
"""

切换数据库
"""
1)在连入数据库后执行
>: select 数据库编号
"""

关闭服务
"""
1)先连接数据库,再关闭redis服务
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码
>: shutdown

2)直接连接数据库并关闭redis服务
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码 shutdown
"""

数据持久化
"""
1)配置文件默认配置
save 900 1  # 超过900秒有1个键值对操作,会自动调用save完成数据持久化
save 300 10  # 超过300秒有10个键值对操作,会自动调用save完成数据持久化
save 60 10000  # 超过60秒有10000个键值对操作,会自动调用save完成数据持久化

2)安全机制
# 当redis服务不可控宕机,会默认调用一下save完成数据持久化

3)主动持久化
>: save  # 连入数据库时,主动调用save完成数据持久化

注:数据持久化默认保存文件 dump.rdb,保存路径默认为启动redis服务的当前路径
"""

redis相关配置
"""
1)绑定的ip地址,多个ip用空格隔开
bind 127.0.0.1

2)端口,默认6379,一般不做修改
port 6379

3)是否以守护进程启动,默认为no,一般改为yes代表后台启动(windows系统不支持)
daemonize no

4)定义日志级别,默认值为notice,有如下4种取值:
	debug(记录大量日志信息,适用于开发、测试阶段)
	verbose(较多日志信息)
	notice(适量日志信息,使用于生产环境)
	warning(仅有部分重要、关键信息才会被记录)
loglevel notice

5)配置日志文件保持地址,默认打印在命令行终端的窗口上
	如果填写 "./redis.log" 就会在启动redis服务的终端所在目录下,用redis.log记录redis日志
logfile ""

6)数据库个数,默认是16个,没特殊情况,不建议修改
databases 16

7)数据持久化
save 900 1  # 超过900秒有1个键值对操作,会自动调用save完成数据持久化
save 300 10  # 超过300秒有10个键值对操作,会自动调用save完成数据持久化
save 60 10000  # 超过60秒有10000个键值对操作,会自动调用save完成数据持久化

8)数据库持久化到硬盘失败,redis会立即停止接收用户数据,让用户知道redis持久化异常,避免数据灾难发生(重启redis即可),默认为yes,不能做修改
stop-writes-on-bgsave-error yes

9)消耗cpu来压缩数据进行持久化,数据量小,但会消耗cpu性能,根据实际情况可以做调整
rdbcompression yes

10)增持cpu 10%性能销毁来完成持久化数据的校验,可以取消掉
rdbchecksum yes

11)持久化存储的文件名称
dbfilename dump.rdb

12)持久化存储文件的路径,默认是启动服务的终端所在目录
dir ./
"""

Redis数据类型

"""
数据操作:字符串、列表、哈希(字典)、无序集合、有序(排序)集合
	有序集合:游戏排行榜
	
字符串:
	set key value
	get key
	mset k1 v1 k2 v2 ...
	mget k1 k2 ...
	setex key exp value
	incrby key increment
	
列表:
	rpush key value1 value2 ...
	lpush key value1 value2 ...
	lrange key bindex eindex
	lindex key index
	lpop key | rpop key
	linsert key before|after old_value new_value
	
哈希:
	hset key field value
	hget key field
	hmset key field1 value1 field2 value2 ...
	hmget key field1 field2
	hkeys key
	hvals key
	hdel key field
	
集合:
	sadd key member1 member2 ...
	sdiff key1 key2 ...
	sdiffstore newkey key1 key2 ...
	sinter key1 key2 ...
	sunion key1 key2 ...
	smembers key
	spop key
	
有序集合:
	zadd key grade1 member1 grade2 member2 ...
	zincrby key grade member
	zrange key start end
	zrevrange key start end
"""

python使用redis

依赖
>: pip3 install redis

直接使用
import redis
r = redis.Redis(host='127.0.0.1', port=6379, db=1, password=None, decode_responses=True)

连接池使用
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=1, max_connections=100, password=None, decode_responses=True)
r = redis.Redis(connection_pool=pool)

缓存使用:要额外安装 django-redis
# 1.将缓存存储位置配置到redis中:settings.py
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/0",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
            "DECODE_RESPONSES": True,
            "PSAAWORD": "",
        }
    }
}

# 2.操作cache模块直接操作缓存:views.py
from django.core.cache import cache  # 结合配置文件实现插拔式
# 存放token,可以直接设置过期时间
cache.set('token', 'header.payload.signature', 300)
# 取出token
token = cache.get('token')

短信验证码接口

后台

urls.py
path('sms/', views.SMSViewSet.as_view({'get': 'send'})),

throttles.py
from rest_framework.throttling import SimpleRateThrottle
from django.core.cache import cache
from django.conf import settings
# 结合手机验证码接口来书写
class SMSRateThrottle(SimpleRateThrottle):
    scope = 'sms'
    def get_cache_key(self, request, view):
        # 手机号是通过get请求提交的
        mobile = request.query_params.get('mobile', None)
        if not mobile:
            return None  # 不限制

        # 手机验证码发送失败,不限制,只有发送成功才限制,如果需求是发送失败也做频率限制,就注释下方三行
        code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
        if not code:
            return None

        return self.cache_format % {
            'scope': self.scope,
            'ident': mobile,
        }

const.py
# 短信验证码缓存key
SMS_CACHE_KEY = 'sms_cache_%(mobile)s'

# 短信验证码缓存时间s
SMS_CACHE_TIME = 300

dev.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'utils.exception.exception_handler',
    'DEFAULT_THROTTLE_RATES': {
        'sms': '1/min'
    }
}

views.py
from libs import tx_sms
from django.core.cache import cache
from django.conf import settings
from .throttles import SMSRateThrottle
class SMSViewSet(ViewSet):
    # 设置频率限制,一个手机号一分钟只能访问一次
    throttle_classes = [SMSRateThrottle]

    def send(self, request, *args, **kwargs):
        # return APIResponse(result=False)
        # 1)接收前台手机号验证手机格式
        mobile = request.query_params.get('mobile', None)
        if not mobile:
            return APIResponse(1, 'mobile field required')
        if not re.match(r'^1[3-9][0-9]{9}$', mobile):
            return APIResponse(1, 'mobile field error')
        # 2)后台产生短信验证码
        code = tx_sms.get_code()
        # 3)把验证码交给第三方,发送短信
        result = tx_sms.send_code(mobile, code, settings.SMS_CACHE_TIME // 60)
        # 4)如果短信发送成功,服务器缓存验证码(内存数据库),方便下一次校验
        if result:
            cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, code, settings.SMS_CACHE_TIME)
        # 5)响应前台短信是否发生成功
        return APIResponse(result=result)

短信登录接口

后台

urls.py
path('mobile/login/', views.MobileLoginViewSet.as_view({'post': 'login'})),

serializers.py
import re
from django.core.cache import cache
class MobileLoginSerializer(serializers.ModelSerializer):
    # 覆盖
    mobile = serializers.CharField(required=True, write_only=True)
    # 自定义
    code = serializers.CharField(min_length=4, max_length=4, required=True, write_only=True)
    class Meta:
        model = models.User
        fields = ('id', 'username', 'icon', 'mobile', 'code')
        extra_kwargs = {
            'id': {
                'read_only': True,
            },
            'username': {
                'read_only': True,
            },
            'icon': {
                'read_only': True,
            },

        }

    # 手机号格式校验(手机号是否存在校验规则自己考量)
    def validate_mobile(self, value):
        if not re.match(r'^1[3-9][0-9]{9}$', value):
            raise exceptions.ValidationError('mobile field error')
        return value


    def validate(self, attrs):
        # 验证码校验 - 需要验证码与手机号两者参与
        mobile = self._check_code(attrs)
        # 多方式得到user
        user = self._get_user(mobile)
        # user签发token
        token = self._get_token(user)
        # token用context属性携带给视图类
        self.context['token'] = token
        # 将登录用户对象直接传给视图
        self.context['user'] = user
        return attrs

    def _check_code(self, attrs):
        mobile = attrs.get('mobile')
        code = attrs.pop('code')
        old_code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
        if code != old_code:
            raise exceptions.ValidationError({'code': 'double code error'})
        else:
            # 验证码的时效性:一旦验证码验证通过,代表改验证码已使用,需要立即失效
            # cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, '', -1)
            pass
        return mobile

    def _get_user(self, mobile):
        try:
            return models.User.objects.get(mobile=mobile)
        except:
            raise exceptions.ValidationError({'mobile': 'user not exist'})

    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


views.py
class MobileLoginViewSet(ViewSet):
    # 局部禁用认证、权限组件
    authentication_classes = ()
    permission_classes = ()

    def login(self, request, *args, **kwargs):
        serializer = serializers.MobileLoginSerializer(data=request.data, context={'request': request})
        if serializer.is_valid():
            token = serializer.context.get('token')
            # 拿到登录用户,直接走序列化过程,将要返回给前台的数据直接序列化好给前台
            user = serializer.context.get('user')
            # 返回给前台的数据结果:id,username,icon,token
            result = serializers.MobileLoginSerializer(user, context={'request': request}).data
            result['token'] = token

            return APIResponse(result=result)
        return APIResponse(1, serializer.errors)

短信注册接口

后台

urls.py
router.register('register', views.RegisterViewSet, 'register')

serializers.py
class RegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(min_length=4, max_length=4, required=True, write_only=True)
    class Meta:
        model = models.User
        fields = ('mobile', 'password', 'code')
        extra_kwargs = {
            'password': {
                'min_length': 8,
                'max_length': 16,
                'write_only': True,
            }
        }

    def validate_mobile(self, value):
        if not re.match(r'^1[3-9][0-9]{9}$', value):
            raise exceptions.ValidationError('mobile field error')
        return value


    def validate(self, attrs):
        # 验证码校验 - 需要验证码与手机号两者参与
        mobile = attrs.get('mobile')
        code = attrs.pop('code')
        old_code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
        if code != old_code:
            raise exceptions.ValidationError({'code': 'double code error'})
        else:
            # 验证码的时效性:一旦验证码验证通过,代表改验证码已使用,需要立即失效
            # cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, '', -1)
            pass

        # 数据入库必须需要唯一账号:1)前台注册必须提供账号 2)默认用手机号作为账号名,后期可以修改
        attrs['username'] = mobile

        return attrs

    def create(self, validated_data):  # 入库的数据:mobile,password,username
        return models.User.objects.create_user(**validated_data)


views.py
# 手机验证码注册
from rest_framework.viewsets import GenericViewSet
from rest_framework import mixins
class RegisterViewSet(GenericViewSet, mixins.CreateModelMixin):
    queryset = models.User.objects.all()
    serializer_class = serializers.RegisterSerializer

    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        return APIResponse(result=response.data, http_status=response.status_code)

前台登录注册修订

前台

Login.vue
<template>
    <div class="login">
        <div class="box">
            <i class="el-icon-close" @click="close_login"></i>
            <div class="content">
                <div class="nav">
                    <span :class="{active: login_method === 'is_pwd'}"
                          @click="change_login_method('is_pwd')">密码登录</span>
                    <span :class="{active: login_method === 'is_sms'}"
                          @click="change_login_method('is_sms')">短信登录</span>
                </div>

                <el-form v-if="login_method === 'is_pwd'">
                    <el-input
                            placeholder="用户名/手机号/邮箱"
                            prefix-icon="el-icon-user"
                            v-model="username"
                            clearable>
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-button type="primary" @click="login">登录</el-button>
                </el-form>

                <el-form v-if="login_method === 'is_sms'">
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button @click="mobile_login" 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;
                // js正则:/正则语法/
                // '字符串'.match(/正则语法/)
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                // 后台校验手机号是否已存在
                this.$axios({
                    url: this.$settings.base_url + '/user/mobile/',
                    method: 'post',
                    data: {
                        mobile: this.mobile
                    }
                }).then(response => {
                    let result = response.data.result;
                    if (result) {
                        this.$message({
                            message: '账号正常',
                            type: 'success',
                            duration: 1000,
                        });
                        // 发生验证码按钮才可以被点击
                        this.is_send = true;
                    } else {
                        this.$message({
                            message: '账号不存在',
                            type: 'warning',
                            duration: 1000,
                            onClose: () => {
                                this.mobile = '';
                            }
                        })
                    }
                }).catch(() => {
                });
            },
            send_sms() {
                // this.is_send必须允许发生验证码,才可以往下执行逻辑
                if (!this.is_send) return;
                // 按钮点一次立即禁用
                this.is_send = false;

                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                // 定时器: setInterval(fn, time, args)

                // 往后台发送验证码
                this.$axios({
                    url: this.$settings.base_url + '/user/sms/',
                    method: 'get',
                    params: {
                        mobile: this.mobile
                    }
                }).then(response => {
                    let result = response.data.result;
                    if (result) { // 发送成功
                        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);
                    } else {  // 发送失败
                        this.sms_interval = "重新获取";
                        this.is_send = true;
                        this.$message({
                            message: '短信发送失败',
                            type: 'warning',
                            duration: 3000
                        });
                    }
                }).catch(() => {
                    this.sms_interval = "频率过快";
                    this.is_send = true;
                })


            },
            login() {
                if (!(this.username && this.password)) {
                    this.$message({
                        message: '请填好账号密码',
                        type: 'warning',
                        duration: 1500
                    });
                    return false  // 直接结束逻辑
                }

                this.$axios({
                    url: this.$settings.base_url + '/user/login/',
                    method: 'post',
                    data: {
                        username: this.username,
                        password: this.password,
                    }
                }).then(response => {
                    let username = response.data.result.username;
                    let token = response.data.result.token;
                    let user_id = response.data.result.id;
                    this.$cookies.set('username', username, '7d');
                    this.$cookies.set('token', token, '7d');
                    this.$cookies.set('user_id', user_id, '7d');
                    this.$emit('success', response.data.result);
                }).catch(error => {
                    console.log(error.response.data)
                })
            },
            mobile_login () {
                if (!(this.mobile && this.sms)) {
                    this.$message({
                        message: '请填好手机与验证码',
                        type: 'warning',
                        duration: 1500
                    });
                    return false  // 直接结束逻辑
                }

                this.$axios({
                    url: this.$settings.base_url + '/user/mobile/login/',
                    method: 'post',
                    data: {
                        mobile: this.mobile,
                        code: this.sms,
                    }
                }).then(response => {
                    let username = response.data.result.username;
                    let token = response.data.result.token;
                    let user_id = response.data.result.id;
                    this.$cookies.set('username', username, '7d');
                    this.$cookies.set('token', token, '7d');
                    this.$cookies.set('user_id', user_id, '7d');
                    this.$emit('success', response.data.result);
                }).catch(error => {
                    console.log(error.response.data)
                })
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

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

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

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

Register.vue
<template>
    <div class="register">
        <div class="box">
            <i class="el-icon-close" @click="close_register"></i>
            <div class="content">
                <div class="nav">
                    <span class="active">新用户注册</span>
                </div>
                <el-form>
                    <el-input
                            placeholder="手机号"
                            prefix-icon="el-icon-phone-outline"
                            v-model="mobile"
                            clearable
                            @blur="check_mobile">
                    </el-input>
                    <el-input
                            placeholder="密码"
                            prefix-icon="el-icon-key"
                            v-model="password"
                            clearable
                            show-password>
                    </el-input>
                    <el-input
                            placeholder="验证码"
                            prefix-icon="el-icon-chat-line-round"
                            v-model="sms"
                            clearable>
                        <template slot="append">
                            <span class="sms" @click="send_sms">{{ sms_interval }}</span>
                        </template>
                    </el-input>
                    <el-button @click="register" 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;
                // js正则:/正则语法/
                // '字符串'.match(/正则语法/)
                if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
                    this.$message({
                        message: '手机号有误',
                        type: 'warning',
                        duration: 1000,
                        onClose: () => {
                            this.mobile = '';
                        }
                    });
                    return false;
                }
                // 后台校验手机号是否已存在
                this.$axios({
                    url: this.$settings.base_url + '/user/mobile/',
                    method: 'post',
                    data: {
                        mobile: this.mobile
                    }
                }).then(response => {
                    let result = response.data.result;
                    if (!result) {
                        this.$message({
                            message: '欢迎注册我们的平台',
                            type: 'success',
                            duration: 1500,
                        });
                        // 发生验证码按钮才可以被点击
                        this.is_send = true;
                    } else {
                        this.$message({
                            message: '账号已存在,请直接登录',
                            type: 'warning',
                            duration: 1500,
                        })
                    }
                }).catch(() => {});
            },
            send_sms() {
                // this.is_send必须允许发生验证码,才可以往下执行逻辑
                if (!this.is_send) return;
                // 按钮点一次立即禁用
                this.is_send = false;

                let sms_interval_time = 60;
                this.sms_interval = "发送中...";

                // 定时器: setInterval(fn, time, args)

                // 往后台发送验证码
                this.$axios({
                    url: this.$settings.base_url + '/user/sms/',
                    method: 'get',
                    params: {
                        mobile: this.mobile
                    }
                }).then(response => {
                    let result = response.data.result;
                    if (result) { // 发送成功
                        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);
                    } else {  // 发送失败
                        this.sms_interval = "重新获取";
                        this.is_send = true;
                        this.$message({
                            message: '短信发送失败',
                            type: 'warning',
                            duration: 3000
                        });
                    }
                }).catch(() => {
                    this.sms_interval = "频率过快";
                    this.is_send = true;
                })


            },
            register () {
                if (!(this.mobile && this.sms && this.password)) {
                    this.$message({
                        message: '请填好手机、密码与验证码',
                        type: 'warning',
                        duration: 1500
                    });
                    return false  // 直接结束逻辑
                }

                this.$axios({
                    url: this.$settings.base_url + '/user/register/',
                    method: 'post',
                    data: {
                        mobile: this.mobile,
                        code: this.sms,
                        password: this.password
                    }
                }).then(response => {
                    this.$message({
                        message: '注册成功,3秒跳转登录页面',
                        type: 'success',
                        duration: 3000,
                        showClose: true,
                        onClose: () => {
                            // 去向成功页面
                            this.$emit('success')
                        }
                    });
                }).catch(error => {
                    this.$message({
                        message: '注册失败,请重新注册',
                        type: 'warning',
                        duration: 1500,
                        showClose: true,
                        onClose: () => {
                            // 清空所有输入框
                            this.mobile = '';
                            this.password = '';
                            this.sms = '';
                        }
                    });
                })
            }
        }
    }
</script>

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

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

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

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

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

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

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

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

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

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

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

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

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('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
                </li>
                <li class="ele">
                    <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
                </li>
            </ul>

            <div class="right-part">
                <div v-if="!token">
                    <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" @success="success_login" />
        <Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register" />
    </div>
</template>

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

    export default {
        name: "Header",
        data() {
            return {
                url_path: sessionStorage.url_path || '/',
                token: '',
                username: '',
                user_id: '',
                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;
            },
            close_register() {
                this.is_register = false;
            },
            success_login(data) {
                this.is_login = false;
                this.username = data.username;
                this.token = data.token;
                this.user_id = data.user_id;
            },
            logout() {
                this.token = '';
                this.username = '';
                this.user_id = '';
                this.$cookies.remove('username');
                this.$cookies.remove('token');
                this.$cookies.remove('user_id');
            },
            success_register () {
                this.is_register = false;
                this.is_login = true;
            }
        },
        created() {
            sessionStorage.url_path = this.$route.path;
            this.url_path = this.$route.path;

            // 检测cookies,查看登录状态
            this.username = this.$cookies.get('username');
            this.token = this.$cookies.get('token');
            this.user_id = this.$cookies.get('user_id');
        },
        components: {
            Login,
            Register,
        }
    }
</script>

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

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

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

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

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

    }

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

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

    .nav ul li {
        float: left;
    }

    .logo {
        margin-right: 20px;
    }

    .ele {
        margin: 0 20px;
    }

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

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

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

    .right-part {
        float: right;
    }

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

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

六、redis服务器的启动

redis 操作

2redis介绍

redis安装
"""
1、官网下载:安装包或是绿色面安装
2、安装并配置环境变量
"""

redis VS mysql
"""
redis: 内存数据库(读写快)、非关系型(操作数据方便、数据固定)
mysql: 硬盘数据库(数据持久化)、关系型(操作数据间关系、可以不同组合)

大量访问的临时数据,才有redis数据库更优
"""

redis VS memcache
"""
redis: 操作字符串、列表、字典、无序集合、有序集合 | 支持数据持久化(数据丢失可以找回(默认持久化,主动持久化save)、可以将数据同步给mysql) | 高并发支持
memcache: 操作字符串 | 不支持数据持久化 | 并发量小
"""

Redis操作

启动服务
"""
前提:前往一个方便管理redis持久化文件的逻辑再启动服务:dump.rdb
1)前台启动服务
>: redis-server

2)后台启动服务
>: redis-server --service-start
注)Linux系统后台启动(或是修改配置文件,建议采用方式)
>: redis-server &

3)配置文件启动前台服务
>: redis-server 配置文件的绝对路径

4)配置文件启动后台服务
注)windows系统默认按Redis安装包下的redis.windows-service.conf配置文件启动
>: redis-server --service-start
注)Linux系统可以完全自定义配置文件(redis.conf)后台启动
>: redis-server 配置文件的绝对路径 &
"""


"""
windows系统
1)前台启动
	i)打开终端切换到redis安装目录
	>: cd C:AppsRedis
	
	ii)启动服务
	>: redis-server redis.windows.conf

2)后台启动
	i)打开终端切换到redis安装目录
	>: cd C:AppsRedis
	
	ii)启动服务(后面的配置文件可以省略)
	>: redis-server --service-start redis.windows-service.conf
"""

密码管理
"""
1)提倡在配置文件中配置,采用配置文件启动
requirepass 密码

2)当服务启动后,并且连入数据库(redis数据库不能轻易重启),可以再改当前服务的密码(服务重启,密码重置)
config set requirepass 新密码

3)已连入数据库,可以查看当前数据库服务密码
config get requirepass
"""

连接数据库
"""
1)默认连接:-h默认127.0.0.1,-p默认6379,-n默认0,-a默认无
>: redis-cli

2)完整连接:
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码

3)先连接,后输入密码
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号
>: auth 密码
"""

切换数据库
"""
1)在连入数据库后执行
>: select 数据库编号
"""

关闭服务
"""
1)先连接数据库,再关闭redis服务
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码
>: shutdown

2)直接连接数据库并关闭redis服务
>: redis-cli -h ip地址 -p 端口号 -n 数据库编号 -a 密码 shutdown
"""

清空redis数据库
"""
1)连接数据库执行
>: flushall
"""

数据持久化
"""
1)配置文件默认配置
save 900 1  # 超过900秒有1个键值对操作,会自动调用save完成数据持久化
save 300 10  # 超过300秒有10个键值对操作,会自动调用save完成数据持久化
save 60 10000  # 超过60秒有10000个键值对操作,会自动调用save完成数据持久化

2)安全机制
# 当redis服务不可控宕机,会默认调用一下save完成数据持久化(如果数据量过大,也可能存在部分数据丢失)

3)主动持久化
>: save  # 连入数据库时,主动调用save完成数据持久化

注:数据持久化默认保存文件 dump.rdb,保存路径默认为启动redis服务的当前路径
"""

redis相关配置
"""
1)绑定的ip地址,多个ip用空格隔开
bind 127.0.0.1

2)端口,默认6379,一般不做修改
port 6379

3)是否以守护进程启动,默认为no,一般改为yes代表后台启动(windows系统不支持)
daemonize no

4)定义日志级别,默认值为notice,有如下4种取值:
	debug(记录大量日志信息,适用于开发、测试阶段)
	verbose(较多日志信息)
	notice(适量日志信息,使用于生产环境)
	warning(仅有部分重要、关键信息才会被记录)
loglevel notice

5)配置日志文件保持地址,默认打印在命令行终端的窗口上
	如果填写 "./redis.log" 就会在启动redis服务的终端所在目录下,用redis.log记录redis日志
logfile ""

eg)终端首先切断到log文件夹所在目录(一般就可以采用redis的安装目录,也可以自定义),再启动reids服务
logfile "./log/redis.log"

6)数据库个数,默认是16个,没特殊情况,不建议修改
databases 16

7)数据持久化
save 900 1  # 超过900秒有1个键值对操作,会自动调用save完成数据持久化
save 300 10  # 超过300秒有10个键值对操作,会自动调用save完成数据持久化
save 60 10000  # 超过60秒有10000个键值对操作,会自动调用save完成数据持久化

8)数据库持久化到硬盘失败,redis会立即停止接收用户数据,让用户知道redis持久化异常,避免数据灾难发生(重启redis即可),默认为yes,不能做修改
stop-writes-on-bgsave-error yes

9)消耗cpu来压缩数据进行持久化,数据量小,但会消耗cpu性能,根据实际情况可以做调整
rdbcompression yes

10)增持cpu 10%性能销毁来完成持久化数据的校验,可以取消掉
rdbchecksum yes

11)持久化存储的文件名称
dbfilename dump.rdb

12)持久化存储文件的路径,默认是启动服务的终端所在目录
dir ./

13)reids数据库密码
requirepass 密码
"""

Redis数据类型

"""
数据操作:字符串、列表、哈希(字典)、无序集合、有序(排序)集合
	有序集合:游戏排行榜
	
字符串:
	set key value
	get key
	mset k1 v1 k2 v2 ...
	mget k1 k2 ...
	setex key exp value
	incrby key increment
	
列表:
	rpush key value1 value2 ...
	lpush key value1 value2 ...
	lrange key bindex eindex
	lindex key index
	lpop key | rpop key
	linsert key before|after old_value new_value
	
哈希:
	hset key field value
	hget key field
	hmset key field1 value1 field2 value2 ...
	hmget key field1 field2
	hkeys key
	hvals key
	hdel key field
	
集合:
	sadd key member1 member2 ...
	sdiff key1 key2 ...
	sdiffstore newkey key1 key2 ...
	sinter key1 key2 ...
	sunion key1 key2 ...
	smembers key
	spop key
	
有序集合:
	zadd key grade1 member1 grade2 member2 ...
	zincrby key grade member
	zrange key start end
	zrevrange key start end
"""

python使用redis

依赖
>: pip3 install redis

直接使用
import redis
# decode_responses=True得到的结果会自动解码(不是二进制数据)
r = redis.Redis(host='127.0.0.1', port=6379, db=1, password=None, decode_responses=True)

连接池使用
import redis
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=1, max_connections=100, password=None, decode_responses=True)
r = redis.Redis(connection_pool=pool)

缓存使用:要额外安装 django-redis
# 1.将缓存存储位置配置到redis中:settings.py
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/0",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100},
            "DECODE_RESPONSES": True,
            "PASSWORD": "",
        }
    }
}

# 2.操作cache模块直接操作缓存:views.py
from django.core.cache import cache  # 结合配置文件实现插拔式
# 存放token,可以直接设置过期时间
cache.set('token', 'header.payload.signature', 300)
# 取出token
token = cache.get('token')

复习

"""
多方式登录
手机号是否存在校验
短信验证码接口 + 短信验证码频率组件
短信验证码登录:填写手机号(校验手机号) - 发送验证码 - 手机验证码登录
短信验证码注册:填写手机号(校验手机号) - 发送验证码 - 手机验证码密码注册
前台登录注册页面与后台接口交互、导航栏登录状态(cookies)、注销

1)写出axios的完整结构,用post往/login/发送请求,要携带请求头、拼接参数、数据包参数,接收正常响应回调和异常响应回调
this.axios({
	url: '/login/',
	method: 'post',
	headers: {
		authorization: 'jwt token',
	},
	params: {
		
	},
	data: {
		
	}
}).then(response => {
	response.data
}).catch(error => {
	error.response.data
}) 

2)简述一下,手机验证码的整个生命周期(从如何产生到如何销毁)
	i)后台产生验证码
	ii)将验证码交给第三方发送给用户
	iii)后台缓存验证码(过期时间同第三方通知给用户的有效时间:5分钟)
	iv)需要验证码校验的接口,从缓存中去除验证码进行校验
	v)如果校验通过,验证码立即失效;一直没有使用,5分钟后自动失效

3)我们在写注册接口的序列化类时,重写了create方法,解释一下
	i)注册接口操作的是User表,且入库信息中有password字段,该字段需要密文处理
	ii)注册接口完成的是增数据入库操作,使用在视图类中serializer.save()本质是调用序列化类的create方法完成入库的
	iii)默认入库调用的是model类的create方法,这样操作密码是明文,所以重写序列化类的create方法,调用auth组件封装的create_user()方法完成入库(密码会密文)

4)redis数据库启动服务命令,连接数据库命令,切换数据库命令,关闭服务命令
	redis-server
	redis-cli -h 127.0.0.1 -p 6379 -a admin -n number
	
	在已连接数据库情况下
	select number
	shutdown

补充:
进程:资源单位
线程:执行单位
"""

内容

"""
1)redis服务启动:
	终端在哪个目录下启动是有目的的:日志文件、数据持久化文件(快照)
	不管前台还是后台启动,要通过一个可控的配置文件

2)redis配置:ip、port、dbcount、log、rdb、requirepass

3)reids数据类型:字符串、列表、哈希、集合、有序集合

4)python中使用redis:pip install redis

5)django缓存配置redis:pip install django-redis
"""

总结

"""
1)redis基础命令
	redis-server | redis-server --service-start | redis-server &
	redis-server 配置文件的绝对路径
	redis-cli -h -p -n -a
	select 数字 | save | flushall | shutdown | auth 密码
	
2)基础配置
	host、port、requirepass、db个数、rdb、log
	
3)redis数据持久化
	自动持久化的配置
	主动调用save,立即持久化
	安全措施:redis服务不正常退出,会紧急调用save

4)redis的数据类型
	字符串:set | mset | setex
	列表:rpush | lpush
	哈希:hset | hmset key f1 v1 f2 v2
	集合:sadd | 去重 | 运算
	有序集合:zadd | 游戏排行

5)python使用redis
	安装redis模块,django项目还有额外安装django-redis
	i)直接使用redis操作
	ii)利用连接池操作redis
	iii)配置django缓存数据库采用redis,操作django缓存就可以直接操作redis
		django缓存可以直接处理django项目支持的所以单数据、列表数据、字典数据入(redis)库操作
"""

七、内容大纲

"""
1)接口缓存

2)celery异步任务框架

3)异步同步接口

4)分析课程模块表
"""

接口缓存

"""
1)什么是接口的后台缓存
	前台访问后台接口,后台会优先从缓存(内存)中查找接口数据
		如果有数据,直接对前台响应缓存数据
		如果没有数据,与(mysql)数据库交互,得到数据,对前台响应,同时将数据进行缓存,以备下次使用
	
	了解:前台缓存 - 前台在请求到接口数据后,在前台建立缓存,再发送同样请求时,发现前台缓存有数据,就不再对后台做请求了
	
2)什么的接口会进行接口缓存
	i)接口会被大量访问:比如主页中的接口,几乎所有人都会访问,而且会重复访问
	ii)在一定时间内数据不会变化(或数据不变化)的接口
	iii)接口数据的时效性不是特别强(数据库数据发生变化了,不是立即同步给前台,验后时间同步给前台也没事)
	注:理论上所有接口都可以建立缓存,只要数据库与缓存数据同步及时
	
3)如何实现接口缓存:主页轮播图接口
"""

Celery框架

导入

"""
1)查看课件了解:什么是celery框架,框架的各部分组成,框架的应用场景

2)python两中方式封装下,使用celery框架
"""

# 如果 Celery对象:Celery(...) 是放在一个模块下的
# 1)终端切换到该模块所在文件夹位置:scripts
# 2)执行启动worker的命令:celery worker -A 模块名 -l info -P eventlet
# 注:windows系统需要eventlet支持,Linux与MacOS直接执行:celery worker -A 模块名 -l info
# 注:模块名随意


# 如果 Celery对象:Celery(...) 是放在一个包下的
# 1)必须在这个包下建一个celery.py的文件,将Celery(...)产生对象的语句放在该文件中
# 2)执行启动worker的命令:celery worker -A 包名 -l info -P eventlet
# 注:windows系统需要eventlet支持,Linux与MacOS直接执行:celery worker -A 模块名 -l info
# 注:包名随意

第一种:t_celery.py
from celery import Celery
app = Celery()  # 传入必要参数

第二种:tt_celery/celery.py
#  tt_celey是包
from celery import Celery
app = Celery()  # 传入必要参数

三种任务使用

scripts/celery_task_1/celery.py
from celery import Celery

# 连接redis: 'redis://:密码@服务器IP:端口/数据库编号'
broker = 'redis://:Admin123@127.0.0.1:6379/1'
backend = 'redis://:Admin123@127.0.0.1:6379/2'

# worker配置
app = Celery(broker=broker, backend=backend, include=['celery_task_1.tasks'])

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

# 定时任务的配置
'''
app.conf.beat_schedule = {
    '自定义定时任务名': {
        'task': '指向任务函数',
        'schedule': '下一次再添加任务的时间间隔或固定的时间配置',
        'args': '无名参数',
        'kwargs': '有名参数'
    }
}
'''

# beat配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
    'test-task': {
        'task': 'celery_task_1.tasks.test_task',
        'schedule': timedelta(seconds=3),
        # 'schedule': crontab(hour=8, day_of_week=1),  # 每周一早八点
        'args': (666,),
    }
}

scripts/celery_task_1/tasks.py
from .celery import app

@app.task
def test_task(data):
    print('该方法就是任务,任务被执行了,传入的参数:%s' % data)
    return '该内容就是任务结果'


@app.task
def add(n1, n2):
    r = n1 + n2
    print('%s + %s = %s' % (n1, n2, r))
    return r

scripts/add_task.py
# 总结:该文件一定要独立开celery封装的包
# 原因:比如celery有一个更新轮播图缓存的任务,django项目是可以响应前台或后台用户主动更新轮播图数据库的数据,
# 当用户更新了数据库数据,就可以执行一下代码,通知celery可以去异步执行更新轮播图缓存的任务了


# 右键执行该文件,下面带导包是合理的
from celery_task_1.tasks import test_task

# 直接导入函数,调用函数,和celery没有任何关系
# test_task(666)

# 要将任务交给celery来执行
# 1)异步任务(立即去异步执行) => 视频同步
# t1 = test_task.delay(666)  # 返回值是任务对象,直接输出代表任务唯一标识:id
# print(t1.id)


# 2)延迟任务(达到设定的延迟时间后再去异步执行) => 定时发送邮件
from datetime import datetime, timedelta
eta = datetime.utcnow() + timedelta(seconds=10)

# t2 = test_task.apply_async(args=(888, ), eta=eta)
# print(t2.id)

from celery_task_1.tasks import add
t3 = add.apply_async(args=(33, 66), eta=eta)
# t3 = add.apply_async(kwargs={'n1': 44, 'n2': 66}, eta=eta)
print(t3.id)


# 3)定时任务(在worker服务以外,再启动一个beat服务,定时帮我们自动添加任务) => 定时更新轮播图
# i)在celery中配置好beat_schedule的配置后,执行命令启动定时添加任务服务
# >: celery beat -A 包名|模块名 -l info

scripts/get_task_result.py
# 获取任务结果也是项目正常逻辑来调用的

from celery.result import AsyncResult
from celery_task_1.celery import app

id = 'fad4b75c-c168-443a-be7c-7696b3233295'

if __name__ == '__main__':
    task_result = AsyncResult(id=id, app=app)
    if task_result.successful():
        result = task_result.get()
        print(result)
    elif task_result.failed():
        print('任务失败')
    elif task_result.status == 'PENDING':
        print('任务等待中被执行')
    elif task_result.status == 'RETRY':
        print('任务异常后正在重试')
    elif task_result.status == 'STARTED':
        print('任务已经开始被执行')

Django项目实现轮播图缓存更新

luffyapi/celery_task/celery.py
# 加载django的环境
import os

# 如果不想把celery_task包放在项目根目录,必须添加如下几句,将项目根目录要添加到环境变量中,
# 因为加载django环境需要加载dev
# import sys
# sys.path.append(r'C:UsersowenDesktopluffyluffyapi')

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev")


from celery import Celery

# 连接redis:
broker = 'redis://:Admin123@127.0.0.1:6379/1'
backend = 'redis://:Admin123@127.0.0.1:6379/2'

# worker
app = Celery(broker=broker, backend=backend, include=['celery_task.tasks'])

# 时区
app.conf.timezone = 'Asia/Shanghai'

# beat
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),
        # 'schedule': crontab(hour=8),  # 每天早八点
        'args': (),
    }
}

luffyapi/celery_task/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

在luffyapi项目所在目录终端下的命令(需要开辟两个终端)
>: celery worker -A celery_task -l info -P eventlet
    
>: celery beat -A celery_task -l info

小结

"""
1)谈一谈redis数据库持久化机制
	主动:save
	自动:900:1 300:10 60:10000
	被动:异常退出自动调用save
	文件:dump.rbd

2)什么样的接口数据一般会做缓存处理,接口缓存的原理是什么
	缓存接口一般:大量访问、数据较固定、时间的时效性要求不高
	缓存原理:优先找缓存数据,有走缓存,没有走数据库更新缓存
	缓存技术:django cache (redis)

3)介绍一下celery异步任务框架,组成部分,工作原理
	框架:独立运行的服务
	组成:broker(中间件) > worker(执行者) > backend(结果仓库)
	应用:Celery(broker, backend, include)  # include就是任务文件
	封装:
		模块:在模块内有Celery()应用对象
		包:包内必须有celery.py文件,在文件内有Celery()应用对象 ***
	命令:
		>: cd 包所在的文件夹
		>: celery worker -A 包名 -l info -P eventlet
	工作:
		脚本或正常服务,为celery框架添加任务到broker
		celery会自动从broker中拿任务执行
		将执行的任务结果放在backend
		脚本或正常服务,可以再在backend中获取任务执行结果

4)celery框架可以帮我们解决哪些问题,可以举例阐述一下
	异步执行:耗时任务 - 服务器同步客户端生成的视频
	延迟执行:延迟任务 - 延迟发送邮件
	定时执行:周期任务 - 定时更新缓存
	
5)轮播图接口缓存更新
	将接口的缓存清除(cache.set('banner_list', '', -1) | cache.delete('banner_list'))
"""

八、内容大纲

"""
课程模块:
1)表设计

2)表关系优化

3)群查接口模块:分页、搜索、排序、分类、区间

4)视频托管
"""

课程

准备工作

"""
1)创建course应用
2)dev中注册
3)建立子urls完成路由分发
4)配置课程业务相关表,并录入测试数据(见课件)
5)完成课程分类与课程两个群查接口
"""

课程分类群查接口

serializers.py
from rest_framework import serializers
from . import models
class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.CourseCategory
        fields = ('name', )

views.py
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from . import models, serializers
# 课程分类群查
class CourseCategoryViewSet(GenericViewSet, ListModelMixin):
    queryset = models.CourseCategory.objects.filter(is_delete=False, is_show=True).all()
    serializer_class = serializers.CourseCategorySerializer


urls.py
router.register('categories', views.CourseCategoryViewSet, 'categories')

课程群查接口

serializers.py
# 子序列化
class TeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Teacher
        fields = ('name', 'role_name', 'title', 'signature', 'image', 'brief')


class CourseSerializer(serializers.ModelSerializer):
    # teacher = TeacherSerializer(many=False)

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

views.py
class CourseViewSet(GenericViewSet, ListModelMixin):
    queryset = models.Course.objects.filter(is_delete=False, is_show=True).all()
    serializer_class = serializers.CourseSerializer

urls.py
router.register('free', views.CourseViewSet, 'free')

分页组件

""" 分页组件的使用
1)重写分页类 - 自定义同名类继承drf的分页类
2)完成必要的配置
3)将重写的分页类配置给群查需求的视图类
"""

pagination.py
from rest_framework.pagination import PageNumberPagination as DrfPageNumberPagination

class PageNumberPagination(DrfPageNumberPagination):
    # 默认一页显示的条数
    page_size = 2
    # url中携带页码的key
    page_query_param = 'page'
    # url中用户携带自定义一页条数的key
    page_size_query_param = 'page_size'
    # 用户最大可自定义一页的条数
    max_page_size = 3


from rest_framework.pagination import LimitOffsetPagination as DrfLimitOffsetPagination
class LimitOffsetPagination(DrfLimitOffsetPagination):
    # 默认一页显示的条数
    default_limit = 2
    # url中用户携带自定义一页条数的key
    limit_query_param = 'limit'
    # url中用户携带自定义偏移条数的key
    offset_query_param = 'offset'
    # 用户最大可自定义一页的条数
    max_limit = 2


from rest_framework.pagination import CursorPagination as DrfCursorPagination
class CursorPagination(DrfCursorPagination):
    # 默认一页显示的条数
    page_size = 2
    # url中携带页码的key(编码后的结果)
    cursor_query_param = 'cursor'
    # url中用户携带自定义一页条数的key
    page_size_query_param = 'page_size'
    # 用户最大可自定义一页的条数
    max_page_size = 3
    # 游标分页器的特殊点:
    # 1)如果视图类没有配 排序过滤组件filter_backends = [OrderingFilter],采用 ordering 设置的作为默认排序规则
    # 2)如果视图类配了 排序过滤组件filter_backends = [OrderingFilter],url请求必须带上ordering排序规则,因为默认排序规则失效
    # 注:因为游标分页是基于排序后结果上的分页
    ordering = '-price'

搜索组件

"""
搜索组件
1)在视图文件views.py中导入drf的搜索组件
from rest_framework.filters import SearchFilter

2)将搜索组件配置给群查接口视图类的filter_backends
filter_backends = [SearchFilter]

3)配置视图类关联的Model表参与搜索的字段
search_fields = ['name', 'id']

4)前台访问该群查接口,采用拼接参数方式用search关键字将搜索目标提供给后台
http://127.0.0.1:8000/course/free/?search=2  # id或name中包含2的所有结果
"""

排序组件

"""
排序组件
1)在视图文件views.py中导入drf的搜索组件
from rest_framework.filters import OrderingFilter

2)将搜索组件配置给群查接口视图类的filter_backends
filter_backends = [OrderingFilter]

3)配置视图类关联的Model表允许排序的字段
ordering_fields = ['id', 'price']

4)前台访问该群查接口,采用拼接参数方式用search关键字将搜索目标提供给后台
http://127.0.0.1:8000/course/free/?ordering=price,-id  # 按price升序,如果price相同,再按id降序
"""

自定义过滤组件

"""
自定义过滤器
1)自定义类实现filter_queryset方法即可,接收request, queryset, view参数

2)制定过滤条件,将过滤成功后的queryset返回即可,如果过滤失败,返回原样的queryset

3)将自定义过滤类配置给群查视图类的filter_backends
"""

filters.py
# 前台接口:/course/free/?count=2 ,代表只对前台返回2条数据
class CountFilter:
    def filter_queryset(self, request, queryset, view):
        count = request.query_params.get('count', None)
        try:
            # TODO: 切片后的queryset不能再做ORM Q查询,如何实现queryset切片,现在再过滤时后配置
            # 结论:drf的搜索组件和排序组件都是建立在表的所有数据基础上的过滤规则,所以该自定义过滤类在视图类配置中
            # filter_backends = [SearchFilter, OrderingFilter, CountFilter] 必须在前两者之后
            return queryset[:int(count)]
        except:
            return queryset

有分页、搜索、排序、自定义过滤的课程群查接口

views.py
# 分页组件:基础分页(采用)、偏移分页、游标分页(了解)
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
from . import pagination

# 过滤组件:搜索功能、排序功能
from rest_framework.filters import SearchFilter, OrderingFilter
from .filters import CountFilter
class CourseViewSet(GenericViewSet, ListModelMixin):
    queryset = models.Course.objects.filter(is_delete=False, is_show=True).all()
    serializer_class = serializers.CourseSerializer

    # 分页组件
    # 方法一:直接使用drf分页类,在视图类中完成分页类的必要配置
    # pagination_class = PageNumberPagination
    # PageNumberPagination.page_size = 1

    # 方法二:自定义分页类继承drf分页类,在自定义分页类中完成配置,视图类中使用自定义分页类
    # 基础分页器
    # pagination_class = pagination.PageNumberPagination
    # 偏移分页器,没有固定页码,自定义从偏移量开始往后查询自定义条数
    # pagination_class = pagination.LimitOffsetPagination
    # 游标分页器
    # pagination_class = pagination.CursorPagination

    # 过滤组件:实际开发,有多个过滤条件时,要把优先级高的放在前面
    filter_backends = [SearchFilter, OrderingFilter, CountFilter]
    # 参与搜索的字段
    search_fields = ['name', 'id']

    # 允许排序的字段
    ordering_fields = ['id', 'price']

总结

"""
1)分析课程业务
	三条独立的课程线:免费课、实战课、学位课
	重点:一些连表计算的结果,可以直接用一个字段表示(能不连表尽量不连表)

2)课程相关表设计(CV)

3)课程分类群查接口(简单)

4)课程群查接口:
	i)参与序列化的字段:display、子序列化、连表序列化
	ii)分页器:基础、偏移、游标分页器(了解)
	iii)过滤:搜索过滤、排序过滤、自定义过滤
"""

九、内容大纲

"""
1)分类过滤、区间过滤:django-filter

2)课程主页修订

3)课程详情页:课程单查接口、章节群查接口

4)搜索业务

5)支付业务

6)课程模块  (课件夹里)

7)搜索功能 (课件夹里)

8)支付 (课件夹里)

"""

复习

"""
1)谈一谈你知道的所有MySql数据库优化方案
	i)索引:主键、外键、唯一键、index、联合索引(最左查询) id name age
	ii)sql:id * 10 = 100 | id = 100 / 10
	iii)分库分表:业务结构更清晰,表数据更精确
	iv)能不连表尽量不连表
	v)断关联(增删改)
	vi)连表数据的缓存
	vii)慢查询优化
	viii)读写分离:主从复制 - 主:增删改(写) 从:查(读)

2)对一个群查接口,如何实现基础分页功能
	i)自定义分页类继承drf分页类完成配置page_size
	ii)将自定义分页类配置给视图类

3)对一个群查接口配置搜索功能,如何实现,前台如何请求
	i)导入搜索组件
	ii)视图类配置搜索组件
	iii)配置参与搜索字段
	iv)url提交搜索目标:?search=目标

4)对一个群查接口配置排序功能,如何实现,前台如何请求
	i)导入排序组件
	ii)视图类配置排序组件
	iii)配置允许排序字段
	iv)url提交排序规则:?ordering=price | ?ordering=price,-id
	
5)自定义过滤器
	i)自定义类,实现filter_queryset(self, request, queryset, view)
	ii)从request中拿到前台提交的拼接参数的过滤数据
	iii)过滤条件配置可以从view中反射得到(从配置中拿,或在类中写死)
	iv)返回过滤后的queryset,如果过滤失败,原样访问queryset
"""

Django-filter插件

安装

>: pip install django-filter

分类过滤

"""
方式一
1)在视图文件views.py中导入django-filter的功能组件
from django_filters.rest_framework import DjangoFilterBackend

2)将搜索组件配置给群查接口视图类的filter_backends
filter_backends = [DjangoFilterBackend]

3)配置视图类关联的Model表可以分类的字段(通常是可以分组的字段)
filter_fields = ['course_category']

4)前台访问该群查接口,采用拼接参数方式用分类course_category字段将分类条件提供给后台
http://127.0.0.1:8000/course/free/?course_category=1  # 拿课程分类1下的所有课程


方式二
1)自定义过滤类继承django-filter插件的FilterSet类,绑定Model表,并设置分类字段
from django_filters.filterset import FilterSet
from . import models
class CourseFilterSet(FilterSet):
    class Meta:
        model = models.Course
        fields = ['course_category']

2)在视图文件views.py中导入django-filter的功能组件及自定义的过滤类
from django_filters.rest_framework import DjangoFilterBackend
from .filters import CourseFilterSet
        
3)将搜索组件配置给群查接口视图类的filter_backends
filter_backends = [DjangoFilterBackend]

4)配置视图类关联的自定义过滤类
filter_class = CourseFilterSet

5)前台访问该群查接口,采用拼接参数方式用分类course_category字段将分类条件提供给后台
http://127.0.0.1:8000/course/free/?course_category=1  # 拿课程分类1下的所有课程
"""

区间过滤

"""
1)自定义过滤类继承django-filter插件的FilterSet类,绑定Model表,并设置自定义区间规则字段
from django_filters.filterset import FilterSet
from . import models
class CourseFilterSet(FilterSet):
    # 区间过滤:field_name关联的Model字段;lookup_expr设置规则;gt是大于,gte是大于等于;
    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 = ['min_price', 'max_price']

2)在视图文件views.py中导入django-filter的功能组件及自定义的过滤类
from django_filters.rest_framework import DjangoFilterBackend
from .filters import CourseFilterSet
        
3)将搜索组件配置给群查接口视图类的filter_backends
filter_backends = [DjangoFilterBackend]

4)配置视图类关联的自定义过滤类
filter_class = CourseFilterSet

5)前台访问该群查接口,采用拼接参数方式用自定义区间规则字段将区间条件提供给后台
http://127.0.0.1:8000/course/free/?min_price=30&max_price=60  # 拿课程价格在30~60的所有课程
"""

十、内容大纲

支付

(课件夹里有资料)

上线

(课件夹里有资料)

原文地址:https://www.cnblogs.com/WQ577098649/p/12633740.html