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': '用户名密码错误'})
模板中使用参数:
效果: