luffy项目

luffy项目

luffy介绍

暂不介绍

项目搭建

1.虚拟环境

虚拟环境的创建和使用可以参考 项目搭建之虚拟环境,其内部所写的是python3.3版本后内置的venv功能搭建虚拟环境,而如果需要搭建python2的虚拟环境则不得不用virtualenv第三方模块。

virtualenv安装和初始化

安装virtualenv:

pip3 install virtualenv    # 虚拟环境是它
pip3 install virtualenvwrapper-win # 对虚拟环境的加强,以后只需要简单的命令就可以使用和创建虚拟环境

配置虚拟环境管理器的工作目录:

  • 在环境变量中新建:WORKON_HOME: D:\Virtualenvs

  • 在D盘创建该文件夹,通过命令创建的虚拟环境,都会在这个文件夹下

  • 在python解释器安装目录的scripts目录中找到virtualenvwrapper.bat执行一下

virtualenv创建和使用虚拟环境

创建并自动进入虚拟环境:

ps:mkvirtualenv.bat已经在python的scripts中,因为配置过环境变量,所以随意位置可执行

mkvirtualenv -p python luffy    # 创建了一个以python解释器为准的纯净环境,环境名称luffy

退出虚拟环境

ps:deactive.bat在虚拟环境的scripts文件夹中,在虚拟环境中可以执行

deactive

查看和进入虚拟环境

ps:workon.bat在python的scripts中在win-cmd中任意位置可执行

workon     # 查看所有虚拟环境名
workon luffy    # 进入虚拟环境luffy

删除虚拟环境

rmvirtualenv

2.后端创建

在虚拟环境创建好后,可以引用虚拟环境创建django项目,注意因为虚拟环境初始是纯净的,没有按照各模块,所以需要记得提前在虚拟环境中安装django,并选择当前流行版本如3.2.2。

基于django项目调整目录

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

相较于django本身的项目目录,主要的变化有:

  1. 项目同名文件夹被作为项目主应用,所有可改动的代码都往里塞
  2. 配置文件修改成settings文件夹,存放不同应用环境的配置文件(测试环境和上线环境等)
  3. utils.py修改成utils包进一步细分写公共函数类库
  4. 注册软件从与主文件夹同级调整至主文件夹下的apps中
  5. 软件通过manage工具创建,注册时可能遇到注册名的问题,所以为了方便,需要在settings中配置下环境变量:将BASE_DIR和BASE_DIR'apps'的字符串格式加入sys.path

django配置文件

settings下的dev文件一般表示开发环境下的配置,以下是对django的一些配置的解读:

点击查看代码
from pathlib import Path
import os
import sys

# 项目根路径
BASE_DIR = Path(__file__).resolve().parent.parent  

# 把 apps 路径加入到环境变量
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
# 把BASE_DIR也加入到环境变量,以后直接从小路飞开始导起即可
sys.path.insert(0, str(BASE_DIR))

# 从大路飞开始导起(执行文件所在文件夹)
# 或者小路飞开始导起或者apps开始导起都可以(添加到环境变量)


# 秘钥,涉及到加密的django中
SECRET_KEY = 'django-insecure-!g(8l%fw_#t$pz$x4jdf#e3$b4+c%xzqyq@3zki08vj&i)z4k-'

# 项目是以debug模式运行,还是非debug模式运行

# 项目上线,要改成false
# 调试模式下,对开发者更友好:可以列出所有路径.报了错,前端能看到,但不适合给用户看到
DEBUG = False
# 允许我的项目部署在哪个ip地址上,* 表示允许部署在所有地址上
ALLOWED_HOSTS = ['*']
# 当DEBUG=False时必须指定ALLOWED_HOSTS



# django 是多个app组成的,里面配置app,默认带的app,django内置的app
# django 是一个大而全的框架,有很多内置app:
"""
admin后台管理,
auth权限管理,
contenttypes表中存app也表的关系,
sessions session表,django的session相关
messages:消息框架,flask讲闪现,是一样的东西
staticfiles:静态资源的
"""
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
	# 因为添加了apps环境变量,所以可以这样注册
    'home',
    'user'
]


# 中间件
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  # 安全相关中间件
    'django.contrib.sessions.middleware.SessionMiddleware', # session相关中间件
    'django.middleware.common.CommonMiddleware',            # 带不带 / 问题
    'django.middleware.csrf.CsrfViewMiddleware',            # csrf 认证,生成csrf串
    'django.contrib.auth.middleware.AuthenticationMiddleware', # 用户认证
    'django.contrib.messages.middleware.MessageMiddleware',  #消息框架相关
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


# 根路由
ROOT_URLCONF = 'luffy_api.urls'

# 模板文件(前后端分离的项目用不上)
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],  
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]


# 项目运行的配置---》项目上线运行,使用uwsgi 运行  application()
WSGI_APPLICATION = 'luffy_api.wsgi.application'

# 存在环境变量中,代码中不能直接看到
name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123?')
# 拓展:有的公司,直接有个配置中心---》服务--》只用来存放配置文件


# 数据库配置,mysql 主从搭建完,读写分离
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'USER': name,
        'PASSWORD': password,
        'HOST': '127.0.0.1',
        'PORT': 3306
    },
}

# 忽略掉
AUTH_PASSWORD_VALIDATORS = [...]


# 国际化
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False


# 静态资源
STATIC_URL = '/static/'

# 建表自动都会创建一个自增字段
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

开启media访问

django项目中有个相对固定的配置方式

配置文件:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

路由:

from django.views.static import serve
# settings.MEDIA_ROOT虽然是在settings/dev.py中配置的
# 但是所有的配置都会被最终合并到conf的settings中
from django.conf import settings

path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT})

后台日志封装

这里采取logging模块的日志记录方式,在工作环境中,也许会有专门管理日志的服务去统一管理日志

如:sentry,是由django写的服务,目前开源。

日志组成可以参考 内置模块之logging模块--日志模块

日志settings配置:

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': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            # 实际开发建议使用ERROR
            'level': 'WARNING',
            'class': 'logging.handlers.RotatingFileHandler',
            # 日志位置,日志文件名,日志保存目录必须手动创建,
            # 注:这里的文件路径要注意BASE_DIR代表的是小luffyapi
            # 小luffyapi的dirname是大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,  # 是否让日志信息继续冒泡给其他的日志处理系统
        },
    }
}

产生日志记录者:

# 在utils新建 common_logger.py ,得到日志对象
import logging
logger = logging.getLogger('django')

使用日志:

from utils import logger  # utils包需要在__init__中导入logger以便这样调用
logger.info('info级别的日志')
logger.error('error级别的日志')

全局异常处理封装

在drf中,在APIView的视图类中,会进行三大认证+视图的全局异常捕获,我们可以对全局异常处理的函数进行改写和配置。

原处理异常函数可以处理drf异常,而我们在此基础上进行扩展,使他能处理更多的异常,并且在处理异常时,可以记录日志。

# 按照以下位置写函数和配置
REST_FRAMEWORK = {
	'EXCEPTION_HANDLER': 'utils.common_exceptions.exception_handler',
}

处理函数异常的参考:

from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
from utils import logger


def exception_handler(exc, context):
    request = context.get('request')
    view = context.get('view')
    response = drf_exception_handler(exc, context)
    if response is None:
        # 说明drf没能处理异常
        logger.error(f'用户【{request.user}】|ip【{request.META.get("REMOTE_ADDR")}】'
                     f'|视图【{view}】|错误【{exc}】')
        return Response({'code': 888, 'msg': '服务器错误'})
	logger.info(f'用户【{request.user}】|ip【{request.META.get("REMOTE_ADDR")}】'
                f'|视图【{view}】|错误【{exc}】')
    return response

响应格式封装

Response没有地方可以替换,但是我们可以继承它的Response类再写一个响应类,来帮助更方便的产生固定格式的响应类。

我们希望达到如下的效果:

  • 默认有{code:100,msg:成功}这两个字段打头

  • 如果有其他状态码则按code,msg传即可

  • 原本data={返回对象}会被组织成{code:100,msg:成功,data:{..}}

  • 如APIResponse(name='leethon')传参,则前端会收到

    {code:100,msg:成功,name:leethon}的响应体。

from rest_framework.response import Response


class APIResponse(Response):
    # 原Response有什么,我们就初始化什么
    def __init__(self, code=100, msg='成功', status=None,
                 template_name=None, headers=None,
                 exception=False, content_type=None, **kwargs):
        data = {'code': code, 'msg': msg}
        # 如果有多余的参数,一律塞到data字典中
        if kwargs:
            data.update(kwargs)
        # 派生老Response的初始化
        super().__init__(data=data, status=status,
                         template_name=template_name, headers=headers,
                         exception=exception, content_type=content_type)

3.后端数据库建立

创建luffy数据库

对于一般程序员来说,开发人员有专门的用户,用户对于数据库的权限是有限的,那么我们可以在创建库后,再创建一个用户来模拟这种情况:

root登录mysql:

# 创建数据库
create database luffy;
# 查看mysql用户信息
select user,host from mysql.user;
# 将库的权限给一个新的mysql用户
grant 权限(create, update) on 库.表 to '账号'@'host' identified by '密码'
# 将库的所有权限给一个所有地址登录的luffy用户密码为luffy123?
grant all privileges on luffy.* to 'luffy'@'%' identified by 'Luffy123?';
# 刷新权限到硬盘
flush privileges;

以上:就创建了一个用户只对luffy库有增删改查所有权限

用户:luffy

密码:Luffy123?

项目连接库

这里后端是用django写的,原本Python2中可以使用mysqlDB连接mysql,在使用python3后,mysqlDB不能使用了,但django仍然默认连接mysqlDB,需要一个猴子补丁将其替换成可以使用的程序。则需要下载mysqlclient模块。

pip install mysqlclient

修改settings配置:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'luffy',
        'USER': 'luffy',
        'PASSWORD': 'Luffy123?',
        'HOST': '127.0.0.1',
        'PORT': 3306
    }
}

这里的user和password配置仅为测试环境dev会用到的配置,而实际上线的库可能都是不一样的,所以暂不必担心代码泄露密码问题。

不过我们也可以在系统环境变量中存放用户名和密码等信息,用os模块获取来模拟这种情况:

name = os.environ.get('LUFFY_NAME', 'luffy')
password = os.environ.get('LUFFY_PASSWORD', 'Luffy123?')

创建用户表

class User(AbstractUser):
    # 扩写手机号和头像字段
    mobile = models.CharField(max_length=11, unique=True)
    # 需要pillow包的支持
    icon = models.ImageField(upload_to='icon', default='icon/default.png')

    class Meta:
        # 数据库中的表名
        db_table = 'luffy_user'
        verbose_name = '用户表'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

4.前端搭建

全局css和全局配置文件

asserts文件夹就是用来存放一些项目所用的img、js、css的

  1. 全局css

    /*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; /* 合并边框 */
    }
    

    用于取消页面标签各种默认的样式,如ul的小点,a标签的下划线。

  2. 全局js

    书写一些配置,并导出

    // settings.js
    export default {
        BASE_URL: 'http://127.0.0.1:8001/api/v1'
    }
    

    在main.js中导入调用设置为Vue的全局变量:

    import settings from "@/assets/js/settings";
    
    Vue.prototype.$settings = settings
    

安装一些前端模块依赖

一些前端模块十分的常用,很多组件都会用到,可以将其添加到全局变量中。

  • axios模块 cnpm i axios -S

    import axios from "axios";
    Vue.prototype.$axios = axios
    
  • vue-cookies模块 cnpm i vue-cookies -S

    import cookies from "vue-cookies"
    Vue.prototype.$cookies = cookies
    
  • elementui cnpm install element-ui -S

    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    Vue.use(ElementUI)
    

项目启动

1.主页设计

由原型图分析接口

e9fd70b6efa8e27d6c2912b64630dd0

在上图中我们首先可以看到轮播图,这个轮播图是需要一个接口提供的,所以后端需要开一个轮播图接口。

由接口分析表模型

轮播图接口,主要就是查看所有list。

而数据方面需要提供轮播图数据,轮播图的特征有:

  • 图片资源

  • 排序优先级(这个很多地方可能用到,通过不同方式排序)

    所以我们可以建一个父表模型在utils中以便继承调用。

    • 创建时间
    • 更新时间
    • 是否删除
    • 是否上架
    • 优先级
  • 跳转链接

  • 图的标题和详情

建立表模型:

# utils.common_models.py
from django.db import models

class BaseBanner(models.Model):
    created_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    updated_time = models.DateTimeField(verbose_name='更新时间', auto_now=True)
    is_delete = models.BooleanField(verbose_name='是否删除')
    is_show = models.BooleanField(verbose_name='是否上架')
    order_rank = models.IntegerField(verbose_name='优先级')

    class Meta:
        # 不建表,专门用于给其他类继承
        abstract = True

        
# home.models.py
from django.db import models
from utils import BaseBanner

class Banner(BaseBanner):
    title = models.CharField(verbose_name='标题', max_length=16)
    detail = models.TextField(verbose_name='详情')
    link = models.CharField(verbose_name='链接', max_length=64)
    image = models.ImageField(verbose_name='图片', upload_to='banner')

接口单独列项目的场景:

有时一个项目有限时活动,需要开设一些后端接口来配合这个应用,但是不一定要在原本的项目中开设接口,可以重新开一个小项目(足够轻量化可以使用flask),当活动开启时启动小项目,活动结束时关闭小项目即可。

这里仅是分享一个小的思路,即后端的接口可以部署在多个项目上。

接口视图编写思路

针对轮播图接口,目前已经建立好了表,并使用django-admin后台管理添加了一些数据,我们只需要按照一定顺序排序,将轮播图的数据序列化返回出去即可。

# 视图函数
class BannerView(GenericViewSet, ListModelMixin):
    # 数据是通过筛选的未删除并上架按照优先级排序的queryset
    queryset = Banner.objects.filter(is_delete=False, is_show=True).order_by('order_rank')
    serializer_class = BannerSerializers

    # 查看所有接口,将响应转为统一格式的响应
    def list(self, request, *args, **kwargs):
        response = super().list(request, *args, **kwargs)
        return APIResponse(results=response.data)
    
# 序列化类
class BannerSerializers(serializers.ModelSerializer):
    class Meta:
        model = Banner
        fields = ['id', 'title', 'detail', 'link', 'image']

一般情况下的视图可拓展思路:

  1. get_serializer(self)

  2. get_queryset(self)

  3. perform_update(self, serializer)

  4. perform_destroy(self, instance)

  5. perform_create(self, serializer)

  6. utils封装常见工具,如:

    可以封装基于APIResponse的五个视图扩展类

    # 这里封装一个演示 
    class MyListMixin(mixins.ListModelMixin):
        def list(self, request, *args, **kwargs):
            response = super(MyListMixin, self).list(self, request, *args, **kwargs)
            return APIResponse(results=response.data)
    
    

2.前端实现轮播图

前端首页界面除了轮播图还有一些其他界面,只不过由于轮播图接口写好了,所以可以结合后端写。

首页还有页头页脚等其他组件,我们都可以认为这些事页面内组件,放入components文件夹,而首页组件属于页面组件,归为view文件夹更为合适。

页头组件

<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: route === '/free-course'}">免费课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/actual-course')" :class="{active: route === '/actual-course'}">实战课</span>
        </li>
        <li class="ele">
          <span @click="goPage('/light-course')" :class="{active: route === '/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 {
      route: '',
    }
  },
  methods: {
    goPage(route) {
      this.route = route
      this.$router.push(route)
    }
  }

}
</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>

页尾组件

<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: "Foot"
}
</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>

组件统合至页面组件

<template>
  <div class="home">
    <Header></Header>
    <Banner></Banner>

    <Foot></Foot>
  </div>
</template>

<script>
import Header from "@/components/Header";
import Banner from "@/components/Banner";
import Foot from "@/components/Foot";

export default {
  name: 'HomeView',

  components: {Header, Banner, Foot}
}
</script>

<style scoped>

</style>
  • 导入组件
  • 注册组件(components中)
  • 模板中引用组件(标签形式)

3.登录注册接口

效果图及需求分析

image

有两种登录方式

  • 密码+账号的登录,账号可以是用户名、手机号、邮箱-->多方式登录接口
  • 短信验证码的登录,账号必须输入手机号-->短信登录接口
  • 发送验证码-->发送验证码的接口(需要借助第三方发短信)

image

注册只要求手机号:

  • 注册账号接口
  • 发送验证码接口
  • 手机号可能不存在-->校验手机号是否存在的接口(可能要借助第三方,也可以简单的正则匹配)

思考:登录接口是否需要先认证手机号是否存在,大概过一下逻辑,如果手机号不存在,那么注册时就不应该存入用户表,所以不需要。

多种方式登录接口

多种方式登录接口分析:

  • 需要提交数据,post请求
  • 数据校验,校验成功返回user和token给前端,校验失败返回错误信息
  • 校验逻辑可以写在序列化类中,让is_valid去完成
# GenericViewSet
@action(methods=['POST'], detail=False)
def login_mul(self, request):
    ser = LoginMulSerializer(data=request.data)
    ser.is_valid(raise_exception=True)  # 校验错误直接报错交由异常捕获处理
    return APIResponse(
        token=ser.context.get('token'),
        refresh=ser.context.get('refresh')
    )
    
# Serializer
class LoginMulSerializer(BaseLoginSerializer):
    input_user = serializers.CharField()
    password = serializers.CharField()
    
    def validate(self, attrs):
        # 全局钩子的校验步骤分步进行,在后续会有扩展性改动
        self._get_user(attrs)
        self._get_token()
        return attrs

    def _get_user(self, attrs):
        input_user = attrs.get('input_user')
        password = attrs.get('password')
        user = User.objects.filter(Q(username=input_user) | 
                                   Q(email=input_user) | Q(mobile=input_user)).first()
        if user and user.check_password(password):
            self.context['user'] = user
        else:
            raise MsgException('用户名或者密码输入错误')

    def _get_token(self):
        refresh = RefreshToken.for_user(self.context.get('user'))
        self.context['refresh'] = str(refresh)
        self.context['token'] = str(refresh.access_token)

ps:在序列化类中,context属性会被存到序列化对象中,这个可以用于多个方法中的通信,也可以用于序列化类与视图类方法之间的通信

发送验证码接口

我们需要借助第三方或者公司内部的发短信接口,这里使用了腾讯云的SDK短信接口。

逻辑上,我们需要做:

  1. 判断手机用户是否存在于User表(登录验证码才需要,注册的发送验证码不需要)

  2. 发送短信验证码

  3. 验证码存储到缓存以便验证

def send_auth_code(self, request):
    mobile = request.query_params.get('mobile')
    user = self.get_queryset().filter(mobile=mobile).first()
    if not user:
        return APIResponse(code=444, msg='手机号未在本系统注册')
    # 发送验证码和存储缓存的逻辑都写在了验证码类中
    code_obj = AuthCode(mobile=mobile, code_digit=4, expired_min=1)
    send_results = code_obj.send_code()
    return APIResponse(expired_time='1分钟', sendStatusSet=send_results.get('SendStatusSet'))

# 存储到缓存,django自带缓存
from django.core.cache import cache
cache.set(f"auth_code{self.mobile}", code)

短信验证码登录功能

逻辑上,两种登录功能只有校验逻辑上的细微差别,而签发token的过程一致(通过user签发token)

那么分叉就出现了,在get_user上。

于是我们可以按以下书写序列化类:

class BaseLoginSerializer(serializers.Serializer):
    def validate(self, attrs):
        self._get_user(attrs)
        self._get_token()
        return attrs

    def _get_user(self, attrs):
        raise Exception('你必须重写_get_user方法')

    def _get_token(self):
        refresh = RefreshToken.for_user(self.context.get('user'))
        self.context['refresh'] = str(refresh)
        self.context['token'] = str(refresh.access_token)


class LoginMulSerializer(BaseLoginSerializer):
    input_user = serializers.CharField()
    password = serializers.CharField()

    def _get_user(self, attrs):
        input_user = attrs.get('input_user')
        password = attrs.get('password')
        user = User.objects.filter(Q(username=input_user) | Q(email=input_user) | Q(mobile=input_user)).first()
        if user and user.check_password(password):
            self.context['user'] = user
        else:
            raise MsgException('用户名或者密码输入错误')


class LoginSacSerializer(BaseLoginSerializer):
    code = serializers.CharField()
    mobile = serializers.CharField()

    def _get_user(self, attrs):
        code = attrs.get('code')
        mobile = attrs.get('mobile')
        auth_code = cache.get(f"auth_code{mobile}", None)
        if not code == auth_code:
            raise MsgException('验证码无效')
        user = User.objects.filter(mobile=mobile).first()
        # if not user:
        #     raise MsgException('你小子,跟刚才输入的号码不一样哦')
        self.context['user'] = user

将两个序列化类的validate方法和_get_token方法抽象出来,得到一个父类BaseLoginSerializer,而其中又必须配合一个_get_user方法,需要在子类中书写形成一个拥有完整校验逻辑的序列化类。

而视图函数,也可以进行一定程度的合并:

@action(methods=['POST'], detail=False)
def login_sac(self, request):
    self.serializer_class = LoginSacSerializer
    return self._login(request)

@action(methods=['POST'], detail=False)
def login_mul(self, request):
    # 在函数体中分别配置序列化类,最终会被get_serializer捕捉到
    self.serializer_class = LoginMulSerializer
    return self._login(request)
	"""原视图代码↓↓"""
    # ser = LoginMulSerializer(data=request.data)
    # ser.is_valid(raise_exception=True)  # 校验错误直接报错交由异常捕获处理
    # return APIResponse(
    #     token=ser.context.get('token'),
    #     refresh=ser.context.get('refresh')
    # )

def _login(self, request):
    """两登录接口的代码一致,只是校验的序列化类不一样"""
    ser = self.get_serializer(data=request.data)
    ser.is_valid(raise_exception=True)  # 校验错误直接报错交由异常捕获处理
    return APIResponse(
        token=ser.context.get('token'),
        refresh=ser.context.get('refresh')
    )

短信验证码注册接口

注册接口也需要验证码校验,需要发送验证码和校验验证码,校验的逻辑与短信登录接口类似,但是不同的是,注册接口在校验后需要反序列化写入数据库。

对于User表的写入要注意,密码字段要通过密文的形式写入,所以用create_user的方法。

class RegisterSerializer(serializers.ModelSerializer):
    code = serializers.CharField(write_only=True)  # 为什么code要有write_only属性

    class Meta:
        model = User
        fields = ['mobile', 'password', 'code']
        extra_kwargs = {
            "password": {"write_only": True}
        }

    def validate(self, attrs):
        code = attrs.pop('code')  # 获取code进行校验,同时从attrs中剔除
        mobile = attrs.get('mobile')
        auth_code = cache.get(f"auth_code{mobile}", None)
        if not (code == auth_code or code == "8888"):
            raise MsgException('验证码无效')
        attrs['username'] = '新用户' + str(uuid4())
        # attrs最终传入以下的validated_data中,与最初的request.data相比,多了username,少了code
        return attrs	

    def create(self, validated_data):
        # 写入表格,创建用户,username是必填且unique字段,用uuid生成了一个用户名
        user = User.objects.create_user(**validated_data)  # 创建的是密文
        return user

为什么code要有write_only属性?

在createModelMixin重写的create方法中,我们除了执行了反序列化校验,和序列化.save之外,还将最终创建的数据对象序列化返回出去了(restful规范),那么在序列化类时,code如果被计入,就会因为数据对象中没有code字段而导致报错,所以用write_only取消code字段的序列化即可。

posted @ 2023-02-28 15:45  leethon  阅读(32)  评论(0编辑  收藏  举报