课程动态价格策略实现

动态价格策略设计

商城往往为了提高销量都会出现活动内容,商品因为参加了活动所以会产生价格的变动。

  1. 价格优惠活动类型名称: 限时免费, 限时折扣, 限时减免, 限时满减, 积分抵扣, 优惠券
  2. 针对单个商品的动态价格策略,公式:
  3. 限时免费 0
  4. 限时折扣 原价*0.8
  5. 限时减免 原价-减免价
  6. 针对单次下单的动态价格策略,公式:
  7. 限时满减 总价-(满减计算后换算价格)
  8. 积分抵扣 总价-(积分计算后换算价格) ->> 积分与现金换算比率
  9. 优惠券 总价-(优惠券计算后的优惠价格) ->> 优惠券

模型创建

新增4个课程优惠相关的4个模型,courses/models.py,代码:

  1. from django.utils import timezone as datetime
  2. class Activity(BaseModel):
  3. start_time = models.DateTimeField(default=datetime.now, verbose_name="开始时间")
  4. end_time = models.DateTimeField(default=datetime.now, verbose_name="结束时间")
  5. description = RichTextUploadingField(blank=True, null=True, verbose_name="活动介绍")
  6. remark = models.TextField(blank=True, null=True, verbose_name="备注信息")
  7. class Meta:
  8. db_table = "fg_activity"
  9. verbose_name = "优惠活动"
  10. verbose_name_plural = verbose_name
  11. def __str__(self):
  12. return self.name
  13. class DiscountType(BaseModel):
  14. remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息")
  15. class Meta:
  16. db_table = "fg_discount_type"
  17. verbose_name = "优惠类型"
  18. verbose_name_plural = verbose_name
  19. def __str__(self):
  20. return self.name
  21. class Discount(BaseModel):
  22. discount_type = models.ForeignKey("DiscountType", on_delete=models.CASCADE, related_name='discount_list', db_constraint=False, verbose_name="优惠类型")
  23. condition = models.IntegerField(blank=True, default=0, verbose_name="满足优惠的价格条件", help_text="设置享受优惠的价格条件,如果不填或0则没有优惠门槛")
  24. sale = models.TextField(verbose_name="优惠公式", help_text="""
  25. 0表示免费;<br>
  26. *号开头表示折扣价,例如填写*0.82,则表示八二折;<br>
  27. -号开头表示减免价, 例如填写-100,则表示减免100;<br>""")
  28. class Meta:
  29. db_table = "fg_discount"
  30. verbose_name = "优惠公式"
  31. verbose_name_plural = verbose_name
  32. def __str__(self):
  33. return "价格优惠:%s,优惠条件:%s,优惠公式: %s" % (self.discount_type.name, self.condition, self.sale)
  34. class CourseActivityPrice(BaseModel):
  35. activity = models.ForeignKey("Activity", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="活动")
  36. course = models.ForeignKey("Course", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="课程")
  37. discount = models.ForeignKey("Discount", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="优惠")
  38. class Meta:
  39. db_table = "fg_course_activity_price"
  40. verbose_name = "课程参与活动的价格表"
  41. verbose_name_plural = verbose_name
  42. def __str__(self):
  43. return "活动:%s-课程:%s-优惠公式:%s" % (self.activity.name, self.course.name, self.discount.sale)

执行数据迁移

  1. cd luffycityapi
  2. python manage.py makemigrations
  3. python manage.py migrate

提交代码版本

  1. # 合并前面的course分支到master
  2. git checkout master
  3. git merge feature/course
  4. cd /home/moluo/Desktop/luffycity/
  5. git add .
  6. git commit -m "feature: 动态价格策略模型的创建"
  7. git push
  8. git checkout -b feature/discount

admin站点配置活动相关模型管理器

courses/admin.py,代码:

  1. from .models import Activity, DiscountType, Discount, CourseActivityPrice
  2. class ActivityModelAdmin(admin.ModelAdmin):
  3. """优惠活动的模型管理器"""
  4. list_display = ["id", "name", "start_time", "end_time", "remark"]
  5. admin.site.register(Activity, ActivityModelAdmin)
  6. class DiscountTypeModelAdmin(admin.ModelAdmin):
  7. """优惠类型的模型管理器"""
  8. list_display = ["id", "name", "remark"]
  9. admin.site.register(DiscountType, DiscountTypeModelAdmin)
  10. class DiscountModelAdmin(admin.ModelAdmin):
  11. """优惠公式的模型管理器"""
  12. list_display = ["id", "name","discount_type","condition","sale"]
  13. admin.site.register(Discount, DiscountModelAdmin)
  14. class CourseActivityPriceModelAdmin(admin.ModelAdmin):
  15. """课程活动价格的模型管理器"""
  16. list_display = ["id", "activity", "course","discount"]
  17. admin.site.register(CourseActivityPrice, CourseActivityPriceModelAdmin)

因为涉及到时间的转换计算,所以此处我们需要在settings/dev.py中设置时区相关的配置信息。

  1. LANGUAGE_CODE = 'zh-hans'
  2. TIME_ZONE = 'Asia/Shanghai'
  3. USE_I18N = True
  4. USE_L10N = True
  5. USE_TZ = False # 关闭时区转换以后,django会默认使用TIME_ZONE作为时区。

添加测试数据

  1. INSERT INTO luffycity.fg_activity (id, name, orders, is_show, is_deleted, created_time, updated_time, start_time, end_time, description, remark) VALUES (1, '路飞学城-5周年庆', 1, 1, 0, '2022-02-17 10:42:54.340893', '2022-02-17 10:42:54.340933', '2022-02-17 00:00:00', '2021-08-01 00:00:00', '<p>5周年庆,各种活动促销内容展示图片</p>', '负责人:
  2. 组织:
  3. 外勤:');
  4. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (1, '免费', 1, 1, 0, '2022-02-17 10:43:38.546870', '2022-02-17 10:43:38.546901', null);
  5. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (2, '折扣', 1, 1, 0, '2022-02-17 10:43:49.161997', '2022-02-17 11:19:58.799363', null);
  6. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (3, '减免', 1, 1, 0, '2022-02-17 10:44:05.712935', '2022-02-17 11:41:16.504340', null);
  7. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (4, '限时免费', 1, 1, 0, '2022-02-17 10:44:23.053845', '2022-02-17 10:44:23.053925', null);
  8. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (5, '限时折扣', 1, 1, 0, '2022-02-17 10:44:31.999352', '2022-02-17 10:44:31.999382', null);
  9. INSERT INTO luffycity.fg_discount_type (id, name, orders, is_show, is_deleted, created_time, updated_time, remark) VALUES (6, '限时减免', 1, 1, 0, '2022-02-17 10:44:39.100270', '2022-02-17 10:44:39.100305', null);
  10. INSERT INTO luffycity.fg_discount (id, name, orders, is_show, is_deleted, created_time, updated_time, `condition`, sale, discount_type_id) VALUES (1, '免费购买', 1, 1, 0, '2022-02-17 10:45:54.027034', '2022-02-17 10:45:54.027079', 0, '0', 4);
  11. INSERT INTO luffycity.fg_discount (id, name, orders, is_show, is_deleted, created_time, updated_time, `condition`, sale, discount_type_id) VALUES (2, '九折折扣', 1, 1, 0, '2022-02-17 10:47:12.855454', '2022-02-17 11:32:27.148655', 1, '*0.9', 2);
  12. INSERT INTO luffycity.fg_discount (id, name, orders, is_show, is_deleted, created_time, updated_time, `condition`, sale, discount_type_id) VALUES (3, '课程减免100', 1, 1, 0, '2022-02-17 11:40:44.499026', '2022-02-17 11:40:44.499060', 300, '-100', 3);
  13. INSERT INTO luffycity.fg_course_activity_price (id, name, orders, is_show, is_deleted, created_time, updated_time, activity_id, course_id, discount_id) VALUES (1, '九折-3天Typescript', 1, 1, 0, '2022-02-17 10:48:12.600755', '2022-02-17 10:48:12.600801', 1, 2, 2);
  14. INSERT INTO luffycity.fg_course_activity_price (id, name, orders, is_show, is_deleted, created_time, updated_time, activity_id, course_id, discount_id) VALUES (2, '免费送课', 1, 1, 0, '2022-02-17 11:36:34.192896', '2022-02-17 11:36:34.192941', 1, 1, 1);
  15. INSERT INTO luffycity.fg_course_activity_price (id, name, orders, is_show, is_deleted, created_time, updated_time, activity_id, course_id, discount_id) VALUES (3, '减免课程', 1, 1, 0, '2022-02-17 11:40:49.240245', '2022-02-17 11:40:49.240276', 1, 3, 3);

提交代码版本

  1. cd /home/moluo/Desktop/luffycity/
  2. git add .
  3. git commit -m "feature: admin站点配置活动相关的模型管理器并添加测试数据"
  4. git push --set-upstream origin feature/discount

在课程模型中计算课程优惠信息

courses/models.py,代码:

  1. class Course(BaseModel):
  2. course_type = (
  3. (0, '付费购买'),
  4. (1, '会员专享'),
  5. (2, '学位课程'),
  6. )
  7. level_choices = (
  8. (0, '初级'),
  9. (1, '中级'),
  10. (2, '高级'),
  11. )
  12. status_choices = (
  13. (0, '上线'),
  14. (1, '下线'),
  15. (2, '预上线'),
  16. )
  17. # course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True)
  18. course_cover = StdImageField(variations={
  19. 'thumb_1080x608': (1080, 608), # 高清图
  20. 'thumb_540x304': (540, 304), # 中等比例,
  21. 'thumb_108x61': (108, 61, True), # 小图(第三个参数表示保持图片质量),
  22. }, max_length=255, delete_orphans=True, upload_to="course/cover", null=True, verbose_name="封面图片",blank=True)
  23. course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True)
  24. course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
  25. level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
  26. description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍")
  27. pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期")
  28. period = models.IntegerField(default=7, verbose_name="建议学习周期(day)")
  29. attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径")
  30. attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接")
  31. status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
  32. students = models.IntegerField(default=0, verbose_name="学习人数")
  33. lessons = models.IntegerField(default=0, verbose_name="总课时数量")
  34. pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量")
  35. price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0)
  36. recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目")
  37. recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目")
  38. direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向")
  39. category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类")
  40. teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师")
  41. class Meta:
  42. db_table = "fg_course_info"
  43. verbose_name = "课程信息"
  44. verbose_name_plural = verbose_name
  45. def course_cover_small(self):
  46. if self.course_cover:
  47. return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_108x61.url}">')
  48. return ""
  49. course_cover_small.short_description = "封面图片(108x61)"
  50. course_cover_small.allow_tags = True
  51. course_cover_small.admin_order_field = "course_cover"
  52. def course_cover_medium(self):
  53. if self.course_cover:
  54. return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_540x304.url}">')
  55. return ""
  56. course_cover_medium.short_description = "封面图片(540x304)"
  57. course_cover_medium.allow_tags = True
  58. course_cover_medium.admin_order_field = "course_cover"
  59. def course_cover_large(self):
  60. if self.course_cover:
  61. return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_1080x608.url}">')
  62. return ""
  63. course_cover_large.short_description = "封面图片(1080x608)"
  64. course_cover_large.allow_tags = True
  65. course_cover_large.admin_order_field = "course_cover"
  66. @property
  67. def discount(self):
  68. """通过计算获取当前课程的折扣优惠相关的信息"""
  69. # 获取折扣优惠相关的信息
  70. now_time = datetime.now() # 活动__结束时间 > 当前时间 and 活动__开始时间 < 当前时间(29)
  71. # 获取当前课程参与的最新活动记录
  72. last_activity_log = self.price_list.filter(
  73. activity__end_time__gt=now_time,
  74. activity__start_time__lt=now_time
  75. ).order_by("-id").first()
  76. type_text = "" # 优惠类型的默认值
  77. price = -1 # 优惠价格
  78. expire = 0 # 优惠剩余时间
  79. if last_activity_log:
  80. # 获取优惠类型的提示文本
  81. type_text = last_activity_log.discount.discount_type.name
  82. # 获取限时活动剩余时间戳[单位:s]
  83. expire = last_activity_log.activity.end_time.timestamp() - now_time.timestamp()
  84. # 判断当前课程的价格是否满足优惠条件
  85. course_price = float(self.price)
  86. condition_price = float(last_activity_log.discount.condition)
  87. if course_price >= condition_price:
  88. # 计算本次课程参与了优惠以后的价格
  89. sale = last_activity_log.discount.sale
  90. print(f"{type_text}-{sale}")
  91. if sale == "0":
  92. # 免费,则最终价格为0
  93. price = 0
  94. elif sale[0] == "*":
  95. # 折扣
  96. price = course_price * float(sale[1:])
  97. elif sale[0] == "-":
  98. # 减免
  99. price = course_price - float(sale[1:])
  100. price = float(f"{price:.2f}")
  101. data = {}
  102. if type_text:
  103. data["type"] = type_text
  104. if expire > 0:
  105. data["expire"] = expire
  106. if price != -1:
  107. data["price"] = price
  108. return data
  109. def discount_json(self):
  110. # 必须转成字符串才能保存到es中。所以该方法提供给es使用的。
  111. return json.dumps(self.discount)
  112. @property
  113. def can_free_study(self):
  114. """是否允许试学"""
  115. lesson_list = self.lesson_list.filter(is_delete=False, is_show=True, free_trail=True).order_by("orders").all()
  116. return len(lesson_list) > 0

给Elasticsearch重建索引

  1. cd /home/moluo/Desktop/luffycity/luffycityapi
  2. python manage.py rebuild_index

客户端课程列表页展示课程优惠价格时增加免费的判断逻辑

views/Course.vue,代码:

  1. <p class="two clearfix">
  2. <span class="price l red bold" v-if="course_info.discount.price>=0">¥{{parseFloat(course_info.discount.price).toFixed(2)}}</span>
  3. <span class="price l red bold" v-else>¥{{parseFloat(course_info.price).toFixed(2)}}</span>
  4. <span class="origin-price l delete-line" v-if="course_info.discount.price>=0">¥{{parseFloat(course_info.price).toFixed(2)}}</span>
  5. <span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>
  6. </p>

客户端课程详情页展示真实课程的价格

views/Info.vue,代码:

  1. <p class="course-price" v-if="course.info.discount.price >= 0">
  2. <span>活动价</span>
  3. <span class="discount">¥{{parseFloat(course.info.discount.price).toFixed(2)}}</span>
  4. <span class="original">¥{{parseFloat(course.info.price).toFixed(2)}}</span>
  5. </p>

提交代码版本

  1. git add .
  2. git commit -m "feature: 课程优惠活动的实现"
  3. git push