开源web框架django知识总结(十三)
省市区三级联动
展示收货地址界面
提示:
- 省市区数据是在收货地址界面展示的,所以我们先渲染出收货地址界面。
- 收货地址界面中基础的交互已经提前实现。
1.新建app areas,新建子urls.py,同步,注册areas,
python ../../manage.py startapp areas
- 准备省市区模型和数据 areas.models.py
class Area(models.Model):
"""
行政区划
"""
# 创建 name 字段, 用户保存名称
name = models.CharField(max_length=20,
verbose_name='名称')
# 自关联字段 parent
# 第一个参数是 self : parent关联自己.
# on_delete=models.SET_NULL: 如果省删掉了,省内其他的信息为 NULL
# related_name='subs': 设置之后
# 我们就这样调用获取市: area.area_set.all() ==> area.subs.all()
parent = models.ForeignKey('self',
on_delete=models.SET_NULL,
related_name='subs',
null=True, #表示数据库创建时该字段可不填,用NULL填充
blank=True, # 表示代码中创建数据库记录时该字段可传空白(空串,空字符串).
verbose_name='上级行政区划')
class Meta:
db_table = 'tb_areas'
verbose_name = '行政区划'
verbose_name_plural = '行政区划'
def __str__(self):
return self.name
迁移模型类
python manage.py makemigrations
python manage.py migrate
模型说明:
-
自关联字段的外键指向自身,所以
models.ForeignKey('self')
-
反向查询:没有外键属性一方,可以调用反向属性查询到关联的另一方。
反向关联属性为“实例对象.引用类名(小写)”,使用
related_name
指明父级查询子级数据的语法
- 默认
Area模型类对象.area_set
语法
related_name='subs'
-
- 现在
Area模型类对象.subs
语法
- 现在
导入省市区数据:在项目根目录下建立scripts文件夹,将数据库文件areas.sql拷贝进来,在xshell进入到目录中执行下面语句。
# mysql -u数据库用户名 -p数据库密码 -D 数据库 < areas.sql #注意要在数据库文件所在的目录内执行
mysql -usuifeng -p123456 -D aerf_mall < areas.sql
注意:如果出现错误信息:
ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/tmp/mysql.sock’ (2)
mysql.service failed because the control process exited with error code问题
应该配置为bind-address=0.0.0.0,并且这行应该加在/etc/mysql/mysql.conf.d/mysqld.cnf
配置文件里.
也可以重启一下虚拟机试试,或者用下面的方法:
# mysql -h127.0.0.1 -u数据库用户名 -p数据库密码 -D 数据库 < areas.sql #注意要在数据库文件所在的目录
mysql -h127.0.0.1 -usuifeng -p123456 -D aerf_mall < areas.sql
3. 查询省市区数据
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /areas/ |
2.请求参数:查询参数
- 如果前端没有传入
area_id
,表示用户需要省份数据- 如果前端传入了
area_id
,表示用户需要市或区数据
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
area_id | string | 否 | 地区ID |
3.响应结果:JSON
- 省份数据
{
"code":"0",
"errmsg":"OK",
"province_list":[
{
"id":110000,
"name":"北京市"
},
{
"id":120000,
"name":"天津市"
},
{
"id":130000,
"name":"河北省"
},
......
]
}
市或区数据
{
"code":"0",
"errmsg":"OK",
"sub_data":{
"id":130000,
"name":"河北省",
"subs":[
{
"id":130100,
"name":"石家庄市"
},
......
]
}
}
4.查询省市区数据后端逻辑实现
- 如果前端没有传入
area_id
,表示用户需要省份数据 - 如果前端传入了
area_id
,表示用户需要市或区数据
获取可选省份信息、获取可选市区信息 areas.views.py
from django.views import View
from django.http import JsonResponse
from .models import Area
from django.core.cache import cache
# Create your views here.
# 获取可选省份信息
class ProvinceAreasView(View):
def get(self, request):
# 优先判断缓存中有没有数据
p_list = cache.get('province_list')
if not p_list:
# 把省信息按照格式返回
# 1、读取模型类查询集
provinces = Area.objects.filter(
parent=None
)
# 2、把所有的模型类对象,转化成字典{id, name},json不认模型对象
p_list = []
for province in provinces:
# province: 是省模型类对象
p_list.append({
'id': province.id,
'name': province.name
})
# 读取mysql省数据之后,写入缓存
# cache模块写入缓存是key-value形式
cache.set('province_list', p_list, 3600)
# 3、构建响应返回
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'province_list': p_list
})
# 获取可选市区信息
class SubAreasView(View):
def get(self, request, pk):
# 路径中传入的pk
# 1、pk是省的主键,请求所有市信息
# 2、pk是市的主键,请求所有区信息
sub_data = cache.get('sub_area_%s'%pk)
if not sub_data:
# 当前pk过滤出的父级行政区对象
p_area = Area.objects.get(
pk=pk
)
# 当前父级行政区对象关联的多个子级行政区
subs = Area.objects.filter(
parent_id=pk
)
sub_list = []
for sub in subs:
# sub是子级行政区对象
sub_list.append({
'id': sub.id,
'name': sub.name
})
sub_data = {
'id': p_area.id,
'name': p_area.name,
'subs': sub_list
}
cache.set('sub_area_%s'%pk, sub_data, 3600)
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'sub_data': sub_data
})
5. areas.urls.py
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^areas/$', views.ProvinceAreasView.as_view()),
re_path(r'^areas/(?P<pk>[1-9]\d+)/$', views.SubAreasView.as_view()),
]
收货地址
用户地址的主要业务逻辑有:
- 展示省市区数据
- 用户地址的增删改查处理
- 设置默认地址
- 设置地址标题
==============================
新增地址前后端逻辑
1. 定义用户地址模型类 user.models.py
1.用户地址模型类
from aerf_mall.utils.BaseModel import BaseModel
class Address(BaseModel):
"""
用户地址
"""
user = models.ForeignKey(User,
on_delete=models.CASCADE,
related_name='addresses',
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='区')
title = models.CharField(max_length=20, verbose_name='地址名称')
receiver = models.CharField(max_length=20, 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_addresses'
verbose_name = '用户地址'
verbose_name_plural = verbose_name
# 定义默认查询集排序方式
ordering = ['-update_time']
2.Address模型类说明
-
Address
模型类中的外键指向areas/models
里面的Area
。指明外键时,可以使用应用名.模型类名
来定义。 -
ordering
表示在进行排序展示
Address
查询时,默认使用的排序方式。
ordering = ['-update_time']
: 根据更新的时间倒叙。
3.补充用户模型默认地址字段
class User(AbstractUser):
"""自定义用户模型类"""
mobile = models.CharField(
unique=True,
verbose_name='手机号',
null=True,
max_length=11
)
# 新增 email_active 字段
# 用于记录邮箱是否激活, 默认为 False: 未激活
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 Meta:
db_table = 'tb_users'
verbose_name = '用户'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
执行数据迁移:
python manage.py makemigrations
python manage.py migrate
2. 新增地址接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | POST |
请求地址 | /addresses/create/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
receiver | string | 是 | 收货人 |
province_id | string | 是 | 省份ID |
city_id | string | 是 | 城市ID |
district_id | string | 是 | 区县ID |
place | string | 是 | 收货地址 |
mobile | string | 是 | 手机号 |
tel | string | 否 | 固定电话 |
string | 否 | 邮箱 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
id | 地址ID |
receiver | 收货人 |
province | 省份名称 |
city | 城市名称 |
district | 区县名称 |
place | 收货地址 |
mobile | 手机号 |
tel | 固定电话 |
邮箱 |
3. 新增地址后端逻辑实现 users.views.py
提示:
- 用户地址数量有上限,最多20个,超过地址数量上限就返回错误信息
from .models import Address
# 新增用户地址 re_path(r'^addresses/create/$', CreateAddressView.as_view()),
class CreateAddressView(View):
def post(self, request):
# 1、提取参数
data = json.loads(request.body.decode())
receiver = data.get('receiver')
province_id = data.get('province_id')
city_id = data.get('city_id')
district_id = data.get('district_id')
place = data.get('place') # 详细地址
mobile = data.get('mobile')
tel = data.get('tel')
email = data.get('email')
# 判断用户地址数量是否超过10个
user = request.user
count = Address.objects.filter(user=user).count()
if count >= 10:
return JsonResponse({'code': 400, 'errmsg': '数量超限'})
# 2、校验参数
if not all([receiver, province_id, city_id, district_id, place, mobile]):
return JsonResponse({"code": 400, 'errmsg': '缺少参数!'})
if not re.match(r'^1[3-9]\d{9}$', mobile):
return JsonResponse({'code': 400,
'errmsg': '参数mobile有误'})
if tel:
if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
return JsonResponse({'code': 400,
'errmsg': '参数tel有误'})
if email:
if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return JsonResponse({'code': 400,
'errmsg': '参数email有误'})
# 3、新建用户地址
try:
address = Address.objects.create(
user=user,
province_id=province_id,
city_id=city_id,
district_id=district_id,
title=receiver, # 当前地址的标题,默认收货人名称就作为地址标题
receiver=receiver,
place=place,
mobile=mobile,
tel=tel
)
# 如果当前新增地址的时候,用户没有设置默认地址,那么
# 我们把当前新增的地址设置为用户的默认地址
if not user.default_address:
user.default_address = address
user.save()
except Exception as e:
print(e)
return JsonResponse({'code': 400, 'errmsg': '新增地址失败!'})
address_info = {
"id": address.id,
"title": address.title,
"receiver": address.receiver,
"province": address.province.name,
"city": address.city.name,
"district": address.district.name,
"place": address.place,
"mobile": address.mobile,
"tel": address.tel,
"email": address.email
}
# 4、返回响应
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'address': address_info
})
注意:循环调用问题。
===============================
展示地址前后端逻辑
1. 展示地址接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /addresses/ |
2.请求参数 无
2. 展示地址后端逻辑实现
# 网页地址展示接口 re_path(r'^addresses/$', AddressView.as_view()),
# 本质:把当前用户所有地址信息返回
class AddressView(View):
def get(self, request):
# 1、根据用户,过滤出当前用户的所有地址
user = request.user
addresses = Address.objects.filter(
user=user,
is_deleted=False # 没有逻辑删除的地址
)
# 2、把地址转化成字典
address_list = []
for address in addresses:
if address.id != user.default_address_id:
# address:每一个地址对象
address_list.append({
'id': address.id,
'title': address.title,
'receiver': address.receiver,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email
})
else:
address_list.insert(0, {
'id': address.id,
'title': address.title,
'receiver': address.receiver,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email
})
# 3、构建响应返回
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'default_address_id': user.default_address_id,
'addresses': address_list
})
========================
修改地址前后端逻辑
1. 修改地址接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | delete、PUT |
请求地址 | /addresses/(?P<address_id>\d+)/ |
2.请求参数:路径参数 和 JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
address_id | string | 是 | 要修改的地址ID(路径参数) |
receiver | string | 是 | 收货人 |
province_id | string | 是 | 省份ID |
city_id | string | 是 | 城市ID |
district_id | string | 是 | 区县ID |
place | string | 是 | 收货地址 |
mobile | string | 是 | 手机号 |
tel | string | 否 | 固定电话 |
string | 否 | 邮箱 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
id | 地址ID |
receiver | 收货人 |
province | 省份名称 |
city | 城市名称 |
district | 区县名称 |
place | 收货地址 |
mobile | 手机号 |
tel | 固定电话 |
邮箱 |
2. 修改地址后端逻辑实现
提示
- 删除地址后端逻辑和新增地址后端逻辑非常的相似。
- 都是更新用户地址模型类,需要保存用户地址信息。
# 总结:相同的请求路径+不同的请求方法 = 统一类视图中
#re_path(r'^addresses/(?P<address_id>\d+)/$', UpdateDestroyAddressView.as_view()),
class UpdateDestroyAddressView(View):
# 删除地址
def delete(self, request, address_id):
# 1、根据路径中的地址主键,获取地址对象
try:
address = Address.objects.get(pk=address_id)
except Address.DoesNotExist as e:
print(e)
return JsonResponse({'code': 400, 'errmsg': '地址不存在'}, status=404)
# 2、通过对象删除(真删除,逻辑删除)
# 真删除: address.delete()
# 逻辑删除
address.is_deleted = True
address.save()
# 3、构建响应
return JsonResponse({
'code': 0,
'errmsg': 'ok'
})
# 更新地址接口
def put(self, request, address_id):
# 1、获取被更新的地址
try:
address = Address.objects.get(pk=address_id)
except Address.DoesNotExist as e:
print(e)
return JsonResponse({'code': 400, 'errmsg': '资源未找到!'})
# 2、提取参数
data = json.loads(request.body.decode())
receiver = data.get('receiver')
province_id = data.get('province_id')
city_id = data.get('city_id')
district_id = data.get('district_id')
place = data.get('place') # 详细地址
mobile = data.get('mobile')
tel = data.get('tel')
email = data.get('email')
# 3、校验参数
if not all([receiver, province_id, city_id, district_id, place, mobile]):
return JsonResponse({"code": 400, 'errmsg': '缺少参数!'})
if not re.match(r'^1[3-9]\d{9}$', mobile):
return JsonResponse({'code': 400,
'errmsg': '参数mobile有误'})
if tel:
if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
return JsonResponse({'code': 400,
'errmsg': '参数tel有误'})
if email:
if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return JsonResponse({'code': 400,
'errmsg': '参数email有误'})
# 构造数据存储
address.receiver = receiver
address.province_id = province_id
address.city_id = city_id
address.district_id = district_id
address.place = place
address.mobile = mobile
address.tel = tel
address.email = email
address.save()
# 构造返回前端数据
address_info = {
"id": address.id,
"title": address.title,
"receiver": address.receiver,
"province": address.province.name,
"city": address.city.name,
"district": address.district.name,
"place": address.place,
"mobile": address.mobile,
"tel": address.tel,
"email": address.email
}
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'address': address_info
})
================================
设置默认地址
1. 设置默认地址接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | PUT |
请求地址 | /addresses/(?P<address_id>\d+)/default/ |
2.请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
address_id | string | 是 | 要修改的地址ID(路径参数) |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
2. 设置默认地址后端逻辑实现
# 设置默认地址 re_path(r'^addresses/(?P<address_id>\d+)/default/$', DefaultAddressView.as_view()),
class DefaultAddressView(View):
def put(self, request, address_id):
# 修改当前登陆用户对象的default_address指向address_id的地址
user = request.user
# default_address是ForeignKey类型,是Address对象
# user.default_address = <Address对象>
# user.default_address = Address.objects.get(pk=address_id)
# user.default_address_id = <Address对象的主键>
user.default_address_id = address_id
user.save()
return JsonResponse({
'code': 0,
'errmsg': 'ok'
})
==========================
修改地址标题
1. 修改地址标题接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | PUT |
请求地址 | /addresses/(?P<address_id>\d+)/title/ |
2.请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
address_id | string | 是 | 要修改的地址ID(路径参数) |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
2. 修改地址标题后端逻辑实现
# 修改地址标题 re_path(r'^addresses/(?P<address_id>\d+)/title/$', UpdateTitleAddressView.as_view()),
class UpdateTitleAddressView(View):
def put(self, request, address_id):
# 1、获取更新数据
data = json.loads(request.body.decode())
title = data.get('title')
# 2、获取被修改的地址对象
address = Address.objects.get(pk=address_id)
# 3、修改并返回响应
address.title = title
address.save()
return JsonResponse({'code': 0, 'errmsg': 'ok'})
==============================
修改密码
1. 修改密码后端逻辑
提示:
- 修改密码前需要校验原始密码是否正确,以校验修改密码的用户身份。
- 如果原始密码正确,再将新的密码赋值给用户。
# 修改用户密码 re_path(r'^password/$', ChangePasswordView.as_view()),
class ChangePasswordView(View):
def put(self, request):
# 1、提取参数
data = json.loads(request.body.decode())
old_password = data.get('old_password')
new_password = data.get('new_password')
new_password2 = data.get('new_password2')
# 2、校验参数
if not all([old_password, new_password, new_password2]):
return JsonResponse({'code':400, 'errmsg': '参数缺失'})
# 新密码格式校验
if not re.match(r'^[0-9A-Za-z]{8,20}$', new_password):
return JsonResponse({'code': 400,
'errmsg': '密码最少8位,最长20位'})
# 两次输入是否一致校验
if new_password != new_password2:
return JsonResponse({'code': 400,
'errmsg': '两次输入密码不一致'})
# 旧密码校验
# User.set_password()
# User.check_password()
user = request.user
if not user.check_password(old_password):
return JsonResponse({'code': 400, 'errmsg': '旧密码有误!'}, status=400)
# 3、更新数据
user.set_password(new_password)
user.save()
# 补充逻辑:清楚登陆状态
logout(request)
# 4、返回响应
response = JsonResponse({'code': 0, 'errmsg': 'ok'})
response.delete_cookie('username')
return response
========================
补全users.urls.py中的urlpatterns:
# 新增收货地址
re_path(r'^addresses/create/$', CreateAddressView.as_view()),
# 展示地址
re_path(r'^addresses/$', AddressView.as_view()),
# 修改地址
re_path(r'^addresses/(?P<address_id>\d+)/$', UpdateDestroyAddressView.as_view()),
# 修改默认地址
re_path(r'^addresses/(?P<address_id>\d+)/default/$', DefaultAddressView.as_view()),
# 修改标题
re_path(r'^addresses/(?P<address_id>\d+)/title/$', UpdateTitleAddressView.as_view()),
# 修改密码
re_path(r'^password/$', ChangePasswordView.as_view()),
替换user_center_site.js(其中有2个链接接口不对,一个参数,后端未做处理)
========================