QQ登录成功
-
当前用户成功完成QQ登录的动作以后,QQ服务器会返回 code 和 'url回调地址'
回调地址即 用户登录完成以后,要跳转的页面 -
前端要在该回调地址做的事情
-
当该页面加载完成以后,立即带着 code向后端发起请求
以便后端带着code向腾讯服务器获取 access_token 和 openid -
对后端返回的数据进行判断
-
有包含用户信息,就存储到本地
- 依据后端提供的原网页地址,跳转回原页面
-
没有包含用户信息,存储后端加密过后的 openid
- 展示注册表单,让用户填
-
-
### oauth_call.html
......
var vm = new Vue({
el: '#app',
data: {
......
},
mounted: function () {
// 从路径中获取qq重定向返回的code
var code = this.get_query_string('code');
axios.get(this.host + '/oauth/qq/user/?code=' + code, {
responseType: 'json',
}).then(response => {
if (response.data.user_id){
// 用户已绑定,存储token
sessionStorage.clear();
localStorage.clear();
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
localStorage.token = response.data.token;
var state = this.get_query_string('state');
location.href = state;
}else{
// 用户未绑定,存储openid并展示注册页面
this.access_token = response.data.access_token;
this.is_show_waiting = false;
}
}).catch(error => {
console.log(error.response.data);
alert('服务器异常');
})
},
methods: {
// 获取url路径参数
get_query_string: function (name) {
v......
},
// 生成uuid
generate_uuid: function () {
......
},
check_pwd: function () {
......
},
check_phone: function () {
......
},
check_sms_code: function () {
......
},
// 发送手机短信验证码
send_sms_code: function () {
.......
},
// 保存
on_submit: function () {
......
}
});
后端新增接口,响应前端发过来code处理,要做的事情如下
- 获取前端传入的code
- 调用QQ登录SDK,向QQ服务器发起两次请求
- 第一次带着code获取access_token
- 第二次带着access_token获取openid
- 查询db有没有这个openid
- 如果没有,就创建一个新用户与此openid绑定
- 如果有,则代码登录成功,返回jwt状态保存信息
class QQAuthUserView(APIView):
def get(self,request,*args,**kwargs):
code = request.query_params.get('code')
if not code:
return Response({'message':'缺少code'},status=status.HTTP_400_BAD_REQUEST)
qq_auth = OAuthQQ(client_id=settings.QQ_CLIENT_ID,client_secret=settings.QQ_CLIENT_SECRET,redirect_uri=settings.QQ_REDIRECT_URI)
try:
access_token = qq_auth.get_access_token(code)
openid = qq_auth.get_open_id(access_token)
except Exception as e: # 这里要甩锅腾讯,日志记录一下
logger.info(e)
return Response({'message':'qq服务器错误'},status=status.HTTP_503_SERVICE_UNAVAILABLE)
try:
qq_user = QQAuthUser.objects.get(openid=openid)
except QQAuthUser.DoesNotExist:
# 这里不可能再实现注册请求,因为要发post请求,而我们写在了get
# return Response({'access_token':openid}) # 让前端帮我们先临时存着(要加密)
else:
# 手动签发jwt,返回给前端用户信息
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(qq_user.user)
token = jwt_encode_handler(payload)
qq_user.user.token = token # 把token加到user字段
return Response({
'user_id':qq_user.user.id,
'username':qq_user.user.username,
'token':token
})
openid的加密处理
- 引入 itsdangerous 库,利用其时间加密类来完成需求
def generate_save_user_token(openid):
# 生成加密序列化器(加盐)
serializer = Serializer(settings.SECRET_KEY,600)
data = {'openid':openid}
token = serializer.dumps(data) # bytes类型
return token.decode() # 最终返回字符串
- 把之前的逻辑小改一下,加密openid后,再返回给前端
......
class QQAuthUserView(APIView):
def get(self,request,*args,**kwargs):
......
try:
qq_user = QQAuthUser.objects.get(openid=openid)
except QQAuthUser.DoesNotExist:
# 加密处理,有效期10分钟
secret_openid = generate_save_user_token(openid)
return Response({'access_token':secret_openid}) # 让前端帮我们先临时存着(要加密)
......
QQ首次登录,用户填写表单流程
- 3+1字段: 手机号,密码,短信验证码(加密过后的openid)
- 先拿手机号查询此手机号是否已注册
- 已注册,再检测密码是否正确
如果正确,就不创建新用户,直接和原有用户绑定
- 未注册,就创建一个新用户,再合openid绑定
- 把校验的逻辑和存储用户的逻辑分开
- 校验的逻辑放在 validate()
- 存储的逻辑放在 create()
- 接口逻辑
- 对前端传过来的数据进行校验
- 校验通过
- 检查db是否注册过
- 已注册,和openid进行绑定,返回jwt维持状态
- 未注册,创建新用户并绑定openid,返回jwt维持状态
- 为了节省url,利用之前的 QQAuthUserView 的post方法来实现注册需求
......
class QQAuthUserView(APIView):
def get(self,request,*args,**kwargs):
......
def post(self,request):
# QQAuthUserSerializer目前还没有定义
serializer = QQAuthUserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 把签发jwt的逻辑放在 create()方法中
user = serializer.save()
return Response({
'user_id': user.id,
'username': user.username,
'token': token
})
-
序列化器的逻辑
- 校验四个字段
-
解密前端传过来的openid
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer,BadData
......
def generate_save_user_token(secret_openid):
serializer = Serializer(settings.SECRET_KEY,600)
try:
data = serializer.loads(secret_openid) # 解密
except BadData:
return
else:
return data.get('openid')
- 主逻辑
from rest_framework import serializers
from django_redis import get_redis_connection
from rest_framework_jwt.settings import api_settings
from utils.auth import check_save_user_token
from users.models import UserInfo
from .models import QQAuthUser
# 也可以用ModelSerilizers
class QQAuthUserSerializer(serializers.Serializer):
sms_code = serializers.CharField(label='短信验证码')
access_token = serializers.CharField(label='操作凭证')
password = serializers.CharField(label='密码',max_length=20,min_length=8)
mobile = serializers.RegexField(label='手机号', regex=r'^1[3-9]\d{9}$')
def validate(self, attrs):
# 检验access_token
access_token = attrs.pop('access_token')
openid = check_save_user_token(access_token)
if not openid:
raise serializers.ValidationError('openid无效!')
attrs['openid'] = openid # 把openid加入 attrs
# 检验短信验证码(拷贝之前的校验逻辑)
mobile = attrs.get('mobile')
conn = get_redis_connection('verify_codes')
redis_code = conn.get(mobile)
if redis_code is None:
raise serializers.ValidationError('验证码已过期,请重新获取')
user_code = attrs['sms_code']
format_redis_code = redis_code.decode()
if user_code != format_redis_code:
raise serializers.ValidationError('验证码错误')
# 如果用户存在,检查用户密码
try:
user = UserInfo.objects.get(mobile=mobile)
except UserInfo.DoesNotExist:
pass # 用户不存在,暂时先过(具体的逻辑放到下面)
else:
password = attrs.get('password')
if not user.check_password(password):
raise serializers.ValidationError('密码错误')
attrs['user'] = user # 把user对象加入 attrs
return attrs
def create(self, validated_data):
openid = validated_data['openid']
user = validated_data.get('user')
mobile = validated_data['mobile']
password = validated_data['password']
if not user:
# 如果用户不存在,创建用户,绑定openid(创建了OAuthQQUser数据)
# user = UserInfo.objects.create_user(username=mobile, mobile=mobile, password=password)
user = UserInfo(
username=mobile,
mobile=mobile
)
user.set_password(password)
user.save()
# 不管有无user,绑定openid 这步是无法避免的(用户首次QQ登录的时候)
QQAuthUser.objects.create(user=user, openid=openid)
# 签发jwt token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
user.token = token
return user
- 释疑: 当用户再次使用QQ登录的时候,前端逻辑如下
......
mounted: function () {
var code = this.get_query_string('code');
axios.get(this.host + '/oauth/qq/user/?code=' + code, {
responseType: 'json',
}).then(response => {
if (response.data.user_id){
// 用户已绑定,直接跳转url到原来地址,所以不用再展示'表单数据页面'
sessionStorage.clear();
localStorage.clear();
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
localStorage.token = response.data.token;
var state = this.get_query_string('state');
location.href = state;
}else{
// 用户未绑定,才展示表单页面
this.access_token = response.data.access_token;
this.is_show_waiting = false;
}
}).catch(error => {
console.log(error.response.data);
alert('服务器异常');
})
},
用户个人中心信息展示
- 新增一个字段(邮箱是否被激活)
......
class UserInfo(AbstractUser):
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
email_active = models.BooleanField(default=False,verbose_name='邮箱激活状态') # 新增字段
class Meta:
......
- 展示单个模型的信息,drf提供了 RetrieveAPIView
- 如果使用原生的 RetrieveAPIView,我们还需要在url路径中提供pk值
- 缺点: 由于提供了pk值,我们必须到db中查询一次,获取该条记录的信息
当用户量很大的时候,db的开销就比较大了
- 所以,为了节省性能,我们不再url路径中提供pk值,而是重写 get_object()
直接返回 模型对象即可
# views
......
class UserDetailView(RetrieveAPIView):
serializer_class = UserDetailSerializer
# queryset = UserInfo.objects.get_queryset()
permission_classes = [IsAuthenticated,] # 局部权限(用户名/密码校验无误才能访问这个视图)
'''
若 IsAuthenticated 校验失败,会抛出401状态码的错误信息
后端把这个状态码返回给前端即可,错误信息由前端自己定义
HTTP 401 Unauthorized
......
{
"detail": "Authentication credentials were not provided."
}
'''
def get_object(self):
return self.request.user # 重新指定查询集
......
# serializer: 只作数据的展示
class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ('id','username','mobile','email','email_active')
......
# settings
#---------DRF配置项------------#
REST_FRAMEWORK = {
......
# 认证
# drf 中的身份验证方式
'DEFAULT_AUTHENTICATION_CLASSES': (
# 首选是JWT:前端带着 'Authorization': 'JWT ' + this.token 就得经过这个认证类的校验
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
- 前端代码
......
mounted: function () {
// 判断用户的登录状态
if (this.user_id && this.token) {
axios.get(this.host + '/user/', {
// 向后端传递JWT token的方法
// 请求头必须添加jwt验证:注意'JWT '有一个空格,后端要根据这个空格进行拆分
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json',
})
.then(response => {
// 加载用户数据
this.user_id = response.data.id;
this.username = response.data.username;
this.mobile = response.data.mobile;
this.email = response.data.email;
this.email_active = response.data.email_active;
......
})
.catch(error => { // 认证失败也跳转回登录界面
if (error.response.status == 401 || error.response.status == 403) {
location.href = '/login.html?next=/user_center_info.html';
}
});
} else { // 如果用户没登录,就跳转会登录界面
location.href = '/login.html?next=/user_center_info.html';
}
},
用户填写邮箱字段
- 接收用户发过来的邮箱字段,并保存db,然后发送激活邮件
......
# 继承 UpdateAPIView
class Emailview(UpdateAPIView):
serializer_class = EmailSerializer
permission_classes = [IsAuthenticated] # 必须已登录
# 逻辑和 RetrieveAPIView 一样,不需要提供pk值
def get_object(self):
return self.request.user
......
class EmailSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ('id', 'email')
extra_kwargs = {
'emali':{ # django默认的邮箱字段,可以为空,这里显然不可以,所以要配置一下
'required':True
}
}
def update(self, instance, validated_data):
# 重写update,是为了发送激活邮件(若不需要发送激活邮箱,无需重写)
email = validated_data.get('email')
instance.email = email
instance.save()
return instance
发送邮件功能的开发
-
Django中内置了邮件发送功能,被定义在django.core.mail模块中。发送邮件需要使用SMTP服务器(邮件服务器)
常用的有 163/qq 邮箱等 -
也就是说,使用 django模块+配置邮件服务器,即可实现发送邮件功能
# settings
#--------邮箱配置-------------#
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
#发送邮件的邮箱
EMAIL_HOST_USER = 'itcast88@163.com'
#在邮箱中设置的客户端授权密码
EMAIL_HOST_PASSWORD = 'python808'
#收件人看到的发件人
EMAIL_FROM = 'python<itcast88@163.com>'
- 新建celery耗时任务
......
class EmailSerializer(serializers.ModelSerializer):
class Meta:
......
def update(self, instance, validated_data):
email = validated_data.get('email')
instance.email = email
instance.save()
# celery处理邮件耗时操作
# 所以这里必须先注册异步任务
return instance
......
# email.tasks
from celery_tasks.main import celery_app
from django.core.mail import send_mail
from django.conf import settings
'''
send_mail(subject, message, from_email, recipient_list,
fail_silently=False, auth_user=None, auth_password=None,
connection=None, html_message=None)
'''
@celery_app.task(name='send_verify_email')
def send_verify_email(to_email,verify_url):
"""
发送验证邮箱邮件
:param to_email: 收件人邮箱
:param verify_url: 验证链接
:return: None
"""
subject = "美多商城邮箱验证"
html_message = '<p>尊敬的用户您好!</p>' \
'<p>感谢您使用美多商城。</p>' \
'<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
'<p><a href="%s">%s<a></p>' % (to_email, verify_url, verify_url)
send_mail(subject, "", settings.EMAIL_FROM, [to_email], html_message=html_message)
# main: 注册任务
......
celery_app.autodiscover_tasks(['celery_tasks.sms','celery_tasks.email'])
# 把耗时任务加入序列化器的update的逻辑
......
def update(self, instance, validated_data):
email = validated_data.get('email')
instance.email = email
instance.save()
# 传入收件人,以及'验证地址'
send_verify_email.delay(instance.email,'xxx')
return instance
# 启动 celery: celery -A celery_tasks.main worker -l info
'''
- 报错: 原因是 send_mail(subject, "", settings.EMAIL_FROM......中,settings配置无法读取到
- 解决办法:把settings.EMAIL_FROM 替换成 写死的 字符串地址,比如 'yyyy@qq.com'
- 另外一种解决办法: main.py中,加入django配置环境即可
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'meiduo.settings') # 加入这句
......
# 自动注册异步任务
celery_app.autodiscover_tasks(['celery_tasks.sms','celery_tasks.email'])
'''
......
raised unexpected: ImproperlyConfigured('Requested setting EMAIL_FROM, but settings are not configured. You must......
- 前端代码
......
ethods: {
......
// 带着 email和token向后端发起请求,成功就发送邮件,不成功展示错误信息
save_email: function () {
......
axios.put(this.host + '/email/',
{email: this.email},
{
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
this.set_email = false;
this.send_email_btn_disabled = true;
this.send_email_tip = '已发送验证邮件'
})
.catch(error => {
alert(error.data);
});
}
}
生成邮箱激活链接
-
在之前的逻辑中,已经实现发送邮件的功能,但是激活链接只是随便写写,并没有完成
现在,实现这个功能 -
必须给每个用户生成独一无二的链接,以便把db的 email_active 字段修改成 True
所以,这个链接必须包含该用户相关联的个人信息,以便后端查询对应 email_active 字段
在实操中,可以把 user_id 和 email 写入链接,然后加密发送给用户
让用户点击该链接,从而激活 email_active -
我们把这个功能的逻辑,放到 models类方法中
好处在于,类方法直接提供了 self 实例
而我们刚好要用到实例去查询对应的 user_id 和 email
......
# models
from django.conf import settings
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer
class UserInfo(AbstractUser):
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
email_active = models.BooleanField(default=False,verbose_name='邮箱激活状态')
class Meta:
......
def generate_email_verify_url(self):
serializer = TJWSSerializer(settings.SECRET_KEY,3600 * 24) # 有效期一天
data = {
'user_id':self.id,
'email':self.email
}
token = serializer.dumps(data).decode()
# 生成token并拼接激活地址(当用户点击这个地址时,我们还得写接口去校验)
url = 'http://127.0.0.1:8000/success_verify_email.html?token={}'.format(token)
return url
# serilizers 补充激活地址
......
class EmailSerializer(serializers.ModelSerializer):
class Meta:
......
def update(self, instance, validated_data):
......
instance.save()
# 获取校验地址并扔给邮件
verify_url = instance.generate_email_verify_url()
task_number = send_verify_email.delay(instance.email, verify_url)
return instance
- 当用户点击邮件激活地址,加载页面的时候,前端立即向后端发请求,校验该地址是否正确
# success_verify_email.html
......
<script>
var vm = new Vue({
......
// 模板还没加载之前,立即发送请求(带着用户的查询字符串)
created: function(){
axios.get(this.host+'/emails/verification/'+ document.location.search)
.then(response => {
this.success = true; // 显示激活成功
})
.catch(error => {
this.success = false; // 显示激活失败,请重新校验
});
}
});
</script>
- 后端接口
# models
class UserInfo(AbstractUser):
......
def generate_email_verify_url(self):
......
# 解密的逻辑,一般和加密放在一起
@staticmethod # 不需要self,所以定义成了静态方法
def check_email_verify_url(token):
'''
- 获取前端传过来的查询字符串,解密
- 获取解密后的 user_id 和 email 去db查,如果有有就返回user,没有就返回None
'''
serializer = TJWSSerializer(settings.SECRET_KEY, 3600 * 24)
try:
data = serializer.loads(token)
except BadData: # data可能过期
return None
user_id = data.get('user_id')
email = data.get('email')
try:
user = UserInfo.objects.get(id=user_id,email=email)
except user.DoesNotExist:
return None # 查不到就返回None
else:
return user
# views
class EmailVerifyView(APIView):
def get(self,request):
'''
- 获取token,调用校验方法
- 校验成功,把user的email_active字段改为True,成功响应
- 校验失败,就返回400错误
'''
token = request.query_params.get('token')
user = UserInfo.check_email_verify_url(token)
if not user:
return Response({'message':'校验失败'},status=status.HTTP_400_BAD_REQUEST)
user.email_active = True
user.save()
return Response({'message':'ok'})
......
# urls
url(r'^emails/verification/$', views.EmailVerifyView.as_view()),
- 至此,邮件激活开发完成
省市区数据模型的创建以及视图
from django.db import models
class Area(models.Model):
"""
行政区划
"""
name = models.CharField(max_length=20, verbose_name='名称')
# 自关联:self
parent = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='subs', null=True, blank=True, verbose_name='上级行政区划')
'''
province=Area.objects.get(pk=1)
province.parent:获取上级对象
province.***_set:获取下级对象--改名-->province.subs
'''
class Meta:
db_table = 'tb_areas'
verbose_name = '行政区划'
verbose_name_plural = '行政区划'
def __str__(self):
return self.name
- 创建获取省/直辖市的接口
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Area
class AreaListView(APIView):
def get(self,request):
qs = Area.objects.filter(parent=None) # 先取第一层数据(各省,直辖市)
serializer = AreaSerializer
return Response(serializer.data)
......
# serializers
......
# 只做序列化,无需校验
class AreaSerializer(serializers.ModelSerializer):
class Meta:
model = Area
fields = ['id', 'name'] # 不需要父级的字段
- 测试数据接口: http://127.0.0.1:8000/areas
[
{
"id": 110000,
"name": "北京市"
},
{
"id": 120000,
"name": "天津市"
},
{
"id": 130000,
"name": "河北省"
},
{
"id": 140000,
"name": "山西省"
}
......
- 创建获取市/区 接口(父子地区关系,只有一层)
# views
class AreaDetailView(APIView):
'''
- 先取父级模型对象(校验是否存在)
- 模型存在就序列化数据,返回给前端
'''
def get(self,request,pk):
try:
obj = Area.objects.get(id=pk)
except Area.DoesNotExist:
return Response({'message':'无效的pk值'},status=status.HTTP_400_BAD_REQUEST)
else:
serializer = AreaDetailSerializer(obj)
return Response(serializer.data)
# serializer
class AreaDetailSerializer(serializers.ModelSerializer):
'''
- 返回类似这样的数据 {
'id':1,
'name':'福建省',
'subs':[
{子地区1},
{子地区2},
......
]
}
'''
# 不用传instance,只传many即可(subs本身就表示所有子集,所以不用传)
subs = AreaSerializer(many=True)
class Meta:
model = Area
# subs要和模型中,定义的related_name名称保持一致,才表示反向查询
# 这里如果取别的名称,比如叫son,django就报错,因为Area类没有这个属性
fields = ['id', 'name','subs']
# urls
urlpatterns = [
url(r'^areas$', views.AreaListView.as_view())
# 新增
url(r'^areas/(?P<pk>\d+)$', views.AreaDetailView.as_view()),
]
# 测试: http://127.0.0.1:8000/areas/130100
'''
{
"id": 130100,
"name": "石家庄市",
"subs": [
{
"id": 130102,
"name": "长安区"
},
{
"id": 130104,
"name": "桥西区"
}
......
'''
- 注意事项:如果觉得上面的子集的逻辑不好理解,这个接口还可以这么写
class AreaDetailSerializer(serializers.ModelSerializer):
# subs = AreaSerializer(many=True)
sons = serializers.SerializerMethodField() # 新增自定义字段(自己构造子集数据,然后返回)
class Meta:
model = Area
# fields = ['id', 'name','subs']
fields = ['id', 'name','sons'] # 自定义数据写进去
def get_sons(self,obj):
sons_list = [] # 收集
queryset = obj.subs.all() # 获取所有的子集,然后构造数据
for item in queryset:
sons_list.append({'id':item.id,'name':item.name})
return sons_list
优化views代码,让代码更简洁
- 放弃 APIView,改用 GenericAPIView,写法如下
class AreaListView(GenericAPIView):
queryset = Area.objects.filter(parent=None)
serializer_class = AreaSerializer
def get(self,request):
qs = self.get_queryset()
# qs = self.queryset # 这样写会报错!drf不希望你直接这样写
serializer = self.get_serializer(qs,many=True)
return Response(serializer.data)
- 配合 ListModelMixin,再次简化代码
class AreaListView(ListModelMixin,GenericAPIView):
queryset = Area.objects.filter(parent=None)
serializer_class = AreaSerializer
def get(self,request):
return self.list(request)
- 使用 ListAPIView,实现代码的终极简化
class AreaListView(ListAPIView):
queryset = Area.objects.filter(parent=None)
serializer_class = AreaSerializer
- 同理,检索单个模型的对象的 AreaDetailView 精简代码
class AreaDetailView(RetrieveAPIView):
serializer_class = AreaDetailSerializer
queryset = Area.objects.get_queryset()
-
通过上述两个视图可以发现,同样是使用get方法
-
一个视图检索所有的数据
-
另外一个视图只检索单条数据
-
可以合并成单个视图来处理: ReadOnlyModelViewSet(A viewset that provides default
list()
andretrieve()
actions)
-
# views
class AreaViewSet(ReadOnlyModelViewSet):
def get_queryset(self):
if self.action == 'list':
return Area.objects.filter(parent=None)
else:
return Area.objects.get_queryset()
def get_serializer_class(self): # 返回的是类对象!
# def get_serializer(self,*args,**kwargs): # 返回的是类实例对象,不再搞混!
if self.action == 'list':
return AreaSerializer
else:
return AreaDetailSerializer
# urls
urlpatterns = [
# url(r'^areas$', views.AreaListView.as_view()),
# url(r'^areas/(?P<pk>\d+)$', views.AreaDetailView.as_view()),
# get方法调用不同的动作
url(r'^areas$', views.AreaViewSet.as_view({'get':'list'})),
url(r'^areas/(?P<pk>\d+)$', views.AreaViewSet.as_view({'get':'retrieve'})),
]
- 同样的,url路由也可以使用drf优化,参考博文地址
https://blog.csdn.net/li944254211/article/details/109487723
from django.conf.urls import url
from rest_framework.routers import DefaultRouter
from . import views
urlpatterns = [ # 空的list
# url(r'^areas$', views.AreaListView.as_view()),
# url(r'^areas/(?P<pk>\d+)$', views.AreaDetailView.as_view()),
# url(r'^areas$', views.AreaViewSet.as_view({'get':'list'})),
# url(r'^areas/(?P<pk>\d+)$', views.AreaViewSet.as_view({'get':'retrieve'})),
]
# 以下代码的效果,和上面一模一样
router = DefaultRouter()
# 传入'路由的前缀','视图集','路由别名'
router.register(r'areas',views.AreaViewSet,basename='area')
urlpatterns += router.urls
- 前端逻辑的实现
......
<div class="form_group">
<label>*所在地区:</label>
<select v-model="form_address.province_id">
<option v-for="province in provinces" v-bind:value="province.id">{{ province.name }}</option>
</select>
<select v-model="form_address.city_id">
<option v-for="city in cities" v-bind:value="city.id">{{ city.name }}</option>
</select>
<select v-model="form_address.district_id">
<option v-for="district in districts" v-bind:value="district.id">{{ district.name }}</option>
</select>
</div>
......
// 页面加载完毕,立马向后端发请求,获取 省/直辖市 数据,然后渲染
mounted: function () {
axios.get(this.host + '/areas/', {
responseType: 'json'
})
.then(response => {
this.provinces = response.data;
})
.catch(error => {
alert(error.response.data);
});
......
watch: { // 监视 form_address.province_id 的值
// 原本无值,一旦有值了,带着值的id向后端再次发送请求,渲染后面的子数据
'form_address.province_id': function () {
if (this.form_address.province_id) {
axios.get(this.host + '/areas/' + this.form_address.province_id + '/', {
responseType: 'json'
})
.then(response => {
this.cities = response.data.subs;
})
.catch(error => {
console.log(error.response.data);
this.cities = [];
});
}
},
// 和上面一样的套路
'form_address.city_id': function () {
if (this.form_address.city_id) {
axios.get(this.host + '/areas/' + this.form_address.city_id + '/', {
responseType: 'json'
})
.then(response => {
this.districts = response.data.subs;
})
.catch(error => {
console.log(error.response.data);
this.districts = [];
});
}
}
},
- 也就是说,从页面加载完毕到用户输全了收货地址,总共发了3次请求
- INFO basehttp 157 "GET /areas/ HTTP/1.1" 200 1204 # 页面加载完毕
- INFO basehttp 157 "GET /areas/120000/ HTTP/1.1" 200 74 # 用户选择省,就加载市数据
- INFO basehttp 157 "GET /areas/120100/ HTTP/1.1" 200 572 # 用户选择市,就加载县数据
缓存省市区数据
-
作用: 省市区的数据是经常被用户查询使用的,而且数据基本不变化
所以我们可以将省市区数据进行缓存处理,减少数据库的查询次数 -
在Django REST framework中使用缓存,可以通过drf-extensions扩展来实现
http://chibisov.github.io/drf-extensions/docs/#caching
-
使用方法
......
from rest_framework_extensions.cache.mixins import CacheResponseMixin
# 继承 CacheResponseMixin 缓存类
class AreaViewSet(CacheResponseMixin,ReadOnlyModelViewSet):
def get_queryset(self):
if self.action == 'list':
return Area.objects.filter(parent=None)
else:
return Area.objects.get_queryset()
def get_serializer_class(self):
# def get_serializer(self,*args,**kwargs):
if self.action == 'list':
return AreaSerializer
else:
return AreaDetailSerializer
# settings
......
###------DRF Redis缓存拓展-----#
REST_FRAMEWORK_EXTENSIONS = {
# 缓存时间
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 60,
# 缓存存储
'DEFAULT_USE_CACHE': 'default',
}
- 现在,在redis客户端查看redis数据库的数据,测试效果
当缓存一次省市区数据以后,再次发请求的时候,数据就直接从redis取了
用户收货地址建模
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer,BadData
from utils.models import BaseModel
# Create your models here.
class UserInfo(AbstractUser):
......
# 新增外键
default_address = models.ForeignKey('Address', related_name='users', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='默认地址')
......
# ???: 由于BaseModel是抽象模型,所以 Address 根本不会被写入db
class Address(BaseModel):
"""
用户地址
"""
user = models.ForeignKey(UserInfo, on_delete=models.CASCADE, related_name='addresses', verbose_name='用户')
title = models.CharField(max_length=20, verbose_name='地址名称')
receiver = models.CharField(max_length=20, verbose_name='收货人')
province = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='province_addresses', verbose_name='省')
city = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='city_addresses', verbose_name='市')
district = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='district_addresses', verbose_name='区')
place = models.CharField(max_length=50, verbose_name='地址')
mobile = models.CharField(max_length=11, verbose_name='手机')
tel = models.CharField(max_length=20, null=True, blank=True, default='', verbose_name='固定电话')
email = models.CharField(max_length=30, null=True, blank=True, default='', verbose_name='电子邮箱')
is_deleted = models.BooleanField(default=False, verbose_name='逻辑删除')
class Meta:
db_table = 'tb_address'
verbose_name = '用户地址'
verbose_name_plural = verbose_name
ordering = ['-update_time']
- 收货地址新增 视图以及序列化器
# views
# GenericViewSet只是在GenericAPIView的基础上,添加了动作的支持(还有路由支持)
class AddressViewSet(GenericViewSet):
'''
- 限制该用户地址的数量,大于10个就返回400错误
- 创建序列化器,校验,保存,返回响应数据(201)
'''
permission_classes = [IsAuthenticated]
serializer_class = UserAddressSerializer
# 自定义的 create()方法,django会帮你传request
# 这里其实 self里面就有request对象了,从self去取request可以
def create(self,request):
# count = request.user.addresses.all().count()
count = Address.objects.filter(user=request.user).count()
if count > 10:
return Response({'message':'收货地址不能大于10个'},status=status.HTTP_400_BAD_REQUEST)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data,status=status.HTTP_201_CREATED)
# serializers
class UserAddressSerializer(serializers.ModelSerializer):
# 不对前端传过来的 地址字符串作校验,只作输出
province = serializers.StringRelatedField(read_only=True)
city = serializers.StringRelatedField(read_only=True)
district = serializers.StringRelatedField(read_only=True)
province_id = serializers.IntegerField(label='省ID', required=True)
city_id = serializers.IntegerField(label='市ID', required=True)
district_id = serializers.IntegerField(label='区ID', required=True)
class Meta:
model = Address
# 把 user排除掉,纯粹为了学习
exclude = ('user', 'is_deleted', 'create_time', 'update_time')
# 只校验手机号码
def validate_mobile(self, value):
if not re.match(r'^1[3-9]\d{9}$', value):
raise serializers.ValidationError('手机号格式错误')
return value
# 除了固定电话以及email/逻辑删除有默认值,其他都是必填字段
def create(self,validated_data):
# serilizer中,如何获取 user --- self.context(继承 GenericViewSet 才有,如果是APIView,是没有的)
# user = self.context['request'].get('user')
# self.context['request']的值是一个对象,当然不能get()...
user = self.context['request'].user
validated_data['user'] = user
instance = Address.objects.create(**validated_data)
return instance
# users.urls
......
from rest_framework.routers import DefaultRouter
from . import views
urlpatterns = [
......
]
router = DefaultRouter()
router.register(r'addresses',views.AddressViewSet,basename='addresses')
urlpatterns += router.urls
- 此时,新增功能完成,但是页面一刷新,刚刚保存的数据又没来,因为我们还没有做'查询'的功能
现在补上
class AddressViewSet(UpdateModelMixin,GenericViewSet):
permission_classes = [IsAuthenticated]
serializer_class = UserAddressSerializer
def create(self,request):
......
def get_queryset(self):
# queryset = self.request.user.addresses.filter(is_deleted=False)
queryset = Address.objects.filter(user=self.request.user,is_deleted=False)
return queryset
# 序列化,响应数据(和前端商量好)
def list(self,request):
serializer = self.get_serializer(self.get_queryset(),many=True)
return Response({
'user_id':request.user.id,
'default_address_id':request.user.default_address_id,
'limit':10,
'addresses':serializer.data
})
- 更新的功能更简单,继承 UpdateModelMixin 即可,就写完了
......
class AddressViewSet(UpdateModelMixin,GenericViewSet):
......
- 处理删除(逻辑删除,drf默认的是物理删除)
class AddressViewSet(UpdateModelMixin,GenericViewSet):
permission_classes = [IsAuthenticated]
serializer_class = UserAddressSerializer
def create(self,request):
......
def get_queryset(self):
......
def list(self,request):
......
def destroy(self,request,*args,**kwargs):
'''
- 获取模型对象
- 把 is_deleted 修改为 True,保存
- 返回204状态码
'''
obj = self.get_object()
obj.is_deleted = True
obj.save()
return Response({'message':'删除成功'},status=status.HTTP_204_NO_CONTENT)
- 实现局部更新title字段
# serializers
class AddressTitleSerializer(serializers.ModelSerializer):
class Meta:
model = Address
fields = ('title',)
# views
class AddressViewSet(UpdateModelMixin,GenericViewSet):
permission_classes = [IsAuthenticated]
serializer_class = UserAddressSerializer
def create(self,request):
......
def get_queryset(self):
......
def list(self,request):
......
def destroy(self,request,*args,**kwargs):
......
# 自定义的title必须和action关联起来,否则路由无法对应上
@action(methods=['put'], detail=True)
def title(self,request,*args,**kwargs):
'''
- 获取模型对象
- 序列化(更新)
- 校验,无误就保存,响应ser.data
'''
obj = self.get_object()
serializer = AddressTitleSerializer(instance=obj,data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
- 实现设为'默认地址'(???:设为默认地址以后,该字段必须排在第一,然后并没有这样!)
因为它是根据'updata_time'降序排列的
# views
class AddressViewSet(UpdateModelMixin,GenericViewSet):
permission_classes = [IsAuthenticated]
serializer_class = UserAddressSerializer
def create(self,request):
......
def get_queryset(self):
......
def list(self,request):
......
def destroy(self,request,*args,**kwargs):
......
@action(methods=['put'], detail=True)
def title(self,request,*args,**kwargs):
......
@action(methods=['put'], detail=True)
def status(self,request,*args,**kwargs):
'''
- 获取模型对象
- 把user.default_address = 模型对象,保存
- 响应OK信息(状态码200)
'''
obj = self.get_object()
request.user.default_address = obj
request.user.save()
return Response({'message':'OK'},status=status.HTTP_200_OK)