购物车部分

  • 购物车商品应当存储那些数据

    • sku_id,count(用户购买几个),selected(是否被勾选)
- 登录用户: 允许使用服务器资源

    - 存储到 redis,每条数据分两种格式存储

        - Set:{sku_id_1,sku_id_2......} # 有放入集合(自带去重功能),就表示已勾选

        - Hash:dict {sku_id_1:count,sku_id_2:count......}

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

    - 存储到 浏览器cookie(存到 localStorage也可以...)

    - 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模块: 提供了对于 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}}
  • base64模块:把bytes类型数据,作进一步'加密'/'解密'处理
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'
  • 后端编写购物车接口
# apps.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,]
    # permission_classes = [IsAuthenticated,]

    def post(self,request):
        return Response({'msg':'响应成功'})


    def get(self,request):
        pass

    def put(self,request):
        pass

    def delete(self,request):
        pass

  • 前端代码
......
// 添加购物车
add_cart: function(){
    axios.post(this.host+'/carts/', {
            // 把 sku_id 和 count 发送到后端
            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);
        })
},
  • 未登录用户的认证问题
- 不管用户是否登录,都必须实现购物车功能

- 而当用户未登录时候,前端依然在 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
        校验才会继续进行
        '''

### setttings
......
#---------DRF配置项------------#
REST_FRAMEWORK = {
   ......
    'DEFAULT_AUTHENTICATION_CLASSES': [
        # 首选是JWT
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ],

    # 这个权限类不能加,加上就验证: IsAuthenticated 会调用 request.user/auth
    # 'DEFAULT_PERMISSION_CLASSES': [
    #     # IsAuthenticated 仅通过认证的用户
    #     'rest_framework.permissions.IsAuthenticated'
    # ]
}

### views
......
class CartView(APIView):

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

    def post(self,request):
        return Response({'msg':'响应成功'})


    def get(self,request):
        pass

    def put(self,request):
        pass

    def delete(self,request):
        pass

- 测试: http://127.0.0.1:8000/carts/

    - 参数: Authorization: JWT空格

- 加上 request.user 测试

    def post(self,request):

        # 启用校验逻辑,触发异常: "detail": "Invalid Authorization header. No credentials provided."
        user = request.user
        return Response({'msg':'响应成功'})

- 捕获该异常并继续执行我们的逻辑

    def post(self,request):

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

        return Response({'msg':'响应成功'})

新增购物车数据

  • 先对前端提交的数据,作校验
### carts.serializer
from rest_framework import serializers

from 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

  • 视图逻辑

- redis新增 4号 数据库,存储购物车数据 'cart'

- 序列化器校验/获取前端传过来的数据

    - 没通过就异常

    - 通过,就获取sku_id,count,selected


- 获取未登录用户的购物车数据:经过更新,返回给cookie

    - 如果有值: 'sdhfjzkdafsdfnNdfseksjDSDfjsdfjskdfsd'

        - 转成 bytes类型,再转成 b64 解码类型,再pickle成功 python对象

    - 如果没值: 空dict包裹,往后再扔数据


    - 当用户再点击一次商品,要作'增量计数'

        - 获取 原先的 origin_count,现有的count+

    - 更新 cart_dict[sku_id]

        - count,selected

    - 数值类型的转换

        先pickle.dumps,b64encode ,decode()

    - 创建响应对象并设置 cookie,响应

        - response

        - response.set_cookie('cart',cart_str)

        - return response


- 获取已登录用户的购物车数据: 把用户传过来的数据存入redis,然后响应(传过来什么,就回什么)

    - 一样的思路,只不过把cookie字符串换成了操作 redis而已

    - 创建 redis连接对象

    - 管道处理以下两个流程

        - 利用 .hincrby(cart_{}.format(user.id),sku_id,count)

        - 若 selected 存在 .sadd(selected_{}.format(user.id),sku_id)

    - 返回响应
class CartView(APIView):

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

        # user有值,也可能是匿名用户,所以再作一次校验,验证是否是登录用户
        if user and user.is_authenticated:

            redis_conn = get_redis_connection('cart')
            pl = redis_conn.pipeline()
            pl.hincrby('cart_{}'.format(user.id),sku_id,count)
            if selected:
                pl.sadd('selected_{}'.format(user.id),sku_id)
            pl.execute()

            response = Response(serializer.data, status=status.HTTP_201_CREATED)
            return response


        else:
            # 获取cookie做了一堆事情,最终返回data,并设置cookie(购物车数据丢到里面)
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_str_bytes = cart_str.encode()
                b64_bytes = base64.b64decode(cart_str_bytes)
                cart_dict = pickle.loads(b64_bytes)
            else:
                cart_dict = {}

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

            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


    def get(self,request):
        pass

    def put(self,request):
        pass

    def delete(self,request):
        pass


查询购物车数据(即用户点击购物车,展示商品)

### 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,SKUCartSerializer
from goods.models import SKU

class CartView(APIView):

    # 延迟校验
    def perform_authentication(self, request):
        pass

    # 新增
    def post(self,request):
        ......


    def get(self,request):

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

        '''
        - 构造数据: {
                        'sku_id_1':{'count':1,'selected':True},
                        'sku_id_2':{'count':3,'selected':True},
                    }
        '''

        if user and user.is_authenticated:
            redis_conn = get_redis_connection('cart')
            cart_redis_dict = redis_conn.hgetall('cart_{}'.format(user.id))
            selected = redis_conn.smembers('selected_{}'.format(user.id))
            cart_dict = {}
            for bytes_sku_id,bytes_count in cart_redis_dict.items():

                # 不管用户是否登录,构造成一样格式的数据,方便后续统一处理
                cart_dict[int(bytes_sku_id)] = {
                    'count':int(bytes_count),
                    'selected': bytes_sku_id in selected
                }

        else:
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.decode(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

### serializer
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']

更新购物车数据

- 序列化器校验

    - 不过就触发异常

    - 过就获取三个字段: 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并响应

### 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,SKUCartSerializer
from goods.models import SKU

class CartView(APIView):

    def perform_authentication(self, request):
        pass

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

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

    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:
            redis_conn = get_redis_connection('cart')
            pl = redis_conn.pipeline()
            pl.hset('cart_{}'.format(user.id),sku_id,count)
            if selected:
                pl.sadd('cart_{}'.format(user.id),sku_id)
            else:
                pl.srem('cart_{}'.format(user.id),sku_id)
            pl.execute()
            return Response(serializer.data)

        else:
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.decode(cart_str.encode()))
                cart_dict[sku_id] = {
                    'count':count,
                    'selected':selected
                }
                cart_str = base64.encode(pickle.dumps(cart_dict)).decode()
                response = Response(serializer.data)
                response.set_cookie('cart',cart_str)
                return response

            else:
                return Response({'msg':'没有购物车数据'},status=status.HTTP_400_BAD_REQUEST)

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

购物车删除

  • 序列化器:前端只传一个 sku_id 过来,校验一下
### serializers
class CartDeleteSerializer(serializers.Serializer):
    
    sku_id = serializers.IntegerField(min_value=1, 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
  • views 有些小细节要处理一下
- 操作未登录用户的cookie时

    - 注意字典的key是否存在
    - 一直删,删光/未删光要分别作处理

### views
class CartView(APIView):

    # 延迟校验
    def perform_authentication(self, request):
        pass

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


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

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


    def delete(self,request):

        # 获取用户数据
        serializer = CartSerializer(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:
            redis_conn = get_redis_connection('cart')
            pl = redis_conn.pipeline()
            # 删除
            pl.hdel('cart_{}'.format(user.id),sku_id)
            pl.srem('cart_{}'.format(user.id),sku_id)
            pl.execute()
        else:

            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.decode(cart_str.encode()))
            else:
                return Response({'msg':'没有购物车数据'},status=status.HTTP_400_BAD_REQUEST)

            # 检验 sku_id是否存在,才删(若不存在,就不作处理,省事)
            if sku_id in cart_dict:
                del cart_dict[sku_id]

            # 是否删光,没有删光才返回cookie('cart');若删光了,就没有必要存在cookie('cart')
            if len(cart_dict.keys()):
                cart_str = base64.encode(pickle.dumps(cart_dict)).decode()
                response.set_cookie('cart',cart_str)
            else:
                response.delete_cookie('cart')

        # 返回公用响应
        return response

购物车的全选/取消

  • 序列化器:前端只传一个 布尔值selected 过来,校验一下
### serializers
class CartSelectedAllSerializer(serializers.Serializer):
    selected = serializers.BooleanField(label='是否全选')
  • 新建 views 来处理
### views
class CartSelectedAllView(APIView):

    def perform_authentication(self, request):
        pass

    def put(self, request):

        serializer = CartSerializer(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:
            # 获取dict的所有sku_id,写入/删除 集合
            redis_conn = get_redis_connection('cart')
            redis_dict = redis_conn.hgetall('cart_{}'.format(user.id))
            sku_ids = redis_dict.keys()
            if selected:
                redis_conn.add('cart_{}'.format(user.id),*sku_ids)
            else:
                redis_conn.srem('cart_{}'.format(user.id),*sku_ids)

        else:
            cart_str = request.COOKIES.get('cart')
            if cart_str:
                cart_dict = pickle.loads(base64.decode(cart_str.encode()))
            else:
                return Response({'msg': '没有购物车数据'}, status=status.HTTP_400_BAD_REQUEST)

            # 修改所有dict的值:前端传真就是真,传假就为假,再返回
            for sku_id in cart_dict:
                cart_dict[sku_id]['selected'] = selected

            cart_str = base64.encode(pickle.dumps(cart_dict)).decode()
            response.set_cookie('cart', cart_str)

        return response

依据用户是否登录,合并购物车分析

- 已登录用户添加的购物车

    - 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中,只有勾选了一个,最终它就是勾选的

代码实现流程

- 先获取cookie

- 判断cookie是否有购物车数据

    - 没有就直接响应

- 把购物车字符串转换成python对象

- 创建redis连接对象

- 遍历cookie购物车大字典,把sku_id及count向redis的hash存储

- 删除cookie购物车数据

# 经过分析,需要传入这三个对象
# 合并的动作,要在登录时候完成,所以函数写完,要扔到登录的逻辑中
# 登录逻辑: 用户名/密码登录  QQ登录
def merge_cookie_to_redis(request,user,response):
    pass
### cart.utils.py
import base64
import pickle

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.b64encode(cart_str.encode()))
    '''
        {
            'sku_id':{'count':1,'selected':True}
        }
    '''

    redis_conn = get_redis_connection('cart')
    pl = redis_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('cart_{}'.format(user.id),sku_id)

    pl.execute()
    response.delete_cookie('cart')


  • 把上述逻辑,扔到QQ登录的逻辑中
### qqauth.views
......
class QQAuthUserView(APIView):
    def get(self,request,*args,**kwargs):
        ......
        else:
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(qq_user.user)
            token = jwt_encode_handler(payload)
            qq_user.user.token = token  # 把token加到user字段

            response = Response({
                            'user_id':qq_user.user.id,
                            'username':qq_user.user.username,
                            'token':token
                        })
            # 合并的逻辑
            merge_cookie_to_redis(request,qq_user.user,response)

            return response

    def post(self,request):
        ......
  • 用户名/密码 登录,在原先的基础上,需要写一个子类继承父类,重写post方法逻辑
    加入我们的购物车和并代码
### users.urls
# 登录逻辑:走的是框架提供的登录逻辑
url(r'^authorizations/$', obtain_jwt_token),

# 源码
obtain_jwt_token = ObtainJSONWebToken.as_view()
......
class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer
......
class JSONWebTokenAPIView(APIView):
    ......
    def get_serializer_context(self):
        ......
    def get_serializer_class(self):
        ......
    def get_serializer(self, *args, **kwargs):
        ......
    # 我们需要在post()方法中,加入合并购物车的逻辑
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
### users.views
from datetime import datetime
from rest_framework_jwt.settings import api_settings
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER
from carts.utils import merge_cookie_to_redis
class UserAuthorizeView(ObtainJSONWebToken):

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)

        if serializer.is_valid():
            user = serializer.object.get('user') or request.user
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            # 只添加这句
            merge_cookie_to_redis(request,user,response)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

### users.urls
......
# 登录
# url(r'^authorizations/$', obtain_jwt_token),
url(r'^authorizations/$', views.UserAuthorizeView.as_view()),