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')
course.py
# -*- 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)
article.py
# -*- 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)
account.py

 

登录认证:注意返回(  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]
course.py
# -*- 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"]
article.py

 

表结构 

    - 课程(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)
shopping.py

 

四、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版本:

        https://github.com/alice-bj/luffy/releases

posted @ 2018-07-25 08:34  Alice的小屋  阅读(466)  评论(0编辑  收藏  举报