Django项目实战——8—(判断用户是否登录并返回JSON、Django发送邮件的配置、生成邮箱验证链接、验证邮箱后端逻辑)

1、判断用户是否登录并返回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装饰器
  • 在shop.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类,验证用户登录的状态问题apps/users/views.py文件

"""
视图文件
apps/users/views.py文件,用户后端验证视图文件
"""
from django.shortcuts import render, redirect, reverse
from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseServerError
from django.views import View
from .forms import RegisterForm, LoginForm
from .models import User
from django.contrib.auth import login, logout, authenticate    # authenticate封装的验证用户名和密码是否正确的方法
from django_redis import get_redis_connection
from django.contrib.auth.mixins import LoginRequiredMixin      # 验证用户是否登录的类
import json
from django.http import QueryDict      # 转换数据类型
import re                              # 正则表达式
import logging
from utils.response_code import RETCODE
from utils.views import LoginRequiredJsonMixin    # 验证用户登录的状态并返回代码


logger = logging.getLogger('django')   # 记录到django的日志中


# 用户邮箱绑定,登录之后才能访问该方法,需要登录验证方法
class EmailView(LoginRequiredJsonMixin, View):             # 继承自LoginRequiredJsonMixin类,验证用户登录的状态问题
    """添加邮箱"""
    # 此时是put请求,一般用于更新数据
    def put(self, request):
        """添加邮箱的业务逻辑实现 """
        '''
        put 提交的数据是在body属性中,是字节数据类型Bytes
        get,post提交的数据是QueryDict的数据类型
        '''
        # print(request.body.decode())          # {"email":"xxxxx@qq.com"}
        # print(type(request.body.decode()))    # str数据类型
        # 从request中获取email
        email = json.loads(request.body.decode()).get('email')    # json.loads转换成字典类型
        # print(email)
        
        # 校验邮箱   邮箱的正则表达式
        if not re.match(r'^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$', email):
            return HttpResponseForbidden('参数email错误!')
        
        try:
            # 根据当前登录的用户保存到数据库的email字段中
            request.user.email = email
            request.user.save()
        except Exception as e:
            logger.error(e)             # 记录错误信息到日志
            return HttpResponseServerError('邮箱激活失败')
        
        # 发送邮件
        
        # 返回Json数据类型 传输给前端js文件进行邮件验证逻辑
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "OK"})


# 个人用户中心
class UserInfoView(LoginRequiredMixin, View):
    """用户个人中心"""
    def get(self, request):
        """提供用户个人中心"""
        '''
        login_url = None
        permission_denied_message = ''
        raise_exception = False
        redirect_field_name = REDIRECT_FIELD_NAME
        '''
        # 验证用户是否登陆
        # if request.user.is_authenticated:
        #     return render(request, 'user_center_info.html')
        # else:
        #     return redirect(reverse('users:login'))           # 用户未登录,跳转至登陆界面
        # print(request.user)
        # print(request.user.username)
        # print(request.user.mobile)
        
        # 数据由Django后端来提供,前端数据的读取方式采用Vue方式读取[[ username ]]
        context = {
            'username': request.user.username,
            'mobile': request.user.mobile,
        }
        
        # 上面的代码后期需要复用多次,可以引入LoginRequiredMixin类封装的方法和REDIRECT_FIELD_NAME = 'next'参数来重定向
        return render(request, 'user_center_info.html', context=context)        # 重定向到个人中心


# 退出登录
class LogoutView(View):
    """退出登陆逻辑实现"""
    def get(self, request):
        """实现用户退出登录的功能"""
        # 清除状态保持信息
        logout(request)
        
        # 退出登录之后重定向到首页
        response = redirect(reverse('contents:index'))
        
        # 删除cookies中的用户名
        # result.set_cookie('username', user.username, max_age=3600*24*14)    # 保存两周
        response.delete_cookie('username')
        return response                           # 响应结果
    

# 用户登陆
class LoginView(View):
    """用户名登陆"""
    def get(self, request):
        """  提供登陆界面
        :return: 登陆界面
        """
        return render(request, 'login.html')

    def post(self, request):
        """
        实现登录逻辑
        :param request: 请求对象
        :return: 登录结果
        """
        # 接受参数
        login_form = LoginForm(request.POST)
        
        # 校验参数
        if login_form.is_valid():
            # 接收参数
            username = login_form.cleaned_data.get('username')
            password = login_form.cleaned_data.get('password')
            remembered = request.POST.get('remembered')                 # 没经过form验证,使用request接收参数

            # 认证登录用户
            # users = User.objects.get(username=username)
            # users.check_password(password)                            # check_password验证密码封装的方法,返回值bool类型
            """  authenticate方法源码
             def authenticate(self, request, username=None, password=None, **kwargs):
                if username is None:
                    username = kwargs.get(UserModel.USERNAME_FIELD)
                try:
                    user = UserModel._default_manager.get_by_natural_key(username)
                except UserModel.DoesNotExist:
                    # Run the default password hasher once to reduce the timing
                    # difference between an existing and a nonexistent user (#20760).
                    UserModel().set_password(password)
                else:
                    if user.check_password(password) and self.user_can_authenticate(user):
                        return user
            """
            user = authenticate(username=username, password=password)   # 重构authenticate方法之后,可以验证手机号登录和用户名登录
            if user is None:
                # 用户名或者密码输入错误
                return render(request, 'login.html', {"errmsg": "用户名或者密码输入错误"})
            
            # 实现状态保持
            login(request, user)

            # 设置状态保持的周期
            if remembered != 'on':
                # 没选中记住密码,浏览器关闭就需要销毁session信息
                request.session.set_expiry(0)                  # set_expiry过期时间
            else:
                # 选中记住密码,session信息默认保存两周
                # request.session.set_expiry(60*60*24*14)
                request.session.set_expiry(None)
            
            # REDIRECT_FIELD_NAME = 'next'      LoginRequiredMixin类中源码的参数 ,用于获取登陆前的路由请求,方便登陆后直接定向到之前的请求界面
            next = request.GET.get('next')       # 获取url中的‘next’字符串参数
            if next:
                result = redirect(next)          # 如果存在next参数,则重定向到这个地址
            else:
                # 后端将用户信息存入cookie
                result = redirect(reverse('contents:index'))            # redirect方法源码中会返回一个redirect_class
            # set_cookie('key', 'value', 'erpriy')   erpriy过期时间
            result.set_cookie('username', user.username, max_age=3600*24*14)    # 保存两周
            
            # 响应登录结果    跳转到首页
            return result
        else:
            print(login_form.errors.get_json_data())
            context = {
                "form_errors": login_form.errors,
            }
            return render(request, 'login.html', context=context)
    

# 用户注册
class RegisterView(View):
    """用户注册"""
    def get(self, request):
        """提供用户的注册界面"""
        return render(request, 'register.html')
    
    def post(self, request):
        """提供用户的注册逻辑"""
        # 前端用户提交数据
        form = RegisterForm(request.POST)
        if form.is_valid():
            # 接收参数
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            mobile = form.cleaned_data.get('mobile')
            sms_code_client = request.POST.get('sms_code')         # 验证短信验证码  sms_code是register.html 文件中命名的
            
            # 判断用户输入的短信验证码是否正确
            redis_conn = get_redis_connection('verify_code')       # 链接redis中配置的数据库
            sms_code_server = redis_conn.get('sms_%s' % mobile)    # 根据存储时候的格式写
            if sms_code_server is None:
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码已经失效'})     # 错误信息渲染到前端界面
            if sms_code_server.decode() != sms_code_client:       # sms_code_server数据类型需要转换
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码填写错误'})
            
            try:
                # user = User(username=username, password=password, mobile=mobile)
                # 下面的添加数据的方法是封装了加密等功能的函数,更安全
                users = User.objects.create_user(username=username, password=password, mobile=mobile)
            except:    # 如果保存数据失败
                return render(request, 'register.html', {'register_error_message': '注册失败'})
            
            # 保持用户登录的状态
            login(request, users)
            
            # 返回响应
            # return HttpResponse('success')
            return redirect(reverse('contents:index'))           # 注册成功,跳转到首页
        else:
            print(form.errors.get_json_data())
            # return HttpResponse("fail")
            # 返回注册错误信息到前端界面
            context = {
                'form_error': form.errors,
            }
            return render(request, 'register.html', context=context)
    

# 判断用户名是否已经存在
class UsernameExists(View):
    """ 判断用户名是否已经存在"""
    def get(self, request, username):     # username用户名
        count = User.objects.filter(username=username).count()      # 查询数据库中信息
        return JsonResponse({"code": 0, "errmsg": "OK", "count": count})   # 返回给前端界面
    

登录验证文件utils/views.py

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/8/19 16:11
@Author  : chen

登录验证文件:utils/views.py
"""
''' 重写 django/contrib/auth/mixins.py文件中的源码

def handle_no_permission(self):
    # 没有登陆的用户,对应的操作,往哪里跳转
    if self.raise_exception or self.request.user.is_authenticated:
        raise PermissionDenied(self.get_permission_denied_message())
    return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())


class LoginRequiredMixin(AccessMixin):
    """Verify that the current user is authenticated."""
    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return self.handle_no_permission()
        return super().dispatch(request, *args, **kwargs)
'''
from django.contrib.auth.mixins import LoginRequiredMixin      # 继承的类文件,也是重写的方法
from django import http
from utils.response_code import RETCODE         # 状态码定义文件


class LoginRequiredJsonMixin(LoginRequiredMixin):
    # 没有登陆的用户,对应的操作,往哪里跳转
    def handle_no_permission(self):
        """ 返回JSON数据 """
        # 状态码4101,对应了static/js/user_center_info.js文件中的code
        return http.JsonResponse({"code": RETCODE.SESSIONERR, 'errmsg': "用户未登录"})

2、Django发送邮件的配置

Django发送邮件流程分析

在这里插入图片描述
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字符串

准备发邮件服务器

1.点击进入《设置》界面
2. POP3/SMTP/IMAP,开启服务
3.设置授权码,记住设置的密码,Django中登录就是这个密码

邮箱获取授权码的方式:邮箱设置中查找POP3/SMTP/IMAP服务,注意IMAP/SMTP服务也需要开启
在这里插入图片描述

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 指定邮件后端
EMAIL_HOST = 'smtp.163.com' # 发邮件主机
EMAIL_PORT = 25 # 发邮件端口
EMAIL_HOST_USER = 'xxx@163.com' # 授权的邮箱
EMAIL_HOST_PASSWORD = 'xxxxxxxx' # 邮箱授权时获得的密码,非注册登录密码
EMAIL_FROM = 'xxxx' # 发件人抬头

项目实例代码

apps/users/views.py文件,用户后端验证视图文件

		# 发送邮件
        # send_mail(subject, message, from_email, recipient_list,
        #               fail_silently=False, auth_user=None, auth_password=None,
        #               connection=None, html_message=None):
        html_message = '<p>尊敬的用户您好!</p>' \
                       '<p>感谢您使用商城。</p>' \
                       '<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
                       '<p><a href="%s">%s<a></p>' % (email, verify_url, verify_url)   # verify_url是验证路由
        send_mail(subject='LG_商城邮箱验证', message='', from_email=settings.EMAIL_FROM,
                  recipient_list=[email], html_message=html_message)           # [email]是列表,
        
        # 返回Json数据类型 传输给前端js文件进行邮件验证逻辑
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "OK"})

发送邮箱验证邮件

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

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

apps/users/views.py文件,用户后端验证视图文件

        # 发送邮件  这种方法不是异步执行,会阻塞项目进程,需要替换成Celery异步
        '''  send_mail源码
        send_mail(subject, message, from_email, recipient_list,
                      fail_silently=False, auth_user=None, auth_password=None,
                      connection=None, html_message=None):
        '''
        # try:
        #     verify_url = 'www.baidu.com'
        #     html_message = '<p>尊敬的用户您好!</p>' \
        #                    '<p>感谢您使用商城。</p>' \
        #                    '<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
        #                    '<p><a href="%s">%s<a></p>' % (email, verify_url, verify_url)  # verify_url是验证路由
        #
        #     send_mail(subject='LG_商城邮箱验证', message='', from_email=settings.EMAIL_FROM,
        #               recipient_list=[email], html_message=html_message)                         # [email]是列表,
        # except smtplib.SMTPServerDisconnected:
        #     print('123')
        
        # 异步celery发送邮件进行验证   异步发送需要.delay方法
        verify_url = 'wwww.baidu.com'
        send_verify_emails.delay(to_email=email, verify_url=verify_url)
        
        # 返回Json数据类型 传输给前端js文件进行邮件验证逻辑
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "OK"})

开发环境配置文件dev.py

# qq邮箱发送邮件配置
# 授权码  ehnwvybobdaybcee
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'    # 指定邮件后端
EMAIL_HOST = 'smtp.qq.com'                                       # 发邮件主机
EMAIL_PORT = 25                                                  # 发邮件端口
EMAIL_HOST_USER = '727506892@qq.com'                             # 授权的邮箱
EMAIL_HOST_PASSWORD = 'ehxxxxxoxxxxx'                         # 邮箱授权时获得的密码,非注册登录密码
EMAIL_FROM = '商城测试邮件<727506892@qq.com>'                                  # 发件人抬头
# EMAIL_USE_TLS = True                                           # 这里必须是 True,否则发送不成功

celery主文件celery_tasks/main.py,注册发邮件的任务

# -*- encoding: utf-8 -*-
"""
@File    : main.py
@Time    : 2020/8/7 14:23
@Author  : chen

celery主文件:celery_tasks/main.py
"""
# celery启动文件
from celery import Celery

# celery的入口
import os
if not os.getenv('DJANGO_SETTINGS_MODULE'):
    os.environ['DJANGO_SETTINGS_MODULE'] = 'shop.dev'         # 让celery识别开发配置的文件


# 创建celery实例   'shop'名称
celery_app = Celery('shop')

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

# 自动注册celery任务
celery_app.autodiscover_tasks(['celery_tasks.sms', 'celery_tasks.email'])   # 自动去查找tasks.py文件并执行
"""
Windows系统运行代码
celery -A celery_tasks.main worker -l info --pool=solo

linux系统运行代码
celery -A celery_tasks.main worker -l info

• -A指对应的应用程序, 其参数是项目中 Celery实例的位置。
• worker指这里要启动的worker。
• -l指日志等级,比如info等级。
"""

Celery异步发送邮件task任务celery_tasks/email/tasks.py

# -*- encoding: utf-8 -*-
"""
@File    : tasks.py
@Time    : 2020/8/19 20:53
@Author  : chen

Celery异步发送邮件task任务:celery_tasks/email/tasks.py
"""
'''
html_message = '<p>尊敬的用户您好!</p>' \
               '<p>感谢您使用商城。</p>' \
               '<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
               '<p><a href="%s">%s<a></p>' % (email, verify_url, verify_url)   # verify_url是验证路由
send_mail(subject='LG_商城邮箱验证', message='', from_email=settings.EMAIL_FROM,
          recipient_list=[email], html_message=html_message)           # [email]是列表,
'''
from django.core.mail import send_mail
from django.conf import settings
from celery_tasks.main import celery_app


# 使用装饰器,celery识别任务,name是任务名称      retry_backoff 异常自动重试的时间间隔,发送邮件不成功,间隔时间retry_backoff*2^(retry_backoff-1)秒
@celery_app.task(bind=True, name="send_verify_emails", retry_backoff=3)
def send_verify_emails(self, to_email, verify_url):  # self代表当前task的任务,当bind=True,时,需要添加该参数
    html_message = '<p>尊敬的用户您好!</p>' \
                   '<p>感谢您使用商城。</p>' \
                   '<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
                   '<p><a href="%s">%s<a></p>' % (to_email, verify_url, verify_url)  # verify_url是验证路由
    try:
        send_mail(subject='LG_商城邮箱验证', message='', from_email=settings.EMAIL_FROM,
                  recipient_list=[to_email], html_message=html_message)  # [to_email]是列表
    except Exception as e:
        # 重新发送邮件的次数,重新调用该方法发送邮件3次
        raise self.retry(max_retries=3)  # self代表当前task任务

启动Celery

celery -A celery_tasks.main worker -l info --pool=solo

开启Celery异步发送邮件:
在这里插入图片描述
Celery 异步发送邮件结果:
在这里插入图片描述

3、生成邮箱验证链接

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

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

配置相关参数

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

用邮箱验证链接

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

项目实例代码

邮箱验证文件apps/users/utils.py

# -*- encoding: utf-8 -*-
"""
@File    : utils.py
@Time    : 2020/8/11 9:42
@Author  : chen


重写authenticate方法用于验证手机登录和用户名登录,邮箱验证文件:apps/users/utils.py
"""
from django.contrib.auth.backends import ModelBackend  # authenticate方法外的类
import re   # 正则表达式
from users.models import User
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings


# 邮箱反序列化操作,即解码
def check_verify_email_url(token):
    """
    反序列化操作
    :param token: 序列化之后的用户信息
    :return: 用户信息user
    """
    s = Serializer(settings.SECRET_KEY, 60*60*24)
    try:
        data = s.loads(token)       # 反序列化   loads方法
    except Exception as e:
        return None
    else:
        user_id = data.get('user.id')        # 从链接token中获取用户信息
        email = data.get('email')
        
        try:
            user = User.objects.get(id=user_id, email=email)    # 如果在数据库中查询到了该用户信息
        except:
            return None                                         # 查不到信息返回None
        else:
            return user                                         # 返回用户对象
        

# 1. 生成验证邮箱的链接url   www.meiduo.com:8000/users/emails/verification/?token=xxxxx
def generate_verify_email_url(user):
    """
    生成邮箱的激活链接
    :param user: 当前登录的用户
    :return: 验证的url
    """
    s = Serializer(settings.SECRET_KEY, 60*60*24)  # settings.SECRET_KEY加密字符串,有效期60*60*24=1天
    data = {"user_id": user.id, "email": user.email}      # token中需要包含有用户的信息
    token = s.dumps(data)                                 # 将信息序列化,即加密操作
    # print(type(token))                                 # <class 'bytes'>
    return settings.EMAIL_VERIFY_URL + '?token=' + token.decode()      # 返回拼接的url

开发环境配置文件dev.py

# 邮箱验证的url拼接,主地址
EMAIL_VERIFY_URL = 'http://www.meiduo.com:8000/users/emails/verification/'

apps/users/urls.py 子路由,users模块的路由

# 用户验证邮箱路由     www.meiduo.com:8000/users/emails/verification/?token=xxxxx
    path("emails/verification/", views.VerifyEmailView.as_view()),

4、验证邮箱后端逻辑

验证邮箱接口设计和定义

请求方式
在这里插入图片描述
请求参数:查询参数
在这里插入图片描述
响应结果:HTML
在这里插入图片描述

验证链接提取用户信息

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

验证邮箱后端逻辑实现

验证邮箱的核心:就是将用户的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'))

项目实例代码如下

apps/users/views.py文件,用户后端验证视图文件

"""
apps/users/views.py文件,用户后端验证视图文件
"""
from django.shortcuts import render, redirect, reverse
from django.http import HttpResponse, JsonResponse, HttpResponseForbidden, HttpResponseServerError,HttpResponseBadRequest
from django.views import View
from .forms import RegisterForm, LoginForm
from .models import User
from django.contrib.auth import login, logout, authenticate    # authenticate封装的验证用户名和密码是否正确的方法
from django_redis import get_redis_connection
from django.contrib.auth.mixins import LoginRequiredMixin      # 验证用户是否登录的类
import json
from django.http import QueryDict      # 转换数据类型
import re                              # 正则表达式
import logging
from utils.response_code import RETCODE
from utils.views import LoginRequiredJsonMixin    # 验证用户登录的状态并返回代码
from django.core.mail import send_mail            # 发送邮件
from django.conf import settings                  # 导入设置参数
from celery_tasks.email.tasks import send_verify_emails      # celery异步发送邮件进行验证
from .utils import generate_verify_email_url, check_verify_email_url   # 生成邮箱验证的url方法和反序列化方法


logger = logging.getLogger('django')   # 记录到django的日志中


# 邮箱验证
# 1. 生成验证邮箱的链接url   www.meiduo.com:8000/users/emails/verification/?token=xxxxx
# 2. 点击之后,将email_active改为true
class VerifyEmailView(LoginRequiredJsonMixin, View):
    """验证邮箱"""
    def get(self, request):
        """实现邮箱验证逻辑"""
        # 接收参数
        token = request.GET.get('token')
        # 校验参数:判断token是否为空和过期,提取user
        if not token:
            return HttpResponseBadRequest('缺少token')
        
        user = check_verify_email_url(token)                  # 反序列化操作token,提取token中的用户信息
        if not user:                                          # token中无用户信息
            return HttpResponseForbidden('无效的token')
        
        # 修改email_active的值为True
        try:
            user.email_active = True                          # 修改该字段
            user.save()                                       # 保存到数据库中
        except Exception as e:
            logger.error(e)                                   # 保存到日志中
            return HttpResponseServerError('激活邮件失败')
        
        # 返回邮箱验证结果,跳转到用户个人中心
        return redirect(reverse('users:info'))
    

# 用户邮箱绑定,登录之后才能访问该方法,需要登录验证方法
class EmailView(LoginRequiredJsonMixin, View):             # 继承自LoginRequiredJsonMixin类,验证用户登录的状态问题
    """添加邮箱"""
    # 此时是put请求,一般用于更新数据
    def put(self, request):
        """添加邮箱的业务逻辑实现 """
        '''
        put 提交的数据是在body属性中,是字节数据类型Bytes
        get,post提交的数据是QueryDict的数据类型
        '''
        # print(request.body.decode())          # {"email":"xxxxx@qq.com"}
        # print(type(request.body.decode()))    # str数据类型
        # 从request中获取email
        email = json.loads(request.body.decode()).get('email')    # json.loads转换成字典类型
        # print(email)
        
        # 校验邮箱   邮箱的正则表达式
        if not re.match(r'^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$', email):
            return HttpResponseForbidden('参数email错误!')
        
        try:
            # 根据当前登录的用户保存到数据库的email字段中
            request.user.email = email
            request.user.save()
        except Exception as e:
            logger.error(e)             # 记录错误信息到日志
            return HttpResponseServerError('邮箱激活失败')
        
        # 发送邮件  这种方法不是异步执行,会阻塞项目进程,需要替换成Celery异步
        '''  send_mail源码
        send_mail(subject, message, from_email, recipient_list,
                      fail_silently=False, auth_user=None, auth_password=None,
                      connection=None, html_message=None):
        '''
        # try:
        #     verify_url = 'www.baidu.com'
        #     html_message = '<p>尊敬的用户您好!</p>' \
        #                    '<p>感谢您使用商城。</p>' \
        #                    '<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
        #                    '<p><a href="%s">%s<a></p>' % (email, verify_url, verify_url)  # verify_url是验证路由
        #
        #     send_mail(subject='LG_商城邮箱验证', message='', from_email=settings.EMAIL_FROM,
        #               recipient_list=[email], html_message=html_message)                         # [email]是列表,
        # except smtplib.SMTPServerDisconnected:
        #     print('123')

        verify_url = generate_verify_email_url(request.user)        # 生成邮箱验证的url,用户对象为request.user
        # 异步celery发送邮件进行验证   异步发送需要.delay方法
        send_verify_emails.delay(to_email=email, verify_url=verify_url)
        
        # 返回Json数据类型 传输给前端js文件进行邮件验证逻辑
        return JsonResponse({"code": RETCODE.OK, 'errmsg': "OK"})


# 个人用户中心
class UserInfoView(LoginRequiredMixin, View):
    """用户个人中心"""
    def get(self, request):
        """提供用户个人中心"""
        '''
        login_url = None
        permission_denied_message = ''
        raise_exception = False
        redirect_field_name = REDIRECT_FIELD_NAME
        '''
        # 验证用户是否登陆
        # if request.user.is_authenticated:
        #     return render(request, 'user_center_info.html')
        # else:
        #     return redirect(reverse('users:login'))           # 用户未登录,跳转至登陆界面
        # print(request.user)
        # print(request.user.username)
        # print(request.user.mobile)
        
        # 数据由Django后端来提供,前端数据的读取方式采用Vue方式读取[[ username ]]
        context = {
            'username': request.user.username,
            'mobile': request.user.mobile,
            'email': request.user.email,                             # 传输email和email_active参数到前端是为了邮箱验证的功能
            'email_active': request.user.email_active,
        }
        
        # 上面的代码后期需要复用多次,可以引入LoginRequiredMixin类封装的方法和REDIRECT_FIELD_NAME = 'next'参数来重定向
        return render(request, 'user_center_info.html', context=context)        # 重定向到个人中心


# 退出登录
class LogoutView(View):
    """退出登陆逻辑实现"""
    def get(self, request):
        """实现用户退出登录的功能"""
        # 清除状态保持信息
        logout(request)
        
        # 退出登录之后重定向到首页
        response = redirect(reverse('contents:index'))
        
        # 删除cookies中的用户名
        # result.set_cookie('username', user.username, max_age=3600*24*14)    # 保存两周
        response.delete_cookie('username')
        return response                           # 响应结果
    

# 用户登陆
class LoginView(View):
    """用户名登陆"""
    def get(self, request):
        """  提供登陆界面
        :return: 登陆界面
        """
        return render(request, 'login.html')

    def post(self, request):
        """
        实现登录逻辑
        :param request: 请求对象
        :return: 登录结果
        """
        # 接受参数
        login_form = LoginForm(request.POST)
        
        # 校验参数
        if login_form.is_valid():
            # 接收参数
            username = login_form.cleaned_data.get('username')
            password = login_form.cleaned_data.get('password')
            remembered = request.POST.get('remembered')                 # 没经过form验证,使用request接收参数

            # 认证登录用户
            # users = User.objects.get(username=username)
            # users.check_password(password)                            # check_password验证密码封装的方法,返回值bool类型
            """  authenticate方法源码
             def authenticate(self, request, username=None, password=None, **kwargs):
                if username is None:
                    username = kwargs.get(UserModel.USERNAME_FIELD)
                try:
                    user = UserModel._default_manager.get_by_natural_key(username)
                except UserModel.DoesNotExist:
                    # Run the default password hasher once to reduce the timing
                    # difference between an existing and a nonexistent user (#20760).
                    UserModel().set_password(password)
                else:
                    if user.check_password(password) and self.user_can_authenticate(user):
                        return user
            """
            user = authenticate(username=username, password=password)   # 重构authenticate方法之后,可以验证手机号登录和用户名登录
            if user is None:
                # 用户名或者密码输入错误
                return render(request, 'login.html', {"errmsg": "用户名或者密码输入错误"})
            
            # 实现状态保持
            login(request, user)

            # 设置状态保持的周期
            if remembered != 'on':
                # 没选中记住密码,浏览器关闭就需要销毁session信息
                request.session.set_expiry(0)                  # set_expiry过期时间
            else:
                # 选中记住密码,session信息默认保存两周
                # request.session.set_expiry(60*60*24*14)
                request.session.set_expiry(None)
            
            # REDIRECT_FIELD_NAME = 'next'      LoginRequiredMixin类中源码的参数 ,用于获取登陆前的路由请求,方便登陆后直接定向到之前的请求界面
            next = request.GET.get('next')       # 获取url中的‘next’字符串参数
            if next:
                result = redirect(next)          # 如果存在next参数,则重定向到这个地址
            else:
                # 后端将用户信息存入cookie
                result = redirect(reverse('contents:index'))            # redirect方法源码中会返回一个redirect_class
            # set_cookie('key', 'value', 'erpriy')   erpriy过期时间
            result.set_cookie('username', user.username, max_age=3600*24*14)    # 保存两周
            
            # 响应登录结果    跳转到首页
            return result
        else:
            print(login_form.errors.get_json_data())
            context = {
                "form_errors": login_form.errors,
            }
            return render(request, 'login.html', context=context)
    

# 用户注册
class RegisterView(View):
    """用户注册"""
    def get(self, request):
        """提供用户的注册界面"""
        return render(request, 'register.html')
    
    def post(self, request):
        """提供用户的注册逻辑"""
        # 前端用户提交数据
        form = RegisterForm(request.POST)
        if form.is_valid():
            # 接收参数
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password')
            mobile = form.cleaned_data.get('mobile')
            sms_code_client = request.POST.get('sms_code')         # 验证短信验证码  sms_code是register.html 文件中命名的
            
            # 判断用户输入的短信验证码是否正确
            redis_conn = get_redis_connection('verify_code')       # 链接redis中配置的数据库
            sms_code_server = redis_conn.get('sms_%s' % mobile)    # 根据存储时候的格式写
            if sms_code_server is None:
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码已经失效'})     # 错误信息渲染到前端界面
            if sms_code_server.decode() != sms_code_client:       # sms_code_server数据类型需要转换
                return render(request, 'register.html', {'sms_code_errmsg': '短信验证码填写错误'})
            
            try:
                # user = User(username=username, password=password, mobile=mobile)
                # 下面的添加数据的方法是封装了加密等功能的函数,更安全
                users = User.objects.create_user(username=username, password=password, mobile=mobile)
            except:    # 如果保存数据失败
                return render(request, 'register.html', {'register_error_message': '注册失败'})
            
            # 保持用户登录的状态
            login(request, users)
            
            # 返回响应
            # return HttpResponse('success')
            return redirect(reverse('contents:index'))           # 注册成功,跳转到首页
        else:
            print(form.errors.get_json_data())
            # return HttpResponse("fail")
            # 返回注册错误信息到前端界面
            context = {
                'form_error': form.errors,
            }
            return render(request, 'register.html', context=context)
    

# 判断用户名是否已经存在
class UsernameExists(View):
    """ 判断用户名是否已经存在"""
    def get(self, request, username):     # username用户名
        count = User.objects.filter(username=username).count()      # 查询数据库中信息
        return JsonResponse({"code": 0, "errmsg": "OK", "count": count})   # 返回给前端界面
    

5、收货地址

省市区三级联动

展示收货地址界面

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

准备省市区模型和数据

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

创建新的app模块:
在这里插入图片描述
模型说明:

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

项目实例代码

个人用户中心地址界面文件templates/user_center_site.html

{# 个人用户中心地址界面文件:templates/user_center_site.html #}
{% load static %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
	<title>LG商城-用户中心</title>
	<link rel="stylesheet" type="text/css" href="{% static 'css/reset.css' %}">
	<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}">
    <script type="text/javascript" src="{% static 'js/vue-2.5.16.js' %}"></script>
    <script type="text/javascript" src="{% static 'js/axios-0.18.0.min.js'  %}"></script>
</head>
<body>
	<div id="app">
	<div class="header_con">
		<div class="header" v-cloak>
			<div class="welcome fl">欢迎来到LG商城!</div>
			<div class="fr">
                <div v-if="username" class="login_btn fl">
                    欢迎您:<em>[[ username ]]</em>
                    <span>|</span>
                    <a href="{% url 'users:logout' %}">退出</a>
                </div>
                <div v-else class="login_btn fl">
                    <a href="{% url 'users:login' %}">登录</a>
                    <span>|</span>
                    <a href="{% url 'users:register' %}">注册</a>
                </div>
				<div class="user_link fl">
					<span>|</span>
					<a href="{% url 'users:info' %}">用户中心</a>
					<span>|</span>
					<a href="cart.html">我的购物车</a>
					<span>|</span>
					<a href="user_center_order.html">我的订单</a>
				</div>
			</div>
		</div>
	</div>
	<div class="search_bar clearfix">
		<a href="{% url 'contents:index' %}" class="logo fl"><img src="{% static 'images/logo.png' %}"></a>
		<div class="search_wrap fl">
			<form method="get" action="/search/" class="search_con">
                <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
                <input type="submit" class="input_btn fr" name="" value="搜索">
            </form>
			<ul class="search_suggest fl">
				<li><a href="#">索尼微单</a></li>
				<li><a href="#">优惠15元</a></li>
				<li><a href="#">美妆个护</a></li>
				<li><a href="#">买2免1</a></li>
			</ul>
		</div>
	</div>
	<div class="main_con clearfix">
		<div class="left_menu_con clearfix">
			<h3>用户中心</h3>
			<ul>
				<li><a href="{% url 'users:info' %}">· 个人信息</a></li>
				<li><a href="{% url 'users:address' %}" class="active">· 收货地址</a></li>
                <li><a href="user_center_order.html">· 全部订单</a></li>
				<li><a href="user_center_pass.html">· 修改密码</a></li>
			</ul>
		</div>
		<div class="right_content clearfix" v-cloak>
			<div class="site_top_con">
				<a @click="show_add_site">新增收货地址</a>
				<span>你已创建了<b>2</b>个收货地址,最多可创建<b>20</b></span>
			</div>
			<div class="site_con">
				<div class="site_title">
					<h3>居然 长沙</h3>
					<a href="javascript:;" class="edit_icon"></a>
					<em>默认地址</em>
					<span class="del_site">×</span>
				</div>
				<ul class="site_list">
					<li><span>收货人:</span><b>居然</b></li>
					<li><span>所在地区:</span><b>长沙市昌平区</b></li>
					<li><span>地址:</span><b>建材城西路</b></li>
					<li><span>手机:</span><b>188****0001</b></li>
					<li><span>固定电话:</span><b>78912345</b></li>
					<li><span>电子邮箱:</span><b>lgcode@163.com</b></li>
				</ul>
				<div class="down_btn">
					<a href="javascript:;" class="edit_icon">编辑</a>
				</div>
			</div>
			<div class="site_con">
				<div class="site_title">
					<h3>居然 长沙</h3>
					<a href="javascript:;" class="edit_icon"></a>
					<span class="del_site">×</span>
				</div>
				<ul class="site_list">
					<li><span>收货人:</span><b>居然</b></li>
					<li><span>所在地区:</span><b>长沙市昌平区</b></li>
					<li><span>地址:</span><b>建材城西路</b></li>
					<li><span>手机:</span><b>188****0001</b></li>
					<li><span>固定电话:</span><b>78912345</b></li>
					<li><span>电子邮箱:</span><b>lgcode@163.com</b></li>
				</ul>
				<div class="down_btn">
					<a href="javascript:;" class="set_default">设为默认</a>
					<a href="javascript:;" class="edit_icon">编辑</a>
				</div>
			</div>
		</div>
	</div>
	<div class="footer">
		<div class="foot_link">
			<a href="#">关于我们</a>
			<span>|</span>
			<a href="#">联系我们</a>
			<span>|</span>
			<a href="#">招聘人才</a>
			<span>|</span>
			<a href="#">友情链接</a>
		</div>
		<p>CopyRight © 2016 长沙LG商业股份有限公司 All Rights Reserved</p>
		<p>电话:010-****888    京ICP备*******8号</p>
	</div>
	<div class="pop_con" v-show="is_show_edit" v-cloak>
		<div class="site_con site_pop">
            <div class="site_pop_title">
                <h3 v-if="editing_address_index">编辑收货地址</h3>
                <h3 v-else>新增收货地址</h3>
                <a @click="is_show_edit=false">×</a>
            </div>
            <form>
                <div class="form_group">
                    <label>*收货人:</label>
                    <input v-model="form_address.receiver" @blur="check_receiver" type="text" class="receiver">
                    <span v-show="error_receiver" class="receiver_error">请填写收件人</span>
                </div>
                <div class="form_group">
                    <label>*所在地区:</label>
                    <select v-model="form_address.province_id">
                        <option value="0">请选择</option>
                        <option value="1">长沙</option>
                        <option value="2">上海</option>
                        <option value="3">广州</option>
                        <option value="4">深圳</option>
                    </select>
                    <select v-model="form_address.city_id">
                        <option value="0">请选择</option>
                        <option value="1">长沙</option>
                        <option value="2">上海</option>
                        <option value="3">广州</option>
                        <option value="4">深圳</option>
                    </select>
                    <select v-model="form_address.district_id">
                        <option value="0">请选择</option>
                        <option value="1">东城区</option>
                        <option value="2">西城区</option>
                        <option value="3">昌平区</option>
                        <option value="4">海淀区</option>
                    </select>
                </div>
                <div class="form_group">
                    <label>*详细地址:</label>
                    <input v-model="form_address.place" @blur="check_place" type="text" class="place">
                    <span v-show="error_place" class="place_error">请填写地址信息</span>
                </div>
                <div class="form_group">
                    <label>*手机:</label>
                    <input v-model="form_address.mobile" @blur="check_mobile" type="text" class="mobile">
                    <span v-show="error_mobile" class="mobile_error">手机信息有误</span>
                </div>
                <div class="form_group">
                    <label>固定电话:</label>
                    <input v-model="form_address.tel" @blur="check_tel" type="text" class="tel">
                    <span v-show="error_tel" class="tel_error">固定电话有误</span>
                </div>
                <div class="form_group">
                    <label>邮箱:</label>
                    <input v-model="form_address.email" @blur="check_email" type="text" class="email">
                    <span v-show="error_email" class="email_error">邮箱信息有误</span>
                </div>
                <input type="button" name="" value="新 增" class="info_submit">
                <input @click="is_show_edit=false" type="reset" name="" value="取 消" class="info_submit info_reset">
            </form>
		</div>
		<div class="mask"></div>
	</div>
	</div>
	<script type="text/javascript">
		let addresses = "";
		let default_address_id = "";
    </script>
	<script type="text/javascript" src="{% static 'js/common.js' %}"></script>
	<script type="text/javascript" src="{% static 'js/user_center_site.js' %}"></script>
</body>
</html>

个人用户中心地址界面静态文件static/js/user_center_site.js

// 个人用户中心地址界面静态文件:static/js/user_center_site.js
let vm = new Vue({
    el: '#app',
    delimiters: ['[[', ']]'],
    data: {
        username: getCookie('username'),
        is_show_edit: false,
        form_address: {
            receiver: '',
            province_id: '',
            city_id: '',
            district_id: '',
            place: '',
            mobile: '',
            tel: '',
            email: '',
        },

        provinces: [],
        cities: [],
        districts: [],
        addresses: JSON.parse(JSON.stringify(addresses)),
        default_address_id: default_address_id,
        editing_address_index: '',
        edit_title_index: '',
        new_title: '',

        error_receiver: false,
        error_place: false,
        error_mobile: false,
        error_tel: false,
        error_email: false,
    },
    mounted() {
        // 获取省份数据
        this.get_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 = [];
                    })
            }
        }
    },
    methods: {
        // 展示新增地址弹框
        show_add_site(){
            this.is_show_edit = true;
            // 清空错误提示信息
            this.clear_all_errors();
            // 清空原有数据
            this.form_address.receiver = '';
            this.form_address.province_id = '';
            this.form_address.city_id = '';
            this.form_address.district_id = '';
            this.form_address.place = '';
            this.form_address.mobile = '';
            this.form_address.tel = '';
            this.form_address.email = '';
            this.editing_address_index = '';
        },
        // 展示编辑地址弹框
        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]));
        },
        // 校验收货人
        check_receiver(){
            if (!this.form_address.receiver) {
                this.error_receiver = true;
            } else {
                this.error_receiver = false;
            }
        },
        // 校验收货地址
        check_place(){
            if (!this.form_address.place) {
                this.error_place = true;
            } else {
                this.error_place = false;
            }
        },
        // 校验手机号
        check_mobile(){
            let re = /^1[3-9]\d{9}$/;
            if(re.test(this.form_address.mobile)) {
                this.error_mobile = false;
            } else {
                this.error_mobile = true;
            }
        },
        // 校验固定电话
        check_tel(){
            if (this.form_address.tel) {
                let re = /^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$/;
                if (re.test(this.form_address.tel)) {
                    this.error_tel = false;
                } else {
                    this.error_tel = true;
                }
            } else {
                this.error_tel = false;
            }
        },
        // 校验邮箱
        check_email(){
            if (this.form_address.email) {
                let re = /^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$/;
                if(re.test(this.form_address.email)) {
                    this.error_email = false;
                } else {
                    this.error_email = true;
                }
            } else {
                this.error_email = false;
            }
        },
        // 清空错误提示信息
        clear_all_errors(){
            this.error_receiver = false;
            this.error_mobile = false;
            this.error_place = false;
            this.error_tel = false;
            this.error_email = false;
        },
        // 获取省份数据
        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 = [];
                })
        },
        // 新增地址
        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 {
                // 注意:0 == '';返回true; 0 === '';返回false;
                if (this.editing_address_index === '') {
                    // 新增地址
                    let url = '/addresses/create/';
                    axios.post(url, this.form_address, {
                        headers: {
                            'X-CSRFToken':getCookie('csrftoken')
                        },
                        responseType: 'json'
                    })
                        .then(response => {
                            if (response.data.code == '0') {
                                // 局部刷新界面:展示所有地址信息,将新的地址添加到头部
                                this.addresses.splice(0, 0, 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 => {
                            console.log(error.response);
                        })
                } 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);
                        })
                }
            }
        },
        // 删除地址
        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);
                })
        },
        // 设置默认地址
        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);
                })
        },
        // 展示地址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);
                    })
            }
        },
    }
});

地址区域模型文件apps/areas/models.py

"""
地址区域模型文件:apps/areas/models.py
"""
from django.db import models


class Area(models.Model):
    """省市区"""
    name = models.CharField(max_length=20, verbose_name='名称')            #
    # related_name='subs'自关联,反向引用功能
    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 = verbose_name
    
    def __str__(self):
        return self.name
    

注册到apps中:开发环境配置文件dev.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 'apps.users',                     # 在apps文件夹下的users模块进行app注册
    'users',                            # 用户模块
    'contents',                         # 网页首页模块
    'verifications',                    # 验证码模块app
    'oauth',                            # 第三方登陆QQ的模块
    'areas',                            # 用户地址模块
]

apps/users/views.py文件,用户后端验证视图文件

# 收货地址的类
class AddressView(LoginRequiredMixin, View):            # 登录验证LoginRequiredJsonMixin类
    """用户收货地址"""
    def get(self, request):
        """提供收货地址界面"""
        return render(request, 'user_center_site.html')

在这里插入图片描述

导入省市区数据

mysql -h 数据库ip地址 -u 数据库用户名 -p 数据库密码 数据库 < areas.sql
mysql -h127.0.0.1 -uroot -pxxx shop < areas.sql

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值