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() and retrieve() 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)