最近在用django restframe框架做一个商城项目,有一个关于购物车的业务逻辑,是用cookie和redis存储的购物车信息,在这里记录一下。

完成一个商城项目,如果不做一个购物车,就是十分可惜的。我们先来分析一下业务逻辑,参照,京东、淘宝等大型电商网站,可以发现,对于登录用户以及未登录用户,都是可以使用购物车功能。所以首先我们将这两种情况区分开来,采用不同的存储方式。先来看一下已登录用户,购物车其实类似我们在游览网页时的收藏功能,用于收藏用户喜欢的一些商品,用户使用频率较高,所以我们应该优先使用内存型的数据库redis进行存储,这样查询起来会更快。确定了使用什么数据库,我们还要在思考一下用什么形式存储数据。我使用的是2.x版本的redis,共有string,hash,set,zset,list等几种存储格式。hash类似python中的字典,其他几种格式也和python中对应的list,set,string类似。需要额外注意的是,redis中所有数据都是以bytes方式存储。再来看一下我们的需求,对于一个购物车,我们可以看到商品以及商品数量,以及是否勾选商品。对于商品,我们可以只存储其商品id,需要用到商品信息时在进行查询,而对于勾选状态,我们则需要用一个额外的字段存储,由于每个人的购物车都应该是独立的个体,所有我们可以用用户的id进行存储,我们会发现要存储上述信息,我们只使用一种存储格式是很难完成的,所以我们可以考虑用两个分别存储。商品及数量我们可以考虑使用hash格式进行存储,用key存储商品id,value存储商品数量,用户id进行区分不同的购物车,而对于勾选状态,我们可以用set进行存储,对于不同的商品只有勾选和未勾选状态,我们可以考虑将已经勾选的商品的id进行存储,在set内的商品即为勾选,不在的即为未勾选。登录用户搞定了我们再来看看未登录用户,未登录用户的话,应该只在本机使用,而在登录时进行合并处理,所以我们没有必要存储到数据库中,同时,在登录时要进行合并操作,我们可以想到cookie,在登录请求时,游览器会自己带上cookie,所以我们可以考虑用cookie存储,存储格式可以用一个嵌套的大字典。分析完毕,就开始进行真正的操作把。

增删改查四个逻辑,首先来看看新增把。对于新增购物车,我们需要接受的参数为商品id和数量,而勾选状态可以默认为勾选,添加购物车后可以在进行修改,这里的商品我们采用sku的形式进行存储。新增操作的话是post请求,我们可以在视图类中定义一个post方法来接受新增请求,这里的视图类我们继承的是APIView。这里我只写视图方法,对于序列化器就不做描写。首先我们应该接受前端传过来的参数,并进行校验,校验完成后,对用户登录状态进行判断(我是采用JWT来进行用户登录状态存储),对于不同用户,采用不同方式存储购物车信息。

 

 def post(self, request):
        # 获取参数,校验参数 (使用序列化器)
        serializer = CartSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
     # 取出验证后的参数 sku_id = serializer.validated_data['sku_id'] count = serializer.validated_data['count'] selected = serializer.validated_data['selected'] # 判断用户是否登录,这里不明白的可以看一下我之前写的django中的user验证 try: user = request.user except Exception: user = None if user is not None and user.is_authenticated: # 登录,将数据存到redis 默认勾选 redis_conn = get_redis_connection('cart') # 建立redis链接 pl = redis_conn.pipeline() # 建立管道,一次发送所有redis命令,不用多次连接redis pl.hincrby('cart_%s'%user.id, sku_id, count) # 插入域名为'cart_%s'%user.id,key为sku_id,value为count的数据,域不存在会自己创建 if selected: pl.sadd('cart_select_%s'%user.id, sku_id) # 若勾选,则会将商品id加入集合,若集合不存在则创建 pl.execute() # 将命令一次执行 return Response(serializer.validated_data, status=status.HTTP_201_CREATED) # 返回相应给前端 else: # 未登录,将数据存到cookie cart_dict = request.COOKIES.get('cart') # 从cookie中拿到购物车数据 if cart_dict is not None: cookie_cart = pickle.loads(base64.b64decode(cart_dict.encode())) # 若存在,将数据转化为字典 else: cookie_cart = {} # 若不存在,建立一个新的字典 if sku_id in cookie_cart:
          # 若商品已在购物车中,则将数据进行更新 cookie_cart[sku_id]['count'] += count cookie_cart[sku_id]['selected'] = selected else:
          # 若不存在,则建立新的数据 cookie_cart[sku_id] = { 'count':count, 'selected':selected } cart_cookie = base64.b64encode(pickle.dumps(cookie_cart)).decode() # 将数据进行加密,并转化为字符串 response = Response(serializer.validated_data, status=status.HTTP_201_CREATED) response.set_cookie('cart', cart_cookie, max_age=constants.CART_COOKIE_EXPIRES) # 给相应设置cookie return response

 

然后来看一下获取的逻辑,以get请求进行请求,获取不需要额外的参数,通过用户id查到商品的id和数量以及勾选状态,然后从数据库查到具体的商品信息,返回给前端即可。

    def get(self, request):
        try:
            user = request.user
        except Exception:
            user = None
     # 判断用户登录状态 if user is not None and user.is_authenticated: redis_conn = get_redis_connection('cart') redis_cart = redis_conn.hgetall('cart_%s'%user.id) # 获取购物车信息 redis_cart_selected = redis_conn.smembers('cart_select_%s'%user.id) # 获取勾选状态 cart_dict = {}
       # 由于redis中所有信息都是bytes类型,所以我们需要进行转化 for sku_id, count in redis_cart.items(): cart_dict[int(sku_id)] = { 'count':int(count), 'selected':sku_id in redis_cart_selected } else: cart_dict = request.COOKIES.get('cart') # 从cookie中获取购物车信息 if cart_dict is not None: cart_dict = pickle.loads(base64.b64decode(cart_dict.encode())) else: cart_dict = {} skus = SKU.objects.filter(id__in=cart_dict.keys()) for sku in skus: sku.count = cart_dict[sku.id]['count'] sku.selected = cart_dict[sku.id]['selected'] serializer = CartSKUSerializer(skus, many=True) return Response(serializer.data)

然后是修改的逻辑,以put形式请求,商品数量和勾选状态我们可以修改,所以我们需要接受这两个参数,并对redis或者cookie进行修改并返回

    def put(self, request):
        serializer = CartSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        sku_id = serializer.validated_data['sku_id']
        count = serializer.validated_data['count']
        selected = serializer.validated_data['selected']
        try:
            user = request.user
        except Exception:
            user = None
        if user is not None and user.is_authenticated:
            redis_conn = get_redis_connection('cart')
            pl = redis_conn.pipeline() # 对于要进行多次的redis操作,我们就考虑使用管道
            pl.hset('cart_%s'%user.id, sku_id, count) # 对哈希表进行数据插入,如果字段存在,就进行覆盖
            if selected:
                pl.sadd('cart_selected_%s'%user.id, sku_id) # 如果勾选,就在set中加入商品id
            else:
                pl.srem('cart_selected_%s'%user.id, sku_id) # 如果未勾选,则删除该商品id,若id不存在,则忽略操作
            pl.execute()
            return Response(serializer.validated_data)
        else:
            cart_dict = request.COOKIES.get('cart')
            if cart_dict is not None:
                cookie_cart = pickle.loads(base64.b64decode(cart_dict.encode()))
            else:
                cookie_cart = {}
            cookie_cart[sku_id] = {
                'count':count,
                'selected':selected
            }
            cart_cookie = base64.b64encode(pickle.dumps(cookie_cart)).decode() # 加密cookie
            response = Response(serializer.validated_data, status=status.HTTP_201_CREATED)
            response.set_cookie('cart', cart_cookie, max_age=constants.CART_COOKIE_EXPIRES) # 在响应中设置新的cookie返回给前端
            return response

最后是删除操作,删除需要接受的是对应的商品id,然后将对应的数据删除即可

    def delete(self, request):
     # 考虑到参数少,并不需要将数据返回,所以这里我自行对参数进行校验,这样比写序列化器代码量更少 sku_id = request.data.get('sku_id', None) if not sku_id and not isinstance(sku_id, int): return Response('请求方式错误',status=status.HTTP_400_BAD_REQUEST) if not SKU.objects.filter(id=sku_id).first(): return Response('商品不存在',status=status.HTTP_400_BAD_REQUEST) try: user = request.user except Exception: user = None if user is not None and user.is_authenticated: redis_conn = get_redis_connection('cart') pl = redis_conn.pipeline() pl.hdel('cart_%s'%user.id, sku_id) # 删除单个域,如不存在则忽略操作 pl.srem('cart_selected_%s'%user.id, sku_id) pl.execute() return Response(status=status.HTTP_204_NO_CONTENT) else: response = Response(status=status.HTTP_204_NO_CONTENT) cart_dict = request.COOKIES.get('cart') if cart_dict: cookie_cart = pickle.loads(base64.b64decode(cart_dict.encode())) else: cookie_cart = {} if sku_id in cookie_cart: del cookie_cart[sku_id] cart_cookie = base64.b64encode(pickle.dumps(cookie_cart)).decode() response.set_cookie('cart', cart_cookie, max_age=constants.CART_COOKIE_EXPIRES) return response

由于涉及到登录和未登录,所以也就涉及到用户购物车合并的问题,而cookie中信息是最新的信息,我们合并时就以cookie中信息为准,在合并完成后,会删除cookie信息进行重置。由于合并购物车不涉及业务逻辑,仅仅在登录或注册等逻辑时触发,所以不必写额外的视图,而是写成工具函数的形式,哪里需要触发,就调用该函数

def merge_cart_cookie_to_redis(request, response, user):
    """
    合并请求用户的购物车数据,将未登录保存在cookie里的保存到redis中
    遇到cookie与redis中出现相同的商品时以cookie数据为主,覆盖redis中的数据
    :param request: 用户的请求对象
    :param response: 响应对象,用于清楚购物车cookie
    :param user: 当前登录的用户
    :return:
    """
  # 首先在cookie中获取标准信息,若未登录状态下没有添加商品到购物车cookie中也就没有购物车,直接返回响应,不需要做合并处理 cart_dict = request.COOKIES.get('cart') if not cart_dict: return response cookie_cart = pickle.loads(base64.b64decode(cart_dict.encode())) redis_conn = get_redis_connection('cart') redis_cart = redis_conn.hgetall('cart_%s'%user.id) # 从redis中取出过期的信息,并进行转化 cart = {} for sku_id, count in redis_cart.items(): cart[int(sku_id)] = int(count)
   # 首先将商品id和数量进行更新,并将selected为真的存在add的列表中,为假的存在remove的列表中,下面可以一次进行操作 redis_cart_selected_add = [] redis_cart_selected_remove = []
   # 更新商品id和数量 for sku_id, count_selected_dict in cookie_cart.items(): cart[sku_id] = count_selected_dict['count'] if count_selected_dict['selected']: redis_cart_selected_add.append(sku_id) else: redis_cart_selected_remove.append(sku_id) if cart: pl = redis_conn.pipeline() pl.hmset('cart_%s' % user.id, cart) if redis_cart_selected_add: pl.sadd('cart_selected_%s' % user.id, *redis_cart_selected_add) # sadd可以直接添加一组数据 if redis_cart_selected_remove: pl.srem('cart_selected_%s' % user.id, *redis_cart_selected_remove) # srem可以直接删除一组数据 pl.execute() response.delete_cookie('cart') return response

至此,整个逻辑完成。

新人写博客锻炼自己,有错误欢迎大家指出,我的QQ:595395786!!!