redis所有参考命令:http://doc.redisfans.com/
redis所有方法的使用:http://redis-py.readthedocs.io/en/latest/#indices-and-tables
pickle模块的使用
pickle模块是python的标准模块,提供了对于python数据的序列化操作,可以将数据转换为bytes类型,其序列化速度比json模块要高。
pickle.dumps() 将python数据序列化为bytes类型
pickle.loads() 将bytes类型数据反序列化为python的数据类型
>>> import pickle >>> d = {'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}} >>> s = pickle.dumps(d) >>> s b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.' >>> pickle.loads(s) {'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}}
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类型进行解码,返回解码后的bytes类型
>>> import base64 >>> s b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.' >>> b = base64.b64encode(s) >>> b b'gAN9cQAoWAEAAAAxcQF9cQIoWAUAAABjb3VudHEDSwpYCAAAAHNlbGVjdGVkcQSIdVgBAAAAMnEFfXEGKGgDSxRoBIl1dS4=' >>> base64.b64decode(b) b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}q\x02(X\x05\x00\x00\x00countq\x03K\nX\x08\x00\x00\x00selectedq\x04\x88uX\x01\x00\x00\x002q\x05}q\x06(h\x03K\x14h\x04\x89uu.'
用到购物车未登录用户数据存储上 逻辑如下图所示:
使用终端命令 创建购物车的子应用carts:python manage.py startapp carts
添加到购物车
因为前端可能携带cookie,为了保证跨域请求中,允许后端使用cookie,确保在配置文件有如下设置:
CORS_ALLOW_CREDENTIALS = True
主应用urls.py文件中添加路由:
urlpatterns = [ 。。。。。。 # 购物车 url(r'^', include('carts.urls')), ]
在子应用carts/urls.py文件中添加路由:
from django.conf.urls import url from . import views urlpatterns = [ # 购物车 url(r'^cart/$', views.CartView.as_view()), ]
编写视图:
注意:因为前端请求时携带了Authorization请求头(主要是JWT),而如果用户未登录,此请求头的JWT无意义(没有值),为了防止REST framework框架在验证此无意义的JWT时抛出401异常,在视图中需要做两个处理
- 重写perform_authentication()方法,此方法是REST framework检查用户身份的方法
- 在获取request.user属性时捕获异常,REST framework在返回user时,会检查Authorization请求头,无效的Authorization请求头会导致抛出异常
在carts/views.py中创建视图:
from rest_framework.views import APIView from django_redis import get_redis_connection from rest_framework.response import Response from rest_framework import status import base64 import pickle from . import serializers class CartView(APIView): """购物车后端:增删改查""" def perform_authentication(self, request): """ 重写父类的用户验证方法,不在进入视图前就检查JWT 保证用户未登录也可以进入下面的请求方法,不让他执行request.user方法 """ pass def post(self, request): """新增""" # 新建序列化器对象 serializer = serializers.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') # 判断用户是否是登录的 # request.user : 当用户是登录的用户,就会获取到登录用户的信息,反之,会抛出异常或者返回匿名用户 try: user = request.user except Exception: user = None if user is not None and user.is_authenticated: # 用户已登录,操作redis # 获取连接到数据库的对象 redis_conn = get_redis_connection('carts') # 管道 pl = redis_conn.pipeline() # 操作hash类型的数据,存储哪个用户的哪件商品数量 # hincrby() : 实现增量计算,根据key是否存在,实现是累加还是赋新值 pl.hincrby('cart_%s' % user.id, sku_id, count) # 存储该商品是否勾选 if selected: pl.sadd('selected_%s' % user.id, sku_id) # 执行 pl.execute() # 响应结果:查询和修改 200, 新增是 201, 删除是204 return Response(serializer.data, status=status.HTTP_201_CREATED) else: # 用户未登录,操作cookie # 从cookie中获取已存在的购物车数据 cart_str = request.COOKIES.get('cart') # cart_str = (str)udHEDSwpYCAAA' if cart_str: # 将cart_str字符串转成二进制类型的字符串 b'udHEDSwpYCAAA' # cart_str.encode() # 再将b'udHEDSwpYCAAA'转成b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01} # base64.b64decode(cart_str.encode()) # 将b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01}转成字典 cart_dict = pickle.loads(base64.b64decode(cart_str.encode())) else: # 表示用户从来没有在cookie中存储过购物车数据 cart_dict = {} # 将前端传入的sku_id,count,selected存储到cookie # { # sku_id10: { # "count": 10, // 数量 # "selected": True // 是否勾选 # }, # sku_id20: { # "count": 20, # "selected": False # }, # ... # } if sku_id in cart_dict: # 取出原始的值,进行累加。 origin_count = cart_dict[sku_id]['count'] count += origin_count # 将累加或者最新的count保存到字典 cart_dict[sku_id] = { 'count':count, 'selected':selected } # 将购物车字典转正字符串 # (dict){'1': {'count': 10, 'selected': True}, '2': {'count': 20, 'selected': False}} # (str)gAN9cQAoWAEAAAAxcQF9cQIoWAUAAABjb3VudHEDSwpYCAAAAHNlbGVjdGVkcQSIdVgBAAAAMnEFfXEGKGgDSxRoBIl1dS4= cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode() # 创建response对象 response = Response(serializer.data, status=status.HTTP_201_CREATED) # 将新的购物车数据写入到cookie response.set_cookie('cart', cookie_cart_str) # 响应结果 return response
settings.py文件中配置登录用户购物车的redis数据库:
CACHES = { 。。。。。。 "carts": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/4", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } }, }
在carts/serialziers.py中创建序列化器
from rest_framework import serializers from goods.models import SKU class CartSerializer(serializers.Serializer): """购物车序列化器""" # 指定字段 sku_id = serializers.IntegerField(label='商品 SKU 编号', min_value=1) count = serializers.IntegerField(label='商品数量', min_value=1) selected = serializers.BooleanField(label='商品是否勾选', default=True) def validate(self, attrs): # 获取字段 sku_id = attrs['sku_id'] count = attrs['count'] try: sku = SKU.objects.get(id=sku_id) except SKU.DoesNotExist: raise serializers.ValidationError('商品 sku id 不存在') # 判断库存 if count > sku.stock: raise serializers.ValidationError('库存不足') return attrs
前端实现
在detail.js中编写添加购物车的调用
注意:前端在此跨域请求中要携带cookie,需要在axios中添加配置withCredentials: true
// 添加购物车 add_cart: function(){ axios.post(this.host+'/cart/', { 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); }) },
查询购物车数据
在carts/serializers.py中创建序列化器
class CartSKUSerializer(serializers.ModelSerializer): """ 购物车商品数据序列化器 """ # 如果不指定,默认readonly(输出) count = serializers.IntegerField(label='数量') selected = serializers.BooleanField(label='是否勾选') class Meta: model = SKU fields = ('id', 'count', 'name', 'default_image_url', 'price', 'selected')
在carts/views.py 中修改视图,增加get方法
def get(self, request): """查询 """ # 判断用户是否是登录的 try: user = request.user except Exception: user = None if user is not None and user.is_authenticated: # 用户已登录,操作redis # 获取连接到数据库的对象 redis_conn = get_redis_connection('carts') # 查询所有的购物车数据 redis_cart = redis_conn.hgetall('cart_%s' % user.id) # { # b'sku_id':b'count', # b'sku_id': b'count' # } # 查询哪些商品被勾选 # cart_selected = [sku_id,sku_id] cart_selected = redis_conn.smembers('selected_%s' % user.id) # { # sku_id10: { # "count": 10, // 数量 # "selected": True // 是否勾选 # }, # sku_id20: { # "count": 20, # "selected": False # }, # ... # } # 将redis_cart和cart_selected内部的数据,整合到cart_dict,实现格式的统一,方便后续查询sku信息 cart_dict = {} for sku_id, count in redis_cart.items(): cart_dict[int(sku_id)] = { 'count':int(count), 'selected':sku_id in cart_selected # 当sku_id在cart_selected中,说明该商品勾选 } else: # 用户未登录,操作cookie cart_str = request.COOKIES.get('cart') if cart_str: cart_dict = pickle.loads(base64.b64decode(cart_str.encode())) else: # 表示用户从来没有在cookie中存储过购物车数据 cart_dict = {} # 根据cart_dict,查询购物车中的商品数据 sku_ids = cart_dict.keys() # 查询出id在sku_ids中的所有的商品,构造我列表 skus = SKU.objects.filter(id__in=sku_ids) # 为了能够在序列化skus的同时又序列化count和selected,就需要将他们追加到sku for sku in skus: sku.count = cart_dict[sku.id]['count'] sku.selected = cart_dict[sku.id]['selected'] # 创建序列化器对象,对skus进行序列化 serializer = serializers.CartSKUSerializer(skus, many=True) # 响应数据, 状态码200 return Response(serializer.data)
前端:修改cart.html文件,增加Vue变量
修改购物车数据
拓展知识点:幂等和非幂等
在carts/views.py中修改视图,添加put方法:
def put(self, request): """修改""" # 新建序列化器对象 serializer = serializers.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: user = None if user is not None and user.is_authenticated: # 用户已登录,操作redis # 获取连接到数据库的对象 redis_conn = get_redis_connection('carts') # 管道 pl = redis_conn.pipeline() # 修改商品的数量 : 后端使用幂等的接口设计方案,要求前端将用户最终在界面的上的结果发送给我们,直接覆盖写入。不做增量计算 pl.hset('cart_%s' % user.id, sku_id, count) # 修改商品是否勾选 if selected: pl.sadd('selected_%s' % user.id, sku_id) else: # 如果前端传入的是false,那么就把sku_id从列表中移除,表示该商品未勾选 pl.srem('selected_%s' % user.id, sku_id) # 记住:需要调用excute() pl.execute() # 响应,200 return Response(serializer.data) else: # 用户未登录,操作cookie cart_str = request.COOKIES.get('cart') if cart_str: cart_dict = pickle.loads(base64.b64decode(cart_str.encode())) else: # 表示用户从来没有在cookie中存储过购物车数据 cart_dict = {} # 使用幂等的方式,覆盖写入新的数据 cart_dict[sku_id] = { 'count':count, 'selected':selected } # 将新的购物车数据写入到cookie cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode() # 创建response对象 response = Response(serializer.data) # 将新的购物车数据写入到cookie response.set_cookie('cart', cookie_cart_str) # 响应结果 return response
修改前端代码 在cart.js中添加:
on_input: function(index){ var val = parseInt(this.cart[index].count); if (isNaN(val) || val <= 0) { this.cart[index].count = this.origin_input; } else { // 更新购物车数据 axios.put(this.host+'/cart/', { sku_id: this.cart[index].id, count: val, selected: this.cart[index].selected }, { headers:{ 'Authorization': 'JWT ' + this.token }, responseType: 'json', withCredentials: true }) .then(response => { this.cart[index].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); this.cart[index].count = this.origin_input; }) } }, // 更新购物车数据 update_count: function(index, count){ axios.put(this.host+'/cart/', { sku_id: this.cart[index].id, count, selected: this.cart[index].selected }, { headers:{ 'Authorization': 'JWT ' + this.token }, responseType: 'json', withCredentials: true }) .then(response => { this.cart[index].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); }) }, // 更新购物车数据 update_selected: function(index) { axios.put(this.host+'/cart/', { 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); }) }
删除购物车数据
在carts/serializers.py 中新建序列化器
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
在carts/views.py 中修改视图,增加delete方法
def delete(self, request): """删除""" # 创建序列化器对象 serializer = serializers.CartDeleteSerializer(data=request.data) serializer.is_valid(raise_exception=True) # 读取校验后的sku_id sku_id = serializer.validated_data.get('sku_id') # 判断用户是否是登录的 try: user = request.user except Exception: user = None if user is not None and user.is_authenticated: # 用户已登录,操作redis # 获取连接到数据库的对象 redis_conn = get_redis_connection('carts') # 管道 pl = redis_conn.pipeline() # 删除某一条记录 pl.hdel('cart_%s' % user.id, sku_id) # 移除勾选的标记 pl.srem('selected_%s' % user.id, sku_id) # 执行 pl.execute() # 响应:只响应状态码204,因为数据已经被删除,没有序列化的数据了 return Response(status=status.HTTP_204_NO_CONTENT) else: # 用户未登录,cookie操作购物车 cart_str = request.COOKIES.get('cart') if cart_str: # 用户操作过cookie购物车 # 将cart_str转成bytes,再将bytes转成base64的bytes,最后将bytes转字典 cart_dict = pickle.loads(base64.b64decode(cart_str.encode())) else: # 用户从没有操作过cookie购物车 cart_dict = {} # 创建响应对象 response = Response(status=status.HTTP_204_NO_CONTENT) if sku_id in cart_dict: del cart_dict[sku_id] # 将字典转成bytes,再将bytes转成base64的bytes,最后将bytes转字符串 cookie_cart_str = base64.b64encode(pickle.dumps(cart_dict)).decode() # 响应结果并将购物车数据写入到cookie response.set_cookie('cart', cookie_cart_str) return response
前端代码 在cart.js中增加:
// 删除购物车数据 on_delete: function(index){ axios.delete(this.host+'/cart/', { data: { sku_id: this.cart[index].id }, headers:{ 'Authorization': 'JWT ' + this.token }, responseType: 'json', withCredentials: true }) .then(response => { this.cart.splice(index, 1); }) .catch(error => { console.log(error.response.data); }) },