购物车数据存储--pickle模块和base64模块的使用

redis所有参考命令:http://doc.redisfans.com/

redis所有方法的使用:http://redis-py.readthedocs.io/en/latest/#indices-and-tables

pickle模块的使用

pickle模块是python的标准模块,提供了对于python数据的序列化操作,可以将数据转换为bytes类型,其序列化速度比json模块要高。

pickle.dumps() 将python数据序列化为bytes类型

pickle.loads() 将bytes类型数据反序列化为python的数据类型

 

>>> import pickle

>>> d = {'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}
>>> s = pickle.dumps(d)
>>> s
b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.'
>>> pickle.loads(s)
{'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}

base64模块的使用

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2^6=64,所以每6个比特为一个单元,对应某个可打印字符。3个字节有24个比特,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。在Base64中的可打印字符包括字母A-Za-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。

Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括MIME的电子邮件及XML的一些复杂数据。

python标准库中提供了base64模块,用来进行转换

  • base64.b64encode() 将bytes类型数据进行base64编码,返回编码后的bytes类型
  • base64.b64deocde() 将base64编码的bytes类型进行解码,返回解码后的bytes类型
    >>> import base64
    >>> s
    b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.'
    >>> b = base64.b64encode(s)
    >>> b
    b'gAN9cQAoWAEAAAAxcQF9cQIoWAUAAABjb3VudHEDSwpYCAAAAHNlbGVjdGVkcQSIdVgBAAAAMnEFfXEGKGgDSxRoBIl1dS4='
    >>> base64.b64decode(b)
    b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.'

用到购物车未登录用户数据存储上 逻辑如下图所示:

使用终端命令 创建购物车的子应用carts:python manage.py startapp carts 

添加到购物车

 因为前端可能携带cookie,为了保证跨域请求中,允许后端使用cookie,确保在配置文件有如下设置:

CORS_ALLOW_CREDENTIALS = True

主应用urls.py文件中添加路由:

urlpatterns = [
    。。。。。。
    # 购物车
    url(r'^', include('carts.urls')),
]

在子应用carts/urls.py文件中添加路由:

from django.conf.urls import url
from . import views


urlpatterns = [
    # 购物车
    url(r'^cart/$', views.CartView.as_view()),
]

编写视图

注意:因为前端请求时携带了Authorization请求头(主要是JWT),而如果用户未登录,此请求头的JWT无意义(没有值),为了防止REST framework框架在验证此无意义的JWT时抛出401异常,在视图中需要做两个处理

  • 重写perform_authentication()方法,此方法是REST framework检查用户身份的方法
  • 在获取request.user属性时捕获异常,REST framework在返回user时,会检查Authorization请求头,无效的Authorization请求头会导致抛出异常

在carts/views.py中创建视图:

from rest_framework.views import APIView
from django_redis import get_redis_connection
from rest_framework.response import Response
from rest_framework import status
import base64
import pickle

from . import serializers


class CartView(APIView):
    """购物车后端:增删改查"""

    def perform_authentication(self, request):
        """
        重写父类的用户验证方法,不在进入视图前就检查JWT
        保证用户未登录也可以进入下面的请求方法,不让他执行request.user方法
        """
        pass

    def post(self, request):
        """新增"""
        # 新建序列化器对象
        serializer = serializers.CartSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # 读取校验之后的数据
        sku_id = serializer.validated_data.get('sku_id')
        count = serializer.validated_data.get('count')
        selected = serializer.validated_data.get('selected')

        # 判断用户是否是登录的
        # request.user : 当用户是登录的用户,就会获取到登录用户的信息,反之,会抛出异常或者返回匿名用户
        try:
            user = request.user
        except Exception:
            user = None

        if user is not None and user.is_authenticated:
            # 用户已登录,操作redis
            # 获取连接到数据库的对象
            redis_conn = get_redis_connection('carts')
            # 管道
            pl = redis_conn.pipeline()

            # 操作hash类型的数据,存储哪个用户的哪件商品数量
            # hincrby() : 实现增量计算,根据key是否存在,实现是累加还是赋新值
            pl.hincrby('cart_%s' % user.id, sku_id, count)
            # 存储该商品是否勾选
            if selected:
                pl.sadd('selected_%s' % user.id, sku_id)
            # 执行
            pl.execute()

            # 响应结果:查询和修改 200, 新增是 201, 删除是204
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            # 用户未登录,操作cookie
            # 从cookie中获取已存在的购物车数据
            cart_str = request.COOKIES.get('cart') # cart_str = (str)udHEDSwpYCAAA'
            if cart_str:
                # 将cart_str字符串转成二进制类型的字符串 b'udHEDSwpYCAAA'
                # cart_str.encode()
                # 再将b'udHEDSwpYCAAA'转成b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}
                # base64.b64decode(cart_str.encode())
                # 将b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}转成字典
                cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            else:
                # 表示用户从来没有在cookie中存储过购物车数据
                cart_dict = {}

            # 将前端传入的sku_id,count,selected存储到cookie
                # {
                #     sku_id10: {
                #         "count": 10, // 数量
                #         "selected": True // 是否勾选
                #     },
                #     sku_id20: {
                #         "count": 20,
                #         "selected": False
                #     },
                #     ...
                # }
            if sku_id in cart_dict:
                # 取出原始的值,进行累加。
                origin_count = cart_dict[sku_id]['count']
                count += origin_count

            # 将累加或者最新的count保存到字典
            cart_dict[sku_id] = {
                'count':count,
                'selected':selected
            }

            # 将购物车字典转正字符串
            # (dict){'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}
            # (str)gAN9cQAoWAEAAAAxcQF9cQIoWAUAAABjb3VudHEDSwpYCAAAAHNlbGVjdGVkcQSIdVgBAAAAMnEFfXEGKGgDSxRoBIl1dS4=
            cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()

            # 创建response对象
            response = Response(serializer.data, status=status.HTTP_201_CREATED)

            # 将新的购物车数据写入到cookie
            response.set_cookie('cart', cookie_cart_str)

            # 响应结果
            return response

settings.py文件中配置登录用户购物车的redis数据库:

CACHES = {
    。。。。。。
    "carts": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/4",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    },
}

在carts/serialziers.py中创建序列化器

from rest_framework import serializers

from goods.models import SKU


class CartSerializer(serializers.Serializer):
    """购物车序列化器"""
    # 指定字段
    sku_id = serializers.IntegerField(label='商品 SKU 编号', min_value=1)
    count = serializers.IntegerField(label='商品数量', min_value=1)
    selected = serializers.BooleanField(label='商品是否勾选', default=True)

    def validate(self, attrs):
        # 获取字段
        sku_id = attrs['sku_id']
        count = attrs['count']

        try:
            sku = SKU.objects.get(id=sku_id)
        except SKU.DoesNotExist:
            raise serializers.ValidationError('商品 sku id 不存在')

        # 判断库存
        if count > sku.stock:
            raise serializers.ValidationError('库存不足')

        return attrs

 前端实现

在detail.js中编写添加购物车的调用

注意:前端在此跨域请求中要携带cookie,需要在axios中添加配置withCredentials: true

// 添加购物车
        add_cart: function(){
            axios.post(this.host+'/cart/', {
                    sku_id: parseInt(this.sku_id),
                    count: this.sku_count
                }, {
                    headers: {
                        'Authorization': 'JWT ' + this.token
                    },
                    responseType: 'json',
                    withCredentials: true
                })
                .then(response => {
                    alert('添加购物车成功');
                    this.cart_total_count += response.data.count;
                })
                .catch(error => {
                    if ('non_field_errors' in error.response.data) {
                        alert(error.response.data.non_field_errors[0]);
                    } else {
                        alert('添加购物车失败');
                    }
                    console.log(error.response.data);
                })
        },

查询购物车数据

 在carts/serializers.py中创建序列化器

class CartSKUSerializer(serializers.ModelSerializer):
    """
    购物车商品数据序列化器
    """
    # 如果不指定,默认readonly(输出)
    count = serializers.IntegerField(label='数量')
    selected = serializers.BooleanField(label='是否勾选')

    class Meta:
        model = SKU
        fields = ('id', 'count', 'name', 'default_image_url', 'price', 'selected')

 在carts/views.py 中修改视图,增加get方法

def get(self, request):
        """查询
        """
        # 判断用户是否是登录的
        try:
            user = request.user
        except Exception:
            user = None

        if user is not None and user.is_authenticated:
            # 用户已登录,操作redis
            # 获取连接到数据库的对象
            redis_conn = get_redis_connection('carts')
            # 查询所有的购物车数据
            redis_cart = redis_conn.hgetall('cart_%s' % user.id)
            # {
            #     b'sku_id':b'count',
            #     b'sku_id': b'count'
            # }
            # 查询哪些商品被勾选
            # cart_selected = [sku_id,sku_id]
            cart_selected = redis_conn.smembers('selected_%s' % user.id)

            # {
            #     sku_id10: {
            #         "count": 10, // 数量
            #         "selected": True // 是否勾选
            #     },
            #     sku_id20: {
            #         "count": 20,
            #         "selected": False
            #     },
            #     ...
            # }

            # 将redis_cart和cart_selected内部的数据,整合到cart_dict,实现格式的统一,方便后续查询sku信息
            cart_dict = {}
            for sku_id, count in redis_cart.items():
                cart_dict[int(sku_id)] = {
                    'count':int(count),
                    'selected':sku_id in cart_selected # 当sku_id在cart_selected中,说明该商品勾选
                }
        else:
            # 用户未登录,操作cookie
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            else:
                # 表示用户从来没有在cookie中存储过购物车数据
                cart_dict = {}

        # 根据cart_dict,查询购物车中的商品数据
        sku_ids = cart_dict.keys()
        # 查询出id在sku_ids中的所有的商品,构造我列表
        skus = SKU.objects.filter(id__in=sku_ids)

        # 为了能够在序列化skus的同时又序列化count和selected,就需要将他们追加到sku
        for sku in skus:
            sku.count = cart_dict[sku.id]['count']
            sku.selected = cart_dict[sku.id]['selected']

        # 创建序列化器对象,对skus进行序列化
        serializer = serializers.CartSKUSerializer(skus, many=True)
        # 响应数据, 状态码200
        return Response(serializer.data)

前端:修改cart.html文件,增加Vue变量

修改购物车数据

拓展知识点:幂等和非幂等

在carts/views.py中修改视图,添加put方法:

def put(self, request):
        """修改"""
        # 新建序列化器对象
        serializer = serializers.CartSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # 读取校验之后的数据
        sku_id = serializer.validated_data.get('sku_id')
        count = serializer.validated_data.get('count')
        selected = serializer.validated_data.get('selected')

        # 判断用户是否是登录的
        try:
            user = request.user
        except Exception:
            user = None

        if user is not None and user.is_authenticated:
            # 用户已登录,操作redis
            # 获取连接到数据库的对象
            redis_conn = get_redis_connection('carts')
            # 管道
            pl = redis_conn.pipeline()
            # 修改商品的数量 : 后端使用幂等的接口设计方案,要求前端将用户最终在界面的上的结果发送给我们,直接覆盖写入。不做增量计算
            pl.hset('cart_%s' % user.id, sku_id, count)
            # 修改商品是否勾选
            if selected:
                pl.sadd('selected_%s' % user.id, sku_id)
            else:
                # 如果前端传入的是false,那么就把sku_id从列表中移除,表示该商品未勾选
                pl.srem('selected_%s' % user.id, sku_id)

            # 记住:需要调用excute()
            pl.execute()

            # 响应,200
            return Response(serializer.data)
        else:
            # 用户未登录,操作cookie
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            else:
                # 表示用户从来没有在cookie中存储过购物车数据
                cart_dict = {}

            # 使用幂等的方式,覆盖写入新的数据
            cart_dict[sku_id] = {
                'count':count,
                'selected':selected
            }

            # 将新的购物车数据写入到cookie
            cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()

            # 创建response对象
            response = Response(serializer.data)

            # 将新的购物车数据写入到cookie
            response.set_cookie('cart', cookie_cart_str)

            # 响应结果
            return response

修改前端代码 在cart.js中添加:

on_input: function(index){
            var val = parseInt(this.cart[index].count);
            if (isNaN(val) || val <= 0) {
                this.cart[index].count = this.origin_input;
            } else {
                // 更新购物车数据
                axios.put(this.host+'/cart/', {
                        sku_id: this.cart[index].id,
                        count: val,
                        selected: this.cart[index].selected
                    }, {
                        headers:{
                            'Authorization': 'JWT ' + this.token
                        },
                        responseType: 'json',
                        withCredentials: true
                    })
                    .then(response => {
                        this.cart[index].count = response.data.count;
                    })
                    .catch(error => {
                        if ('non_field_errors' in error.response.data) {
                            alert(error.response.data.non_field_errors[0]);
                        } else {
                            alert('修改购物车失败');
                        }
                        console.log(error.response.data);
                        this.cart[index].count = this.origin_input;
                    })
            }
        },
        // 更新购物车数据
        update_count: function(index, count){
            axios.put(this.host+'/cart/', {
                    sku_id: this.cart[index].id,
                    count,
                    selected: this.cart[index].selected
                }, {
                    headers:{
                        'Authorization': 'JWT ' + this.token
                    },
                    responseType: 'json',
                    withCredentials: true
                })
                .then(response => {
                    this.cart[index].count = response.data.count;
                })
                .catch(error => {
                    if ('non_field_errors' in error.response.data) {
                        alert(error.response.data.non_field_errors[0]);
                    } else {
                        alert('修改购物车失败');
                    }
                    console.log(error.response.data);
                })
        },
        // 更新购物车数据
        update_selected: function(index) {
            axios.put(this.host+'/cart/', {
                    sku_id: this.cart[index].id,
                    count: this.cart[index].count,
                    selected: this.cart[index].selected
                }, {
                    headers: {
                        'Authorization': 'JWT ' + this.token
                    },
                    responseType: 'json',
                    withCredentials: true
                })
                .then(response => {
                    this.cart[index].selected = response.data.selected;
                })
                .catch(error => {
                    if ('non_field_errors' in error.response.data) {
                        alert(error.response.data.non_field_errors[0]);
                    } else {
                        alert('修改购物车失败');
                    }
                    console.log(error.response.data);
                })
        }

删除购物车数据

在carts/serializers.py 中新建序列化器

class CartDeleteSerializer(serializers.Serializer):
    """
    删除购物车数据序列化器
    """
    sku_id = serializers.IntegerField(label='商品id', min_value=1)

    def validate_sku_id(self, value):
        try:
            sku = SKU.objects.get(id=value)
        except SKU.DoesNotExist:
            raise serializers.ValidationError('商品不存在')

        return value

在carts/views.py 中修改视图,增加delete方法

def delete(self, request):
        """删除"""
        # 创建序列化器对象
        serializer = serializers.CartDeleteSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # 读取校验后的sku_id
        sku_id = serializer.validated_data.get('sku_id')

        # 判断用户是否是登录的
        try:
            user = request.user
        except Exception:
            user = None

        if user is not None and user.is_authenticated:
            # 用户已登录,操作redis
            # 获取连接到数据库的对象
            redis_conn = get_redis_connection('carts')
            # 管道
            pl = redis_conn.pipeline()
            # 删除某一条记录
            pl.hdel('cart_%s' % user.id, sku_id)
            # 移除勾选的标记
            pl.srem('selected_%s' % user.id, sku_id)
            # 执行
            pl.execute()

            # 响应:只响应状态码204,因为数据已经被删除,没有序列化的数据了
            return Response(status=status.HTTP_204_NO_CONTENT)
        else:
            # 用户未登录,cookie操作购物车
            cart_str = request.COOKIES.get('cart')

            if cart_str:  # 用户操作过cookie购物车
                # 将cart_str转成bytes,再将bytes转成base64的bytes,最后将bytes转字典
                cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            else:  # 用户从没有操作过cookie购物车
                cart_dict = {}

            # 创建响应对象
            response = Response(status=status.HTTP_204_NO_CONTENT)

            if sku_id in cart_dict:
                del cart_dict[sku_id]

                # 将字典转成bytes,再将bytes转成base64的bytes,最后将bytes转字符串
                cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()

                # 响应结果并将购物车数据写入到cookie
                response.set_cookie('cart', cookie_cart_str)

            return response

前端代码 在cart.js中增加:

 // 删除购物车数据
        on_delete: function(index){
            axios.delete(this.host+'/cart/', {
                    data: {
                        sku_id: this.cart[index].id
                    },
                    headers:{
                        'Authorization': 'JWT ' + this.token
                    },
                    responseType: 'json',
                    withCredentials: true
                })
                .then(response => {
                    this.cart.splice(index, 1);
                })
                .catch(error => {
                    console.log(error.response.data);
                })
        },

猜你喜欢

转载自www.cnblogs.com/cl-python/p/9302340.html