用户个人中心信息展示
功能 --- 用户个人中心信息展示
-
请求方式
请求方法 请求地址 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()
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 # 用户选择市,就加载县数据
缓存省市区数据
-
作用: 省市区的数据是经常被用户查询使用的,而且数据基本不变化
所以我们可以将省市区数据进行缓存处理,减少数据库的查询次数(请求的次数一样,但是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