Django 项目搭建


Django项目搭建

本项目是一个典型的电商项目,采用Python语言编写,Django框架搭建。

github中创建远程仓库

github上创建仓库meiduo,填写描述,选择.gitignore文件的语言为python,软件许可协议为MIT

修改.gitignore的内容为:.idea/*.pyc*.log

新建分支dev

在本地克隆项目

git clone https://github.com/junsircoding/meiduo.git # 克隆项目
cd meiduo # 进入项目目录
git branch # 查看当前分支
git branch dev origin/dev # 克隆远程仓库中的dev分支
git checkout dev # 切换到dev分支

在虚拟环境中创建项目

workon django_py3_1.11 # 进入虚拟环境
django-admin startproject shop # 创建项目,项目名称为shop
cd shop # 进入项目目录
pwd # 查看当前地址并拷贝,在pycharm中打开

在Pycharm中搭建项目

重设settings文件路径

开发环境和上线环境用不同的配置文件比较容易部署和维护,故而最好重新设置settings的路径。

django自动创建的项目中,根目录下有一个同名目录,在此做一个约定:根目录的shop一级shop,根目录下的同名目录为二级shop

新建名为settingspython package于二级shop中,将原二级shop中的settings.py更名为dev.py,并将其移动到新建的settings目录中。

修改一级shop下的manage.py中的环境变量:

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings") # 原
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "shop.settings.dev") # 现

配置jinja2模板

dev.py(位于二级shop)中配置jinja2的模板:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2',  # jinja2模板引擎
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            # 补充Jinja2模板引擎环境
            'environment': 'meiduo_mall.utils.jinja2_env.jinja2_environment', 
        },
    },
]

新建名为utilspython package二级shop中。

新建名为jinja2_env.pypython fileutils目录中,并在其中写如下代码:

from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from jinja2 import Environment


def jinja2_environment(**options):
    env = Environment(**options)
    env.globals.update({
        'static': staticfiles_storage.url,
        'url': reverse,
    })
    return env


"""
确保可以使用Django模板引擎中的{% url('') %} {% static('') %}这类的语句 
"""

配置mysql数据库

新建mysql数据库

mysql -uroot -p111
create database shop charset=utf8; # 创建名为shop的数据库,指定字符编码为utf8
create user shoproot identified by '111'; # 创建名为shoproot的mysql新用户,授权其访问shop数据库
grant all on shop.* to 'shop'@'%'; # %表示匹配用户名为junsir的所有ip地址
flush privileges; # 刷新刚才所做的设置

配置mysql数据库于dev.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # 数据库引擎
        'HOST': '127.0.0.1', # 数据库主机
        'PORT': 3306, # 数据库端口
        'USER': 'shoproot', # 数据库用户名
        'PASSWORD': '111', # 数据库用户密码
        'NAME': 'shop' # 数据库名字
    },
}

修改__init__.py(二级shop目录),配置pymysql连接

from pymysql import install_as_MySQLdb
install_as_MySQLdb()

配置Redis中的缓存Session

dev.py(二级shop/settings)中添加如下内容:

CACHES = {
    "default": { # 默认
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/0",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    },
    "session": { # session
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    },
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"

配置工程日志

新建名为logspython package一级shop

新建名为shop.logfile于目录logs

dev.py(二级shop/settings)中添加日志的配置信息:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,  # 是否禁用已经存在的日志器
    'formatters': {  # 日志信息显示的格式
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s'
        },
        'simple': {
            'format': '%(levelname)s %(module)s %(lineno)d %(message)s'
        },
    },
    'filters': {  # 对日志进行过滤
        'require_debug_true': {  # django在debug模式下才输出日志
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {  # 日志处理方法
        'console': {  # 向终端中输出日志
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {  # 向文件中输出日志
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/shop.log'),  # 日志文件的位置
            'maxBytes': 300 * 1024 * 1024,
            'backupCount': 10,
            'formatter': 'verbose'
        },
    },
    'loggers': {  # 日志器
        'django': {  # 定义了一个名为django的日志器
            'handlers': ['console', 'file'],  # 可以同时向终端与文件中输出日志
            'propagate': True,  # 是否继续传递日志信息
            'level': 'INFO',  # 日志器接收的最低日志级别
        },
    }
}

配置静态页面

将静态页面文件拷贝到二级shop目录下

dev.py中配置静态文件路径

STATIC_URL = '/static/'

# 配置静态文件加载路径
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

配置模板文件

新建名为templatespython package二级shop中,将其标记为Template Folder

项目环境搭建完毕,开启服务器,在浏览器中查看效果

在浏览器中输入地址:127.0.0.1:8000/static/index.html

编写用户模块代码

新建名为appspython package二级shop

进入apps目录,用Django命令创建子应用:

cd apps
python ../../manage.py startapp users

dev.py中注册app,在二级shop/apps/users/apps.py中,右击app名称sConfig,选择Copy Reference,拷贝引用,在dev.py中粘贴

‘shop.apps.users.apps.UsersConfig’,

dev.py中追加导包路径

import sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))

改写注册内容:

‘users.apps.UsersConfig’,

新建名为urls.pypython file二级shop/apps/users中,在其中填写如下内容:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^register/$', views.RegisterView.as_view()),  # 匹配注册功能的路由
]

将此子路由添加至总路由(二级shop/urls.py):

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),  # 匹配admin站点的路由
    url(r'^', include('users.urls')),  # 匹配用户模块的路由
]

编写视图函数RegisterView二级shop/apps/users/views.py中:

from django.shortcuts import render
from django.views import View


class RegisterView(View):
    def get(self, request):
        return render(request, 'register.html')


编写用户模型类

Django自带了用户模型类,如要添加别的字段,只需继承Django自带的模型类,再添加自己的特有字段即可

二级shop/apps/users/models.py中添加如下内容:

from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.


class User(AbstractUser):
    """自定义用户模型类"""
    mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')

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

    def __str__(self):
        return self.username

dev.py中指定用户模型类:

AUTH_USER_MODEL = 'users.User' # 应用名.模型类名

迁移用户模型类

创建迁移文件

python manage.py makemigrations

执行迁移文件

python manage.py migrate

编码

注册功能

填写完注册页面表单后,后台要处理POST请求

二级shop/apps/users/views.py/RegisterView中添加如下代码:

def post(self, request):
    # 1.接收
    user_name = request.POST.get('user_name')
    pwd = request.POST.get('pwd')
    cpwd = request.POST.get('cpwd')
    phone = request.POST.get('phone')
    allow = request.POST.get('allow')
    # 2.验证
    # 2.1验证非空
    if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]):
        return http.HttpResponseBadRequest('参数不完整') # from django import http
    # 2.2用户名拼写是否符合规范,是否已存在该用户名
    if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
        return http.HttpResponseBadRequest('请输入5-20个字符') # import re
    if User.object.filter(username=user_name).count() > 0: # from .models import User
        return http.HttpResponseBadRequest('用户名已存在')
    # 2.3密码拼写是否符合规范,两次密码输入是否一致
    if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
        return http.HttpResponseBadRequest('请输入8-20的密码')
    if pwd != cpwd:
        return http.HttpResponseBadRequest('两次密码输入不一致')
    # 2.4手机号拼写是否符合规范,手机号是否已经被使用
    if not re.match(r'^1[345789]\d{9}', phone):
        return http.HttpResponseBadRequest('手机号格式不正确')
    if User.objects.filter(mobile=phone).count() > 0:
        return http.HttpResponseBadRequest('手机号已存在')
    # 3.创建用户对象并保存在列表中
    user = User.objects.create_user(username=user_name, password=pwd, mobile=phone)
    # 4.状态保持
    login(request, user) # from django.contrib.auth import login
    # 5.响应,返回首页
    return redirect('/') # from django.shortcuts import redirect

用Ajax异步校验的方式验证用户名是否存在

在子路由中添加ajax的路由:

url(r'^usernames/(?P[a-zA-Z0-9_-]{5,20})/count/$', views.UsernameCountView.as_view()),

二级shop/apps/users/views.py中添加视图类:

class UsernameCountView(View):
    def get(self, request, username):
        # 接收
        # 验证
        # 处理
        count = User.objects.filter(username=username).count()
        # 响应
        return http.JsonResponse({
            'code':RETCODE.OK, # from shop.utils.response_code import RETCODE
            'errmsg':'OK',
            'count':count
        })

用Ajax异步校验的方式验证手机号是否存在

在子路由中添加ajax的路由:

url('^mobiles/(?P1[3-9]\d{9})/count/$', views.MobileCountView.as_view()),

二级shop/apps/users/views.py中添加视图类:

class MobileCountView(View):
    def get(self, request, mobile):
        # 接收
        # 验证
        # 处理,查询统计
        count = User.objects.filter(mobile=mobile).count()
        # 响应
        return http.JsonResponse({
            'code':RETCODE.OK,
            'errmsg':'OK',
            'count':count
        })

新建名为response_codepython fileutils目录中,内容如下:

class RETCODE:
    OK                  = "0"
    IMAGECODEERR        = "4001"
    THROTTLINGERR       = "4002"
    NECESSARYPARAMERR   = "4003"
    USERERR             = "4004"
    PWDERR              = "4005"
    CPWDERR             = "4006"
    MOBILEERR           = "4007"
    SMSCODERR           = "4008"
    ALLOWERR            = "4009"
    SESSIONERR          = "4101"
    DBERR               = "5000"
    EMAILERR            = "5001"
    TELERR              = "5002"
    NODATAERR           = "5003"
    NEWPWDERR           = "5004"
    OPENIDERR           = "5005"
    PARAMERR            = "5006"
    STOCKERR            = "5007"

图片验证码

安装pillow

pip install Pillow

新建名为libspython package目录于二级shop中,将第三方图片验证码工具captcha拷贝至这里。

新建名为verificationsappapps中:

python ../../manage.py startapp verifications

注册appdev.py中:

'verifications.apps.VerificationsConfig',

新建路由表urls,在子路由中添加路由:

urlpatterns = [
    url(r'^image_codes/(?P[\w-]+)/$', views.ImagecodeView.as_view()), # from . import views
] # from django.conf.urls import url

在总路由中包含子路由:

urlpatterns = [
    url(r'^', include('verifications.urls')),
]

编写视图函数:

class ImagecodeView(View):
    def get(self, request, uuid):
        # 接收
        # 验证
        # 处理
        # 1.生成图形验证码数据:字符code、图片image
        text, code, image = captcha.generate_captcha()
        # from shop.libs.captcha.captcha import captcha
        # 2.保存字符,用于后续验证
        # 2.1连接redis,参数为caches中的键
        redis_cli = get_redis_connection('verify_code') # from django_redis import get_redis_connection
        # 2.2向redis中写数据
        redis_cli.setex(uuid, constants.IMAGE_CODE_EXPIRES, code)
        # from . import constants
        # 响应,输出图片数据
        return http.HttpResponse(image, content_type='image/png')

新建名为constantspy文件于verifications目录下,编写其内容如下:

# 验证码过期时间,当前为5分钟
IMAGE_CODE_EXPIRES = 60 * 5 # 设置过期时间
# 短信验证码过期时间,当前为5分钟
SMS_CODE_EXPIRES = 60 * 5
# 过期时间of短信是否重复发送的标记,当前为60s
SMS_CODE_FLAG_EXPIRES = 60

验证码字符在redis缓存中存储,在dev.py中新建缓存字段verify_code

"verify_code":{
    "BACKEND":"django_redis.cache.RedisCache",
    "LOCATION":"redis://127.0.0.1:6379/2", # 用第二个数据库
    "OPTIONS":{
        "CLIENT_CLASS":"django_redis.client.DefaultClient",
    }
},

注意:chrome有个大坑,它会缓存之前请求过的地址。验证码的请求地址是和host.js关联的。当更改了host.js时,重新访问发现地址并没有更改,这是chrome缓存的缘故,记得定期清理缓存。

短信验证码

拷贝工具代码yuntongxun(短信)子shop/libs目录中

verifications/url中添加路由:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^image_codes/(?P[\w-]+)/$', views.ImagecodeViews.as_view()),
    url('^sms_codes/(?P1[3-9]\d{9})/$', views.SmscodeView.as_view()),
]

verifications/views.py中添加名为SmscodeView的视图函数:

class SmscodeView(View):
    def get(self, request):
        # 接收
        image_code_request = request.GET.get('image_code') # 用户填写的图形验证码文本
        uuid = request.GET.get('image_code_id') # 图形验证码的唯一编号
        # 验证,图形验证码是否正确
        redis_cli = get_redis_connection('verify_code')
        image_code_redis = redis_cli.get(uuid)
        if not image_code_redis:
            return http.JsonResponse({
                'code':RETCODE.PARAMERR,
                'errmsg':'图形验证码已过期'
            })
        # 强制验证码只能使用一次
        redis_cli.delete(uuid)
        # 判断用户输入的值是否正确
        # 注意:所有从redis总读取的数据都是bytes,需要转码
        if image_code_redis.decode() != image_code_request.upper():
            return http.JsonResponse({
                'code':RETCODE.PARAMERR,
                'errmsg':'图形验证码错误'
            })
        # 处理
        # 1.生成六位随机数
        sms_code = '%6d' % random.randint(0, 999999)
        # 2.保存到redis
        redis_cli.setex('sms_'+mobile, 300, sms_code)
        # 发短信,from shop.lib.yuntongxun.sms import CCP
        # ccp = CCP()
        # ret = ccp.send_template_sms(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)
        print(sms_code)
        # 响应
        return http.JsonResponse({
            'code':RETCODE.OK,
            'errmsg':'OK'
        })

在注册视图中验证短信验证码,改写二级shop/apps/users/views.py

def post(self, request):
    # 1.接收
    user_name = request.POST.get('user_name')
    pwd = request.POST.get('pwd')
    cpwd = request.POST.get('cpwd')
    phone = request.POST.get('phone')
    allow = request.POST.get('allow')
    sms_code_request = request.POST.get('msg_code') # 新添加
    # 2.验证
    # 2.1验证非空
    if not all([user_name, pwd, cpwd, phone, allow, sms_code_request]):
        return http.HttpResponseBadRequest('参数不完整') # from django import http
    # 2.2用户名拼写是否符合规范,是否已存在该用户名
    if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
        return http.HttpResponseBadRequest('请输入5-20个字符') # import re
    if User.objects.filter(username=user_name).count() > 0: # from .models import User
        return http.HttpResponseBadRequest('用户名已存在')
    # 2.3密码拼写是否符合规范,两次密码输入是否一致
    if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
        return http.HttpResponseBadRequest('请输入8-20的密码')
    if pwd != cpwd:
        return http.HttpResponseBadRequest('两次密码输入不一致')
    # 2.4手机号拼写是否符合规范,手机号是否已经被使用
    if not re.match(r'^1[345789]\d{9}', phone):
        return http.HttpResponseBadRequest('手机号格式不正确')
    if User.objects.filter(mobile=phone).count() > 0:
        return http.HttpResponseBadRequest('手机号已存在')
    # 2.5短信验证码,from django_redis import get_redis_connection
    # 连接redis
    redis_cli = get_redis_connection('verify_code')
    # 读取短信验证码
    sms_code_redis = redis_cli.get('sms_' + phone)
    # 判断是否过期
    if not sms_code_redis:
        return http.HttpResponseBadRequest('短信验证已过期')
    # 强制立即过期
    redis_cli.delete('sms_' + phone)
    # 判断短信验证码是否正确
    if sms_code_redis != sms_code_request:
        return http.HttpResponseBadRequest('短信验证码错误')
    
    # 3.创建用户对象并保存在列表中
    user = User.objects.create_user(username=user_name, password=pwd, mobile=phone)
    # 4.状态保持
    login(request, user) # from django.contrib.auth import login
    # 5.响应,返回首页
    return redirect('/') # from django.shortcuts import redirect

避免频繁发送短信,添加如下代码于verificatons/views.py中:

# 判断用户输入的值是否正确
# 注意:所有从redis总读取的数据都是bytes,需要转码
if image_code_redis.decode() != image_code_request.upper():
    return http.JsonRequest({
        'code':RETCODE.PARAMERR,
        'errmsg':'图形验证码错误'
    })
# 是否已经向此手机号发送过短信,新加代码
if redis_cli.get('sms_flag_' + mobile):
    return http.JsonResponse({
        'code':RETCODE.PARAMERR,
        'errmsg':'已经向次手机号发过短信,请查看手机'
    })
# 处理
# 1.生成六位随机数
sms_code = '%6d' % random.randint(0, 999999)
# 2.保存到redis
redis_cli.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code)
# 存储60s发送的标记,新加代码
redis_cli.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1)
# 发短信,from shop.lib.yuntongxun.sms import CCP
# ccp = CCP()
# ret = ccp.send_template_sms(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)
print(sms_code)

使用pipeline优化与redis交互,只与redis服务器交互一次,执行多条命令

# 1.生成六位随机数
# sms_code = '%6d' % random.randint(0, 999999)
# 2.保存到redis
# redis_cli.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code)
# 存储60s发送的标记,新加代码
# redis_cli.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1)
# 改写成下面这样
redis_pl = redis_cli.pipeline()
redis_pl.setex('sms_'+mobile, constants.SMS_CODE_EXPIRES, sms_code)
redis_pl.setex('sms_flag_' + mobile, constants.SMS_CODE_FLAG_EXPIRES, 1)
redis_pl.execute()

使用celery框架实现异步

新建名为celery_taskspython package一级shop

新建名为mainpython filecelery_tasks中,内容如下:

from celery import Celery
import os

os.environ["DJANGO_SETTINGS_MODULE"] = "shop.settings.dev"
# 创建主对象
app = Celery('shop')
# 读取配置:指定消息队列,当前使用redis
app.config_from_object('celery_tasks.config')
# 自动识别
app.autodiscover_tasks([
    'celery_tasks.sms',
])

新建名为configpython filecelery_tasks中,内容如下:

broker_url = 'redis://127.0.0.1:6379/15'

新建名为smspython packagecelery_tasks

新建名为taskspython filesms中,内容如下:

from shop.libs.yuntongxun.sms import CCP
from celery_tasks.main import app

@app.task(bind=True, name='send_sms', retry_backoff = 3)
def send_sms(self, to, datas, tempid):
    try:
        #ccp = CCP()
        #ret = ccp.send_template_sms(to, datas, 1)
        print(datas[0])
    except Exception as e:
        self.retry(exc = e, max_retries = 3)

shell中运行如下命令,开启celery服务:

celery -A celery_tasks.main worker -l info

调用任务,于二级shop/apps/users/views.py中:

from celery_tasks.sms.tasks import send_sms
# 发短信,from shop.lib.yuntongxun.sms import CCP
# ccp = CCP()
# ret = ccp.send_template_sms(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)
# print(sms_code)
send_sms.delay(mobile, [sms_code, contansts.SMS_CODE_EXPIRES / 60], 1)

登录功能

二级shop/apps/users/urls.py中添加登录的路由:

url('^login/$', views.LoginView.as_view()),

二级shop/apps/users/views.py中添加视图类:

class LoginView(View):
    def get(self, request):
        return render(request, 'login.html') # 之后拷贝模板
    def post(self, request):
        # 接收
        username = request.POST.get('username')
        pwd = request.POST.get('pwd')
        # 验证
        if not all([user_name, pwd]):
            return http.HttpResponseBadRequest('参数不完整') # from django import http
        # 2.2用户名拼写是否符合规范,是否已存在该用户名
        if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
            return http.HttpResponseBadRequest('请输入5-20个字符') # import re
        if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
            return http.HttpResponseBadRequest('请输入8-20的密码')
        # 处理
        user = authenticate(username = username, pwd = pwd) # from django.contrib.auth import authenticate
        if user is None:
            # 用户名或密码错误
            return render(request, 'login.html', {
                'loginerror':'用户名或密码错误'
            })	
        else:
            # 登录成功,状态保持
            login(request, user)
            return redirect('/')
        # 响应

多账号登录

新建名为shop_backendspython fileutils中,内容如下:

from django.contrib.auth.backends import ModelBackend
class ShopModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
    	# username接收的数据,可能是手机号,也可能是用户名
        try:
            if re.match('^1[3-9]\d{9}$', username):
                # 手机号
                user = User.objects.get(mobile=username)
            else:
                # 用户名
                user = User.objects.get(username=username)
        except:
           	return None
        else:
            # 验证密码
            if user.check_password(password):
                return user
            else:
                return None

dev.py中添加自定义认证类型:

AUTHENTICATION_BACKENDS = ['shop.utils.auth_backends.ShopModelBackend']

首页用户名显示

新建应用,cd shop/shop/appspython ../../manage.py startapp contents

注册应用,新建urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    url('^$', views.IndexView.as_view()),
]

dev.py中注册应用:

'contents.apps.ContentsConfig',

在总路由中添加子路由:

url('^', include('content.urls')),

添加视图类:

from django.shortcuts import render
from django.views import View
class IndexView(View):
    	def get(self, request):
            return render(request, 'index.html')

更改users/views.py.LoginView

else:
    # 登录成功,状态保持
    login(request, user)
    # 向cookie中输出用户名,用于在前端提示登录状态
    response = redirect('/')
    response.set_cookie('username', user.username, max_age=60*60*24*14)
    return response

更改users/views.py.RegisterView

# 响应
response = redirect('/')
    response.set_cookie('username', user.username, max_age=60*60*24*14)
    return response		

退出

users/views.py中添加新的视图类:

class LogoutView(View):
    def get(self, request):
        # 删除session
        logout(request)
        # 删除cookie
        response = redirect('/')
        response.delete_cookie('usename')
        return response

users.urls.py中添加路由:

url('^logout/$', views.LogoutView.as_view()),

用户个人信息

添加路由在users/urls.py中:

url('^info/$', views.InfoView.as_view()),

添加视图类在users/views.py中:

class InfoView(View):
    def get(self, request):
        # 判断是否登录
        if request.user.is_authenticated:
            # 已登录
            return render(request, 'user_center_info.html')
        else:
            # 未登录
            return redirect('/login/')

拷贝页面至templates中。

判断是否登录

dev.py中指定登录页视图:

LOGIN_URL = '/login/'

改写views.py中的代码:

from django.contrib.auth.mixins import LoginRequiredMixin
class InfoView(LoginRequiredMixin, View):
    def get(self, request):
        # 判断是否登录
        # if request.user.is_authenticated:
            # 已登录
            # return render(request, 'user_center_info.html')
        # else:
            # 未登录
            # return redirect('/login/')
        return render(request, 'user_center_info.html')

完善登录视图代码:

class LoginView(View):
    def get(self, request):
        return render(request, 'login.html') # 之后拷贝模板
    def post(self, request):
        # 接收
        username = request.POST.get('username')
        pwd = request.POST.get('pwd')
        # 接收重定向参数,新加代码
        next_url = request.GET.get('next', '/')
        # 验证
        if not all([user_name, pwd]):
            return http.HttpResponseBadRequest('参数不完整') # from django import http
        # 2.2用户名拼写是否符合规范,是否已存在该用户名
        if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', user_name):
            return http.HttpResponseBadRequest('请输入5-20个字符') # import re
        if not re.match(r'[0-9A-Za-z]{8,20}$', pwd):
            return http.HttpResponseBadRequest('请输入8-20的密码')
        # 处理
        user = authenticate(username = username, pwd = pwd) # from django.contrib.auth import authenticate
        if user is None:
            # 用户名或密码错误
            return render(request, 'login.html', {
                'loginerror':'用户名或密码错误'
            })	
        else:
        # 登录成功,状态保持
        login(request, user)
        # 向cookie中输出用户名,用于在前端提示登录状态
        response = redirect(next_url)
        response.set_cookie('username', user.username, max_age=60*60*24*14)
        return response

QQ授权登录

在虚拟环境中安装QQLoginTool

pip install QQLoginTool

新建名为oauth的应用,python ../../manage.py startapp oauth

新建urls.py,内容如下:

from django.conf.urls import url
from . import views
urlpatterns = [
    url('^qq/login/$', views.QQurlView.as_view()),
    url('^oauth_callback$', views.QQopenidView.as_view()),
]

在总路由中添加子路由:

url('^', include('oauth.urls')),

dev.py中注册app

'oauth.apps.OauthConfig',

views.py中创建类视图:

from django.shortcuts import render
from django.views import View
from django import http
from QQLoginTool.QQtool import  OAuthQQ
from django.conf import settings

class QQurlView(View):
    def get(self, request):
        # 生成QQ授权地址url
        next_url = request.GET.get('next', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        login_url = oauthqq_tool.get_qq_url()
        return http.JsonResponse({
            'code':RETCODE.OK,
            'errmsg':'OK',
            'login_url':login_url
        })
class QQopenidView(View):
    def get(self, request):
        # 获取授权用户的唯一标识openid
        # 接收code
        code = request.GET.get('code')
        next_url = request.GET.get('state', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        try:
            # 根据code获取token
            token = oauthqq_tool.get_access_token(code)
            # 根据token获取openid
            openid = oauthqq_tool.get_openid(token)
        except:
            openid = '0'
        return http.HttpResponse(openid)

dev.py中添加QQ授权信息:

QQ_CLIENT_ID = '101518219'
QQ_CLIENT_SECRET = '418d84ebdc7241efb79536886ae95224'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8000/oauth_callback'

/etc/hosts中添加127.0.0.1 www.meiduo.site

dev.py中添加:ALLOWED_HOSTS = ['www.meiduo.site',]

host.js中的var host = 'http://www.meiduo.site:8000'的注释打开,其余均注释

QQ账号信息与本网站绑定

新建视图类于oauth/model.py中:

from users.models import User
from shop.utils.models import BaseModel

class OAuthQQUser(models.Model):
    user = models.ForeignKey(User)
    openid = models.CharField(max_length = 50)

    class Meta:
        db_table = 'tb_oauth_qq'

新建名为models.pypython file文件于utils目录中,内容为:

from django.db import models
class BaseModel(models.Model):
    create_time = models.DateField(auto_now_add = True)
    update_time = models.DateField(auto_now = True)
    class Meta:
        # 当前模型类为抽象类,用于继承,不需要创建表
        abstract = True
    

执行迁移:

python manage.py makemigrations
python manage.py migrate

改写oauth/views.py文件:

from django.shortcuts import render
from django.views import View
from django import http
from QQLoginTool.QQtool import  OAuthQQ
from django.conf import settings
from .models import OAuthQQUser

class QQurlView(View):
    def get(self, request):
        # 生成QQ授权地址url
        next_url = request.GET.get('next', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        login_url = oauthqq_tool.get_qq_url()
        return http.JsonResponse({
            'code':RETCODE.OK,
            'errmsg':'OK',
            'login_url':login_url
        })
class QQopenidView(View):
    def get(self, request):
        # 获取授权用户的唯一标识openid
        # 接收code
        code = request.GET.get('code')
        next_url = request.GET.get('state', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        try:
            # 根据code获取token
            token = oauthqq_tool.get_access_token(code)
            # 根据token获取openid
            openid = oauthqq_tool.get_openid(token)
            # 绑定
            try:
                # 1.查询是否已经绑定过
                qquser = OAuthQQUser.objects.get(openid=openid)
            except:
                # 2.如果未绑定过,则提示绑定页面
                return render(request, 'oauth_callback.html') # 将oauth_callback.html放到templates目录中
                # 3.如果已绑定过,则状态保持
        except:
            openid = '0'
        return http.HttpResponse(openid)

views.py中添加post方法:

class QQopenidView(View):
    def get(self, request):
        # 获取授权用户的唯一标识openid
        # 接收code
        code = request.GET.get('code')
        next_url = request.GET.get('state', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        try:
            # 根据code获取token
            token = oauthqq_tool.get_access_token(code)
            # 根据token获取openid
            openid = oauthqq_tool.get_openid(token)
            # 绑定
            try:
                # 1.查询是否已经绑定过
                qquser = OAuthQQUser.objects.get(openid=openid)
            except:
                # 2.如果未绑定过,则提示绑定页面
                context = {'token':openid}
                return render(request, 'oauth_callback.html', context) # 将oauth_callback.html放到templates目录中
                # 3.如果已绑定过,则状态保持
        except:
            openid = '0'
        return http.HttpResponse(openid)
    def post(self, request):
        # openid与账号绑定

安装加密的包:itsdangerous

pip install itsdangerous

新建名为shop_signature.pypython file文件于utils.py中,内容如下:

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings

def dumps(json, expires):
    '''
    :param json:字典
    :param expires:加密数据的过期时间
    :return:字符串
    '''
    # 将字典加密,返回字符串
    # 1.创建工具对象
	serializer = Serializer(settings.SECRET_KEY, expires)
    # 2.加密
    serializer.dumps(json)
    # 3.转字符串,返回
    return s1.decode()

def loadds(s1, expires):
    '''
    :param s1:字符串
    :param expires:加密数据的过期时间
    :return:字典
    '''
    # 将字符串解密,返回字典
    # 1.创建工具对象
	serializer = Serializer(settings.SECRET_KEY, expires)
    # 2.解密
    try:
        json = serializer.loads(s1)
    except:
        return None
    # 3.返回字典
    return json
    

更改views.py

class QQopenidView(View):
    def get(self, request):
        # 获取授权用户的唯一标识openid
        # 接收code
        code = request.GET.get('code')
        next_url = request.GET.get('state', '/')
        # 创建工具对象,提供appid、appkey、回调地址
        oauthqq_tool = OAuthQQ(
            settings.QQ_CLIENT_ID,
            settings.QQ_CLIENT_SECRET,
            settings.QQ_CLIENT_URI,
            next_url
        )
        try:
            # 根据code获取token
            token = oauthqq_tool.get_access_token(code)
            # 根据token获取openid
            openid = oauthqq_tool.get_openid(token)
            # 绑定
            try:
                # 1.查询是否已经绑定过
                qquser = OAuthQQUser.objects.get(openid=openid)
            except:
                # 2.如果未绑定过,则提示绑定页面
                token = shop_signature.dumps({'openid':openid}, contants.OPENID_EXPIRES) # from shop.utils import shop_signature, from . import contants
                context = {'token':openid}
                return render(request, 'oauth_callback.html', context) # 将oauth_callback.html放到templates目录中
                # 3.如果已绑定过,则状态保持
        except:
            openid = '0'
        return http.HttpResponse(openid)
    def post(self, request):
        # openid与账号绑定

新建名为contantspython file文件于oauth目录中,内容如下:

# openid的过期时间
OPENID_EXPIRES = 60 * 10

写post方法:

def post(self, request):
    # openid与账号绑定
    # 接收
    mobile = request.POST.get('mobile')
    pwd = request.POST.get('pwd')
    sms_code_request = request.POST.get('sms_code')
    access_token = request.POST.get('access_token')
    next_url = request.GET.get('state')
    # 验证:与注册逻辑相同,参考注册代码
    json = shop_signature.loadds(access_token, contants.OPENID_EXPIRES)
    if json is None:
        return http.HttpResponseBadRequest('授权信息无效,请重新授权')
    openid = json.get('openid')
    # 处理
    # 根据手机号查询注册对象
    try:
        user = User.objects.get(mobile = mobile)
    except:
        # 手机号不存在,则注册新用户
        user = User.objects.create_user(username=mobile, password=pwd, mobile=mobile)
    else:
        # 手机号存在,则验证密码
        if not user.check_password(pwd):
            return http.HttpResponseBadRequest('密码错误')
    # 绑定
    OAuthQQUSer.objects.create(user=user, openid=openid) # from .models import OAuthQQUser
    # 状态保持
    login(request, user) # from django.contrib.auth import login
    # 响应
    response = redirect(next_url) # from django.shortcuts import redirect
    response.set_cookie('username', user.username, max_age = 60 * 60 * 24 * 14)
    return response

非初次授权的情况。补充get方法:

# 3.如果已绑定过,则状态保持
login(request, qquser.user)
response = redirect(next_url)
response.set_cookie('username', qquser.user.username, max_age = 60 * 60 * 24 * 14)
return response

显示用户的个人信息

改写users/views.py/InfoView

def InfoView(LoginRequiredMixin, View):
    def get(self, request):
        # 获取当前登录的用户
        user = request.user
        context = {
            'username':user.username,
            'mobile':user.mobile,
            'email':user.email,
            'email_active':user.email_active
        }
        

models.py/User中添加邮箱激活属性:

class User(AbstractUser):
    mobile = model.CharField(max_length=11)
    # 邮箱是否被激活
    email_active = models.BooleanField(default=False)

迁移:

python manage.py makemigratons
python manage.py migrate

邮箱

users/urls.py中添加路由:

url('^emails$', views.EmailView.as_view()),

users.views.py中添加视类图:

class EmailView(LoginRequiredMixin, View):
    def put(self, request):
        # 接收,以put方式发送的请求
        dict1 = json.loads(request.body.decode()) # import json
        email = dict1.get('email')
        # 验证
        if not all([email]):
            return http.JsonResponse({
                'code':RETCODE.PARAMERR,
                'errmsg':'没有邮箱参数'
            })
        if not re.match('^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return http.JsonResponse({
                'code':RETCODE.PARAMERR,
                'errmsg':'邮箱格式错误'
            })
        # 处理
        # 修改属性
        user = request.user
        user.email = email
        user.save()
        # 发邮件
        # 响应
        return http.JsonResponse({
            'code':RETCODE.OK,
            'errmsg':'OK'
        })
    

发邮件

dev.py中添加邮件服务器配置:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'hmmeiduo@163.com'
EMAIL_HOST_PASSWORD = 'hmmeiduo123'
EMAIL_FROM = '美多商城'
EMAIL_VERIFY_URL = 'http://www.meiduo.site:8000/emails/verification/'

新建名为mailpython packagecelery_tasks目录中,在其中新加文件tasks.py

在其中定义方法:

from django.core.mail import send_mail
from django.conf import settings
from celery_tasks.main import app
@app.task(name='send_user_email', bind=True)
def send_user_email(to_mail, verify_url):
    html_message = '您的邮箱为:%s,激活链接为%s' % (to_mail, verify_url)
    try:
        send_mail('美多商城-邮箱激活','', settings.EMAIL_FROM, [to_mail], html_message=html_message)
    except Exception as e:
        self.retry(exc=e, max_retries=2)

celery_tasks/sms/main.py中添加任务:

app.autodiscover_tasks([
    'celery_tasks.sms',
    'celery_tasks.mail',
])

启动celery:celery -A celery tasks.main worker -l info

views.py中调用任务

from celery_tasks.mail.tasks import send_user_email
...
# 发邮件,from django.conf import settings, from shop.utils import shop_signature,from . import contants
token = shop_signature.dumps({'user_id':user_id}, contants.EMAIL_EXPIRES)
verify_url = settings.EMAIL_VERIFY_URL + '?token=%s' % token
send_user_email.delay(email, verify_url)

users,目录总新建contants.py,内容为:

# 邮箱激活的有效时间
EMAIL_EXPIRES = 60 * 60 * 2

激活邮箱

新建视图类:

class EmailVerifyView(View):
    def get(self, request):
        # 接收
        token = request.GET.get('token')

        # 验证
        dict1 = meiduo_signature.loadds(token, contants.EMAIL_EXPIRES)
        if dict1 is None:
            return http.HttpResponseBadRequest('激活信息无效,请重新发邮件')
        user_id = dict1.get('user_id')

        # 处理
        try:
            user = User.objects.get(pk=user_id)
        except:
            return http.HttpResponseBadRequest('激活信息无效')
        else:
            user.email_active = True
            user.save()

        # 响应
        return redirect('/info/')

添加路由:

url('^emails/verifyication$', views.EmailVerifyView.as_view()),

收货地址

users/urls.py中添加路由:

url('^addresses$', views.AddressesView.as_view()),

users/views.py中定义视图类:

class AddressesView(LoginRequiredMixin, View):
    def get(self, request):
        return render(request, 'user_center_site.html')

新建应用

cd shop/shop/apps/
python ../../manage.py startapp areas

新建路由urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    
]

在总路由中添加子路由

url('^', include('areas.urls')),

注册app

'areas.apps.AreasConfig',