项目课程模块
课程模块,包括了免费课程以及专题课程两个方向。
主要是课程的展示,点击课程进入课程详细页面。课程详细页面展示,课程的概述,课程的价格策略,课程章节,评价以及常见问题。
一、根据功能设计表结构
1、设计表结构
在项目中创建课程模块APP——Course。设计表结构如下所示:
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") # GenericRelation只用于反向查询不生成字段 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) # 执行父类的save方法 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") # 联合唯一,课程下的章节应该是唯一的(比如:第2章第2节) 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个值,以备后续插入课时") free_trail = models.BooleanField("是否可试看", default=False) # 是否可试看 section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频')) # 对应不同的链接 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): """价格策略表,ContentType来实现价格策略,避免出现多个ForeignKey""" content_type = models.ForeignKey(ContentType, on_delete=None) # 价格策略与contentType表建立外键关系,同时关联course表和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() # 价格,float格式 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="个人头像") 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) # TextField可以保存并展示空格、回车 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') # 联合唯一:一个课程详情下的大纲标题只能有一个
LuffyCity/Course/models.py文件初始内容设计如上所示。
2、添加表数据
在完成数据库迁移和管理员创建后。通过admin组件在数据库添加数据示例如下所示:
二、接口编写——课程分类接口
表结构基本定下来后,可以根据业务场景看需要哪些接口。
对于课程这个模块,所有的功能都是展示,基于数据展示,通常称为数据接口。
1、梳理需要哪些接口
课程页面:课程所有分类的接口、展示课程的接口。
课程详情页面:点击课程要进入课程详情页面,即详情页面的数据接口。
详情页面下子路由对应子组件数据接口:课程章节课时、课程评论、课程常见问题。
可以看到,上述接口都是读取数据库、序列化数据、返回。因此主要是使用DRF的序列化组件实现。
2、注册数据模型到 admin
编写LuffyCity/Course/admin.py文件如下所示:
from django.contrib import admin # Register your models here. from . import models for table in models.__all__: # __all__变量保存所有的表名 admin.site.register(getattr(models, table)) # 用反射在models中找到每一个表并注册进来
由于models定义了__all__属性,以列表格式保存所有表名。
因此可以使用上述方法将Model中所有类注册,即可在Admin中实现增删改查的功能。
3、课程路由配置
编写LuffyCity/Course/urls.py文件如下所示:
from django.urls import path from .views import CategoryView urlpatterns = [ path('category', CategoryView.as_view()), ]
4、序列化数据
创建新文件:LuffyCity/Course/serializers.py,在该文件中执行序列化操作。
from rest_framework import serializers from . import models class CategorySerializer(serializers.ModelSerializer): class Meta: # 配置元信息 model = models.Category # 课程分类表 fields = "__all__"
5、课程视图编写
编写课程分类视图如下所示:
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from . import models # 引入所有的models from .serializers import CategorySerializer # 引入序列化器 # Create your views here. class CategoryView(APIView): def get(self, request): # 通过ORM操作获取所有分类数据 queryset = models.Category.objects.all() # 利用DRF序列化器去序列化数据 ser_obj = CategorySerializer(queryset, many=True) # 返回 return Response(ser_obj.data)
执行程序后,通过postman访问接口如下所示:
三、获取课程接口
1、添加课程url
在LuffyCity/Course/urls.py中添加查看课程内容:
from django.urls import path from .views import CategoryView, CourseView urlpatterns = [ path('category', CategoryView.as_view()), # 课程分类 path('list', CourseView.as_view()) # 查看课程 ]
2、添加课程序列号
在LuffyCity/Course/serializers.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字段获取的是数字,需要处理 level = serializers.CharField(source="get_level_display") # 指定资源,执行ORM操作get_level_display,拿到中文展示 # 根据价格策略获取价格数据 price = serializers.SerializerMethodField() def get_price(self, obj): # 拿到所有的价格策略中,最便宜的价格 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"] # 提取部分字段
3、添加课程视图
在LuffyCity/Course/views.py中添加课程视图内容:
from django.shortcuts import render from rest_framework.views import APIView from rest_framework.response import Response from . import models # 引入所有的models from .serializers import CategorySerializer, CourseSerializer # 引入序列化器 # Create your views here. class CategoryView(APIView):... class CourseView(APIView): def get(self, request): # 获取过滤条件中的分类id category_id = request.query_params.get("category", 0) # 默认id为0,即所有课程 # 分类id作为过滤条件来获取课程 if category_id == 0: queryset = models.Course.objects.all().order_by("order") # 以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)
四、Django的MEDIA配置
静态资源(多媒体资源)配置。
1、settings.py配置
# Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ STATIC_URL = '/static/' # Media配置 MEDIA_URL = "media/" # 前端访问图片资源,存的时候要放一个路径 MEDIA_ROOT = os.path.join(BASE_DIR, "media")
(1)MEDIA_ROOT
默认值: '' (空的字符串)
一个绝对路径, 用于保存媒体文件. 例子: "/home/media/media.lawrence.com/" 。
(2)MEDIA_URL
默认值: '' (空的字符串)
处理媒体服务的URL(媒体文件来自 MEDIA_ROOT). 如: "http://media.lawrence.com"。
2、urls.py配置media路径
修改LuffyCity/LuffyCity/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 urlpatterns = [ path('admin/', admin.site.urls), path('api/course/', include("Course.urls")), # media路径配置 re_path('media/(?P<path>.*)', serve, {'document_root': settings.MEDIA_ROOT}) ]
通过admin组件添加数据,为课程添加图片数据。可以看到项目自动创建media目录如下所示:
3、访问接口查看保存的图片
访问接口如下所示:
因此我们上传的图片,数据库保存的是路径地址,前端向后端的media路径发送请求来拿到图片、视频等资源。