用户个人中心信息展示

功能 --- 用户个人中心信息展示

  • 请求方式

    请求方法 请求地址
    GET http://127.0.0.1:8000/user
  • 请求参数

    请求头 key 类型 是否必传 说明
    headers Authorization 'JWT ' + token string JWT字符串一定要加空格(后端以空格进行拆分)
  • 响应成功结果:JSON

    字段 说明
    user_id 数字 用户ID
    username 字符串 用户名
    mobile xxxxxxx 手机号码
    email 字符串 用户邮箱
    email_active 布尔值 邮箱是否激活
  • 后端接口注意事项

    - 首先,该接口必须带上jwt认证和权限(必须同时加上,否则校验会产生各种不可思议的问题)
    
    - 响应的字段,全部都在User模型里面,所以继承 RetrieveAPIView 来写即可
    
    	- 具体要响应那些字段,我们在序列化器里面指定
    	
    	- 如果使用原生的 RetrieveAPIView,我们还需要在url路径中提供pk值
    		
              - 缺点: 由于提供了pk值,我们必须到db中查询一次,获取该条记录的信息
                当用户量很大的时候,db的开销就比较大了
      
              - 所以,为了节省性能,我们不再url路径中提供pk值,而是重写 get_object()
                直接返回 request.user
    
  • 新增一个字段(邮箱是否被激活)

    ### users.models
    ......
    class UserInfo(AbstractUser):
    
        mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
        email_active = models.BooleanField(default=False,verbose_name='邮箱激活状态') # 新增字段
    
        class Meta:
            ......
    
    
    
  • 后端其他逻辑

    ### views
    from rest_framework.generics import GenericAPIView, CreateAPIView, RetrieveAPIView
    from rest_framework.permissions import IsAuthenticated
    from rest_framework_jwt.authentication import JSONWebTokenAuthentication
    ......
    class UserDetailView(RetrieveAPIView):
        serializer_class = UserDetailSerializer
        # 认证类 + 权限类 一定要搭配使用,否则登录认证会出现一堆问题
        # JSONWebTokenAuthentication 只查token,若不带token,就不会进行检查
        # 为了避免这种情况,搭配 IsAuthenticated 使用,只有登录过的用户才能访问
        authentication_classes = [JSONWebTokenAuthentication,]
        permission_classes = [IsAuthenticated,] # 局部权限(用户名/密码校验无误才能访问这个视图)
        '''
        若 IsAuthenticated 校验失败,会抛出401状态码的错误信息
        后端把这个状态码返回给前端即可,错误信息由前端自己定义
    
        HTTP 401 Unauthorized
        ......
    
        {
            "detail": "Authentication credentials were not provided."
        }
        '''
    	
    	# 不再根据pk值获取 用户对象,而是直接返回用户对象
        def get_object(self):
            return self.request.user
    
    ......
    ### serializer: 只作数据的展示
    class UserDetailSerializer(serializers.ModelSerializer):
        class Meta:
            model = UserInfo
            fields = ('id','username','mobile','email','email_active')
    ......
    
    ### urls
    urlpatterns = [
        ......
        # 用户个人中心首页
        url(r'^user/$', views.UserDetailView.as_view()),
    
    ]
    
    

功能 --- 用户更新邮箱(当用户进入个人中心页面的时候,设置一个按钮让用户填写邮箱)

  • 请求方式

    请求方法 请求地址
    PUT http://127.0.0.1:8000/email
  • 请求参数

    请求体key 类型 是否必传 说明
    email 用户输入的值 string 带给后端校验
  • 响应成功结果:JSON

    字段 说明
    user_id 数字 用户ID
    email 字符串 用户邮箱
  • 后端接口注意事项

    - 首先,该接口必须带上jwt认证和权限
    
    - 这是一个标准的更新字段的行为,所以继承 UpdateAPIView 
    
    	- 具体要响应那些字段,我们在序列化器里面指定
    	
    	- 如果使用原生的 UpdateAPIView ,我们还需要在url路径中提供pk值
    		
            - 缺点: 由于提供了pk值,我们必须到db中查询一次,获取该条记录的信息
              当用户量很大的时候,db的开销就比较大了
    
            - 所以,为了节省性能,我们不再url路径中提供pk值,而是重写 get_object()
              直接返回 request.user
    
    - 重写序列化器的update()方法: 目的是要把'发送激活邮件'的逻辑丢里面
    
    
......
# 继承 UpdateAPIView
class Emailview(UpdateAPIView):
    serializer_class = EmailSerializer # 校验邮箱字段是否必须
    
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    # 逻辑和 RetrieveAPIView 一样,不需要提供pk值
    def get_object(self):
        return self.request.user

......
class EmailSerializer(serializers.ModelSerializer):

    class Meta:
        model = UserInfo
        fields = ('id', 'email')
        # 为什么不用校验email字段是否合法?因为这是django内置的字段,已内置校验好
        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

  • 前端逻辑: 先作校验,校验通过之后就发送put请求

    // 保存email
            save_email: function () {
                // 保存email
                var re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
                if (re.test(this.email)) {
                    this.email_error = false;
                } else {
                    this.email_error = true;
                    return;
                }
                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);
                    });
            }
    

    发送邮件功能的开发

  • Django中内置了邮件发送功能,被定义在django.core.mail模块中。发送邮件需要使用SMTP服务器(邮件服务器)
    常用的有 163/qq 邮箱等

  • 也就是说,使用 django模块+配置邮件服务器,即可实现发送邮件功能

### settings
......
# --------邮箱配置-------------#
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sina.com'
EMAIL_PORT = 25
#发送邮件的邮箱
EMAIL_HOST_USER = 'xxxx@sina.com'
#在邮箱中设置的客户端授权密码
EMAIL_HOST_PASSWORD = 'xxxxx'
#收件人看到的发件人
EMAIL_FROM = 'xxx<xxxxx@sina.com>'
  • 配置注意事项
- 这里不要把密匙的配置写到'local_settings',如果写到 local_settings,后续celery异步发送邮件的时候,会报路径错误
	......
	raise ImportError("%s doesn't look like a module path" % dotted_path) from err
  • 配置注意事项补充说明(正确写法)
### local_settings
# #--------邮箱配置-------------#
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sina.com'
EMAIL_PORT = 25
#发送邮件的邮箱
EMAIL_HOST_USER = 'xxxx@sina.com'
#在邮箱中设置的客户端授权密码
EMAIL_HOST_PASSWORD = 'xxxxx'
#收件人看到的发件人
EMAIL_FROM = 'xxxx@sina.com>'

### settings
......
# 这部分其实可以省略,但是为了给测试,所以保留
EMAIL_BACKEND = ''
EMAIL_HOST = ''
EMAIL_PORT = ''
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_FROM = ''

###-------------------local_settings-------------------------###
try:
    from .local_settings import *
    # 若写成 from . import local_settings 就没有效果
except ImportError:
    pass
  • 新建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

......
### tasks.py
from celery_tasks.main import celery_app
from django.core.mail import send_mail
from django.conf import settings, ENVIRONMENT_VARIABLE


'''
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') # 给任务函数取一个名字(celery默认的任务名很长...)
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.py 注册任务
......
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);
            });
    }
}

生成邮箱激活链接(模型方法+update方法搭配使用)

  • 在之前的逻辑中,已经实现发送邮件的功能,但是激活链接只是随便写写,并没有完成
    现在,实现这个功能
  • 必须给每个用户生成独一无二的链接,以便把db的 email_active 字段修改成 True
    所以,这个链接必须包含该用户相关联的个人信息,以便后端查询对应 email_active 字段
    在实操中,可以把 user_id 和 email 写入链接,然后加密发送给用户
    让用户点击该链接,从而激活 email_active
  • 我们把这个功能的逻辑,放到 models类方法中
    好处在于,类方法直接提供了 self 实例
    而我们刚好要用到实例去查询对应的 user_id 和 email
  • 使用 itsdangerous模块 加密&解密
pip install itsdangerous
......
# 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):
    	# 加盐: 使用django自带的密匙
        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


功能 --- 校验邮箱激活链接

  • 请求方式

    请求方法 请求地址
    GET http://127.0.0.1:8000/emails/verification/?token=xxxx
  • 请求参数

    查询字符串key 类型 是否必传 说明
    token 链接生成的token值 string 带给后端校验
  • 响应成功结果:JSON

    字段 说明
    message ok
  • 后端接口注意事项

    - 获取前端传过来的token并调用实例方法解密返回user对象
    
    	- 若user取不到,则返回400响应,提示{'message':'校验失败'}
    	
    	- 取到user,把email_active更改为True,并保存,返回 {'message':'校验成功'}
    
    
    
  • 当用户点击邮件激活地址,加载页面的时候,前端立即向后端发请求,校验该地址是否正确

# 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()),
  • 至此,邮件激活开发完成

新建 areas app

### meiduo_mail.urls(路由分发)
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include

urlpatterns = [
   ......
    url(r'^', include('apps.areas.urls')),  # 新增收货地址
]

功能 --- 省市区数据模型的创建以及视图

  • 请求方式

    请求方法 请求地址
    GET http://127.0.0.1:8000/areas
  • 响应成功结果:JSON

    字段 说明
    id 数字 数据ID
    name 字符串 省/直辖市
  • 数据源:tb_areas.sql

  • 后端接口注意事项

    - 继承APIView,过滤获取第一层数据,序列化返回即可
    
    - 序列化器也十分简单,序列化 id,name即可
    
    - 返回这样的json
    
    	[
            {
                "id": 110000,
                "name": "北京市"
            },
            {
                "id": 120000,
                "name": "天津市"
            },
            {
                "id": 130000,
                "name": "河北省"
            },
            {
                "id": 140000,
                "name": "山西省"
            }
            ......
    
    
### areas.urls
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^areas/$',views.AreaListView.as_view()), # 第一层数据接口
]

### areas.models
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
        
 - 插入sql全国省市区数据: tb_areas.sql
  • 创建获取省/直辖市的接口
### views
from rest_framework.views import APIView
from rest_framework.response import Response

from .models import Area
from .serializers import AreaSerializer

class AreaListView(APIView):

    def get(self,request):
        queryset = Area.objects.filter(parent=None) # 先取第一层数据(各省,直辖市)
        serializer = AreaSerializer(instance=queryset,many=True)
        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": "山西省"
    }
    ......

功能 --- 展示市/区 数据(父子地区关系,只有一层)

  • 请求方式

    请求方法 请求地址
    GET http://127.0.0.1:8000/areas/pk
  • 请求参数

    请求参数 类型 是否必传 说明
    pk string 一级地址的ID
  • 响应成功结果:JSON

    字段 说明
    id 数字 一级地址ID
    name 字符串 一级地址
    subs [{...},{...},{...}] 二级地址集
  • 后端接口注意事项

    - 首先,该接口必须传pk值,对pk值进行校验
    
    - 序列化器关于子集地址的渲染,有小技巧
    	- parent = models.ForeignKey(...... ,related_name='subs', .....)
    	- subs = AreaSerializer(many=True)
    	
    - 返回这样的JSON数据
    
    	{
        "id": 130000,
        "name": "河北省",
        "subs": [
            {
                "id": 130100,
                "name": "石家庄市"
            },
            {
                "id": 130200,
                "name": "唐山市"
            },
            {
               ......
            },
        ]
    }
    
### ursl
......
urlpatterns = [
    ......
    url(r'^areas/(?P<pk>\d+)$', views.AreaDetailView.as_view()),
]

### serializers
......
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']

### 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) # 序列化单条数据,无需传 many=True
            return Response(serializer.data)

# 测试: 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 # 用户选择市,就加载县数据

缓存省市区数据

  • 作用: 省市区的数据是经常被用户查询使用的,而且数据基本不变化
    所以我们可以将省市区数据进行缓存处理,减少数据库的查询次数(请求的次数一样,但是db查询次数大大降低)

  • 在Django REST framework中使用缓存,可以通过drf-extensions扩展来实现

    - pip install 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):
        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取了

用户收货地址建模

  • 新建 Address 模型(利用UserInfo和Area当外键,丰富Address模型)
- 新建 scripts.models.py(继承专用)

    from django.db import models

    class BaseModel(models.Model): 
        create_time = models.DateTimeField(auto_now_add=True,verbose_name='创建时间')
        update_time = models.DateTimeField(auto_now=True,verbose_name='更新时间')

        class Meta:
            abstract = True

- users.models 新建 Address模型,同时UserInfo模型新增'默认地址'字段

	class UserInfo(AbstractUser):
        mobile = models.CharField(max_length=11,unique=True,verbose_name='手机号码')
        email_active = models.BooleanField(default=False, verbose_name='邮箱激活状态')
        # 新增
        default_address = models.ForeignKey('Address', related_name='users', null=True, blank=True,
                                            on_delete=models.SET_NULL, verbose_name='默认地址')


    class Address(BaseModel):
        """
        - 用户地址
            - 必传8个字段: user,title,receiver,province,city,district,place,mobile
            - 外键: user,province,city,district
        """
        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='收货人')
        # 引用 areas 模型,注意写法,没有经过models
        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']

功能 --- 新增用户收货地址

  • 请求方式

    请求方法 请求地址
    POST http://127.0.0.1:8000/addresses
  • 请求头

    请求头 key 类型 是否必传 说明
    headers Authorization 'JWT ' + token string JWT字符串一定要加空格(后端以空格进行拆分)
  • 请求体(9个字段)

    key 类型 是否必传 说明
    receiver 字符串 string 收货人
    province_id 数值 int
    city_id 数值 int 市/区
    district_id 数值 int
    place 字符串 string 具体地址
    mobile 字符串 string 收货人手机号码
    title 字符串 string 地址名称
    tel 字符串 string 固定电话
    email 字符串 string 收货人邮箱
  • 响应成功结果:JSON(返回13个字段)

    字段 说明
    id 数字 用户ID
    receiver 字符串 收货人
    title 字符串 收货地址名称
    place 字符串 具体地址
    mobile 字符串 收货人手机号码
    province 字符串
    province_id 数字 省ID
    city 字符串 市/区
    city_id 数字 市/区 ID
    district 字符串
    district_id 数字 县ID
    tel 字符串(非必须) 固定电话
    email 字符串(非必须) 收货人邮箱
  • 后端接口注意事项

    - 该接口必须带上jwt认证和权限
    
    
  • 路由方面,这次使用DRF自动路由来实现

### users.urls
......
from rest_framework.routers import DefaultRouter
from . import views

urlpatterns = [
   ......
]

router = DefaultRouter()
# 前端使用post方法提交,自动路由会自动调用视图的create()方法,所以views必须实现create()来响应
router.register(r'addresses',views.AddressViewSet,basename='addresses')
urlpatterns += router.urls

  • 序列化器: 响应的字段,既要包含外键值(数字ID),也要包含主表的字段值,使用 StringRelatedField 帮助我们实现
class UserAddressSerializer(serializers.ModelSerializer):
  
    # StringRelatedField会关联主表的 __str___返回值(刚好就是字段的名称)
    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排除掉,纯粹为了学习,比如在create()方法中,如何获取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
  • 视图: 使用GenericViewSet只是在GenericAPIView的基础上,添加了动作的支持(还有路由支持)
class AddressViewSet(GenericViewSet):
	
    '''
        - 必须实现 create()方法,响应自动路由的post方法
        - 限制该用户地址的数量,大于10个就返回400错误
        - 创建序列化器,校验,保存,返回响应数据(201)
    '''
    serializer_class = UserAddressSerializer
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]

    def create(self,request):
        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)
        # 这种写法,序列化对象这句 user = self.context['request'].user 会报错,因为没有传context(self.get_serializer就是加了context)
        # serializer = UserAddressSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data, status=status.HTTP_201_CREATED)

新增用户收货地址,还可以这么写(利用CreateAPIView)

### urls
......
url(r'^addresses/demo/$', views.AddressCreateView.as_view()),

### views(保持简洁,复杂的逻辑放在serializer判断)
class AddressCreateView(CreateAPIView):
    serializer_class = AddressCreateSerializer
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    
### serializers
class AddressCreateSerializer(serializers.ModelSerializer):
	
    # 若加上参数 source='area.name',响应就没有province这个字段
    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
        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
	
	# 收货地址数量判断的逻辑放在这
    def validate(self, attrs):
        user = self.context['request'].user
        count = Address.objects.filter(user=user).count()
        if count > 10:
            raise serializers.ValidationError('收货地址最多只能10个')
        return attrs

    def create(self,validated_data):
        user = self.context['request'].user
        validated_data['user'] = user
        instance = Address(**validated_data)
        instance.save()
        return instance
  • 还可以这么写,利用SerializerMethodField
class AddressCreateSerializer(serializers.ModelSerializer):
    province = serializers.SerializerMethodField(read_only=True)
    city = serializers.SerializerMethodField(read_only=True)
    district = serializers.SerializerMethodField(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
        exclude = ['user', 'is_deleted', 'create_time', 'update_time']

    def get_province(self,obj):
        return obj.province.name

    def get_city(self,obj):
        return obj.city.name

    def get_district(self,obj):
        return obj.district.name

    def validate_mobile(self, value):
        ......

    
    def validate(self, attrs):
        ......
        return attrs


    def create(self, validated_data):
		.......
        return instance
  • 此时,新增功能完成,但是页面一刷新,刚刚保存的数据又没来,因为我们还没有做'查询'的功能,现在补上
- 展示数据是一个get请求,自动路由响应list()方法

- 如果使用 ListModelMixin,那么返回的只有 Address模型数据,这个接口还需要返回 user_id 和 default_address_id
  所以我们不继承 ListModelMixin,手动实现list()方法,构造data并响应
class AddressViewSet(GenericViewSet):

    serializer_class = UserAddressSerializer
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]

    def create(self,request):
        ......
	
	# 获取用户所有的收货地址
    def get_queryset(self):
    	# 这句不能放在类属性,因为写在上面,过滤条件无法获取 user
        queryset = Address.objects.filter(user=self.request.user,is_deleted=False)
        return queryset

    def list(self,request):
        serializer = self.get_serializer(instance=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
        })

- 一个数据接口的开发顺序:
	- 增,查,改,删
  • 的动作,可以使用ListCreateAPIView来完成
# class AddressCreateView(CreateAPIView):
class AddressCreateView(ListCreateAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    serializer_class = AddressCreateSerializer

    def get_queryset(self):
        queryset = Address.objects.filter(user=self.request.user,is_deleted=False)
        return queryset

	# 根据需求,在原来序列化器的基础上,再增加一点数据,并响应
    def list(self, request, *args, **kwargs):
        serializer = self.get_serializer(instance=self.get_queryset(),many=True)
        return Response({
            'user_id':request.user.id,
            'default_address_id': request.user.default_address_id,
            'limit':3,
            'addresses': serializer.data
        })
  • 还可以这么(前端需修改原来的数据格式)
### views
......
class AddressCreateView(ListCreateAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    # serializer_class = AddressCreateSerializer

    def get_queryset(self):
        queryset = Address.objects.filter(user=self.request.user,is_deleted=False)
        return queryset


    # def list(self, request, *args, **kwargs):
    #     serializer = self.get_serializer(instance=self.get_queryset(),many=True)
    #     return Response({
    #         'user_id':request.user.id,
    #         'default_address_id': request.user.default_address_id,
    #         'limit':3,
    #         'addresses': serializer.data
    #     })

    def get_serializer_class(self, *args, **kwargs):
        if self.request.method == 'GET':
            return AddressListSerializer
        elif self.request.method == 'POST':
            return AddressCreateSerializer
            
### serilizers
......
class AddressListSerializer(serializers.ModelSerializer):

    # addresses = AddressCreateSerializer(many=True)
    addresses = serializers.SerializerMethodField(read_only=True)
    user_id = serializers.SerializerMethodField(read_only=True)
    default_address_id = serializers.SerializerMethodField(read_only=True)
    limit = serializers.SerializerMethodField(read_only=True)

    class Meta:
        model = Address
        fields = ('user_id','addresses','default_address_id','limit')
        # fields = ('user_id','default_address_id','limit')

    def get_user_id(self,obj):
        return obj.user.id

    def get_default_address_id(self,obj):
        return obj.user.default_address_id

    def get_limit(self,obj):
        return 3

    def get_addresses(self,obj):
        ser = AddressCreateSerializer(instance=obj)
        return ser.data
        
- 响应:
	
	[{user_id: 1,…}, {user_id: 1,…}, {user_id: 1,…}]
        0: {user_id: 1,…}
            addresses: {id: 13, province: "北京市", city: "北京市", district: "东城区", province_id: 110000, city_id: 110100,…}
            default_address_id: 1
            limit: 3
            user_id: 1

  • 更新功能,要更新的数据全部在 Address模型中,所以继承 UpdateModelMixin 即可快速实现更新功能
class AddressViewSet(UpdateModelMixin,GenericViewSet):
	......
	
// 前端修改地址
axios.put(this.host + '/addresses/' + this.addresses[this.editing_address_index].id + '/', this.form_address, {
                        headers: {
                            'Authorization': 'JWT ' + this.token
                        },
                        responseType: 'json'
                    })
                        .then(response => {
  • 更新功能还可以这么写
### user.urls
......
# 收货地址(更新)
url(r'^addresses/(?P<pk>\d+)/$', views.AddressUpdateView.as_view()),

### views
......
class AddressUpdateView(UpdateAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    serializer_class = AddressUpdateSerializer
	
	# 需指定queryset
    def get_queryset(self):
        queryset = Address.objects.filter(id=self.kwargs['pk'])
        return queryset
        
### serilizers
......
# 继承AddressCreateSerializer,重写validate
class AddressUpdateSerializer(AddressCreateSerializer):

    def validate(self, attrs): # 解除收货地址只能3个的限制
        return attrs

  • 删除功能: (使用逻辑删除is_deleted,drf默认的是物理删除)
class AddressViewSet(UpdateModelMixin,GenericViewSet):
    ......
    def destroy(self, request,*args,**kwargs):
        instance = self.get_object()
        instance.is_deleted = True # 逻辑删除
        instance.save()
        return Response({'message':'删除成功'},status=status.HTTP_204_NO_CONTENT)
  • title字段的更新(当用户修改收货地址名称)
- 这个接口属于'局部'更新的行为

- 如果前端发put请求,没有携带title字段,那么就认为是整体更新
  如果有携带title字段,那么就认为是'局部更新',也就是说,根据是否传title字段,来响应不同的接口
  
- 整体更新和局部更新url设计如下

	- 整体: axios.put(this.host + '/addresses/' + this.addresses[this.editing_address_index].id + '/'
	
	- 局部: axios.put(this.host + '/addresses/' + this.addresses[index].id + '/title/'
### serializers

class AddressTitleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Address
        fields = ('title',)
        
### views
class AddressViewSet(UpdateModelMixin,GenericViewSet):

    serializer_class = UserAddressSerializer
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]

    def create(self,request):
        ......

    def get_queryset(self):
        ......
	
    def list(self,request):
        ......

    def destroy(self, request,*args,**kwargs):
        ......
	
	# 传title,调用这个接口;不传title,就调用UpdateModelMixin的update()方法
    @action(methods=['put'],detail=True)
    def title(self,request,*args,**kwargs):
        obj = self.get_object()
        # 这里传不传 partical,结果都一样
        # serializer = AddressTitleSerializer(instance=obj,data=request.data,partial=True)
        serializer = AddressTitleSerializer(instance=obj,data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(serializer.data)
        
- 这里如果不使用序列化器,那么要校验title字段不能为空,然后才能执行 obj.title=request.data.get('title')
  用序列化器来校验字段是否为空,更简单
  • 功能-设置默认地址
- 本质就是填充 User 模型的 default_address 字段

- 接口的套路和 title类似,只要url路径传了status,就响应这个put请求接口
### views
class AddressViewSet(UpdateModelMixin,GenericViewSet):

    serializer_class = UserAddressSerializer
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]

    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':'设置默认地址成功'},status=status.HTTP_200_OK)
  • 删除功能还可以这么写,继承DestroyAPIView,重写destory方法
class AddressUpdateView(UpdateAPIView,DestroyAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    serializer_class = AddressUpdateSerializer

    def get_queryset(self):
        queryset = Address.objects.filter(id=self.kwargs['pk'])
        return queryset
	
	# 把默认的'物理删除',变成'逻辑删除'
    def destroy(self,request,*args,**kwargs): 
        instance = self.get_object()
        instance.is_deleted = True
        instance.save()
        return Response({'message': '删除成功'}, status=status.HTTP_204_NO_CONTENT)
  • detail=True解析
detail: 声明该action的路径是否与单一资源对应,及是否是xxx/<pk>/action方法名/
        True 表示路径格式是xxx/<pk>/action方法名/
        False 表示路径格式是xxx/action方法名/
  • title字段的更新,可以这么写
### users.urls
......
# 收货地址(title更新)
url(r'^addresses/(?P<pk>\d+)/title/$', views.AddressUpdateTitleView.as_view()),

### serilizers
......
class AddressUpdateTitleSerializer(serializers.ModelSerializer):

    class Meta:
        model = Address
        fields = ['title',]
        
### views
......
class AddressUpdateTitleView(UpdateAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    serializer_class = AddressUpdateTitleSerializer

    def get_queryset(self):
        queryset = Address.objects.filter(id=self.kwargs['pk'],is_deleted=False)
        return queryset
  • default_address字段的更新,可以这么写
### users.urls
......
# 收货地址(默认收货地址更新)
url(r'^addresses/(?P<pk>\d+)/status/$', views.AddressUpdateDefaultView.as_view()),

### views
......
class AddressUpdateDefaultView(UpdateAPIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    serializer_class = AddressUpdateDefaultSerializer

    def get_queryset(self):
        queryset = Address.objects.filter(id=self.kwargs['pk'],is_deleted=False)
        return queryset

### serilizers
......
class AddressUpdateDefaultSerializer(serializers.ModelSerializer):

    class Meta:
        model = Address
        fields = ['id',]

    def update(self, instance, validated_data):
        self.context['request'].user.default_address_id = instance.id
        self.context['request'].user.save()
        return instance