在线教育平台
软件依赖:
-
WEB框架:Django(1.11.7)、Django REST framework
-
前端框架:Vue(2.5.16)
-
数据库: MySql、redis
-
支付平台:支付宝
-
消息推送平台:微信服务号,电子邮件
主要功能如下:
-
允许学生注册、登陆
-
浏览本在线教育平台所提供的课程列表、课程介绍(课程大纲,)
-
学生将课程加入购物车
-
完成购物后,进入结算中心,通过支付宝支付购买课程
-
完成课程购买后,分配导师
-
开始学习,观看本站提供的教学视频
-
通过答疑系统,提交学习中遇到的问题,与同学讨论或者导师答疑
-
完成章节学习后,提交作业
-
导师批改作业并打分
-
模块学习完成后进行考核
项目意义
目标:
通过互联技术,变革传统教育方式,依赖强大的导师服务系统,提高在线教育的效率与成功率。
项目策略:
-
提供具有时间限制的教学服务
-
安排合理的模块化学习
-
聘用高素质的导师,提供优质服务
-
导师对学员一对一在线考核
-
学习周期中全程导师监督(跟进记录)
-
提供高效的答疑解惑服务(12小时)
-
建立完善的奖学金制度
-
是否按学习计划完成学业
-
每章节作业完成情况
-
项目开发团队与进度
团队构成:
-
开发
-
导师后台,stark组件+rbac : 1人
-
管理后台,stark组件+rbac : 1人
-
主站
-
vue.js 1人
-
api 村长+1/2文州+1/2Alex+其他 + 村长
-
-
-
运维(1人)
-
测试(1人)
-
产品经理(1人)
-
UI设计(1人)
-
运营(1人)
-
销售(4人)
-
全职导师(2人)
-
签约讲师(...)
周期:
-
7月份项目开始
-
11月份上线
-
11月份~次年5月份: 修Bug,活动支持,广告。。。
-
-
-
课程大类
-
课程子类
-
学位课
-
讲师
-
奖学金
-
-
专题课(学位课模块表)
-
价格策略(contenttype)
-
课程详细(o2o -> 水平分表)
-
常见问题
-
课程大纲
-
章节
-
课时
-
作业
-
-
深科技(4+2)
-
用户表
-
用户token
-
文章来源
-
文章表
-
通用评论表
-
-
支付相关(4+2)
-
优惠券表
-
订单表
-
订单详情表
-
文章表
-
通用评论表
-
Django中实现:
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.db.models import Q class CourseCategory(models.Model): """课程大类, e.g 前端 后端...""" name = models.CharField(max_length=64, unique=True) def __str__(self): return "%s" % self.name class Meta: verbose_name = "课程大类" verbose_name_plural = "课程大类" class CourseSubCategory(models.Model): """课程子类, e.g python linux """ category = models.ForeignKey("CourseCategory") name = models.CharField(max_length=64, unique=True) def __str__(self): return "%s" % self.name class Meta: verbose_name = "课程子类" verbose_name_plural = "课程子类" class DegreeCourse(models.Model): """学位课程""" name = models.CharField(max_length=128, unique=True) course_img = models.CharField(max_length=255, verbose_name="缩略图") brief = models.TextField(verbose_name="学位课程简介", ) total_scholarship = models.PositiveIntegerField(verbose_name="总奖学金(贝里)", default=40000) mentor_compensation_bonus = models.PositiveIntegerField(verbose_name="本课程的导师辅导费用(贝里)", default=15000) # 忽略,用于GenericForeignKey反向查询, 不会生成表字段,切勿删除 coupon = GenericRelation("Coupon") # 为了计算学位奖学金 period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=150) prerequisite = models.TextField(verbose_name="课程先修要求", max_length=1024) teachers = models.ManyToManyField("Teacher", verbose_name="课程讲师") # 忽略,用于GenericForeignKey反向查询,不会生成表字段,切勿删除 degreecourse_price_policy = GenericRelation("PricePolicy") def __str__(self): return self.name class Scholarship(models.Model): """学位课程奖学金""" degree_course = models.ForeignKey("DegreeCourse") time_percent = models.PositiveSmallIntegerField(verbose_name="奖励档位(时间百分比)", help_text="只填百分值,如80,代表80%") value = models.PositiveIntegerField(verbose_name="奖学金数额") def __str__(self): return "%s:%s" % (self.degree_course, self.value) class Course(models.Model): """课程""" name = models.CharField(max_length=128, unique=True) course_img = models.CharField(max_length=255) sub_category = models.ForeignKey("CourseSubCategory") course_type_choices = ((0, \'付费\'), (1, \'VIP专享\'), (2, \'学位课程\')) course_type = models.SmallIntegerField(choices=course_type_choices) degree_course = models.ForeignKey("DegreeCourse", blank=True, null=True, help_text="若是学位课程,此处关联学位表") brief = models.TextField(verbose_name="课程概述", max_length=2048) level_choices = ((0, \'初级\'), (1, \'中级\'), (2, \'高级\')) level = models.SmallIntegerField(choices=level_choices, default=1) pub_date = models.DateField(verbose_name="发布日期", blank=True, null=True) period = models.PositiveIntegerField(verbose_name="建议学习周期(days)", default=7) order = models.IntegerField("课程顺序", help_text="从上一个课程数字往后排") attachment_path = models.CharField(max_length=128, verbose_name="课件路径", blank=True, null=True) status_choices = ((0, \'上线\'), (1, \'下线\'), (2, \'预上线\')) status = models.SmallIntegerField(choices=status_choices, default=0) template_id = models.SmallIntegerField("前端模板id", default=1) coupon = GenericRelation("Coupon") # 用于GenericForeignKey反向查询,不会生成表字段,切勿删除 price_policy = GenericRelation("PricePolicy") def __str__(self): return "%s(%s)" % (self.name, self.get_course_type_display()) def save(self, *args, **kwargs): if self.course_type == 2: if not self.degree_course: raise ValueError("学位课程必须关联对应的学位表") super(Course, self).save(*args, **kwargs) class PricePolicy(models.Model): """价格与有课程效期表""" content_type = models.ForeignKey(ContentType) 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个月\'), (180, \'6个月\'), (210, \'12个月\'), (540, \'18个月\'), (720, \'24个月\'), ) valid_period = models.SmallIntegerField(choices=valid_period_choices) price = models.FloatField() class Meta: unique_together = ("content_type", \'object_id\', "valid_period") def __str__(self): return "%s(%s)%s" % (self.content_object, self.get_valid_period_display(), self.price) class CourseDetail(models.Model): """课程详情页内容""" course = models.OneToOneField("Course") hours = models.IntegerField("课时") course_slogan = models.CharField(max_length=125, blank=True, null=True) video_brief_link = models.CharField(verbose_name=\'课程介绍\', max_length=255, blank=True, null=True) 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 "%s" % self.course class OftenAskedQuestion(models.Model): """常见问题""" content_type = models.ForeignKey(ContentType,limit_choices_to={\'model__contains\': \'course\'}) # 关联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: unique_together = (\'content_type\', \'object_id\', \'question\') class CourseOutline(models.Model): """课程大纲""" course_detail = models.ForeignKey("CourseDetail") 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: unique_together = (\'course_detail\', \'title\') class CourseChapter(models.Model): """课程章节""" course = models.ForeignKey("Course", related_name=\'coursechapters\') chapter = models.SmallIntegerField(verbose_name="第几章", default=1) name = models.CharField(max_length=128) summary = models.TextField(verbose_name="章节介绍", blank=True, null=True) pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True) class Meta: unique_together = ("course", \'chapter\') def __str__(self): return "%s:(第%s章)%s" % (self.course, self.chapter, self.name) class CourseSection(models.Model): """课时目录""" chapter = models.ForeignKey("CourseChapter", related_name=\'coursesections\') name = models.CharField(max_length=128) order = models.PositiveSmallIntegerField(verbose_name="课时排序", help_text="建议每个课时之间空1至2个值,以备后续插入课时") section_type_choices = ((0, \'文档\'), (1, \'练习\'), (2, \'视频\')) section_type = models.SmallIntegerField(default=2, choices=section_type_choices) # vid: cc视频,唯一标识:Dfadljfosdfjlsdfjs section_link = models.CharField(max_length=255, blank=True, null=True, help_text="若是video,填vid,若是文档,填link") video_time = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32) # 仅在前端展示使用 pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True) free_trail = models.BooleanField("是否可试看", default=False) class Meta: unique_together = (\'chapter\', \'section_link\') def __str__(self): return "%s-%s" % (self.chapter, self.name) class Homework(models.Model): chapter = models.ForeignKey("CourseChapter") title = models.CharField(max_length=128, verbose_name="作业题目") order = models.PositiveSmallIntegerField("作业顺序", help_text="同一课程的每个作业之前的order值间隔1-2个数") homework_type_choices = ((0, \'作业\'), (1, \'模块通关考核\')) homework_type = models.SmallIntegerField(choices=homework_type_choices, default=0) requirement = models.TextField(max_length=1024, verbose_name="作业需求") threshold = models.TextField(max_length=1024, verbose_name="踩分点") recommend_period = models.PositiveSmallIntegerField("推荐完成周期(天)", default=7) scholarship_value = models.PositiveSmallIntegerField("为该作业分配的奖学金(贝里)") note = models.TextField(blank=True, null=True) enabled = models.BooleanField(default=True, help_text="本作业如果后期不需要了,不想让学员看到,可以设置为False") class Meta: unique_together = ("chapter", "title") def __str__(self): return "%s - %s" % (self.chapter, self.title) class Teacher(models.Model): """讲师、导师表""" name = models.CharField(max_length=32) role_choices = ((0, \'讲师\'), (1, \'导师\')) role = models.SmallIntegerField(choices=role_choices, default=0) title = models.CharField(max_length=64, verbose_name="职位、职称") signature = models.CharField(max_length=255, help_text="导师签名", blank=True, null=True) image = models.CharField(max_length=128) brief = models.TextField(max_length=1024) def __str__(self): return self.name # 普通课购买课程后,我的订单中去评价。 class CourseReview(models.Model): """课程评价""" enrolled_course = models.OneToOneField("EnrolledCourse") about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰") about_video = models.FloatField(default=0, verbose_name="内容实用") about_course = models.FloatField(default=0, verbose_name="课程内容通俗易懂") review = models.TextField(max_length=1024, verbose_name="评价") disagree_number = models.IntegerField(default=0, verbose_name="踩") agree_number = models.IntegerField(default=0, verbose_name="赞同数") date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期") is_recommend = models.BooleanField("热评推荐", default=False) hide = models.BooleanField("不在前端页面显示此条评价", default=False) def __str__(self): return "%s-%s" % (self.enrolled_course.course, self.review) # 学位课购买课程后,我的订单中去天报名表,然后就编程去学习(以后去评价)。 class DegreeCourseReview(models.Model): """学位课程评价 为了以后可以定制单独的评价内容,所以不与普通课程的评价混在一起,单独建表 """ enrolled_course = models.ForeignKey("EnrolledDegreeCourse") course = models.ForeignKey("Course", verbose_name="评价学位模块", blank=True, null=True, help_text="不填写即代表评价整个学位课程", limit_choices_to={\'course_type\': 2}) about_teacher = models.FloatField(default=0, verbose_name="讲师讲解是否清晰") about_video = models.FloatField(default=0, verbose_name="视频质量") about_course = models.FloatField(default=0, verbose_name="课程") review = models.TextField(max_length=1024, verbose_name="评价") disagree_number = models.IntegerField(default=0, verbose_name="踩") agree_number = models.IntegerField(default=0, verbose_name="赞同数") date = models.DateTimeField(auto_now_add=True, verbose_name="评价日期") is_recommend = models.BooleanField("热评推荐", default=False) hide = models.BooleanField("不在前端页面显示此条评价", default=False) def __str__(self): return "%s-%s" % (self.enrolled_course, self.review) # 购买学位课后,为每个学生每个模块会立刻生成一条学习纪录(未开通) class StudyRecord(models.Model): """学位课程的模块学习进度 报名学位课程后,每个模块会立刻生成一条学习纪录 """ enrolled_degree_course = models.ForeignKey("EnrolledDegreeCourse") course_module = models.ForeignKey("Course", verbose_name="学位模块", limit_choices_to={\'course_type\': 2}) open_date = models.DateField(blank=True, null=True, verbose_name="开通日期") end_date = models.DateField(blank=True, null=True, verbose_name="完成日期") status_choices = ((2, \'在学\'), (1, \'未开通\'), (0, \'已完成\')) status = models.SmallIntegerField(choices=status_choices, default=1) class Meta: unique_together = (\'enrolled_degree_course\', \'course_module\') def __str__(self): return \'%s-%s\' % (self.enrolled_degree_course, self.course_module) def save(self, *args, **kwargs): if self.course_module.degree_course_id != self.enrolled_degree_course.degree_course_id: raise ValueError("学员要开通的模块必须与其报名的学位课程一致!") super(StudyRecord, self).save(*args, **kwargs) # 为学生开通一个模块 class CourseSchedule(models.Model): """课程进度计划表,针对学位课程,每开通一个模块,就为这个学员生成这个模块的推荐学习计划表,后面的奖惩均按此表进行""" study_record = models.ForeignKey("StudyRecord") homework = models.ForeignKey("Homework") recommend_date = models.DateField("推荐交作业日期") def __str__(self): return "%s - %s - %s " % (self.study_record, self.homework, self.recommend_date) class Meta: unique_together = (\'study_record\', \'homework\') # 学生交作业 class HomeworkRecord(models.Model): """学员作业记录及成绩""" homework = models.ForeignKey("Homework") student = models.ForeignKey("EnrolledDegreeCourse", verbose_name="学生") score_choices = ( (100, \'A+\'), (90, \'A\'), (85, \'B+\'), (80, \'B\'), (70, \'B-\'), (60, \'C+\'), (50, \'C\'), (40, \'C-\'), (-1, \'D\'), (0, \'N/A\'), (-100, \'COPY\'), ) score = models.SmallIntegerField(verbose_name="分数", choices=score_choices, null=True, blank=True) mentor = models.ForeignKey("Account", related_name="my_stu_homework_record", limit_choices_to={\'role\': 1}, verbose_name="导师") mentor_comment = models.TextField(verbose_name="导师批注", blank=True, null=True) # 导师 status_choice = ( (0, \'待批改\'), (1, \'已通过\'), (2, \'不合格\'), ) status = models.SmallIntegerField(verbose_name=\'作业状态\', choices=status_choice, default=0) submit_num = models.SmallIntegerField(verbose_name=\'提交次数\', default=0) correct_date = models.DateTimeField(\'备注日期\', blank=True, null=True) note = models.TextField(blank=True, null=True) date = models.DateTimeField("作业提交日期", auto_now_add=True) check_date = models.DateTimeField("批改日期", null=True, blank=True) update_time = models.DateTimeField(auto_now=True, verbose_name="提交日期") # homework_path = models.CharField(verbose_name=\'作业路径\', max_length=256,blank=True,null=True) 作业路径可以动态拿到,没必要存 reward_choice = ( (0, \'新提交\'), (1, \'按时提交\'), (2, \'未按时提交\'), (3, \'成绩已奖励\'), (4, \'成绩已处罚\'), (5, \'未作按时检测\'), ) reward_status = models.SmallIntegerField(verbose_name=\'作业记录奖惩状态\', default=0) def __str__(self): return "%s %s" % (self.homework, self.student) class Meta: unique_together = ("homework", "student") # 导师跟进记录 class StuFollowUpRecord(models.Model): """学员跟进记录""" enrolled_degree_course = models.ForeignKey("EnrolledDegreeCourse", verbose_name="学生") mentor = models.ForeignKey("Account", related_name=\'mentor\', limit_choices_to={\'role\': 1}, verbose_name="导师") followup_tool_choices = ((0, \'QQ\'), (1, \'微信\'), (2, \'电话\'), (3, \'系统通知\')) followup_tool = models.SmallIntegerField(choices=followup_tool_choices, default=1) record = models.TextField(verbose_name="跟进记录") attachment_path = models.CharField(max_length=128, blank=True, null=True, verbose_name="附件路径", help_text="跟进记录的截图等") date = models.DateTimeField(auto_now_add=True) def __str__(self): return "%s --%s --%s" % (self.enrolled_degree_course, self.record, self.date) # ######################## 深科技 ######################## class ArticleSource(models.Model): """文章来源""" name = models.CharField(max_length=64, unique=True) def __str__(self): return self.name class Article(models.Model): """文章资讯""" title = models.CharField(max_length=255, unique=True, db_index=True, verbose_name="标题") source = models.ForeignKey("ArticleSource", verbose_name="来源") article_type_choices = ((0, \'资讯\'), (1, \'视频\')) article_type = models.SmallIntegerField(choices=article_type_choices, default=0) brief = models.TextField(max_length=512, verbose_name="摘要") head_img = models.CharField(max_length=255) content = models.TextField(verbose_name="文章正文") pub_date = models.DateTimeField(verbose_name="上架日期") offline_date = models.DateTimeField(verbose_name="下架日期") status_choices = ((0, \'在线\'), (1, \'下线\')) status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="状态") order = models.SmallIntegerField(default=0, verbose_name="权重", help_text="文章想置顶,可以把数字调大,不要超过1000") vid = models.CharField(max_length=128, verbose_name="视频VID", help_text="文章类型是视频, 则需要添加视频VID", blank=True, null=True) comment_num = models.SmallIntegerField(default=0, verbose_name="评论数") agree_num = models.SmallIntegerField(default=0, verbose_name="点赞数") view_num = models.SmallIntegerField(default=0, verbose_name="观看数") collect_num = models.SmallIntegerField(default=0, verbose_name="收藏数") # tags = models.ManyToManyField("Tags", blank=True, verbose_name="标签") date = models.DateTimeField(auto_now_add=True, verbose_name="创建日期") position_choices = ((0, \'信息流\'), (1, \'banner大图\'), (2, \'banner小图\')) position = models.SmallIntegerField(choices=position_choices, default=0, verbose_name="位置") # comment = GenericRelation("Comment") # 用于GenericForeignKey反向查询, 不会生成表字段,切勿删除,如有疑问请联系老村长 def __str__(self): return "%s-%s" % (self.source, self.title) class Collection(models.Model): """收藏""" content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = GenericForeignKey(\'content_type\', \'object_id\') account = models.ForeignKey("Account") date = models.DateTimeField(auto_now_add=True) class Meta: unique_together = (\'content_type\', \'object_id\', \'account\') class Comment(models.Model): """通用的评论表""" # content_type = models.ForeignKey(ContentType, blank=True, null=True, verbose_name="类型") # object_id = models.PositiveIntegerField(blank=True, null=True) # content_object = GenericForeignKey(\'content_type\', \'object_id\') # FK(Article) article = models.ForeignKey(\'Article\') p_node = models.ForeignKey("self", blank=True, null=True, verbose_name="父级评论") content = models.TextField(max_length=1024) account = models.ForeignKey("Account", verbose_name="会员名") disagree_number = models.IntegerField(default=0, verbose_name="踩") agree_number = models.IntegerField(default=0, verbose_name="赞同数") date = models.DateTimeField(auto_now_add=True) def __str__(self): return self.content # ######################## 购买课程相关 ######################## class EnrolledCourse(models.Model): """已报名课程,不包括学位课程""" account = models.ForeignKey("Account") course = models.ForeignKey("Course", limit_choices_to=~Q(course_type=2)) enrolled_date = models.DateTimeField(auto_now_add=True) valid_begin_date = models.DateField(verbose_name="有效期开始自") valid_end_date = models.DateField(verbose_name="有效期结束至") status_choices = ((0, \'已开通\'), (1, \'已过期\')) status = models.SmallIntegerField(choices=status_choices, default=0) order_detail = models.OneToOneField("OrderDetail") # 使订单购买后支持 课程评价 # order = models.ForeignKey("Order",blank=True,null=True) def __str__(self): return "%s:%s" % (self.account, self.course) class DegreeRegistrationForm(models.Model): """学位课程报名表""" enrolled_degree = models.OneToOneField("EnrolledDegreeCourse") current_company = models.CharField(max_length=64, ) current_position = models.CharField(max_length=64, ) current_salary = models.IntegerField() work_experience_choices = ( (0, "应届生"), (1, "1年"), (2, "2年"), (3, "3年"), (4, "4年"), (5, "5年"), (6, "6年"), (7, "7年"), (8, "8年"), (9, "9年"), (10, "10年"), (11, "超过10年"), ) work_experience = models.IntegerField() open_module = models.BooleanField("是否开通第1模块", default=True) stu_specified_mentor = models.CharField("学员自行指定的导师名", max_length=32, blank=True, null=True) study_plan_choices = ((0, "1-2小时/天"), (1, "2-3小时/天"), (2, "3-5小时/天"), (3, "5小时+/天"), ) study_plan = models.SmallIntegerField(choices=study_plan_choices, default=1) why_take_this_course = models.TextField("报此课程原因", max_length=1024) why_choose_us = models.TextField("为何选路飞", max_length=1024) your_expectation = models.TextField("你的期待", max_length=1024) memo = models.CharField(max_length=255, blank=True, null=True) def __str__(self): return "%s" % self.enrolled_degree class EnrolledDegreeCourse(models.Model): """已报名的学位课程""" account = models.ForeignKey("Account") degree_course = models.ForeignKey("DegreeCourse") enrolled_date = models.DateTimeField(auto_now_add=True) valid_begin_date = models.DateField(verbose_name="有效期开始自", blank=True, null=True) # 开通第一个模块时,再添加课程有效期,2年 valid_end_date = models.DateField(verbose_name="有效期结束至", blank=True, null=True) status_choices = ( (0, \'在学中\'), (1, \'休学中\'), (2, \'已毕业\'), (3, \'超时结业\'), (4, \'未开始\'), # (3, \'其它\'), ) study_status = models.SmallIntegerField(choices=status_choices, default=0) mentor = models.ForeignKey("Account", verbose_name="导师", related_name=\'my_students\', blank=True, null=True, limit_choices_to={\'role\': 1}) mentor_fee_balance = models.PositiveIntegerField("导师费用余额", help_text="这个学员的导师费用,每有惩罚,需在此字段同时扣除") order_detail = models.OneToOneField("OrderDetail") # 使订单购买后支持填写报名表 def __str__(self): return "%s:%s" % (self.account, self.degree_course) class Meta: unique_together = (\'account\', \'degree_course\') 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="等值货币") off_percent = models.PositiveSmallIntegerField("折扣百分比", help_text="只针对折扣券,例7.9折,写79", blank=True, null=True) minimum_consume = models.PositiveIntegerField("最低消费", default=0, help_text="仅在满减券时填写此字段") 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\') quantity = models.PositiveIntegerField("数量(张)", default=1) 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) 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) account = models.ForeignKey("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 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("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="订单取消时间") 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("折后价格") content = models.CharField(max_length=255, blank=True, null=True) # ? valid_period_display = models.CharField("有效期显示", max_length=32) # 在订单页显示 valid_period = models.PositiveIntegerField("有效期(days)") # 课程有效期 memo = models.CharField(max_length=255, blank=True, null=True) # def __str__(self): # return "%s - %s - %s" % (self.order, self.content_type, self.price) class Meta: # unique_together = ("order", \'course\') unique_together = ("order", \'content_type\', \'object_id\') class TransactionRecord(models.Model): """贝里交易纪录""" account = models.ForeignKey("Account") amount = models.IntegerField("金额") balance = models.IntegerField("账户余额") transaction_type_choices = ((0, \'收入\'), (1, \'支出\'), (2, \'退款\'), (3, "提现")) # 2 为了处理 订单过期未支付时,锁定期贝里的回退 transaction_type = models.SmallIntegerField(choices=transaction_type_choices) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.PositiveIntegerField(blank=True, null=True, verbose_name="关联对象") content_object = GenericForeignKey(\'content_type\', \'object_id\') 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) def __str__(self): return "%s" % self.transaction_number # ######################## 用户 ######################## class Account(models.Model): username = models.CharField("用户名", max_length=64, unique=True) password = models.CharField(\'password\', max_length=128) class UserAuthToken(models.Model): """ 用户Token表 """ user = models.OneToOneField(to="Account") token = models.CharField(max_length=40) created = models.DateTimeField(auto_now_add=True) models
支付流程复盘
支付需求起始于用户点击立即支付或者加入购物车
1.添加到购物车
当用户选择需要购买的课程和价格策略后,有两种方式购买课程:
1.1 立即购买
将课程id和选择的价格策略放到redis中,跳到去支付页面,从redis中获取购买课程的id和价格策略id,如果该用户要使用优惠券和贝利,则选择当前用户所拥有并且未使用和没过期的优惠券,得到折后价格,点击去支付完成支付,
1.2 添加到购物车中完成支付(post)
前端需要提供的数据包括:所选课程的id和所选择的价格策略id。约定的数据格式如下:
{ "course_id":1, "policy_id":3 }
后端获取到所选课程的id和所选择的价格策略id,根据课程id获取到当前课程,然后根据当前课程获取到当前课程所有的价格策略,判断在后端获取到的价格策略id在不在当前课程的价格策略中,不在的话则抛出异常价格策略不存在,在的话继续执行,将该课程所有价格策略的id,时间周期和价格存放到price_policy_list中
# 1.在这里获得用户的课程ID与价格策略ID course_id = int(request.data.get(\'course_id\')) policy_id = int(request.data.get(\'policy_id\')) # 2. 获取专题课信息 course = models.Course.objects.get(id=course_id) # 3.获取课程相关的所有价格策略 price_policy_list = course.price_policy.all() price_policy_dict = {} for item in price_policy_list: price_policy_dict[item.id] = { "period": item.valid_period, "period_display": item.get_valid_period_display(), "price": item.price } if policy_id not in price_policy_dict: raise PricePolicyInvalid(\'价格策略不合法\')
继续将该课程的所有信息主要包括:课程id,课程图片地址,课程标题,所有价格策略,默认价格策略封装在car_dict中
下面是redis中保存用户数据的key
SHOPPING_CART_KEY = "luffy_shopping_cart_%s_%s" PAYMENT_KEY = "luffy_payment_%s_%s" PAYMENT_COUPON_KEY = "luffy_payment_coupon_%s"
car_key = settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) car_dict = { \'title\': course.name, \'img\': course.course_img, \'default_policy\': policy_id, \'policy\': json.dumps(price_policy_dict) }
完成后,在redis中用户购物车的数据如下:
{ luffy_shopping_car_6_11:{ \'title\':\'21天入门到放弃\', \'src\':\'xxx.png\', \'policy\':{ 1:{id:\'xx\'.....}, 2:{id:\'xx\'.....}, 3:{id:\'xx\'.....}, 4:{id:\'xx\'.....}, }, \'default_policy\':3 }, luffy_shopping_car_6_13:{ ... } }
购物车完整的CURD接口:
#!/usr/bin/env python # -*- coding: utf-8 -*- """ @Author : Young @Email : hyc554@outlook.com @site : http://www.cnblogs.com/huang-yc/ @File : shopping_cart.py @version : 1.0 @Time : 2018/11/4 20:51 Description about this file: """ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ViewSetMixin from django_redis import get_redis_connection from utils.response_dict import BaseResponse from api.views.auth import YcAuth from api import models from utils.error import PricePolicyInvalid from django.core.exceptions import ObjectDoesNotExist from django.conf import settings import json class ShoppingCart(APIView): authentication_classes = [YcAuth] conn = get_redis_connection(\'default\') def post(self, request, *args, **kwargs): """ 向购物车中添加商品 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: # 在这里获得用户的课程ID与价格策略ID course_id = int(request.data.get(\'course_id\')) policy_id = int(request.data.get(\'policy_id\')) # 2. 获取专题课信息 course = models.Course.objects.get(id=course_id) # 3.获取课程相关的所有价格策略 price_policy_list = course.price_policy.all() price_policy_dict = {} for item in price_policy_list: price_policy_dict[item.id] = { "period": item.valid_period, "period_display": item.get_valid_period_display(), "price": item.price } if policy_id not in price_policy_dict: raise PricePolicyInvalid(\'价格策略不合法\') # 5. 将购物信息添加到redis中 # self.conn # car_key = "luffy_shopping_car_%s_%s" car_key = settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) car_dict = { \'title\': course.name, \'img\': course.course_img, \'default_policy\': policy_id, \'policy\': json.dumps(price_policy_dict) } # conn = get_redis_connection("default") self.conn.hmset(car_key, car_dict) ret.data = \'添加成功\' except PricePolicyInvalid as e: ret.data = 2001 ret.error = e.msg except ObjectDoesNotExist as e: ret.data = 2002 ret.error = \'课程不存在\' except Exception as e: ret.code = 1001 ret.data = \'获取购物车数据失败\' return Response(ret.dict) def delete(self, request, *args, **kwargs): ret = BaseResponse() try: course_id_list = request.data.get(\'courseids\') key_list = [settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id,) for course_id in course_id_list] self.conn.delete(*key_list) except Exception as e: ret.code = 1002 ret.error = "删除失败" return Response(ret.dict) def patch(self, request, *args, **kwargs): ret = BaseResponse() try: course_id = int(request.data.get(\'course_id\')) policy_id = str(request.data.get(\'policy_id\')) key = settings.SHOPPING_CART_KEY % (request.auth.user_id, course_id) if not self.conn.exists(key): ret.code = 1002 ret.error = "购物车中不存在此课程" return Response(ret.dict) # 3. redis中获取所有的价格策略 # 请注意这里对redis里的数据进行读取的操作 policy_dict = json.loads(str(self.conn.hget(key, \'policy\'), encoding=\'utf-8\')) if policy_id not in policy_dict: ret.code = 1003 ret.error = "价格策略不合法" return Response(ret.dict) self.conn.hset(key, \'default_policy\', policy_id) ret.data = "修改成功" except Exception as e: ret.code = 2003 ret.error = \'修改数据失败\' return Response(ret.dict) def get(self, request, *args, **kwargs): """ 查看购物车中所有的商品 :param request: :param args: :param kwargs: :return: """ ret = BaseResponse() try: key_match = settings.SHOPPING_CART_KEY % (request.auth.user_id, "*") course_list = [] for key in self.conn.scan_iter(key_match, count=10): info = { "title": self.conn.hget(key, \'title\').decode(\'utf-8\'), "img": self.conn.hget(key, \'img\').decode(\'utf-8\'), "policy": json.loads(self.conn.hget(key, \'policy\').decode(\'utf-8\')), "default_policy": self.conn.hget(key, \'default_policy\').decode(\'utf-8\') } course_list.append(info) ret.data = course_list except Exception as e: ret.code = 1002 ret.error = "获取失败" return Response(ret.dict)
修改购物车总的价格策略(patch):
向后台发课程id和要修改为的价格策略id,判断课程是否在购物车中,判断传递过来的价格策略是否在当前课程的价格策略中,在的话将 redis中的当前课程的默认价格策略修改为当前的价格策略具体将上面代码
结算中心
在用户完成购物车的后,点击结算,前端服务器发送结算数据("courseids":["1","2"],1和2对应的就是课程的ID)给我们后端的Django服务器,约定的数据格如下:
{ "courseids":["1","2"] }
1.获取用户提交的课程id, [1,2]
判断是否选择要结算的课程,没选择则抛出异常
course_id_list = request.data.get(\'course_list\') if not course_id_list or not isinstance(course_id_list, list): raise Exception(\'请选择要结算的课程\')
2.检测购物车中检查是否已经有课程(应该有课程的)
product_dict = redis_pool.conn.hget(settings.LUFFY_SHOPPING_CAR, request.user.id) if not product_dict: raise Exception(\'购物车无课程\')
3.检测购物车中是否有用户要购买的课程
product_dict = json.loads(product_dict.decode(\'utf-8\')) # ###### 课程、价格和优惠券 ####### policy_course_dict = {} # 循环用户传递过来的要结算的课程ID列表 for course_id in course_id_list: course_id = str(course_id) product = product_dict.get(course_id) if not product: raise Exception(\'购买的课程必须先加入购物车\')
如果所结算的课程在购物车中,
4.获取选中价格策略的价格详细,
选择购物车中当前课程下的所有价格策略和当前课程的所选择的价格策略相等的价格策略,获取其信息,
# c. 购物车中是否有用户要购买的课程 product_dict = json.loads(product_dict.decode(\'utf-8\')) # ###### 课程、价格和优惠券 ####### policy_course_dict = {} # 循环用户传递过来的要结算的课程ID列表 for course_id in course_id_list: course_id = str(course_id) product = product_dict.get(course_id) if not product: raise Exception(\'购买的课程必须先加入购物车\') # 获取选中价格策略的价格详细 policy_exist = False for policy in product[\'price_policy_list\']: if policy[\'id\'] == product[\'choice_policy_id\']: policy_price = policy[\'price\'] policy_period = policy[\'period\'] policy_valid_period = policy[\'valid_period\'] policy_exist = True break if not policy_exist: raise Exception(\'购物车中的课程无此价格\')
将上面我们获取的课程信息和价格策略信息封装在policy_course中
policy_course = { \'course_id\': course_id, \'course_name\': product[\'name\'], \'course_img\': product[\'course_img\'], \'policy_id\': product[\'choice_policy_id\'], \'policy_price\': policy_price, \'policy_period\': policy_period, \'policy_valid_period\': policy_valid_period, \'default_coupon_id\': 0, \'coupon_record_list\': { 0:{\'id\': 0, \'text\': \'请选择优惠券\'}, }, }
5.获取当前用户所有的优惠券
user_coupon_list = models.CouponRecord.objects.filter(account=request.user,status=0)
6.区分用户的优惠券种类,课程优惠券添加到课程中;全局优惠券添加到全局
# ###### 全局优惠券 ####### global_coupon_record_dict = {}
循环遍历当前用户的所有优惠券,判断他们是否过期
begin_date = record.coupon.valid_begin_date end_date = record.coupon.valid_end_date if begin_date: if current_date < begin_date: continue if end_date: if current_date > end_date: continue
,如果没过期,判断他们是全局优惠券还是针对某个课程的优惠券,区分好是什么优惠券以后还的区分该优惠券是什么类型,
如果是通用券
if record.coupon.coupon_type == 0: temp = {\'type\': 0, \'text\': "通用优惠券", \'id\': record.id, \'begin_date\': begin_date, \'end_date\': end_date, \'money_equivalent_value\': record.coupon.money_equivalent_value}
如果是满减券:
elif record.coupon.coupon_type == 1: temp = {\'type\': 1, \'text\': "满减券", \'id\': record.id, \'begin_date\': begin_date, \'end_date\': end_date, \'minimum_consume\': record.coupon.minimum_consume, \'money_equivalent_value\': record.coupon.money_equivalent_value}
如果是折扣券:
elif record.coupon.coupon_type == 2: temp = {\'type\': 2, \'text\': "折扣券", \'id\': record.id, \'begin_date\': begin_date, \'end_date\': end_date, \'off_percent\': record.coupon.off_percent}
如果是全局优惠券,则
global_coupon_record_dict[record.id] = temp
如果但是针对课程的优惠券:
policy_course_dict[cid][\'coupon_record_list\'][record.id] = temp
最后将所有数据封装在user_pay中放到redis上
user_pay = { \'policy_course_dict\': policy_course_dict, \'global_coupon_record_dict\': global_coupon_record_dict, \'default_global_coupon_id\': 0, }
redis_pool.conn.hset(settings.LUFFY_PAYMENT, request.user.id, json.dumps(user_pay))
user_pay数据结构
结算中心 = { 用户ID: { policy_course_dict:{ 1:{ \'course_id\': course_id, \'course_name\': product[\'name\'], \'course_img\': product[\'course_img\'], \'policy_id\': product[\'choice_policy_id\'], \'policy_price\': policy_price, \'policy_\': policy_period, # 30/ \'default_coupon_id\': 1, \'coupon_record_list\': { 0:{\'id\': 0, \'text\': \'请选择优惠券\'}, 1:{\'id\': 1, \'type\':1, \'text\': \'优惠券1\', ..}, 2:{\'id\': 2, \'type\':2, \'text\': \'优惠券1\', ..}, 3: {\'id\': 3, \'type\':3, \'text\': \'优惠券1\', ..}, }, }, 2:{ \'course_id\': course_id, \'course_name\': product[\'name\'], \'course_img\': product[\'course_img\'], \'policy_id\': product[\'choice_policy_id\'], \'policy_price\': policy_price, \'policy_\': policy_period, \'default_coupon_id\': 0, \'coupon_record_list\': { 0:{\'id\': 0, \'text\': \'请选择优惠券\'}, 1:{\'id\': 1, \'type\':1, \'text\': \'优惠券1\', ..}, 2:{\'id\': 2, \'type\':2, \'text\': \'优惠券1\', ..}, 3: {\'id\': 3, \'type\':3, \'text\': \'优惠券1\', ..}, }, } }, global_coupon_dict:{ 1:{\'type\': 0, \'text\': "通用优惠券", \'id\': 1, ..}, 2:{\'type\': 0, \'text\': "通用优惠券", \'id\': 2, ..}, 3:{\'type\': 0, \'text\': "通用优惠券", \'id\': 3, ...}, 4:{\'type\': 0, \'text\': "通用优惠券", \'id\': 4, ...}, }, choice_global_coupon:3 } }
去支付
1.去结算中心获取要结算的所有课程和优惠券
2.循环遍历每一个课程
开始总价格totalprice和折扣价totaldiscount都为0,
总价 = 0 总折扣 = 0
2.1.如果该课程没有使用优惠券,则总价格=totalprice+课程原价,totaldiscount=0,
b. 循环购买的所有课程 当前时间 = datetime.datetime.now() 当前日期 = datetime.datetime.now().date ****课程信息 = [] ***** ****使用的优惠券ID列表 = [] ***** for course_id,values in policy_course_dict.items(): 课程原价 = values[\'policy_price\'] 使用的优惠券ID = values[\'default_coupon_id\'] discount = 0 # 未使用优惠券 temp = { 课程ID: 1, 原价: 10, 折扣价:10, 有效期:30 } 课程信息.append(temp) 总价 += 课程原价 折扣 += discount
2.3.如果使用了优惠券,则需要去判断所使用的优惠券是否已经过期,或是否已经被使用,如果过期了或者被使用了就抛出异常,否则继续往下执行,判断该优惠券的类型
如果使用了优惠券: 去数据库查询:指定优惠券是否已经使用、是否已经过期 如果优惠券可不用: raise Exception(\'优惠券不可用\') 如果是通用优惠券: discount = 通用优惠券(如果大于课程原价,课程原价) elif 如果是满减优惠券: if 价格是否大于最小满减要求: discount = 通用优惠券(如果大于课程原价,课程原价) elif 如果是折扣优惠券: discount = 课程原价 * (1-折扣) 使用的优惠券ID列表.append(绑定可以的优惠券ID) temp = { 课程ID: 1, 原价: 10, 折扣价:9, 有效期:30 } 课程信息.append(temp) 总价 += 课程原价 折扣 += discount
到此为止
pay = 总价 - 总折扣
3.继续计算看是否使用全局优惠券
全站优惠券ID = choice_global_coupon 数据库获取并检查是否可用(优惠券是否已经使用、是否已经过期) 如果优惠券可不用: raise(\'全站优惠券不可用\') g_discount = 0 如果是通用优惠券: g_discount = 通用优惠券(如果大于pay,pay) elif 如果是满减优惠券: if 价格是否大于最小满减要求: g_discount = 通用优惠券(如果大于pay,pay) elif 如果是折扣优惠券: g_discount = pay * (1- 折扣) 总折扣 += g_discount 使用的优惠券ID列表.append(全站优惠券ID)
4.贝利支付
if balance <= request.user.balance: 总折扣 += balance
5.总结算
总价 - 总折扣 = alipay
if alipay ==0: 贝里&优惠券 pay_type = 0 else: 支付宝支付 pay_type = 1
如果最后支付=0,就直接修改支付状态为已支付,否则改为待支付
6.点击立即支付以后 进行数据库操作
事务: # 1. 创建订单表 order_obj = models.Order.objects.create(....status=0) # pay_type = 0 或 order_obj = models.Order.objects.create(....status=1) # pay_type = 1 # 2. 生成订单详细 for item in 课程信息: detail_obj = models.OrderDetail.objects.create(order_obj,课程ID,原价和折扣价) models.EnrolledCourse.objects.create(...当前时间,当前时间+30,status=1) # 3. 处理优惠券 models.CouponRecord.objects.filter(account=request.user,status=0,id__in=使用的优惠券ID列表]).update(status=1,order=order_obj) # 4. 处理贝里交易 models.Account.objects.filter(id=reuqest.user.id).update(balance=F(balance)-balance) models.TransactionRecord.objects.create(amount=balance,balance=request.user.balance,transaction_type=1,content_object=order_obj)
点击立即支付要生成订单,订单根据前面支付是否等于0来判断支付状态是否要修改为已支付或者待支付;生成订单详情,循环课程信息,写入课程的原价,折后价,视频的有效期,即订单的有效期,对优惠券进行处理,把该用户已使用的优惠券状态改为已使用;:修改贝里,该账户的贝里减去使用的贝里,并更新贝里消费记录表,显示账户的消费金额,账户的余额等。
最后
if pay_type==1: 生成支付宝链接(自动生成自己的订单号),并返回给前端Vue
# ##################################### 支付宝的回调 ###################################### def callback(request,*args,**kwargs): models.Order.objects.filter(订单号).update(status=0)
项目源码