收货地址:
在这个页面中,我们要实现用户地址的管理,主要的业务逻辑有:
- 省市区地址的数据库建立与查询
- 用户地址的增删改查处理
- 设置默认地址
- 设置地址标题
一、省市区三级联动:
1.三级联动:
在用户录入地址是,需要进行省市区的选择。在页面加载时,向后端请求省份数据,当用户选择确定省份后,向后端请求该省份下的城市数据;在用户选择确定城市数据后,向后端请求该城市的区县信息。我们把这个过程称为省市区三级联动。
2.数据库建表:
新建一个应用areas来实现省市区三级联动。
# /meiduo/meiduo_mall/meiduo_mall/apps
python ../../manage.py startapp area
在areas/models.py中,我们创建省市区数据表,采用自关联的方式。
# /meiduo_mall/apps/areas/models.py
from django.db import models
# Create your models here.
class Area(models.Model):
"""
行政区划
"""
name = models.CharField(max_length=20, verbose_name='名称')
parent = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='subs', null=True, blank=True, verbose_name='上级行政区划')
class Meta:
db_table = 'tb_areas'
verbose_name = '行政区划'
verbose_name_plural = '行政区划'
def __str__(self):
return self.name
2.1自关联说明
- 自关联字段的外键指向自身,所以ForeignKey(‘self’)
- 需要使用related_name指明查询一个行政区划的所有下级行政区划时,使用哪种语法查询,如本模型类中指明通过Area模型类对象.subs查询所有下属行政区划,而不是使用Django默认的Area模型类对象.area_set语法。
2.2 迁移到数据库:
在配置文件中增加应用:
# meiduo_mall/settings/dev.py
...
INSTALLED_APPS = [
...
'areas.apps.AreasConfig',
]
...
迁移到数据库:
python manage.py makemigrations
python manage.py migrate
3.导入数据到数据库:
迁移到数据库后,我们向数据库中添加全国省市区数据,将areas.sql导入数据库
# mysql -h数据库ip地址 -u数据库用户名 -p数据库密码 数据库 < areas.sql
mysql -h127.0.0.1 -uroot -pmysql meiduo_mall < areas.sql
4.后端实现:
4.1 请求省份数据
请求方式: GET areas/
请求参数: 无
**返回数据:**JSON
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
id | int | 是 | 省份id |
name | str | 是 | 省份名称 |
[
{
"id": 110000,
"name": "北京市"
},
{
"id": 120000,
"name": "天津市"
},
{
"id": 130000,
"name": "河北省"
},
...
]
业务逻辑:
- 查询省信息
- 结果返回
4.2 请求城市或区县的数据
请求方式: GET areas/(?P)/
请求参数: 路径传参
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
id | int | 是 | 父级行政区划id |
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
id | int | 是 | 当前行政区划的id |
name | str | 是 | 当前行政区划的名称 |
[
{"id":140100,"name":"太原市"},
{"id":140200,"name":"大同市"},
{"id":140300,"name":"阳泉市"},
...
]
业务逻辑:
- 获取id
- 查询市/区信息
- 返回结果
4.3 后端代码实现:
序列化实现数据的序列化
# apps/areas/serializers.py
# -*-coding:utf-8-*-
from rest_framework import serializers
from areas.models import Areas
class AreasSerializer(serializers.ModelSerializer):
"""行政区划信息序列化器"""
class Meta:
model = Areas
fields = ("id", "name")
视图类中的实现
# apps/areas/views.py
from django.shortcuts import render
# Create your views here.
from rest_framework.generics import ListAPIView
from areas.models import Areas
from areas.serializers import AreasSerializer
class ProvinceView(ListAPIView):
"""返回省份数据"""
queryset = Areas.objects.filter(parent_id=None) # 过滤查询,返回父级为空的数据
serializer_class = AreasSerializer
class AreasView(ListAPIView):
"""返回市、区镇数据"""
serializer_class = AreasSerializer
def get_queryset(self):
"""
因为需要使用参数,所以构造一个方法
:return:
"""
id = self.kwargs["id"]
return Areas.objects.filter(parent_id=id)
二、使用缓存:
省市区的数据是经常被用户查询使用的,而且数据基本不变化,所以我们可以将省市区数据进行缓存处理,减少数据库的查询次数。
在Django REST framework中使用缓存,可以通过drf-extensions扩展来实现。
关于扩展使用缓存的文档,可参考链接http://chibisov.github.io/drf-extensions/docs/#caching
1.安装:
pip install drf-extensions
2.使用方法:
2.1 直接添加装饰器:
可以在使用rest_framework_extensions.cache.decorators中的cache_response装饰器来装饰返回数据的类视图的对象方法,如:
class AreasView(ListAPIView):
"""返回市、区镇数据"""
serializer_class = AreasSerializer
@cache_response()
def get_queryset(self):
"""
因为需要使用参数,所以构造一个方法
:return:
"""
...
cache_response装饰器可以接受两个参数:
@cache_response(timeout=60*60, cache='default')
- timeout: 缓存时间
- default:缓存使用Django混村后端(即CACHES配置中的键名称)
如果在使用cache_response装饰器时未指明timeout或者cache参数,则会使用配置文件中的配置,可以通过如下方法指明:
# DRF扩展
REST_FRAMEWORK_EXTENSIONS = {
# 缓存时间
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 60,
# 缓存存储
'DEFAULT_USE_CACHE': 'default',
}
- DEFAULT_CACHE_RESPONSE_TIMEOUT: 缓存有效时间, 单位是秒。
- DEFAULT_USE_CACHE: 缓存的存储方式,与配置文件中CHCHES的键对应。
注意:cache_response装饰器既可以装饰在视图类中的get方法上,也可以装饰在REST framework扩展类提供的list或retrieve方法上。使用cache_response装饰器无需使用method_decorator进行转换。
2.2 使用drf-extensions提供的扩展类:
drf-extensions扩展对于缓存提供了三个扩展类,三个扩展类都是在rest_framework_extensions.cache.mixins中:
- ListCacheResponseMixin
用于混村返回列表数据的视图,与ListModelMixin扩展类配合使用,实际是为list方法添加了cache_response装饰器。 - RetrieveCacheResponseMixin
用于混村返回单一数据的视图,与RetrieveModelMixin扩展类配合使用,实际是为了retrieve方法添加了chche_response装饰器。 - CahceResponseMixin
为视图集同时补充List和Retrieve两种缓存,与ListModelMixin一起配合使用。
3. 为省市区视图添加缓存:
因为省市区视图使用了视图集,并且视图集中有提供ListModel
Mixin和RetrieveModelMixin的扩展(由ReadOnlyModelViewSet提供 ),所哟可以直接添加CacheRespponseMixin扩展类。
修改返回省市区信息的视图:
# apps/areas/views.py
from django.shortcuts import render
# Create your views here.
from rest_framework.generics import ListAPIView
from rest_framework_extensions.cache.mixins import CacheResponseMixin
from areas.models import Areas
from areas.serializers import AreasSerializer
class ProvinceView(CacheResponseMixin, ListAPIView):
"""返回省份数据"""
queryset = Areas.objects.filter(parent_id=None) # 过滤查询,返回父级为空的数据
serializer_class = AreasSerializer
class AreasView(CacheResponseMixin, ListAPIView):
"""返回市、区镇数据"""
serializer_class = AreasSerializer
def get_queryset(self):
"""
因为需要使用参数,所以构造一个方法
:return:
"""
id = self.kwargs["id"]
return Areas.objects.filter(parent_id=id)
4.缓存数据保存位置与有效期的设置:
我们想把缓存在redis中,且设置有效期,可以在配置文件中定义的方式来实现。
在配置文件中增加:
# settings/dev.py
# DRF扩展
REST_FRAMEWORK_EXTENSIONS = {
# 缓存时间
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 60,
# 缓存存储
'DEFAULT_USE_CACHE': 'default',
}
三、用户地址管理:
我们为保存用户的地址信息,创建数据库表,在users/models.py中定义模型类:
class Address(BaseModel):
"""
用户地址
"""
user = models.ForeignKey(User, 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']
说明:
- Address模型类中的外键指向Areas/models里面的Area,指明外键ForeignKey时,可以使用字符串应用名.模型类名来定义
- related_name 在进行反向关联查询时使用的属性,如 city = models.ForeignKey(‘areas.Area’, related_name=‘city_addresses’)表示可以通过Area对象.city_addresses属性获取所有相关的city数据。
- ordering 表名在进行Address查询时,默认使用的排序方式
为User模型类添加模型地址:
class User(AbstractUser):
...
default_address = models.ForeignKey('Address', related_name='users', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='默认地址')
...
四 、用户地址管理代码
1.添加和修改用户收件地址:
1.1 接口分析:
请求方式: POST /addresses/
请求参数: json
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
title | str | 是 | 地址标题(同收件人) |
receiver | str | 是 | 收件人 |
province_id | int | 是 | 省份id |
city_id | int | 是 | 城市id |
district_id | int | 是 | 区县id |
place | str | 是 | 详细地址 |
mobile | str | 是 | 手机号 |
str | 否 | 邮箱 | |
tel | str | 否 | 固定电话 |
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
id | int | 是 | 收件地址id |
title | str | 是 | 地址标题(同收件人) |
receiver | str | 是 | 收件人 |
province_id | int | 是 | 省份id |
city_id | int | 是 | 城市id |
district_id | int | 是 | 区县id |
place | str | 是 | 详细地址 |
mobile | str | 是 | 手机号 |
str | 否 | 邮箱 | |
tel | str | 否 | 固定电话 |
1.2 业务逻辑:
- 获取前端数据
- 验证数据 ==> 定义序列化器 ==> 验证手机号
- 保存数据 ==> create()
- 返回结果
2.获取用户收件地址:
2.1 接口分析:
请求方式: GET /addresses/
请求参数: headers传参jwt
参数 | 类型 | 是否必传 | 说明 |
---|
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
id | int | 是 | 收件地址id |
title | str | 是 | 地址标题(同收件人) |
receiver | str | 是 | 收件人 |
province_id | int | 是 | 省份id |
city_id | int | 是 | 城市id |
district_id | int | 是 | 区县id |
place | str | 是 | 详细地址 |
mobile | str | 是 | 手机号 |
str | 否 | 邮箱 | |
tel | str | 否 | 固定电话 |
数据示例:
{
"addresses":
[
{
"id":3,
"province":"山西省",
"city":"阳泉市",
"district":"郊区",
"title":"zzz",
"receiver":"zzz",
"place":"哈哈哈哈哈",
"mobile":"13211111111",
"tel":"",
"email":"",
"default_address":null
},
{
"id":1,
"province":"山西省",
"city":"晋城市",
"district":"陵川县",
"title":"python",
"receiver":"python",
"place":"tttttttttttt",
"mobile":"13211111111",
"tel":"",
"email":"",
"default_address":null
}
]
}
2.2 业务逻辑:
- 查询数据
- 序列化返回数据
3. 删除收货地址:
3.1 接口分析:
请求方式: GET /addresses/
请求参数: 路径传参
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
pk | int | 是 | 收货地址的id |
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
“ok” | str | 否 | 成功状态 |
3.2业务逻辑:
- 获取前端传来的pk
- 查询对应的地址
- 将逻辑删除属性置为True
- 返回成功状态
4.修改收货地址标题:
4.1 接口分析:
请求方式: PUT addresses//title/
请求参数: 路径传参和请求体传参
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
pk | int | 是 | 收货地址的id |
title | str | 是 | 新收货地址标签 |
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
“ok” | str | 否 | 成功状态 |
4.2业务逻辑:
- 获取前端传来的pk
- 查询对应的地址
- 将title属性修改为
- 返回成功状态
5.代码实现:
视图类:
# /apps/areas/views.py
# url(r"^addresses/(?P<pk>\d+)/$",views.AddressesView().as_view()),
class AddressesView(CreateAPIView, ListAPIView, DestroyAPIView, UpdateAPIView):
"""用户地址管理"""
"""序列化器实现保存和修改收货地址"""
serializer_class = AddressSerializers
permissions = [IsAuthenticated]
def get_queryset(self):
"""
重写方法,制定查询集
:return:
"""
return Address.objects.filter(user = self.request.user,
is_deleted=False)
def list(self,request, *args, **kwargs):
"""
返回所有地址
根据前端需求,重写list方法
:param request:
:param args:
:param kwargs:
:return:
"""
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response({"addresses":serializer.data})
def destroy(self, request, *args, **kwargs):
"""
重写删除方法,实现逻辑删除
:param request:
:param args:
:param kwargs:
:return:
"""
instance = self.get_object()
instance.is_deleted = True
instance.save()
return Response(status=status.HTTP_204_NO_CONTENT)
# url(r"^addresses/(?P<pk>\d+)/title/$", views.AddressesChangeTitleView.as_view()),
class AddressesChangeTitleView(APIView):
"""修改收货地址的标题"""
def put(self,request,pk):
"""
修改收货地址标题
:param requset:
:param pk:
:return:
"""
address = Address.objects.filter(id=pk)[0]
new_title = request.data["title"]
address.title = new_title
address.save()
return Response("ok")
序列化器:
# /apps/areas/serializers.py
class AddressSerializers(serializers.ModelSerializer):
"""收货地址序列化器"""
"""显示指名地址的id"""
province_id = serializers.IntegerField(label='省ID', required=True,write_only= True)
city_id = serializers.IntegerField(label='市ID', required=True,write_only= True)
district_id = serializers.IntegerField(label='区ID', required=True,write_only= True)
"""序列化输出"""
province = serializers.StringRelatedField(read_only=True)
city = serializers.StringRelatedField(read_only=True)
district = serializers.StringRelatedField(read_only=True)
class Meta:
model = Address
exclude = ('user',)
def validated_mobile(self, value):
"""
验证手机号格式
:param value:
:return:
"""
if not re.match(r'1[3-9]\d{9}$', value):
raise serializers.ValidationError("手机号格式错误")
return value
def create(self, validated_data):
"""
重写父类方法
需要在验证后的数据添加user
:param validated_data:
:return:
"""
user = self.context['request'].user
validated_data['user'] = user
address = super().create(validated_data)
return address