登录、注册、页头页面 手机号验证接口 发送短信接口 注册接口 多方式登录接口 手机号登录接口 发送短信接口频率限制 异常处理修订 接口缓存
登录页面(Login.vue):
<template> <div class="login box"> <img src="@/assets/img/Loginbg.jpg" alt=""> <div class="login"> <div class="login-title"> <img src="@/assets/img/Logotitle.png" alt=""> <p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> </div> <div class="login_box"> <div class="title"> <span :class="{active: a0}" @click="changeLogin(0)">密码登录</span> <span :class="{active: a1}" @click="changeLogin(1)">短信登录</span> </div> <div class="inp" v-if="login_type===0"> <input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model="password" type="password" name="" class="pwd" placeholder="密码"> <div id="geetest1"></div> <div class="rember"> <p> <input id="checkbox" type="checkbox" class="no" v-model="remember"/> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn" @click="loginAction">登录</button> <p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p> </div> <div class="inp" v-show="login_type===1"> <input v-model="mobile" type="text" placeholder="手机号码" class="user"> <div class="sms"> <input v-model="sms" type="text" placeholder="输入验证码" class="user"> <span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span> </div> <button class="login_btn" @click="loginMobile">登录</button> <p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data() { return { a0: 1, a1: 0, login_type: 0, username: "", password: "", remember: false, mobile: "", sms: "", is_send: false, // 是否在60s内发送了短信 sms_interval_tips: "获取验证码", } }, methods: { changeLogin(i) { this.login_type = i; if (i) { this.a0 = 0; this.a1 = 1; } else { this.a0 = 1; this.a1 = 0; } }, loginAction() { if (!this.username || !this.password) { return } this.$axios({ url: this.$settings.base_url + '/user/login/', method: 'post', data: { 'usr': this.username, 'pwd': this.password } }).then((response) => { // 判断用户是否要记住密码 // window.console.log(">>>>", response.data); if (this.remember) { // 记住密码 sessionStorage.clear(); localStorage.token = response.data.token; localStorage.user_name = response.data.results.username; localStorage.user_mobile = response.data.results.mobile; } else { /// 没记住密码 localStorage.clear(); sessionStorage.token = response.data.token; sessionStorage.user_name = response.data.results.username; sessionStorage.user_mobile = response.data.results.mobile; } // 页面跳转 this.$alert("欢迎回来!", "登录成功!", { confirmButtonText: '确定', callback: () => { // 跳转页面 // this.$router.go(-1); // 返回上一页 // 进行制定的网站内部地址跳转 // this.$router.push("站内地址"); this.$router.push("/"); // 前往主页 } }) }).catch(() => { this.$alert("检查账号密码!", "登录失败!", { confirmButtonText: '确定', callback: () => { this.username = ''; this.password = ''; } }); }) }, send_sms() { // 发送短信 if (!/^1[3-9]\d{9}$/.test(this.mobile)) { this.$message({ message: "对不起!手机号码格式有误!" }); return false; } // 判断是否在60s内发送过短信 if (this.is_send) { this.$message({ message: "对不起,不能频繁发送短信验证!" }); return false; } // 请求发送短信 this.$axios({ url: this.$settings.base_url + '/user/sms/', method: 'post', data: { mobile: this.mobile } }).then(response => { this.$message({ message: response.data.msg, }); }).catch(error => { this.$message({ message: error.response.data.result, }) }); // 修改短信的发送状态 this.is_send = true; // 设置间隔时间60s let sms_interval_time = 60; // 设置短信发送间隔倒计时,.60s后把is_send改成false let timer = setInterval(() => { if (sms_interval_time <= 1) { clearInterval(timer); this.sms_interval_tips = "获取验证码"; this.is_send = false; // 重新回复点击发送功能的条件 } else { sms_interval_time -= 1; this.sms_interval_tips = `${sms_interval_time}秒后再次获取`; } }, 1000); }, loginMobile() { // 注册信息提交 if (!/^1[3-9]\d{9}$/.test(this.mobile)) { this.$message({ message: "对不起!手机号码格式有误!" }); return false; } if (this.sms.length < 1) { this.$message({ message: "短信验证码不能为空!" }); return false; } this.$axios({ url: this.$settings.base_url + '/user/login/mobile/', method: 'post', data: { mobile: this.mobile, code: this.sms } }).then(response => { let _this = this; let status = response.data.status; let msg = response.data.msg; _this.$message({ message: msg, duration: 1500, onClose() { if (status === 0) { // 保存登录状态 sessionStorage.token = response.data.token; sessionStorage.user_name = response.data.results.username; sessionStorage.user_mobile = response.data.results.mobile; // 跳转到主页 _this.$router.push('/'); } else { // 清空数据库 sessionStorage.clear(); // 清空输入框 _this.mobile = ''; _this.sms = ''; } } }); }).catch(error => { this.$message({ message: error.response.data.result }); }) }, }, }; </script> <style scoped> .box { width: 100%; height: 100%; position: relative; overflow: hidden; } .box img { width: 100%; min-height: 100%; } .box .login { position: absolute; width: 500px; height: 400px; left: 0; margin: auto; right: 0; bottom: 0; top: -338px; } .login .login-title { width: 100%; text-align: center; padding-top: 20px; } .login-title img { width: 190px; height: auto; } .login-title p { font-family: PingFangSC-Regular; font-size: 18px; color: #fff; letter-spacing: .29px; padding-top: 10px; padding-bottom: 50px; } .login_box { width: 400px; height: auto; background: #fff; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); border-radius: 4px; margin: 0 auto; padding-bottom: 40px; } .login_box .title { font-size: 20px; color: #9b9b9b; letter-spacing: .32px; border-bottom: 1px solid #e6e6e6; display: flex; justify-content: space-around; padding: 50px 60px 0 60px; margin-bottom: 20px; cursor: pointer; } .login_box .title span.active { color: #4a4a4a; border-bottom: 2px solid #84cc39; } .inp { width: 350px; margin: 0 auto; } .inp input { outline: 0; width: 100%; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp input.user { margin-bottom: 16px; } .inp .rember { display: flex; justify-content: space-between; align-items: center; position: relative; margin-top: 10px; } .inp .rember p:first-of-type { font-size: 12px; color: #4a4a4a; letter-spacing: .19px; margin-left: 22px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; /*position: relative;*/ } .inp .rember p:nth-of-type(2) { font-size: 14px; color: #9b9b9b; letter-spacing: .19px; cursor: pointer; } .inp .rember input { outline: 0; width: 30px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp .rember p span { display: inline-block; font-size: 12px; width: 100px; /*position: absolute;*/ /*left: 20px;*/ } #geetest { margin-top: 20px; } .login_btn { width: 100%; height: 45px; background: #84cc39; border-radius: 5px; font-size: 16px; color: #fff; letter-spacing: .26px; margin-top: 30px; } .inp .go_login { text-align: center; font-size: 14px; color: #9b9b9b; letter-spacing: .26px; padding-top: 20px; } .inp .go_login a { color: #84cc39; cursor: pointer; } #get_code { border: 0; width: 120px; height: 30px; background-color: antiquewhite; outline: none; } #get_code:active { color: white; } #checkbox { width: 20px; height: 20px; } .sms { position: relative; } .sms .sms_btn { position: absolute; top: -12px; right: 0; bottom: 0; margin: auto; width: 130px; text-align: center; height: 24px; color: #ff7000; cursor: pointer; border-left: 1px solid #999; } </style>
注册页面(Register.vue):
<template> <div class="box"> <img src="@/assets/img/Loginbg.jpg" alt=""> <div class="register"> <div class="register_box"> <div class="register-title">注册路飞学城</div> <div class="inp"> <input v-model="mobile" @blur="checkMobile" type="text" placeholder="手机号码" class="user"> <input v-model="password" type="password" placeholder="用户密码" class="user"> <div class="sms"> <input v-model="sms" type="text" placeholder="输入验证码" class="user"> <span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span> </div> <div id="geetest"></div> <button class="register_btn" @click="registerMobile">注册</button> <p class="go_login">已有账号 <router-link to="/login">直接登录</router-link> </p> </div> </div> </div> </div> </template> <script> export default { name: 'Register', data() { return { sms: "", mobile: "", password: "", is_send: false, // 是否在60s内发送了短信 sms_interval_tips: "获取验证码", } }, created() { }, methods: { checkMobile() { // 手机框必须填内容 if (this.mobile.length < 1) { return false; } // 手机号码格式是否正确 // js正则语法糖 /正则语法/ if (!/^1[3-9]\d{9}$/.test(this.mobile)) { this.$message({ message: "对不起!手机号码格式有误!" }); return false; } // 验证手机号码是否已经注册了 // this.$axios.get(this.$settings.base_url+"/user/mobile/?mobile="+this.mobile+"/"); this.$axios({ url: this.$settings.base_url + '/user/mobile/', method: 'post', data: { mobile: this.mobile } }).then(response => { let data = response.data; // window.console.log(data); if (data.status !== 0) { this.$message({ message: "对不起!手机号码已经被注册!", type: 'warning' }); return false; } else { this.$message({ message: "期待您加入我们!" }); } }).catch(error => { let data = error.response.data; this.$message({ message: data.message }) }) }, send_sms() { // 发送短信 if (!/^1[3-9]\d{9}$/.test(this.mobile)) { this.$message({ message: "对不起!手机号码格式有误!" }); return false; } // 判断是否在60s内发送过短信 if (this.is_send) { this.$message({ message: "对不起,不能频繁发送短信验证!" }); return false; } // 请求发送短信 this.$axios({ url: this.$settings.base_url + '/user/sms/', method: 'post', data: { mobile: this.mobile } }).then(response => { this.$message({ message: response.data.msg, }); }).catch(error => { this.$message({ message: error.response.data.result, }) }); // 修改短信的发送状态 this.is_send = true; // 设置间隔时间60s let sms_interval_time = 60; // 设置短信发送间隔倒计时,.60s后把is_send改成false let timer = setInterval(() => { if (sms_interval_time <= 1) { clearInterval(timer); this.sms_interval_tips = "获取验证码"; this.is_send = false; // 重新回复点击发送功能的条件 } else { sms_interval_time -= 1; this.sms_interval_tips = `${sms_interval_time}秒后再次获取`; } }, 1000); }, registerMobile() { // 注册信息提交 if (!/^1[3-9]\d{9}$/.test(this.mobile)) { this.$message({ message: "对不起!手机号码格式有误!" }); return false; } if (this.sms.length < 1) { this.$message({ message: "短信验证码不能为空!" }); return false; } if (this.password.length < 6 || this.password.length > 18) { this.$message({ message: "对不起,密码长度必须在6-16个字符之间!" }); return false; } this.$axios({ url: this.$settings.base_url + '/user/register/', method: 'post', data: { mobile: this.mobile, password: this.password, code: this.sms } }).then(response => { window.console.log(response); // let _this = this; let status = response.data.status; this.$message({ message: '注册成功', duration: 1500, onClose: () => { if (status === 0) { // 跳转到主页 this.$router.push('/') } } }); }).catch(error => { window.console.log(error) // this.$message({ // message: error.response.data.result // }); }) } }, }; </script> <style scoped> .box { width: 100%; height: 100%; position: relative; overflow: hidden; } .box img { width: 100%; min-height: 100%; } .box .register { position: absolute; width: 500px; height: 400px; left: 0; margin: auto; right: 0; bottom: 0; top: -238px; } .register .register-title { width: 100%; font-size: 24px; text-align: center; padding-top: 30px; padding-bottom: 30px; color: #4a4a4a; letter-spacing: .39px; } .register-title img { width: 190px; height: auto; } .register-title p { font-size: 18px; color: #fff; letter-spacing: .29px; padding-top: 10px; padding-bottom: 50px; } .register_box { width: 400px; height: auto; background: #fff; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); border-radius: 4px; margin: 0 auto; padding-bottom: 40px; } .register_box .title { font-size: 20px; color: #9b9b9b; letter-spacing: .32px; border-bottom: 1px solid #e6e6e6; display: flex; justify-content: space-around; padding: 50px 60px 0 60px; margin-bottom: 20px; cursor: pointer; } .register_box .title span:nth-of-type(1) { color: #4a4a4a; border-bottom: 2px solid #84cc39; } .inp { width: 350px; margin: 0 auto; } .inp input { outline: 0; width: 100%; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp input.user { margin-bottom: 16px; } .inp .rember { display: flex; justify-content: space-between; align-items: center; position: relative; margin-top: 10px; } .inp .rember p:first-of-type { font-size: 12px; color: #4a4a4a; letter-spacing: .19px; margin-left: 22px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; /*position: relative;*/ } .inp .rember p:nth-of-type(2) { font-size: 14px; color: #9b9b9b; letter-spacing: .19px; cursor: pointer; } .inp .rember input { outline: 0; width: 30px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp .rember p span { display: inline-block; font-size: 12px; width: 100px; /*position: absolute;*/ /*left: 20px;*/ } #geetest { margin-top: 20px; } .register_btn { width: 100%; height: 45px; background: #84cc39; border-radius: 5px; font-size: 16px; color: #fff; letter-spacing: .26px; margin-top: 30px; } .inp .go_login { text-align: center; font-size: 14px; color: #9b9b9b; letter-spacing: .26px; padding-top: 20px; } .inp .go_login a { color: #84cc39; cursor: pointer; } .sms { position: relative; } .sms .sms_btn { position: absolute; top: -12px; right: 0; bottom: 0; margin: auto; width: 130px; text-align: center; height: 24px; color: #ff7000; cursor: pointer; border-left: 1px solid #999; } </style>
页头(Header.vue):
<template> <div class="header-box"> <div class="header"> <div class="content"> <div class="logo full-left"> <router-link to="/"><img @click="jump('/')" src="@/assets/img/logo.svg" alt=""></router-link> </div> <ul class="nav full-left"> <li><span @click="jump('/course')" :class="this_nav=='/course'?'this':''">免费课</span></li> <li><span @click="jump('/light-course')" :class="this_nav=='/light-course'?'this':''">轻课</span></li> <li><span>学位课</span></li> <li><span>题库</span></li> <li><span>老男孩教育</span></li> </ul> <div class="login-bar full-right"> <div class="shop-cart full-left"> <img src="@/assets/img/cart.svg" alt=""> <span><router-link to="/cart">购物车</router-link></span> </div> <div class="login-box full-left"> <span v-if="!token"> <router-link to="/login">登录</router-link> | <router-link to="/register">注册</router-link> </span> <span v-else> <b>{{ username }}</b> | <i @click="logoutAction">注销</i> </span> </div> </div> </div> </div> </div> </template> <script> export default { name: "Header", data() { return { this_nav: "", token: '', username: '' } }, created() { this.this_nav = localStorage.this_nav; this.token = sessionStorage.token || localStorage.token || ''; this.username = sessionStorage.user_name || localStorage.user_name || ''; }, methods: { jump(location) { localStorage.this_nav = location; // vue-router除了提供router-link标签跳转页面以外,还提供了js跳转的方式 this.$router.push(location); }, logoutAction() { // 清除登录信息 sessionStorage.clear(); localStorage.clear(); this.token = ''; this.username = ''; } } } </script> <style scoped> .header-box { height: 80px; } .header { width: 100%; height: 80px; box-shadow: 0 0.5px 0.5px 0 #c9c9c9; position: fixed; top: 0; left: 0; right: 0; margin: auto; z-index: 99; background: #fff; } .header .content { max-width: 1200px; width: 100%; margin: 0 auto; } .header .content .logo { height: 80px; line-height: 80px; margin-right: 50px; cursor: pointer; /* 设置光标的形状为爪子 */ } .header .content .logo img { vertical-align: middle; } .header .nav li { float: left; height: 80px; line-height: 80px; margin-right: 30px; font-size: 16px; color: #4a4a4a; cursor: pointer; } .header .nav li span { padding-bottom: 16px; padding-left: 5px; padding-right: 5px; } .header .nav li span a { display: inline-block; } .header .nav li .this { color: #4a4a4a; border-bottom: 4px solid #ffc210; } .header .nav li:hover span { color: #000; } .header .login-bar { height: 80px; } .header .login-bar .shop-cart { margin-right: 20px; border-radius: 17px; background: #f7f7f7; cursor: pointer; font-size: 14px; height: 28px; width: 88px; margin-top: 30px; line-height: 32px; text-align: center; } .header .login-bar .shop-cart:hover { background: #f0f0f0; } .header .login-bar .shop-cart img { width: 15px; margin-right: 4px; margin-left: 6px; } .header .login-bar .shop-cart span { margin-right: 6px; } .header .login-bar .login-box { margin-top: 33px; } .header .login-bar .login-box span { color: #4a4a4a; cursor: pointer; } .header .login-bar .login-box span:hover { color: #000000; } .full-left { float: left !important; } .full-right { float: right !important; } /*[class*=" el-icon-"], [class^=el-icon-] { font-size: 50px; }*/ .el-carousel__arrow { width: 120px; height: 120px; } .el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner { background: #ffc210; border-color: #ffc210; border: none; } .el-checkbox__inner:hover { border-color: #9b9b9b; } .el-checkbox__inner { width: 16px; height: 16px; border: 1px solid #9b9b9b; border-radius: 0; } .el-checkbox__inner::after { height: 9px; width: 5px; } a { color: #333; } </style>
路由层:
from django.urls import path from . import views urlpatterns = [ path('mobile/', views.MobileAPIView.as_view()), path('sms/', views.SMSAPIView.as_view()), path('register/', views.RegisterCreateAPIView.as_view()), path('login/', views.LoginAPIView.as_view()), path('login/mobile/', views.LoginMobileAPIView.as_view()), ]
常量配置:
# settings/const.py # 短信过期时间(单位:s) SMS_EXP = 300000 # 短信缓存key SMS_CACHE_KEY = 'sms_%(mobile)s' # 轮播图推荐量 BANNER_COUNT = 3 # settings/dev.py REST_FRAMEWORK = { # 异常配置 'EXCEPTION_HANDLER': 'utils.exception.exception_handler', # 频率限制配置 'DEFAULT_THROTTLE_RATES': { 'user': None, 'anon': None, 'sms': '1/m', }, }
视图层:
from rest_framework.views import APIView from .models import User from utils.response import APIResponse import re # 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册 # 校验手机号 class MobileAPIView(APIView): def post(self, request, *args, **kwargs): mobile = request.data.get('mobile') if not mobile or not re.match(r'^1[3-9]\d{9}$', mobile): return APIResponse(1, '数据有误') try: User.objects.get(mobile=mobile) return APIResponse(2, '已注册') except: return APIResponse(0, '未注册') # 发送验证码 from libs import txsms from django.core.cache import cache from settings.const import SMS_EXP, SMS_CACHE_KEY from .thorttles import SMSRateThrottle class SMSAPIView(APIView): # 频率限制 throttle_classes = [SMSRateThrottle] def post(self, request, *args, **kwargs): # 1)拿到前台的手机号 mobile = request.data.get('mobile') if not mobile or not re.match(r'^1[3-9]\d{9}$', mobile): return APIResponse(2, '数据有误') # 2)调用txsms生成手机验证码 code = txsms.get_code() # 3)调用txsms发送手机验证码 result = txsms.send_sms(mobile, code, SMS_EXP // 60) # 4)失败反馈信息给前台 if not result: return APIResponse(1, '短信发送失败') # 5)成功服务器缓存手机验证码 - 用缓存存储(方便管理) - redis cache.set(SMS_CACHE_KEY % {'mobile': mobile}, code, SMS_EXP) # 6)反馈成功信息给前台 return APIResponse(0, '短信发送成功') # 注册 from rest_framework.generics import CreateAPIView from . import serializers class RegisterCreateAPIView(CreateAPIView): # queryset = User.objects.filter(is_active=True) serializer_class = serializers.RegisterModelSerializer # 自定义响应结果 def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # 校验失败就主动抛异常 => 自定义异常结果,配置异常模块 user_obj = serializer.save() # 要自定义入库逻辑,重写create方法 headers = self.get_success_headers(serializer.data) # 响应结果需要格式化,使用序列化类要提供序列化与反序列化两套规则 return APIResponse(0, 'ok', results=serializers.RegisterModelSerializer(user_obj).data, http_status=201, headers=headers ) # 多方式登录 class LoginAPIView(APIView): # 1) 禁用认证与权限组件 authentication_classes = [] permission_classes = [] def post(self, request, *args, **kwargs): # 2) 拿到前台登录信息,交给序列化类,规则:账号用usr传,密码用pwd传 user_ser = serializers.LoginModelSerializer(data=request.data) # 3) 序列化类校验得到登录用户与token存放在序列化对象中 user_ser.is_valid(raise_exception=True) # 4) 取出登录用户与token返回给前台 return APIResponse(token=user_ser.token,
results=serializers.LoginModelSerializer(user_ser.user).data) # 手机验证码登录 from rest_framework_jwt.serializers import jwt_payload_handler from rest_framework_jwt.serializers import jwt_encode_handler class LoginMobileAPIView(APIView): authentication_classes = [] permission_classes = [] def post(self, request, *args, **kwargs): mobile = request.data.get('mobile') code = request.data.get('code') if not mobile or not code: return APIResponse(1, '数据有误') old_code = cache.get(SMS_CACHE_KEY % {'mobile': mobile}) if code != old_code: return APIResponse(1, '验证码错误') try: user = User.objects.get(mobile=mobile) payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) return APIResponse(token=token, results=serializers.LoginModelSerializer(user).data) except: return APIResponse(1, '用户不存在')
序列化组件:
from rest_framework import serializers from . import models import re from django.core.cache import cache from settings.const import SMS_CACHE_KEY class RegisterModelSerializer(serializers.ModelSerializer): # 自定义反序列化字段的规则必须在字段声明时规定 code = serializers.CharField(write_only=True, min_length=4, max_length=4) class Meta: model = models.User fields = ('mobile', 'password', 'code', 'username', 'email') extra_kwargs = { 'password': { 'min_length': 6, 'max_length': 18, 'write_only': True }, 'username': { 'read_only': True }, 'email': { 'read_only': True } } def validate_mobile(self, value): if not re.match(r'^1[3-9]\d{9}$', value): raise serializers.ValidationError('手机号有误') return value def validate(self, attrs): mobile = attrs.get('mobile') code = attrs.pop('code') # code不入库 old_code = cache.get(SMS_CACHE_KEY % {'mobile': mobile}) if not old_code: raise serializers.ValidationError({'code': '验证码已失效'}) if code != old_code: raise serializers.ValidationError({'code': '验证码错误'}) # 验证码一旦验证成功,就失效(一次性) # cache.set(SMS_CACHE_KEY % {'mobile': mobile}, '0000', 1) return attrs # create方法重写:通过手机号注册的用户,用户名默认就是手机号 def create(self, validated_data): mobile = validated_data.get('mobile') username = mobile password = validated_data.get('password') return models.User.objects.create_user(username=username, mobile=mobile, password=password) from rest_framework_jwt.serializers import jwt_payload_handler from rest_framework_jwt.serializers import jwt_encode_handler class LoginModelSerializer(serializers.ModelSerializer): usr = serializers.CharField(write_only=True) pwd = serializers.CharField(write_only=True) class Meta: model = models.User fields = ['usr', 'pwd', 'username', 'mobile', 'email'] extra_kwargs = { 'username': { 'read_only': True }, 'mobile': { 'read_only': True }, 'email': { 'read_only': True }, } def validate(self, attrs): usr = attrs.get('usr') pwd = attrs.get('pwd') # 多方式登录:各分支处理得到该方式下对应的用户 if re.match(r'.+@.+', usr): user_query = models.User.objects.filter(email=usr) elif re.match(r'1[3-9][0-9]{9}', usr): user_query = models.User.objects.filter(mobile=usr) else: user_query = models.User.objects.filter(username=usr) user_obj = user_query.first() # 签发:得到登录用户,签发token并存储在实例化对象中 if user_obj and user_obj.check_password(pwd): # 签发token,将token存放到 实例化类对象的token 名字中 payload = jwt_payload_handler(user_obj) token = jwt_encode_handler(payload) # 将当前用户与签发的token都保存在序列化对象中 self.user = user_obj self.token = token return attrs raise serializers.ValidationError({'data': '数据有误'})
from rest_framework.throttling import SimpleRateThrottle class SMSRateThrottle(SimpleRateThrottle): scope = 'sms' def get_cache_key(self, request, view): mobile = request.data.get('mobile') or request.query_params.get('mobile') if not mobile: return None return self.cache_format % {'scope': self.scope, 'ident': mobile}
异常处理组件:
from rest_framework.views import exception_handler as drf_exception_handler from rest_framework import status from utils.logging import logger from utils.response import APIResponse def exception_handler(exc, context): response = drf_exception_handler(exc, context) if response is None: logger.error('%s - %s - %s' % (context['view'], context['request'].method, exc)) return APIResponse(3, '异常', results={'detail': '服务器错误'}, http_status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=True ) return APIResponse(3, '异常', results=response.data, http_status=status.HTTP_401_UNAUTHORIZED)
接口缓存:
from rest_framework.generics import ListAPIView from . import models, serializers from settings.const import BANNER_COUNT # 访问量大,且数据较固定的接口,建议建立接口缓存 from django.core.cache import cache from rest_framework.response import Response class BannerListAPIView(ListAPIView): queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('-orders')[:BANNER_COUNT] serializer_class = serializers.BannerModelSerializer # 缓存有,走缓存,缓存没有走数据库 def list(self, request, *args, **kwargs): banner_data = cache.get('banner_list') if not banner_data: print('走数据库') response = super().list(request, *args, **kwargs) banner_data = response.data # 建立缓存,不建议设置缓存过期时间,用celery等框架后台异步更新缓存即可 cache.set('banner_list', banner_data) return Response(banner_data)