Django组件-ContentType

一 项目背景

路飞学成项目,有课程,学位课(不同的课程字段不一样),价格策略

问题,1 如何设计表结构,来表示这种规则
        2 为专题课,添加三个价格策略
          3 查询所有价格策略,并且显示对应的课程名称
          4 通过课程id,获取课程信息和价格策略

二 版本一

一个课程表,包含学位课和专题课,一个价格策略表,一对多关联

三 版本二

学位课表,专题课表,装逼课表,价格策略表(在价格策略课表中加入多个FK跟课程表做关联):后期再加其它课程,可维护性差

四 最终版(使用ContentType)

通过Django提供的ContentType表,来构建

 

 

models层创建:

from django.db import models

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation


class Course(models.Model):
    title = models.CharField(max_length=32)
    # 不会在数据库中生成字段,只用于数据库操作
    # policy = GenericRelation('PricePolicy',object_id_field='object_id',content_type_field='contentType')


class DegreeCourse(models.Model):
    title = models.CharField(max_length=32)


class PricePolicy(models.Model):
    # 跟ContentType表做外键关联
    contentType = models.ForeignKey(to=ContentType)
    # 正数
    object_id = models.PositiveIntegerField()

    # 引入一个字段,不会在数据库中创建,只用来做数据库操作
    # content_obj = GenericForeignKey('contentType', 'object_id')

    period = models.CharField(max_length=32)
    price = models.FloatField()

views层:

from app01 import models
def test(request):
    import json
    # 方式一插入价格规则
    # ret=models.ContentType.objects.filter(model='course').first()
    # course=models.Course.objects.filter(pk=1).first()
    # print(ret.id)
    # models.PricePolicy.objects.create(period='30',price=100,object_id=course.id,contentType_id=ret.id)

    # 方式二插入价格规则
    # course=models.Course.objects.filter(pk=1).first()
    # # content_obj=course  会自动的把课程id放到object_id上,并且去ContentType表中查询课程表的id,放到contentType上
    # models.PricePolicy.objects.create(period='60',price=800,content_obj=course)
    # 增加学位课,价格规则
    # degreecourse = models.DegreeCourse.objects.filter(pk=1).first()
    # models.PricePolicy.objects.create(period='60', price=800, content_obj=degreecourse)

    # 查询所有价格策略,并且显示对应的课程名称
    # ret=models.PricePolicy.objects.all()
    # for i in ret:
    #     print(i.price)
    #     print(i.period)
    #     # content_obj 就是代指关联的课程,或者学位课程的那个对象
    #     print(type(i.content_obj))
    #     print(i.content_obj.title)

    # 通过课程id,获取课程信息和价格策略
    course=models.Course.objects.filter(pk=1).first()
    print(course.policy.all())



    return render(request,'test.html')

ContentType组件

ContentType是Django的内置的一个应用,可以追踪项目中所有的APP和model的对应关系,并记录在ContentType表中。
当项目做数据迁移后,会有很多django自带的表,其中就有django_content_type表

ContentType组件应用

  • 在model中定义ForeignKey字段,并关联到ContentType表,通常这个字段命名为content-type
  • 在model中定义PositiveIntergerField字段, 用来存储关联表中的主键,通常用object_id
  • 在model中定义GenericForeignKey字段,传入上面两个字段的名字
  • 方便反向查询可以定义GenericRelation字段
  • postman
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation

# Create your models here.


class Food(models.Model):
    """
    id      title
    1       面包
    2       牛奶
    """
    title = models.CharField(max_length=32)
    # 不会生成字段 只用于反向查询
    coupons = GenericRelation(to="Coupon")


class Fruit(models.Model):
    """
    id      title
    1       苹果
    2       香蕉
    """
    title = models.CharField(max_length=32)


# 如果有40张表
# class Coupon(models.Model):
#     """
#     id      title          food_id    fruit_id
#     1       面包九五折         1         null
#     2       香蕉满10元减5元    null       2
#     """
#     title = models.CharField(max_length=32)
#     food = models.ForeignKey(to="Food")
#     fruit = models.ForeignKey(to="Fruit")


# class Coupon(models.Model):
#     """
#     id      title        table_id      object_id
#     1       面包九五折       1             1
#     2       香蕉满10元减5元  2             2
#     """
#     title = models.CharField(max_length=32)
#     table = models.ForeignKey(to="Table")
#     object_id = models.IntegerField()
#
#
# class Table(models.Model):
#     """
#     id      app_name       table_name
#     1       demo            food
#     2       demo            fruit
#     """
#     app_name = models.CharField(max_length=32)
#     table_name = models.CharField(max_length=32)


class Coupon(models.Model):
    title = models.CharField(max_length=32)
    # 第一步:注意没有引号因为是导入的
    content_type = models.ForeignKey(to=ContentType, on_delete=None)
    # 第二步
    object_id = models.IntegerField()
    # 第三步 不会生成字段,用来操作增删改查
    content_object = GenericForeignKey("content_type", "object_id")
models.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Food, Coupon
from django.contrib.contenttypes.models import ContentType

# Create your views here.


class DemoView(APIView):

    def get(self, request):
        # 给面包创建一个优惠券
        food_obj = Food.objects.filter(id=1).first()
        # Coupon.objects.create(title="面包九五折", content_type_id=8, object_id=1)
        # Coupon.objects.create(title="双十一面包九折促销", content_object=food_obj)

        #查询食物都有哪些优惠券
        #定义了反向查询
        coupons = food_obj.coupons.all()
        print(coupons)

        # 如果没定义反向查询
        content = ContentType.objects.filter(app_label="app01", model="food").first()
        coupons = Coupon.objects.filter(content_type=content, object_id=1).all()
        print(coupons)

        # 优惠券查对象
        # 查询优惠券id=1绑定了哪个商品
        coupon_obj = Coupon.objects.filter(id=1).first()
        content_obj = coupon_obj.content_object
        print(coupon_obj.title,content_obj.title)

        # 通过ContentType表找表模型
        content = ContentType.objects.filter(app_label="app01", model="food").first()
        # content=food  获取food表的表模型用model_class() 
        model_class = content.model_class()
        ret = model_class.objects.all()
        print(ret)

        return Response("ContentType测试")
views.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.auth.models import User

class Post(models.Model):
    """帖子表"""
    author = models.ForeignKey(User)
    title = models.CharField(max_length=72)

    comments = GenericRelation('Comment')  # 支持反向查找评论数据(不会在数据库中创建字段)


class Picture(models.Model):
    """图片表"""
    author = models.ForeignKey(User)
    image = models.ImageField()

    comments = GenericRelation('Comment')  # 支持反向查找评论数据(不会在数据库中创建字段)


class Comment(models.Model):
    """评论表"""
    author = models.ForeignKey(User)
    content = models.TextField()

    content_type = models.ForeignKey(ContentType)  # 外键关联django_content_type表
    object_id = models.PositiveIntegerField()  # 关联数据的主键
    # 告诉Django,content_type和object_id是一组动态关联的数据,不会生成字段,用来操作增删改查
    content_object = GenericForeignKey('content_type', 'object_id')




######################################################################################

class Post(models.Model):
    """帖子表"""
    author = models.ForeignKey(User)
    title = models.CharField(max_length=72)

    comments = GenericRelation('Comment',object_id_field='obj_id',content_type_field='table_name')  # 支持反向查找评论数据(不会在数据库中创建字段)


class Picture(models.Model):
    """图片表"""
    author = models.ForeignKey(User)
    image = models.ImageField()

    comments = GenericRelation('Comment',object_id_field='obj_id',content_type_field='table_name')  # 支持反向查找评论数据(不会在数据库中创建字段)


class Comment(models.Model):
    """评论表"""
    author = models.ForeignKey(User)
    content = models.TextField()

    table_name = models.ForeignKey(ContentType)  # 外键关联django_content_type表
    obj_id = models.PositiveIntegerField()  # 关联数据的主键
    content_object = GenericForeignKey('table_name', 'obj_id') #不会生成字段,用来操作增删改查


######################################################################################
import os

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "about_contenttype.settings")

    import django
    django.setup()

    from app01.models import Post, Picture, Comment
    from django.contrib.auth.models import User
    # 准备测试数据
    user_1 = User.objects.create_user(username='aaa', password='123')
    user_2 = User.objects.create_user(username='bbb', password='123')
    user_3 = User.objects.create_user(username='ccc', password='123')

    post_1 = Post.objects.create(author=user_1, title='Python入门教程')
    post_2 = Post.objects.create(author=user_2, title='Python进阶教程')
    post_3 = Post.objects.create(author=user_1, title='Python入土教程')

    picture_1 = Picture.objects.create(author=user_1, image='小姐姐01.jpg')
    picture_2 = Picture.objects.create(author=user_1, image='小姐姐02.jpg')
    picture_3 = Picture.objects.create(author=user_3, image='小哥哥01.jpg')

    # 给帖子创建评论数据
    comment_1 = Comment.objects.create(author=user_1, content='好文!', content_object=post_1)
    # 给图片创建评论数据
    comment_2 = Comment.objects.create(author=user_2, content='好美!', content_object=picture_1)

    #查询示例:  #定义了反向查询
    post_1 = Post.objects.filter(id=1).first()
    comment_list = post_1.comments.all()
ContentType

media的配置

#静态文件
STATIC_URL = '/static/'  
STATICFILES_DIRS=(
    os.path.join(BASE_DIR,'static'),  
)
# Media配置
MEDIA_URL = "media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
settings.py
from django.conf.urls import url, include
from django.contrib import admin
from django.views.static import serve
from new_luffy import settings


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/course/', include("course.urls")),

    # media路径配置
    url(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT})
]
urls.py

项目路由配置

from django.contrib import admin
from django.urls import path, include, re_path
from django.views.static import serve
from LuffyCity import settings
from Login.views import GeetestView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/course/', include("Course.urls")),
    path('api/shop/', include("shopping.urls")),
    path('api/', include("Login.urls")),
    path('pc-geetest/register', GeetestView.as_view()),
    path('pc-geetest/ajax_validate', GeetestView.as_view()),


    # media路径配置
    # path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT})
    re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT})
]
项目urls.py

一、课程模块

  1. 课程模块,包括了免费课程以及专题课程
  2. 主要是课程的展示,点击课程进入课程详细页面
  3. 课程详细页面展示,课程的概述,课程的价格策略,课程章节,评价以及常见问题

1、设计表结构

from django.db import models
# Create your models here.

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType

# Create your models here.
__all__ = ["Category", "Course", "CourseDetail", "Teacher", "DegreeCourse", "CourseChapter",
           "CourseSection", "PricePolicy", "OftenAskedQuestion", "Comment", "Account", "CourseOutline"]


class Category(models.Model):
    """课程分类表"""
    title = models.CharField(max_length=32, unique=True, verbose_name="课程的分类")

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "01-课程分类表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class Course(models.Model):
    """课程表"""
    title = models.CharField(max_length=128, unique=True, verbose_name="课程的名称")
    course_img = models.ImageField(upload_to="course/%Y-%m", verbose_name='课程的图片')
    category = models.ForeignKey(to="Category", verbose_name="课程的分类", on_delete=None)


    COURSE_TYPE_CHOICES = ((0, "付费"), (1, "vip专享"), (2, "学位课程"))
    course_type = models.SmallIntegerField(choices=COURSE_TYPE_CHOICES)
    degree_course = models.ForeignKey(to="DegreeCourse", blank=True, null=True, help_text="如果是学位课程,必须关联学位表", on_delete=None)


    brief = models.CharField(verbose_name="课程简介", max_length=1024)
    level_choices = ((0, '初级'), (1, '中级'), (2, '高级'))
    level = models.SmallIntegerField(choices=level_choices, default=1)

    status_choices = ((0, '上线'), (1, '下线'), (2, '预上线'))
    status = models.SmallIntegerField(choices=status_choices, default=0)
    pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True)

    order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排")
    study_num = models.IntegerField(verbose_name="学习人数", help_text="只要有人买课程,订单表加入数据的同时给这个字段+1")

    # order_details = GenericRelation("OrderDetail", related_query_name="course")
    # coupon = GenericRelation("Coupon")
    # 只用于反向查询不生成字段
    price_policy = GenericRelation("PricePolicy")
    often_ask_questions = GenericRelation("OftenAskedQuestion")
    course_comments = GenericRelation("Comment")

    def save(self, *args, **kwargs):
        if self.course_type == 2:
            if not self.degree_course:
                raise ValueError("学位课必须关联学位课程表")
        super(Course, self).save(*args, **kwargs)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "02-课程表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class CourseDetail(models.Model):
    """课程详细表"""
    course = models.OneToOneField(to="Course", on_delete=None)
    hours = models.IntegerField(verbose_name="课时", default=7)
    course_slogan = models.CharField(max_length=125, blank=True, null=True, verbose_name="课程口号")
    video_brief_link = models.CharField(max_length=255, blank=True, null=True)
    summary = models.TextField(max_length=2048, verbose_name="课程概述")
    why_study = models.TextField(verbose_name="为什么学习这门课程")
    what_to_study_brief = models.TextField(verbose_name="我将学到哪些内容")
    career_improvement = models.TextField(verbose_name="此项目如何有助于我的职业生涯")
    prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024)
    recommend_courses = models.ManyToManyField("Course", related_name="recommend_by", blank=True)

    teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师")

    def __str__(self):
        return self.course.title

    class Meta:
        verbose_name = "03-课程详细表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class Teacher(models.Model):
    """讲师表"""
    name = models.CharField(max_length=32, verbose_name="讲师名字")
    brief = models.TextField(max_length=1024, verbose_name="讲师介绍")

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = "04-教师表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class DegreeCourse(models.Model):
    """
    字段大体跟课程表相同,哪些不同根据业务逻辑去区分
    """
    title = models.CharField(max_length=32, verbose_name="学位课程名字")

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "05-学位课程表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class CourseChapter(models.Model):
    """课程章节表"""
    course = models.ForeignKey(to="Course", related_name="course_chapters", on_delete=None)
    chapter = models.SmallIntegerField(default=1, verbose_name="第几章")
    title = models.CharField(max_length=32, verbose_name="课程章节名称")

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "06-课程章节表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
        unique_together = ("course", "chapter")


class CourseSection(models.Model):
    """课时表"""
    chapter = models.ForeignKey(to="CourseChapter", related_name="course_sections", on_delete=None)
    title = models.CharField(max_length=32, verbose_name="课时")
    section_order = models.SmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时")
    section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频'))
    free_trail = models.BooleanField("是否可试看", default=False)
    section_type = models.SmallIntegerField(default=2, choices=section_type_choices)
    section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link")

    def course_chapter(self):
        return self.chapter.chapter

    def course_name(self):
        return self.chapter.course.title

    def __str__(self):
        return "%s-%s" % (self.chapter, self.title)

    class Meta:
        verbose_name = "07-课程课时表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
        unique_together = ('chapter', 'section_link')


class PricePolicy(models.Model):
    """价格策略表"""
    content_type = models.ForeignKey(ContentType, on_delete=None)  # 关联course or degree_course
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    valid_period_choices = ((1, '1天'), (3, '3天'),
                            (7, '1周'), (14, '2周'),
                            (30, '1个月'),
                            (60, '2个月'),
                            (90, '3个月'),
                            (120, '4个月'),
                            (180, '6个月'), (210, '12个月'),
                            (540, '18个月'), (720, '24个月'),
                            (722, '24个月'), (723, '24个月'),
                            )
    valid_period = models.SmallIntegerField(choices=valid_period_choices)
    price = models.FloatField()

    def __str__(self):
        return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price)

    class Meta:
        verbose_name = "08-价格策略表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
        unique_together = ("content_type", 'object_id', "valid_period")


class OftenAskedQuestion(models.Model):
    """常见问题"""
    content_type = models.ForeignKey(ContentType, on_delete=None)  # 关联course or degree_course
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    question = models.CharField(max_length=255)
    answer = models.TextField(max_length=1024)

    def __str__(self):
        return "%s-%s" % (self.content_object, self.question)

    class Meta:
        verbose_name = "09-常见问题表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
        unique_together = ('content_type', 'object_id', 'question')


class Comment(models.Model):
    """通用的评论表"""
    content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=None)
    object_id = models.PositiveIntegerField(blank=True, null=True)
    content_object = GenericForeignKey('content_type', 'object_id')

    content = models.TextField(max_length=1024, verbose_name="评论内容")
    account = models.ForeignKey("Account", verbose_name="会员名", on_delete=None)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.content

    class Meta:
        verbose_name = "10-评价表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class Account(models.Model):
    username = models.CharField(max_length=32, verbose_name="用户姓名")
    pwd = models.CharField(max_length=32, verbose_name="密文密码")
    # head_img = models.CharField(max_length=256, default='/static/frontend/head_portrait/logo@2x.png',
    #                             verbose_name="个人头像")
    balance = models.IntegerField(verbose_name="贝里余额", default=0)

    def __str__(self):
        return self.username

    class Meta:
        verbose_name = "11-用户表"
        db_table = verbose_name
        verbose_name_plural = verbose_name


class CourseOutline(models.Model):
    """课程大纲"""
    course_detail = models.ForeignKey(to="CourseDetail", related_name="course_outline", on_delete=None)
    title = models.CharField(max_length=128)
    order = models.PositiveSmallIntegerField(default=1)
    # 前端显示顺序

    content = models.TextField("内容", max_length=2048)

    def __str__(self):
        return "%s" % self.title

    class Meta:
        verbose_name = "12-课程大纲表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
        unique_together = ('course_detail', 'title')
Course中models.py

2、接口的编写

  • 课程这个模块,所有的功能都是展示,基于数据展示的,通常称为数据接口
  • 课程页面:有课程所有分类这个接口,有展示课程的接口
  • 课程详情页面:详情页面的数据接口
  • 详情页面下的子路由对应子组件的数据接口:课程章节课时、课程的评论、课程的常见问题
from django.urls import path
from .views import CategoryView, CourseView, CourseDetailView, CourseChapterView, CourseCommentView, QuestionView
from .video_view import PolyvView


urlpatterns = [
    path('category', CategoryView.as_view()),
    path('list', CourseView.as_view()),
    path('detail/<int:pk>', CourseDetailView.as_view()),
    path('chapter/<int:pk>', CourseChapterView.as_view()),
    path('comment/<int:pk>', CourseCommentView.as_view()),
    path('question/<int:pk>', QuestionView.as_view()),
    path('polyv', PolyvView.as_view()),

]
Course中urls.py
from rest_framework import serializers
from . import models


class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Category
        fields = "__all__"


class CourseSerializer(serializers.ModelSerializer):
    level = serializers.CharField(source="get_level_display")
    price = serializers.SerializerMethodField()

    def get_price(self, obj):
        print(obj.price_policy.all())
        return obj.price_policy.all().order_by("price").first().price

    class Meta:
        model = models.Course
        fields = ["id", "title", "course_img", "brief", "level", "study_num", "price"]


class CourseDetailSerializer(serializers.ModelSerializer):
    level = serializers.CharField(source="course.get_level_display")
    study_num = serializers.IntegerField(source="course.study_num")
    recommend_courses = serializers.SerializerMethodField()
    teachers = serializers.SerializerMethodField()
    price_policy = serializers.SerializerMethodField()
    course_outline = serializers.SerializerMethodField()


    def get_course_outline(self, obj):
        return [{"id": outline.id, "title": outline.title, "content": outline.content} for outline in obj.course_outline.all().order_by("order")]


    def get_price_policy(self, obj):
        return [{"id": price.id, "valid_price_display": price.get_valid_period_display(), "price": price.price} for price in obj.course.price_policy.all()]


    def get_teachers(self, obj):
        return [{"id": teacher.id, "name": teacher.name} for teacher in obj.teachers.all()]

    def get_recommend_courses(self, obj):
        return [{"id": course.id, "title": course.title} for course in obj.recommend_courses.all()]

    class Meta:
        model = models.CourseDetail
        fields = ["id", "hours", "summary", "level", "study_num", "recommend_courses", "teachers",
                  "price_policy", "course_outline"]


class CourseChapterSerializer(serializers.ModelSerializer):
    sections = serializers.SerializerMethodField()

    def get_sections(self, obj):
        return [{"id": section.id, "title": section.title, "free_trail": section.free_trail} for section in obj.course_sections.all().order_by("section_order")]

    class Meta:
        model = models.CourseChapter
        fields = ["id", "title", "sections"]


class CourseCommentSerializer(serializers.ModelSerializer):
    account = serializers.CharField(source="account.username")

    class Meta:
        model = models.Comment
        fields = ["id", "account", "content", "date"]


class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.OftenAskedQuestion
        fields = ["id", "question", "answer"]
Course中serializers.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from . import models
from .serializers import CategorySerializer, CourseSerializer, CourseDetailSerializer, CourseChapterSerializer
from .serializers import CourseCommentSerializer, QuestionSerializer

# Create your views here.


class CategoryView(APIView):
    """课程分类接口"""
    def get(self, request):
        # 通过ORM操作获取所有分类数据
        queryset = models.Category.objects.all()
        # 利用序列化器去序列化我们的数据
        ser_obj = CategorySerializer(queryset, many=True)
        # 返回
        return Response(ser_obj.data)


class CourseView(APIView):
    """查看所有免费课程的接口"""
    def get(self, request):
        # 获取过滤条件中的分类ID
        category_id = request.query_params.get("category", 0)
        # 根据分类获取课程
        if category_id == 0:
            # 证明没有分类,可以拿所有的课程数据
            queryset = models.Course.objects.all().order_by("order")
        else:
            queryset = models.Course.objects.filter(category_id=category_id).all().order_by("order")
        # 序列化课程数据
        ser_obj = CourseSerializer(queryset, many=True)
        # 返回
        return Response(ser_obj.data)


class CourseDetailView(APIView):
    """课程详情页面"""
    def get(self, request, pk):
        # 根据pk获取到课程详情对象
        course_detail_obj = models.CourseDetail.objects.filter(course__id=pk).first()
        if not course_detail_obj:
            return Response({"code": 1001, "error": "查询的课程详情不存在"})
        # 序列化课程详情
        ser_obj = CourseDetailSerializer(course_detail_obj)
        # 返回
        return Response(ser_obj.data)


class CourseChapterView(APIView):
    """课程章节接口"""
    def get(self, request, pk):
        # 数据结构["第一章": {课时一, 课时二}]
        queryset = models.CourseChapter.objects.filter(course_id=pk).all().order_by("chapter")
        # 序列化章节对象
        ser_obj = CourseChapterSerializer(queryset, many=True)
        # 返回
        return Response(ser_obj.data)


class CourseCommentView(APIView):
    def get(self, request, pk):
        # 通过课程id找到课程所有的评论
        queryset = models.Course.objects.filter(id=pk).first().course_comments.all()
        # 序列化
        ser_obj = CourseCommentSerializer(queryset, many=True)
        # 返回
        return Response(ser_obj.data)


class QuestionView(APIView):
    def get(self, request, pk):
        queryset = models.Course.objects.filter(id=pk).first().often_ask_questions.all()
        ser_obj = QuestionSerializer(queryset, many=True)
        return Response(ser_obj.data)
Course中views.py
from django.contrib import admin

# Register your models here.
from . import models

for table in models.__all__:
    admin.site.register(getattr(models, table))
Course中admin.py

二、登录认证模块(token存Redis)

  • 以前前后端不分离用cookie,session解决,现在前后端分离使用token令牌。
  • 用户登录成功后,生成一个随机字符串token给前端返回
  • 前端以后都携带这个token来访问,这样后端只需要鉴别这个token就可以做认证
import redis
POOL = redis.ConnectionPool(host="127.0.0.1", port=6379, decode_responses=True, max_connections=10)
utils中redis_pool.py
class BaseResponse(object):

    def __init__(self):
        self.code = 1000
        self.data = None
        self.error = None

    @property
    def dict(self):
        return self.__dict__
utils中base_response.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .redis_pool import POOL
from Course.models import Account
import redis



CONN = redis.Redis(connection_pool=POOL)


class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 从请求头中获取前端带过来的token
        token = request.META.get("HTTP_AUTHENTICATION", "")
        if not token:
            raise AuthenticationFailed("没有携带token")
        # 去redis比对
        user_id = CONN.get(str(token))
        if user_id == None:
            raise AuthenticationFailed("token过期")
        user_obj = Account.objects.filter(id=user_id).first()
        return user_obj, token
utils中my_auth.py
urlpatterns = [
    path('register', RegisterView.as_view()),
    path('login', LoginView.as_view()),
    path('test_auth', TestView.as_view()),

]
Login中urls.py
from rest_framework import serializers
from Course.models import Account
import hashlib


class RegisterSerializer(serializers.ModelSerializer):

    class Meta:
        model = Account
        fields = "__all__"

    def create(self, validated_data):
        pwd = validated_data["pwd"]
        pwd_salt = "luffy_password" + pwd
        md5_str = hashlib.md5(pwd_salt.encode()).hexdigest()
        user_obj = Account.objects.create(username=validated_data["username"], pwd=md5_str)
        return user_obj
Login中serializers.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import RegisterSerializer
from utils.base_response import BaseResponse
from Course.models import Account
from utils.redis_pool import POOL
import redis
import uuid
from utils.my_auth import LoginAuth
from utils.geetest import GeetestLib
from django.http import HttpResponse
import json

# Create your views here.


class RegisterView(APIView):

    def post(self, request):
        res = BaseResponse()
        # 用序列化器做校验
        ser_obj = RegisterSerializer(data=request.data)
        if ser_obj.is_valid():
            ser_obj.save()
            res.data = ser_obj.data
        else:
            res.code = 1020
            res.error = ser_obj.errors
        return Response(res.dict)


class LoginView(APIView):

    def post(self, request):
        res = BaseResponse()
        username = request.data.get("username", "")
        pwd = request.data.get("pwd", "")
        user_obj = Account.objects.filter(username=username, pwd=pwd).first()
        if not user_obj:
            res.code = 1030
            res.error = "用户名或密码错误"
            return Response(res.dict)

        # 用户登录成功生成一个token写入redis
        # 写入redis  token : user_id
        conn = redis.Redis(connection_pool=POOL)
        try:
            token = uuid.uuid4()
            # conn.set(str(token), user_obj.id, ex=10)
            conn.set(str(token), user_obj.id)
            res.data = token
        except Exception as e:
            print(e)
            res.code = 1031
            res.error = "创建令牌失败"
        return Response(res.dict)


class TestView(APIView):
    authentication_classes = [LoginAuth, ]

    def get(self, request):
        return Response("认证测试")


pc_geetest_id = "b46d1900d0a894591916ea94ea91bd2c"
pc_geetest_key = "36fc3fe98530eea08dfc6ce76e3d24c4"
REDIS_CONN = redis.Redis(connection_pool=POOL)


class GeetestView(APIView):

    def get(self, request):
        user_id = 'test'
        gt = GeetestLib(pc_geetest_id, pc_geetest_key)
        status = gt.pre_process(user_id)
        # request.session[gt.GT_STATUS_SESSION_KEY] = status
        REDIS_CONN.set(gt.GT_STATUS_SESSION_KEY, status)
        # request.session["user_id"] = user_id
        REDIS_CONN.set("gt_user_id", user_id)
        response_str = gt.get_response_str()
        return HttpResponse(response_str)

    def post(self, request):
        # print(request.session.get("user_id"))
        print(request.META.get("HTTP_AUTHENTICATION"))
        print(request.data)
        gt = GeetestLib(pc_geetest_id, pc_geetest_key)
        challenge = request.data.get(gt.FN_CHALLENGE, '')
        validate = request.data.get(gt.FN_VALIDATE, '')
        seccode = request.data.get(gt.FN_SECCODE, '')
        # username
        # pwd
        # status = request.session.get(gt.GT_STATUS_SESSION_KEY)
        # print(status)
        # user_id = request.session.get("user_id")
        # print(user_id)
        status = REDIS_CONN.get(gt.GT_STATUS_SESSION_KEY)
        user_id = REDIS_CONN.get("gt_user_id")
        if status:
            result = gt.success_validate(challenge, validate, seccode, user_id)
        else:
            result = gt.failback_validate(challenge, validate, seccode)
        result = {"status": "success"} if result else {"status": "fail"}
        # if result:
        #     # 证明验证码通过
        #     # 判断用户名和密码
        # else:
        #     #  返回验证码错误
        return HttpResponse(json.dumps(result))
Login中views.py

三、登录认证模块(token存mysql)

# 拓展之前课程模块下的用户表
class Account(models.Model):
    username = models.CharField(max_length=32, verbose_name="用户姓名", unique=True)
    password = models.CharField(max_length=32, verbose_name="用户密码")
    # head_img = models.CharField(max_length=256, default='/static/frontend/head_portrait/logo@2x.png',
    #                             verbose_name="个人头像")
    token = models.UUIDField(null=True, blank=True)

    def __str__(self):
        return self.username

    class Meta:
        verbose_name = "11-用户表"
        db_table = verbose_name
        verbose_name_plural = verbose_name
models.py 扩展之前功能模块的用户表
class BaseResponse(object):

    def __init__(self):
        self.code = 1000
        self.data = None
        self.error = None

    @property
    def dict(self):
        return self.__dict__
utils中base_response.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from Course.models import Account
# django 提供的拿时间的接口 提供的是根据django配置的时区拿到的当前时间
from django.utils.timezone import now


class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 从请求头中获取前端带过来的token
        token = request.META.get("HTTP_AUTHENTICATION", "")
        if not token:
            raise AuthenticationFailed("没有携带token")
        # 去redis比对
        user_obj = Account.objects.filter(token=token).first()
        if not user_obj:
            raise AuthenticationFailed("token过期")
        else:
            old_time = user_obj.create_token_time
            if (now() - old_time).days > 7:
                raise AuthenticationFailed({"code": 1020, "error": "无效的token"})
            return user_obj, token
utils中my_auth.py
urlpatterns = [
    path('register', RegisterView.as_view()),
    path('login', LoginView.as_view()),
    path('test_auth', TestView.as_view()),

]
Login中urls.py
from rest_framework import serializers
from Course.models import Account
import hashlib


class RegisterSerializer(serializers.ModelSerializer):

    class Meta:
        model = Account
        fields = "__all__"

    def create(self, validated_data):
        pwd = validated_data["pwd"]
        pwd_salt = "luffy_password" + pwd
        md5_str = hashlib.md5(pwd_salt.encode()).hexdigest()
        user_obj = Account.objects.create(username=validated_data["username"], pwd=md5_str)
        return user_obj
Login中serializers.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import RegisterSerializer
from utils.base_response import BaseResponse
from Course.models import Account
import uuid



# Create your views here.

class RegisterView(APIView):

    def post(self, request):
        res = BaseResponse()
        # 用序列化器做校验
        ser_obj = RegisterSerializer(data=request.data)
        if ser_obj.is_valid():
            ser_obj.save()
            res.data = ser_obj.data
        else:
            res.code = 1020
            res.error = ser_obj.errors
        return Response(res.dict)


class LoginView(APIView):

    def post(self, request):
        res = BaseResponse()
        username = request.data.get("username", "")
        pwd = request.data.get("pwd", "")
        user_obj = Account.objects.filter(username=username, pwd=pwd).first()
        if not user_obj:
            res.code = 1030
            res.error = "用户名或密码错误"
            return Response(res.dict)

        try:
            token = uuid.uuid4()
            ###
            user_obj.update(token=token)
            res.data = token
        except Exception as e:
            print(e)
            res.code = 1031
            res.error = "创建令牌失败"
        return Response(res.dict)

# 所有这是一个需要认证的接口
class TestView(APIView):
    authentication_classes = [LoginAuth, ]

    def get(self, request):
        return Response("认证测试")
Login中views.py

四、购物车模块

  1. 用户点击商品加入购物车,个人中心可以查看自己所有购物车中数据
  2. 在购物车中可以删除课程,还可以更新购物车中课程的价格策略
  3. 所以接口应该有四种请求方式, get,post,patch,delete
  4. 因为购物车是属于中间状态数据而且很多时候需要过期时间所以选择存储到redis
from django.urls import path
from .views import ShoppingCarView
from .settlement_view import SettlementView
from .payment_view import PaymentView

urlpatterns = [
    path('shopping_car', ShoppingCarView.as_view()),
    path('settlement', SettlementView.as_view()),
    path('payment', PaymentView.as_view()),
]
shopping中urls.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .redis_pool import POOL
from Course.models import Account
import redis



CONN = redis.Redis(connection_pool=POOL)


class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
        # 从请求头中获取前端带过来的token
        token = request.META.get("HTTP_AUTHENTICATION", "")
        if not token:
            raise AuthenticationFailed("没有携带token")
        # 去redis比对
        user_id = CONN.get(str(token))
        if user_id == None:
            raise AuthenticationFailed("token过期")
        user_obj = Account.objects.filter(id=user_id).first()
        return user_obj, token
utils中my_auth.py
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.base_response import BaseResponse
from utils.my_auth import LoginAuth
from utils.redis_pool import POOL
from Course.models import Course
import json
import redis

# Create your views here.

# 前端传过来 course_id  price_policy_id
# 把购物车数据放入redis
"""
{
    SHOPPINGCAR_USERID_COURSE_ID: {
        "id",   课程id
        "title",  课程标题
        "course_img", 
        "price_policy_dict": {
            price_policy_id: "{valid_period,  price, valid_period_display}"
            price_policy_id2: "{valid_period,  price, valid_period_display}"
            price_policy_id3: "{valid_period,  price, valid_period_display}"
        
        },
        "default_price_policy_id": 1  默认选中的价格id
        
    
    }


}
"""

SHOPPINGCAR_KEY = "SHOPPINGCAR_%s_%s"
CONN = redis.Redis(connection_pool=POOL)


class ShoppingCarView(APIView):
    authentication_classes = [LoginAuth, ]
    # 给购物车增加商品
    def post(self, request):
        res = BaseResponse()
        try:
            # 1, 获取前端传过来的数据以及user_id
            course_id = request.data.get("course_id", "")
            price_policy_id = request.data.get("price_policy_id", "")
            user_id = request.user.pk
            # 2, 校验数据的合法性
            # 2.1 校验课程id合法性
            course_obj = Course.objects.filter(id=course_id).first()
            if not course_obj:
                res.code = 1040
                res.error = "课程id不合法"
                return Response(res.dict)
            # 2.2 校验价格策略id是否合法
            price_policy_queryset = course_obj.price_policy.all()
            price_policy_dict = {}
            for price_policy in price_policy_queryset:
                price_policy_dict[price_policy.id] = {
                    "price": price_policy.price,
                    "valid_period": price_policy.valid_period,
                    "valid_period_display": price_policy.get_valid_period_display()
                }
            if price_policy_id not in price_policy_dict:
                res.code = 1041
                res.error = "价格策略id不合法"
                return Response(res.dict)
            # 3,构建redisKEY
            key = SHOPPINGCAR_KEY % (user_id, course_id)
            # 4,构建数据结构
            course_info = {
                "id": course_obj.id,
                "title": course_obj.title,
                "course_img": str(course_obj.course_img),
                "price_policy_dict": json.dumps(price_policy_dict, ensure_ascii=False),
                "default_price_policy_id": price_policy_id
            }
            # 5  写入redis
            CONN.hmset(key, course_info)
            res.data = "加入购物车成功"
        except Exception as e:
            res.code = 1012
            res.error = "加入购物车失败"
        return Response(res.dict)

    def get(self, request):
        res = BaseResponse()
        try:
            # 1, 拼接redis key
            user_id = request.user.pk
            shopping_car_key = SHOPPINGCAR_KEY % (user_id, "*")
            # 2, 去redis中读取数据
            # 2.1 模糊匹配所有的keys
            # 3,构建数据结构展示
            all_keys = CONN.scan_iter(shopping_car_key)
            ret = []
            for key in all_keys:
                ret.append(CONN.hgetall(key))
            res.data = ret
        except Exception as e:
            res.code = 1013
            res.error = "获取购物车失败"
        return Response(res.dict)

    def put(self, request):
        # 前端 course_id  price_policy_id
        res = BaseResponse()
        try:
            # 1, 获取前端传过来的数据以及user_id
            course_id = request.data.get("course_id", "")
            price_policy_id = request.data.get("price_policy_id", "")
            user_id = request.user.pk
            # 2, 校验数据的合法性
            # 2.1 course_id是否合法
            key = SHOPPINGCAR_KEY % (user_id, course_id)
            if not CONN.exists(key):
                res.code = 1043
                res.error = "课程id不合法"
                return Response(res.dict)
            # 2,2 price_policy_id是否合法
            price_policy_dict = json.loads(CONN.hget(key, "price_policy_dict"))
            if str(price_policy_id) not in price_policy_dict:
                res.code = 1044
                res.error = "价格策略不合法"
                return Response(res.dict)
            # 3, 更新redis  default_price_policy_id
            CONN.hset(key, "default_price_policy_id", price_policy_id)
            res.data = "更新成功"
        except Exception as e:
            res.code = 1014
            res.error = "更新购物车失败"
        return Response(res.dict)

    def delete(self, request):
        # course_list = [course_id, ]
        res = BaseResponse()
        try:
            # 1 获取前端传来的数据以及user_id
            course_list = request.data.get("course_list", "")
            user_id = request.user.pk
            # 2 校验course_id是否合法
            for course_id in course_list:
                key = SHOPPINGCAR_KEY % (user_id, course_id)
                if not CONN.exists(key):
                    res.code = 1045
                    res.error = "课程ID不合法"
                    return Response(res.dict)
                # 3, 删除redis数据
                CONN.delete(key)
            res.data = "删除成功"
        except Exception as e:
            res.code = 1014
            res.error = "删除购物车失败"
        return Response(res.dict)
shopping中views.py

五、结算中心模块

结算中心要开始选择优惠券了,有单独的课程优惠券还有全局优惠券。

from django.db import models

# Create your models here.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from Course.models import Account

# Create your models here.
__all__ = ["Coupon", "CouponRecord", "Order", "OrderDetail", "TransactionRecord"]


class Coupon(models.Model):
    """优惠券生成规则"""
    name = models.CharField(max_length=64, verbose_name="活动名称")
    brief = models.TextField(blank=True, null=True, verbose_name="优惠券介绍")
    coupon_type_choices = ((0, '通用券'), (1, '满减券'), (2, '折扣券'))
    coupon_type = models.SmallIntegerField(choices=coupon_type_choices, default=0, verbose_name="券类型")

    money_equivalent_value = models.IntegerField(verbose_name="等值货币", null=True, blank=True, default=0)
    off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True, default=100)
    minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段", null=True, blank=True)

    content_type = models.ForeignKey(ContentType, blank=True, null=True, on_delete=None)
    object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定")
    # 不绑定代表全局优惠券
    content_object = GenericForeignKey('content_type', 'object_id')

    open_date = models.DateField("优惠券领取开始时间")
    close_date = models.DateField("优惠券领取结束时间")
    valid_begin_date = models.DateField(verbose_name="有效期开始时间", blank=True, null=True)
    valid_end_date = models.DateField(verbose_name="有效结束时间", blank=True, null=True)
    coupon_valid_days = models.PositiveIntegerField(verbose_name="优惠券有效期(天)", blank=True, null=True,
                                                    help_text="自券被领时开始算起")
    date = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = "13. 优惠券生成规则记录"
        db_table = verbose_name_plural
        verbose_name = verbose_name_plural

    def __str__(self):
        return "%s(%s)" % (self.get_coupon_type_display(), self.name)

    def save(self, *args, **kwargs):
        if not self.coupon_valid_days or (self.valid_begin_date and self.valid_end_date):
            if self.valid_begin_date and self.valid_end_date:
                if self.valid_end_date <= self.valid_begin_date:
                    raise ValueError("valid_end_date 有效期结束日期必须晚于 valid_begin_date ")
            if self.coupon_valid_days == 0:
                raise ValueError("coupon_valid_days 有效期不能为0")
        if self.close_date < self.open_date:
            raise ValueError("close_date 优惠券领取结束时间必须晚于 open_date优惠券领取开始时间 ")

        super(Coupon, self).save(*args, **kwargs)


class CouponRecord(models.Model):
    """优惠券发放、消费纪录"""
    coupon = models.ForeignKey("Coupon", on_delete=None)
    number = models.CharField(max_length=64, unique=True, verbose_name="用户优惠券记录的流水号")
    account = models.ForeignKey(to=Account, verbose_name="拥有者", on_delete=None)
    status_choices = ((0, '未使用'), (1, '已使用'), (2, '已过期'))
    status = models.SmallIntegerField(choices=status_choices, default=0)
    get_time = models.DateTimeField(verbose_name="领取时间", help_text="用户领取时间")
    used_time = models.DateTimeField(blank=True, null=True, verbose_name="使用时间")
    order = models.ForeignKey("Order", blank=True, null=True, verbose_name="关联订单", on_delete=None)  # 一个订单可以有多个优惠券

    class Meta:
        verbose_name_plural = "14. 用户优惠券领取使用记录表"
        db_table = verbose_name_plural
        verbose_name = verbose_name_plural

    def __str__(self):
        return '%s-%s-%s' % (self.account, self.number, self.status)


class Order(models.Model):
    """订单"""
    payment_type_choices = ((0, '微信'), (1, '支付宝'), (2, '优惠码'), (3, '贝里'))
    payment_type = models.SmallIntegerField(choices=payment_type_choices)

    payment_number = models.CharField(max_length=128, verbose_name="支付第3方订单号", null=True, blank=True)
    order_number = models.CharField(max_length=128, verbose_name="订单号", unique=True)  # 考虑到订单合并支付的问题
    account = models.ForeignKey(to=Account, on_delete=None)
    actual_amount = models.FloatField(verbose_name="实付金额")

    status_choices = ((0, '交易成功'), (1, '待支付'), (2, '退费申请中'), (3, '已退费'), (4, '主动取消'), (5, '超时取消'))
    status = models.SmallIntegerField(choices=status_choices, verbose_name="状态")
    date = models.DateTimeField(auto_now_add=True, verbose_name="订单生成时间")
    pay_time = models.DateTimeField(blank=True, null=True, verbose_name="付款时间")
    cancel_time = models.DateTimeField(blank=True, null=True, verbose_name="订单取消时间")

    class Meta:
        verbose_name_plural = "15. 订单表"
        db_table = verbose_name_plural
        verbose_name = verbose_name_plural

    def __str__(self):
        return "%s" % self.order_number


class OrderDetail(models.Model):
    """订单详情"""
    order = models.ForeignKey("Order", on_delete=None)

    content_type = models.ForeignKey(ContentType, on_delete=None)  # 可关联普通课程或学位
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    original_price = models.FloatField("课程原价")
    price = models.FloatField("折后价格")
    valid_period_display = models.CharField("有效期显示", max_length=32)  # 在订单页显示
    valid_period = models.PositiveIntegerField("有效期(days)")  # 课程有效期
    memo = models.CharField(max_length=255, blank=True, null=True, verbose_name="备忘录")

    def __str__(self):
        return "%s - %s - %s" % (self.order, self.content_type, self.price)

    class Meta:
        verbose_name_plural = "16. 订单详细"
        db_table = verbose_name_plural
        verbose_name = verbose_name_plural


class TransactionRecord(models.Model):
    """贝里交易纪录"""
    account = models.ForeignKey(to=Account, on_delete=None)
    amount = models.IntegerField("金额")
    balance = models.IntegerField("账户余额")
    transaction_type_choices = ((0, '收入'), (1, '支出'), (2, '退款'), (3, "提现"))  # 2 为了处理 订单过期未支付时,锁定期贝里的回退
    transaction_type = models.SmallIntegerField(choices=transaction_type_choices)
    transaction_number = models.CharField(unique=True, verbose_name="流水号", max_length=128)
    date = models.DateTimeField(auto_now_add=True)
    memo = models.CharField(max_length=128, blank=True, null=True, verbose_name="备忘录")

    class Meta:
        verbose_name_plural = "17. 贝里交易记录"
        db_table = verbose_name_plural
        verbose_name = verbose_name_plural

    def __str__(self):
        return "%s" % self.transaction_number
shopping中models.py
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.base_response import BaseResponse
from utils.redis_pool import POOL
from django.utils.timezone import now
from utils.my_auth import LoginAuth
import redis
from .views import SHOPPINGCAR_KEY
from .models import CouponRecord
import json


CONN = redis.Redis(connection_pool=POOL)
SETTLEMENT_KEY = "SETTLEMENT_%s_%s"
GLOBAL_COUPON_KEY = "GLOBAL_COUPON_%s"
"""
结算中心
在购物车里选择了商品以及价格策略点击结算 才进入结算中心
在结算中心用户可以选择优惠券

前端传过来数据 course_list
redis = {
    settlement_userid_courseid: {
            id, 课程id,
            title,
            course_img,
            valid_period_display,
            price,
            course_coupon_dict: {
                coupon_id: {优惠券信息}
                coupon_id2: {优惠券信息}
                coupon_id3: {优惠券信息}
            }
            # 默认不给你选  这个字段只有更新的时候才添加
            default_coupon_id: 1  
    }
    
    global_coupon_userid: {
        coupon_id: {优惠券信息}
        coupon_id2: {优惠券信息}
        coupon_id3: {优惠券信息},
        # 这个字段只有更新的时候才添加
        # 在用户进入结算中心选择优惠券的时候 也就是更新请求的时候更改 
        default_global_coupon_id: 1
    
    }

}
"""


class SettlementView(APIView):
    authentication_classes = [LoginAuth, ]

    def post(self, request):
        res = BaseResponse()
        try:
            # 1 获取前端的数据以及user_id
            course_list = request.data.get("course_list", "")
            user_id = request.user.pk
            # 2 校验数据的合法性
            for course_id in course_list:
                # 2.1 判断course_id 是否在购物车中
                shopping_car_key = SHOPPINGCAR_KEY % (user_id, course_id)
                if not CONN.exists(shopping_car_key):
                    res.code = 1050
                    res.error = "课程ID不合法"
                    return Response(res.dict)
                # 3 构建数据结构
                # 3.1 获取用户的所有合法优惠券
                user_all_coupons = CouponRecord.objects.filter(
                    account_id=user_id,
                    status=0,
                    coupon__valid_begin_date__lte=now(),
                    coupon__valid_end_date__gte=now(),
                ).all()
                print(user_all_coupons)
                # 3.2 构建优惠券dict
                course_coupon_dict = {}
                global_coupon_dict = {}
                for coupon_record in user_all_coupons:
                    coupon = coupon_record.coupon
                    if coupon.object_id == course_id:
                        course_coupon_dict[coupon.id] = {
                            "id": coupon.id,
                            "name": coupon.name,
                            "coupon_type": coupon.get_coupon_type_display(),
                            "object_id": coupon.object_id,
                            "money_equivalent_value": coupon.money_equivalent_value,
                            "off_percent": coupon.off_percent,
                            "minimum_consume": coupon.minimum_consume
                        }
                    elif coupon.object_id == "":
                        global_coupon_dict[coupon.id] = {
                            "id": coupon.id,
                            "name": coupon.name,
                            "coupon_type": coupon.get_coupon_type_display(),
                            "money_equivalent_value": coupon.money_equivalent_value,
                            "off_percent": coupon.off_percent,
                            "minimum_consume": coupon.minimum_consume
                        }
                # 3.3 构建写入redis的数据结构
                course_info = CONN.hgetall(shopping_car_key)
                price_policy_dict = json.loads(course_info["price_policy_dict"])
                default_policy_id = course_info["default_price_policy_id"]
                valid_period = price_policy_dict[default_policy_id]["valid_period_display"]
                price = price_policy_dict[default_policy_id]["price"]

                settlement_info = {
                    "id": course_info["id"],
                    "title": course_info["title"],
                    "course_img": course_info["course_img"],
                    "valid_period": valid_period,
                    "price": price,
                    "course_coupon_dict": json.dumps(course_coupon_dict, ensure_ascii=False)
                }
                # 4 写入redis
                settlement_key = SETTLEMENT_KEY % (user_id, course_id)
                global_coupon_key = GLOBAL_COUPON_KEY % user_id
                CONN.hmset(settlement_key, settlement_info)
                if global_coupon_dict:
                    CONN.hmset(global_coupon_key, global_coupon_dict)
                # 5 删除购物车中的数据
                CONN.delete(shopping_car_key)
            res.data = "加入结算中心成功"
        except Exception as e:
            res.code = 1020
            res.error = "结算失败"
        return Response(res.dict)

    def get(self, request):
        res = BaseResponse()
        try:
            # 1, 获取user_id
            user_id = request.user.pk
            # 2,  拼接所有key
            # 3, 去redis取数据
            settlement_key = SETTLEMENT_KEY % (user_id, "*")
            global_coupon_key = GLOBAL_COUPON_KEY % user_id
            all_keys = CONN.scan_iter(settlement_key)
            ret = []
            for key in all_keys:
                ret.append(CONN.hgetall(key))
            global_coupon_info = CONN.hgetall(global_coupon_key)
            res.data = {
                "settlement_info": ret,
                "global_coupon_dict": global_coupon_info
            }
        except Exception as e:
            res.code = 1024
            res.error = "获取结算中心失败"
        return Response(res.dict)

    def put(self, request):
        # course_id  course_coupon_id  global_coupon_id
        res = BaseResponse()
        try:
            # 1, 获取前端传过来数据
            course_id = request.data.get("course_id", "")
            course_coupon_id = request.data.get("course_coupon_id", "")
            global_coupon_id = request.data.get("global_coupon_id", "")
            user_id = request.user.pk
            # 2, 校验数据合法性
            # 2.1 校验course_id
            key = SETTLEMENT_KEY % (user_id, course_id)
            if course_id:
                if not CONN.exists(key):
                    res.code = 1060
                    res.error = "课程ID不合法"
                    return Response(res.dict)
            # 2.2 校验 course_coupon_id
            if course_coupon_id:
                course_coupon_dict = json.loads(CONN.hget(key, "course_coupon_dict"))
                if str(course_coupon_id) not in course_coupon_dict:
                    res.code = 1061
                    res.error = "课程优惠券ID不合法"
                    return Response(res.dict)
            # 2.3 校验global_coupon_id
            if global_coupon_id:
                global_coupon_key = GLOBAL_COUPON_KEY % user_id
                if not CONN.exists(global_coupon_key):
                    res.code = 1062
                    res.error = "全局优惠券ID不合法"
                    return Response(res.dict)
                CONN.hset(global_coupon_key, "default_global_coupon_id", global_coupon_id)
            # 3,修改redis中数据
            CONN.hset(key, "default_coupon_id", course_coupon_id)
            res.data = "更新优惠券成功"
        except Exception as e:
            res.code = 1026
            res.error = "更改优惠券失败"
        return Response(res.dict)
shopping中settlement_view.py

六、支付中心模块

from rest_framework.views import APIView
from rest_framework.response import Response
from utils.my_auth import LoginAuth
from utils.base_response import BaseResponse
from .settlement_view import SETTLEMENT_KEY, GLOBAL_COUPON_KEY
from utils.redis_pool import POOL
import redis
from Course.models import Course
from .models import Coupon
from django.utils.timezone import now


COON = redis.Redis(connection_pool=POOL)

"""
点击去支付 -POST-》 校验 课程 价格策略 课程优惠券 通用优惠券  贝里 和 应收价钱是否一致
生成订单(未支付状态)   ---->  引导你跳转到支付宝 二维码页面
支付宝 支付成功   - POST请求   -》 我  我收到请求去修改订单的状态为已支付
5秒之后 会            -  GET 请求    -》 我给他返回一个订单页面(支付成功) 
"""


#  price  balance
class PaymentView(APIView):
    authentication_classes = [LoginAuth, ]

    def post(self, request):
        res = BaseResponse()
        # 1 获取数据
        balance = request.data.get("balance", 0)
        price = request.data.get("price", "")
        user_id = request.user.pk
        # 2 校验数据的合法性
        # 2.1 校验贝里数是否合法
        if int(balance) > request.user.balance:
            res.code = 1070
            res.error = "抵扣的贝里错误"
            return Response(res.dict)
        # 2.2 从用户的结算中心拿数据 跟数据库比对是否合法
        settlement_key = SETTLEMENT_KEY % (user_id, "*")
        all_keys = COON.scan_iter(settlement_key)
        # 课程id是否合法
        course_rebate_total_price = 0
        for key in all_keys:
            settlement_info = COON.hgetall(key)
            course_id = settlement_info["id"]
            course_obj = Course.objects.filter(id=course_id).first()
            if not course_obj or course_obj.status == 1:
                res.code = 1071
                res.error = "课程id不合法"
                return Response(res.dict)
            # 课程优惠券是否过期
            course_coupon_id = settlement_info.get("default_coupon_id", 0)
            if course_coupon_id:
                coupon_dict = Coupon.objects.filter(
                            id=course_coupon_id,
                            couponrecord__status=0,
                            couponrecord__account_id=user_id,
                            object_id=course_id,
                            valid_begin_date__lte=now(),
                            valid_end_date__gte=now(),
                            ).values("coupon_type", "money_equivalent_value", "off_percent", "minimum_consume")
            if not coupon_dict:
                res.code = 1072
                res.error = "优惠券不合法"
                return Response(res.dict)
            # 2.3 校验price
            # 得到所有的课程的折后价格和
            course_pirce = settlement_info["price"]
            course_rebate_price = self.account_price(coupon_dict, course_pirce)
            if course_rebate_price == -1:
                res.code = 1074
                res.error = "课程优惠券不符合要求"
                return Response(res.dict)
            course_rebate_total_price += course_rebate_price
        # 跟全局优惠券做折扣
        # 校验全局优惠券是否合法
        global_coupon_key = GLOBAL_COUPON_KEY % user_id
        global_coupon_id = int(COON.hget(global_coupon_key, "default_global_coupon_id"))
        if global_coupon_id:
            global_coupon_dict = Coupon.objects.filter(
                id=global_coupon_id,
                couponrecord__status=0,
                couponrecord__account_id=user_id,
                valid_begin_date__lte=now(),
                valid_end_date__gte=now(),
            ).values("coupon_type", "money_equivalent_value", "off_percent", "minimum_consume")
        if not global_coupon_dict:
            res.code = 1073
            res.error = "全局优惠券id不合法"
            return Response(res.dict)
        global_rebate_price = self.account_price(global_coupon_dict, course_rebate_total_price)
        if global_rebate_price == -1:
            res.code = 1076
            res.error = "全局优惠券不符合要求"
            return Response(res.dict)
        # 抵扣贝里
        balance_money = balance / 100
        balance_rebate_price = global_rebate_price - balance
        if balance_rebate_price < 0:
            balance_rebate_price = 0
        # 终极校验price
        if balance_rebate_price != price:
            res.code = 1078
            res.error = "价格不合法"
            return Response(res.dict)
        # 先去创建订单  订单状态未支付状态
        # 3 调用支付宝接口支付
            # 如果成功支付支付宝会给我们发回调
            # 改变订单的状态
            # 注意订单详情表有多个记录
            # 更改优惠券的使用状态
            # 更改用户表里的贝里 贝里要添加交易记录







    def account_price(self, coupon_dict, price):
        coupon_type = coupon_dict["coupon_type"]
        if coupon_type == 0:
            # 通用优惠券
            money_equivalent_value = coupon_dict["money_equivalent_value"]
            if price - money_equivalent_value >=0:
                rebate_price = price - money_equivalent_value
            else:
                rebate_price = 0
        elif coupon_type == 1:
            # 满减
            money_equivalent_value = coupon_dict["money_equivalent_value"]
            minimum_consume = coupon_dict["minimum_consume"]
            if price >= minimum_consume:
                rebate_price = price - money_equivalent_value
            else:
                return -1
        elif coupon_type == 2:
            # 折扣
            minimum_consume = coupon_dict["minimum_consume"]
            off_percent = coupon_dict["off_percent"]
            if price >= minimum_consume:
                rebate_price = price * (off_percent / 100)
            else:
                return -1
        return rebate_price
shopping中payment_view.py

 

posted @ 2019-03-08 05:46  silencio。  阅读(323)  评论(0编辑  收藏  举报