在線教育平臺前端
軟件依賴:vue
WEB框架:Django(1.11.7)、Django REST frameworknode
前端框架:Vue(2.5.16)python
數據庫: MySql、redislinux
支付平臺:支付寶git
消息推送平臺:微信服務號,電子郵件github
主要功能以下:redis
容許學生註冊、登錄數據庫
瀏覽本在線教育平臺所提供的課程列表、課程介紹(課程大綱,)django
學生將課程加入購物車
完成購物後,進入結算中心,經過支付寶支付購買課程
完成課程購買後,分配導師
開始學習,觀看本站提供的教學視頻
經過答疑係統,提交學習中遇到的問題,與同窗討論或者導師答疑
完成章節學習後,提交做業
導師批改做業並打分
模塊學習完成後進行考覈
目標:
經過互聯技術,變革傳統教育方式,依賴強大的導師服務系統,提升在線教育的效率與成功率。
項目策略:
提供具備時間限制的教學服務
安排合理的模塊化學習
聘用高素質的導師,提供優質服務
導師對學員一對一在線考覈
學習週期中全程導師監督(跟進記錄)
提供高效的答疑解惑服務(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
支付需求起始於用戶點擊當即支付或者加入購物車
當用戶選擇須要購買的課程和價格策略後,有兩種方式購買課程:
將課程id和選擇的價格策略放到redis中,跳到去支付頁面,從redis中獲取購買課程的id和價格策略id,若是該用戶要使用優惠券和貝利,則選擇當前用戶所擁有而且未使用和沒過時的優惠券,獲得折後價格,點擊去支付完成支付,
前端須要提供的數據包括:所選課程的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)