Python_美多商城(用户中心)_5

用户基本信息

用户基本信息逻辑分析

1. 用户基本信息逻辑分析

Python_美多商城(用户中心)_5_第1张图片

以下是要实现的后端逻辑

  1. 用户模型补充email_active字段
  2. 查询并渲染用户基本信息
  3. 添加邮箱
  4. 发送邮箱验证邮件
  5. 验证邮箱

提示:

  • 用户添加邮箱时,界面的局部刷新,我们选择使用Vue.js来实现。

===========================

查询并渲染用户基本信息

1. 用户模型补充email_active字段

  • 由于在渲染用户基本信息时,需要渲染用户邮箱验证的状态,所以需要给用户模型补充email_active字段

  • 补充完字段后,需要进行迁移。

      $ python manage.py makemigrations
      $ python manage.py migrate
    
class User(AbstractUser):
    """自定义用户模型类"""
    mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
    email_active = models.BooleanField(default=False, verbose_name='邮箱验证状态')

    class Meta:
        db_table = 'tb_users'
        verbose_name = '用户'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

2. 查询用户基本信息

class UserInfoView(LoginRequiredMixin, View):
    """用户中心"""

    def get(self, request):
        """提供个人信息界面"""
        context = {
            'username': request.user.username,
            'mobile': request.user.mobile,
            'email': request.user.email,
            'email_active': request.user.email_active
        }
        return render(request, 'user_center_info.html', context=context)

3. 渲染用户基本信息

1.将后端模板数据传递到Vue.js

  • 为了方便实现用户添加邮箱时的界面局部刷新
    • 我们将后端提供的用户数据传入到user_center_info.js



data: {
    username: username,
    mobile: mobile,
    email: email,
    email_active: email_active,
},

2.Vue渲染用户基本信息:user_center_info.html

基本信息

=============================

添加和验证邮箱

添加邮箱后端逻辑

1. 添加邮箱接口设计和定义

1.请求方式

选项 方案
请求方法 PUT
请求地址 /emails/

2.请求参数

参数名 类型 是否必传 说明
email string 邮箱

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息

2. 添加邮箱后端逻辑实现

class EmailView(View):
    """添加邮箱"""

    def put(self, request):
        """实现添加邮箱逻辑"""
        # 接收参数
        json_dict = json.loads(request.body.decode())
        email = json_dict.get('email')

        # 校验参数
        if not email:
            return http.HttpResponseForbidden('缺少email参数')
        if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return http.HttpResponseForbidden('参数email有误')

        # 赋值email字段
        try:
            request.user.email = email
            request.user.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '添加邮箱失败'})

        # 响应添加邮箱结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '添加邮箱成功'})

3. 判断用户是否登录并返回JSON

重要提示:

  • 只有用户登录时才能让其绑定邮箱。
  • 此时前后端交互的数据类型是JSON,所以需要判断用户是否登录并返回JSON给用户。

方案一:

  • 使用Django用户认证系统提供的is_authenticated()
class EmailView(View):
    """添加邮箱"""

    def put(self, request):
        """实现添加邮箱逻辑"""
        # 判断用户是否登录并返回JSON
        if not request.user.is_authenticated():
            return http.JsonResponse({'code': RETCODE.SESSIONERR, 'errmsg': '用户未登录'})
        pass

方案二:

  • 自定义返回JSON的login_required装饰器
  • meiduo_mall.utils.views.py
def login_required_json(view_func):
    """
    判断用户是否登录的装饰器,并返回json
    :param view_func: 被装饰的视图函数
    :return: json、view_func
    """
    # 恢复view_func的名字和文档
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):

        # 如果用户未登录,返回json数据
        if not request.user.is_authenticated():
            return http.JsonResponse({'code': RETCODE.SESSIONERR, 'errmsg': '用户未登录'})
        else:
            # 如果用户登录,进入到view_func中
            return view_func(request, *args, **kwargs)

    return wrapper


class LoginRequiredJSONMixin(object):
    """验证用户是否登陆并返回json的扩展类"""
    @classmethod
    def as_view(cls, **initkwargs):
        view = super().as_view(**initkwargs)
        return login_required_json(view)

LoginRequiredJSONMixin的使用

class EmailView(LoginRequiredJSONMixin, View):
    """添加邮箱"""

    def put(self, request):
        """实现添加邮箱逻辑"""
        # 判断用户是否登录并返回JSON
        pass

===========================

Django发送邮件的配置

Python_美多商城(用户中心)_5_第2张图片

1. Django发送邮件流程分析

Python_美多商城(用户中心)_5_第3张图片

send_mall()方法介绍

  • 位置:
    • django.core.mail模块提供了send_mail()来发送邮件。
  • 方法参数:
    • send_mail(subject, message, from_email, recipient_list, html_message=None)
subject 邮件标题
message 普通邮件正文,普通字符串
from_email 发件人
recipient_list 收件人列表
html_message 多媒体邮件正文,可以是html字符串

2. 准备发邮件服务器

1.点击进入《设置》界面

Python_美多商城(用户中心)_5_第4张图片

3.开启《授权码》,并完成验证短信

Python_美多商城(用户中心)_5_第5张图片

4.填写《授权码》

Python_美多商城(用户中心)_5_第6张图片

5.完成《授权码》设置

Python_美多商城(用户中心)_5_第7张图片

6.配置邮件服务器

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 指定邮件后端
EMAIL_HOST = 'smtp.163.com' # 发邮件主机
EMAIL_PORT = 25 # 发邮件端口
EMAIL_HOST_USER = '[email protected]' # 授权的邮箱
EMAIL_HOST_PASSWORD = 'hmmeiduo123' # 邮箱授权时获得的密码,非注册登录密码
EMAIL_FROM = '美多商城' # 发件人抬头

============================= 

发送邮箱验证邮件

重要提示:

  • 发送邮箱验证邮件是耗时的操作,不能阻塞美多商城的响应,所以需要异步发送邮件
  • 我们继续使用Celery实现异步任务。

1. 定义和调用发送邮件异步任务

Python_美多商城(用户中心)_5_第8张图片

@celery_app.task(bind=True, name='send_verify_email', retry_backoff=3)
def send_verify_email(self, to_email, verify_url):
    """
    发送验证邮箱邮件
    :param to_email: 收件人邮箱
    :param verify_url: 验证链接
    :return: None
    """
    subject = "美多商城邮箱验证"
    html_message = '

尊敬的用户您好!

' \ '

感谢您使用美多商城。

' \ '

您的邮箱为:%s 。请点击此链接激活您的邮箱:

' \ '

%s

' % (to_email, verify_url, verify_url) try: send_mail(subject, "", settings.EMAIL_FROM, [to_email], html_message=html_message) except Exception as e: logger.error(e) # 有异常自动重试三次 raise self.retry(exc=e, max_retries=3)

2.注册发邮件的任务:main.py

  • 在发送邮件的异步任务中,我们用到了Django的配置文件。
  • 所以我们需要修改celery的启动文件main.py。
  • 在其中指明celery可以读取的Django配置文件。
  • 最后记得注册新添加的email的任务
# celery启动文件
from celery import Celery


# 为celery使用django配置文件进行设置
import os
if not os.getenv('DJANGO_SETTINGS_MODULE'):
    os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev'

# 创建celery实例
celery_app = Celery('meiduo')

# 加载celery配置
celery_app.config_from_object('celery_tasks.config')

# 自动注册celery任务
celery_app.autodiscover_tasks(['celery_tasks.sms', 'celery_tasks.email'])

3.调用发送邮件异步任务

# 赋值email字段
try:
    request.user.email = email
    request.user.save()
except Exception as e:
    logger.error(e)
    return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '添加邮箱失败'})

# 异步发送验证邮件
verify_url = '邮件验证链接'
send_verify_email.delay(email, verify_url)

# 响应添加邮箱结果
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '添加邮箱成功'})

4.启动Celery

$ celery -A celery_tasks.main worker -l info

2. 生成邮箱验证链接

1.定义生成邮箱验证链接方法

def generate_verify_email_url(user):
    """
    生成邮箱验证链接
    :param user: 当前登录用户
    :return: verify_url
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=constants.VERIFY_EMAIL_TOKEN_EXPIRES)
    data = {'user_id': user.id, 'email': user.email}
    token = serializer.dumps(data).decode()
    verify_url = settings.EMAIL_VERIFY_URL + '?token=' + token
    return verify_url

2.配置相关参数

# 邮箱验证链接
EMAIL_VERIFY_URL = 'http://www.meiduo.site:8000/emails/verification/'

3.使用邮箱验证链接

verify_url = generate_verify_email_url(request.user)
send_verify_email.delay(email, verify_url)

=============================

验证邮箱后端逻辑

1. 验证邮箱接口设计和定义

1.请求方式

选项 方案
请求方法 GET
请求地址 /emails/verification/

2.请求参数:查询参数

参数名 类型 是否必传 说明
token string 邮箱激活链接

3.响应结果:HTML

字段 说明
邮箱验证失败 响应错误提示
邮箱验证成功 重定向到用户中心

2. 验证链接提取用户信息

def check_verify_email_token(token):
    """
    验证token并提取user
    :param token: 用户信息签名后的结果
    :return: user, None
    """
    serializer = Serializer(settings.SECRET_KEY, expires_in=constants.VERIFY_EMAIL_TOKEN_EXPIRES)
    try:
        data = serializer.loads(token)
    except BadData:
        return None
    else:
        user_id = data.get('user_id')
        email = data.get('email')
        try:
            user = User.objects.get(id=user_id, email=email)
        except User.DoesNotExist:
            return None
        else:
            return user

3. 验证邮箱后端逻辑实现

验证邮箱的核心:就是将用户的email_active字段设置为True

class VerifyEmailView(View):
    """验证邮箱"""

    def get(self, request):
        """实现邮箱验证逻辑"""
        # 接收参数
        token = request.GET.get('token')

        # 校验参数:判断token是否为空和过期,提取user
        if not token:
            return http.HttpResponseBadRequest('缺少token')

        user = check_verify_email_token(token)
        if not user:
            return http.HttpResponseForbidden('无效的token')

        # 修改email_active的值为True
        try:
            user.email_active = True
            user.save()
        except Exception as e:
            logger.error(e)
            return http.HttpResponseServerError('激活邮件失败')

        # 返回邮箱验证结果
        return redirect(reverse('users:info'))

==========================

收货地址

Python_美多商城(用户中心)_5_第9张图片

Python_美多商城(用户中心)_5_第10张图片

用户地址的主要业务逻辑有:

  1. 展示省市区数据
  2. 用户地址的增删改查处理
  3. 设置默认地址
  4. 设置地址标题

==================================

省市区三级联动

1. 展示收货地址界面

提示:

  • 省市区数据是在收货地址界面展示的,所以我们先渲染出收货地址界面。
  • 收货地址界面中基础的交互已经提前实现。
class AddressView(LoginRequiredMixin, View):
    """用户收货地址"""

    def get(self, request):
        """提供收货地址界面"""
        return render(request, 'user_center_site.html')

2. 准备省市区模型和数据

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

模型说明:

  • 自关联字段的外键指向自身,所以 models.ForeignKey('self')
  • 使用related_name指明父级查询子级数据的语法
    • 默认Area模型类对象.area_set语法
  • related_name='subs'
    • 现在Area模型类对象.subs语法

导入省市区数据

mysql -h数据库ip地址 -u数据库用户名 -p数据库密码 数据库 < areas.sql
mysql -h127.0.0.1 -uroot -pmysql meiduo_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,表示用户需要市或区数据
class AreasView(View):
    """省市区数据"""

    def get(self, request):
        """提供省市区数据"""
        area_id = request.GET.get('area_id')

        if not area_id:
            # 提供省份数据
            try:
                # 查询省份数据
                province_model_list = Area.objects.filter(parent__isnull=True)

                # 序列化省级数据
                province_list = []
                for province_model in province_model_list:
                    province_list.append({'id': province_model.id, 'name': province_model.name})
            except Exception as e:
                logger.error(e)
                return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '省份数据错误'})

            # 响应省份数据
            return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'province_list': province_list})
        else:
            # 提供市或区数据
            try:
                parent_model = Area.objects.get(id=area_id)  # 查询市或区的父级
                sub_model_list = parent_model.subs.all()

                # 序列化市或区数据
                sub_list = []
                for sub_model in sub_model_list:
                    sub_list.append({'id': sub_model.id, 'name': sub_model.name})

                sub_data = {
                    'id': parent_model.id,  # 父级pk
                    'name': parent_model.name,  # 父级name
                    'subs': sub_list  # 父级的子集
                }
            except Exception as e:
                logger.error(e)
                return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '城市或区数据错误'})

            # 响应市或区数据
            return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'sub_data': sub_data})

4. Vue渲染省市区数据

1.user_center_site.js

mounted() {
    // 获取省份数据
    this.get_provinces();
},
// 获取省份数据
get_provinces(){
    let url = '/areas/';
    axios.get(url, {
        responseType: 'json'
    })
        .then(response => {
            if (response.data.code == '0') {
                this.provinces = response.data.province_list;
            } else {
                console.log(response.data);
                this.provinces = [];
            }
        })
        .catch(error => {
            console.log(error.response);
            this.provinces = [];
        })
},
watch: {
    // 监听到省份id变化
    'form_address.province_id': function(){
        if (this.form_address.province_id) {
            let url = '/areas/?area_id=' + this.form_address.province_id;
            axios.get(url, {
                responseType: 'json'
            })
                .then(response => {
                    if (response.data.code == '0') {
                        this.cities = response.data.sub_data.subs;
                    } else {
                        console.log(response.data);
                        this.cities = [];
                    }
                })
                .catch(error => {
                    console.log(error.response);
                    this.cities = [];
                })
        }
    },
    // 监听到城市id变化
    'form_address.city_id': function(){
        if (this.form_address.city_id){
            let url = '/areas/?area_id='+ this.form_address.city_id;
            axios.get(url, {
                responseType: 'json'
            })
                .then(response => {
                    if (response.data.code == '0') {
                        this.districts = response.data.sub_data.subs;
                    } else {
                        console.log(response.data);
                        this.districts = [];
                    }
                })
                .catch(error => {
                    console.log(error.response);
                    this.districts = [];
                })
        }
    }
},

2.user_center_site.html

5. 缓存省市区数据

提示:

  • 省市区数据是我们动态查询的结果。
  • 但是省市区数据不是频繁变化的数据,所以没有必要每次都重新查询。
  • 所以我们可以选择对省市区数据进行缓存处理。

1.缓存工具

  • from django.core.cache import cache
  • 存储缓存数据:cache.set('key', 内容, 有效期)
  • 读取缓存数据:cache.get('key')
  • 删除缓存数据:cache.delete('key')
  • 注意:存储进去和读取出来的数据类型相同,所以读取出来后可以直接使用。

2.缓存逻辑

Python_美多商城(用户中心)_5_第11张图片

3.缓存逻辑实现

  • 省份缓存数据
    • cache.set('province_list', province_list, 3600)
  • 市或区缓存数据
    • cache.set('sub_area_' + area_id, sub_data, 3600)
class AreasView(View):
    """省市区数据"""

    def get(self, request):
        """提供省市区数据"""
        area_id = request.GET.get('area_id')

        if not area_id:
            # 读取省份缓存数据
            province_list = cache.get('province_list')

            if not province_list:
                # 提供省份数据
                try:
                    # 查询省份数据
                    province_model_list = Area.objects.filter(parent__isnull=True)

                    # 序列化省级数据
                    province_list = []
                    for province_model in province_model_list:
                        province_list.append({'id': province_model.id, 'name': province_model.name})
                except Exception as e:
                    logger.error(e)
                    return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '省份数据错误'})

                # 存储省份缓存数据
                cache.set('province_list', province_list, 3600)

            # 响应省份数据
            return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'province_list': province_list})
        else:
            # 读取市或区缓存数据
            sub_data = cache.get('sub_area_' + area_id)

            if not sub_data:
                # 提供市或区数据
                try:
                    parent_model = Area.objects.get(id=area_id)  # 查询市或区的父级
                    sub_model_list = parent_model.subs.all()

                    # 序列化市或区数据
                    sub_list = []
                    for sub_model in sub_model_list:
                        sub_list.append({'id': sub_model.id, 'name': sub_model.name})

                    sub_data = {
                        'id': parent_model.id,  # 父级pk
                        'name': parent_model.name,  # 父级name
                        'subs': sub_list  # 父级的子集
                    }
                except Exception as e:
                    logger.error(e)
                    return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '城市或区数据错误'})

                # 储存市或区缓存数据
                cache.set('sub_area_' + area_id, sub_data, 3600)

            # 响应市或区数据
            return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'sub_data': sub_data})

==============================

新增地址前后端逻辑

1. 定义用户地址模型类

1.用户地址模型类

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']

2.Address模型类说明

  • Address模型类中的外键指向areas/models里面的Area。指明外键时,可以使用应用名.模型类名来定义。
  • ordering 表示在进行Address查询时,默认使用的排序方式。
    • ordering = ['-update_time'] : 根据更新的时间倒叙。

3.补充用户模型默认地址字段

class User(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 Meta:
        db_table = 'tb_users'
        verbose_name = '用户'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

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 固定电话
email string 邮箱

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息
id 地址ID
receiver 收货人
province 省份名称
city 城市名称
district 区县名称
place 收货地址
mobile 手机号
tel 固定电话
email 邮箱

3. 新增地址后端逻辑实现

提示:

  • 用户地址数量有上限,最多20个,超过地址数量上限就返回错误信息
class CreateAddressView(LoginRequiredJSONMixin, View):
    """新增地址"""

    def post(self, request):
        """实现新增地址逻辑"""
        # 判断是否超过地址上限:最多20个
        # Address.objects.filter(user=request.user).count()
        count = request.user.addresses.count()
        if count >= constants.USER_ADDRESS_COUNTS_LIMIT:
            return http.JsonResponse({'code': RETCODE.THROTTLINGERR, 'errmsg': '超过地址数量上限'})

        # 接收参数
        json_dict = json.loads(request.body.decode())
        receiver = json_dict.get('receiver')
        province_id = json_dict.get('province_id')
        city_id = json_dict.get('city_id')
        district_id = json_dict.get('district_id')
        place = json_dict.get('place')
        mobile = json_dict.get('mobile')
        tel = json_dict.get('tel')
        email = json_dict.get('email')

        # 校验参数
        if not all([receiver, province_id, city_id, district_id, place, mobile]):
            return http.HttpResponseForbidden('缺少必传参数')
        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return http.HttpResponseForbidden('参数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 http.HttpResponseForbidden('参数tel有误')
        if email:
            if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
                return http.HttpResponseForbidden('参数email有误')

        # 保存地址信息
        try:
            address = Address.objects.create(
                user=request.user,
                title = receiver,
                receiver = receiver,
                province_id = province_id,
                city_id = city_id,
                district_id = district_id,
                place = place,
                mobile = mobile,
                tel = tel,
                email = email
            )

            # 设置默认地址
            if not request.user.default_address:
                request.user.default_address = address
                request.user.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '新增地址失败'})

        # 新增地址成功,将新增的地址响应给前端实现局部刷新
        address_dict = {
            "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 http.JsonResponse({'code': RETCODE.OK, 'errmsg': '新增地址成功', 'address':address_dict})

4. 新增地址前端逻辑实现

save_address(){
    if (this.error_receiver || this.error_place || this.error_mobile || this.error_email || !this.form_address.province_id || !this.form_address.city_id || !this.form_address.district_id ) {
        alert('信息填写有误!');
    } else {
        // 新增地址
        let url = '/addresses/create/';
        axios.post(url, this.form_address, {
            headers: {
                'X-CSRFToken':getCookie('csrftoken')
            },
            responseType: 'json'
        })
            .then(response => {
                if (response.data.code == '0') {
                    // 局部刷新界面:展示所有地址信息

                } else if (response.data.code == '4101') {
                    location.href = '/login/?next=/addresses/';
                } else {
                    alert(response.data.errmsg);
                }
            })
            .catch(error => {
                console.log(error.response);
            })
    }
},

===============================

展示地址前后端逻辑

1. 展示地址接口设计和定义

1.请求方式

选项 方案
请求方法 GET
请求地址 /addresses/

2.请求参数

3.响应结果:HTML

user_center_site.html

2. 展示地址后端逻辑实现

class AddressView(LoginRequiredMixin, View):
    """用户收货地址"""

    def get(self, request):
        """提供收货地址界面"""
        # 获取用户地址列表
        login_user = request.user
        addresses = Address.objects.filter(user=login_user, is_deleted=False)

        address_dict_list = []
        for address in addresses:
            address_dict = {
                "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
            }

        context = {
            'default_address_id': login_user.default_address_id,
            'addresses': address_dict_list,
        }

        return render(request, 'user_center_site.html', context)

3. 展示地址前端逻辑实现

1.将后端模板数据传递到Vue.js


data: {
    addresses: JSON.parse(JSON.stringify(addresses)),
    default_address_id: default_address_id,
},

2.user_center_site.html中渲染地址信息

新增收货地址 你已创建了[[ addresses.length ]]个收货地址,最多可创建20

[[ address.title ]]

默认地址 ×
  • 收货人:[[ address.receiver ]]
  • 所在地区:[[ address.province ]] [[address.city]] [[ address.district ]]
  • 地址:[[ address.place ]]
  • 手机:[[ address.mobile ]]
  • 固定电话:[[ address.tel ]]
  • 电子邮箱:[[ address.email ]]

3.完善user_center_site.js中成功新增地址后的局部刷新

if (response.data.code == '0') {
    // 局部刷新界面:展示所有地址信息,将新的地址添加到头部
    this.addresses.splice(0, 0, response.data.address);
    this.is_show_edit = false;
}

========================

修改地址前后端逻辑

1. 修改地址接口设计和定义

1.请求方式

选项 方案
请求方法 PUT
请求地址 /addresses/(?P\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 固定电话
email string 邮箱

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息
id 地址ID
receiver 收货人
province 省份名称
city 城市名称
district 区县名称
place 收货地址
mobile 手机号
tel 固定电话
email 邮箱

2. 修改地址后端逻辑实现

提示

  • 删除地址后端逻辑和新增地址后端逻辑非常的相似。
  • 都是更新用户地址模型类,需要保存用户地址信息。
class UpdateDestroyAddressView(LoginRequiredJSONMixin, View):
    """修改和删除地址"""

    def put(self, request, address_id):
        """修改地址"""
        # 接收参数
        json_dict = json.loads(request.body.decode())
        receiver = json_dict.get('receiver')
        province_id = json_dict.get('province_id')
        city_id = json_dict.get('city_id')
        district_id = json_dict.get('district_id')
        place = json_dict.get('place')
        mobile = json_dict.get('mobile')
        tel = json_dict.get('tel')
        email = json_dict.get('email')

        # 校验参数
        if not all([receiver, province_id, city_id, district_id, place, mobile]):
            return http.HttpResponseForbidden('缺少必传参数')
        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return http.HttpResponseForbidden('参数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 http.HttpResponseForbidden('参数tel有误')
        if email:
            if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
                return http.HttpResponseForbidden('参数email有误')

        # 判断地址是否存在,并更新地址信息
        try:
            Address.objects.filter(id=address_id).update(
                user = request.user,
                title = receiver,
                receiver = receiver,
                province_id = province_id,
                city_id = city_id,
                district_id = district_id,
                place = place,
                mobile = mobile,
                tel = tel,
                email = email
            )
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '更新地址失败'})

        # 构造响应数据
        address = Address.objects.get(id=address_id)
        address_dict = {
            "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 http.JsonResponse({'code': RETCODE.OK, 'errmsg': '更新地址成功', 'address': address_dict})

3. 修改地址前端逻辑实现

1.添加修改地址的标记

  • 新增地址和修改地址的交互不同。
  • 为了区分用户是新增地址还是修改地址,我们可以选择添加一个变量,作为标记。
  • 为了方便得到正在修改的地址信息,我们可以选择展示地址时对应的序号作为标记。
data: {
    editing_address_index: '', 
},

2.实现编辑按钮对应的事件

show_edit_site(index){
    this.is_show_edit = true;
    this.clear_all_errors();
    this.editing_address_index = index.toString();
},

3.展示要重新编辑的数据

show_edit_site(index){
    this.is_show_edit = true;
    this.clear_all_errors();
    this.editing_address_index = index.toString();
    // 只获取要编辑的数据
    this.form_address = JSON.parse(JSON.stringify(this.addresses[index]));
},

4.发送修改地址请求

  • 重要提示:
    • 0 == '' 返回true
    • 0 === '' 返回false
    • 为了避免第0个索引出错,我们选择this.editing_address_index === ''的方式进行判断
if (this.editing_address_index === '') {
    // 新增地址
    ......
} else {
    // 修改地址
    let url = '/addresses/' + this.addresses[this.editing_address_index].id + '/';
    axios.put(url, this.form_address, {
        headers: {
            'X-CSRFToken':getCookie('csrftoken')
        },
        responseType: 'json'
    })
        .then(response => {
            if (response.data.code == '0') {
                this.addresses[this.editing_address_index] = response.data.address;
                this.is_show_edit = false;
            } else if (response.data.code == '4101') {
                location.href = '/login/?next=/addresses/';
            } else {
                alert(response.data.errmsg);
            }
        })
        .catch(error => {
            alert(error.response);
        })
}

=========================

删除地址前后端逻辑

1. 删除地址接口设计和定义

1.请求方式

选项 方案
请求方法 DELETE
请求地址 /addresses/(?P\d+)/

2.请求参数:路径参数

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息

2. 删除地址后端逻辑实现

提示:

  • 删除地址不是物理删除,是逻辑删除。
class UpdateDestroyAddressView(LoginRequiredJSONMixin, View):
    """修改和删除地址"""

    def put(self, request, address_id):
        """修改地址"""
        ......

    def delete(self, request, address_id):
        """删除地址"""
        try:
            # 查询要删除的地址
            address = Address.objects.get(id=address_id)

            # 将地址逻辑删除设置为True
            address.is_deleted = True
            address.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '删除地址失败'})

        # 响应删除地址结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '删除地址成功'})

3. 删除地址前端逻辑实现

delete_address(index){
    let url = '/addresses/' + this.addresses[index].id + '/';
    axios.delete(url, {
        headers: {
            'X-CSRFToken':getCookie('csrftoken')
        },
        responseType: 'json'
    })
        .then(response => {
            if (response.data.code == '0') {
                // 删除对应的标签
                this.addresses.splice(index, 1);
            } else if (response.data.code == '4101') {
                location.href = '/login/?next=/addresses/';
            }else {
                alert(response.data.errmsg);
            }
        })
        .catch(error => {
            console.log(error.response);
        })
},

[[ address.title ]]

默认地址 ×

================================

设置默认地址

1. 设置默认地址接口设计和定义

1.请求方式

选项 方案
请求方法 PUT
请求地址 /addresses/(?P\d+)/default/

2.请求参数:路径参数

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息

2. 设置默认地址后端逻辑实现

class DefaultAddressView(LoginRequiredJSONMixin, View):
    """设置默认地址"""

    def put(self, request, address_id):
        """设置默认地址"""
        try:
            # 接收参数,查询地址
            address = Address.objects.get(id=address_id)

            # 设置地址为默认地址
            request.user.default_address = address
            request.user.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '设置默认地址失败'})

        # 响应设置默认地址结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '设置默认地址成功'})

3. 设置默认地址前端逻辑实现

set_default(index){
    let url = '/addresses/' + this.addresses[index].id + '/default/';
    axios.put(url, {}, {
        headers: {
            'X-CSRFToken':getCookie('csrftoken')
        },
        responseType: 'json'
    })
        .then(response => {
            if (response.data.code == '0') {
                // 设置默认地址标签
                this.default_address_id = this.addresses[index].id;
            } else if (response.data.code == '4101') {
                location.href = '/login/?next=/addresses/';
            } else {
                alert(response.data.errmsg);
            }
        })
        .catch(error => {
            console.log(error.response);
        })
},

==========================

修改地址标题

1. 修改地址标题接口设计和定义

1.请求方式

选项 方案
请求方法 PUT
请求地址 /addresses/(?P\d+)/title/

2.请求参数:路径参数

参数名 类型 是否必传 说明
address_id string 要修改的地址ID(路径参数)

3.响应结果:JSON

字段 说明
code 状态码
errmsg 错误信息

2. 修改地址标题后端逻辑实现

class UpdateTitleAddressView(LoginRequiredJSONMixin, View):
    """设置地址标题"""

    def put(self, request, address_id):
        """设置地址标题"""
        # 接收参数:地址标题
        json_dict = json.loads(request.body.decode())
        title = json_dict.get('title')

        try:
            # 查询地址
            address = Address.objects.get(id=address_id)

            # 设置新的地址标题
            address.title = title
            address.save()
        except Exception as e:
            logger.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '设置地址标题失败'})

        # 4.响应删除地址结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '设置地址标题成功'})

3. 修改地址标题前端逻辑实现

Python_美多商城(用户中心)_5_第12张图片

[[ address.title ]]

默认地址 ×
data: {
    edit_title_index: '',
    new_title: '',
},
// 展示地址title编辑框
show_edit_title(index){
    this.edit_title_index = index;
},
// 取消保存地址title
cancel_title(){
    this.edit_title_index = '';
    this.new_title = '';
},
// 修改地址title
save_title(index){
    if (!this.new_title) {
        alert("请填写标题后再保存!");
    } else {
        let url = '/addresses/' + this.addresses[index].id + '/title/';
        axios.put(url, {
            title: this.new_title
        }, {
            headers: {
                'X-CSRFToken':getCookie('csrftoken')
            },
            responseType: 'json'
        })
            .then(response => {
                if (response.data.code == '0') {
                    // 更新地址title
                    this.addresses[index].title = this.new_title;
                    this.cancel_title();
                } else if (response.data.code == '4101') {
                    location.href = '/login/?next=/addresses/';
                } else {
                    alert(response.data.errmsg);
                }
            })
            .catch(error => {
                console.log(error.response);
            })
    }
},

==============================

修改密码

Python_美多商城(用户中心)_5_第13张图片

1. 修改密码后端逻辑

提示:

  • 修改密码前需要校验原始密码是否正确,以校验修改密码的用户身份。
  • 如果原始密码正确,再将新的密码赋值给用户。
class ChangePasswordView(LoginRequiredMixin, View):
    """修改密码"""

    def get(self, request):
        """展示修改密码界面"""
        return render(request, 'user_center_pass.html')

    def post(self, request):
        """实现修改密码逻辑"""
        # 接收参数
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        new_password2 = request.POST.get('new_password2')

        # 校验参数
        if not all([old_password, new_password, new_password2]):
            return http.HttpResponseForbidden('缺少必传参数')
        try:
            request.user.check_password(old_password)
        except Exception as e:
            logger.error(e)
            return render(request, 'user_center_pass.html', {'origin_pwd_errmsg':'原始密码错误'})
        if not re.match(r'^[0-9A-Za-z]{8,20}$', new_password):
            return http.HttpResponseForbidden('密码最少8位,最长20位')
        if new_password != new_password2:
            return http.HttpResponseForbidden('两次输入的密码不一致')

        # 修改密码
        try:
            request.user.set_password(new_password)
            request.user.save()
        except Exception as e:
            logger.error(e)
            return render(request, 'user_center_pass.html', {'change_pwd_errmsg': '修改密码失败'})

        # 清理状态保持信息
        logout(request)
        response = redirect(reverse('users:login'))
        response.delete_cookie('username')

        # # 响应密码修改结果:重定向到登录界面
        return response

你可能感兴趣的:(Python_美多商城(用户中心)_5)