03 - rest framework - v1.3 、v1.4、v1.5、v1.6 - 接口开发
一、README
线上教育平台:
接口得开发
1.建表models.py
2.引用admin
3.createsuperuser root root!2345
4.CC视频上传(账号),video.html显示
https://admin.bokecc.com/main.bo
http://127.0.0.1:8000/ccvideo/ (可看到上传得视频)
5.v1.3 - 接口: (与登录 课程 文章相关得接口)
1.查询所有课程
http://127.0.0.1:8000/api/v1/courses/
http://127.0.0.1:8000/api/v1/courses/?page=2 (全局设置得分页)
http://127.0.0.1:8000/api/v1/course/
http://127.0.0.1:8000/api/v1/course/?page=2 (自定义设置得分页)
2.查询课程详细
http://127.0.0.1:8000/api/v1/courses/1/ (简单得内容,继承)
http://127.0.0.1:8000/api/v1/course/1/ (自定制)
3.用户登录
http://127.0.0.1:8000/api/v1/auth/
- 返回随机字符串token
uid = str(uuid.uuid4())
- 库里update_or_create 新增或修改token
models.UserToken.objects.update_or_create(user=user_obj,defaults={'token':uid})
4.登录之后才能访问得页面,文章相关
http://127.0.0.1:8000/api/v1/articles/?page=2&token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
http://127.0.0.1:8000/api/v1/articles/2/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
# 不同得视图函数
http://127.0.0.1:8000/api/v1/article/?page=2&token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
http://127.0.0.1:8000/api/v1/article/2/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
- 认证组件
authentication_classes = [LuffyAuth, ]
- 验证从前端传来得token是否存在
token = request.query_params.get("token")
存在,返回元祖 return (obj.user.user,obj)
不存在,抛异常 raise AuthenticationFailed({'code':1001,'error':'认证失败'})
5.对文章评论,需要先登录,所以接口有登录认证
http://127.0.0.1:8000/api/v1/article/1/comment/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
{"content":"评论得内容"}
{"content":"评论得内容","pid":5}
6.对文章点赞,需要登录认证
http://127.0.0.1:8000/api/v1/article/1/agree/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
不需要传什么,文章得id随着url传过去了。
7.对文章收藏,需要登录认证
http://127.0.0.1:8000/api/v1/article/4/collect/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
不需要传什么,文章得id随着url传过去了。
delete请求:
取消对文章得收藏
6.v1.4 - 接口:(与购物车 相关得接口,需要登录认证)
方案一: 这种方式不好,每次都要全部拿出来,修改,在放进去
http://127.0.0.1:8000/api/v1/shopping/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
购物车中添加一条数据 {"courseid":2,"policyid":3}
get请求:
查看自己购物车中的所有数据 ...
delete请求:
删除购物车中的数据 {"delete_ids":[1,2]}
put请求:
更新价格策略 ... {"courseid":2,"policyid":3}
方案二:数据结构得设计好坏,直接影响了代码实现得复杂度;
http://127.0.0.1:8000/api/v1/shoppingcart/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
{"courseid":2,"policyid":3}
delete请求:
{"courseids":[1,2]}
patch请求:
{"courseid":2,"policyid":3}
get请求:
...
7.v1.5 - 接口:(与支付中心 相关得接口,需要登录认证)
方式一:这种方式不好,查询数据时,每次都要去数据库查询,优惠劵,逻辑还有点问题,查看版本二.
http://127.0.0.1:8000/api/v1/payment/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
将购物车得课程,加入到支付中心
{"courseids":[1,2]}
get请求:
获取支付中心得数据
。。。
patch请求:
修改课程得优惠券
{"courseid":1,"couponid":1}
方式二:
http://127.0.0.1:8000/api/v1/paymentview/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
将购物车得课程,加入到支付中心
{"courseids":[1,2]}
get请求:
获取支付中心得数据
。。。
patch请求:
修改课程得优惠券,全局得优惠劵
{"courseid":1,"couponid":1,"gcouponid":5}
8.v1.6 - 接口:(生成订单得接口,需要登录认证)
http://127.0.0.1:8000/api/v1/order/?token=22abe8c6-717a-4b1b-a164-84d4c18aea5b
post请求:
将redis中,支付中心得数据,生成订单,存到数据库中
{"balance":1000,"money":245} # balance用户得抵扣得贝里数,money是实付金额
对于用户,前端传来得数据,后台入库之前,一定要做校验,验证合法性。
二、v1.3 - 知识点 - 登录、课程、文章相关得接口
版本 分页
REST_FRAMEWORK = { 'DEFAULT_RENDERER_CLASSES':['rest_framework.renderers.JSONRenderer','rest_framework.renderers.BrowsableAPIRenderer'], 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允许的版本 'VERSION_PARAM': 'version', # 参数 'DEFAULT_VERSION': 'v1', # 默认版本 "DEFAULT_PAGINATION_CLASS":"rest_framework.pagination.PageNumberPagination", "PAGE_SIZE":1, }
from rest_framework.pagination import PageNumberPagination class MyPageNumberPagination(PageNumberPagination): page_size = 1 page_query_param = "page"
urls.py
url(r'^api/(?P<version>\w+)/',include('api.urls')), ------------------------------------------------- from django.conf.urls import url, include from rest_framework import routers from api.views import course, account, article routers = routers.DefaultRouter() routers.register("courses", course.CourseViewSet) routers.register("articles", article.ArticelViewSet) urlpatterns = [ url(r'^auth/$', account.AuthView.as_view()), url(r'^', include(routers.urls)), url(r'^course/$', course.CourseView.as_view({"get": "list"})), url(r'^course/(?P<pk>\d+)/$', course.CourseView.as_view({"get": "retrieve"})), url(r'^article/$', article.ArticleView.as_view({"get": "list"})), url(r'^article/(?P<pk>\d+)/$', article.ArticleView.as_view({"get": "retrieve"})), url(r'^article/(?P<pk>\d+)/comment/$', article.CommentView.as_view()), # 对文章评论 url(r'^article/(?P<pk>\d+)/agree/$', article.AgreeView.as_view()), # 对文章点赞 url(r'^article/(?P<pk>\d+)/collect/$', article.CollectView.as_view()), # 对文章收藏、取消(post delete) ]
中间件 - 跨域处理 cors.py
from django.utils.deprecation import MiddlewareMixin class CORSMiddleware(MiddlewareMixin): def process_response(self, request, response): # 允许你的域名来访问 response['Access-Control-Allow-Origin'] = "*" if request.method == 'OPTIONS': # 允许你携带 Content-Type 请求头 不能写* response['Access-Control-Allow-Headers'] = 'Content-Type' # 允许你发送 DELETE PUT请求 response['Access-Control-Allow-Methods'] = 'DELETE,PUT' return response
课程 文章 登录 (相关得接口)
涉及到
分页
视图 ViewSetMinin ModelViewSet APIView
序列化
认证 authentication_classes = [LuffyAuth, ]
事务 with transaction.atomic(): ... ...
异常 try ... except 异常捕获
# -*- coding:utf-8 -*- from rest_framework import mixins from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.viewsets import ViewSetMixin, GenericViewSet from utils.response import BaseResponse from api.models import Course, CourseDetail from utils.paginator import MyPageNumberPagination from api.serializers.course import CourseSerializers, CourseDetailSerializers class CourseView(ViewSetMixin, APIView): """ 课程相关得接口 """ def list(self, request, *args, **kwargs): """ 查看全部课程 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() pnp = MyPageNumberPagination() try: course_list = Course.objects.all() courses_page = pnp.paginate_queryset(course_list, request, self) ser = CourseSerializers(courses_page, many=True) ret.data = ser.data except Exception as e: ret.code = "1001" ret.error = "获取课程失败" return pnp.get_paginated_response(ret.dict) def retrieve(self, request, *args, **kwargs): """ 查看单条课程 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: pk = kwargs.get("pk") course_obj = CourseDetail.objects.filter(course_id=pk).first() ser = CourseDetailSerializers(instance=course_obj, many=False) ret.data = ser.data except Exception as e: ret.code = 1001 ret.data = "获取课程失败" return Response(ret.dict) class CourseViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet): """ 查看全部、单条课程 """ queryset = Course.objects.all() serializer_class = CourseSerializers def video(request): """ 播放视频 """ return render(request, 'video.html')
# -*- coding:utf-8 -*- from django.db.models import F from django.db import transaction from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.viewsets import ViewSetMixin, ReadOnlyModelViewSet from api.serializers import article from api.auth.auth import LuffyAuth from utils.response import BaseResponse from utils.paginator import MyPageNumberPagination from api.models import Article, Comment, Collection class ArticelViewSet(ReadOnlyModelViewSet): """ 查看全部、单条文章 """ authentication_classes = [LuffyAuth, ] queryset = Article.objects.all() serializer_class = article.ArticleSerializers class ArticleView(ViewSetMixin, APIView): """ 文章相关得接口 """ authentication_classes = [LuffyAuth, ] def list(self, request, *args, **kwargs): """ 查看全部文章 :param request: 请求相关得数据 :param args: URL传参 :param kwargs: URL关键字传参 :return: """ ret = BaseResponse() # print(request.user,type(request.user)) # name 用户认证通过后,这两个值就可以使用了!! # print(request.auth,type(request.auth)) # obj try: pnp = MyPageNumberPagination() article_list = Article.objects.all() article_page = pnp.paginate_queryset(article_list, request, self) ser = article.ArticleSerializers(instance=article_page, many=True) ret.data = ser.data return pnp.get_paginated_response(ret.dict) except Exception as e: ret.code = 1001 ret.data = "获取文章失败" return Response(ret.dict) def retrieve(self, request, *args, **kwargs): """ 查看单条文章 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: id = kwargs.get("pk") article_obj = Article.objects.filter(id=id).first() ser = article.ArticleSerializers(instance=article_obj, many=False) ret.data = ser.data except Exception as e: ret.code = 1001 ret.error = "获取课程失败" return Response(ret.dict) class CommentView(APIView): """ 文章评论得接口 """ authentication_classes = [LuffyAuth, ] def post(self, request, pk, *args, **kwargs): """ 评论 :param request: 请求相关得数据 :param pk: 评论得文章id :param args: URL传参 :param kwargs: URL关键字传参 :return: """ ret = BaseResponse() try: content = request.data.get("content") account = request.auth.user # 当前登录得用户对象 pid = request.data.get("pid") # 父id if not pid: comment_obj = Comment.objects.create( content_object=Article.objects.get(pk=pk), content=content, account=account, ) else: comment_obj = Comment.objects.create( content_object=Article.objects.get(pk=pk), content=content, account=account, p_node=Comment.objects.get(pk=pid) ) ser = article.CommentSerializers(instance=comment_obj, many=False) ret.data = ser.data except Exception as e: ret.code = 1001 ret.error = "评论失败" return Response(ret.dict) class AgreeView(APIView): """ 文章点赞得接口 """ authentication_classes = [LuffyAuth, ] def post(self, request, pk, *args, **kwargs): """ 点赞 :param request: :param pk: 文章id :param args: :param kwargs: :return: """ ret = BaseResponse() try: with transaction.atomic(): Article.objects.filter(pk=pk).update(agree_num=F("agree_num") + 1) ret.data = Article.objects.get(pk=pk).agree_num except Exception as e: ret.code = 1001 ret.error = "点赞失败" return Response(ret.dict) class CollectView(APIView): """ 文章收藏得接口 """ authentication_classes = [LuffyAuth, ] def post(self, request, pk, *args, **kwargs): """ 收藏 :param request: :param pk: 文章id :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_obj = request.auth.user with transaction.atomic(): collect_obj = Collection.objects.create( content_object=Article.objects.get(pk=pk), account=user_obj ) Article.objects.filter(pk=pk).update(collect_num=F("collect_num") + 1) ser = article.CollectionSerializers(instance=collect_obj, many=False) ret.data = ser.data except Exception as e: ret.code = 1001 ret.error = "收藏失败" return Response(ret.dict) def delete(self, request, pk, *args, **kwargs): """ 取消收藏 :param request: :param pk: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_obj = request.auth.user with transaction.atomic(): article_obj = Article.objects.get(pk=pk) if not article_obj.collect.all(): ret.code = 1001 ret.error = "不存在" for item in article_obj.collect.all(): if item.account == user_obj: item.delete() Article.objects.filter(pk=pk).update(collect_num=F("collect_num") - 1) ret.data = "取消成功" except Exception as e: ret.code = 1001 ret.error = "取消失败" return Response(ret.dict)
# -*- coding: utf-8 -*- import uuid from rest_framework.views import APIView from rest_framework.response import Response from api import models from utils.response import BaseResponse class AuthView(APIView): """ 用户认证相关接口 """ def post(self, request, *args, **kwargs): """ 用户认证 :param request: 请求相关得数据 :param args: URL传参 :param kwargs: URL关键字传参 :return: """ ret = BaseResponse() try: user = request.data.get("user") pwd = request.data.get("pwd") user_obj = models.Account.objects.filter(username=user, password=pwd).first() if not user_obj: ret.code = 1001 ret.error = "用户名或密码错误" return Response(ret.dict) token = str(uuid.uuid4()) models.UserAuthToken.objects.update_or_create(user=user_obj, defaults={"token": token}) ret.data = token except Exception as e: ret.code = 1002 return Response(ret.dict)
登录认证:注意返回( AuthenticationFailed (user_obj.user.username, user_obj) )
# -*- coding:utf-8 -*- from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication from api.models import UserAuthToken class LuffyAuth(BaseAuthentication): """ 登录认证 """ def authenticate(self, request): token = request.query_params.get("token") user_obj = UserAuthToken.objects.filter(token=token).first() if not user_obj: raise exceptions.AuthenticationFailed({"code": 1001, "error": "认证失败"}) return (user_obj.user.username, user_obj) # ( request.user request.auth )
序列化 课程 文章 (相关)
depth = 1 / 2 (深度,关联显示)
一对一 一对多:source
多对多:SerializerMethodField()
class CourseDetailSerializers(serializers.ModelSerializer): """ 课程详细序列化 """ # o2o fk course = serializers.CharField(source="course.name") course_type = serializers.CharField(source='course.get_course_type_display') period = serializers.CharField(source='course.period') level = serializers.CharField(source='course.get_level_display') # m2m recommend_courses = serializers.SerializerMethodField() teachers = serializers.SerializerMethodField() courseoutline = serializers.SerializerMethodField() pricepolicy = serializers.SerializerMethodField() askedquestion = serializers.SerializerMethodField() class Meta: model = models.CourseDetail fields = ["id", "course", "course_type", "period", "level", "hours", "course_slogan", "recommend_courses", "teachers", "courseoutline", "pricepolicy", "askedquestion"] def get_recommend_courses(self, obj): queryset = obj.recommend_courses.all() return [{"id": row.id, "name": row.name} for row in queryset] def get_teachers(self, obj): queryset = obj.teachers.all() return [{"id": row.id, "name": row.name, "role": row.get_role_display(), "image": row.image} for row in queryset] def get_courseoutline(self, obj): queryset = obj.courseoutline_set.all() return [{"title": row.title, "content": row.content} for row in queryset] def get_pricepolicy(self, obj): queryset = obj.course.price_policy.all() return [{"valid_period": row.get_valid_period_display(), "price": row.price} for row in queryset] def get_askedquestion(self, obj): queryset = obj.course.asked_question.all() return [{"question": row.question, "answer": row.answer} for row in queryset]
class CommentSerializers(serializers.ModelSerializer): """ 评论序列化 """ content_title = serializers.CharField(source="content_object.title") # 当前文章得title p_node = serializers.CharField(source="p_node.content_object.title", default=None) # 父评论得内容,若没有,则为空。 account = serializers.CharField(source="account.username") class Meta: model = models.Comment fields = "__all__" depth = 1
from rest_framework import serializers from api import models class CourseSerializers(serializers.ModelSerializer): """ 课程序列化 """ sub_category = serializers.CharField(source='sub_category.name') course_type = serializers.CharField(source="get_course_type_display") degree_course = serializers.CharField(source='degree_course.name') why_studys = serializers.CharField(source='coursedetail.why_study') level = serializers.CharField(source="get_level_display") status = serializers.CharField(source="get_status_display") class Meta: model = models.Course fields = "__all__" class CourseDetailSerializers(serializers.ModelSerializer): """ 课程详细序列化 """ # o2o fk course = serializers.CharField(source="course.name") course_type = serializers.CharField(source='course.get_course_type_display') period = serializers.CharField(source='course.period') level = serializers.CharField(source='course.get_level_display') # m2m recommend_courses = serializers.SerializerMethodField() teachers = serializers.SerializerMethodField() courseoutline = serializers.SerializerMethodField() pricepolicy = serializers.SerializerMethodField() askedquestion = serializers.SerializerMethodField() class Meta: model = models.CourseDetail fields = ["id", "course", "course_type", "period", "level", "hours", "course_slogan", "recommend_courses", "teachers", "courseoutline", "pricepolicy", "askedquestion"] def get_recommend_courses(self, obj): queryset = obj.recommend_courses.all() return [{"id": row.id, "name": row.name} for row in queryset] def get_teachers(self, obj): queryset = obj.teachers.all() return [{"id": row.id, "name": row.name, "role": row.get_role_display(), "image": row.image} for row in queryset] def get_courseoutline(self, obj): queryset = obj.courseoutline_set.all() return [{"title": row.title, "content": row.content} for row in queryset] def get_pricepolicy(self, obj): queryset = obj.course.price_policy.all() return [{"valid_period": row.get_valid_period_display(), "price": row.price} for row in queryset] def get_askedquestion(self, obj): queryset = obj.course.asked_question.all() return [{"question": row.question, "answer": row.answer} for row in queryset]
# -*- coding:utf-8 -*- from api import models from rest_framework import serializers class ArticleSerializers(serializers.ModelSerializer): """ 文章序列化 """ source = serializers.CharField(source="source.name") article_type = serializers.CharField(source="get_article_type_display") status = serializers.CharField(source="get_status_display") position = serializers.CharField(source="get_position_display") class Meta: model = models.Article # fields = "__all__" fields = ["id", "title", "source", "article_type", "head_img", "content", "status", "date", "position"] class CommentSerializers(serializers.ModelSerializer): """ 评论序列化 """ content_title = serializers.CharField(source="content_object.title") # 当前文章得title p_node = serializers.CharField(source="p_node.content_object.title", default=None) # 父评论得内容,若没有,则为空。 account = serializers.CharField(source="account.username") class Meta: model = models.Comment fields = "__all__" depth = 1 class CollectionSerializers(serializers.ModelSerializer): """ 收藏序列化 """ article_title = serializers.CharField(source="content_object.title") account_name = serializers.CharField(source="account.username") class Meta: model = models.Collection fields = ["id", "article_title", "account_name", "date"]
表结构
- 课程(13) 课程大类 课程子类 学位课 讲师 奖励 专题课(学位课模块表) 价格策略(ContentType) 课程详细(o2o 水平分表) 常见问题 课程大纲 章节 课时 作业 - 深科技 用户表 用户Token 文章来源 文章表 通用评论表 通用收藏表
三、v1.4 - 知识点 - redis - 与购物车相关得接口
redis介绍: http://www.cnblogs.com/alice-bj/articles/9365352.html
用户挑选课程,添加到购物车,数据存到redis中:
使用redis原因:中间状态,可能会频繁得修改数据,
redis中得数据增删改查:
数据结构:
版本一:
redis->{ shopping_car:{ 用户ID:{ 课程1:{ title:'金融量化分析入门', img:'/xx/xx/xx.png', policy:{ 10: {'name':'有效期1个月','price':599}, 11: {'name':'有效期3个月','price':1599}, 13: {'name':'有效期6个月','price':2599}, }, default_policy:12 }, 课程2:{ title:'金融量化分析入门', img:'/xx/xx/xx.png', policy:{ 10: {'name':'有效期1个月','price':599}, 11: {'name':'有效期3个月','price':1599}, 13: {'name':'有效期6个月','price':2599}, }, default_policy:10 } }, 用户ID:{...}, } }
版本二:
{ luffy_shopping_car_6_11:{ # luffy_shopping_用户id_课程id 'title':'21天入门到放弃', 'src':'xxx.png', 'policy':{ 1:{id:'xx'.....}, 2:{id:'xx'.....}, 3:{id:'xx'.....}, 4:{id:'xx'.....}, }, 'default_policy':3 }, luffy_shopping_car_6_13:{ ... } }
POST请求:购物车中添加一条数据 请求体: { courseid:1, policy_id:10 } 后台: 检验当前课程是否有此价格策略,合法:将数据构造字典,再添加到redis GET请求:查看自己购物车中的所有数据 获取当前登录用户ID,根据用户ID去redis的购物车中获取数据。 DELETE请求:删除购物车中的数据 请求体: { course_ids:[1,2] } PUT/PATCH请求:更新价格策略 请求体: { courseid:1, policy_id:13 } 验证是否存在;
版本一、二对比:
版本一数据结构复杂,直接影响了代码实现得逻辑复杂性;
版本二数据结构简单,使得在操作数据得时候能简洁,方便一些;
数据结构得设计很重要,很重要,再有一点是手中得剑要多呀!!要不然写起来,很吃力,效率不高;
使用redis - 购物车实现得步骤
1. 配置
# redis配置 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://10.0.0.200:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100}, "PASSWORD": "luffy1234", } } }
2.路由
from django.conf.urls import url, include from rest_framework import routers from api.views import (course, account, article, shopping) routers = routers.DefaultRouter() routers.register("courses", course.CourseViewSet) routers.register("articles", article.ArticelViewSet) urlpatterns = [ url(r'^auth/$', account.AuthView.as_view()), # 课程 文章 modelview 视图方法 url(r'^', include(routers.urls)), # 课程相关 url(r'^course/$', course.CourseView.as_view({"get": "list"})), url(r'^course/(?P<pk>\d+)/$', course.CourseView.as_view({"get": "retrieve"})), # 文章相关 url(r'^article/$', article.ArticleView.as_view({"get": "list"})), url(r'^article/(?P<pk>\d+)/$', article.ArticleView.as_view({"get": "retrieve"})), url(r'^article/(?P<pk>\d+)/comment/$', article.CommentView.as_view()), # 对文章评论 url(r'^article/(?P<pk>\d+)/agree/$', article.AgreeView.as_view()), # 对文章点赞 url(r'^article/(?P<pk>\d+)/collect/$', article.CollectView.as_view()), # 对文章收藏、取消(post delete) # 购物车相关 方案一: url(r'^shopping/$', shopping.ShoppingView.as_view()), # 购物车相关 方案二: # url(r'^shoppingcart/$', shopping.ShoppingCartViewSet.as_view({"post": "create","delete":"destroy"})) url(r'^shoppingcart/$', shopping.ShoppingCartViewSet.as_view()) # 用这种,否则要写多个 get post put delete ]
3. 认证组件:
authentication_classes = [LuffyAuth, ] ------------------------------
# -*- coding:utf-8 -*- from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication from api.models import UserAuthToken class LuffyAuth(BaseAuthentication): """ 用户请求进行认证认证 """ def authenticate(self, request): token = request.query_params.get("token") user_obj = UserAuthToken.objects.filter(token=token).first() if not user_obj: raise exceptions.AuthenticationFailed({"code": 1001, "error": "认证失败"}) return (user_obj.user.username, user_obj) # ( request.user request.auth )
4. 业务(注意合法性):
合法性,一定要判断,用户传过来数据,都不一定是安全得,会有爬虫;所以一定要判断。
对数据 增 删 该 查
注意: 对增加 修改 传过来得数据,一定要判断合法性,该抛得异常一定要抛
4.1. 增: post
try .. except
1. 获取课程id,价格策略id
前端传过来得数据
2. 获取课程对象
如何没有,捕获异常,ObjectDoesNoExist
3. 获取该课程得价格策略
存到字典中
4. 判断用户提交得价格策略是否合法
价格策略id是否在字典中, 不合法,抛异常,自定义异常 PricePolicyInvalid
5. 构造购物信息添加到redis中
{ luffy_shopping_car_6_11:{ 'title':'21天入门到放弃', 'src':'xxx.png', 'policy':{ 1:{id:'xx'.....}, 2:{id:'xx'.....}, 3:{id:'xx'.....}, 4:{id:'xx'.....}, }, 'default_policy':3 }, luffy_shopping_car_6_13:{ ... } }
cart_key = settings.SHOPPING_CART_KEY % (request.auth.user.id, course_id,) # 写到配置文件了
cart_dict = {
'title': course_obj.name,
'img': course_obj.course_img,
'default_policy': policy_id,
'policy': json.dumps(price_policy_dict)
}
self.conn_new.hmset(cart_key, cart_dict)
4.2. 删:delete
try ... except ...
1. 删除课程ids
前端传得是course_ids列表 {"courseids":[1,2]}
2. 列表生成式构造了redis中存得 key
courseid_list = request.data.get("courseids") key_list = [settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) for course_id in courseid_list] self.conn_new.delete(*key_list)
4.3. 改:patch / put
try ... catch ...
1. 修改价格策略
前端传过得数据 课程id 价格策略id {"courseid":2,"policyid":3}
拼接redis中得key
key = settings.SHOPPING_CART_KEY % (request.auth.user_id,course_id)
2. 判断 是否存在 该课程,redis中是否有此该课程得key
if not self.conn_new.exists(key): pass
3. redis中获取所有该课程得价格策略,判断修改得价格策略是否在该策略字典中
policy_dict = json.loads(str(self.conn_new.hget(key,"policy"),encoding="utf-8"))
if policy_id not in policy_dict: pass
4. 修改默认得价格策略
self.conn_new.hset(key,"default_policy",policy_id)
4.4. 查:get
try ... except ...
1. 构造该用户得key
key_match = settings.SHOPPING_CART_KEY % (request.auth.user_id,"*")
2. 在redis中取出,该key_match对应得所有购物车列表
scan_iter (一定用这个,以防数据量大)
课程id 也应该传过去得,以方便用户可以 发post
for key in self.conn_new.scan_iter(key_match,count=10): course_dict = { "course_id": key.decode("utf-8").rsplit('_',maxsplit=1)[1], "img" : self.conn_new.hget(key,"img").decode("utf-8"), "title" : self.conn_new.hget(key,'title').decode("utf-8"), "default_policy" : self.conn_new.hget(key,'default_policy').decode("utf-8"), "policy" : json.loads(self.conn_new.hget(key,'policy').decode("utf-8")) } course_list.append(course_dict)
传到前端得数据:
4.5. code
class ShoppingCartViewSet(APIView): """ 方案二: 购物车得相关接口 """ authentication_classes = [LuffyAuth, ] conn_new = get_redis_connection("default") def post(self, request, *args, **kwargs): """ 将课程添加到购物车 {"courseid":2,"policyid":3} 数据结构: { luffy_shopping_car_6_11:{ 'title':'21天入门到放弃', 'src':'xxx.png', 'policy':{ 1:{id:'xx'.....}, 2:{id:'xx'.....}, 3:{id:'xx'.....}, 4:{id:'xx'.....}, }, 'default_policy':3 }, luffy_shopping_car_6_13:{ ... } } :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: course_id = int(request.data.get("courseid")) policy_id = int(request.data.get('policyid')) # 1.课程对象 course_obj = Course.objects.get(pk=course_id) # 2.获取价格策略 price_policy_list = course_obj.price_policy.all() price_policy_dict = {} for item in price_policy_list: price_policy_dict[item.id] = { "period": item.valid_period, "period_display": item.get_valid_period_display(), "price": item.price, } # 3.判断用户提交得价格策略是否合法 if policy_id not in price_policy_dict: raise PricePolicyInvalid("价格策略不合法") # 4.将购物信息添加到redis中 self.conn_new cart_key = settings.SHOPPING_CART_KEY % (request.auth.user.id, course_id,) # 写到配置文件了 cart_dict = { 'title': course_obj.name, 'img': course_obj.course_img, 'default_policy': policy_id, 'policy': json.dumps(price_policy_dict) } self.conn_new.hmset(cart_key, cart_dict) ret.data = "添加成功" except PricePolicyInvalid as e: ret.code = 2001 ret.error = e.msg except ObjectDoesNotExist as e: ret.code = 2001 ret.error = "课程不存在" except Exception as e: ret.code = 1001 ret.error = "添加购物车失败" return Response(ret.dict) def delete(self, request, *args, **kwargs): """ 购物车中删除数据 {"courseids":[1,2]} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: courseid_list = request.data.get("courseids") key_list = [settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) for course_id in courseid_list] self.conn_new.delete(*key_list) ret.data = "删除成功" except Exception as e: ret.code = 1002 ret.error = '删除失败' return Response(ret.dict) def patch(self,request,*args,**kwargs): """ 修改价格策略 {"courseid":2,"policyid":3} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: course_id = int(request.data.get("courseid")) policy_id = str(request.data.get('policyid')) # 1.拼接课程key key = settings.SHOPPING_CART_KEY % (request.auth.user_id,course_id) if not self.conn_new.exists(key): ret.code = 1002 ret.error = "购物车不存在此课程" return Response(ret.dict) # 2.在redis中获取所有得价格策略 policy_dict = json.loads(str(self.conn_new.hget(key,"policy"),encoding="utf-8")) if policy_id not in policy_dict: ret.code = 1003 ret.error = "价格策略不合法" return Response(ret.dict) # 3. 修改默认得价格策略 self.conn_new.hset(key,"default_policy",policy_id) ret.data = "修改成功" except Exception as e: ret.code = 1004 ret.error = "修改失败" return Response(ret.dict) def get(self, request, *args, **kwargs): """ 查看购物车中所有商品 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: key_match = settings.SHOPPING_CART_KEY % (request.auth.user_id,"*") course_list = [] # 获取该用户得所有购物车列表 for key in self.conn_new.scan_iter(key_match,count=10): course_dict = { "img" : self.conn_new.hget(key,"img").decode("utf-8"), "title" : self.conn_new.hget(key,'title').decode("utf-8"), "default_policy" : self.conn_new.hget(key,'default_policy').decode("utf-8"), "policy" : json.loads(self.conn_new.hget(key,'policy').decode("utf-8")) } course_list.append(course_dict) ret.data = course_list except Exception as e: ret.code = 1001 ret.error = "获取失败" return Response(ret.dict)
# settings.py SHOPPING_CART_KEY = "shopping_cart_%s_%s" # utils/exception.py class PricePolicyInvalid(Exception): def __init__(self,msg): self.msg = msg
# -*- coding:utf-8 -*- import json from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin from rest_framework.response import Response from django_redis import get_redis_connection from django.core.exceptions import ObjectDoesNotExist from django.conf import settings from api.auth.auth import LuffyAuth from api.models import Course from utils.response import BaseResponse from utils.exception import PricePolicyInvalid conn = get_redis_connection("default") class ShoppingView(APIView): """ 方案一: 购物车相关得接口,数据存在redis中,临时数据 """ authentication_classes = [LuffyAuth, ] def post(self, request, *args, **kwargs): """ 加入购物车 数据:{"courseid":2,"policyid":10} 数据结构: "2": { "title": "网络编程", "img": "xxxx.png", "policy": { "3": { "name": "3个月", "price": 2222 }, "10": { "name": "2个月", "price": 222 } }, "default_policy": 3 } :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user.id course_id = str(request.data.get("courseid")) policy_id = request.data.get("policyid") course_obj = Course.objects.filter(pk=course_id).first() # 验证是否有此价格策略 policy_dict = {} for policy_item in course_obj.price_policy.all(): if policy_id is policy_item.id: for item in course_obj.price_policy.all(): policy_dict[item.id] = {"name": item.get_valid_period_display(), "price": item.price} if not policy_dict: raise ValueError("价格策略不存在") shop_dict = { course_id: { "title": course_obj.name, "img": course_obj.course_img, "policy": policy_dict, # 价格策略得字典 "default_policy": policy_id # 默认得价格id } } # conn.flushall() # 用来做测试 清空 redis # 判断是否第一次存在redis if not conn.hexists("shopping_cart", user_id): shopping_cart_dict = shop_dict else: shopping_cart_dict = json.loads(str(conn.hget("shopping_cart", user_id), encoding="utf-8")) # 判断是新增字典,还是修改字典,如果请求得数据已经存在,就是修改 if not course_id in shopping_cart_dict.keys(): shopping_cart_dict = dict(shopping_cart_dict, **shop_dict) # 两个字典 合并 else: for item in shopping_cart_dict.keys(): if item == course_id: shopping_cart_dict[course_id] = shop_dict[course_id] conn.hset("shopping_cart", user_id, json.dumps(shopping_cart_dict)) # 注意,需要dumps 存到redis中 ret.data = shopping_cart_dict except ValueError as e: ret.code = 1001 ret.error = str(e) except Exception as e: ret.code = 1001 ret.error = "加入购物车失败" return Response(ret.dict) def get(self, request, *args, **kwargs): """ 查看购物车 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user.id val = conn.hget("shopping_cart", user_id) val_str = str(val, encoding="utf-8") # 将redis得value值转成字典 val_dict = json.loads(val_str) ret.data = val_dict except Exception as e: ret.code = 1001 ret.error = "查看购物车失败" return Response(ret.dict) def put(self, request, *args, **kwargs): """ 更新 价格策略 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user.id course_id = str(request.data.get("courseid")) policy_id = request.data.get("policyid") course_obj = Course.objects.filter(pk=course_id).first() shopping_cart = conn.hget("shopping_cart", user_id) shopping_cart_str = str(shopping_cart, encoding="utf-8") # 将redis得value值转成字典 shopping_cart_dict = json.loads(shopping_cart_str) # 验证是否有此价格策略 for policy_item in course_obj.price_policy.all(): if policy_id is policy_item.id: shopping_cart_dict[course_id]["default_policy"] = policy_id conn.hset("shopping_cart", user_id, json.dumps(shopping_cart_dict)) # 注意,需要dumps 存到redis中 ret.data = shopping_cart_dict except Exception as e: ret.code = 1001 ret.data = "更新失败" return Response(ret.dict) def delete(self, request, *args, **kwargs): """ 删除选中得课程 数据:{"delete_ids":[1,2]} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user.id delete_ids = request.data.get("delete_ids") shopping_cart = conn.hget("shopping_cart", user_id) shopping_cart_str = str(shopping_cart, encoding="utf-8") # 将redis得value值转成字典 shopping_cart_dict = json.loads(shopping_cart_str) for delete_id in delete_ids: delete_id = str(delete_id) # 判断删除得id是否在字典列表中 if delete_id in shopping_cart_dict.keys(): shopping_cart_dict.pop(delete_id) conn.hset("shopping_cart", user_id, json.dumps(shopping_cart_dict)) ret.data = shopping_cart_dict if shopping_cart_dict else "数据为空" except Exception as e: ret.code = 1001 ret.error = "删除购物车失败" return Response(ret.dict) class ShoppingCartViewSet(APIView): """ 方案二: 购物车得相关接口 """ authentication_classes = [LuffyAuth, ] conn_new = get_redis_connection("default") def post(self, request, *args, **kwargs): """ 将课程添加到购物车 {"courseid":2,"policyid":3} 数据结构: { luffy_shopping_car_6_11:{ 'title':'21天入门到放弃', 'src':'xxx.png', 'policy':{ 1:{id:'xx'.....}, 2:{id:'xx'.....}, 3:{id:'xx'.....}, 4:{id:'xx'.....}, }, 'default_policy':3 }, luffy_shopping_car_6_13:{ ... } } :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: course_id = int(request.data.get("courseid")) policy_id = int(request.data.get('policyid')) # 1.课程对象 course_obj = Course.objects.get(pk=course_id) # 2.获取价格策略 price_policy_list = course_obj.price_policy.all() price_policy_dict = {} for item in price_policy_list: price_policy_dict[item.id] = { "period": item.valid_period, "period_display": item.get_valid_period_display(), "price": item.price, } # 3.判断用户提交得价格策略是否合法 if policy_id not in price_policy_dict: raise PricePolicyInvalid("价格策略不合法") # 4.将购物信息添加到redis中 self.conn_new cart_key = settings.SHOPPING_CART_KEY % (request.auth.user.id, course_id,) # 写到配置文件了 cart_dict = { 'title': course_obj.name, 'img': course_obj.course_img, 'default_policy': policy_id, 'policy': json.dumps(price_policy_dict) } self.conn_new.hmset(cart_key, cart_dict) ret.data = "添加成功" except PricePolicyInvalid as e: ret.code = 2001 ret.error = e.msg except ObjectDoesNotExist as e: ret.code = 2001 ret.error = "课程不存在" except Exception as e: ret.code = 1001 ret.error = "添加购物车失败" return Response(ret.dict) def delete(self, request, *args, **kwargs): """ 购物车中删除数据 {"courseids":[1,2]} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: courseid_list = request.data.get("courseids") key_list = [settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) for course_id in courseid_list] self.conn_new.delete(*key_list) ret.data = "删除成功" except Exception as e: ret.code = 1002 ret.error = '删除失败' return Response(ret.dict) def patch(self,request,*args,**kwargs): """ 修改价格策略 {"courseid":2,"policyid":3} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: course_id = int(request.data.get("courseid")) policy_id = str(request.data.get('policyid')) # 1.拼接课程key key = settings.SHOPPING_CART_KEY % (request.auth.user_id,course_id) if not self.conn_new.exists(key): ret.code = 1002 ret.error = "购物车不存在此课程" return Response(ret.dict) # 2.在redis中获取所有得价格策略 policy_dict = json.loads(str(self.conn_new.hget(key,"policy"),encoding="utf-8")) if policy_id not in policy_dict: ret.code = 1003 ret.error = "价格策略不合法" return Response(ret.dict) # 3. 修改默认得价格策略 self.conn_new.hset(key,"default_policy",policy_id) ret.data = "修改成功" except Exception as e: ret.code = 1004 ret.error = "修改失败" return Response(ret.dict) def get(self, request, *args, **kwargs): """ 查看购物车中所有商品 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: key_match = settings.SHOPPING_CART_KEY % (request.auth.user_id,"*") course_list = [] # 获取该用户得所有购物车列表 for key in self.conn_new.scan_iter(key_match,count=10): course_dict = { "img" : self.conn_new.hget(key,"img").decode("utf-8"), "title" : self.conn_new.hget(key,'title').decode("utf-8"), "default_policy" : self.conn_new.hget(key,'default_policy').decode("utf-8"), "policy" : json.loads(self.conn_new.hget(key,'policy').decode("utf-8")) } course_list.append(course_dict) ret.data = course_list except Exception as e: ret.code = 1001 ret.error = "获取失败" return Response(ret.dict)
四、v1.4 - 购物车相关 - 逻辑补充知识点
1. 路飞学城的购物车如何实现? 放在redis中,构造特定得数据结构,方便结算 支付 2. 商品是否有个数? - 价格策略(用时间来类比个数) - 购买之后就开始计时 3. 购物车在redis的结构? redis得字典存,用户_id_courseid,对修改,查找,都很方便,数据结构得算法直接决定程序得得复杂度 4. 购物车购买数量有限制吗? .keys('xxxxx') .length 竞争对手 会恶意捣蛋, 上下这两个一定要设置得,分布式得redis 5. 购物车是否设置超时时间? 支付完成清空; .conn.expire("shopping_car_1_1" ,60*30) 方案:购买课程个数限制(200个) 一定要设置,个数限制,想买,就放着吧,支付完成在设置清空; 6. 为什么把购物车信息放入redis? - 临时状态,支付完成之后,就清空了。 - 频繁修改的话,速度快。减轻服务器压力; 7. 具体购物车的逻辑? 添加: 1. 用户选择:课程、价格策略,提交 2. 获取课程、价格策略进行合法性校验(数据库查询) 3. 数据获取,构造结构: { shopping_car_用户ID_课程ID:{ title:"...", img:'xxx', policy:{ ... } } } 4. 将数据以字典的形式保存到redis中。 修改: 1. 用户选择:课程、价格策略,提交 2. 获取课程、价格策略进行合法性校验(redis查询) 3. 更新价格策略 删除: 1. 用户选择:课程提交 2. 获取课程合法性校验(redis查询) 3. 删除 查看: 1. 构造Key shopping_car_用户ID_* 2. scan_iter 8. 代码编写原则: - 简答逻辑先处理 - try - 细粒度异常+自定义异常 - 导入模块 - 内置 - 框架 - 自定义 - 注释 - 文件 - 类 - 函数 - 文件名、类、函数、project - 对功能进行分类 - 减少代码层级 - BaseResponse
五、v1.5 - 知识点 - redis - 与支付相关得接口
用户选中购物车得课程,点击结算,跳到支付中心
涉及到 redis、认证
数据结构:(要提前想好,影响了代码实现得复杂性)
redis = { payment_1_2:{ # 用户id_课程id 'course_id':2, 'title': 'CRM客户关系管理系统实战开发-专题', 'img': 'CRM.jpg', 'policy_id': '4', 'coupon': {}, 'default_coupon': 0, 'period': 210, 'period_display': '12个月', 'price': 122.0}, }, payment_1_1:{ 'course_id':1, 'title': '爬虫开发-专题', 'img': '爬虫开发-专题.jpg', 'policy_id': '2', 'coupon': { 4: {'coupon_type': 0, 'coupon_display': '立减券', 'money_equivalent_value': 40}, 6: {'coupon_type': 1, 'coupon_display': '满减券', 'money_equivalent_value': 60, 'minimum_consume': 100} }, 'default_coupon': 0, 'period': 60, 'period_display': '2个月', 'price': 599.0} }, payment_global_coupon_1:{ 'coupon': { 2: {'coupon_type': 1, 'coupon_display': '满减券', 'money_equivalent_value': 200, 'minimum_consume': 500} }, 'default_coupon': 0 } }
5.1. post请求
{"courseids":[1,2]}
1. 获取用户要结算得课程id
2. 检测用户要结算得课程是否已经加入购物车
3. 获取指定课程得信息
3.1. 获取标题,图片
3.2. 获取价格策略信息
3.3. 构造redis结构
4. 获取当前用户得优惠劵
4.1. 获取全局得优惠劵
4.2. 获取绑定课程得优惠劵,绑定在对应得redis课程中
5. 准备数据,存到redis中,多条数据操作,事务
5.1. 在存之前,先清除该用户得支付中心,在存
5.2. patch请求
{"courseid":1,"couponid":1,"gcouponid":5}
1. 获取课程的id 优惠劵id 全局优惠劵id
2. 判断课程id是否在支付中心
3. 判断课程优惠劵是否可用
4. 判断全局优惠劵是否可用
5. 修改默认的优惠劵事务
5.3. get请求
5.4. code
class PaymentViewSet(APIView): """ 与支付相关得接口 """ authentication_classes = [LuffyAuth, ] conn = get_redis_connection("default") def post(self, request, *args, **kwargs): """ 将购物车得课程,加入到支付中心 {"courseids":[1,2]} :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: # 1.获取用户要结算得课程id user_id = request.auth.user_id course_ids = request.data.get("courseids") shopping_course_list = [settings.SHOPPING_CART_KEY % (user_id, course_id) for course_id in course_ids] # 2.检测用户要结算得课程是否已经加入购物车 for key in shopping_course_list: if not self.conn.exists(key): raise CourseInvalid("课程不在购物车") # 3.获取指定课程得信息 payment_dict = {} for shopping_key in shopping_course_list: # 3.1 获取标题,图片信息 title = self.conn.hget(shopping_key, "title").decode("utf-8") img = self.conn.hget(shopping_key, "img").decode("utf-8") # 3.2 获取价格策略信息 default_policy = self.conn.hget(shopping_key, "default_policy").decode("utf-8") policy_dict = json.loads(self.conn.hget(shopping_key, "policy").decode("utf-8")) default_policy_dict = policy_dict.get(default_policy) # 3.3.构造redis结构 course_id = shopping_key.rsplit("_", maxsplit=1)[1] payment_dict[course_id] = { "courseid":course_id, "title": title, "img": img, "coupon": {}, "default_coupon": 0 } payment_dict[course_id].update(default_policy_dict) print(payment_dict) # 4.获取当前用户得优惠劵 注意时间 。。。 nowtime = datetime.date.today() coupon_list = CouponRecord.objects.filter( account=request.auth.user, status=0, coupon__valid_begin_date__lte=nowtime, coupon__valid_end_date__gte=nowtime ) coupon_dict = {} global_coupon_dict = {} for item in coupon_list: print(item.coupon.name,item.coupon.object_id) # 4.1 获取全局得优惠劵 if not item.coupon.object_id: if item.coupon.coupon_type == 0: # 通用 global_coupon_dict[item.coupon.id] = { "coupon_type": item.coupon.coupon_type, "coupon_display": item.coupon.get_coupon_type_display(), "money_equivalent_value": item.coupon.money_equivalent_value } elif item.coupon.coupon_type == 1: # 满减 global_coupon_dict[item.coupon.id] = { "coupon_type": item.coupon.coupon_type, "coupon_display": item.coupon.get_coupon_type_display(), "money_equivalent_value": item.coupon.money_equivalent_value, "minimum_consume": item.coupon.minimum_consume } else: # 折扣 global_coupon_dict[item.coupon.id] = { "coupon_type": item.coupon.coupon_type, "coupon_display": item.coupon.get_coupon_type_display(), "off_percent": item.coupon.off_percent, } continue # 4.2 获取绑定课程得优惠劵 if item.coupon.coupon_type == 0: # 通用 coupon_dict[item.coupon.id] = { "coupon_type":item.coupon.coupon_type, "coupon_display":item.coupon.get_coupon_type_display(), "money_equivalent_value":item.coupon.money_equivalent_value } elif item.coupon.coupon_type == 1: # 满减 coupon_dict[item.coupon.id] = { "coupon_type": item.coupon.coupon_type, "coupon_display": item.coupon.get_coupon_type_display(), "money_equivalent_value": item.coupon.money_equivalent_value, "minimum_consume":item.coupon.minimum_consume } else: # 折扣 coupon_dict[item.coupon.id] = { "coupon_type": item.coupon.coupon_type, "coupon_display": item.coupon.get_coupon_type_display(), "off_percent": item.coupon.off_percent, } # 4.3 绑定该课程得优惠劵 object_id = str(item.coupon.object_id) if object_id in payment_dict: payment_dict[object_id]["coupon"] = json.dumps(coupon_dict) # 5.准备数据,存到redis中 redis_dict = {} global_key = settings.PAYMENT_GLOBAL_KEY%(user_id) global_dict = {"coupon":json.dumps(global_coupon_dict),"default_coupon":0} redis_dict[global_key] = global_dict for item_id,item_dict in payment_dict.items(): payment_key = settings.PAYMENT_KEY %(user_id,item_id) redis_dict[payment_key] = item_dict # 6.数据存到redis中 事务 pipe = self.conn.pipeline(transaction=True) pipe.multi() # 7. 先清除该用户得支付中心,在加入redis payment_now = list(self.conn.scan_iter(settings.PAYMENT_KEY % (user_id, '*'))) payment_global_now = list(self.conn.scan_iter(settings.PAYMENT_GLOBAL_KEY % (user_id))) if payment_now: self.conn.delete(*payment_now) if payment_global_now: self.conn.delete(*payment_global_now) for redis_key,redis_dict in redis_dict.items(): pipe.hmset(redis_key,redis_dict) pipe.execute() ret.data = "添加成功" except CourseInvalid as e: ret.code = 1002 ret.error = e.msg except Exception as e: ret.code = 1001 ret.error = "添加到支付中心失败" return Response(ret.dict) def get(self, request, *args, **kwargs): """ 获取支付中心得数据 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user_id payment_list = [] # 1.获取该用户绑定课程得优惠劵payment_key,遍历 for payment_key in self.conn.scan_iter(settings.PAYMENT_KEY % (user_id, '*'), count=10): payment_dict = self.conn.hgetall(payment_key) payment_dict_item = {} for item_key,item_dict in payment_dict.items(): if item_key.decode("utf-8") == "coupon": payment_dict_item[item_key.decode("utf-8")] = json.loads(item_dict.decode("utf-8")) else: payment_dict_item[item_key.decode("utf-8")] = item_dict.decode("utf-8") payment_list.append(payment_dict_item) # 2.获取该用户全局得优惠劵得payment_key,遍历 for paymeny_global_key in self.conn.scan_iter(settings.PAYMENT_GLOBAL_KEY%(user_id)): payment_global = self.conn.hgetall(paymeny_global_key) print(payment_global) payment_global_dict = { "coupon":json.loads(payment_global.get("coupon".encode("utf-8"))), "default_coupon":payment_global.get("default_coupon".encode('utf-8')), } payment_list.append(payment_global_dict) ret.data = payment_list except Exception as e: ret.code = 1001 ret.error = "获取失败" return Response(ret.dict) def patch(self, request, *args, **kwargs): """ 修改课程得优惠券 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: user_id = request.auth.user_id course_id = request.data.get("courseid") coupon_id = str(request.data.get("couponid")) gcoupon_id = str(request.data.get("gcouponid")) # 1.判断课程id是否在支付中心 pay_key = settings.PAYMENT_KEY % (user_id, course_id) if not self.conn.exists(pay_key): raise CourseInvalid("课程不在支付中心") # 2.判断课程优惠券是否可用 coupon = json.loads(self.conn.hget(pay_key, "coupon").decode("utf-8")) if coupon_id not in coupon and coupon_id != "0": raise CouponInvalid("课程优惠劵不可用") # 3.判断全局优惠劵是否可用 global_key = settings.PAYMENT_GLOBAL_KEY %(user_id) global_coupon = json.loads(self.conn.hget(global_key,"coupon").decode("utf-8")) if gcoupon_id not in global_coupon and gcoupon_id != "0": raise CouponInvalid("全局得课程优惠劵不可用") # 4.修改默认得优惠劵 事务 pipe = self.conn.pipeline(transaction=True) pipe.multi() self.conn.hset(pay_key, "default_coupon", coupon_id) self.conn.hset(global_key,"default_coupon", gcoupon_id) pipe.execute() ret.data = "修改成功" except CourseInvalid as e: ret.code = 1002 ret.error = e.msg except CouponInvalid as e: ret.code = 1003 ret.error = e.msg except Exception as e: ret.code = 1001 ret.error = "修改失败" return Response(ret.dict)
六、v1.6 - 知识点 - 存数据库 - 事务 - 生成订单 - 得接口
用户将redis中得支付中心得课程数据,点击立即支付,发post请求,生成订单,订单详情,存数据库中。
post请求:
{"balance":1000,"money":245}
1. 获取用户提交数据 { balance:1000, money:900 } balance = request.data.get("balance") money = request.data.get("money") 2. 数据验证 - 大于等于0 - 个人账户是否有1000贝里 if user.auth.user.balance < balance: 账户贝里余额不足 优惠券ID_LIST = [1,3,4] 总价 实际支付 3. 去结算中获取课程信息 for course_dict in redis的结算中获取: # 获取课程ID # 根据course_id去数据库检查状态 # 获取价格策略 # 根据policy_id去数据库检查是否还依然存在 # 获取使用优惠券ID # 根据优惠券ID检查优惠券是否过期 # 获取原价+获取优惠券类型 - 立减 0 = 获取原价 - 优惠券金额 或 折后价格 = 获取原价 - 优惠券金额 - 满减:是否满足限制 折后价格 = 获取原价 - 优惠券金额 - 折扣: 折后价格 = 获取原价 * 80 / 100 4. 全站优惠券 - 去数据库校验全站优惠券的合法性 - 应用优惠券: - 立减 0 = 实际支付 - 优惠券金额 或 折后价格 =实际支付 - 优惠券金额 - 满减:是否满足限制 折后价格 = 实际支付 - 优惠券金额 - 折扣: 折后价格 = 实际支付 * 80 / 100 - 实际支付 5. 贝里抵扣 6. 总金额校验 实际支付 - 贝里 = money:900 7. 为当前课程生成订单 事务: - 订单表创建一条数据 Order - 订单详细表创建一条数据 OrderDetail EnrolledCourse - 订单详细表创建一条数据 OrderDetail EnrolledCourse - 订单详细表创建一条数据 OrderDetail EnrolledCourse - 如果有贝里支付 - 贝里金额扣除 Account - 交易记录 TransactionRecord - 优惠券状态更新 CouponRecord
urls.py
from django.conf.urls import url, include from rest_framework import routers from api.views import (course, account, article, shopping, payment, order) routers = routers.DefaultRouter() routers.register("courses", course.CourseViewSet) routers.register("articles", article.ArticelViewSet) urlpatterns = [ url(r'^auth/$', account.AuthView.as_view()), # 课程 文章 modelview 视图方法 url(r'^', include(routers.urls)), # 课程相关 url(r'^course/$', course.CourseView.as_view({"get": "list"})), url(r'^course/(?P<pk>\d+)/$', course.CourseView.as_view({"get": "retrieve"})), # 文章相关 url(r'^article/$', article.ArticleView.as_view({"get": "list"})), url(r'^article/(?P<pk>\d+)/$', article.ArticleView.as_view({"get": "retrieve"})), url(r'^article/(?P<pk>\d+)/comment/$', article.CommentView.as_view()), # 对文章评论 url(r'^article/(?P<pk>\d+)/agree/$', article.AgreeView.as_view()), # 对文章点赞 url(r'^article/(?P<pk>\d+)/collect/$', article.CollectView.as_view()), # 对文章收藏、取消(post delete) # 购物车相关 方案一: url(r'^shopping/$', shopping.ShoppingView.as_view()), # 购物车相关 方案二: # url(r'^shoppingcart/$', shopping.ShoppingCartViewSet.as_view({"post": "create","delete":"destroy"})) url(r'^shoppingcart/$', shopping.ShoppingCartViewSet.as_view()), # 用这种,否则要写多个 get post put delete # 支付相关 方案一: url(r'^payment/$', payment.PaymentView.as_view()), # 支付相关 方案二: url(r'^paymentview/$', payment.PaymentViewSet.as_view()), # 生成订单 url(r'^order/$', order.OrderView.as_view()) ]
order.py
# -*- coding:utf-8 -*- """ 支付生成订单 """ import datetime import time import uuid import hashlib from rest_framework.views import APIView from rest_framework.response import Response from django_redis import get_redis_connection from django.conf import settings from django.db import transaction from django.db.models import F from utils.auth import LuffyAuth from api.models import (Account, Course, Coupon, CouponRecord, PricePolicy, Order, OrderDetail, TransactionRecord) from utils.response import BaseResponse class OrderView(APIView): """ 与订单相关的接口 """ authentication_classes = [LuffyAuth, ] conn = get_redis_connection("default") def post(self, request, *args, **kwargs): """ 将支付中心的课程,创建订单 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: # 1.获取用户提交的数据 balance = request.data.get("balance") # 用户得贝里 money = request.data.get("money") # 用户实付金额 # 2.验证账户余额是否够 if request.auth.user.balance < balance: ret.code = 1002 ret.data = "账户余额不足" return Response(ret.dict) # 3.去结算中心获取课程信息 all_coupon_obj = [] # 所有得优惠劵得id sale_price_dict = {} payment_key = settings.PAYMENT_KEY % (request.auth.user_id, '*') for item in self.conn.scan_iter(payment_key, count=10): course_id = self.conn.hget(item, "courseid") # 3.1.检查课程的状态,上线 or下线 if Course.objects.filter(id=course_id).first().status != 0: ret.code = 1003 ret.data = "课程已下线" return Response(ret.dict) # 3.2.获取价格策略,去数据库检查价格策略是否依然存在 policy_id = self.conn.hget(item,"policyid") policy_id = policy_id.decode('utf-8') policy_obj = PricePolicy.objects.filter(pk=policy_id).first() if not policy_obj: ret.code = 1004 ret.data = "该价格策略已不存在" return Response(ret.dict) # 3.3.获取优惠劵,去数据库检查优惠劵否依然存在,是否依然有效 default_coupon = self.conn.hget(item, "default_coupon").decode('utf-8') coupon_obj = Coupon.objects.filter(id=default_coupon).first() all_coupon_obj.append(coupon_obj) if coupon_obj: # 3.3.1.判断用户是否已使用过该优惠劵 if CouponRecord.objects.filter(coupon=coupon_obj).first().status is not 0: ret.code = 1005 ret.data = "该优惠劵已不能使用" return Response(ret.dict) # 3.3.2.判断优惠劵是否已经过了有效期 ctime = datetime.date.today() if ctime > coupon_obj.valid_end_date: ret.code = 1006 ret.data = "该优惠劵已过期" return Response(ret.dict) # 3.4 获取原价 + 获取优惠劵类型,差值 src_price = policy_obj.price # 原价 coupon_type = coupon_obj.coupon_type if coupon_obj else None # 优惠劵类型 sale_price = src_price # 折后得价格,如果没有优惠劵则是原价 if coupon_type is 0: # 立减,折后价格 = 原价 - 优惠券金额 money_equivalent_value = coupon_obj.money_equivalent_value val = src_price-money_equivalent_value sale_price = val if val >= 0 else 0 elif coupon_type is 1: # 满减,折后价格 = 获取原价 - 优惠券金额 if src_price >= coupon_obj.minimum_consume: sale_price = src_price - coupon_obj.money_equivalent_value elif coupon_type is 2: # 折扣卷,折后价格 = 获取原价 * 80 / 100 sale_price = sale_price * coupon_obj.off_percent /100 sale_price_dict[course_id.decode('utf-8')] = sale_price price_sale = 0 # 课程优惠劵使用后,应该付的钱数 for item in sale_price_dict: price_sale += sale_price_dict[item] # 4.获取全栈优惠劵,校验,应用 payment_global_key = settings.PAYMENT_GLOBAL_KEY%(request.auth.user_id) default_coupon = self.conn.hget(payment_global_key,"default_coupon").decode('utf-8') coupon_obj = Coupon.objects.filter(id=default_coupon).first() all_coupon_obj.append(coupon_obj) if coupon_obj: # 4.1.判断用户是否已使用过该全栈得优惠劵 if CouponRecord.objects.filter(coupon=coupon_obj).first().status is not 0: ret.code = 1005 ret.data = "该优惠劵已不能使用" return Response(ret.dict) # 4.2.判断该全栈得优惠劵是否已经过了有效期 ctime = datetime.date.today() if ctime > coupon_obj.valid_end_date: ret.code = 1006 ret.data = "该优惠劵已过期" return Response(ret.dict) # 5.获取课程优惠劵操作之后 + 获取全栈优惠劵类型,差值 src_price = price_sale # 原价 coupon_type = coupon_obj.coupon_type if coupon_obj else None # 优惠劵类型 sale_price = src_price # 折后得价格,如果没有优惠劵则是原价 if coupon_type is 0: # 立减,折后价格 = 原价 - 优惠券金额 money_equivalent_value = coupon_obj.money_equivalent_value val = src_price - money_equivalent_value sale_price = val if val >= 0 else 0 elif coupon_type is 1: # 满减,折后价格 = 获取原价 - 优惠券金额 if src_price >= coupon_obj.minimum_consume: sale_price = src_price - coupon_obj.money_equivalent_value elif coupon_type is 2: # 折扣卷,折后价格 = 获取原价 * 80 / 100 sale_price = sale_price * coupon_obj.off_percent / 100 # 6.实际支付 sale_price 实际支付 - 贝里 = money: if sale_price - balance != money: ret.code = 1007 ret.data = "实际支付与应付不符合" return Response(ret.dict) # 7.为当前课程生成订单 事务 with transaction.atomic(): # 7.1 订单表创建一条数据 order_dic = { "payment_type":3, "order_number":int(time.time()), "account":request.auth.user, "actual_amount":money, "status":1, } order_obj = Order.objects.create(**order_dic) # 7.2 关联得订单详细表创建多条数据(每个课程都有一个订单详细表) for item in self.conn.scan_iter(payment_key, count=10): course_id = self.conn.hget(item, "courseid") src_price = self.conn.hget(item,"price") course_obj = Course.objects.filter(pk=course_id).first() OrderDetail_dic = { "order":order_obj, "content_object":course_obj, "original_price":src_price, # 原价 "price":sale_price_dict.get(course_id.decode('utf-8')), # 打折后得价格 "valid_period_display":'xxxxxx', "valid_period":100 } OrderDetail.objects.create(**OrderDetail_dic) # 7.3 用户贝里余额扣除 Account.objects.filter(pk=request.auth.user_id).update(balance=F("balance")-balance) # 7.4 创建一条交易记录 TransactionRecord_dict = { "account": request.auth.user, "amount":balance, "transaction_type":1, # "transaction_number":uuid.uuid4() "transaction_number":hashlib.md5(bytes(str(time.time()),encoding="utf-8")).hexdigest() } TransactionRecord.objects.create(**TransactionRecord_dict) # 7.5 更新用户优惠劵状态,有课程优惠劵,有全局优惠劵 for coupon_obj in all_coupon_obj: if coupon_obj: CouponRecord.objects.filter(coupon=coupon_obj,account=request.auth.user).update(status = 1) ret.data = "生成订单成功" except Exception as e: ret.code = 1010 ret.error = "生成订单失败" return Response(ret.dict)
七、补充
1. 为什么要开发“学城”? - 提高在线 完课率(学成率)。 - 具体: - 购买时间周期 - 闯关式学习 - 考核 - 导师筛选 - 导师监督(跟进记录) - 答疑时间(12小时) - 奖惩措施 - 时间 - 作业 2. 开发周期和团队? 团队: - 开发 - 导师后台,stark组件+rbac : 1人 - 管理后台,stark组件+rbac : 1人 - 主站 - vue.js 1人 - api 村长+1/2文州+1/2Alex+其他 + 村长 - 运维(1人) - 测试(1人) - 产品经理(1人) - UI设计(1人) - 运营(1人) - 销售(4人) - 全职导师(2人) - 签约讲师(...) 周期: - 7月份 - 11月份上线 - 11月份~次年5月份: 修Bug,活动支持,广告。。。 - 6月份:开发题库系统 3. 购买流程 - 加入购物车 - 去结算 - 去支付
八、code
v1.3版本、v1.4版本、v1.5版本、v1.6版本: