首先分析数据库模型!
用户表: id, 用户名, 密码, 邮箱, 激活标志, 权限标识(是否管理员)
地址表 :id, 收件人, 收件地址, 邮编, 联系方式, 用户id, 是否默认(是否是默认地址),
- 用户表和地址表是一对多的关系。
商品SKU表 :id , 商品名称, 简介,价格, 单位,库存量,图片(显示的图片),种id ,
销量(排序人气时直接用), 状态(是否上架下架) ,SPUid(根据这个id在列出其他规格的此类商品)
商品SPU表(保存通用概念): id,泛指名称,详情
SKU就是具体的商品 例如 32g官方标配 黑色iPhone
SPU是泛指的商品 例如iphone
作用: 在用户选中某个具体的商品时,会出现其他规格的这种商品。例如选中32g官方标配黑色 iPhone,会有 64g的选项,或者银色的选项。
商品种类表: id 种类名称 logo,种类图片
商品图片表: id 图片 sku-id
首页轮播商品表: id ,skuid ,图片,index(前后)
促销活动表: id ,图片,活动页面的url地址, index
首页分类商品展示表: id ,skuid, 种类id,展示标识(文字表示还是图片表示) index
Redis来实现购物车的功能。
Redis 实现历史浏览记录
订单信息表: id ,收货地址id,用户id,支付方式 ,*总金额,运费,支付状态,创建时间
订单商品表: id ,订单id ,skuid, 商品数量,商品价格,评论
创建djang项目
修改数据库配置, 在init中设置mysql默认连接,此时可能会碰到两个错误,(decode编码问题和pymysql版本问题 ,强制修改一下decode-->encode 注释pymysql的if判断版本语句)
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='更新时间')
delete_time = models.DateTimeField(default=False,verbose_name='删除标识')
class Meta:
abstract = True # 设置为抽象类
创建APP,设置根目录
- 在终端中 py manage.py startapp app 创建一个四个app,把它们放在一个python package下。package的名字为apps
sys.path是python搜索模块的路径集合
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
#可以import apps下面的模块。用到时就把这些模块都当作在外面,只是为了好看才放到里面。
- 但是在pycharm中可能会显示为错误,没有关系,这只是pycharm找不到,不代表程序找不到
-
注册时就可以直接写apps下面的模块名字。
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user', # 用户模块
'cart', # 购物车
'goods', # 商品
'order', # 订单
]
创建模型类
因为所有的模型类都有 创建时间,修改时间 ,删除标识,所以创建了一个继承自django.db.models.Model 的Base_Model类(单独创建一个python package把它放里面,用时引入即可)
然后good,order各模块中的类都继承自这个BaseModel,user继承自AbstractUser,和BaseModel
将静态文件拷贝到项目中
接收数据
temp = request.POST.get('id')数据校验
all([username,password,email]),其中的username,password,email全部为真返回True。数据处理,业务处理逻辑
user = User.objects.create(username=username,email=email ,password= password)
django自带的创建用户
返回页面
反向解析
from django.urls import reverse
return redirect(reverse('namespace,name'))
发送邮件
- 首先要有一个stmp邮箱 例如 163
然后在settings中配置邮箱
# 邮箱配置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.163.com'
# 163邮箱的 SMTP 地址
EMAIL_PORT = 25
# SMTP端口
EMAIL_HOST_USER = 'youremail@163.com'
# 我自己的邮箱
EMAIL_HOST_PASSWORD = '授权码'
# 我的邮箱授权码
EMAIL_SUBJECT_PREFIX = '[:)]'
# 为邮件Subject-line前缀,默认是'[django]'
EMAIL_USE_TLS = False
# 与SMTP服务器通信时,是否启动TLS链接(安全链接)。默认是false
EMAIL_FROM = '天天生鲜<daily_and_plan@163.com>'
# 与 EMAIL_HOST_USER 相同
- 然后再views.py中引入django内部的发邮件的模块
from django.core.mail import send_mail
send_mail(主题,正文,发件人,【收件人列表】)
subject = '中国欢迎你'
message = '正文'
html_message = '会解析为html'
sender = settings.EMAIL_FROM
mail =[email] 一定要是列表形式
send_email(subject,message,sender,email)
激活帐户
- 激活账户链接( http://127.0.0.1/user/active/
加密的身份信息
)
加密的方法:
pip install itsdangerous
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# 创建一个对象 它现在充当加密工具,但是如果知道密匙,就是解密工具。
#serializer = Serializer('密钥',过期时间)
serializer = Serializer('djsaklhd#%JFJF%^',3600) #加密后一小时过期
info = {'confirm':user.id} 身份信息
token = serializer.dumps(info) 加密
解密时:result = serializer.loads(info)
- 加密完成后,把链接发送到邮箱中。
- 当用户点击链接时
关于url调度器: -
https://yiyibooks.cn/qy/django2/topics/http/urls.html
路由匹配:进行解密。
路由匹配
url(r'^active/(?P<token>.*)$', views.ActiveView.as_view(), name='active'),
#或者
path('active/<token>', views.ActiveView.as_view(), name='active'),
视图中:
class ActiveView(View):
def get(self,request,token): 接受这个token
# 获取
from django.conf import settings
serializer = Serializer(settings.SECRET_KEY, 3600)
try:
info = serializer.loads(token) 解密
user_id = info["confirm"] 获取id
user = User.objects.get(id=user_id) 找到这个用户信息
user.is_active =1 修改使其激活状态
user.save() !保存
# 跳转到登陆页面
return redirect(reverse('user:login')) 重定向反向解析
except SignatureExpired as e:
# 激活链接过期
return HttpResponse('激活链接已经失效')
celery异步发邮件
- celery使用背景
当系统需要执行某些比较耗时的操作时,我们交由celery进行异步执行,例如:文件上传,发送邮件,图片处理。防止阻塞。
要有发布任务的,还要有broker(任务队列),还有worker监控任务队列。
发布任务
django程序
pip install celery
pip install redis
在虚拟环境中也要安装,除此之外还要改虚拟环境中的两个文件,下面worker中会说
broker (redis任务队列)
- 在服务器中安装redis
curl -O http://download.redis.io/releases/redis-4.0.9.tar.gz
mkdir redis
mv redis-4.09.tar.gz redis
cd redis
tar -xvf redis-4.09.tar.gz
cd redis-4.0.9
make
- 安装完redis,修改配置文件redis.conf
vim /home/downloads/redis/redis-4.0.9/redis.conf
daemonize yes
后台启动
bind ip
绑定本机网卡ip(ifconfig命令看一下),一般不要绑定127.0.0.1,回环地址,无法远程访问
protected-mode no
是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。要是开启了密码和bind,可以开启。否则最好关闭,设置为no。
timeout 100
延迟在100内会尝试重新链接
- 启动redis服务
进入相应的文件夹,ls可以看到redis.conf时,输入
./src/redis-server redis.conf
,按照配置文件启动
- 注意
打开6379端口,并在服务器中添加安全组规则6379
firewall-cmd --zone=public --add-port=6379/tcp --permanent
firewall-cmd --reload
firewall-cmd --query-port=6379/tcp
想要验证远程链接可以在windows中打开telnet
telnet 服务器公网ip 6379
出现空屏,左上角显示ip地址即为成功
- 注意2
在服务器本地验证,先看看redis有没有开启
ps aux|grep redis
如果有两个,那么一个是服务,一个是查询
kill 5386
先关闭redis
查看下redis的配置文件vim /相关目录/redis.conf
,看绑定的ip是不是本机的ip
./src/redis-server redis.conf
启动
./srcredis-cli -h ip地址 -p 6379
尝试连接 这个ip可以是网卡地址,可以是公网地址。(因为是自己连自己)
进入redis数据库,输入命令试试
# set key value
# get key
"value"
可以用了!
这里可以给redis起了个别名alias redis="./home/downloads/redis/redis-4.0.9/src/redis-server /home/downloads/redis/redis-4.0.9/redis.conf"
方便启动
worker
- 先进入root在进入虚拟环境
- 在任务的文件夹中执行
celery worker -A tasks --loglevel=info
tasks是任务名 -
celery multi start w1 -A celery_tasks.tasks -l info
后台会运行celery - 如果tasks在python package下,则
celery worker -A packagename.tasks --loglevel=info
- 注意:
在服务器中遇到了mysqlclient和decode的问题,某个虚拟环境目录中的lib
/data/env/pyweb/lib/python3.7/site-packages/django/db/backends/mysql
修改base.py 和 operations.py 注释if条件,decode->encode
缺少什么包,pip安装,因为不是在你pycharm的虚拟环境下,(windows)pycharm创建的虚拟环境无法进入(文件不一样,无法执行)。
收到了任务,但是任务一直为完成,没有显示succeed,说明程序执行到某个地方停止了,逐一排查后,发现在send_mail()处,原因竟是因为阿里云屏蔽了25端口,尝试以上添加6379端口的方法后无解,换成465端口,并在服务器中打开465,就可以得到邮件了。
登陆
- 前端页面中的form,如果不写action,跳转到地址栏中的地址。
在登陆逻辑中,使用django自带的认证系统authenticate(username,password),认证成功返回user对象,失败则返回None。
在认证成功返回到首页之前,记录一下user的状态,login(request,user)函数也是django自带的,默认保存在django的数据库中
from django.contrib.auth import authenticate,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":'数据不完整'})
user = authenticate(username=username,password=password) # 认证成功返回对象, 否则返回None
if user is not None:
if user.is_active:
login(request,user) # 记录登陆状态
return render(request,'index.html')
else:
return render(request,'login.html',{'errmsg':"用户未激活"})
else:
return render(request,'login.html',{'errmsg':"用户名或者密码错误"})
记住用户名
在校验完用户合法后,做了login()。然后不着急返回HttpResponse(回应),只是先创建一个对象,并给response。然会在这个response对象上加的东西COOKIE
在返回。设置COOKIE
使用httpresponse的set_cookie(‘key',value,max_age=过期时间秒
)方法
#if authenticate(username=username,password=password) is not None:
#if user.is_active:
response = redirect(reverse('goods:index'))
rem = request.POST.get('remember')
if rem =='on':
response.set_cookie('username',username,max_age=7*24*3600)
else:
response.delete_cookie('username')
return response
模板抽取
找一个具有代表性的页面,把所有地方都相同的直接放在base中,有部分页面相同的放在{%block name%}{endblock}中,各不相同的地方删除,然后设置一个block。可以设置多个base页面,最初的base页面内容应该是最少的,因为都放在了block中。然后可以进一步创建较为详细的base页面,user_center_base继承自base,用于作为用户信息的父模板。
模板中的地址使用反向解析{% url '
namespace
:name
'%}
django认证系统的装饰器login_required
用户未登录时,应该无法查看UserInfo,UserOrder,Address的信息。此时就用到了django认证系统中的装饰器
from django.contrib.auth.decorators import login_required
path('', login_required(UserInfoView.as_view()), name='user'),
在函数前用login_require装饰器装饰,如果用户未登录,会重定向到settings中设置的LOGIN_URL,所以在settings中要为LOGIN_URL赋值'/user/login'。
并且,重定向到的这个地址后面会跟一个问好,然后接参数next,这个参数的值就是你未登陆时的地址,我们可以把这个值接受,然后在用户登陆后转到这个地址。
这是get请求。所以可以使用GET方法获取接的参数next
部分代码
if user.is_active:
login(request, user) # 记录登陆状态
# logout(request)
next_url = request.GET.get('next',reverse('goods:index'))
#next的值为None,next_url = reverse('goods:index')
# print(next_url)
# 默认跳转到goods:index
# print(reverse('goods:index')) reverse的值是个字符串
# 先不返回,我们先接 一下
response = redirect(next_url)
rem = request.POST.get('remember')
if rem == 'on':
response.set_cookie(
'username', username, max_age=7 * 24 * 3600)
else:
response.delete_cookie('username')
return response
因为我们写的时类试图,所以无法在view中加入装饰器,但是可以在url中用login_required装饰
path('', login_required(UserInfoView.as_view()), name='user')
测试时记得把缓存清了,不然login()会在缓存中,系统会认为你登陆了。
改进login_required
- 新建一个工具package。然后在里面创建一个py文件,定义一个类。作用就是
Login_required
mixin.py
from django.contrib.auth.decorators import login_required
class LoginRequiredMixin(object):
@classmethod
#def as_view() 这个方法可以拷贝 ctrl b
def as_view(cls, **initkwargs):
view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
return login_required(view)
views.py
from utils.mixin import LoginRequiredMixin
class UserInfoView(LoginRequiredMixin,View):
'''用户信息'''
def get(self, request):
return render(request, 'user_center_info.html',{'page':'user'})
urls.py
path('', UserInfoView.as_view(), name='user'),
UserInfoView中并没有as_view()方法,所以会先找它第一个父类,LoginRequiredMixin,有as_view()方法,调用,它第二个父类的as_view()方法,然后返回的再用login_required包装。
跟上面的原理其实是一样的,用login_required装饰真正的as_view()。
登陆后注册登陆按钮隐藏
- django本身会在return的request加入一些属性,例如当前的用户的相关信息。
request.user.is_authenticated判断当前用户是否认证
request.user.属性 获取user的属性
<div class="fr">
{% if user.is_authenticated %}
<div class="login_btn fl">
欢迎你:{{ user.username }}
<span>|</span>
<a href="/user/logout">注销</a>
</div>
{% else %}
<div class="login_btn fl">
<a href="/user/login">登录</a>
<span>|</span>
<a href="/user/register">注册</a>
<span>|</span>
</div>
{% endif %}
<div class="user_link fl">
<span>|</span>
<a href="/user/">用户中心</a>
<span>|</span>
<a href="cart.html">我的购物车</a>
<span>|</span>
<a href="/user/order">我的订单</a>
</div>
</div>
用户地址页面
- get 显示
接受request中的user,根据user查询address,将这些变量返回到模板中显示。 - post 添加
接受数据request.POST.get
数据校验检测合法性以及有效性
业务处理添加数据 Address.objects.create(********************)
返回应答
小插曲 :一些继承自models.Manage的类可以定义一些方法。例如验证当前用户是否有默认地址。
class AddressManager(models.Manager):
"""地址模型管理器类"""
# 1. 改变原有查询的结果集:all()
# 2. 封装方法:用户操作模型类对应的数据表(增删查改)
def get_default_address(self, user):
# 获取用户的默认收货地址
try:
address = self.get(user=user, is_default=True)
except self.model.DoesNotExist:
address = None # 不存在默认地址
return address
用户中心的历史浏览记录
- 访问商品的详情页面时,添加历史浏览记录
所以在这里只有读redis的逻辑,没有写的逻辑,具体写的逻辑在详情的函数中,后面
- 访问用户中心个人信息页的时,显示历史浏览记录
- 历史浏览记录保存在redis数据库中,并用list来存储访问的商品id。
在UserInfoView中,首先导入模块
from goods.models import GoodsSKU
from django_redis import get_redis_connection
获取redis的链接,这个default就是settings中cache的default
con = get_redis_connection("default")
history_key = 'history_%d'%user.id
# 获取用户最新浏览的五个商品
sku_ids = con.lrange(history_key, 0, 4) # 获取商品ids
当用户访问商品的detail页面时,才会记录在历史浏览记录中,所以在detail中写存储的逻辑, history_key = 'history_%d'%user.id,然后根据键获取相应的value(也就是商品的id)sku_ids = con.lrange(history_key,0,4)
,根据商品的id,按顺序查询商品
goods_li = []
for id in sku_ids:
goods_li.append(GoodsSKU.objects.get(id=id))
最后把good_li返回,在模板中就可以使 用for循环来输出历史浏览记录了
{% for goods in goods_li %}
***********************goods.id,good.price
****注意下***********good.image.url 后面会说FastDFS
{% empty %} #如果为空
没有浏览记录
{% endfor %}
FastDFS上传和下载(删除)
客户端发出上传请求,tracker server 查看可用的存储空间,然后返回storage server的ip和端口号,然后客户端直接访问ip和端口号,将文件存储在storage server上,然后在返回file_id,文件内容是以hash存储的,所以上传相同的文件时,会直接给你返回file-id。
客户端发出下载请求,tracker看一下在哪个storage上,然后把ip和端口号返回给客户端,然后获取文件并下载。
安装fastdfs
https://my.oschina.net/harlanblog/blog/466487?fromerr=cqe6bTu2
参考上述链接
- 总结:安装依赖的文件,安装fastdfs,创建一个fastdfs目录,里面再创建两个目录,一个storage用于存储日志和上传的文件,一个tracker用户调度。
在tracker.conf中,设置base_path 的值 为tracker的目录路径
在storage.conf 中,设置base_path 的值为 storage的目录路径
设置storage_path0的值为 storage的目录路径
设置tracker_server=ip
:22122 这里的ip是ifconfig的ip
然后启动fastdfs的图tracker和storage
service fdfs_trackerd start
service fdfs_storage start
这里遇到了错误,换命令
systemctl status fdfs_trackerd.service
显示/usr/local/bin/fdfs_trackerd
Does not exit
或者显示/usr/local/bin/stop
,/bin/restart
,/bin/fdfs_storaged
Does not exit
这些文件都在/usr/bin中,逐一拷贝过去即可。再次执行命令成功启动。
但是ps aux|grep fdfs显示并没有启动tracker和storage,所以尝试还是到/usr/bin中启动
/usr/bin/fdfs_trackerd /export/FastDFS/conf/tracker.conf
/usr/bin/fdfs_storaged /export/FastDFS/conf/storage.conf
ps aux|grep fdfs
指定对应的conf文件就可以启动成功trackerd和storaged
打开端口
firewall-cmd --zone=public --add-port=22122/tcp --permanent
firewall-cmd --reload
firewall-cmd --query-port=22122/tcp
修改客户端配置:编写client.conf 文件,指定日志保存的位置base_path=/export/fastdfs/tracker
tracker_server =ip
:22122
- 进行上传文件测试
语法fdfs_upload_file /export/FastDFS/conf/client.conf 上传的文件
fdfs_upload_file /export/FastDFS/conf/client.conf /test.txt
- 上传成功,并返回了一个group*****.txt.
fdfs遇到的坑
我之前修改的tracker.conf 是在/export/Fastdfs/conf/tracker.conf,所以在启动的时候用
/usr/bin/fdfs_trackerd /export/FastDFS/conf/tracker.conf
这条命令指定了对应的conf来启动。
应该修改的conf文件应该是在/etc/fdfs/下面,然后回到/etc/fdfs目录下:按照下面修改
tracker.conf
base_path =
tracker的目录路径/export/fastdfs/tracker
storage.conf,base_path =
storage的目录路径/export/fastdfs/storage
storage_path0=
storage的目录路径/export/fastdfs/storage
tracker_server=
ip:22122
此时,使用service fdfs_trackerd start 也可启动成功
接着把这里的client.conf文件也改了
base_path = /export/fastdfs/tracker
tracker_server = ip:22122
然后执行上传文件,下图,成功。
配合使用fastdfs与nginx
之前安装的nginx为编译完的,所以无法动态的添加第三方模块,删除/usr/local/nginx,下载源码编译安装。
- 为nginx添加fastdfs-nginx-module
下载地址:https://github.com/happyfish100/fastdfs-nginx-module/
记住这个文件的路径,待会添加这个模块时,需要这个路径。 - 进入nginx的源码文件夹,执行
./configure --prefix=/usr/local/nginx-1.17.3 --add-module= /export/fastdfs-nginx-module/src
报错 Fatal erro Error 1
https://blog.csdn.net/zzzgd_666/article/details/81911892
修改fastdfs-nginx-module/src/config
ngx_module_incs="/usr/include/fastdfs /usr/include/fastcommon/
CORE_INCS="$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon/
- 重新执行
./configure --prefix=/usr/local/nginx-1.17.3 --add-module= /export/fastdfs-nginx-module/src
FastDFS+Nginx(补)
首先下载最新的libfastcommon,解压,进入文件夹,
./make.sh
,然后,./make.sh install
下载最新版本的fastdfs,解压,进入文件夹,编译,安装。
创建tracker和storage文件夹。
cd /etc/fdfs
cp tracker.conf.sample tracker.conf
cp storage.conf.sample storage.conf
vim tracker.conf
base_path=/export/tracker
vim storage.conf
base_path=/export/storage
srore_path0=/export/storage
tracker_server=ip:22122
service fdfs_trackerd start
service fdfs_storaged start
成功启动配置客户端
vim client.conf
base_path=/export/tracker
tracker_server=ip:22122测试
fdfs_upload_file /etc/fdfs/client.conf /test.txt
返回一串字符,成功。
安装nginx
make ,make install
为它添加第三方模块fastdfs_nginx_module
-
vim /export/download/fastdfd_nginx_module/src/config
修改一些内容,否则在执行make时会报错(Fatal error
)
下面的内容发生了更改
ngx_module_incs="/usr/include/fastdfs /usr/include/fastcommon/"
CORE_INCS="$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon/"
- 进入nginx源码目录
./configure --prefix=/usr/local/nginx-1.17.3 --add-module= /export/fastdfs-nginx-module/src
make
make install
- 未出现什么异常,添加第三方模块完成。
让nginx配合fastdfs
将模块中src的一些文件拷贝到/etc/fdfs下面
cp /export/download/fastdfs-nginx-module-1.20/src/mod_fastdfs.conf /etc/fdfs/mod_fastdfs.conf
vim
connetc_timeout=10
tracker_server=ip:22122
url_have_group_name=true
store_path0=/export/storage
将fastdfs中的conf下面的一些文件拷贝到/etc/fdfs下面
cd /export/download /fastdfs/conf
cp http.conf /etc/fdfs/http.conf
cp mime.types /etc/fdfs/mime.types
现在/etc/fdfs/下面有文件从哪里拷贝没有关系,就是让fdfs中有这些文件。
配置nginx配置文件
添加一个server{}
- 启动fdfs_tracker,fdfdfs_storage,
- 启动mginx,
- 阿里云端口打开,本地浏览器输入IP(或域名):8888/group1/M00*******.txt可正常访问。
FastDFS+Nginx完成,项目中自定义存储
https://yiyibooks.cn/xx/django_182/howto/custom-file-storage.html
windows中安装fdfs_client有点麻烦,需要下载fdfs_client.tar.gz。
解压,提取其中的fdfs_client_4.07
文件,将set.py中的31,32行注释(带有sendfilemodule.c
)
然后在虚拟环境中,进入到fdfs_client_4.07
,执行python setup.py install
。`
在工具包utils中创建一个python package,创建一个py文件作为自定的存储类。
from django.core.files.storage import Storage
from fdfs_client.client import Fdfs_client
class FDFSStorage(Storage):
def _open(self,name, mode='rb'):
pass
def _save(self,name, content):
'''保存文件时使用'''
# name 是你上传的文件的名字
# 包含你上传文件内容的File对象
# 创建一个对象
client = Fdfs_client('./utils/fdfs/client.conf')
# 上传到fdfs文件系统中 按照文件内容上传
res = client.upload_by_buffer(content.read())
# 返回的字典形式,可以在Fdfs_client函数中找到
# dict {
# 'Group name' : group_name,
# 'Remote file_id' : remote_file_id,
# 'Status' : 'Upload successed.',
# 'Local file name' : local_file_name,
# 'Uploaded size' : upload_size,
# 'Storage IP' : storage_ip
# }
if res.get('Status')!='Upload successed.':
# 上传失败
raise Exception('上传到fdfs失败')
# 获取返回的文件id
filename = res.get('Remote file_id')
return filename
def exists(self, name):
'''django判断文件名是否可用'''
'''重写这个方法,因为在fdfs中所有的文件名都是可用的,但是经过django,django会判断,所以要重写他'''
return False
重写的save方法流程类似于在服务器的上传文件的测试
通过配置文件创建一个对象;
调用这个对象的upload方法,(这里是按照内容上传);
得到返回的值,用于查看,下载。
- 安 装fdfs_client:
-windows中下载fdfs-client-py-master,注释setup中的ext_modules
两行代码,然后在fdfs_client文件夹中storage-client.py,注释from fdfs_client.sendfile import *
,然后在你的虚拟环境python setup.py install 。安装完还需要安装两个依赖的库pip install mutagen
pip install requests
-linux中无需更改。
注册一个类来进行上传测试,
from django.contrib import admin
Register your models here.
from .models import GoodsType
admin.site.register(GoodsType)
- 现在已经可以添加内容了,但是当查看的时候会出现错误,原因是因为你的自定义存储类中没有url方法。
def url(self,name):
'''返回文件的url路径'''
return 'i-sekai.site:8888'+name
此时返回的路径在浏览器中可以直接打开的
获取模型类,完成首页的前端页面展示
- 需要获取种类,轮播图,促销活动图,和首页中分类商品的展示信息,前三个比较简单,直接用.all()方法得到数据。
分类商品:
- 可以按照类型筛选,然后通过是文字显示还是图片显示分离出两种。
for type in types:
# 图片种类
image_banners = IndexTypeGoodsBanner.objects.filter(type = type,display_type=1).order_by('index')
# 文字种类
title_banners = IndexTypeGoodsBanner.objects.filter(type = type,display_type=0).order_by('index')
- 然后介于python是动态语言,所以可以动态的为他增加属性
# 动态增加属性
type.image_banners = image_banners
type.title_banners = title_banners
现在就 比较明朗了,一种有很多种类型,每种类型中有image_banners属性和title_banners属性,这两种属性中又包含很多组信息。
在前端展示时,
{% for type in types %}
{{ type.name }}
{% for foo1 in type.title_banners %}
{{ foo1.title }}
{% endfor %}
{% for foo2 in type.image_banners %}
{{ foo2.image.url}}
{% endfor %}
{% endfor %}
- 完成后,在admin中注册模型类,然后在后端管理界面上传图片,添加商品等。最后刷新网页,这里遇到一个问题。图片不能正常显示,F12查看,将src的内容放到地址栏中,一敲回车显示就有个另存为界面,下载完图片也是正常的,就是在前端无法显示,更改配置nginx配置文件无果,最后发现好像是地址不太对,前面没有http://。在setting中设置
FDFS_URL = 'http://i-sekai.site:8888/'
,CG!
- 还有我的购物车未实现。
因为用户会频繁的访问购物车,添加删除,所以放在redis中比较有效率。然后存储方式采用hash存储。
键 域 值[ 域 值 ···]
'cart_%d'%user.id商品(id)
1(商品数量)
4
在首页视图中,购物车只有当用户登陆时才会有数量的变化,所以,令cart_count = 0,当用登陆时,根据用户名获取该用户的一条数据,cart_count = hlen('cart_%d'%user.id),将cart_count的值返回到模板。
验证:登陆服务器。
cd /home/downloads/redis/redis-4.0.9
./src/redis-cli -h
ip地址
在setting中查看默认的redis的数据库为1号,进入redis中select 1
,进入一号数据库,物理的加入两条数据hmset cart_67 1 3 2 5
我的用户id为67,加入1号商品3件,2号商品5件,刷新页面我的购物车显示为2。CG!
首页 页面静态化及修改
- 当管理员修改首页信息对应的数据时,需要重新生成静态页面
celery
生成,生成的页面在worker端,所以在本地计算机是访问不到的。 - 我们借助nginx来让本地服务器访问。
在本地系统的task中再定义一个任务
import os
# import django
# os.environ.setdefault('DJANGO_SETTINGS_MODULE','dailyfresh.settings')
# 或者os.environ['DJANGO_SETTINGS_MODULE'] ='daily_fresh.settings')
# django.setup()
# 在worker中要先启动django环境,否则无法正常的导入类goods.models。
from django.http import request
from django.shortcuts import render
from django.template import loader,RequestContext
# 使用celery
from celery import Celery
from django.conf import settings
from django.core.mail import send_mail
from goods.models import GoodsType, IndexGoodsBanner, IndexPromotionBanner, IndexTypeGoodsBanner
from django_redis import get_redis_connection
@app.task
def generate_static_index_html():
types = GoodsType.objects.all()
# 获取轮播
goods_banners = IndexGoodsBanner.objects.all().order_by('index')
# 获取首页的促销活动信息
promotion_banners = IndexPromotionBanner.objects.all().order_by('index')
# 获取首页分类商品展示信息
for type in types:
# 图片种类
image_banners = IndexTypeGoodsBanner.objects.filter(
type=type, display_type=1).order_by('index')
# 文字种类
title_banners = IndexTypeGoodsBanner.objects.filter(
type=type, display_type=0).order_by('index')
# 动态增加属性
type.image_banners = image_banners
type.title_banners = title_banners
context = {
'types': types,
'goods_banners': goods_banners,
'promotion_banners': promotion_banners,
}
# 使用模板
# 加载模板文件
temp = loader.get_template('static_index.html')
# 定义模板上下文
context = RequestContext(request,context)
# 模板渲染
static_index_html = temp.render(context)
# 生成首页对应的静态文件
save_path = os.path.join(settings.BASE_DIR, 'static/index.html')
with open(save_path,'w') as f:
f.write(static_index_html)
将项目复制到服务器端,并取消注释django启动的那段代码。然后进入虚拟环境,
celery -A celery.tasks.tasks worker -l info
启动celery。
此时的celery worker已经准备就绪,等待任务的发出。我们在本地系统中python console中,导入任务函数,调用
.delay()
方法,模拟发布任务.
from celerytasks.tasks import generate_static_index_html
generate_static_index_html.delay()
遇到了问题,celery任务出错,原因是缺少fdfs_client, mutagen, requests。安装即可
服务器端收到任务,做出处理,并在项目中的static/下生成index.html文件。
- 打开nginx的配置文件,再添加一个server,监控80端口(
浏览器打开时的默认端口
),因为我们希望输入域名时就能打开静态页面,而不是还要在后面加上:8888
。- location就相当于url,匹配static时,访问static下面匹配的文件,匹配 / 时,访问static下面的index 或者index.htm或者index.html。
- 然后就是什么时候会发布任务?当用户登陆超级管理员,对首页的模型类进行增加和删除时!
- 在admin中,我们注册的模型类是使用的默认的admin.ModelAdmin,在完成修改操作时,并不能发布任务,所以我们可以定义一个它的子类,继承它的方法,在方法体中加上一句发布任务的代码,然后随着模型类注册。
from .models import GoodsType,IndexGoodsBanner,IndexPromotionBanner,IndexTypeGoodsBanner,GoodsSKU
from celery_tasks.tasks import generate_static_index_html
class BaseAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
generate_static_index_html.delay()
def delete_model(self, request, obj):
super().delete_model(request, obj)
generate_static_index_html.delay()
admin.site.register(GoodsType, BaseAdmin)
admin.site.register(IndexGoodsBanner, BaseAdmin)
admin.site.register(IndexPromotionBanner, BaseAdmin)
admin.site.register(IndexTypeGoodsBanner, BaseAdmin)
- 这样做并不完美,很多模型类与同一个ModelAdmin注册,不利于自定义,想要给某个类的字段修改属性,修改显示的列名比较不方便,所以可以为每个模型类都定义一个类,继承自BaseAdmin,类中pass,调用save_model()时,类中没有,向父类中找,在父类中,再调用父类的父类的save_model(),然后
.delay()
发布任务。
-
至此,完成首页的页面静态化(django中url配置'host/index--->index.html')(nginx调度判断是访问django网站的index还是celery生成的index)
首页数据的缓存
- 当用户访问index时,还是会多次的查询数据库,但是得到的东西在短时间内是相同的,所以我们可以设置缓存,把首页中的数据库数据放在上下文中(字典),然后把上下文存到缓存中,当用户再次访问这个链接时会先查看缓存中有没有数据,没有就查询并设置缓存,有就直接使用缓存中的数据。
缓存分为多种:站点级别的缓存,将整个网站缓存下来,太暴力了;单个view的缓存,我们的 IndexView并不是所有的数据都是相同的,不同的用户缓存相同的数据是没有意义的;模块片段缓存,这个听起来好像是符合要求,但是,在返回模板之前,我们就将数据放在了上下文中,而上下文中有用户的数据,所以不合适。
我们直接操作缓存的api:
from django.core.cache import cache
cache.set(key,value,timeout)
key是什么自己定义
value可以是很多类型,这里的上下文是字典。
timeout 过期时间,秒为单位。
用到缓存数据时
from django.core.cache import cache
context = cache.get(key)
存的是字典,拿出来还是字典。
什么时候需要更新首页的缓存数据?
- 当管理员修改首页数据时。所以在admin.ModelAdmin中修改。
怎么更新?
- 删除就好了,让他再次生成缓存。
历史浏览记录
- 当用户访问了某个商品的详情页面时,才会添加历史浏览记录
所以在DetailView中添加历史浏览记录的逻辑,大前提是用户已登录登陆,所以当用户登录时
# 添加历史浏览记录
# 如果用户访问了之前访问商品,要先把之前的删除,再添加
con = get_redis_connection('default')
history_key = 'history_%d' % user.id
con.lrem(history_key, 0, goods_id)
# 把goods_id插入到左侧
con.lpush(history_key, goods_id)
# 只保存用户最新浏览的五条信息
con.ltrim(history_key, 0, 4) # ltrim 裁剪
因为读取逻辑已经再用户中心写了,所以当你进入首页,点击商品详情页时,再次查看用户中心,会出现商品,而且是不会重复的按照最新商品进行排序的
其他规格
- 在详情页面中,有其他规格的同种商品,我们可以查询出来放到页面上显示
same_spu_skus = GoodsSKU.objects.filter(goods = sku.goods).exclude(id = goods_id)
# GoodsSKU有goods属性为SPU的外键,我们就是要查找出相同spu的不同sku。
# 其中不包含自己
列表页面
首先先设计url,需要传入list作为标识,type_id 和page是必须元素,放在斜线中,sort的排序方式是非必需的,在地址栏中用?sort=default传值,用request.GET.get('sort')获取。
path('list/<int:type_id>/<page>', ListView.as_view(), name='list'),
在模板中删除不必要的元素,然后看需要的数据:typeid获取当前类别,所有类别来显示商品分类,new_skus是新品推荐,然后就是此类中所有的商品skus,购物车数量。 在后来还需要skus_page是第page页的Paginator对象,里面是商品信息。还需要sort排序的方式,否则,当你跳转到第二页时,排序方式变了,你就无法自动查看当前排序方式的第二页,那就太不便利了。
class ListView(View):
def get(self,request, type_id,page):
'''显示列表页'''
# 先获取种类信息
try:
type = GoodsType.objects.get(id =type_id)
except GoodsType.DoesNotExist:
return redirect(reverse('goods:index'))
types = GoodsType.objects.all()
# 获取排序的方式
# sort = default ,按照默认id排序
# sort = price ,按照商品的价格排序
# sort = hot ,按照商品的销量排序
sort = request.GET.get('sort')
if sort=='price':
skus = GoodsSKU.objects.filter(type=type).order_by('price')
elif sort == 'hot' :
skus = GoodsSKU.objects.filter(type=type).order_by(('-sales'))
else :
# 其他情况sort = 'default',防止地址栏的sort = None 比较不美观
sort = 'default'
skus = GoodsSKU.objects.filter(type=type).order_by('-id')
# 对数据进行分页
paginator = Paginator(skus, 1)
# 获取第page页的内容
try:
page = int(page)
except Exception as e:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
# 获取到了page页的Paginator对象
skus_page = paginator.page(page)
#新品的信息
new_skus = GoodsSKU.objects.filter(type=type).order_by('-create_time')[:2]
# 购物车数目
user = request.user
cart_count = 0
if user.is_authenticated:
con = get_redis_connection('default')
cart_key = 'cart_%d' % user.id
cart_count = con.hlen(cart_key)
context = {
'type':type,
'types':types,
'skus_page':skus_page,
'new_skus':new_skus,
'cart_count':cart_count,
'sort':sort, # 不穿过去,在列表页跳转后sort方式又变回默认了。也就是说你没办法按照同一种排序方式浏览到第二页。
}
return render(request,'list.html',context)
- 分页
<div class="pagenation">
{% if skus_page.has_previous %}
<a href="{% url 'goods:list' type.id skus_page.previous_page_number %}?sort={{ sort }}">上一页</a>
{% endif %}
{% for pindex in skus_page.paginator.page_range %}
{% if pindex == skus_page.number %}
<a href="{% url 'goods:list' type.id pindex %}?sort={{ sort }}" class="active">{{ pindex }}</a>
{% else %}
<a href="{% url 'goods:list' type.id pindex %}?sort={{ sort }}" >{{ pindex }}</a>
{% endif %}
{% endfor %}
{% if skus_page.has_next %}
<a href="{% url 'goods:list' type.id skus_page.next_page_number %}?sort={{ sort }}">下一页></a>
{% endif %}
</div>
全文检索
全文检索引擎:haystack
搜索引擎:whoosh(纯python,性能不是很好)
安装
pip install django-haystack
haystack支持whoose,但是并没有whoose的包,所以环境中要安装whoose的包,在haystack.backend中的用于支持whoose的文件
中发现了导入whoose类的代码里面还有支持其他搜索引擎的文件
pip install whoosh
注册haystack
配置haystack
# 全文检索引擎配置
HAYSTACK_CONNECTIONS = {
'default': {
# 包中,haystack 中的backends中whoosh_backend-py中的WhooshEngine
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
# 索引文件的路径
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
},
}
# 添加,修改,删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
- 用法:
1.在你的应用的下方建立一个search_indexes.py文件
固定的
在其中定义索引类
from haystack import indexes
from goods.models import GoodsSKU
class GoodsSKUIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
# author = indexes.CharField(model_attr='user')
# pub_date = indexes.DateTimeField(model_attr='pub_date')
def get_model(self):
return GoodsSKU
# 建立索引数据
def index_queryset(self, using=None):
return self.get_model().objects.all() # filter(pub_date__lte=datetime.datetime.now())
- use_template=True代表根据自定义的模板来建立索引,所以我们要创建模板文件
- 创建模板文件
格式为固定
,在templates下创建文件夹名为search,然后在search文件夹中创建indexes文件夹,正好对应了之前的search_indexes.py文件
,在下面创建模型类的名字(goods),然后就到了模板文件,模型类小写_text.txt
# 指定根据表中的那些字段建立商品
{{ object.name }} # 根据商品的名称及建立索引
{{ object.desc }} # 点的是属性
{{ object.goods.detail }} # 根据商品的详情商城索引
object指的是当前模型类,丶 出来的是属性,name属性,desc属性,goods是外键,外键所在的模型类中有属性detail。
- 执行命令
python manage.py rebuild_index
,建立索引。!!!!!!!!!!!
在html中添加一个form标签,方法用get即可,让action = '/search',随便填,就是让浏览器到/search,然后再总url中配置
path('search/', include('haystack.urls')), # 全文检索框架
具体逻辑交由全文检索框架完成。
在网页中搜索草莓,发现找不到search/search.html网页,这是因为在search下面没有具体的search,html网页,我们创建它。
内容和list是相似的,copy一份,删除新品推荐,类别。留下遍历商品的的div和分页的div。商品的遍历使用
{% for item in page%}
{{ item.object.name }}
{{endfor}}
page对象是全文检索传过来的
全文检索框架会向这个页面传递数据
query:搜索关键字
page:当前页的page对象,遍历page对象是SearchResult类的实例对象,调用对象的object得到的是模型类的对象1遍历 2。object
paginator:分页的paginator对象
遇到的问题: 点击搜索,搜索不到东西。
原因是在分页时,需要一个p参数当前搜索的关键字
<a href="/search?q={{ query }}&page={{ pindex }}" class="active">{{ pindex }}</a>
在分页中存在这个参数,所以在被跳转的页面的input中要给name属性赋值,假设name='q',给了name属性,在点击提交时,地址栏就是这种类型127.0.0.1:8000/search/?q=something。然后分页中就是用到了/search?q={{ query }}&page={{pindex}}
。
直接删除分页的代码就可以解决这个问题,顺着找出问题!
遗留的小问题,search。html页面中的购物车是没有cart_count值的。
全文检索-----分词
whoose默认的分词对中文不太友好,我们可以用自定义的jieba。
- 安装
pip install jieba
- 进入到
.../sitepackages/haystack/backends
这个路径,下面有一个whoosh_backend.py,我们在settings中配置过这个文件,copy一份,名为whoosh_cn_backend.py,然后在里面把分词修改为jieba的分词。
from jieba.analyse import ChineseAnalyzer
然后找到
schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer()
,field_boost=field_class.boost)修改为ChinaeseAnalyzer。最后在settings中将全文检索框架的引擎修改为我们的魔改版(whoose_cn_backend.WhooseEngine),OK! - 重新生成一下索引文件,因为搜索是基于索引的。
python manage.py rebuild_index
- 搜索detail里面的中文,已经可以找到了。
jieba 基本用法
import jieba
str = '很不错的草莓'
res = jieba.cut(str,cut_all=True) # res是一个可迭代的对象
for i in res :
print(i)
--------------------------------------------
>>很
>>不错
>>的
>>草莓
详情页的+-和总价自动变化的js代码
update_goods_amount()
// 计算商品的总价
function update_goods_amount() {
price = parseFloat($('.show_pirze').children('em').text())
count = parseInt($('.num_show').val())
amount=price*count //解析为Float或者Int类型
$('.total').children('em').text(amount.toFixed(2)+'元') //保留几位小数,并转换为字符串
}
//增加数量
$('.add').click(
function () {
// 获取当前的数目并加1
count = parseInt($('.num_show').val())+1
$('.num_show').val(count)
update_goods_amount()
}
)
// 减少数量
$('.minus').click(
function () {
// 获取当前的数目并加1
if ((parseInt($('.num_show').val())-1)>0)
{count = parseInt($('.num_show').val())-1}
$('.num_show').val(count)
update_goods_amount()
}
)
// 手动输入商品的数量
$('.num_show').onblur(
function () {
//当失去焦点时,更新
count = $(this).val()
// 校验
if (isNaN(count)||count.trim().length==0||parseInt(count)<=0){
//不是数字,全是空格,数字小于1就不合法
count = 1
}
$(this).val(count)
update_goods_amount()
}
)
Ajax实现加入购物车的动态更新
需要先在view中实现相应的逻辑,再在前端页面发送Ajax请求,
地址
就是view中类试图对应的地址,参数
就是view中所需要的sku_id和count和csrfmiddlewaretoken的值,回调函数
就是指根据view视图中返回的值而进行一些其他的操作。
在view中返回的值全部都是JsonResponse的对象,在js中返回的params是字典类型。
-
其中这个csrf的值可以通过js获取,在前端页面加上 {% csrf_token %},然后刷新网页,查看网页源代码:
本该是csrf的地方变成了<input >这个就是我们需要的csrfmiddlewaretoken,使用js获取它的值放在参数中传递,否则会报403错误。
>>> cart:urls.py
path('add', CartAddView.as_view(), name='add') #添加购物车
>>> cart: views.py
class CartAddView(View):
"""购物车记录添加"""
def post(self, request):
user = request.user
if not user.is_authenticated:
return JsonResponse({'res': 0, 'errmsg': '请先登录'})
# 接收数据
sku_id = request.POST.get('sku_id')
count = request.POST.get('count')
# 数据校验
if not all([sku_id, count]):
return JsonResponse({'res': 1, 'errmsg': '数据不完整'})
# 校验添加的商品数量
# noinspection PyBroadException
try:
count = int(count)
except Exception as e:
return JsonResponse({'res': 2, 'errmsg': '商品数目出错'})
# 校验商品是否存在
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
return JsonResponse({'res': 3, 'errmsg': '商品不存在'})
# 业务处理:添加购物车记录
conn = get_redis_connection('default')
cart_key = 'cart_%d' % user.id
# 先尝试获取sku_id的值 -> hget cart_key 属性: cart_key[sku_id]
# 如果sku_id在hash中不存在,hget返回None
cart_count = conn.hget(cart_key, sku_id)
if cart_count:
# redis中存在该商品,进行数量累加
count += int(cart_count)
# 校验商品的库存
if count > sku.stock:
return JsonResponse({'res': 4, 'errmsg': '商品库存不足'})
# 设置hash中sku_id对应的值
# hset ->如sku_id存在,更新数据,如sku_id不存在,追加数据
conn.hset(cart_key, sku_id, count)
# 获取用户购物车中的条目数
cart_count = conn.hlen(cart_key)
# 返回应答
return JsonResponse({'res': 5, 'cart_count': cart_count, 'message': '添加成功'})
>>> detail.html:js代码
// 动画所需要的参数
var $add_x = $('#add_cart').offset().top;
var $add_y = $('#add_cart').offset().left;
var $to_x = $('#show_count').offset().top;
var $to_y = $('#show_count').offset().left;
$('#add_cart').click(function () {
// 获取商品的id和商品的数量
//
sku_id = $(this).attr('sku_id')
count = parseInt($('.num_show').val())
csrf = $('input[name="csrfmiddlewaretoken"]').val()
params = {'sku_id':sku_id, 'count':count ,'csrfmiddlewaretoken':csrf}
//发起ajax post请求:地址 /cart/add;参数 sku_id ,count;
$.post(
'/cart/add',
params,
function (data) { // 就是view中返回的Json数据
if (data.res == 5) {
// 添加成功 动画
$(".add_jump").css({'left': $add_y + 80, 'top': $add_x + 10, 'display': 'block'})
$(".add_jump").stop().animate({
'left': $to_y + 7,
'top': $to_x + 7
},
"fast", function () {
$(".add_jump").fadeOut('fast', function () {
//根据view中获取的商品数目填写到元素中去
$('#show_count').html(data.cart_count);
});
});
}else{ //回调函数的值为0-4,即产生了各种错误。
alert(data.errmsg)
}
}
)
})
关于添加购物车为什么不继承自自定义的LoginRequired类:?
- ajax发起的请求在后端,浏览器中看不到效果,所以不会正常的跳转到登陆页面,所以使用ajax发起请求时,就要自己在view中判断用户是否登陆,然后返回result,ajax在后端通过回调函数获取这个值,alert(‘错误信息’)。
- 而继承自定义的LoginRequired类则不会在浏览器表面不会发生跳转。
- 总结,loginrequired修饰的方法会先判断用户是否登陆,未登录会跳转到登陆界面,而在涉及ajax请求的view中不应该跳转到另一个页面,这就需要我们自己判断用户是否登陆了,未登录,返回错误信息,js收到错误信息,alert显示,让用户自行登录,或者使用js方法跳转到相应的网页。
购物车结算页面
- 未涉及到ajax请求,而且只有当用户登陆时才可以访问此页面,所以:
定义显示类,继承自定义的类LoginRequired,我们需要获取user,获取商品信息,user在request中,商品信息直接从redis中找,cart_key里面就是我们的购物车商品信息,我们取出的是name为cart_key的字典,其中键值对为商品id和数量,定义一个列表将sku对象存储在列表中,为sku动态增加属性amount和count。然后将值传入前端。如果遇到问题,看看redis中的数据存不存在--例如id为1的商品。
class CartInfoView(LoginRequiredMixin,View):
'''购物车结算页面'''
def get(self,request):
# 登陆的用户
user = request.user
# 获取购物车中商品的信息
con = get_redis_connection('default')
cart_key = 'cart_%d'%user.id
# 商品id :数量
cart_dict = con.hgetall(cart_key)
skus = []
total_count = 0 # 总数目
total_price = 0 # 总价格
# 遍历这个字典
for sku_id,count in cart_dict.items():
# 根据id 获取商品的信息
sku = GoodsSKU.objects.get(id=sku_id)
# 计算小计
amount = sku.price*int(count)
# 动态的位sku增加属性
sku.amount = amount
sku.count = int(count)
skus.append(sku)
total_count += int(count)
total_price += amount
context = {
'skus':skus,
'total_count': total_count,
'total_price':total_price,
}
return render(request,'cart.html',context)
购物车结算界面js代码(未涉及对数据库操作的部分)
- 我们需要一个更新购物车信息的函数,每当checkbox的选中状态改变时就调用此函数刷新记录。
function update_page_info() {
var total_count = 0
var total_price = 0
// 获取所有被选中的商品的ul元素
$('.cart_list_td').find(':checked').parents('ul').each(function () {
// 获取商品的数目和小计
count = $(this).find('.num_show').val()
amount = $(this).children('.col07').text()
// 累加计算商品的总件数和总金额
total_count += parseInt(count)
total_price += parseFloat(amount)
})
// 设置选中商品的总件数和总金额
$('.settlements').find('em').text(total_price.toFixed(2))
$('.settlements').find('b').text(total_count)
}
- 实现全选,全不选按钮
$('.settlements').find(':checkbox').change(function () {
// 获取全选checkbox的选中状态
var is_checked = $(this).prop('checked')
// 设置商品的checkbox和全选的checkbox状态保持一致
$('.cart_list_td').find(':checkbox').each(function () {
$(this).prop('checked', is_checked)
})
// 更新页面信息
update_page_info()
})
//全选按钮的check属性变动
$('.cart_list_td').find(':checkbox').change(function () {
// 当商品的checkbox变化,判断全选是否应该被选中
len_checked = $('.cart_list_td').find(':checked').length
len_checkbox = $('.cart_list_td').find(':checkbox').length
//当所有checkbox的数量大于checked的数量,全选按钮设置为fasle未选中。
if (len_checked<len_checkbox){
is_checked = false
} else{
is_checked = true
}
$('.settlements').find(':checkbox').prop('checked',is_checked)
//每次更改checkbox时候,都应执行此函数书信网页
update_page_info()
})
更改购物车的数量——Ajax动态刷新
- 涉及到对数据库的操作,前端页应使用ajax post请求view,然后回调函数接受返回值。
在view中,数据校验部分的代码与前面是相同的
class CartUpdateView(View):
'''响应前端发来的ajax请求,完成更新购物车的操作'''
def post(self,request):
'''购物车的操作就是对数据库cart—的操作,需要前端发来sku_id和count'''
con = get_redis_connection('default')
user = request.user
if not user.is_authenticated:
return JsonResponse({'res': 0, 'errmsg': '请先登录'})
# 接收数据 得到的就是一种商品的值,每次改变数量都会向这里来发送请求,走的是次数,而不是量
sku_id = request.POST.get('sku_id')
count = request.POST.get('count')
# 数据校验
if not all([sku_id, count]):
return JsonResponse({'res':1, 'errmsg': '数据不完整'})
# 校验添加的商品数量
# noinspection PyBroadException
try:
count = int(count)
except Exception as e:
return JsonResponse({'res':2, 'errmsg': '商品数目出错'})
# 校验商品是否存在
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
return JsonResponse({'res':3, 'errmsg': '商品不存在'})
# 更新数据库
cart_key = 'cart_%d'%user.id
if count>sku.stock:
return JsonResponse({'res':4, 'errmsg': '商品数量不足'})
con.hset(cart_key,sku_id, count)
# 返回应答
return JsonResponse({'res':5, 'message': '更新成功'})
- js部分
$.ajaxSettings.async = false很重要,在.post中默认是以异步的方式进行的,所以全局变量的值在post中无法修改,这就会使得if判断毫无意义,很重要。
//计算商品的小计
function update_goods_amount(sku_ul){
//获取商品的价格和数量
count = sku_ul.find('.num_show').val()
price = sku_ul.children('.col05').text()
amount = parseInt(count)*parseFloat(price)
//设置商品的小计
sku_ul.children('.col07').text(amount.toFixed(2)+"元")
}
//更新购物车的记录
$('.add').click(function () {
//获取商品的id和数量,post给view
count = $(this).next().val()
sku_id = $(this).next().attr('sku_id')
count = parseInt(count) + 1
params = {'sku_id':sku_id,'count':count}
error_update = false
$.ajaxSettings.async = false;
$.post(
'/cart/update',params,function (data) {
if (data.res == 5) {
//更新成功
error_update =false
}else{
//失败
error_update =true
alert(data.errmsg)
}
}
)
if(error_update==false){
//重新设置商品的数目
$(this).next().val(count)
//小计
update_goods_amount($(this).parents('ul'))
update_page_info()
}
else{
count = count-1
$(this).next().val(count)
update_goods_amount($(this).parents('ul'))
update_page_info()
}
alert(error_update)
})
- 现在除了全部商品的数目为实现,其它均以实现。
我们每次add,都会post请求view,可以让view查一下,当前页面中的总件数,然后返回给前端页面,在用js实现刷新。
不能在页面中直接获取total_count的值,因为view中返回的值的类型是Json,而且是由ajax请求的,所以Json会返回到js中,我们在回调函数中获取值,然后为总件数更新。
CartUpdateView中
# 再添加代码
total_count = 0
vals =con.hvals(cart_key) # 将cart_key中的value作为列表返回,相当于dic.values()
for val in vals:
total_count += int(val) # 所有的value值加起来就是商品的总件数。
# 返回应答
return JsonResponse({'res':5, 'message': '更新成功', 'total_count':total_count})
在js中data获取total_count的值,然后为上面的元素赋值
- 减少的商品js操作和上面类似,直接copy一份,然后把next()改为prev()就差不多,count不再加一,而是减一。
- 自动输入数量也是类似,只不过要判断输入的值是否合理,大于库存的会由view判断,所以值要满足>0,为数字类型。next()就是(this)
订单
显示订单
将商品的checkbox的value属性设置为{{ sku.id }},然后给他一个属性name = "sku_ids"
,页面检查,会有多个name属性为sku_ids的input元素,每个元素页都有一个value值,值就是商品的id。
将这些input标签放在一个form中,当点击提交,在检查中看network内容,被选中的checkbox会被提交。我们借助这个checkbox来提交被选中的商品的id。
<form method="post" action="{% url 'order:place' %}">
{% csrf_token%}
<li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}"></li>
</form>
创建订单只需要获取被选中的商品的id就可以,因为数量可以通过redis数据库中查。
在view中通过传过来的sku_ids,遍历得到商品的sku_id ,通过sku_id查询商品的信息和订单中商品的数量。计算出小计,并将小计和数量作为属性添加商品中sku
,因为sku有很多,所以创建一个列表,append到列表中,传到前端,遍历输出。
提交订单
-
提交订单的页面需要的信息
可以根据模型类确定,其中的有很多信息是不必要的,我们只要必须的,order_id需要自己设置,user可以通过request获取,addr必须;pay_method必须;transit_price(运费)必须;order_status(订单状态,需要设置),trade_no支付编号;
还需要知道商品的id
商品的id在sku_ids中,它是一个列表,用循环查出每个的id,然后查出商品sku的信息,并重新放在一个列表中。
重新设置sku_ids的形式为字符串,在发给前端。
` sku_ids = ','.join(sku_ids)
`
然后在前端的提交中,设置sku_ids属性为{{sku_ids}},便于js的获取。
ajax请求需要在将
var pay_method = $('input[name="pay_style"]:checked').val()
var sku_ids = $(this).attr('sku_ids')
var csrf = $('input[name="csrfmiddlewaretoken"]').val()
数据传入/order/commit的view中。
- 定义OrderCommitView获取值,创建订单记录,插入到表中。
- 这里涉及到两张表,一张是订单信息表,另一张是订单商品表。
在向订单商品表中插入数据之前,我们需要判断count的值,因为在创建订单成功的时候才会减少库存的值,此时有人比你快一步就会错误,所以加一步判断。- 还有就是,添加完订单信息表后,在填加订单商品表,如果订单商品表插入失败,那么订单信息表中就多了一条数据,这条数据我们希望能够和订单商品表同生共死,所以我们要开启mysql的事务。
MySql事务!
Begin 开启一个事务
savepoint sp1 存档点sp1
rollback sp1 回滚到存档点sp1
rollback 回滚 事务结束的标志
commit 提交 事务结束的标志
在代码中的操作,官方文档的Model的高级里面。
from django.db import transaction
class OrderCommitView(View):
'''创建订单'''
@transaction.atomic
def post(self, request):
save_id = transaction.savepoint()
设置一个存档点,并接他的值
transaction.savepoint_rollback(save_id )
发生异常就回滚
transaction.savepoint_commit(save_id)
没有异常就提交
解决订单并发的问题
- 悲观锁
当用户进行查询时,先去请求锁,获取锁之后才能进行查询,否则就阻塞,当
事务提交
时,释放锁资源。
Goods.objects.select_for_update().get(id=id)
- 悲观锁的优点:每次查询前都会抢锁,其他人使用时就会阻塞,知道前者完成事务释放锁资源,所以比较适合处理读写操作比较频繁的情况。
- 悲观锁的缺点:不能处理高并发,当用户量很多时,同一时间只能处理一位用户,而其他用户都处于阻塞状态。
- 乐观锁
当用户更新时,认为没人抢资源,假设要更新的字段为case,记录要更新的字段的值为origin_case,在更新sql时做一个判断,若此时的case字段的值等于origin_case的值,代表没有人抢,就更新,若不相等就代表有人捷足先登了。
orgin_stock = sku.stock
new_stock = orgin_stock - int(count)
new_sales = sku.sales + int(count)
# 返回受影响的行数res, 0为失败
res = GoodsSKU.objects.filter(id=sku_id, stock=orgin_stock).update(stock=new_stock,
sales=new_sales) # 乐观锁
if res == 0: # 返回0表示更新失败
transaction.savepoint_rollback(save_id)
return JsonResponse({'res': 8, 'errmsg': '下单失败2'})
更新受影响的行数就是0,更新失败。
- 若是一次查询不同就直接回滚,并返回Json信息就有点太着急了,你可以再次尝试一次,说不定这一次就没有更改的了,所以要结合循环,在外层加入for循环尝试3次,若3次都失败,说明现在人很多,都在更新数据,返回Json,下单失败
服务器很忙
。
- 乐观所的缺点:就像上面说的如果有很多人的话,就会使操作很吃力,所以乐观锁不适合处理写操作比较频繁的情况。
- 若是要操作的数据发生ABA的变化,就会认为他没有发生变化,所以,在更新时要加上version,每次更新version都会+1,这样就能确定是否更新
- 乐观锁的优点:能够处理高并发,虽然处理很慢,但是来者不拒。