路飞学城项目之加入购物车接口
路飞学城项目之加入购物车接口
购物车的 Redis 结构设计
存储结构设计
第一种
以 luffy_shopping_car 作为外键,里面是用户购物车字典,字典的键为用户名 ID ,字典的值是课程字典,字典的键是课 ID,字典的值是课程信息。
{
luffy_shopping_car:{
6:{
11:{
'title':'21天入门到放弃',
'src':'xxx.png'
},
12:{
'title':'21天入门到放弃',
'src':'xxx.png'
}
}
}
}
第二种
以 luffy_shopping_car_用户ID_课程ID
为外键,里面为课程信息。
{
luffy_shopping_car_6_11:{
'title':'21天入门到放弃',
'src':'xxx.png'
},
luffy_shopping_car_6_12:{
'title':'21天入门到放弃',
'src':'xxx.png'
},
luffy_shopping_car_6_14:{
'title':'21天入门到放弃',
'src':'xxx.png'
}
}
最后选择第二种,因为此版本更方便进行删除与更改。
Redis 中的购物车操作
引入 redis 包,并连接。
import redis
conn = redis.Redis(host="127.0.0.1", port=6379)
- 删除所有键
conn.flushall()
- 添加课程
redis_key = "luffy_shopping_car_%s_%s"%(6, 13) conn.hmset(redis_key, {"title":"23天从入门到放弃", "src":"x3.png"})
- 删除课程
conn.delete('luffy_shopping_car_7_12')
- 修改课程
conn.hset('luffy_shopping_car_6_12', 'src', 'x1.png')
- 查看所有课程
for item in conn.scan_iter("luffy_shopping_car_6_*", count=10): course = conn.hgetall(item)
- 查看课程中的某个信息
course_title = conn.hget("luffy_shopping_car_6_12", 'title')
接口逻辑
前期工作
配置文件设置
settings.py 文件中:
用 redis 作为缓存的配置
# 用redis作为缓存的配置
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100},
# "PASSWORD": "1234", # 有时候还需要密码
}
}
}
购物车关键字的设置
# 购物车关键字的设置
SHOPPING_CAR_KEY = "luffy_shopping_car_%s_%s"
URL 设置
from api.views import shoppingcar
url(r'^shoppingcar/$', shoppingcar.ShoppingCarViewSet.as_view()),
登录认证
基于 rest framework 的认证类,文件为 api/auth/auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from api import models
# 登录认证
class LufyyAuth(BaseAuthentication):
def authenticate(self, request):
token = request.query_params.get('token')
obj = models.UserToken.objects.filter(token=token).first()
if not obj:
raise AuthenticationFailed({'code':1001, 'error':'认证失败'})
return (obj.user.user, obj)
响应体基础类
用于返回数据的封装。文件为 api/utils/response.py
class BaseResponse(object):
"""
响应体基础类
"""
def __init__(self):
self.data = None
self.code = 1000
self.error = None
@property
def dict(self):
return self.__dict__
购物车逻辑实现
所用到的包。
from utils.response import BaseResponse
from api.auth.auth import LufyyAuth
from api import models
from utils.exception import PricePolicyInvalid
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
import json
from django_redis import get_redis_connection
认证类和链接
先将 rest framework 的认证类包含进去并连接上 redis 。
class ShoppingCarViewSet(APIView):
authentication_classes = [LufyyAuth, ]
conn = get_redis_connection("default")
添加到购物车
使用的是 post 方法。而且整个逻辑应该放到 try expect 模块中处理。
def post(self, request, *args, **kwargs):
ret = BaseResponse()
...
- 获取用户提交的课程ID和价格策略
course_id = int(request.data.get('courseid'))
policy_id = int(request.data.get('policyid'))
- 获取专题课信息
course = models.Course.objects.get(id=course_id)
此时如果得不到相应的课程对象,应该抛出 ObjectDoesNotExist 异常。
except ObjectDoesNotExist as e:
ret.code = 2001
ret.error = "课程不存在"
- 获得该课程相关的所有价格策略
得到课程的所有价格策略并放到一个字典中。
price_policy_list = course.price_policy.all()
price_policy_dict = {}
for price_policy_item in price_policy_list:
price_policy_dict[price_policy_item.id] = {
"period": price_policy_item.valid_period,
"period_display": price_policy_item.get_valid_period_display(),
"price": price_policy_item.price
}
- 判断用户提交的价格策略是否合法
如果不合法就抛出异常,此处的 PricePolicyInvalid 异常是自己定义的:
class PricePolicyInvalid(Exception):
def __init__(self, msg):
self.msg = msg
if policy_id not in price_policy_dict:
# 价格策略不合法
raise PricePolicyInvalid("价格策略不合法")
except PricePolicyInvalid as e:
ret.code = 2001
ret.error = e.msg
- 将购物信息添加到redis中
应该在此处建立之前提到过得 redis 购物车数据结构。
car_key = settings.SHOPPING_CAR_KEY % (request.auth.user_id, course_id, )
car_dict = {
"title": course.title,
"img": course.course_img,
"default_policy": policy_id,
"policy": json.dumps(price_policy_dict)
}
self.conn.hmset(car_key, car_dict)
ret.data = "添加成功"
最后,如果还发生其他异常,进行捕获并返回响应体。
except Exception as e:
ret.code = 1001
ret.error = "获取购物车失败"
return Response(ret.dict)
从购物车中删除课程
删除使用的是 delete 方法。
从前端获取所有课程的 ID ,并将这些 ID 拼成 SHOPPING_CAR_KEY 的形式,组成一个列表,直接用 delete(*[])
的方式删除所有键值。
def delete(self, request, *args, **kwargs):
"""
删除购物车中的课程
:param request:
:param args:
:param kwargs:
:return:
"""
ret = BaseResponse()
try:
course_id_list = request.data.get('courseids')
key_list = [settings.SHOPPING_CAR_KEY%(request.auth.user_id, course_id, ) for course_id in course_id_list]
self.conn.delete(*key_list)
except Exception as e:
ret.code = 1002
ret.error = "删除失败"
修改购物车的课程
使用的是 patch 方法。
- 获取价格策略ID和课程ID
course_id = int(request.data.get('courseid'))
policy_id = str(request.data.get('policyid'))
- 拼接课程的 key
有错的时候可以通过异常抛出的方式来处理,还可以直接返回 return ,此处用的是第二种方式。
key = settings.SHOPPING_CAR_KEY%(request.auth.user_id, course_id)
if not self.conn.exists(key):
ret.code = 1002
ret.error = "购物车中不存在此课程"
return Response(ret.dict)
- redis中获取所有的价格策略
policy_dict = json.loads(self.conn.hget(key, 'policy').decode('utf-8'))
if policy_id not in policy_dict:
ret.code = 1003
ret.error = "价格策略不合法"
return Response(ret.dict)
- 在购物车中修改该课程的默认价格策略
self.conn.hset(key, 'default_policy', policy_id)
ret.data = "修改成功"
完整代码:
def patch(self, request, *args, **kwargs):
"""
修改购物车的课程
:param request:
:param args:
:param kwargs:
:return:
"""
ret = BaseResponse()
try:
# 1. 获取价格策略ID和课程ID
course_id = int(request.data.get('courseid'))
policy_id = str(request.data.get('policyid'))
# 2. 拼接课程的key
key = settings.SHOPPING_CAR_KEY%(request.auth.user_id, course_id)
if not self.conn.exists(key):
ret.code = 1002
ret.error = "购物车中不存在此课程"
return Response(ret.dict)
# 3. redis中获取所有的价格策略
policy_dict = json.loads(self.conn.hget(key, 'policy').decode('utf-8'))
if policy_id not in policy_dict:
ret.code = 1003
ret.error = "价格策略不合法"
return Response(ret.dict)
# 4. 在购物车中修改该课程的默认价格策略
self.conn.hset(key, 'default_policy', policy_id)
ret.data = "修改成功"
except Exception as e:
ret = 1004
ret.error = "修改失败"
return Response(ret.dict)
查看购物车中的所有课程
使用的是 get 方法。
使用通配符来构建该用户的所有课程 key ,然后通过 scan_iter 函数进行遍历,并返回所有课程信息。
def get(self, request, *args, **kwargs):
"""
查看购物车中的所有商品
:param request:
:param args:
:param kwargs:
:return:
"""
ret = BaseResponse()
try:
key_match = settings.SHOPPING_CAR_KEY%(request.auth.user_id, "*")
course_list = []
for key in self.conn.scan_iter(key_match, count=10):
info = {
"title": self.conn.hget(key, "title").decode('utf-8'),
"img": self.conn.hget(key, "img").decode('utf-8'),
"policy": json.loads(self.conn.hget(key, "policy").decode('utf-8')),
"default_policy": self.conn.hget(key, "default_policy").decode('utf-8'),
}
course_list.append(info)
ret.data = course_list
except Exception as e:
ret.code = 1002
ret.error = "获取失败"
return Response(ret.dict)
GitHub 地址:https://github.com/protea-ban/oldboy/tree/master/s9day112