天天生鲜Django项目(一)

1、电商模式

B2B:Business to Business企业对企业(阿里巴巴)

C2C:Customer to Cusomer个人对个人(淘宝、瓜子二手车)

B2C:Business to Customer企业对个人(唯品会)

C2B:Customer to Business个人对企业(商品宅配)(个人提出需求,企业进行生产)

O2O:Online to Offline线上到线下(美团、饿了么)

F2C:Factory to Customer厂商到个人(省去中间商)

B2B2C:企业(供应商)-企业(电商企业)-个人(消费者)(京东、天猫)

 

2、页面说明

index.html首页(有商品分类菜单,“注册|登录”和“用户信息”切换显示,轮播图)

list.html商品列表页

detail.html商品详情页

cart.html购物车页

place_order.html提交订单页

login.html登录页

register.html注册页

user_center_info.html用户中心-用户信息页

user_center_order.html用户中心-用户订单页

user_center_site.html用户中心-收货地址页

 

3、SPU和SKU

SPU=Standard Product Unit 标准产品单位,是商品信息聚合的最小单位,如iphone7是一个SPU,不管黑色、金色还是64G、128G都共有一个商品简介、商品宣传视频等信息,是一个抽象出来的概念,不能对应一个具体的商品;

SKU=Stock Keeping Unit 库存量单位,是物理上不可分的最小存货单元,经过种类、规格的划分,如iphone7玫瑰金128G是SKU,能真正对应一个具体的商品;

 

4、数据库设计

注意:一对多的关系,不能把多条放在同一个表,应该把多的抽出来单独建个表;

用户表:

ID,用户名,密码,邮箱,是否激活,权限标识(普通用户/管理员)

地址表:

ID,用户ID,收件人,地址,邮编,联系方式,是否默认

商品SKU表:

ID,SKU名称,简介,价格,单位,库存,销量,图片(一张),状态(上下架),种类ID,SPU ID

(商品与图片是一对多关系,应该把图片从商品里分离出来作为一个新表,但是为了在显示商品列表时更快,还是要把一张用于列表的图片放到商品表里,因为如果不放的话,列表里显示50个商品就要关联查询50次,关联查询次数这么多影响效率,所以我们以空间换取时间)

商品SPU表:

ID,SPU名称,详情

(其实种类ID也属于SPU表,因为iphone7金色和黑色都属于同一种类,但是还是因为上一个原因,如果把他放在SPU表,那么在商品列表页就要大量使用关联查询,效率低)

商品图片表:

ID,图片,SKU商品ID

商品种类表:

ID,种类名称,logo,图片

首页轮播表:

ID,SKU ID,图片,index(顺序)

首页促销活动表:

ID,图片,活动页面url,index

首页分类商品展示表:

ID,SKU ID,种类ID,index,展示标识(图片/文字形式)

订单信息表:

ID,收货地址ID,用户ID,支付方式,总金额,运费,支付状态,创建时间

订单商品表:

ID,SKU ID,订单ID,商品数量,下单时价格,评论

redis实现购物车功能、浏览记录功能;

 

5、搭建Django框架

前提:安装了django、pymysql、mysql等包;

找一个合适的路径,生成Django项目和应用:

用PyCharm打开,在项目目录下新建一个python package叫apps,把所有应用包含进去:

 

6、注册应用(经测试方案二不好使,只能用方案一)

1)方案一:路径加一层apps/

因为apps/是自己建的,直接写userDjango找不到在哪,要写apps/user:

# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'apps.cart',  #购物车模块
    'apps.goods',  # 商品模块
    'apps.order',  # 订单模块
    'apps.user',  # 用户模块
]
# urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('apps.user.urls')),
]

2)方案二:把apps/加入BASE_DIR

因为Django找注册的应用时实在path里找,就像电脑的环境变量,BASE_DIR在path里(BASE_DIR就是项目的绝对路径),所以他会找BASE_DIR的儿子目录,但是不会找孙子目录(即能找到apps,找不到apps/user),因此我们只要把apps也加入到path中,就能找到apps下的user了:

# settings.py追加

# 把apps/加入到path
import sys
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'cart',  #购物车模块
    'goods',  # 商品模块
    'order',  # 订单模块
    'user',  # 用户模块
]
# urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include('user.urls', namespace='user')),  # 用户模块;namespace用于反向解析生成url
    path('order/', include('order.urls', namespace='order')),  # 订单模块
    path('cart/', include('cart.urls', namespace='cart')),  # 购物车模块
    path('', include('goods.urls', namespace='goods')),  # 商品模块,作为默认主页,无条件匹配,因此放在最下面

]

代码报黄,是因为PyCharm找不到这个路径,因为代码还没运行,apps/还没有加入到path,运行之后就没问题:

sys.path和os.path不一样,前者是环境变量,后者是操作路径的对象,可以进行路径拼接(join)等操作;

 

7、模板路径

项目根目录下创建templates文件夹并添加这个路径:

 

8、配置数据库

# settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'dailyfresh',
        'USER': 'root',
        'PASSWORD': 'root',
        'HOST': 'localhost',  # 用本机的mysql所以直接写localhost
        'PORT': 3306,
    }
}
# __init__.py

import pymysql

# 欺骗Django,躲过版本检查报错
pymysql.version_info = (1, 3, 13, 'final', 0)

pymysql.install_as_MySQLdb()

手动创建数据库:

desc tablename;  // 查看表结构

 注意:如果项目绑定的mysql在同一子网的另一台主机,那么另一台主机的mysql不能绑定127.0.0.1,要绑定别的主机能访问的IP,但是mysql默认绑定127.0.0.1,修改:

sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
修改 bind-address = xx.xx.xx.xx(ifconfig查到的IP)
sudo service mysql restart

除此之外,mysql所在主机还要授权项目所在主机,进入mysql客户端执行以下命令:

// 把dailyfresh数据库的所有表授权给该IP的root用户用密码root登录
grant all privileges on dailyfresh.* to 'root'@'项目所在主机IP' identified by 'root' with grant option;
// 使授权生效
flush privileges;

 

9、配置语言时区

 

10、静态文件目录

项目根目录下创建static文件夹并在settings.py中添加:

# settings.py追加

# 静态文件目录
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

 

11、应用url

每个应用都建一个urls.py,内容暂时为:

from django.urls import path

urlpatterns = [
   
]

Django的url系统其实就是路由;

 

12、设计模型类

项目目录下新建python package叫db,包含一个python file叫base_model.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='更新时间')
    is_delete = models.BooleanField(default=False, verbose_name='删除标记')

    class Meta:
        # 说明是一个抽象模型类
        abstract = True

# Meta是一个嵌套类,目的是给上级类添加一些功能,或指定一些标准
# 因为接下来要写的模型类都有一些共同的属性,因此把这些属性写到一个类里,让那些模型类继承这个类

接下来在应用的models.py中写模型类:

user/models.py:

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


class User(AbstractUser, BaseModel):
    '''用户模型类'''

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


class Address(BaseModel):
    '''地址模型类'''
    user = models.ForeignKey('User', verbose_name='所属账户', on_delete=models.CASCADE)
    receiver = models.CharField(max_length=20, verbose_name='收件人')
    addr = models.CharField(max_length=256, verbose_name='收件地址')
    zip_code = models.CharField(max_length=6, null=True, verbose_name='邮政编码')
    phone = models.CharField(max_length=11, verbose_name='联系电话')
    is_default = models.BooleanField(default=False, verbose_name='是否默认')

    class Meta:
        db_table = 'df_address'
        verbose_name = '地址'
        verbose_name_plural = verbose_name

# User模型类没有写太多字段,而是继承了Django内置的抽象类AbstractUser
# AbstractUser类是Django内置的用户认证系统用来认证的,
# 我们没有自己写用户认证的代码,而是用Django内置的认证系统,
# AbstractUser类里有用户名、密码、邮箱、激活否、管理员否等字段,
# 继承它之后我们就不用写这些字段了

goods/models.py:

from django.db import models
from db.base_model import BaseModel
from tinymce.models import HTMLField

class GoodsType(BaseModel):
    '''商品类型模型类'''
    name = models.CharField(max_length=20, verbose_name='种类名称')
    logo = models.CharField(max_length=20, verbose_name='标识')
    image = models.ImageField(upload_to='type', verbose_name='商品类型图片')

    class Meta:
        db_table = 'df_goods_type'
        verbose_name = '商品种类'
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.name


class GoodsSKU(BaseModel):
    '''商品SKU模型类'''
    status_choices = (
        (0, '下线'),
        (1, '上线'),
    )
    type = models.ForeignKey('GoodsType', verbose_name='商品种类', on_delete=models.CASCADE)
    goods = models.ForeignKey('Goods', verbose_name='商品SPU', on_delete=models.CASCADE)
    name = models.CharField(max_length=20, verbose_name='商品名称')
    desc = models.CharField(max_length=256, verbose_name='商品简介')
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品价格')
    unite = models.CharField(max_length=20, verbose_name='商品单位')
    image = models.ImageField(upload_to='goods', verbose_name='商品图片')
    stock = models.IntegerField(default=1, verbose_name='商品库存')
    sales = models.IntegerField(default=0, verbose_name='商品销量')
    status = models.SmallIntegerField(default=1, choices=status_choices, verbose_name='商品状态')

    class Meta:
        db_table = 'df_goods_sku'
        verbose_name = '商品'
        verbose_name_plural = verbose_name


class Goods(BaseModel):
    '''商品SPU模型类'''
    name = models.CharField(max_length=20, verbose_name='商品SPU名称')
    # 富文本类型:带有格式的文本
    detail = HTMLField(blank=True, verbose_name='商品详情')

    class Meta:
        db_table = 'df_goods'
        verbose_name = '商品SPU'
        verbose_name_plural = verbose_name


class GoodsImage(BaseModel):
    '''商品图片模型类'''
    sku = models.ForeignKey('GoodsSKU', verbose_name='商品', on_delete=models.CASCADE)
    image = models.ImageField(upload_to='goods', verbose_name='图片路径')

    class Meta:
        db_table = 'df_goods_image'
        verbose_name = '商品图片'
        verbose_name_plural = verbose_name


class IndexGoodsBanner(BaseModel):
    '''首页轮播商品展示模型类'''
    sku = models.ForeignKey('GoodsSKU', verbose_name='商品', on_delete=models.CASCADE)
    image = models.ImageField(upload_to='banner', verbose_name='图片')
    index = models.SmallIntegerField(default=0, verbose_name='展示顺序')

    class Meta:
        db_table = 'df_index_banner'
        verbose_name = '首页轮播商品'
        verbose_name_plural = verbose_name


class IndexTypeGoodsBanner(BaseModel):
    '''首页分类商品展示模型类'''
    DISPLAY_TYPE_CHOICES = (
        (0, "标题"),
        (1, "图片")
    )

    type = models.ForeignKey('GoodsType', verbose_name='商品类型', on_delete=models.CASCADE)
    sku = models.ForeignKey('GoodsSKU', verbose_name='商品SKU', on_delete=models.CASCADE)
    display_type = models.SmallIntegerField(default=1, choices=DISPLAY_TYPE_CHOICES, verbose_name='展示类型')
    index = models.SmallIntegerField(default=0, verbose_name='展示顺序')

    class Meta:
        db_table = 'df_index_type_goods'
        verbose_name = "主页分类展示商品"
        verbose_name_plural = verbose_name


class IndexPromotionBanner(BaseModel):
    '''首页促销活动模型类'''
    name = models.CharField(max_length=20, verbose_name='活动名称')
    # url = models.URLField(verbose_name='活动链接')  # 改成CharField这样可以不存链接
    url = models.CharField(max_length=256, verbose_name='活动链接')
    image = models.ImageField(upload_to='banner', verbose_name='活动图片')
    index = models.SmallIntegerField(default=0, verbose_name='展示顺序')

    class Meta:
        db_table = 'df_index_promotion'
        verbose_name = "主页促销活动"
        verbose_name_plural = verbose_name

# HTMLField不是Django内置的类型,是自己引入的富文本类型,
# 富文本和普通文本的区别是富文本带有格式,
# 商品详情往往不是普通文本而是带有格式的
# logo字段不是图图片类型而是CharField,
# 因为前端的logo不是直接指定的图片,而是一个雪碧图,
# 通过class来指定不同logo,而这个CharField就是class名字
# choices字段用来限定取值范围

这里用到的HTMLField要安装并配置tinymce:

tinymce只是富文本编辑器的一种,登陆到admin界面后,该字段用富文本编辑器编辑,存到数据库中是一段html字符串,其sql类型是longtext;

# settings.py追加

# 富文本编辑器配置
TINYMCE_DEFAULT_CONFIG = {
    'theme': 'advanced',
    'width': 600,
    'height': 400,
}
# urls.py添加url

urlpatterns = [
    path('admin/', admin.site.urls),
    path('tinymce/',include('tinymce.urls')),  # 富文本编辑器
    path('user/', include('user.urls', namespace='user')),
    path('order/', include('order.urls', namespace='order')),
    path('cart/', include('cart.urls', namespace='cart')),
    path('', include('goods.urls', namespace='goods')),
]

用到的ImageField也要安装:

order/models.py:

from django.db import models
from db.base_model import BaseModel


class OrderInfo(BaseModel):
    '''订单模型类'''
    PAY_METHOD_CHOICES = (
        (1, '货到付款'),
        (2, '微信支付'),
        (3, '支付宝'),
        (4, '银联支付')
    )

    ORDER_STATUS_CHOICES = (
        (1, '待支付'),
        (2, '待发货'),
        (3, '待收货'),
        (4, '待评价'),
        (5, '已完成')
    )

    order_id = models.CharField(max_length=128, primary_key=True, verbose_name='订单id')
    user = models.ForeignKey('user.User', verbose_name='用户', on_delete=models.CASCADE)
    addr = models.ForeignKey('user.Address', verbose_name='地址', on_delete=models.CASCADE)
    pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=3, verbose_name='支付方式')
    total_count = models.IntegerField(default=1, verbose_name='商品数量')
    total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品总价')
    transit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单运费')
    order_status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name='订单状态')
    trade_no = models.CharField(max_length=128, verbose_name='支付编号')

    class Meta:
        db_table = 'df_order_info'
        verbose_name = '订单'
        verbose_name_plural = verbose_name


class OrderGoods(BaseModel):
    '''订单商品模型类'''
    order = models.ForeignKey('OrderInfo', verbose_name='订单', on_delete=models.CASCADE)
    sku = models.ForeignKey('goods.GoodsSKU', verbose_name='商品SKU', on_delete=models.CASCADE)
    count = models.IntegerField(default=1, verbose_name='商品数目')
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='商品价格')
    comment = models.CharField(max_length=256, verbose_name='评论')

    class Meta:
        db_table = 'df_order_goods'
        verbose_name = '订单商品'
        verbose_name_plural = verbose_name

# 支付编号是支付宝支付后产生的编号

 

13、指定Django认证系统使用的模型类

# settings.py追加

# Django认证系统使用的模型类
AUTH_USER_MODEL = 'user.User'

如果不指定的话,Django自己也有一个模型类,迁移之后那些字段(用户名密码邮箱激活否管理员否)都会创建在一个auth_user的表里,而不是我们自己创建的user.User,而我们的User模型类之所以没有定义任何字段,就是为了这里指定User作为认证模型,Django自动创建字段。

如果不指定,当执行python manage.py createsuperuser时,产生的管理员用户会加到auth_user表中,指定后,加到指定表中。

字段is_super_user = 1则说明是管理员用户,字段is_staff = 1则说明是普通用户;

 

14、迁移

这两步不是一帆风顺的,第一步报错:

原因:

一对多关系(外键ForeignKey)要指定on_delete属性,即指定如果外键被删除了,这个键怎么处置,参考:https://blog.csdn.net/KreaWu/article/details/89400647

(上面的代码是改完的了)

第二步报错:

Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.

原因:

没有设置app_name,修改:

# apps/cart/urls.py


from django.urls import path

app_name = 'cart'

urlpatterns = [

]
# apps/goods/urls.py


from django.urls import path

app_name = 'goods'

urlpatterns = [

]
# apps/order/urls.py


from django.urls import path

app_name = 'order'

urlpatterns = [

]
# apps/user/urls.py


from django.urls import path

app_name = 'user'

urlpatterns = [

]

即每个应用的urls.py在urlpatterns = [ ]之前添加app_name = 'xxxx' ;

迁移完成后,数据库中创建了表:

 

15、启动服务器

静态页面拷到static目录下:

至此,框架搭完了,还有两个小点:

1)verbose_name和verbose_name_plural

模型类和字段都可以指定verbose_name,就是在admin界面中显示的名称;

模型类还可以指定verbose_name_plural,即指定verbose_name的复数形式;

如果模型类的class Meta中verbose_name = '商品' ,则admin界面显示的表名是商品s,如果再加上verbose_name_plural = verbose_name 则显示的表名是商品,即复数和单数一样;
项目目录/admin.py中注册模型类:

2)模型类中ImageField类型字段有一个属性upload_to='goods'这是给Django内置的文件存储系统用的,在admin界面上传图片时就会保存到goods文件夹,但是我们不用Django内置的文件存储系统,而是用一个分布式文件存储系统,所以这个字段属性无所谓;

 

16、用户注册

把register.html拷贝到templates目录下;

写视图函数:

# user/views.py


from django.shortcuts import render

# /user/register
def register(request):
    '''显示注册页'''
    return render(request, 'register.html')

配置路由:

# user/urls.py

from django.urls import path
from apps.user import views

app_name = 'user'

urlpatterns = [
    path('register', views.register, name='register')
]

浏览器测试:

修改样式路径:

 
{% load static %}  {# 引入静态文件路径 #} 
{% static 'css/reset.css' %}  {# 所有静态文件都写成这种形式 #}

修改表单属性:

{# register.html #}

<form method="post" action="user/register_handle">
    {% csrf_token %}  {# 使用Django的csrf防护 #}

定义视图函数:

#user/views.py追加

# /user/register_handle
def register_handle(request):
    '''注册处理'''
    # 试图函数的基本步骤:接收数据,数据校验,业务处理,返回应答
    username = request.POST.get('user_name')
    password = request.POST.get('pwd')
    email = request.POST.get('email')
    allow = request.POST.get('allow')

    if not all([username, password, email]):  # all检验都不空
        return render(request, 'register.html', {'errmsg': '数据不完整'})
    if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
        return render(request, 'register.html', {'errmsg': '邮箱格式不正确'})
    if allow != 'on':
        return render(request, 'register.html', {'errmsg': '请同意协议'})

    user = User.objects.create_user(username, email, password)

    return redirect(reverse('goods:index'))  # namespace:name

# redirect重定向,reverse反向解析
# 反向解析就是根据namespace和name动态生成路径,不必把路径写死
# 注意反向解析时namespace:name冒号后面不要有空格!!!!!!!!!

配置路由:

# user/urls.py添加url

from django.urls import path
from apps.user import views

app_name = 'user'

urlpatterns = [
    path('register/', views.register, name='register'),  # 注册
    path('register_handle', views.register_handle, name='register_handle'),  # 注册处理
]

重定向的index页也要配置:

把static/index.html拷贝到templates目录下;

# goods/views.py

from django.shortcuts import render

# 127.0.0.1:8000
def index(requst):
    '''首页'''
    return render(requst, 'index.html')
# goods/urls.py

from django.urls import path
from apps.goods import views

app_name = 'goods'

urlpatterns = [
    path('', views.index, name='index'),  # 首页
]

浏览器测试:(用户密码统一11111111)

点击注册,成功跳转:

查看数据库,用户创建成功:(\G让结果分行显示)

密码是加密的(行业规范);is_active=1不行,我们要让刚注册的账户未激活,后面通过邮箱激活,所以:

# user/views.py register_handle修改

user = User.objects.create_user(username, email, password)
user.is_active = 0
user.save()

注册页面和注册处理用了两个路径,可以改成用一个路径:

# user/views.py

import re
from django.shortcuts import render, redirect
from django.urls import reverse

from apps.user.models import User

# /user/register
def register(request):
    if request.method == 'GET':
        # 显示注册页
        return render(request, 'register.html')
    elif request.method == 'POST':
        # 注册处理
        # 试图函数的基本步骤:接收数据,数据校验,业务处理,返回应答
        username = request.POST.get('user_name')
        password = request.POST.get('pwd')
        email = request.POST.get('email')
        allow = request.POST.get('allow')

        if not all([username, password, email]):  # all检验都不空
            return render(request, 'register.html', {'errmsg': '数据不完整'})
        if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return render(request, 'register.html', {'errmsg': '邮箱格式不正确'})
        if allow != 'on':
            return render(request, 'register.html', {'errmsg': '请同意协议'})

        user = User.objects.create_user(username, email, password)
        user.is_active = 0
        user.save()

        return redirect(reverse('goods:index'))  # namespace:name

        # redirect重定向,reverse反向解析
        # 反向解析就是根据namespace和name动态生成路径,不必把路径写死
        # 注意反向解析时namespace:name冒号后面不要有空格!!!!!!!!!
# user/urls.py

from django.urls import path
from apps.user import views

app_name = 'user'

urlpatterns = [
    path('register', views.register, name='register'),  # 注册
]

# 作为路径的最后一节,path('register')不能带/,否则报错Page Not Found
# templates/register.html改表单提交路径

<form method="post" action="/user/register">

到此为止用的是方法视图,不同的请求方式要定义不同的方法,现在改用类视图,定义一个视图类,对应一个路径,定义get()和post()成员方法,对应不同方式的请求:

# user/views.py

import re
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View

from apps.user.models import User

# /user/register
class RegisterView(View):
    '''注册'''
    def get(self, request):
        # 显示注册页
        return render(request, 'register.html')

    def post(self, request):
        # 注册处理
        # 试图函数的基本步骤:接收数据,数据校验,业务处理,返回应答
        username = request.POST.get('user_name')
        password = request.POST.get('pwd')
        email = request.POST.get('email')
        allow = request.POST.get('allow')

        if not all([username, password, email]):  # all检验都不空
            return render(request, 'register.html', {'errmsg': '数据不完整'})
        if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return render(request, 'register.html', {'errmsg': '邮箱格式不正确'})
        if allow != 'on':
            return render(request, 'register.html', {'errmsg': '请同意协议'})

        user = User.objects.create_user(username, email, password)
        user.is_active = 0
        user.save()

        return redirect(reverse('goods:index'))  # namespace:name

        # redirect重定向,reverse反向解析
        # 反向解析就是根据namespace和name动态生成路径,不必把路径写死
        # 注意反向解析时namespace:name冒号后面不要有空格!!!!!!!!!
# user/urls.py修改

path('register', views.RegisterView.as_view(), name='register'),  # 注册

类视图原理:

dispatch()方法,分发,根据不同的请求方式分发给不同的方法,实则获取method字符串,转小写,作为方法名调用方法;

 

17、用户激活

用户注册后,给该邮箱发一个链接,用户点击链接后服务器根据链接中的身份信息将用户激活;用户身份信息可以用用户在数据表中的id,但是不能明文,因为一旦被人看出来是id就会被攻击,因此用itsdangerous加密;

安装itsdangerous:(以后安装timeout的话用PyCharm的Terminal就能安装上)

测试itsdangerous:

Serializer(密钥,过期时间)创建对象,dumps(Json即字典)加密,loads(字节串)解密;

如图,只要密钥相同就能解密,过期之后解密报错:

163邮箱开启smtp服务:

会给手机发验证码,验证成功后设置授权码,授权码保存好,发邮件要用;

发送邮件:

# settings.py追加

# 发送邮件配置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'django_core_mail@163.com'  # 发邮件的邮箱
EMAIL_HOST_PASSWORD = 'django2019'  # 授权码
EMAIL_FROM = 'DailyFresh<django_core_mail@163.com>'  # 发件人昵称<EMAIL_HOST_USER>
# user/views.py修改注册视图

# /user/register
class RegisterView(View):
    '''注册'''
    def get(self, request):
        # 显示注册页
        return render(request, 'register.html')

    def post(self, request):
        # 注册处理
        # 试图函数的基本步骤:接收数据,数据校验,业务处理,返回应答
        username = request.POST.get('user_name')
        password = request.POST.get('pwd')
        email = request.POST.get('email')
        allow = request.POST.get('allow')

        if not all([username, password, email]):  # all检验都不空
            return render(request, 'register.html', {'errmsg': '数据不完整'})
        if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return render(request, 'register.html', {'errmsg': '邮箱格式不正确'})
        if allow != 'on':
            return render(request, 'register.html', {'errmsg': '请同意协议'})

        user = User.objects.create_user(username, email, password)
        user.is_active = 0
        user.save()

        seri = Serializer(settings.SECRET_KEY, 600)
        info = {'confirm': user.id}
        token = seri.dumps(info).decode('utf8')  # 加密完是bytes,转成utf-8

        subject = '天天生鲜用户激活'
        message = '足不出户,新鲜每一天'
        sender = settings.EMAIL_FROM
        receiver_list = [email]
        html_message = '<h1>%s,欢迎使用天天生鲜</h1>请点击链接以激活用户:<br/>' \
                       '<a href="http://127.0.0.1:8000/user/active/%s">' \
                       'http://127.0.0.1:8000/user/active/%s</a>' % (username, token, token)
        send_mail(subject, message, sender, receiver_list, html_message=html_message)

        return redirect(reverse('goods:index'))  # namespace:name

        # redirect重定向,reverse反向解析
        # 反向解析就是根据namespace和name动态生成路径,不必把路径写死
        # 注意反向解析时namespace:name冒号后面不要有空格!!!!!!!!!
        # message是纯文本,html_message可以是html,注意html_message是关键字参数
        # 有了html_message,就不显示message了
# user/views.py追加

# user/active
class ActiveView(View):
    '''用户激活'''
    def get(self, request, token):
        seri = Serializer(settings.SECRET_KEY, 600)
        try:
            info = seri.loads(token)
            id = info['confirm']

            user = User.objects.get(id=id)
            user.is_active = 1
            user.save()

            return redirect(reverse('user:login'))
        except SignatureExpired as e:
            # 激活链接已过期
            return HttpResponse('激活链接已过期')

# user/login
class LoginView(View):
    '''登录'''
    def get(self, request):
        '''显示登录页面'''
        return render(request, 'login.html')
# user/urls.py添加url

path('active/<token>', views.ActiveView.as_view(), name='active'),  # 激活
path('login', views.LoginView.as_view(), name='login'),  # 登录

# 路径中用<>捕获参数

 点击注册,跳转到主页:

  

查看数据库,is_active=0:

查看邮件(可能在垃圾箱),点击链接激活,跳转到登录页:

  

查看数据库,is_active=1:

激活完成。

PS:两处引包不同:

View:django.views.View  or  django.views.generic.base.View?

settings:dailyfresh.settings  or  django.conf.settings?

 

18、异步发邮件

Django内置的send_email()是阻塞的,为了用户体验,应该做成异步的;任何耗时的操作都应该做成异步的;

celery任务队列机制:

安装celery:

# 由于redis集群需要redis2.10.6,而celery的依赖包kombu最新版本不兼容redis2.10.6
# 所以必须用低版本kombu,由于依赖关系所以celery也要低版本

(env1fordjango) pip3 install kombu==4.2.0 
(env1fordjango) pip3 install celery==4.2.0

新建python package、python file:celery_tasks.tasks

定义任务函数

# celery_tasks/tasks.py

from celery import Celery
from django.core.mail import send_mail
from dailyfresh import settings

# 初始化Django环境变量,任务执行者要用到
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dailyfresh.settings')
django.setup()

# 创建一个celery实例对象,参数1随便写,参数2是中间人,本机redis,8号数据库
app = Celery('celery_tasks.tasks', broker='redis://127.0.0.1:6379/8')

# 定义任务函数,一个任务就是一个函数
@app.task
def send_active_email(to_email, username, token):
    subject = '天天生鲜用户激活'
    message = '足不出户,新鲜每一天'
    sender = settings.EMAIL_FROM
    receiver_list = [to_email]
    html_message = '<h1>%s,欢迎使用天天生鲜</h1>请点击链接以激活用户:<br/>' \
                   '<a href="http://127.0.0.1:8000/user/active/%s">' \
                   'http://127.0.0.1:8000/user/active/%s</a>' % (username, token, token)
    send_mail(subject, message, sender, receiver_list, html_message=html_message)

调用任务函数,发出任务:

# user/views.py修改注册视图,把发邮件的代码改为发出任务

# /user/register
class RegisterView(View):
    '''注册'''
    def get(self, request):
        # 显示注册页
        return render(request, 'register.html')

    def post(self, request):
        # 注册处理
        # 试图函数的基本步骤:接收数据,数据校验,业务处理,返回应答
        username = request.POST.get('user_name')
        password = request.POST.get('pwd')
        email = request.POST.get('email')
        allow = request.POST.get('allow')

        if not all([username, password, email]):  # all检验都不空
            return render(request, 'register.html', {'errmsg': '数据不完整'})
        if not re.match(r'^[a-z0-9][\w.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return render(request, 'register.html', {'errmsg': '邮箱格式不正确'})
        if allow != 'on':
            return render(request, 'register.html', {'errmsg': '请同意协议'})

        user = User.objects.create_user(username, email, password)
        user.is_active = 0
        user.save()

        seri = Serializer(settings.SECRET_KEY, 600)
        info = {'confirm': user.id}
        token = seri.dumps(info).decode('utf8')  # 加密完是bytes,转成utf-8

        # 发出任务
        send_active_email.delay(email, username, token)

        return redirect(reverse('goods:index'))  # namespace:name

        # redirect重定向,reverse反向解析
        # 反向解析就是根据namespace和name动态生成路径,不必把路径写死
        # 注意反向解析时namespace:name冒号后面不要有空格!!!!!!!!!
        # message是纯文本,html_message可以是html,注意html_message是关键字参数
        # 有了html_message,就不显示message了

启动redis服务,因为任务函数中指定的中间人是127.0.0.1,所以redis.conf中也要bind 127.0.0.1保持一致,否则连接不到;

启动任务执行者,celery_tasks.tasks是指定任务函数所在py文件,-l info是打印日志信息:

黄色警告不是对celery的,而是对Django的,意思是开启Debug会导致内存泄漏,不要在生产环境下开启Debug,在settings.py中这样改就不警告了,但没必要,因为我们不是生产环境:

注册:

成功收到邮件:

执行者也打印信息:

注意:

1)celery不是独立工作的,他只是Django的一个工具,要靠Django环境才能工作;

2)怎样把天天生鲜项目(任务发出者)和执行者部署在不同机器上:复制完整项目(dailyfresh目录)到执行者机器上,启动执行者,发出者不需要初始化Django环境变量,启动者必须初始化。

 

19、用户登录

login.html改静态文件路径(同register.html):

测试:

修改表单:

因为action不指定的话,是提交到当前地址栏的地址,而我们就是想让它提交到这个地址,跟register一样,显示和处理用同一个地址,所以不指定action;

定义视图:

# user/views.py登录视图添加post方法

# user/login
class LoginView(View):
    '''登录'''
    def get(self, request):
        '''显示登录页面'''
        return render(request, 'login.html')

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('pwd')
        if not all([username, password]):
            return render(request, 'login.html', {'errmsg': '数据不完整'})

        # Django内置的认证系统校验用户名密码(自动把password加密后查询)
        user = authenticate(username=username, password=password)
        print(user)
        if user is not None:
            if user.is_active:
                login(request, user)  # Django内置的认证系统把用户登录状态记录到session中
                return redirect(reverse('goods:index'))
            else:
                return render(request, 'login.html', {'errmsg': '用户未激活'})
        else:
            return render(request, 'login.html', {'errmsg': '用户名密码错误'})

用authenticate()会自动检测is_active字段,即使通过username和password查到了,如果is_active=0也会返回None,解除检测:

# settings.py追加

# 用Django内置的认证系统时不会检测is_active字段
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.AllowAllUsersModelBackend']

找个合适的位置输出errmsg:

 

19、设置用缓存存储session,设置用redis做缓存

安装:

pip3 install django-redis  # 安装django-redis会自动安装redis3.5.0
pip3 install redis==2.10.6  # 搭建集群需要redis2.10.6,再装回来

配置:

# settings.py中追加

# Django缓存设置,默认本机的内存就是缓存,可以设置为redis
CACHES = {
    'default':{
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/9',  # redis服务例程的地址
        'OPTION': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient'
        }
    }
}

# Django session设置,默认是数据库,可以设置为缓存或redis
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

启动redis服务:

 进入客户端,现在没有键:

登录后再看:

 

20、记住用户名

# user/views.py修改登录视图的get和post方法

# user/login
class LoginView(View):
    '''登录'''
    def get(self, request):
        '''显示登录页面'''
        # 判断有没有记住用户名
        if 'username' in request.COOKIES:  
            username = request.COOKIES.get('username')
            checked = 'checked'
        else:
            username = ''
            checked = ''
        return render(request, 'login.html', {'username': username, 'checked': checked})

    def post(self, request):
        username = request.POST.get('username')
        password = request.POST.get('pwd')
        if not all([username, password]):
            return render(request, 'login.html', {'errmsg': '数据不完整'})

        user = authenticate(username=username, password=password)
        print(user)
        if user is not None:
            if user.is_active:
                login(request, user)  # Django内置的认证系统把用户登录状态记录到session中
                response = redirect(reverse('goods:index'))

                # 如果勾选了,就把用户名保存到cookie,不勾选,就清除cookie里的用户名
                remember = request.POST.get('remember')
                if remember == 'on':
                    response.set_cookie('username', username, max_age=7*24*3600)  # cookie过期时间一周
                else:
                    response.delete_cookie('username')
                return response
            else:
                return render(request, 'login.html', {'errmsg': '用户未激活'})
        else:
            return render(request, 'login.html', {'errmsg': '用户名密码错误'})

模板中使用参数:

效果:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值