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下面的模块名字。

    创建模型类

    因为所有的模型类都有 创建时间,修改时间 ,删除标识,所以创建了一个继承自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) 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

  • 安装完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 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

  • 相关文档 https://yiyibooks.cn/xx/django_182/topics/auth/default.html

    用户未登录时,应该无法查看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>
                    {% else %}
                        <div class="login_btn fl">
                            <a href="/user/login">登录</a>
                            <span>|</span>
                            <a href="/user/register">注册</a>
                            <span>|</span>
                    {% 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>
    

    用户地址页面

  • 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):
            # 获取用户的默认收货地址
                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
    

    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 也可启动成功
  • 为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

    下面的内容发生了更改
    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 install

  • 未出现什么异常,添加第三方模块完成。
  • 让nginx配合fastdfs

    将模块中src的一些文件拷贝到/etc/fdfs下面
    cp /export/download/fastdfs-nginx-module-1.20/src/mod_fastdfs.conf /etc/fdfs/mod_fastdfs.conf

    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_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'): 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  %}
    

    因为用户会频繁的访问购物车,添加删除,所以放在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()发布任务。
  • 首页数据的缓存

  • 当用户访问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):
            '''显示列表页'''
            # 先获取种类信息
                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页的内容
                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 %}
    

    全文检索引擎: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())
    {{ 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 %},然后刷新网页,查看网页源代码:

    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 count = int(count) except Exception as e: return JsonResponse({'res': 2, 'errmsg': '商品数目出错'}) # 校验商品是否存在 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 count = int(count) except Exception as e: return JsonResponse({'res':2, 'errmsg': '商品数目出错'}) # 校验商品是否存在 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': '更新成功'})

    $.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,下单失败服务器很忙