Django天天生鲜项目学习笔记

首先分析数据库模型!

用户表: 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)

激活帐户

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(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

然后再在cmd中输入 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 'namespacename'%}

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.confbase_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

卸载fdfs:http://www.leftso.com/blog/244.html

FastDFS+Nginx(补)

https://yq.aliyun.com/articles/512649/

  • 首先下载最新的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())

  1. use_template=True代表根据自定义的模板来建立索引,所以我们要创建模板文件
  1. 创建模板文件格式为固定,在templates下创建文件夹名为search,然后在search文件夹中创建indexes文件夹,正好对应了之前的search_indexes.py文件,在下面创建模型类的名字(goods),然后就到了模板文件,模型类小写_text.txt
# 指定根据表中的那些字段建立商品
{{ object.name }}   # 根据商品的名称及建立索引
{{ object.desc }}   # 点的是属性
{{ object.goods.detail }} # 根据商品的详情商城索引

object指的是当前模型类,丶 出来的是属性,name属性,desc属性,goods是外键,外键所在的模型类中有属性detail。

  1. 执行命令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,这样就能确定是否更新
  • 乐观锁的优点:能够处理高并发,虽然处理很慢,但是来者不拒。

与支付宝的对接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容