购物车逻辑-增删改查
1.需求分析
- 在用户登录或未登录的状态下,都可以保存用户的购物车数据
- 用户可以对购物车数据进行增、删、该、查
- 保存购物车中商品数量,是否勾选(在订单页面会使用到)
- 在用户登录时,合并cookie的购物车数据到redis(最新数据以redis为准)
2.实现方法
- 对于未登录的用户,将购物车数据存储到浏览器cookie中
- 对于已登录的用户,将购物车数据存储到后端的redis
购物车逻辑是一个典型的增删改查的案例,通过设计一个简单的数据存储方式,实现增删改查的操作
3.购物车数据存储方式
redis保存已登录用户的购物车数据,使用string类型
说明:(后面会对操作购物车数据的方法进行封装,之后再调用会很愉快)
cookie保存未登录用户的购物车数据
为了方便购物车的合并,所以尽量将redis和cookie的数据存储方式保持一致
{
sku_{id}: {
"count": xxx, // 数量
"selected": True // 是否勾选
},
sku_{id}: {
"count": xxx,
"selected": False
},
...
}
在Django中设置cookie:response.set_cookie(键,值,有效期)
response.set_cookie('cart', cart_str, max_age=constants.CART_COOKIE_EXPIRES)
4.引入pickle模块和base64模块
在cookie中只能保存字符串数据,所以将上述数据使用pickle进行序列化转换 ,并使用base64编码为字符串,保存到cookie中
pickle模块的使用:
- pickle模块是python的标准模块,提供了对于python数据的序列化操作,可以将数据转换为bytes类型,其序列化速度比json模块要高。
- pickle.dumps() 将python数据序列化为bytes类型
pickle.loads() 将bytes类型数据反序列化为python的数据类型
base64模块的使用:
- Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2^6=64,所以每6个比特为一个单元,对应某个可打印字符。3个字节有24个比特,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。在Base64中的可打印字符包括字母A-Z、a-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。
- Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据,包括MIME的电子邮件及XML的一些复杂数据。
- python标准库中提供了base64模块,用来进行转换
base64.b64encode() 将bytes类型数据进行base64编码,返回编码后的bytes类型
base64.b64deocde() 将base64编码的bytes类型或者str类型
进行解码,返回解码后的bytes类型
import base64
import pickle
cart_dict = {1: [2, True], 2: [1, True]}
# dict -> bytes -> str
cart_bytes = pickle.dumps(cart_dict) # 将python字典转换为16进制bytes类型
print(type(cart_bytes), "|", cart_bytes) # <class 'bytes'>
cart_b64_bytes = base64.b64encode(cart_bytes) # 转换为base64_bytes
print(type(cart_b64_bytes), "|", cart_b64_bytes) # <class 'bytes'>
cart_b64_str = cart_b64_bytes.decode() # 解码成base64_str
print(type(cart_b64_str), "|", cart_b64_str) # <class 'str'>
print("=" * 100)
cart_b64_str = "gAN9cQAoSwFdcQEoSwKIZUsCXXECKEsBiGV1Lg=="
# str -> bytes -> dict
cart_bytes = base64.b64decode(cart_b64_str) # 将字符串直接将base64_str转换为16进制的bytes
print(type(cart_bytes), "|", cart_bytes) # <class 'bytes'>
cart_dict = pickle.loads(cart_bytes) # 将16进制的bytes转换为python字典
print(type(cart_dict), "|", cart_dict) # <class 'dict'>
# 值得注意的是:在得到python字典的过程中,base64.decode()可以解码 base64_bytes,也可以直接解码 base64_str
5.具体逻辑实现
自定义辅助类
实现数据的转换
获取用户的登录状态
已登录:读取redis的数据,或 将数据保存到redis
未登录:读取cookie的数据,或 将数据保存到cookie合并购物车
对用户登录的视图类/函数进行装饰
就可以在用户登录时进行购物车合并操作
import base64
import pickle
from django_redis import get_redis_connection
from carts import constants
# redis 存储方式为字符串类型:
# set key value (user_id,{sku_id: [count, selected]})
class CartMixin(object):
""" 自定义辅助类 """
def read_cart(self, request) -> dict:
""" 读取购物车数据 """
# 获取用户登录状态
user = request.user
# 如果用户存在且通过认证
if user and user.is_authenticated:
# 读取 redis 中购物车数据,返回字典数据
return self.read_from_redis(user)
else:
# 读取 cookie 中购物车数据,返回字典数据
return self.read_from_cookie(request)
def wright_cart(self, request, cart_dict, response):
""" 写入购物车数据 """
# 获取用户登录状态
user = request.user
# 如果用户存在且通过验证
if user and user.is_authenticated:
# 将商品保存到 redis
return self.wright_to_redis(request, cart_dict)
else:
# 将商品保存到 cookie
return self.wright_to_cookie(cart_dict, response)
def read_from_redis(self, user) -> dict:
""" 读取 redis 中的购物车数据,返回字典 """
redis_conn = get_redis_connection("cart")
# 从 redis 取出来的值是 <class 'bytes'> 类型
cart_bytes = redis_conn.get(f"cart_{user.id}")
cart_dict = pickle.loads(base64.b64decode(cart_bytes)) if cart_bytes else dict()
return cart_dict
def wright_to_redis(self, request, cart_dict):
""" 商品保存到redis """
redis_conn = get_redis_connection("cart")
cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()
redis_conn.set(f"cart_{request.user.id}", cart_str)
def read_from_cookie(self, request) -> dict:
""" 读取 cookie 中的购物车数据,返回字典 """
cart_str = request.COOKIES.get("cart") # <class 'str'>
cart_dict = pickle.loads(base64.b64decode(cart_str.encode())) if cart_str else dict()
return cart_dict
def wright_to_cookie(self, cart_dict, response):
""" 将商品信息保存到 cookie 中 """
cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode()
response.set_cookie('cart', cart_str, max_age=constants.CART_COOKIE_EXPIRES)
def merge_cart_cookie_to_redis(request, user, response):
""" 登录时合并购物车,将cookie中的数据合并到redis中 """
# 相同商品如何处理?
# 商品数量:以cookie为准
# 勾选状态:以cookie为准
# 1 业务处理,实例化对象
merge_cart = CartMixin()
# 2 获取redis中的购物车数据
redis_cart_dict = merge_cart.read_from_redis(user)
# 3 获取cookie中的购物车数据
cookie_cart_dict = merge_cart.read_from_cookie(request)
# 4 合并
redis_cart_dict.update(cookie_cart_dict)
# 5 将购物车数据保存到redis中
merge_cart.wright_to_redis(request, redis_cart_dict)
# 6 删除cookie中的购物车数据
response.delete_cookie("cart")
""" 使用装饰器的方式实现合并购物车 """
def merge_cart_decoration(func):
def wrapper(request, *args, **kwargs):
# 执行视图
print("调用原始方法" + request.method)
resp = func(request, *args, **kwargs)
if 200 <= resp.status_code < 300 :
# 判断视图执行结果,如果视图成功执行,且用户登录认证成功,则进行合并购物车操作
if request.user and request.user.is_authenticated:
merge_cart_cookie_to_redis(request, request.user, resp)
print("合并购物车成功")
else:
# raise Exception("如果登录成功,请添加 user 属性到 request 中")
print("如果登录成功,请添加 user 属性到 request 中")
else:
print("身份认证错误,没有合并购物车")
return resp
return wrapper
视图类
from django.core.serializers import get_serializer
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
import logging
from carts.utils import CartMixin
from goods.models import SKU
from .serializers import CartSerializer, CartSKUSerializer, CartDeleteSerializer
logger = logging.getLogger("django")
class CartView(CartMixin, GenericAPIView):
""" 购物车 """
serializer_class = CartSerializer
def post(self, request):
""" 将商品保存到购物车 """
# sku_id, count, selected
# 使用序列化器进行校验
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 获取校验通过的数据
# sku_id, count, selected
attrs = serializer.validated_data
sku_id, count, selected = attrs['sku_id'], attrs['count'], attrs['selected']
# 读取购物车数据
cart_dict = self.read_cart(request)
# 根据幂等性进行处理
cart_dict[sku_id] = [count, selected]
# 保存购物车数据
response = Response(serializer.data)
self.wright_cart(request, cart_dict, response)
return response
def get(self, request):
""" 查询购物车 """
# 读取购物车数据
cart_dict = self.read_cart(request)
sku_id_list = cart_dict.keys()
# 查询数据库,获取商品sku对象
skus = list()
try:
skus = SKU.objects.filter(id__in=sku_id_list)
except Exception as e:
logger.error(f"数据库查询异常:[message: {e}]")
# 遍历sku_obj_list 向sku对象中添加 count 和 selected 属性
for sku in skus:
sku.count = cart_dict[sku.id][0]
sku.selected = cart_dict[sku.id][1]
# 序列化返回
serializer = CartSKUSerializer(skus, many=True)
return Response(serializer.data)
def put(self, request):
""" 修改购物车数据 """
# sku_id, count, selected
# 使用序列化器进行校验
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 获取校验通过的数据
attrs = serializer.validated_data
sku_id, count, selected = attrs['sku_id'], attrs['count'], attrs['selected']
# 读取购物车数据
cart_dict = self.read_cart(request)
# 修改购物车数据
cart_dict[sku_id] = [count, selected]
# 保存购物车数据
response = Response(serializer.data)
self.wright_cart(request, cart_dict, response)
return response
def delete(self, request):
""" 删除购物车中的商品 """
# sku_id
# 校验参数
serializer = CartDeleteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
sku_id = serializer.validated_data["sku_id"]
# 读取购物车数据
cart_dict = self.read_cart(request)
# 删除购物车数据
try:
cart_dict.pop(sku_id)
except Exception:
return Response({"message": "购物车中没有这件商品"}, status=status.HTTP_404_NOT_FOUND)
# 保存购物车数据
response = Response(serializer.data)
self.wright_cart(request, cart_dict, response)
return response
序列化器类
from rest_framework import serializers
from goods.models import SKU
class CartSKUSerializer(serializers.ModelSerializer):
""" 查询购物车商品信息-序列化器 """
count = serializers.IntegerField(label='数量')
selected = serializers.BooleanField(label='是否勾选')
class Meta:
model = SKU
fields = ('id', 'count', 'name', 'default_image_url', 'price', 'selected')
class CartSerializer(serializers.Serializer):
""" 添加商品到购物车-序列化器,只进行数据校验 """
sku_id = serializers.IntegerField(label='sku id ', min_value=1)
count = serializers.IntegerField(label='数量', min_value=1)
selected = serializers.BooleanField(label='是否勾选', default=True)
def validate(self, data):
try:
sku = SKU.objects.get(id=data['sku_id'])
except SKU.DoesNotExist:
raise serializers.ValidationError('商品不存在')
if data['count'] > sku.stock:
raise serializers.ValidationError('商品库存不足')
return data
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
具体的项目代码已上传到码云:https://gitee.com/chenkaichen/mmc.git