本教程的知识点为: 项目准备 项目准备 配置 1. 修改settings/dev.py 文件中的路径信息 2. INSTALLED_APPS 3. 数据库 用户部分 图片 1. 后端接口设计: 视图原型 2. 具体视图实现 用户部分 使用Celery完成发送 判断帐号是否存在 1. 判断用户名是否存在 后端接口设计: 用户部分 JWT 什么是JWT 起源 传统的session认证 用户部分 登录 1. 业务说明 2. 后端接口设计 3. 后端实现 登录 使用登录的流程 创建模型类 urllib使用说明 登录回调处理 登录 使用登录的流程 创建模型类 urllib使用说明 绑定用户身份接口 邮件与验证 学习目标: 业务说明: 技术说明: 保存邮箱并发送验证邮件 省市区地址查询 数据库建表 说明 页面静态化 注意 定时任务 安装 部分 详情页 异步任务的触发 。 后端接口设计 收货地址 使用缓存 安装 使用方法 为省市区视图添加缓存 数据库表设计 表结构 数据表结构 首页数据表结构 Docker使用 Docker简介 用户浏览历史记录 1. 保存 后端接口设计 后端实现 搜索 1. 需求分析 2. 搜索引擎原理 3. Elasticsearch 部分 业务需求分析 技术实现 数据存储设计 1. Redis保存已登录用户 商品部分 业务需求分析 技术实现 查询数据 1. 后端接口设计 部分 业务需求分析 技术实现 登录合并 修改登录视图 部分 保存 1. 后端接口设计 2. 后端实现 保存的思路 创建数据库模型类 接入 开发平台登录 沙箱环境 Xadmin 1. 安装 2. 使用 站点的全局配置 站点Model管理。 在Ubuntu中安装 2. 启动与停止 3. 镜像操作 端与自定义文件存储系统 1. 的Python客户端 安装 使用。

完整笔记资料代码:https://gitee.com/yinuo112/Backend/tree/master/Django/django美多商城项目完整开发4.0/note.md

感兴趣的小伙伴可以自取哦~


全套教程部分目录:


部分文件图片:

登录

登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。

若想实现登录,需要成为互联的开发者,审核通过才可实现。注册方法可参考链接[

成为互联开发者后,还需创建应用,即获取本项目对应与互联的应用ID,创建应用的方法参考链接[

登录开发文档连接[

使用登录的流程

登录时序图

创建模型类

创建一个新的应用oauth,用来实现第三方认证登录。总路由前缀 oauth/

meiduo/meiduo_mall/utils/models.py文件中创建模型类基类,用于增加数据新建时间和更新时间。

from django.db import models

class BaseModel(models.Model):
    """为模型类补充字段"""
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
    update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")

    class Meta:
        abstract = True  # 说明是抽象模型类, 用于继承使用,数据库迁移时不会创建BaseModel的表

在oauth/models.py中定义身份(openid)与用户模型类User的关联关系

from django.db import models
from meiduo_mall.utils.models import BaseModel

class OAuthUser(BaseModel):
    """
    登录用户数据
    """
    user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户')
    openid = models.CharField(max_length=64, verbose_name='openid', db_index=True)

    class Meta:
        db_table = 'tb_oauth_qq'
        verbose_name = '登录用户数据'
        verbose_name_plural = verbose_name

进行数据库迁移

python manage.py makemigrations
python manage.py migrate

urllib使用说明

在后端接口中,我们需要向服务器发送请求,查询用户的信息,Python提供了标准模块urllib可以帮助我们发送http请求。

  • urllib.parse.urlencode(query)

将query字典转换为url路径中的查询字符串

  • urllib.parse.parse_qs(qs)

将qs查询字符串格式数据转换为python的字典

  • urllib.request.urlopen(url, data=None)

发送http请求,如果data为None,发送GET请求,如果data不为None,发送POST请求

返回response响应对象,可以通过read()读取响应体数据,需要注意读取出的响应体数据为bytes类型

绑定用户身份接口

如果用户是首次使用登录,则需要绑定用户,页面如下:

登录绑定用户

业务逻辑:

  • 用户需要填写手机号、密码、图片验证码、短信验证码
  • 如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
  • 如果用户已在美多商城注册过,则检验密码后直接绑定用户

绑定身份的处理流程

绑定身份上

绑定身份下

后端接口设计

请求方式: POST /oauth/qq/user/

请求参数: JSON 或 表单

参数 类型 是否必须 说明
mobile str 手机号
password str 密码
sms_code str 短信验证码
access_token str 凭据 (包含openid)

返回数据: JSON

返回值 类型 是否必须 说明
token str JWT token
user_id int 用户id
username str 用户名

在OAuth辅助类中增加

@staticmethod
    def check_save_user_token(token):
        """
        检验保存用户数据的token
        :param token: token
        :return: openid or None
        """
        serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE__USER_TOKEN_EXPIRES)
        try:
            data = serializer.loads(token)
        except BadData:
            return None
        else:
            return data.get('openid')

新建oauth/serializers.py文件,

class OAuthUserSerializer(serializers.Serializer):
    """
    登录创建用户序列化器
    """
    access_token = serializers.CharField(label='操作凭证')
    mobile = serializers.RegexField(label='手机号', regex=r'^1[3-9]\d{9}$')
    password = serializers.CharField(label='密码', max_length=20, min_length=8)
    sms_code = serializers.CharField(label='短信验证码')

    def validate(self, data):
        # 检验access_token
        access_token = data['access_token']
        openid = OAuth.check_save_user_token(access_token)
        if not openid:
            raise serializers.ValidationError('无效的access_token')

        data['openid'] = openid

        # 检验短信验证码
        mobile = data['mobile']
        sms_code = data['sms_code']
        redis_conn = get_redis_connection('verify_codes')
        real_sms_code = redis_conn.get('sms_%s' % mobile)
        if real_sms_code.decode() != sms_code:
            raise serializers.ValidationError('短信验证码错误')

        # 如果用户存在,检查用户密码
        try:
            user = User.objects.get(mobile=mobile)
        except User.DoesNotExist:
            pass
        else:
            password = data['password']
            if not user.check_password(password):
                raise serializers.ValidationError('密码错误')
            data['user'] = user
        return data

    def create(self, validated_data):
        user = validated_data.get('user')
        if not user:
            # 用户不存在
            user = User.objects.create_user(
                username=validated_data['mobile'],
                password=validated_data['password'],
                mobile=validated_data['mobile'],
            )

        OAuthUser.objects.create(
            openid=validated_data['openid'],
            user=user
        )
        return user

在oauth/views.py修改AuthUserView 视图,增加post方法

class AuthUserView(GenericAPIView):
    """
    登录的用户
    """
    serializer_class = OAuthUserSerializer
    def post(self, request):
        """
        保存登录用户数据
        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()

        # 生成已登录的token
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)

        response = Response({
            'token': token,
            'user_id': user.id,
            'username': user.username
        })

        return response
前端

在oauth_back.js中实现

// 保存
        on_submit: function(){
            this.check_pwd();
            this.check_phone();
            this.check_sms_code();

            if(this.error_password == false && this.error_phone == false && this.error_sms_code == false) {
                axios.post(this.host + '/oauth/qq/user/', {
                        password: this.password,
                        mobile: this.mobile,
                        sms_code: this.sms_code,
                        access_token: this.access_token
                    }, {
                        responseType: 'json',
                    })
                    .then(response => {
                        // 记录用户登录状态
                        sessionStorage.clear();
                        localStorage.clear();
                        localStorage.token = response.data.token;
                        localStorage.user_id = response.data.user_id;
                        localStorage.username = response.data.username;
                        location.href = this.get_query_string('state');    
                    })
                    .catch(error=> {
                        if (error.response.status == 400) {
                            this.error_sms_code_message = error.response.data.message;
                            this.error_sms_code = true;
                        } else {
                            console.log(error.response.data);
                        }
                    })
            }
        }

用户中心个人信息

前端访问个人信息页面时,需要向后端请求个人信息。

在本页面中要显示用户的Email邮箱信息,而对于邮箱信息我们要实现对于邮箱的验证功能,并在本页面中显示邮箱是否已验证,如下所示,

个人信息页面

所以我们需要修改User模型类,增加邮箱是否验证的字段。

class User(AbstractUser):
    """
    用户信息
    """
    mobile = models.CharField(max_length=11, unique=True, verbose_name="手机号")
    email_active = models.BooleanField(default=False, verbose_name='邮箱验证状态')

进行数据库迁移

python manage.py makemigrations
python manage.py migrate
后端接口设计:

请求方式: GET /user/

请求参数: 无

返回数据: JSON

返回值 类型 是否必须 说明
id int 用户id
username str 用户名
mobile str 手机号
email str email邮箱
email_active bool 邮箱是否通过验证

在users/serializers.py中创建序列化器

class UserDetailSerializer(serializers.ModelSerializer):
    """
    用户详细信息序列化器
    """
    class Meta:
        model = User
        fields = ('id', 'username', 'mobile', 'email', 'email_active')

在users/views.py 中新建视图

from rest_framework.permissions import IsAuthenticated

class UserDetailView(RetrieveAPIView):
    """
    用户详情
    """
    serializer_class = serializers.UserDetailSerializer
    permission_classes = [IsAuthenticated]

    def get_object(self):
        return self.request.user

注意:访问视图必须要求用户已通过认证(即登录之后)

前端

修改user_center_info.html,增加Vue的变量

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
<html xmlns=" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>美多商城-用户中心</title>
    <link rel="stylesheet" type="text/css" href="css/reset.css">
    <link rel="stylesheet" type="text/css" href="css/main.css">
    <script type="text/javascript" src="js/host.js"></script>
    <script type="text/javascript" src="js/vue-2.5.16.js"></script>
    <script type="text/javascript" src="js/axios-0.18.0.min.js"></script>
    <script>
        var user_id = sessionStorage.user_id || localStorage.user_id;
        var token = sessionStorage.token || localStorage.token;
        if (!(user_id && token)) {
            location.href = '/login.html?next=/user_center_info.html';
        }
    </script>
</head>
<body>
    <div id="app" v-cloak>
    <div class="header_con">
        <div class="header">
            <div class="welcome fl">欢迎来到美多商城!</div>
            <div class="fr">
                <div class="login_btn fl">
                    欢迎您:<em>{{ username }}</em>
                    <span>|</span>
                    <a @click="logout">退出</a>
                </div>
                <div class="user_link fl">
                    <span>|</span>
                    <a rel="nofollow" href="user_center_info.html">用户中心</a>
                    <span>|</span>
                    <a rel="nofollow" href="cart.html">我的购物车</a>
                    <span>|</span>
                    <a rel="nofollow" href="user_center_order.html">我的订单</a>
                </div>
            </div>
        </div>        
    </div>

    <div class="search_bar clearfix">
        <a rel="nofollow" href="index.html" class="logo fl"><img src="images/logo.png"></a>
        <div class="sub_page_name fl">|&nbsp;&nbsp;&nbsp;&nbsp;用户中心</div>
        <form method="get" action="/search.html" class="search_con fr mt40">
            <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
            <input type="submit" class="input_btn fr" name="" value="搜索">
        </form>
    </div>

    <div class="main_con clearfix">
        <div class="left_menu_con clearfix">
            <h3>用户中心</h3>
            <ul>
                <li><a rel="nofollow" href="user_center_info.html" class="active">· 个人信息</a></li>
                <li><a rel="nofollow" href="user_center_order.html">· 全部订单</a></li>
                <li><a rel="nofollow" href="user_center_site.html">· 收货地址</a></li>
                <li><a rel="nofollow" href="user_center_pass.html">· 修改密码</a></li>
            </ul>
        </div>
        <div class="right_content clearfix">
                <div class="info_con clearfix">
                <h3 class="common_title2">基本信息</h3>
                        <ul class="user_info_list">
                            <li><span>用户名:</span>{{ username }}</li>
                            <li><span>手机号:</span>{{ mobile }}</li>
                            <li>
                                <span>Email:</span>
                                <div v-if="set_email">
                                    <input v-model="email" type="email" name="email">
                                    <input @click="save_email" type="button" name="" value="保 存">
                                    <input @click="set_email=false" type="reset" name="" value="取 消">
                                    <div v-if="email_error">邮箱格式错误</div>
                                </div>
                                <div v-else-if="email">
                                    {{ email }} 
                                    <div v-if="email_active">已验证</div>
                                    <div v-else>
                                        待验证<input @click="save_email" :disabled="send_email_btn_disabled" type="button" :value="send_email_tip">
                                    </div>
                                </div>
                                <div v-else>
                                    <input @click="set_email=true" type="button" name="" value="设 置">
                                </div>
                            </li>            
                        </ul>
                </div>

                <h3 class="common_title2">最近浏览</h3>
                <div class="has_view_list">
                    <ul class="goods_type_list clearfix">
                <li>
                    <a rel="nofollow" href="detail.html"><img src="images/goods/goods003.jpg"></a>
                    <h4><a rel="nofollow" href="detail.html">360手机 N6 Pro 全网通</a></h4>
                    <div class="operate">
                        <span class="prize">¥2699.00</span>
                        <span class="unit">台</span>
                        <a rel="nofollow" href="#" class="add_goods" title="加入购物车"></a>
                    </div>
                </li>

                <li>
                    <a rel="nofollow" href="#"><img src="images/goods/goods004.jpg"></a>
                    <h4><a rel="nofollow" href="#">360手机 N6 Pro 全网通</a></h4>
                    <div class="operate">
                        <span class="prize">¥2699.00</span>
                        <span class="unit">台</span>
                        <a rel="nofollow" href="#" class="add_goods" title="加入购物车"></a>
                    </div>
                </li>

                <li>
                    <a rel="nofollow" href="#"><img src="images/goods/goods005.jpg"></a>
                    <h4><a rel="nofollow" href="#">360手机 N6 Pro 全网通</a></h4>
                    <div class="operate">
                        <span class="prize">¥2699.00</span>
                        <span class="unit">台</span>
                        <a rel="nofollow" href="#" class="add_goods" title="加入购物车"></a>
                    </div>
                </li>

                <li>
                    <a rel="nofollow" href="#"><img src="images/goods/goods006.jpg"></a>
                    <h4><a rel="nofollow" href="#">360手机 N6 Pro 全网通</a></h4>
                    <div class="operate">
                        <span class="prize">¥2699.00</span>
                        <span class="unit">台</span>
                        <a rel="nofollow" href="#" class="add_goods" title="加入购物车"></a>
                    </div>
                </li>

                <li>
                    <a rel="nofollow" href="#"><img src="images/goods/goods007.jpg"></a>
                    <h4><a rel="nofollow" href="#">急速路由</a></h4>
                    <div class="operate">
                        <span class="prize">¥64.5</span>
                        <span class="unit">6.45/500g</span>
                        <a rel="nofollow" href="#" class="add_goods" title="加入购物车"></a>
                    </div>
                </li>
            </ul>
        </div>
            </div>
    </div>
    <div class="footer">
        <div class="foot_link">
            <a rel="nofollow" href="#">关于我们</a>
            <span>|</span>
            <a rel="nofollow" href="#">联系我们</a>
            <span>|</span>
            <a rel="nofollow" href="#">招聘人才</a>
            <span>|</span>
            <a rel="nofollow" href="#">友情链接</a>        
        </div>
        <p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
        <p>电话:010-****888    京ICP备*******8号</p>
    </div>
</div>
<script type="text/javascript" src="js/user_center_info.js"></script>
</body>
</html>

在js目录中新建user_center_info.js

var vm = new Vue({
    el: '#app',
    data: {
        host,
        user_id: sessionStorage.user_id || localStorage.user_id,
        token: sessionStorage.token || localStorage.token,
        username: '',
        mobile: '',
        email: '',
        email_active: false,
        set_email: false,
        send_email_btn_disabled: false,
        send_email_tip: '重新发送验证邮件',
        email_error: false,
    },
    mounted: function(){
        // 判断用户的登录状态
        if (this.user_id && this.token) {
            axios.get(this.host + '/user/', {
                    // 向后端传递JWT token的方法
                    headers: {
                        'Authorization': 'JWT ' + this.token
                    },
                    responseType: 'json',
                })
                .then(response => {
                    // 加载用户数据
                    this.user_id = response.data.id;
                    this.username = response.data.username;
                    this.mobile = response.data.mobile;
                    this.email = response.data.email;
                    this.email_active = response.data.email_active;
                })
                .catch(error => {
                    if (error.response.status==401 || error.response.status==403) {
                        location.href = '/login.html?next=/user_center_info.html';
                    }
                });
        } else {
            location.href = '/login.html?next=/user_center_info.html';
        }
    },
    methods: {
        // 退出
        logout: function(){
            sessionStorage.clear();
            localStorage.clear();
            location.href = '/login.html';
        },
        // 保存email
        save_email: function(){

        }
    }
});

邮件与验证

学习目标:
  • 使用Django发送邮件的方法
  • 邮件激活的机制
业务说明:

在用户中心页面中,我们允许用户设置邮箱

设置邮箱

当用户点击保存后,我们会向用户发送邮件以验证邮箱的有效性。

验证邮件

为了避免用户未收到验证邮箱,我们提供“重新发送验证邮件”按钮允许用户重新发送邮件。

重新发送验证邮件

邮箱验证成功,显示已验证。

邮箱已验证

技术说明:

在邮件中提供的激活链接地址,为了能区分是哪个用户在进行邮箱验证,需要在链接中包含用户和邮箱的识别信息,如user_id和email数据,但是基于安全性的考虑,不能将这两个数据直接暴露在邮件链接中,而是需要进行隐藏和签名处理(能够检测出是否修改过链接数据)。可以使用前面学过的itsdangerous对user_id和email数据进行处理,生成token作为链接的参数。

使用Django发送邮件

Django中内置了邮件发送功能,被定义在django.core.mail模块中。发送邮件需要使用SMTP服务器,常用的免费服务器有:[163](

1)注册163邮箱itcast88,登录后设置。

发送邮件

2)在新页面中点击“客户端授权密码”,勾选“开启”,弹出新窗口填写手机验证码。

发送邮件

3)填写授权码。

发送邮件

4)提示开启成功。

发送邮件

  1. 在Django配置文件中,设置邮箱的配置信息
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
  
  
#发送邮件的邮箱
  
  
EMAIL_HOST_USER = 'itcast88@163.com'
  
  
#在邮箱中设置的客户端授权密码
  
  
EMAIL_HOST_PASSWORD = 'python808'
  
  
#收件人看到的发件人
  
  
EMAIL_FROM = 'python<itcast88@163.com>'
  1. 使用Django提供的模块发送邮件

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字符串

例如:

msg='<a rel="nofollow" href=" target="_blank">点击激活</a>'
send_mail('注册激活','',settings.EMAIL_FROM, ['itcast88@163.com'], html_message=msg)