课程模块

我们要开始写课程模块了~~课程模块都有哪些功能呢~~

我们的课程模块,包括了免费课程以及专题课程两个方向~~

主要是课程的展示,点击课程进入课程详细页面~~

课程详细页面展示,课程的概述,课程的价格策略,课程章节,评价以及常见问题~~

以及一些周边的字段~~~这是主线路~~

根据功能设计表结构

对照着对标路飞网站,我们可以先讨论下表结构~~

我们初步能够确定下来12张表~~

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="课程的分类")
    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="如果是学位课程,必须关联学位表")

    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")
    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")
    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")
    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)  # 关联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)  # 关联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)
    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="会员名")
    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="用户姓名")
    # head_img = models.CharField(max_length=256, default='/static/frontend/head_portrait/logo@2x.png',
    #                             verbose_name="个人头像")

    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")
    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')
课程模块表结构

接口的编写

我们表结构定下来以后,可以根据业务场景看需要哪些接口~~~

对于我们课程这个模块,所有的功能都是展示,基于数据展示的,我们通常称为数据接口~~

这种接口对于我们来说是最简单的~~因为我们只是从数据库拿数据~然后进行展示~~

我们来看下都需要哪些接口~~~

  -- 课程页面  有课程所有分类这个接口

  -- 课程页面  有展示课程的接口

  -- 点击课程进入课程详情页面,详情页面的数据接口~~

  -- 详情页面下的子路由对应子组件的数据接口

    -- 课程章节课时

    -- 课程的评论

    -- 课程的常见问题

这些所有的数据接口~~套路都是读取数据库,序列化数据,返回~~~

那主要就是用DRF的序列化组件~~那我们从路由~然后序列化组件~然后看视图~~

# by gaoxin
from django.conf.urls import url, include
from .views import CourseListView, CourseCategoryView, CourseDetailView, CourseChapterView
from .views import CourseCommentView, OftenAskedQuestionView

# 在项目路由下做了一层分发
# url(r'^api/course/', include("course.urls")),
urlpatterns = [
    url(r"^$", CourseListView.as_view()),
    url(r"^category$", CourseCategoryView.as_view()),
    url(r"^detail/(?P<pk>\d+)$", CourseDetailView.as_view()),
    url(r"^(?P<pk>\d+)/chapters$", CourseChapterView.as_view()),
    url(r"^(?P<pk>\d+)/comment$", CourseCommentView.as_view()),
    url(r"^(?P<pk>\d+)/often-asked-question$", OftenAskedQuestionView.as_view()),
]
urls.py
# by gaoxin
from rest_framework import serializers
from .models import Course, Category, CourseDetail, CourseChapter, Comment, OftenAskedQuestion


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

    def get_price_policy(self, obj):
        price_obj = obj.price_policy.all().order_by("price").first()
        return price_obj.price

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


class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ["id", "title"]


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

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

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

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

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

    class Meta:
        model = CourseDetail
        fields = ["id", "summary", "hours", "level", "study_num", "price_policy", "why_study",
                  "what_to_study_brief", "course_outline", "career_improvement", "prerequisite",
                  "recommend_courses", "teachers"]


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

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

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


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

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


class OftenAskQuestionSerializer(serializers.ModelSerializer):

    class Meta:
        model = OftenAskedQuestion
        fields = ["id", "question", "answer"]
serializers.py
class CourseCategoryView(APIView):
    """课程分类接口"""

    def get(self, request):
        queryset = Category.objects.all()
        ser_obj = CourseCategorySerializer(queryset, many=True)
        return Response(ser_obj.data)


class CourseListView(APIView):
    """查看所有免费课程的接口"""
    def get(self, request):
        category_id = request.query_params.get("category", 0)
        if category_id == 0:
            # 证明没有分类,可以拿所有的课程数据
            queryset = Course.objects.all().order_by("order")
        else:
            queryset = Course.objects.filter(category_id=category_id).order_by("order")
        ser_obj = CourseSerializer(queryset, many=True)
        return Response(ser_obj.data)


class CourseDetailView(APIView):
    """课程详情页面"""
    def get(self, request, pk):
        course_detail_obj = CourseDetail.objects.filter(course__id=pk).first()
        if course_detail_obj:
            ser_obj = CourseDetailSerializer(course_detail_obj)
            return Response(ser_obj.data)
        else:
            return Response({"code": "1001", "msg": "查询的课程不存在"})


class CourseChapterView(APIView):
    """课程章节接口"""
    def get(self, request, pk):
        # 首先我们要清楚数据结构
        # 我们要的是[章节一:{课时,课时2}]
        queryset = CourseChapter.objects.filter(course_id=pk).order_by("chapter")
        ser_obj = CourseChapterSerializer(queryset, many=True)
        return Response(ser_obj.data)


class CourseCommentView(APIView):
    def get(self, request, pk):
        queryset = Course.objects.filter(pk=pk).first().course_comments.all()
        ser_obj = CourseCommentSerializer(queryset, many=True)
        return Response(ser_obj.data)


class OftenAskedQuestionView(APIView):
    def get(self, request, pk):
        queryset = Course.objects.filter(pk=pk).first().often_ask_questions.all()
        ser_obj = OftenAskQuestionSerializer(queryset, many=True)
        return Response(ser_obj.data)
views.py

我们的课程的视图还可以重写编写一下~~利用我们之前学的视图的封装的那些类~~

class CourseCategoryView(generics.ListAPIView):
    queryset = Category.objects.all()
    serializer_class = CourseCategorySerializer
    """课程分类接口"""

    # def get(self, request):
    #     queryset = Category.objects.all()
    #     ser_obj = CourseCategorySerializer(queryset, many=True)
    #     return Response(ser_obj.data)


class CourseChapterView(generics.RetrieveAPIView):
    queryset = CourseChapter.objects.all()
    serializer_class = CourseChapterSerializer
    # 指定过滤的类 用排序的过滤类
    filter_backends = (filters.OrderingFilter,)
    # 排序的字段
    ordering = ("chapter",)

    # def get(self, request, pk):
    #     # 首先我们要清楚数据结构
    #     # 我们要的是[章节一:{课时,课时2}]
    #     queryset = CourseChapter.objects.filter(course_id=pk).order_by("chapter")
    #     ser_obj = CourseChapterSerializer(queryset, many=True)
    #     return Response(ser_obj.data)
升级版视图的示例

我们可以根据上面的更改的视图的示例~~来更改我们所有的视图~~小伙伴们~自己动起手来吧~~

我们课程这个模块下的数据接口我们就介绍这些~~

我们这里涉及课程图片~~用我们Django的mediaURL~~

Django的MEDIA配置

# settings.py

STATIC_URL = '/static/'
# Media配置
MEDIA_URL = "media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
复制代码
# urls.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})
]
复制代码

这样我们上传的图片~数据库存的是路径地址~~我们前端向后端的media路径发送请求~~

拿到我们想要的图片,视频等资源~~

 

认证

任何的项目都需要认证,用户输入了用户名和密码,验证通过,代表用户登录成功~~~

那HTTP请求是无状态的,下次这个用户再请求,我们是不可能识别这个用户是否登录的~~

所以我们就要有自己的方式来实现这个认证,也就是说~用户登录成功以后~~~我们给他们

生成一个随机字符串~~以后这个用户再请求~~都要携带这个随机字符串~~

我们就可以根据这个字符串进行判断这个用户是否登录~~~~

那么大家想一个问题~~就是我们给登录的用户生成的随机字符串放在哪里呢~~~

我们放哪里都可以~~目的是前端发送请求的时候带过来就可以了~~~

以前的cookie,session是我们的一种解决方案~~我们讲认证的时候也用过token的这种解决方案~~

TOKEN

用户登录成功后,生成一个随机字符串token给前端返回~~~

那么前端以后都携带这个token来访问~~这样我们只需要鉴别这个token~来做认证~~

前端如果发送请求~把token放在请求头中~~我们看下我们的认证要怎么写~~

在写认证之前,我们先把登录注册功能写了~~~

# 我们要拓展之前课程模块下的用户表
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 扩展之前功能模块的用户表
# 创建两个视图 一个注册的 一个登录的
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.base_response import BaseResponse
from .serializers import UserSerializer
from course.models import Account
import uuid
# Create your views here.


class UserView(APIView):

    # 注册用户
    def post(self, request):
        res = BaseResponse()
        ser_obj = UserSerializer(data=request.data)
        if ser_obj.is_valid():
            ser_obj.save()
            res.data = ser_obj.validated_data
        else:
            res.code = 1010
            res.data = ser_obj.errors
        return Response(res.dict)


class LoginView(APIView):

    # 登录视图
    def post(self, request):
        res = BaseResponse()
        # 这里要获取我们的用户名密码 进行验证是否有这个用户
        # 而且我们这个密码前端一定是密文传过来 我们通过密文对比进行验证
        username = request.data.get("username", "")
        password = request.data.get("password", "")
        user_obj_queryset = Account.objects.filter(username=username, password=password)
        if not user_obj_queryset:
            res.code = 1003
            res.error = "用户名或密码错误"
        try:
            token = uuid.uuid4()
            user_obj_queryset.update(token=token)
            res.data = token
        except Exception as e:
            res.code = 1004
            res.error = "生成token失败"
        return Response(res.dict)
views.py 编写登录注册视图
# 给前端返回时候用的基础的Response类
class BaseResponse(object):

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

    @property
    def dict(self):
        return self.__dict__
utils.base_response.py
# by gaoxin
from rest_framework import serializers
from course.models import Account
import hashlib

# 我们测试的时候用的重写create方法
# 真正开发前端传过来的就是加密后的密文密码
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = Account
        fields = "__all__"

    def create(self, validated_data):
        password = validated_data["password"]
        password_salt = password + "luffy_password"
        md5_str = hashlib.md5(password_salt.encode()).hexdigest()
        user_obj = Account.objects.create(username=validated_data["username"], password=md5_str)
        return user_obj
serializers.py 注册序列化器

我们的登录注册写完后~~开始写认证~~

# by gaoxin
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 MyAuth(BaseAuthentication):
    def authenticate(self, request):
        if request.method == "OPTIONS":
            return None
        # print(request.META)
        token = request.META.get("HTTP_AUTHENTICATION", "")
        print(token)
        if not token:
            raise AuthenticationFailed({"code": 1021, "error": "缺少token"})
        user_obj = Account.objects.filter(token=token).first()
        if not user_obj:
            raise AuthenticationFailed({"code": 1020, "error": "无效的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.auth.py
# 查看购物车是需要登录后才可以
# 所有这是一个需要认证的接口
class ShoppingCarView(APIView):
    authentication_classes = [MyAuth, ]
    # 展示购物车数据
    def get(self, request, *args, **kwargs):
        print(request.user)
        return Response("test")
views.py 测试用的视图

我们基于请求头的token认证就完成了~~~

 

购物车中心

用户点击价格策略加入购物车,个人中心可以查看自己所有购物车中数据~~

在购物车中可以删除课程,还可以更新购物车中课程的价格策略~~~

所以接口应该有四种请求方式, get,post,patch,delete~~

我们在做这个功能之前,首先要讨论我们购物车中数据存在哪~~~为什么要这么存~~

因为购物车是属于中间状态数据~~而且很多时候需要过期时间~~所以我们选择redis~~

读取速度快~并且redis可以做持久化~~支持的数据类型也比较多~~

然后我们需要讨论~我们存redis~~存什么样的数据结构~对于我们实现需求来说更加方便~

下面看代码中存入redis中的数据结构是什# by gaoxin

复制代码
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.authentication import MyAuth
from utils.redis_pool import pool
from utils.base_response import BaseResponse
from course.models import Course
import redis
import json




"""
购物车
redis 的数据结构设计
redis = {
    shopping_car_<user_id>_<course_id>: {
        "id": course.id, 带用户id是为了用户查看购物车的时候需要
        "title": course.name,
        "img": str(course.course_img),
        "policy_dict": {
            policy_id:{
                "period": item.valid_period, 价格策略
                "period_display": item.get_valid_period_display(), 价格策略中文
                "price":  item.price, 价格
            },
            policy_id2:{
                ....
            }
        },
        "default_policy": policy_id ,选择的价格策略id
    }
        
        
}
"""
REDIS_CONN = redis.Redis(connection_pool=pool)
SHOPPING_CAR_KEY = "shopping_car_%s_%s"


class ShoppingCarView(APIView):
    """
    购物车接口
    1010 代表成功
    1011 课程不存在
    1012 价格策略不存在
    1013 获取购物车失败
    1014 删除的购物车数据不存在
    """
    authentication_classes = [MyAuth, ]

    # 展示购物车数据
    def get(self, request, *args, **kwargs):
        res = BaseResponse()
        # 1 从redis获取购物车数据
        try:
            user_id = request.user.pk
            shopping_car_key = SHOPPING_CAR_KEY % (user_id, "*")
            # 用scan_iter方法获取用户购物车的所有key
            all_keys = REDIS_CONN.scan_iter(shopping_car_key)
            ret = []
            # 循环得到所有想要的数据
            for key in all_keys:print(REDIS_CONN.hgetall(key))
                ret.append(REDIS_CONN.hgetall(key))
            res.data = ret
        except Exception as e:
            res.code = 1013
            res.error = "获取购物车失败"
        return Response(res.dict)

    # 给购物车增加商品
    # 传过来的数据 有 课程ID 以及价格策略ID
    def post(self, request, *args, **kwargs):
        res = BaseResponse()
        try:
            # 1 获取用户提交的课程ID以及价格策略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(pk=course_id).first()
            if not course_obj:
                res.code = 1011
                res.error = "课程对象不存在"
            # 2.2  查看价格策略是否合法
            #  获取该课程的所有价格策略
            price_policy_queryset = course_obj.price_policy.all()
            price_policy_dict = {}
            for price_policy in price_policy_queryset:
                price_policy_dict[price_policy.pk] = {
                    "price": price_policy.price,
                    "valid_period": price_policy.valid_period,
                    "valid_period_text": price_policy.get_valid_period_display(),
                }
            if price_policy_id not in price_policy_dict:
                res.code = 1012
                res.error = "课程的价格策略不存在"

            # 3 用户数据合法 得到用户的购物车key
            shopping_car_key = SHOPPING_CAR_KEY % (user_id, course_id)
            # 4 构建购物车数据
            course_info = {
                    "id": course_id,
                    "title": course_obj.title,
                    "img_src": str(course_obj.course_img),
                    "default_policy": price_policy_id,
                    "price_policy_dict": json.dumps(price_policy_dict)
            }
            # 5 写入redis hmset这个方法有值就更新  没值就新建 解决了用户重复点一个商品的问题
            REDIS_CONN.hmset(shopping_car_key, course_info)

            res.data = "加入购物车成功"
        except Exception as e:
            print(e)
            res.code = 1010
            res.error = "加入购物车失败"
        return Response(res.dict)

    # 删除购物车数据
    # 传过来的数据 {course_ids: [1,2]}
    def delete(self, request, *args, **kwargs):
        res = BaseResponse()
        # 1 验证数据合法性 课程ID是否在购物车里
        try:
            for course_id in request.data["course_ids"]:
                shopping_car_key = SHOPPING_CAR_KEY % (request.user.pk, course_id)
                if not REDIS_CONN.exists(shopping_car_key):
                    res.code = 1014
                    res.error = "课程不存在购物车"
            # 2 数据合法 删除redis购物车里的数据
                REDIS_CONN.delete(shopping_car_key)
        except Exception as e:
            res.code = 1015
            res.error = "删除购物车失败"
        return Response(res.dict)

    # 更新购物车里的课程的价格策略
    # 传过来的数据 有 course_id 以及 price_policy_id
    def patch(self, request, *args, **kwargs):
        res = BaseResponse()
        # 1 验证数据的合法性 course_id 以及 policy_id
        try:
            course_id = request.data["course_id"]
            policy_id = request.data["price_policy_id"]
            # 判断redis购物车里是否有course_id
            shopping_car_key = SHOPPING_CAR_KEY % (request.user.pk, course_id)
            if not REDIS_CONN.exists(shopping_car_key):
                res.code = 1014
                res.error = "课程不存在购物车"
            # 判断policy_id 是否合法
            price_policy_dict = json.loads(REDIS_CONN.hget(shopping_car_key, "price_policy_dict"))
            if str(policy_id) not in price_policy_dict:
                res.code = 1015
                res.error = "价格策略不合法"
            # 2 更改购物车里的数据
            REDIS_CONN.hset(shopping_car_key, "default_policy", policy_id)
        except Exception as e:
            print(e)
            res.code = 1016
            res.error = "更新购物车失败"
        return Response(res.dict)
复制代码

 

结算中心要开始选择优惠券了~~

表结构如下~~

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)
    off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True)
    minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段", null=True, blank=True)

    content_type = models.ForeignKey(ContentType, blank=True, null=True)
    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")
    number = models.CharField(max_length=64, unique=True, verbose_name="用户优惠券记录的流水号")
    account = models.ForeignKey(to=Account, verbose_name="拥有者")
    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="关联订单")  # 一个订单可以有多个优惠券

    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)
    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")

    content_type = models.ForeignKey(ContentType)  # 可关联普通课程或学位
    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
        unique_together = ("order", 'content_type', 'object_id')


class TransactionRecord(models.Model):
    """贝里交易纪录"""
    account = models.ForeignKey(to=Account)
    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
models.py

视图代码如下~~

复制代码
# by gaoxin
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
import redis
from utils.authentication import MyAuth
from .shop import SHOPPING_CAR_KEY
from pay.models import CouponRecord
from django.contrib.contenttypes.models import ContentType
import json
"""
结算中心
在购物车里选择了商品以及价格策略点击结算 才进入结算中心
在结算中心用户可以选择优惠券
传过来的数据 [course_id, course_id]
根据用户id 以及商品id 构建一个存入redis的key
SETTLEMENT_KEY = "settlement_%s_%s"
接下来构建结算中心数据结构
redis = {
    settlement_用户id_课程id: {
        "id": course.id, 带用户id是为了用户查看购物车的时候需要
        "title": course.name,
        "img": str(course.course_img),
        "period_display": item.get_valid_period_display(), 价格策略中文
        "price":  item.price, 价格
        "course_coupons": {
            course_coupon_id: {...}
        } 
        用户选中的优惠券id 在更新请求 也就是用户选了优惠券的时候更改
        前端默认优惠券可以是不选的 根据业务决定
        default_coupon_id: 1
    }
    global_coupon_用户id: {
         global_coupon_id: {....},
         # 在用户进入结算中心选择优惠券的时候 也就是更新请求的时候更改 
         default_global_coupon_id: 1   
    }
}

"""

SETTLEMENT_KEY = "settlement_%s_%s"
GLOBAL_COUPON_KEY = "global_coupon_%s"
REDIS_CONN = redis.Redis(connection_pool=pool)


class SettlementView(APIView):
    """
    code为1020开始的都是结算中心的错误
    1021 获取课程id失败
    """
    authentication_classes = [MyAuth, ]

    # 进入结算中心 也就是用户查看自己的未支付订单
    def get(self, request, *args, **kwargs):
        res = BaseResponse()
        try:
            # 1 从redis结算中心获取数据并展示
            # 根据用户id  得到 settlement_id_userid_*
            user_id = request.user.pk
            settlement_key = SETTLEMENT_KEY % (user_id, "*")
            # scan_iter 得到所有的key
            all_settlement_keys = REDIS_CONN.scan_iter(settlement_key)
            global_coupon_key = GLOBAL_COUPON_KEY % user_id
            # 2 循环遍历得到所有信息
            settlement_list = []
            for key in all_settlement_keys:
                settlement_info = REDIS_CONN.hgetall(key)
                settlement_list.append(settlement_info)
            global_coupon_info = REDIS_CONN.hgetall(global_coupon_key)
            res.data = {"settlement_list": settlement_list, "global_coupon_info": global_coupon_info}
        except Exception as e:
            res.code = 1024
            res.error = "获取结算中心失败"
        return Response(res.dict)

    # 把商品添加到结算中心 用户传过来的数据是 【course_id,course_id】
    def post(self, request, *args, **kwargs):
        res = BaseResponse()
        # 1 获取前端传过来的数据以及用户id
        try:
            course_list = request.data.get("course_list", "")
            if not course_list:
                res.code = 1021
                res.error = "获取课程id失败"
                return Response(res.dict)
            user_id = request.user.pk
            # 2 检查数据的合法性 查看课程id 是否在购物车中
            for course_id in course_list:
                # course_id = int(course_id)
                shopping_car_key = SHOPPING_CAR_KEY % (user_id, course_id)
                if not REDIS_CONN.exists(shopping_car_key):
                    res.code = 1022
                    res.error = "结算的课程id不合法"
                    return Response(res.dict)
                # 3 从购物车中获取课程的信息 并构建成结算中心的数据格式放入redis
                # 3.1 先从redis中拿购物车中的数据
                course_info = REDIS_CONN.hgetall(shopping_car_key)
                # 3.2 获取用户所有符合条件优惠券
                # 注意优惠券的使用期限以及状态
                now_time = now()
                # course_table_id = ContentType.objects.filter(app_label="course", model="course").first().pk
                user_all_coupons = CouponRecord.objects.filter(
                    account_id=user_id,
                    status=0,
                    coupon__valid_begin_date__lte=now_time,
                    coupon__valid_end_date__gte=now_time,
                    # content_type_id=course_table_id,
                    # object_id=course_id
                ).all()
                # 3.2 构建数据结构
                course_coupons = {}
                global_coupons = {}
                for coupon_record in user_all_coupons:
                    # 证明这个优惠券是绑定课程的优惠券
                    coupon = coupon_record.coupon
                    if coupon.object_id == course_id:
                        course_coupons[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,
                            "object_id": coupon.object_id,
                        }
                    elseif coupon.object_id == "":
                        global_coupons[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,
                            "object_id": coupon.object_id,
                        }
                # print(json.loads(course_info["price_policy_dict"]))
                price_policy_dict = json.loads(course_info["price_policy_dict"])
                default_policy_id = course_info["default_policy"]
                price = price_policy_dict[default_policy_id]["price"]
                print(price)
                period_display = price_policy_dict[default_policy_id]["valid_period_text"]
                print(period_display)
                settlement_info = {
                    "id": course_info["id"],
                    "title": course_info["title"],
                    "img_src": course_info["img_src"],
                    "price": price,
                    "period_display": period_display,
                    "course_coupons": json.dumps(course_coupons, ensure_ascii=False),
                }
                # 3.3 准备构建结算中心的key
                settlement_key = SETTLEMENT_KEY % (user_id, course_id)
                global_coupon_key = GLOBAL_COUPON_KEY % user_id
                # # 3.4 写入redis
                REDIS_CONN.hmset(settlement_key, settlement_info)
                print(global_coupons)
                if global_coupons:
                    REDIS_CONN.hmset(global_coupon_key, global_coupons)
                # # 4 加入结算中心成功 清除用户购物车中的相应课程的数据
                REDIS_CONN.delete(shopping_car_key)
                res.data = "加入结算中心成功"
        except Exception as e:
            print(e)
            res.code = 1020
            res.error = "结算失败"
        return Response(res.dict)

    # 用户在结算中心的时候更改优惠券
    # 用户在选择优惠券的时候前端传过来course_id 以及coupon_id
    # 如果选的是全局优惠券 就传 global_coupon_id
    def patch(self, request, *args, **kwargs):
        res = BaseResponse()
        try:
            # 1 获取前端传过来的数据
            course_id = request.data.get("course_id", "")
            coupon_id = request.data.get("coupon_id", "")
            global_coupon_id = request.data.get("global_coupon_id", "")
            user_id = request.user.pk
            # 2 校验数据的合法性
            # 2.2 校验course_id是否合法
            settlement_key = SETTLEMENT_KEY % (user_id, course_id)
            if not REDIS_CONN.exists(settlement_key):
                res.code = 1027
                res.error = "课程id不合法"
                return Response(res.dict)
            # 2.3 校验coupon_id 是否合法
            course_info = REDIS_CONN.hgetall(settlement_key)
            course_coupon_dict = json.loads(course_info["course_coupons"])
            if coupon_id not in course_coupon_dict:
                res.code = 1028
                res.error = "课程的优惠券不合法"
                return Response(res.dict)
            # 2.4 校验global_coupon_id是否合法 如果合法把默认的全局优惠信息放入redis
            if global_coupon_id:
                global_coupon_key = GLOBAL_COUPON_KEY % user_id
                if not REDIS_CONN.exists(global_coupon_key):
                    res.code = 1029
                    res.error = "全局优惠券不合法"
                    return Response(res.dict)
                REDIS_CONN.hset(global_coupon_key, "default_coupon_id", global_coupon_id)
            # 3 给数据里加入默认选择的课程优惠券
            REDIS_CONN.hset(settlement_key, "default_course_coupon_id", coupon_id)
            res.data = "修改优惠券成功"
        except Exception as e:
            res.code = 1026
            res.error = "更改优惠券失败"
        return Response(res.dict)
复制代码

 

posted on 2021-01-31 21:10  必须登录  阅读(76)  评论(0)    收藏  举报