项目:路飞学城(在线学习的平台)
一、背景
目的是做一个在线的学习平台,提高学生的课程完成度
路飞学城:大目标是要取代线下班
- - 录制专门的视频
- - 导师的监管:如果没有主动和那个学生聊天,就扣钱
- - 学生的激励:如果提前学完了,学习能力很强,可以免学费
二、系统架构
- 主站 - rest api - vue - 导师后台 (stark) - django - stark - 管理后台 (stark) - django - stark
三、人员和部门
- 后端开发(5人) - 主站(3) - 导师后台(1) - 管理后台(1) - 前端开发(1) - UI (1) - 测试(1) - 运维(部署:上线)(1) - 运营(推广出去)(2) - 销售 (2) - 导师 (2) - 产品经理 (1) - 人事 - 财务
四、需求分析
主站 - 课程 - 普通课程、学位课 - 普通课程:比较小的课程 - 学位课:从开始+项目+就业 - 分配一对一的导师 - 导师对你进行监管,督促你学习 - 保就业 - 如果你%50的时间学完了,退一般的学费,等。 - 课程列表、课程详细 - 课程周期的不同定价格 - 个人的课程中心 - 支付 - 银联- 支付宝 - 微信 - 优惠券策略(满减(满多少减多少),立减,打折) - 贝里:可直接抵现金 - 个人购物车 - 深科技 - 文章或者爬取的别人的文章(运营需要审核)
- 深刻有哪些功能?
- 点赞、评论、查看、收藏 - 登录 、注册 导师后台 管理后台
具体进一步谈业务需求
a、课程: 业务相关: 1、学位课,如果你学的快;最多返指定金额的65% 2、作业做的好,会把额外的35%返给你 3、普通课程购买课程后,在我的订单中去评价 4、购物车的数据没有数据表。保存到redis里面去了 两大部分操作 #redis操作 在课程详细页面,选择价格策略,点击加入购物车,吧视频和价格策略放到了redis中 在购物车页面, 可以对redis中保存的购买的视频数据和价格数据可以删除,修改,添加,查看(刷新) 选中,去结算中心 #数据库操作 #pip freeze > requirement.txt #生成文件 #pip install -r requirement.txt #下载所有的版本 在结算页面: 优惠券 - 选择单一的优惠券(通用、满减、折扣) - 课程优惠券:绑定课程的优惠券 - 选择全站的优惠券(通用、满减、折扣) #单一的优惠券用完,再计算,看是否满足全站的通用卷 - 选择贝里 - 选择支付宝,点击立即支付 - 在数据库中生成一个订单 这个订单如果用了优惠券,优惠券状态改为已使用 如果用了贝里,贝里的金额减少 如果取消订单,优惠券才不是使用状态 在支付页面,扫码状态为已支付 在我的订单中应该去数据库中读数据 在购物车或者结算中心的时候去redis里面读数据 课程列表和课程详细等等等 如果购买课程 - 普通课程 - 学位课程 - 每个模块生成记录(人和课程模块的记录) - 买完之后自动生成 - 开通模块(后台) - 导师跟进(导师) - 交作业(主站) b、深科技相关 来源 文章 收藏 通用评论表 c、购买支付流程 课程id和价格策略id 传值的时候可以传两个值 - 通过策略id找到买的那个课程 - 通过课程id 获取所有的价格策略 处理请求 1、检测课程是否上线,下线 2、通过课程id 获取所有的价格策略,和传过来的进行对比 3、检测用户提交的价格策略是否在所有的价格策略中 4、将购物的视频添加到购物车(redis) - 同一个课程只能买一个,不允许一下加入两个 吧数据放到redis中,那么怎么存? 一个人就有一个购物车。可以通过用户id来存 所有的购物车= { 1:{ 课程id1:{'prices':[],'price':9,'title':'python','img':'课程图片'}, 课程id2:{'prices':[]} } } #如果看到课程已经在里面了,就不在里面添加了,这也就是用redis的原因 class ShoppingCat(APIView): def post(): #点击加入购物车保存数据到redis def get(): #查看购物车列表 def delete(): #删除 def patch(): #局部更新 pass 5、去结算(先简单的说一下) 买的所有的课程 课程1 价格策略 课程2 价格策略 课程3 价格策略 点击了去结算 优惠券 贝里
五、表结构
课程相关的13张表
课程大类表:e.g 前端,后端 课程子类表:e.g python ,linux 普通课程表 学位课表 ##学位课可以有如下好处 学位奖学金表
##当然学位课和普通课程共有的 价格策略表 优惠券表
课程详细表 课程章节表 课程目录表 课程问答表 课程评价(先不讨论)
课程大纲
## 老师表 contenttype contentType 可以一下关联两张表,django自己提供了,不用自己创建
深科技相关的4张表
文章来源
文章资讯
收藏(既能收藏课程,也能收藏文章,可以用contenttype)
评论(可以用contenttype的形式也可以用ForeignKey的形式)
用户相关的两张表
用户信息表account
用户认证表userauthtoken
- 如果用户初次登录,给用户创建一个token,如果已经有token了,下次用户访问的时候直接带上token,如果没有token就说明没有登录,就没有访问其他页面的权限
购买支付相关的7张表
已报名课程表EnrolledCourse(谁购买了那个课程)
学位课报名表
已报名的学位课程表EnrolledDegreeCourse
优惠券发放、消费记录
订单表
订单详情
贝里交易记录
其他相关的6张表
课程评价
学位课程评价
学位课程的模块学习进度
为学生开通一个模块
学生作业记录及成绩
学员跟进记录
1、简单表结构设计分析:
需要知道的知识点:
现在学位课和普通课程都有自己的价格策略和优惠券,现在是学位课已经有了自己的价格策略,那么如果普通课程也要加价格策略,怎么弄呢?
一下有三种方法:
方法一:再单独创建一个表
方法二:不用单独创建一个表,可以多添加一个字段,其中的一个为NULL。相比方法一省了一张表
方法三:可以通过ContentType ,这个表里面存的是应用的名字以及表名。但是在我们makemigrations和migrate的时候django已经帮我们创建好了,如下图。就不用我们自己创建了
这样关联的好处:可以ForeignKey任何一张表。里面的xid是关联表的某一行数据,
这样关联以后,如果想在价格策略里面在添加一行数据,怎么添加呢?
有两种方式:
方式一:手动插入
- 自己手动查出content_type_id 和关联表里的某一行数据xid。查出来添加到价格策略表里面去
方式二:利用GenericForeignKey
- 可以通过导入 GenericForeignKey(GenericForeignKey有自动查找的功能,协助你快速做操作,没有也可以,自己手动查询)
from django.contrib.contenttypes.fields import GenericForeignKey
使用:
content_object = GenericForeignKey('content_type', 'object_id') #一下可以查到两个表的数据,自动插入到价格策略表里了,
优惠券表也是如此。只是为了查询方便
另外补充一个GenericRelation
from django.contrib.contenttypes.fields import GenericRelation
GenericRelation:也是帮助做快速查询的,就像反向关联的releated_name=‘xx’。以后查找的时候通可以通过点字段名的方式去查找
# 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除 coupon = GenericRelation("Coupon") #也是帮助做快速查询的,可以直接查出所有的学位课的价格策略等
2、具体表结构设计如下:
1 from django.db import models 2 from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation 3 #GenericForeignKey有自动查找的功能,协助你快速做操作,没有也可以,自己手动查询 4 #GenericRelation:也是帮助做快速查询的,可以直接查出所有的学位课的价格策略等 5 6 from django.contrib.contenttypes.models import ContentType 7 from django.db.models import Q 8 from django.utils.safestring import mark_safe 9 from django.db import models 10 import hashlib 11 12 class CourseCategory(models.Model): 13 """课程大类, e.g 前端 后端...""" 14 name = models.CharField(max_length=64, unique=True) 15 16 def __str__(self): 17 return "%s" % self.name 18 19 class Meta: 20 verbose_name = "课程大类" #单数 21 verbose_name_plural = "课程大类" #复数 22 23 class CourseSubCategory(models.Model): 24 """课程子类, e.g python linux """ 25 category = models.ForeignKey("CourseCategory",verbose_name='所属类') 26 name = models.CharField(max_length=64, unique=True) 27 28 def __str__(self): 29 return "%s" % self.name 30 31 class Meta: 32 verbose_name = "课程子类" 33 verbose_name_plural = "课程子类" 34 35 class DegreeCourse(models.Model): 36 """学位课程""" 37 name = models.CharField(max_length=128, unique=True) 38 course_img = models.CharField(max_length=255, verbose_name="缩略图") 39 brief = models.TextField(verbose_name="学位课程简介", ) 40 total_scholarship = models.PositiveIntegerField(verbose_name="总奖学金(贝里)", default=40000) 41 mentor_compensation_bonus = models.PositiveIntegerField(verbose_name="本课程的导师辅导费用(贝里)", default=15000) 42 # 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除 43 coupon = GenericRelation("Coupon") #也是帮助做快速查询的,可以直接查出所有的学位课的价格策略等 44 # 为了计算学位奖学金 45 period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=150) 46 prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024) 47 teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师") 48 # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除 49 degreecourse_price_policy = GenericRelation("PricePolicy") 50 51 def __str__(self): 52 return self.name 53 54 class Course(models.Model): 55 """课程""" 56 name = models.CharField(max_length=128, unique=True) 57 course_img = models.CharField(max_length=255) 58 sub_category = models.ForeignKey("CourseSubCategory") 59 course_type_choices = ((0, '付费'), (1, 'VIP专享'), (2, '学位课程')) 60 course_type = models.SmallIntegerField(choices=course_type_choices) 61 degree_course = models.ForeignKey("DegreeCourse", blank=True, null=True, help_text="若是学位课程,此处关联学位表") 62 brief = models.TextField(verbose_name="课程概述", max_length=2048) 63 level_choices = ((0, '初级'), (1, '中级'), (2, '高级')) 64 level = models.SmallIntegerField(choices=level_choices, default=1) 65 pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True) 66 period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=7) 67 order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排") 68 attachment_path = models.CharField(max_length=128, verbose_name="课件路径", blank=True, null=True) 69 status_choices = ((0, '上线'), (1, '下线'), (2, '预上线')) 70 status = models.SmallIntegerField(choices=status_choices, default=0) 71 template_id = models.SmallIntegerField("前端模板id", default=1) 72 coupon = GenericRelation("Coupon") 73 # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除 74 price_policy = GenericRelation("PricePolicy") 75 76 def __str__(self): 77 return "%s(%s)" % (self.name, self.get_course_type_display()) 78 79 def save(self, *args, **kwargs): 80 if self.course_type == 2: 81 if not self.degree_course: 82 raise ValueError("学位课程必须关联对应的学位表") 83 super(Course, self).save(*args, **kwargs) 84 85 class CourseDetail(models.Model): 86 """课程详情页内容""" 87 course = models.OneToOneField("Course") 88 hours = models.IntegerField("课时") 89 course_slogan = models.CharField(max_length=125, blank=True, null=True) 90 video_brief_link = models.CharField(verbose_name='课程介绍', max_length=255, blank=True, null=True) 91 why_study = models.TextField(verbose_name="为什么学习这门课程") 92 what_to_study_brief = models.TextField(verbose_name="我将学到哪些内容") 93 career_improvement = models.TextField(verbose_name="此项目如何有助于我的职业生涯") 94 prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024) 95 recommend_courses = models.ManyToManyField("Course", related_name="recommend_by", blank=True) 96 teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师") 97 98 def __str__(self): 99 return "%s" % self.course 100 101 class OftenAskedQuestion(models.Model): 102 """常见问题""" 103 content_type = models.ForeignKey(ContentType, 104 limit_choices_to={'model__contains': 'course'}) # 关联course or degree_course 105 object_id = models.PositiveIntegerField() 106 content_object = GenericForeignKey('content_type', 'object_id') 107 108 question = models.CharField(max_length=255) 109 answer = models.TextField(max_length=1024) 110 111 def __str__(self): 112 return "%s-%s" % (self.content_object, self.question) 113 114 class Meta: 115 unique_together = ('content_type', 'object_id', 'question') 116 117 class CourseOutline(models.Model): 118 """课程大纲""" 119 course_detail = models.ForeignKey("CourseDetail") 120 title = models.CharField(max_length=128) 121 # 前端显示顺序 122 order = models.PositiveSmallIntegerField(default=1) 123 124 content = models.TextField("内容", max_length=2048) 125 126 def __str__(self): 127 return "%s" % self.title 128 129 class Meta: 130 unique_together = ('course_detail', 'title') 131 132 class CourseChapter(models.Model): 133 """课程章节""" 134 course = models.ForeignKey("Course", related_name='coursechapters') 135 chapter = models.SmallIntegerField(verbose_name="第几章", default=1) 136 name = models.CharField(max_length=128) 137 summary = models.TextField(verbose_name="章节介绍", blank=True, null=True) 138 pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True) 139 140 class Meta: 141 unique_together = ("course", 'chapter') 142 143 def __str__(self): 144 return "%s:(第%s章)%s" % (self.course, self.chapter, self.name) 145 146 class Teacher(models.Model): 147 """讲师、导师表""" 148 name = models.CharField(max_length=32) 149 role_choices = ((0, '讲师'), (1, '导师')) 150 role = models.SmallIntegerField(choices=role_choices, default=0) 151 title = models.CharField(max_length=64, verbose_name="职位、职称") 152 signature = models.CharField(max_length=255, help_text="导师签名", blank=True, null=True) 153 image = models.CharField(max_length=128) 154 brief = models.TextField(max_length=1024) 155 156 def __str__(self): 157 return self.name 158 159 class PricePolicy(models.Model): 160 """价格与有课程效期表""" 161 content_type = models.ForeignKey(ContentType) # 关联course or degree_course 162 object_id = models.PositiveIntegerField() 163 content_object = GenericForeignKey('content_type', 'object_id') 164 165 # course = models.ForeignKey("Course") 166 valid_period_choices = ((1, '1天'), (3, '3天'), 167 (7, '1周'), (14, '2周'), 168 (30, '1个月'), 169 (60, '2个月'), 170 (90, '3个月'), 171 (180, '6个月'), (210, '12个月'), 172 (540, '18个月'), (720, '24个月'), 173 ) 174 valid_period = models.SmallIntegerField(choices=valid_period_choices) 175 price = models.FloatField() 176 177 class Meta: 178 unique_together = ("content_type", 'object_id', "valid_period") 179 180 def __str__(self): 181 return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price) 182 183 class CourseSection(models.Model): 184 """课时目录""" 185 chapter = models.ForeignKey("CourseChapter", related_name='coursesections') 186 name = models.CharField(max_length=128) 187 order = models.PositiveSmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时") 188 section_type_choices = ((0, '文档'), (1, '练习'), (2, '视频')) 189 section_type = models.SmallIntegerField(default=2, choices=section_type_choices) 190 191 192 section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link") 193 # vid:cc视频,唯一标识:sdfsgdfgdgfhgfh 194 video_time = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32) # 仅在前端展示使用 195 pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True) 196 free_trail = models.BooleanField("是否可试看", default=False) 197 198 199 class Meta: 200 unique_together = ('chapter', 'section_link') 201 202 203 def __str__(self): 204 return "%s-%s" % (self.chapter, self.name) 205 206 class Coupon(models.Model): 207 """优惠券生成规则""" 208 name = models.CharField(max_length=64, verbose_name="活动名称") 209 brief = models.TextField(blank=True, null=True, verbose_name="优惠券介绍") 210 coupon_type_choices = ((0, '通用券'), (1, '满减券'), (2, '折扣券')) 211 coupon_type = models.SmallIntegerField(choices=coupon_type_choices, default=0, verbose_name="券类型") 212 213 money_equivalent_value = models.IntegerField(verbose_name="等值货币") 214 off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True) 215 minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段") 216 217 content_type = models.ForeignKey(ContentType, blank=True, null=True) 218 object_id = models.PositiveIntegerField("绑定课程", blank=True, null=True, help_text="可以把优惠券跟课程绑定") 219 content_object = GenericForeignKey('content_type', 'object_id') 220 221 quantity = models.PositiveIntegerField("数量(张)", default=1) 222 open_date = models.DateField("优惠券领取开始时间") 223 close_date = models.DateField("优惠券领取结束时间") 224 valid_begin_date = models.DateField(verbose_name="有效期开始时间", blank=True, null=True) 225 valid_end_date = models.DateField(verbose_name="有效结束时间", blank=True, null=True) 226 coupon_valid_days = models.PositiveIntegerField(verbose_name="优惠券有效期(天)", blank=True, null=True, 227 help_text="自券被领时开始算起") 228 date = models.DateTimeField(auto_now_add=True) 229 230 def __str__(self): 231 return "%s(%s)" % (self.get_coupon_type_display(), self.name) 232 233 def save(self, *args, **kwargs): 234 if not self.coupon_valid_days or (self.valid_begin_date and self.valid_end_date): 235 if self.valid_begin_date and self.valid_end_date: 236 if self.valid_end_date <= self.valid_begin_date: 237 raise ValueError("valid_end_date 有效期结束日期必须晚于 valid_begin_date ") 238 if self.coupon_valid_days == 0: 239 raise ValueError("coupon_valid_days 有效期不能为0") 240 if self.close_date < self.open_date: 241 raise ValueError("close_date 优惠券领取结束时间必须晚于 open_date优惠券领取开始时间 ") 242 243 super(Coupon, self).save(*args, **kwargs)