day84:luffy:优惠活动策略&用户认证&购物车商品的勾选/结算
目录
1.课程列表页活动和真实价格计算
既然提到了活动,那与之对应的肯定是优惠策略、优惠活动等等。
所以我们要为优惠活动策略单独建立表结构
1.优惠活动策略的model表结构
class CourseDiscountType(BaseModel): """课程优惠类型""" name = models.CharField(max_length=32, verbose_name="优惠类型名称") remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息") class Meta: db_table = "ly_course_discount_type" verbose_name = "课程优惠类型" verbose_name_plural = "课程优惠类型" def __str__(self): return "%s" % (self.name) class CourseDiscount(BaseModel): """课程优惠模型""" discount_type = models.ForeignKey("CourseDiscountType", on_delete=models.CASCADE, related_name='coursediscounts', verbose_name="优惠类型") condition = models.IntegerField(blank=True, default=0, verbose_name="满足优惠的价格条件",help_text="设置参与优惠的价格门槛,表示商品必须在xx价格以上的时候才参与优惠活动,<br>如果不填,则不设置门槛") #因为有的课程不足100,你减免100,还亏钱了 sale = models.TextField(verbose_name="优惠公式",blank=True,null=True, help_text=""" 不填表示免费;<br> *号开头表示折扣价,例如*0.82表示八二折;<br> -号开头则表示减免,例如-20表示原价-20;<br> 如果需要表示满减,则需要使用 原价-优惠价格,例如表示课程价格大于100,优惠10;大于200,优惠25,格式如下:<br> 满100-10<br> 满200-25<br> """) class Meta: db_table = "ly_course_discount" verbose_name = "价格优惠策略" verbose_name_plural = "价格优惠策略" def __str__(self): return "价格优惠:%s,优惠条件:%s,优惠值:%s" % (self.discount_type.name, self.condition, self.sale) class Activity(BaseModel): """优惠活动""" name = models.CharField(max_length=150, verbose_name="活动名称") start_time = models.DateTimeField(verbose_name="优惠策略的开始时间") end_time = models.DateTimeField(verbose_name="优惠策略的结束时间") remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息") class Meta: db_table = "ly_activity" verbose_name="商品活动" verbose_name_plural="商品活动" def __str__(self): return self.name class CoursePriceDiscount(BaseModel): """课程与优惠策略的关系表""" course = models.ForeignKey("Course",on_delete=models.CASCADE, related_name="activeprices",verbose_name="课程") active = models.ForeignKey("Activity",on_delete=models.DO_NOTHING, related_name="activecourses",verbose_name="活动") discount = models.ForeignKey("CourseDiscount",on_delete=models.CASCADE,related_name="discountcourse",verbose_name="优惠折扣") class Meta: db_table = "ly_course_price_dicount" verbose_name="课程与优惠策略的关系表" verbose_name_plural="课程与优惠策略的关系表" def __str__(self): return "课程:%s,优惠活动: %s,开始时间:%s,结束时间:%s" % (self.course.name, self.active.name, self.active.start_time,self.active.end_time)
class Course: def activity(self): import datetime now = datetime.datetime.now() # 获取课程参加的活动名称 activity_list = self.activeprices.filter(is_show=True, is_deleted=False, active__start_time__lte=now, active__end_time__gte=now) return activity_list # 优惠类型名称 def discount_name(self): dis_name = '' a = self.activity() if a: discount_n_list = [] for i in a: # 获取课程的折扣类型名称 discount_n = i.discount.discount_type.name discount_n_list.append(discount_n) dis_name = discount_n_list[0] return dis_name
2.course/serializers.py
序列化器加入该字段
class CourseModelSerializer: field = [,discount_name]
class CourseDetailModelSerializer: field = [,discount_name]
3.drf测试:course/courses
3.课程列表页显示真实价格
1.dev.py
USE_TZ = False # 修改时区
2.course/models.py
class Course: def real_price(self): price = float(self.price) # 获取真实价格 r_price = price a = self.activity() # 获取课程对应的活动名称 if a: # 如果课程参加了活动 sale = a[0].discount.sale # 查看活动对应的优惠公式 condition_price = a[0].discount.condition # 查看活动对应的满足优惠的价格条件 # 限时免费 if not sale.strip(): r_price = 0 # 限时折扣 *0.5 elif '*' in sale.strip(): if price >= condition_price: _, d = sale.split('*') r_price = price * float(d) # 限时减免 -100 elif sale.strip().startswith('-'): if price >= condition_price: _, d = sale.split('-') r_price = price - float(d) # 满减 满100-15 elif '满' in sale: if price >= condition_price: # 只有价格满足优惠条件价格才能满减 l1 = sale.split('\r\n') dis_list = [] #10 50 25 for i in l1: a, b = i[1:].split('-') # 当商品价格(400) 满足100-200-300 应该选择满300那个优惠 if price >= float(a): dis_list.append(float(b)) max_dis = max(dis_list) # 取到最大的那个满减优惠 r_price = price - max_dis # 原价格-满减价格=真实价格 return r_price
3.course/serializers.py
class CourseModelSerializer: field = [,discount_name,real_price]
class CourseDetailModelSerializer: field = [,discount_name,real_price]
4.drf测试:course/courses
4.将优惠类型名称和真实价格显示到前端页面上
1.课程列表页前端
<!-- Course.vue --> <div class="pay-box"> <span class="discount-type" v-if="course.discount_name">{{course.discount_name}}</span> <span class="discount-price">¥{{course.real_price}}元</span> <span class="original-price" v-if="course.discount_name">原价:{{course.price}}元</span> <span class="buy-now">立即购买</span> </div>
2.课程详情页前端
<!-- Detail.vue --> <div class="sale-time"> <p class="sale-type">{{course_data.discount_name}}</p> <p class="expire">距离结束:仅剩 567 天 12小时 52分 <span class="second">32</span> 秒</p> </div> <p class="course-price"> <span>活动价</span> <span class="discount">¥{{course_data.real_price}}</span> <span class="original">¥{{course_data.price}}</span> </p>
5.课程列表页显示具体结束时间
1.course/models.py
class Course: def left_time(self): import datetime now = datetime.datetime.now().timestamp() # 获取当前时间戳 left_t = 0 a = self.activity() # 获取当前课程参加的活动 if a: # 如果当前课程有参加活动 end_time = a[0].active.end_time.timestamp() # 获取当前课程活动的结束时间 left_t = end_time - now # 剩余时间=结束时间-当前时间 return left_t
2.序列化器放入left_time
course/serializers.py
class CourseDetailModelSerializer: field = [,discount_name,real_price,left_time]
3.前端渲染距离结束时间
Detail.vue
<!-- html --> <div class="sale-time"> <p class="sale-type">{{course_data.discount_name}}</p> <p class="expire">距离结束:仅剩 {{course_data.left_time/60/60/24 | pInt}}天 {{course_data.left_time/60/60 % 24| pInt}}小时 {{course_data.left_time/60 % 60 | pInt}}分 <span class="second">{{course_data.left_time % 60 | pInt}}</span> 秒</p> </div>
在js部分需要设置一个定时器,让时间能够一直减1s
Detail.vue
get_course_data(){ this.$axios.get(`${this.$settings.Host}/course/detail/${this.course_id}/`) .then((res)=>{ //console.log(res.data); this.course_data = res.data; this.playerOptions.sources[0].src = res.data.course_video this.playerOptions.poster = res.data.course_img // 设置计时器 setInterval(()=>{ this.course_data.left_time--; },1000) }) },
补充:让页面可以显示出0x分0x秒的效果→过滤器
Detail.vue
<!-- Detail.vue--html --> <p class="expire">距离结束:仅剩 {{course_data.left_time/60/60/24 | pInt}}天
{{course_data.left_time/60/60 % 24| pInt}}小时
{{course_data.left_time/60 % 60 | pInt}}分
<span class="second">{{course_data.left_time % 60 | pInt}}</span> 秒</p>
// Detail.vue--js filters:{ pInt(val){ let a = parseInt(val); if (a < 10){ a = `0${a}`; } return a } }
2.添加购物车/查看购物车的用户认证
为视图添加IsAuthenticated用户认证
# cart/views.py class AddCartView: # 请求头必须携带着token permission_classes = [IsAuthenticated,] # 添加用户认证 def add: user_id = request.user.id # 获取真实的用户id ''' request = user_obj '''
drf接口: cart/add_cart 获取不到数据,因为加了IsAuthenticated认证
添加购物车时需要携带token 否则不能添加购物车
// Detail.vue 添加购物车 methods: { addCart(){ let token = localStorage.token || sessionStorage.token; if (token){ this.$axios.post(`${this.$settings.Host}/users/verify/`,{ token:token, }).then((res)=>{ this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ course_id:this.course_id, },{ // 向后端提交数据需要加上header项,将token也要提交过去 headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ this.$message.success(res.data.msg); console.log('>>>>>',this.$store) this.$store.commit('add_cart', res.data.cart_length) ; console.log(this.$store.state); }) }).catch((error)=>{ ...... }) } else { ...... }) } },
查看购物车时需要携带token 否则不能添加购物车
// Cart.vue 购物车页面 created() { let token = sessionStorage.token || localStorage.token; if (token){ this.$axios.get(`${this.$settings.Host}/cart/add_cart/`,{ // 查看购物车页面也要携带token 否则不能查看 headers:{ 'Authorization':'jwt ' + token } }) .then((res)=>{ this.cart_data_list = res.data.cart_data_list }) .catch((error)=>{ this.$message.error(error.response.data.msg); })
# cart/views.py class AddCartView: def cart_list(self, request): ...... cart_data_list.append({ ...... 'real_price': course_obj.real_price(), ...... }) ......
<!-- cartitem.vue --> <div class="cart_column column_4">¥{{cart.real_price}}</div>
# cart/views.py class AddCartView: def change_select(self, request): # 拿到课程id course_id = request.data.get('course_id') # 校验课程id合法性 try: models.Course.objects.get(id=course_id) except: return Response({'msg': '课程不存在,不要乱搞!'}, status=status.HTTP_400_BAD_REQUEST) # 拿到user_id user_id = request.user.id # 去redis数据库:cart conn = get_redis_connection('cart') # redis存数据-用集合存:用户id:勾选课程id conn.sadd('selected_cart_%s' % user_id, course_id) return Response({'msg':'勾选成功'}) def cancel_select(self, request): course_id = request.data.get('course_id') try: models.Course.objects.get(id=course_id) except: return Response({'msg': '课程不存在,不要乱搞!'}, status=status.HTTP_400_BAD_REQUEST) user_id = request.user.id conn = get_redis_connection('cart') # 1:{1,3} conn.srem('selected_cart_%s' % user_id, course_id) return Response({'msg': '恭喜你!少花钱了,但是你真的不学习了吗!'})
为两个函数配置路由
# cart/urls.py from django.urls import path,re_path from . import views urlpatterns = [ path('add_cart/', views.AddCartView.as_view({'post':'add',
'get':'cart_list',
'patch':'change_select',
'put':'cancel_select'})) # 不同的请求方法走不同函数 ]
<!-- 当用户点击前面的勾选框时,会改变selected的值 近而会被watch监听到 在监听中 无论是选中还是取消选中都会触发父级标签重新计算价格的动作(this.$emit) --> <el-checkbox class="my_el_checkbox" v-model="cart.selected"></el-checkbox>
// Cartitem.vue watch:{ 'cart.selected':function (){ // 添加选中 let token = localStorage.token || sessionStorage.token; if (this.cart.selected){ this.$axios.patch(`${this.$settings.Host}/cart/add_cart/`,{ course_id: this.cart.course_id, },{ headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ this.$message.success(res.data.msg); this.$emit('cal_t_p') // 触发cart组件计算商品总价格的方法 }).catch((error)=>{ this.$message.error(res.data.msg); }) } else { // 取消选中 this.$axios.put(`${this.$settings.Host}/cart/add_cart/`,{ course_id: this.cart.course_id, },{ headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ this.$message.success(res.data.msg); this.$emit('cal_t_p') // 触发cart组件计算商品总价格的方法 }).catch((error)=>{ this.$message.error(res.data.msg); }) } }, }
触发Cart组件(父组件)的计算商品总价格的方法
<!-- Cart.vue --> <div class="cart_course_list"> <CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value" @cal_t_p="cal_total_price"></CartItem> </div>
// Cart.vue methods:{ cal_total_price(){ let t_price = 0 this.cart_data_list.forEach((v,k)=>{ // v是值 k是索引 if (v.selected){ t_price += v.real_price } }) this.total_price = t_price } }
4.购物车列表显示-后端接口
# cart/views.py def cart_list(self, request): user_id = request.user.id conn = get_redis_connection('cart') conn.delete('selected_cart_%s' % user_id) ret = conn.hgetall('cart_%s' % user_id) # dict {b'1': b'0', b'2': b'0'} cart_data_list = [] try: for cid, eid in ret.items(): course_id = cid.decode('utf-8') # 取出用户购物车里的课程id(redis中存着呢) expire_id = eid.decode('utf-8') # 取出用户购物车里的有效期id(redis中存着呢) course_obj = models.Course.objects.get(id=course_id) # 根据课程id,通过ORM查询得到课程对象,在下面就可以通过课程对象.字段 取到对应课程的参数信息 cart_data_list.append({ 'course_id': course_obj.id, 'name': course_obj.name, 'course_img': constants.SERVER_ADDR + course_obj.course_img.url, 'price': course_obj.price, 'real_price': course_obj.real_price(), 'expire_id': expire_id, 'selected': False, # 默认没有勾选 }) except Exception: logger.error('获取购物车数据失败') return Response({'msg': '后台数据库出问题了,请联系管理员'}, status=status.HTTP_507_INSUFFICIENT_STORAGE) return Response({'msg': 'xxx', 'cart_data_list': cart_data_list})
BUG:勾选两个课程 刷新页面 redis中仍然存着两个课程id
# cart/views.py def cart_list(self, request): ...... conn = get_redis_connection('cart') # 用户刷新页面时,从redis中删除用户对应的课程id conn.delete('selected_cart_%s' % user_id) ret = conn.hgetall('cart_%s' % user_id) ......