课程动态价格策略实现
动态价格策略设计
商城往往为了提高销量都会出现活动内容,商品因为参加了活动所以会产生价格的变动。
价格优惠活动类型名称: 限时免费, 限时折扣, 限时减免, 限时满减, 积分抵扣, 优惠券
针对单个商品的动态价格策略,公式:
限时免费 0
限时折扣 原价*0.8
限时减免 原价-减免价
针对单次下单的动态价格策略,公式:
限时满减 总价-(满减计算后换算价格)
积分抵扣 总价-(积分计算后换算价格) ->> 积分与现金换算比率
优惠券 总价-(优惠券计算后的优惠价格) ->> 优惠券
模型创建
新增4个课程优惠相关的4个模型,courses/models.py,代码:
from django.utils import timezone as datetime
class Activity(BaseModel):
start_time = models.DateTimeField(default=datetime.now, verbose_name="开始时间")
end_time = models.DateTimeField(default=datetime.now, verbose_name="结束时间")
description = RichTextUploadingField(blank=True, null=True, verbose_name="活动介绍")
remark = models.TextField(blank=True, null=True, verbose_name="备注信息")
class Meta:
db_table = "fg_activity"
verbose_name = "优惠活动"
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class DiscountType(BaseModel):
remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息")
class Meta:
db_table = "fg_discount_type"
verbose_name = "优惠类型"
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Discount(BaseModel):
discount_type = models.ForeignKey("DiscountType", on_delete=models.CASCADE, related_name='discount_list', db_constraint=False, verbose_name="优惠类型")
condition = models.IntegerField(blank=True, default=0, verbose_name="满足优惠的价格条件", help_text="设置享受优惠的价格条件,如果不填或0则没有优惠门槛")
sale = models.TextField(verbose_name="优惠公式", help_text="""
0表示免费;<br>
*号开头表示折扣价,例如填写*0.82,则表示八二折;<br>
-号开头表示减免价, 例如填写-100,则表示减免100;<br>""")
class Meta:
db_table = "fg_discount"
verbose_name = "优惠公式"
verbose_name_plural = verbose_name
def __str__(self):
return "价格优惠:%s,优惠条件:%s,优惠公式: %s" % (self.discount_type.name, self.condition, self.sale)
class CourseActivityPrice(BaseModel):
activity = models.ForeignKey("Activity", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="活动")
course = models.ForeignKey("Course", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="课程")
discount = models.ForeignKey("Discount", on_delete=models.CASCADE, related_name='price_list', db_constraint=False, verbose_name="优惠")
class Meta:
db_table = "fg_course_activity_price"
verbose_name = "课程参与活动的价格表"
verbose_name_plural = verbose_name
def __str__(self):
return "活动:%s-课程:%s-优惠公式:%s" % (self.activity.name, self.course.name, self.discount.sale)
执行数据迁移
cd luffycityapi
python manage.py makemigrations
python manage.py migrate
提交代码版本
# 合并前面的course分支到master
git checkout master
git merge feature/course
cd /home/moluo/Desktop/luffycity/
git add .
git commit -m "feature: 动态价格策略模型的创建"
git push
git checkout -b feature/discount
admin站点配置活动相关模型管理器
courses/admin.py
,代码:
from .models import Activity, DiscountType, Discount, CourseActivityPrice
class ActivityModelAdmin(admin.ModelAdmin):
"""优惠活动的模型管理器"""
list_display = ["id", "name", "start_time", "end_time", "remark"]
admin.site.register(Activity, ActivityModelAdmin)
class DiscountTypeModelAdmin(admin.ModelAdmin):
"""优惠类型的模型管理器"""
list_display = ["id", "name", "remark"]
admin.site.register(DiscountType, DiscountTypeModelAdmin)
class DiscountModelAdmin(admin.ModelAdmin):
"""优惠公式的模型管理器"""
list_display = ["id", "name","discount_type","condition","sale"]
admin.site.register(Discount, DiscountModelAdmin)
class CourseActivityPriceModelAdmin(admin.ModelAdmin):
"""课程活动价格的模型管理器"""
list_display = ["id", "activity", "course","discount"]
admin.site.register(CourseActivityPrice, CourseActivityPriceModelAdmin)
因为涉及到时间的转换计算,所以此处我们需要在settings/dev.py
中设置时区相关的配置信息。
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False # 关闭时区转换以后,django会默认使用TIME_ZONE作为时区。
添加测试数据
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>', '负责人:
组织:
外勤:');
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
提交代码版本
cd /home/moluo/Desktop/luffycity/
git add .
git commit -m "feature: admin站点配置活动相关的模型管理器并添加测试数据"
git push --set-upstream origin feature/discount
在课程模型中计算课程优惠信息
courses/models.py,代码:
class Course(BaseModel):
course_type = (
(0, '付费购买'),
(1, '会员专享'),
(2, '学位课程'),
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
# course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_cover = StdImageField(variations={
'thumb_1080x608': (1080, 608), # 高清图
'thumb_540x304': (540, 304), # 中等比例,
'thumb_108x61': (108, 61, True), # 小图(第三个参数表示保持图片质量),
}, max_length=255, delete_orphans=True, upload_to="course/cover", null=True, verbose_name="封面图片",blank=True)
course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍")
pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期")
period = models.IntegerField(default=7, verbose_name="建议学习周期(day)")
attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径")
attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接")
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
students = models.IntegerField(default=0, verbose_name="学习人数")
lessons = models.IntegerField(default=0, verbose_name="总课时数量")
pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量")
price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0)
recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目")
recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目")
direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向")
category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类")
teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师")
class Meta:
db_table = "fg_course_info"
verbose_name = "课程信息"
verbose_name_plural = verbose_name
def course_cover_small(self):
if self.course_cover:
return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_108x61.url}">')
return ""
course_cover_small.short_description = "封面图片(108x61)"
course_cover_small.allow_tags = True
course_cover_small.admin_order_field = "course_cover"
def course_cover_medium(self):
if self.course_cover:
return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_540x304.url}">')
return ""
course_cover_medium.short_description = "封面图片(540x304)"
course_cover_medium.allow_tags = True
course_cover_medium.admin_order_field = "course_cover"
def course_cover_large(self):
if self.course_cover:
return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_1080x608.url}">')
return ""
course_cover_large.short_description = "封面图片(1080x608)"
course_cover_large.allow_tags = True
course_cover_large.admin_order_field = "course_cover"
@property
def discount(self):
"""通过计算获取当前课程的折扣优惠相关的信息"""
# 获取折扣优惠相关的信息
now_time = datetime.now() # 活动__结束时间 > 当前时间 and 活动__开始时间 < 当前时间(29)
# 获取当前课程参与的最新活动记录
last_activity_log = self.price_list.filter(
activity__end_time__gt=now_time,
activity__start_time__lt=now_time
).order_by("-id").first()
type_text = "" # 优惠类型的默认值
price = -1 # 优惠价格
expire = 0 # 优惠剩余时间
if last_activity_log:
# 获取优惠类型的提示文本
type_text = last_activity_log.discount.discount_type.name
# 获取限时活动剩余时间戳[单位:s]
expire = last_activity_log.activity.end_time.timestamp() - now_time.timestamp()
# 判断当前课程的价格是否满足优惠条件
course_price = float(self.price)
condition_price = float(last_activity_log.discount.condition)
if course_price >= condition_price:
# 计算本次课程参与了优惠以后的价格
sale = last_activity_log.discount.sale
print(f"{type_text}-{sale}")
if sale == "0":
# 免费,则最终价格为0
price = 0
elif sale[0] == "*":
# 折扣
price = course_price * float(sale[1:])
elif sale[0] == "-":
# 减免
price = course_price - float(sale[1:])
price = float(f"{price:.2f}")
data = {}
if type_text:
data["type"] = type_text
if expire > 0:
data["expire"] = expire
if price != -1:
data["price"] = price
return data
def discount_json(self):
# 必须转成字符串才能保存到es中。所以该方法提供给es使用的。
return json.dumps(self.discount)
@property
def can_free_study(self):
"""是否允许试学"""
lesson_list = self.lesson_list.filter(is_delete=False, is_show=True, free_trail=True).order_by("orders").all()
return len(lesson_list) > 0
给Elasticsearch重建索引
cd /home/moluo/Desktop/luffycity/luffycityapi
python manage.py rebuild_index
客户端课程列表页展示课程优惠价格时增加免费的判断逻辑
views/Course.vue
,代码:
<p class="two clearfix">
<span class="price l red bold" v-if="course_info.discount.price>=0">¥{{parseFloat(course_info.discount.price).toFixed(2)}}</span>
<span class="price l red bold" v-else>¥{{parseFloat(course_info.price).toFixed(2)}}</span>
<span class="origin-price l delete-line" v-if="course_info.discount.price>=0">¥{{parseFloat(course_info.price).toFixed(2)}}</span>
<span class="add-shop-cart r"><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>
</p>
客户端课程详情页展示真实课程的价格
views/Info.vue
,代码:
<p class="course-price" v-if="course.info.discount.price >= 0">
<span>活动价</span>
<span class="discount">¥{{parseFloat(course.info.discount.price).toFixed(2)}}</span>
<span class="original">¥{{parseFloat(course.info.price).toFixed(2)}}</span>
</p>
提交代码版本
git add .
git commit -m "feature: 课程优惠活动的实现"
git push