实战项目-美多商城(六)购物车

购物车应该存储那些数据

  • sku_id(商品ID)
  • count(购买数量)
  • selected(是否被勾选)
- 不管用户是否登录,当点击'购物车'的时候,均向后台接口发请求
	- 判断用户是否登录
		- 登录用户,把购物车数据存到redis
		- 未登录用户,把购物车数据存到 cookie
- 登录用户: 允许使用服务器资源

    - 存储到 redis,每条数据分两种格式存储(为了演示,所以这么搞)

        - Set:{sku_id_1,sku_id_2......} # 有放入集合(自带去重功能),就表示已勾选
        - Hash:dict {sku_id_1:count,sku_id_2:count......}
        
        - 如果是效率搞,可以这么设计,为每个用户创建一个hash: cart_user_id
        	- 里面存储:
                  {sku_id_1:{count:xxx,selected:true},sku_id_2:{count:xxx,selected:false}}

- 未登录用户: 不允许使用服务器资源

    - 存储到 浏览器cookie(存到 localStorage也可以...,为了演示cookie的使用,所以这么搞)

    - cookie中只能存储'键值对',key-value都是必须是str类型

        - response.set_cookie('cart','value','过期时间')

        - 类似的数据格式:

            {
                sku_id_1:{'count':1,'selected':true},
                sku_id_2:{'count':1,'selected':true}
            }

pickle模块(相比json模块,多一个'字符串编码'/'字符串解码'的功能)和base64模块(简单编码/解码)---简单使用

  • pickle模块(将数据在strbytes之间转换): 提供了对于 python 数据的序列化操作,可以将数据转换为 bytes 类型
    • pickle.dumps(): 将 python 数据序列化为 bytes 类型
    • pickle.loads(): 将 bytes 类型数据反序列化为 python 的数据类型( 字典, 对象等 )
import pickle

data = {'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}
bytes_data = pickle.dumps(data)
print(bytes_data) # b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00......
dict_data = pickle.loads(bytes_data)
print(dict_data) # {'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}

如果使用str.encode()或者bytes.decode()太过于直观...

  • base64模块:把bytes类型数据,作进一步'编码'/'解码'处理

  • 小结: 通过picklebase64实现数据的二次编码/解码

import base64

bytes_data = b'king'
encode_bytes_data = base64.b64encode(bytes_data)
print(encode_bytes_data) # b'a2luZw=='
decode_bytes_data = base64.b64decode(encode_bytes_data)
print(decode_bytes_data) # b'king'

未登录用户的认证问题

- 不管用户是否登录,都必须实现购物车功能

- 而当用户未登录时候,前端依然在 headers 加入 'Authorization'
  所以请求还没到视图views,就会被认证类 JSONWebTokenAuthentication 拦截
  无法实现后续购物车的逻辑

    - 解决办法一: 前端不在headers加入 'Authorization'

    - 解决办法二: 后端作'延迟认证'处理(使用这个方法处理)
  • 查看后端认证的APIView源码
- 先走dispatch

class APIView(View):
    ......
    def dispatch(self, request, *args, **kwargs):
        ......
        try:
            # 走这个方法
            self.initial(request, *args, **kwargs)

    def initial(self, request, *args, **kwargs):
        ......
        # 走认证,权限和节流
        # 进 perform_authentication()
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

    def perform_authentication(self, request):

        ### 看注释
        """
        Perform authentication on the incoming request.

        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user

        '''
        当简单重写这个方法的时候, 校验会变'懒'(延迟校验),直至出现 request.user / request.auth
        校验才会继续进行
        '''



### carts.views
......
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated


class CartView(APIView):

    authentication_classes = [JSONWebTokenAuthentication,]
    # 这个方法包含 request.user,会激活校验,不能加;这个接口无需权限都能进,故而也不需要这个权限类
    # permission_classes = [IsAuthenticated,]

    # 重写这个方法,延迟校验
    def perform_authentication(self, request):
        pass

    def post(self,request):
        print('请求进来了吗')
        try:
            user = request.user
        except Exception as e:
            user = None
        print('user的值为:{}'.format(user))
        return Response({'msg':'POST响应成功'})


    def get(self,request):
        ......

    def put(self,request):
        ......

    def delete(self,request):
        .....


功能 --- 新增购物车数据

  • 当用户在商品详情页点击加入购物车的时候,我们为用户新增购物车数据

  • 请求方式

    请求方法 请求地址
    POST http://127.0.0.1:8000/carts
  • 请求体参数

    参数名 类型 是否必传 说明
    sku_id int 商品ID
    count int 购买数量
  • 响应成功结果:JSON(已登录用户)

    - 购物车数据保存在 redis
    
    {
    	"sku_id": 1,
    	"count": 2,
    	"selected": true
    }
    
  • 响应成功结果:JSON(未登录用户)

    - 购物车数据保存在 cookie
    
    {
    	"sku_id": 1,
    	"count": 2,
    	"selected": true
    }
    
- 校验用户传过来的数据
	- 不通过就异常
	- 通过就获取 sku_id,count,selected

- 尝试获取user的值
	- 有就reques.user
	- 没有就 None
	
- 是登录用户: user and user.is_authenticated
	- 创建redis连接并往dict丢数据(管道): pl.hincrby('cart_{}'.format(user.id),sku_id,count)
	- 若 selected: pl.sadd('selected_{}'.format(user.id),sku_id)
	- 返回 ser.data(201)
	
- 是未登录用户
	- 获取cookie: request.COOKIES.get('cart')
	- 有值
		- 解码成bytes,再解码成 b64: base64.b64decode(cart_str_bytes),再pickle成python对象pickle.loads(b64_bytes)
	- 没值
		- cart_dict = {}
		
	- 增量判断
		 if 'sku_id' in cart_dict:
                orgin_count = cart_dict.get('count')
                count += orgin_count
                
    - 更新 cart_dict数据
		 cart_dict['sku_id'] = {
                'count':count,
                'selected':selected
            }

            cart_bytes = pickle.dumps(cart_dict)
            cart_bytes64 = base64.encode(cart_bytes)
            cart_str = cart_bytes64.decode()

            response = Response(serializer.data,status=status.HTTP_201_CREATED)
            response.set_cookie('cart',cart_str)
            return response
	
	

### carts.serializers
from rest_framework import serializers

from apps.goods.models import SKU

class CartSerializer(serializers.Serializer):

    sku_id = serializers.IntegerField(min_value=1,label='SKU商品ID')
    count = serializers.IntegerField(label='购买数量')
    selected = serializers.BooleanField(default=True,label='商品是否勾选')

    def validate_sku_id(self,value):

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

        return value




### carts.views

import pickle
import base64

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from django_redis import get_redis_connection

from .serializers import CartSerializer


class CartView(APIView):

    authentication_classes = [JSONWebTokenAuthentication,]
    # permission_classes = [IsAuthenticated,]

    # 重写这个方法,延迟校验
    def perform_authentication(self, request):
        pass

    def post(self,request):

        serializer = 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 as e:
            user = None

        if user and user.is_authenticated:
            conn = get_redis_connection('cart')
            pl = conn.pipeline()
            pl.hincrby('cart_{}'.format(user.id), sku_id, count)
            if selected:
                pl.sadd('selected_{}'.format(user.id), sku_id)
            pl.execute()
            return Response(serializer.data,status=status.HTTP_201_CREATED)

        cookies = request.COOKIES.get('cart')
        if cookies:
            bytes_carts = cookies.encode()
            b64_bytes_carts = base64.b64decode(bytes_carts)
            cart_dict = pickle.loads(b64_bytes_carts)
        else:
            cart_dict = {}

        if 'sku_id' in cart_dict:
            origin_count = cart_dict.get('count')
            count += origin_count

        cart_dict[sku_id] = {
            'count':count,
            'selected':selected
        }

        str_carts = base64.b64encode(pickle.dumps(cart_dict)).decode()
        response = Response(serializer.data,status=status.HTTP_201_CREATED)
        response.set_cookie('cart',str_carts)
        return response


    def get(self,request):
       ......

使用登录用户未登录用户分别测试一下,查看rediscookie数据正常

功能 --- 展示购物车数据(登录用户||未登录用户都要处理)

  • 当用户点击我的购物车时,把购物车的数据返回给前端

  • 请求方式

    请求方法 请求地址
    GET http://127.0.0.1:8000/carts
  • 请求头headers

    参数名 类型 说明
    Authorization str 'JWT ' + this.token JWT空格 + token
  • 响应成功结果:JSON

    
    [
        {
            "id": 2,
            "name": "Apple MacBook Pro 13.3英寸笔记本 深灰色",
            "price": "12398.01",
            "default_image_url": "http://192.168.11.39:8888/group1/M00/00/02/CtM3BVrPCAOAIKRBAAGvaeRBMfc0463515",
            "count": 1,
            "selected": true
        },
        {
            "id": 16,
            "name": "华为 HUAWEI P10 Plus 6GB+128GB 曜石黑 移动联通电信4G手机 双卡双待",
            "price": "4099.99",
            "default_image_url": "http://192.168.11.39:8888/group1/M00/00/02/CtM3BVrRdPeAXNDMAAYJrpessGQ9777651",
            "count": 1,
            "selected": true
        }
    ]
    
  • 思路分析

    - 取出存储的sku_id,序列化出对应的模型字段,还要增加'count','selected'字段
    	- 所以,序列化器是必须的
    
    - 登录/未登录 用户构造成一样的数据,方便未登录用户 登录以后,对数据进行统一的处理
    
    	- 只需构造 redis 数据即可,cookie的数据就是期望的构造数据
    
            - 登录用户的购物车数据是这样
    
                - 存储到 redis,每条数据分两种格式存储
    
                    - Set:{sku_id_1,sku_id_2......} # 有放入集合(自带去重功能),就表示已勾选
    
                    - Hash:dict {sku_id_1:count,sku_id_2:count......}
    
            - 未登录用户的购物车数据是这样: 
    
                        {
                            sku_id_1:{'count':1,'selected':true},
                            sku_id_2:{'count':1,'selected':true}
                        }
    
            - 把登录/未登录用户的数据,构造成一样的(方便未登录用户,登录以后合并购物车的逻辑):
    
                {
                            'sku_id_1':{'count':1,'selected':True},
                            'sku_id_2':{'count':3,'selected':True},
                }
    
  • 后端逻辑

    ### carts.serializers
    ......
    # 序列化出对应的模型字段,还要增加'count','selected'字段
    class SKUCartSerializer(serializers.ModelSerializer):
    
        count = serializers.IntegerField(label='购买数量')
        selected = serializers.BooleanField(label='是否勾选')
    
        class Meta:
            model = SKU
            fields = ['id','name','price','default_image_url','count','selected']
            
    ### carts.views
    ......
    class CartView(APIView):
    
        authentication_classes = [JSONWebTokenAuthentication,]
       
        def perform_authentication(self, request):
            pass
    
        def post(self,request):
            ......
        def get(self,request):
    
            try:
                user = request.user
            except:
                user = None
    		
    		# 不管用户是否登录,把cart_dict构造成一样的数据格式,返回给前端
            if user and user.is_authenticated:
                conn = get_redis_connection('cart')
                cart_redis_dict = conn.hgetall('cart_{}'.format(user.id)) # {b'1': b'4', b'13': b'1', b'14': b'1'}
                selected = conn.smembers('selected_{}'.format(user.id)) # {b'1', b'14', b'13'}
                cart_dict = {}
    
                for byte_sku_id,byte_count in cart_redis_dict.items():
                    cart_dict[int(byte_sku_id)] = {
                        'count':int(byte_count),
                        'selected':byte_sku_id in selected
                    }
            else:
                cart_str = request.COOKIES.get('cart')
                if cart_str:
                    cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
                else:
                    return Response({'message':'没有cookie'},status=status.HTTP_400_BAD_REQUEST)
    		
            sku_ids = cart_dict.keys()
            sku_queryset = SKU.objects.filter(id__in=sku_ids)
            for sku in sku_queryset:
                sku.count = cart_dict[sku.id].get('count')
                sku.selected = cart_dict[sku.id].get('selected')
    
            serializer = SKUCartSerializer(sku_queryset,many=True)
            return Response(serializer.data)
    
    
        def put(self,request):
            pass
    
        def delete(self,request):
            pass
    
  • 使用登录用户或者未登录用户分别测试效果,成功

更新购物车数据

  • 思路分析
- 序列化器校验

    - 不过就触发异常

    - 过就获取三个字段: sku_id,count,selected

- user验证

    - 登录就修改redis数据

        - 覆盖原来的hash: hset(cart_{},sku_id,count)
        - 如果有selected: sadd(cart_{},sku_id)
        - 如果没有就移除: srem(cart_{},sku_id)
        - 最后响应数据(来什么,回什么)

    - 未登录就修改cookie数据

        - 获取cookie数据

            - 没有就触发异常

            - 有就转成python对象 cart_dict

                - 直接覆盖原来的数据 cart_dict[sku_id] = {...}

                - 数据再次转换为str,设置cookie并响应
                
### carts.views
......
class CartView(APIView):

    authentication_classes = [JSONWebTokenAuthentication,]
    
    def perform_authentication(self, request):
        pass
        
    def put(self,request):

        serializer = 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:
            user = None

        if user and user.is_authenticated:
            conn = get_redis_connection('cart')
            pl = conn.pipeline()
            pl.hset('cart_{}'.format(user.id),sku_id,count)
            if selected:
                pl.sadd('selected_{}'.format(user.id),sku_id)
            else:
                pl.srem('selected_{}'.format(user.id),sku_id)
            pl.execute()
            return Response(serializer.data)
        else:
            cart_str = request.COOKIES.get('cart')
            if not cart_str:
                return Response({'message':'没有cookie'},status=status.HTTP_400_BAD_REQUEST)
            cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            cart_dict[sku_id]={
                'count':count,
                'selected':selected
            }
            cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()
            response = Response(serializer.data)
            response.set_cookie('cart',cart_str)
            return response
            
    - 前端代码:
    
    	......
    	// 更新购物车数据
        update_selected: function (index) {
            axios.put(this.host + '/carts/', {
                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);
                })
        },
        
- 先用已登录用户去测试(未登录用户,需要做的事情还不少,比如应该先跳到'登录页',登录完成以后,作'购物车合并'操作)
        

删除购物车数据

  • 思路分析
- 序列化  CartDeleteSerializer: 只对 sku_id 进行校验

- 创建 serializer,校验数据

	- 不通过就触发异常
	- 通过就继续下面的逻辑
	
- 生成公共的response
	
- 校验user: 用户是否已登录

	- 已登录
		-  hdel | srem|
    - 未登录
    	- 获取cart_str
    		- 没有就返回 400
    		- 有就转换成python对象
    		
    		- 当sku_id在cart_dict里面,才删
    			- 如果没删光,就转换成str,设置cookie返回给前端
    			- 删光,就删除 cookie_cart
    	
   
### carts.serializers(只有一个字段)
......
class CartDeleteSerializer(serializers.Serializer):
    sku_id = serializers.IntegerField(label='SKU商品ID')

    def validate_sku_id(self,value):
        try:
            SKU.objects.get(id=value)
        except SKU.DoesNotExist:
            raise serializers.ValidationError('sku_id不存在')
        return value
        
### carts.views
......
class CartView(APIView):

    authentication_classes = [JSONWebTokenAuthentication,]
   
    def perform_authentication(self, request):
        pass
        
    ......

    def delete(self,request):
        serializer = CartDeleteSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        sku_id = serializer.validated_data.get('sku_id')

        response = Response(serializer.data,status=status.HTTP_204_NO_CONTENT)

        try:
            user = request.user
        except:
            user = None

        if user and user.is_authenticated:
            conn = get_redis_connection('cart')
            pl = conn.pipeline()
            pl.hdel('cart_{}'.format(user.id), sku_id)
            pl.srem('selected_{}'.format(user.id), sku_id)
            pl.execute()

        else:
            cart_str = request.COOKIES.get('cart')
            if not cart_str:
                return Response({'message': '没有cookie'}, status=status.HTTP_400_BAD_REQUEST)
            cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            if 'sku_id' in cart_dict:
                del cart_dict['sku_id']
            if len(cart_dict):
                cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()
                response.set_cookie('cart', cart_str)

            else:
                response.delete_cookie('cart')

        return response

购物车全选 || 全不选

  • 思路分析: 新建视图来处理(如果想利用上面的接口,需搭配viewset,通过自定义动作来实现)
- 已登录用户: 根据 selected 

- 序列化  CartSelectedAllSerializer: 只有 selected(布尔)

- CartSelectedAllView(put)

    - 创建 serializer,校验数据

        - 不通过就触发异常
        - 通过就继续下面的逻辑

    - 生成公共的response

    - 校验user: 用户是否已登录

        - 已登录
            -  获取 dict所有数据,然后取keys()
            	- 如果 selected: sadd
            	- 取反 srem       
        - 未登录
            - 获取cart_str
                - 没有,就返回 400
                - 有,就转换成python对象
            - 遍历 python对象
            	- 每一项的['selected'] = selected # 是否勾选由前端传过来的这个值来决定(True就全勾,False就全不勾)
            	
            - 重新编码成 cart_str,设置cookie并返回
				
    	
### carts.serializers
......
class CartSelectedAllSerializer(serializers.Serializer):
    selected = serializers.BooleanField(label='是否全选')
    
### carts.views
......
class CartSelectedAllView(APIView):

    authentication_classes = [JSONWebTokenAuthentication, ]

    def put(self,request):

        serializer = CartSelectedAllSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        selected = serializer.validated_data.get('selected')

        response = Response(serializer.data)
        try:
            user = request.user
        except:
            user = None

        if user and user.is_authenticated:
            conn = get_redis_connection('cart')
            redis_dict = conn.hgetall('cart_{}'.format(user.id))
            sku_ids = redis_dict.keys()
            if selected:
                conn.sadd('selected_{}'.format(user.id),*sku_ids)
            else:
                conn.srem('selected_{}'.format(user.id),*sku_ids)
        else:
            cart_str = request.COOKIES.get('cart')
            if not cart_str:
                return Response({'message':'没有cookie'},status=status.HTTP_400_BAD_REQUEST)
            cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
            for sku_id in cart_dict:
                cart_dict[sku_id]['selected'] = selected
            cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()
            response.set_cookie('cart',cart_str)

        return response
        
### carts.urls
......
urlpatterns = [
    ......
    url(r"^carts/selection/$",views.CartSelectedAllView.as_view()),
]

未登录用户合并购物车逻辑

  • 思路分析
- 已登录用户购物车数据格式如下

    - hash:{sku_id_16:2}
    - set:{sku_id_1}

- 未登录用户的购物车数据如下

    - cookie{
        sku_id_16:{'count':1,'selected':False}
    }

- 依据业务需求,自定义合并规则

    - 登录后,购物车数据从 cookie 追加到 redis
        - 若cookie中的购物车商品和redis数量相同,则用cookie的覆盖 redis(以客户的实时感觉为准)
        - 商品若在cookie和redis中,只有勾选了一个,最终它就是勾选的

  • 单独写一个函数来实现合并逻辑,最终丢到登录逻辑中,即用户一登录,就合并购物车的数据
def merge_cookie_to_redis(request,user,response):
    pass
    
- 先获取cookie
    - 没有就返回None
    - 有就获取并转换为python对象

- 创建redis连接对象
	- 遍历cookie购物车大字典,把sku_id及count向redis的hash存储
		- 如果有 selected,就往集合丢 sku_id

- 删除cookie购物车数据
### carts.utils

import pickle
import base64

from django_redis import get_redis_connection

def merge_cookie_to_redis(request,user,response):
    cart_str = request.COOKIES.get('cart')
    if not cart_str:
        return

    cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
    '''
        {
            'sku_id':{'count':1,'selected':True}
        }
        
        - hash:{sku_id_16:2}
        - set:{sku_id_1}
    '''
    conn = get_redis_connection('cart')
    pl = conn.pipeline()
    for sku_id in cart_dict:
        pl.hset('cart_{}'.format(user.id),sku_id,cart_dict[sku_id]['count'])
        if cart_dict[sku_id]['selected']:
            pl.sadd('selected_{}'.format(user.id),sku_id)
    pl.execute()

    response.delete_cookie('cart')

### users.views
......
class UserAuthorizeView(ObtainJSONWebToken):

    ......

    def post(self, request, *args, **kwargs):
        ......

        if serializer.is_valid():
            ......
            merge_cookie_to_redis(request,user,response) # 新增合并购物车逻辑
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        
- 测试: 先不登录添加购物车,然后登录,再看看购物车的数据是否正确(经测试,正确...)
posted @   清安宁  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
· DeepSeek “源神”启动!「GitHub 热点速览」
点击右上角即可分享
微信分享提示