python 全栈开发,Day101(redis操作,购物车,DRF解析器)
昨日内容回顾
1. django请求生命周期? - 当用户在浏览器中输入url时,浏览器会生成请求头和请求体发给服务端 请求头和请求体中会包含浏览器的动作(action),这个动作通常为get或者post,体现在url之中. - url经过Django中的wsgi,再经过Django的中间件,最后url到过路由映射表,在路由中一条一条进行匹配, 一旦其中一条匹配成功就执行对应的视图函数,后面的路由就不再继续匹配了. - 视图函数根据客户端的请求查询相应的数据.返回给Django,然后Django把客户端想要的数据做为一个字符串返回给客户端. - 客户端浏览器接收到返回的数据,经过渲染后显示给用户. 1. django请求生命周期? - 当用户在浏览器中输入url时,浏览器会生成请求头和请求体发给服务端 请求头和请求体中会包含浏览器的动作(action),这个动作通常为get或者post,体现在url之中. - url经过Django中的wsgi,再经过Django的中间件,最后url到过路由映射表,在路由中一条一条进行匹配, 一旦其中一条匹配成功就执行对应的视图函数,后面的路由就不再继续匹配了. - 视图函数根据客户端的请求查询相应的数据.返回给Django,然后Django把客户端想要的数据做为一个字符串返回给客户端. - 客户端浏览器接收到返回的数据,经过渲染后显示给用户. 2. django提供的功能 - 必备 - 路由 - 视图 - 模板渲染 - django: - ORM: ... ... - 分页 - Form & ModelForm - admin - auth - session - 中间件 - contenttype - csrf - 缓存(速度块) 3. restful - restful 规范 - django rest framwork - 其他 - 跨域 a. 为什么出现跨域? b. 如何解决跨域? 使用cors,即:设置响应头。 简单请求: 响应头中设置一个允许域名访问 复杂请求: OPTIONS请求做预检,允许特殊请求方式和请求头 + 允许域名访问。 真正请求就可以发送过来进行处理 + 允许域名访问。 c. 跨域 www.baidu.com / www.luffycity.com www.baidu.com / api.luffycity.com www.baidu.com:8001 / www.baidu.com:8002 d. 路飞线上代码无跨域(项目部署时,放在同一处) - vue.js - 前端三大框架:react.js /angular.js / vue.js - vue.js 2版本 - 组件: - axios - vuex - router - 你觉得vue和jQuery的区别? - 双向绑定(数据变动,页面也随之更改) - 单页面应用(切换页面,页面不刷新)
一、redis使用
redis介绍
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它的数据,存在内存中,读写速度快!也可以做持久化。
redis安装
使用centos系统安装
yum install -y redis
redis使用
注意:redis是安装在linux系统里面的,但是python程序是运行在windows系统中的。所以需要进行远程连接!
但是,redis默认使用127.0.0.1连接,端口为6379
修改配置
编辑配置文件
vim /etc/redis.conf
修改IP,关闭保护模式(否则无法远程操作redis)
bind 192.168.218.133
protected-mode no
启动redis
注意:必须指定配置文件
redis-server /etc/redis.conf
注意,此时终端不会有输出,再开一个窗口,查看端口
netstat -anpt
信息如下:
tcp 0 0 192.168.218.133:6379 0.0.0.0:* LISTEN 3736/redis-server 1 tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 995/sshd tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 2275/master tcp 0 0 192.168.218.133:22 192.168.218.1:59646 ESTABLISHED 2575/sshd: root@not tcp 0 0 192.168.218.133:22 192.168.218.1:58928 ESTABLISHED 2500/sshd: root@pts tcp 0 0 192.168.218.133:22 192.168.218.1:55251 ESTABLISHED 3739/sshd: root@pts tcp6 0 0 :::3306 :::* LISTEN 2220/mysqld tcp6 0 0 :::22 :::* LISTEN 995/sshd tcp6 0 0 ::1:25 :::* LISTEN 2275/master
第一个就是redis,端口为6379
注意要关闭防火墙
/etc/init.d/iptables stop
redis相当于是一个在内存中的创建的大字典
redis的value有5大数据类型:字符串,哈希,列表,集合,有序集合
字符串(String)
Redis 字符串数据类型的相关命令用于管理 redis 字符串值,基本语法如下:
COMMAND KEY_NAME
set()
get(name)
分别表示设置和获取
举例:
写一个字符串,并获取
import redis conn = redis.Redis(host='192.168.218.133',port='6379') conn.set('name','xiao') # 写入字符串 val = conn.get('name') # 获取字符串 print(val)
执行程序,输出如下:
b'xiao' xiao
注意:它的返回结果是bytes,那么使用decode('utf-8')解码之后,就会变成字符串
哈希(Hash)
Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)
redis中的Hash 在内存中类似于一个name对应一个dic来存储
hset(name, key, value)
name对应的hash中设置一个键值对(不存在,则创建,否则,修改)
hget(name,key)
在name对应的hash中根据key获取value
举例:
import redis conn = redis.Redis(host='192.168.218.133',port='6379') conn.hset("dic_name","a1","aa") # 写入字典 val = conn.hget("dic_name","a1") # 获取key为a1的值 print(val) print(val.decode('utf-8')) # 解码
执行输出:
b'aa' aa
hgetall(name)
获取name对应hash的所有键值
举例:
import redis conn = redis.Redis(host='192.168.218.133',port='6379') val = conn.hgetall("dic_name") # 获取dic_name的所有值 print(val)
执行输出:
{b'a1': b'aa'}
hmset(name, mapping)
在name对应的hash中批量设置键值对,mapping:字典
hmget(name, keys, *args)
在name对应的hash中获取多个key的值
举例:
import redis conn = redis.Redis(host='192.168.218.133',port='6379') dic={"a1":"aa","b1":"bb"} # 定义一个字典 conn.hmset("dic_name",dic) # 批量设置键值对 val_1 = conn.hget("dic_name","b1") # 获取key为b1的值 val_2 = conn.hmget("dic_name","a1","b1") # 获取多个值 print(val_1) print(val_1.decode('utf-8')) # 解码 print(val_2)
执行输出:
b'bb' bb [b'aa', b'bb']
列表(List)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
redis中的List在在内存中按照一个name对应一个List来存储
lpush(name,values)
llen(name)
lindex(name, index)
举例:
import redis conn = redis.Redis(host='192.168.142.129',port='6379') conn.lpush("list_name",2)# 在list_name中增加一个值2 print(conn.llen("list_name")) # 获取列表元素的个数 val = conn.lindex("list_name",0) #根据索引获取列表内元素 print(val)
执行输出:
1 b'2'
集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
sadd(name,values)
smembers(name)
scard(name)
举例
import redis conn = redis.Redis(host='192.168.142.129',port='6379') conn.sadd("set_name","aa") # 在集合set_name中增加元素 conn.sadd("set_name","aa","bb") print(conn.smembers("set_name")) # 获取set_name集合的所有成员 val = conn.scard("set_name") #获取set_name集合中的元素个数 print(val)
执行输出:
{b'bb', b'aa'} 2
有序集合(sorted set)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
zadd(name, *args, **kwargs)
zcard(name)
zcount(name, min, max)
举例:
import redis conn = redis.Redis(host='192.168.142.129',port='6379') conn.zadd("zset_name", "a1", 6, "a2", 2,"a3",5) # 在有序集合zset_name中增加元素 # 或者使用下面的方式,效果同上! # conn.zadd('zset_name1', b1=10, b2=5) print(conn.zcard("zset_name")) # 获取有序集合内元素的数量 val = conn.zcount("zset_name",1,5) #获取有序集合中分数在[min,max]之间的个数 print(val)
执行输出:
3 2
总结:
a. 五大数据类型:
字符串,哈希,列表,集合,有序集合
b. 列举每种数据类型的操作
字符串:
set
get
字典:
hget
hgetall
hset
hmset
hdel
其他:
delete
expire
keys
flushall()
更多redis操作,请参考以下文章
http://www.runoob.com/redis/redis-lists.html
http://www.cnblogs.com/melonjiang/p/5342505.html
二、购物车
下载代码:
https://github.com/987334176/luffycity/archive/v1.3.zip
下载数据库使用(务必下载,上面的压缩包数据库是空的!!!)
https://github.com/987334176/luffycity/blob/master/db.sqlite3
进入api目录,务必删除views.py,它已经没有用了
先来看一个购物车步骤
1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR
修改api_urls.py
from django.conf.urls import url from api.views import course,degreecourse,auth,shoppingcart urlpatterns = [ url(r'auth/$', auth.AuthView.as_view({'post':'login'})), url(r'courses/$',course.CoursesView.as_view()), url(r'courses/(?P<pk>\d+)/$',course.CourseDetailView.as_view()), url(r'shoppingcart/$', shoppingcart.ShoppingCartView.as_view({'get':'list','post':'create'})), ]
修改views目录下的shoppingcart.py
from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models class ShoppingCartView(ViewSetMixin,APIView): def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID print('要加入购物车了') print(request.body,type(request.body)) print(request.data, type(request.data)) return Response({'code':1000})
使用postman发送json数据
查看返回信息
查看Pycharm控制台输出:
要加入购物车了 b'{"courseid":"1","policyid":"2"}' <class 'bytes'> {'courseid': '1', 'policyid': '2'} <class 'dict'>
可以发现body的数据是bytes类型的。那么request.data的数据,怎么就成字典了呢?
假设抛开request.data。使用request.body的数据,解析成字典。需要经历2个步骤:
1.将数据使用decode('utf-8'),进行解码得到字符串
2.将字符串使用json.load('value'),反序列化成字典。
那么rest framework就自动帮你做了这件事情!详情看下面的内容。
三、DRF解析器
1.Parser对象
REST框架提供了一系列的内建Parser对象来对不同的媒体类型进行解析,也支持为API接口灵活的自定义Parser
如何选择合适的Parser
通常为一个viewset定义一个用于解析的Parser对象列表
当接收到request.data时,REST框架首先检查请求头的Content-Type字段,然后决定使用哪种解析器来处理请求内容
注意:
当你编写客户端应用程序时,发送HTTP请求时,一定要在请求头中设置Content-Type。
如果你没有设置这个属性,大多数客户端默认使用’application/x-www-form-urlencoded’,但这有时并不是你想要的。
例如当你用jQuery的ajax方法发送一个json编码的数据时,应该确保包含contentType: ‘application/json’设置。
设置默认的解析器
REST_FRAMEWORK = { 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', ) }
也可以为基于APIView的单个视图类或者视图集合设置自己的Parser
from rest_framework.parsers import JSONParser from rest_framework.response import Response from rest_framework.views import APIView class ExampleView(APIView): """ 一个能处理post提交的json数据的视图类 """ parser_classes = (JSONParser,) def post(self, request, format=None): return Response({'received data': request.data})
使用装饰器的视图函数:
from rest_framework.decorators import api_view from rest_framework.decorators import parser_classes # 注意装饰器顺序 @api_view(['POST']) @parser_classes((JSONParser,)) def example_view(request, format=None): """ A view that can accept POST requests with JSON content. """ return Response({'received data': request.data})
举例:
修改views目录下的shoppingcart.py,使用解析器
JSONParser对应的数据类型为application/json
FormParser对应的数据类型为application/x-www-form-urlencoded
from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models from rest_framework.parsers import JSONParser,FormParser class ShoppingCartView(ViewSetMixin,APIView): parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID print('要加入购物车了') # print(request.body,type(request.body)) print(request._request, type(request._request)) # 原生django的request print(request._request.body) # 获取body print(request._request.POST) # 获取post print(request.data, type(request.data)) # 封装后的数据 return Response({'code':1000})
使用postman再次发送,查看Pycharm控制台输出:
<WSGIRequest: POST '/api/v1/shoppingcart/'> <class 'django.core.handlers.wsgi.WSGIRequest'> b'{"courseid":"1","policyid":"2"}' <QueryDict: {}> {'policyid': '2', 'courseid': '1'} <class 'dict'>
从上面的信息中,可以看出。原生的django通过body可以获取数据,但是post的数据是空的。因为客户端的请求数据类型不是
application/x-www-form-urlencoded
而经过rest framework封装之后,可以从data中获取数据,并解析成字典了!
查看APIView源码
class APIView(View): # The following policies may be set at either globally, or per-view. renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES parser_classes = api_settings.DEFAULT_PARSER_CLASSES authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS metadata_class = api_settings.DEFAULT_METADATA_CLASS versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
看这一句,它默认会从settings.py中查找解析器
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
如果需要指定默认的解析器,修改settings.py
REST_FRAMEWORK = { 'DEFAULT_VERSIONING_CLASS':'rest_framework.versioning.URLPathVersioning', 'VERSION_PARAM':'version', 'DEFAULT_VERSION':'v1', 'ALLOWED_VERSIONS':['v1','v2'], 'PAGE_SIZE':20, 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', ) }
修改views目录下的shoppingcart.py,注释掉解析器
from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models # from rest_framework.parsers import JSONParser,FormParser class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID print('要加入购物车了') # print(request.body,type(request.body)) print(request._request, type(request._request)) # 原生django的request print(request._request.body) # 获取body print(request._request.POST) # 获取post print(request.data, type(request.data)) # 封装后的数据 return Response({'code':1000})
使用postman再次发送,效果同上!
关于DRF解析器的源码解析,请参考文章
http://www.cnblogs.com/derek1184405959/p/8724455.html
注意:一般在前后端分离的架构中,前端约定俗成发送json数据,后端接收并解析数据!
解析器到这里就结束了,下面继续讲购物车
判断课程id是否合法
修改views目录下的shoppingcart.py,修改post方法
import redis from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models CONN = redis.Redis(host='192.168.142.129',port=6379) class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) return Response({'code':1000})
使用postman发送json数据
查看返回信息
数据放入redis
为什么要将购物车数据,放到redis中呢?
因为购物车的操作比较频繁,它是一个临时数据。用户付款后,数据就删除了。
如果使用数据库,速度太慢,影响用户体验!
购物车数据结构
shopping_car_用户id_课程id:{ id:课程ID name:课程名称 img:课程图片 defaut:默认选中的价格策略 # 所有价格策略 price_list:[ {'策略id':'价格'}, {'策略id':'价格'}, ... ] },
为什么要这么设计呢?
其中我们可以使用3层字典嵌套,来展示用户-->课程id-->价格策略
但是redis不支持字典嵌套,所以这样设计,是为了减少字典嵌套。注意:所有价格策略,存的是json数据!
判断价格策略id是否合法
修改views目录下的shoppingcart.py,修改post方法
import redis from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models CONN = redis.Redis(host='192.168.142.129',port=6379) class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) # 2.2 价格策略是否合法? # 查看当前课程所有价格策略 price_policy_queryset = course.price_policy.all() price_policy_dict = {} # 空字典 for item in price_policy_queryset: temp = { 'id': item.id, # 价格策略id 'price': item.price, # 价格 'valid_period': item.valid_period, # 有效期 'valid_period_display': item.get_valid_period_display() # 有效期中文 } price_policy_dict[item.id] = temp # 循环加入到空字典中 # policy_id类型必须为数字,否则即使存在,这里也会提示价格策略不存在 if policy_id not in price_policy_dict: # 判断价格策略是否存在 return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'}) return Response({'code':1000})
使用postman再次发送,效果同上!
发送一个不存在的价格策略id
查看返回值
商品信息存入redis
修改views目录下的shoppingcart.py
import redis import json from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models CONN = redis.Redis(host='192.168.142.129',port=6379) USER_ID = 1 # 固定用户id class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ return Response('ok') def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) # 2.2 价格策略是否合法? # 查看当前课程所有价格策略 price_policy_queryset = course.price_policy.all() price_policy_dict = {} # 空字典 for item in price_policy_queryset: temp = { 'id': item.id, # 价格策略id 'price': item.price, # 价格 'valid_period': item.valid_period, # 有效期 'valid_period_display': item.get_valid_period_display() # 有效期中文 } price_policy_dict[item.id] = temp # 循环加入到空字典中 # policy_id类型必须为数字,否则即使存在,这里也会提示价格策略不存在 if policy_id not in price_policy_dict: # 判断价格策略是否存在 return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'}) # 3. 把商品和价格策略信息放入购物车 pattern = 'shopping_car_%s_%s' % (USER_ID, '*',) # key的格式 keys = CONN.keys(pattern) # 搜索key,比如:shopping_car_1_* *表示模糊匹配 if keys and len(keys) >= 1000: # 如果key的长度大于1000。意思就是买了1000门课程 return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'}) key = "shopping_car_%s_%s" %(USER_ID,course_id,) # 单个课程 CONN.hset(key, 'id', course_id) # 存入课程id CONN.hset(key, 'name', course.name) CONN.hset(key, 'img', course.course_img) CONN.hset(key, 'default_price_id', policy_id) # 由于价格策略有很多个,需要json一下 CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict)) CONN.expire(key, 60*60*24) # key的有效期为24小时 return Response({'code': 10000, 'data': '购买成功'})
发送一个正确的值
查看返回结果
使用xhsell登录redis,查看所有的key
使用命令:keys *
127.0.0.1:6379> keys * 1) "shopping_car_1_1"
查看key的所有信息
使用命令: hgetall shopping_car_1_1
127.0.0.1:6379> hgetall shopping_car_1_1 1) "default_price_id" 2) "2" 3) "name" 4) "Python\xe5\xbc\x80\xe5\x8f\x91\xe5\x85\xa5\xe9\x97\xa87\xe5\xa4\xa9\xe7\x89\xb9\xe8\xae\xad\xe8\x90\xa5" 5) "id" 6) "1" 7) "price_policy_dict" 8) "{\"1\": {\"valid_period_display\": \"1\\u5468\", \"valid_period\": 7, \"price\": 10.0, \"id\": 1}, \"2\": {\"valid_period_display\": \"1\\u4e2a\\u6708\", \"valid_period\": 30, \"price\": 50.0, \"id\": 2}}" 9) "img" 10) "Python\xe5\xbc\x80\xe5\x8f\x91\xe5\x85\xa5\xe9\x97\xa8"
再购买一个课程
注意:价格策略id是唯一的,看价格策略表
这里展示的价格策略id,就是价格策略表的主键id
object_id 表示course表的主键id,表示具体哪门课程。
content_type_id为8,表示course表。为什么8就是course表呢?
查看django_content_type表,因为主键id为8的。就是course表!
查看所有key
127.0.0.1:6379> keys * 1) "shopping_car_1_2" 2) "shopping_car_1_1"
查看购物车记录
修改views目录下的shoppingcart.py
import redis import json from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models from api.utils.serialization_general import SerializedData CONN = redis.Redis(host='192.168.142.129',port=6379) USER_ID = 1 # 固定用户id KEY_prefix = 'shopping_car' # 购物车key的前缀 class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ ret = {'code': 10000, 'data': None, 'error': None} # 状态字典 try: shopping_car_course_list = [] # 空列表 pattern = "%s_%s_*" % (KEY_prefix,USER_ID,) # 默认匹配用户的购物车 user_key_list = CONN.keys(pattern) for key in user_key_list: temp = { 'id': CONN.hget(key, 'id').decode('utf-8'), # 解码 'name': CONN.hget(key, 'name').decode('utf-8'), 'img': CONN.hget(key, 'img').decode('utf-8'), 'default_price_id': CONN.hget(key, 'default_price_id').decode('utf-8'), # 先解码,再反序列化 'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8')) } shopping_car_course_list.append(temp) ret['data'] = shopping_car_course_list # 状态字典增加key except Exception as e: ret['code'] = 10005 ret['error'] = '获取购物车数据失败' return Response(ret) def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) # 2.2 价格策略是否合法? # 查看当前课程所有价格策略 price_policy_queryset = course.price_policy.all() price_policy_dict = {} # 空字典 for item in price_policy_queryset: temp = { 'id': item.id, # 价格策略id 'price': item.price, # 价格 'valid_period': item.valid_period, # 有效期 'valid_period_display': item.get_valid_period_display() # 有效期中文 } price_policy_dict[item.id] = temp # 循环加入到空字典中 # policy_id类型必须为数字,否则即使存在,这里也会提示价格策略不存在 if policy_id not in price_policy_dict: # 判断价格策略是否存在 return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'}) # 3. 把商品和价格策略信息放入购物车 pattern = '%s_%s_%s' % (KEY_prefix,USER_ID, '*',) # key的格式 keys = CONN.keys(pattern) # 搜索key,比如:shopping_car_1_* *表示模糊匹配 if keys and len(keys) >= 1000: # 如果key的长度大于1000。意思就是买了1000门课程 return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'}) key = "%s_%s_%s" %(KEY_prefix,USER_ID,course_id,) # 单个课程 CONN.hset(key, 'id', course_id) # 存入课程id CONN.hset(key, 'name', course.name) CONN.hset(key, 'img', course.course_img) CONN.hset(key, 'default_price_id', policy_id) # 由于价格策略有很多个,需要json一下 CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict)) CONN.expire(key, 60*60*24) # key的有效期为24小时 return Response({'code': 10000, 'data': '购买成功'})
使用postman发送get请求,不需要参数
购物车删除
删除购物车,需要传入一个课程id。通过url传参就可以了
修改api_urls.py,增加delete
from django.conf.urls import url from api.views import course,degreecourse,auth,shoppingcart urlpatterns = [ url(r'auth/$', auth.AuthView.as_view({'post':'login'})), url(r'courses/$',course.CoursesView.as_view()), url(r'courses/(?P<pk>\d+)/$',course.CourseDetailView.as_view()), url(r'shoppingcart/$', shoppingcart.ShoppingCartView.as_view({'get':'list','post':'create','delete':'destroy'})), ]
修改views目录下的shoppingcart.py
import redis import json from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models from api.utils.response import BaseResponse CONN = redis.Redis(host='192.168.142.129',port=6379) USER_ID = 1 # 固定用户id KEY_PREFIX = 'shopping_car' # 购物车key的前缀 class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ ret = {'code': 10000, 'data': None, 'error': None} # 状态字典 try: shopping_car_course_list = [] # 空列表 pattern = "%s_%s_*" % (KEY_PREFIX,USER_ID,) # 默认匹配用户的购物车 user_key_list = CONN.keys(pattern) for key in user_key_list: temp = { 'id': CONN.hget(key, 'id').decode('utf-8'), # 解码 'name': CONN.hget(key, 'name').decode('utf-8'), 'img': CONN.hget(key, 'img').decode('utf-8'), 'default_price_id': CONN.hget(key, 'default_price_id').decode('utf-8'), # 先解码,再反序列化 'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8')) } shopping_car_course_list.append(temp) ret['data'] = shopping_car_course_list # 状态字典增加key except Exception as e: ret['code'] = 10005 ret['error'] = '获取购物车数据失败' return Response(ret) def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) # 2.2 价格策略是否合法? # 查看当前课程所有价格策略 price_policy_queryset = course.price_policy.all() price_policy_dict = {} # 空字典 for item in price_policy_queryset: temp = { 'id': item.id, # 价格策略id 'price': item.price, # 价格 'valid_period': item.valid_period, # 有效期 'valid_period_display': item.get_valid_period_display() # 有效期中文 } price_policy_dict[item.id] = temp # 循环加入到空字典中 # policy_id类型必须为数字,否则即使存在,这里也会提示价格策略不存在 if policy_id not in price_policy_dict: # 判断价格策略是否存在 return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'}) # 3. 把商品和价格策略信息放入购物车 pattern = '%s_%s_%s' % (KEY_PREFIX,USER_ID, '*',) # key的格式 keys = CONN.keys(pattern) # 搜索key,比如:shopping_car_1_* *表示模糊匹配 if keys and len(keys) >= 1000: # 如果key的长度大于1000。意思就是买了1000门课程 return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'}) key = "%s_%s_%s" %(KEY_PREFIX,USER_ID,course_id,) # 单个课程 CONN.hset(key, 'id', course_id) # 存入课程id CONN.hset(key, 'name', course.name) CONN.hset(key, 'img', course.course_img) CONN.hset(key, 'default_price_id', policy_id) # 由于价格策略有很多个,需要json一下 CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict)) CONN.expire(key, 60*60*24) # key的有效期为24小时 return Response({'code': 10000, 'data': '购买成功'}) def destroy(self, request, *args, **kwargs): """ 删除购物车中的某个课程 :param request: :param args: :param kwargs: :return: """ response = BaseResponse() try: courseid = request.GET.get('courseid') # 获取课程id key = "%s_%s_%s" % (KEY_PREFIX,USER_ID,courseid) # 获取redis中的课程id CONN.delete(key) # 删除单个key response.data = '删除成功' except Exception as e: response.code = 10006 response.error = '删除失败' return Response(response.dict)
使用postman,发送带参数的get请求
提示删除成功
查看购物车,发现只有一个课程
修改价格策略
这里只要选择了一个价格策略,会发送一个ajax请求。后端会修改redis中的数据
修改用户购物车的默认价格策略id
修改api_urls.py,增加put
from django.conf.urls import url from api.views import course,degreecourse,auth,shoppingcart urlpatterns = [ url(r'auth/$', auth.AuthView.as_view({'post':'login'})), url(r'courses/$',course.CoursesView.as_view()), url(r'courses/(?P<pk>\d+)/$',course.CourseDetailView.as_view()), url(r'shoppingcart/$', shoppingcart.ShoppingCartView.as_view({'get':'list','post':'create','delete':'destroy','put':'update'})), ]
修改views目录下的shoppingcart.py
import redis import json from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from api import models from api.utils.response import BaseResponse CONN = redis.Redis(host='192.168.142.129',port=6379) USER_ID = 1 # 固定用户id KEY_PREFIX = 'shopping_car' # 购物车key的前缀 class ShoppingCartView(ViewSetMixin,APIView): # parser_classes = [JSONParser,FormParser] # 指定解析器 def list(self, request, *args, **kwargs): """ 查看购物车信息 :param request: :param args: :param kwargs: :return: """ ret = {'code': 10000, 'data': None, 'error': None} # 状态字典 try: shopping_car_course_list = [] # 空列表 pattern = "%s_%s_*" % (KEY_PREFIX,USER_ID,) # 默认匹配用户的购物车 user_key_list = CONN.keys(pattern) for key in user_key_list: temp = { 'id': CONN.hget(key, 'id').decode('utf-8'), # 解码 'name': CONN.hget(key, 'name').decode('utf-8'), 'img': CONN.hget(key, 'img').decode('utf-8'), 'default_price_id': CONN.hget(key, 'default_price_id').decode('utf-8'), # 先解码,再反序列化 'price_policy_dict': json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8')) } shopping_car_course_list.append(temp) ret['data'] = shopping_car_course_list # 状态字典增加key except Exception as e: ret['code'] = 10005 ret['error'] = '获取购物车数据失败' return Response(ret) def create(self,request,*args,**kwargs): """ 加入购物车 :param request: :param args: :param kwargs: :return: """ """ 1. 接受用户选中的课程ID和价格策略ID 2. 判断合法性 - 课程是否存在? - 价格策略是否合法? 3. 把商品和价格策略信息放入购物车 SHOPPING_CAR 注意:用户ID=1 """ # 1. 接受用户选中的课程ID和价格策略ID course_id = request.data.get('courseid') policy_id = request.data.get('policyid') if course_id.isdigit(): # 判断是否为数字 policy_id = int(policy_id) else: return Response({'code': 10001, 'error': '课程非法'}) # 2. 判断合法性 # - 课程是否存在? # - 价格策略是否合法? # 2.1 课程是否存在? course = models.Course.objects.filter(id=course_id).first() if not course: return Response({'code': 10001, 'error': '课程不存在'}) # 2.2 价格策略是否合法? # 查看当前课程所有价格策略 price_policy_queryset = course.price_policy.all() price_policy_dict = {} # 空字典 for item in price_policy_queryset: temp = { 'id': item.id, # 价格策略id 'price': item.price, # 价格 'valid_period': item.valid_period, # 有效期 'valid_period_display': item.get_valid_period_display() # 有效期中文 } price_policy_dict[item.id] = temp # 循环加入到空字典中 # policy_id类型必须为数字,否则即使存在,这里也会提示价格策略不存在 if policy_id not in price_policy_dict: # 判断价格策略是否存在 return Response({'code': 10002, 'error': '傻×,价格策略别瞎改'}) # 3. 把商品和价格策略信息放入购物车 pattern = '%s_%s_%s' % (KEY_PREFIX,USER_ID, '*',) # key的格式 keys = CONN.keys(pattern) # 搜索key,比如:shopping_car_1_* *表示模糊匹配 if keys and len(keys) >= 1000: # 如果key的长度大于1000。意思就是买了1000门课程 return Response({'code': 10009, 'error': '购物车东西太多,先去结算再进行购买..'}) key = "%s_%s_%s" %(KEY_PREFIX,USER_ID,course_id,) # 单个课程 CONN.hset(key, 'id', course_id) # 存入课程id CONN.hset(key, 'name', course.name) CONN.hset(key, 'img', course.course_img) CONN.hset(key, 'default_price_id', policy_id) # 由于价格策略有很多个,需要json一下 CONN.hset(key, 'price_policy_dict', json.dumps(price_policy_dict)) CONN.expire(key, 60*60*24) # key的有效期为24小时 return Response({'code': 10000, 'data': '购买成功'}) def destroy(self, request, *args, **kwargs): """ 删除购物车中的某个课程 :param request: :param args: :param kwargs: :return: """ response = BaseResponse() try: courseid = request.GET.get('courseid') # 获取课程id key = "%s_%s_%s" % (KEY_PREFIX,USER_ID,courseid) # 获取redis中的课程id CONN.delete(key) # 删除单个key response.data = '删除成功' except Exception as e: response.code = 10006 response.error = '删除失败' return Response(response.dict) def update(self, request, *args, **kwargs): """ 修改用户选中的价格策略 :param request: :param args: :param kwargs: :return: """ """ 1. 获取课程ID、要修改的价格策略ID 2. 校验合法性(去redis中) """ response = BaseResponse() try: course_id = request.data.get('courseid') policy_id = request.data.get('policyid') key = '%s_%s_%s' %(KEY_PREFIX,USER_ID,course_id,) # 获取用户购物车中的单个课程 if not CONN.exists(key): # 判断key是否存在 response.code = 10007 response.error = '课程不存在' return Response(response.dict) # 获取所有的价格策略。先解码,再反序列化。最终是一个字典 price_policy_dict = json.loads(CONN.hget(key, 'price_policy_dict').decode('utf-8')) # 由于反序列化之后,字段的key-value都强制转换为字符串了 # 所以上面获取到的价格策略id必须转换为字符串,才能使用下面的not in 判断 policy_id = str(policy_id) if policy_id not in price_policy_dict: # 判断价格策略id是否存在 response.code = 10008 response.error = '价格策略不存在' return Response(response.dict) CONN.hset(key, 'default_price_id', policy_id) # 修改默认的价格策略id CONN.expire(key, 60*60*24) # 重新设置有效期为24小时,之前的有效期会被覆盖! response.data = '修改成功' except Exception as e: response.code = 10009 response.error = '修改失败' return Response(response.dict)
使用postman发送put请求,注意带上参数
查看返回值
总结:
a. 为什么要把购物车信息放到redis中? - 查询频繁 - 课程是否存在? - 价格策略是否合法? - 中间状态 - 购买成功之后,需要删除。 - 购物车信息删除 b. 购物车有没有数量限制? 使用 keys 查看个数做判断,限制为1000。 如果不限制,会导致redis内存占满,导致内存溢出! c. 购物车的结构 shopping_car_用户id_课程id:{ id:课程ID name:课程名称 img:课程图片 defaut:默认选中的价格策略 # 所有价格策略 price_list:[ {'策略id':'价格'}, {'策略id':'价格'}, ... ] }, d. 对于字典的key,序列化会将数字转换成字符串 比如: info = {1:'xiao',2:'zhang'} new = json.dumps(info) # 结果为 '{"1":"alex", "2":"于超"}' data = json.loads(new) # 结果为 {"1":"alex", "2":"于超"}
作业:
1. 虚拟机安装上redis,redis服务启动 2. 购物车,完成以下功能: - 添加到购物车 - 查看购物车信息 - 删除课程 - 修改课程