实战项目-美多商城(六)购物车
购物车应该存储那些数据
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模块
(将数据在str
和bytes
之间转换): 提供了对于 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
类型数据,作进一步'编码'/'解码'处理 -
小结: 通过
pickle
和base64
实现数据的二次编码/解码
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):
......
使用
登录用户
和未登录用户
分别测试一下,查看redis
和cookie
数据正常
功能 --- 展示购物车数据(登录用户||未登录用户都要处理)
-
当用户点击
我的购物车
时,把购物车的数据返回给前端 -
请求方式
请求方法 请求地址 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)
- 测试: 先不登录添加购物车,然后登录,再看看购物车的数据是否正确(经测试,正确...)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
· DeepSeek “源神”启动!「GitHub 热点速览」