购物车部分
-
购物车商品应当存储那些数据
- 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()),