day85:luffy:购物车根据有效期不同切换价格&购物车删除操作&价格结算&订单页面前戏

目录

1.购物车有效期切换

2.根据有效期不同切换价格

3.购物车删除操作

4.价格结算

5.订单页面-初始化

1.购物车有效期切换

1.关于有效期表结构的设计

1.course/models.py

class CourseExpire(BaseModel):
    """课程有效期模型"""
    # 后面可以在数据库把course和expire_time字段设置为联合索引
    course = models.ForeignKey("Course", related_name='course_expire', on_delete=models.CASCADE, verbose_name="课程名称")

    # 有效期限,天数
    expire_time = models.IntegerField(verbose_name="有效期", null=True, blank=True, help_text="有效期按天数计算")

    # 一个月有效等等
    expire_text = models.CharField(max_length=150, verbose_name="提示文本", null=True, blank=True)
    # 每个有效期的价格
    price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程价格", default=0)

    class Meta:
        db_table = "ly_course_expire"
        verbose_name = "课程有效期"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "课程:%s,有效期:%s,价格:%s" % (self.course, self.expire_text, self.price)

课程有效期表结构设计

在course表中的price字段加一个提示文本

price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0,help_text='如果填写的价格为0,那么表示当前课程在购买的时候,没有永久有效的期限。')

2.插入一些测试数据

INSERT INTO `ly_course_expire`
(`id`,`orders`,`is_show`,`is_deleted`,`created_time`,`updated_time`,`expire_time`,`expire_text`,`course_id`,`price`)
VALUES
(1,1,1,0,'2019-08-19 02:05:22.368823','2019-08-19 02:05:22.368855',30,'一个月有效',1,398.00),
(2,2,1,0,'2019-08-19 02:05:37.397205','2019-08-19 02:05:37.397233',60,'2个月有效',1,588.00),
(3,3,1,0,'2019-08-19 02:05:57.029411','2019-08-19 02:05:57.029440',180,'半年内有效',1,1000.00),
(4,4,1,0,'2019-08-19 02:07:29.066617','2019-08-19 02:08:29.156730',3,'3天内有效',3,0.88),
(5,3,1,0,'2019-08-19 02:07:46.120827','2019-08-19 02:08:18.652452',30,'1个月有效',3,188.00),
(6,3,1,0,'2019-08-19 02:07:59.876421','2019-08-19 02:07:59.876454',60,'2个月有效',3,298.00);

3.xadmin注册

course/adminx.py

from .models import CourseExpire
class CourseExpireModelAdmin(object):
    """商品有效期模型"""
    pass
xadmin.site.register(CourseExpire, CourseExpireModelAdmin)

2.课程有效期-后端接口

course/models.py

class Course:    
    # 获取课程有效期
    def get_expire(self):
        
        # 课程表和课程有效期表时一对多的关系 反向查询
        expire_list = self.course_expire.all() # 查询到当前课程所拥有的有效期种类
        data = []
        for expire in expire_list:
            data.append({
                'id':expire.id,
                'expire_text':expire.expire_text, # 有效期的那个文本 比如'三个月有效'
                'price':expire.price, # 有效期对应的价格
            })

        # 当价格为0时,没有永久有效这一项,其他的都有
        if self.price > 0:
            data.append({
                'id': 0,
                'expire_text': '永久有效',
                'price': self.price,
            })

        return data

cart/views.py

class AddCartView(ViewSet):

    def cart_list(self,request):
          
        try:
            ......
            cart_data_list.append({
               ......
                'expire_list':course_obj.get_expire(),
               ......
            })
        except Exception:
           ......
        return Response({'msg':'xxx','cart_data_list':cart_data_list})

3.课程有效期-前端

cartitem.vue

<div class="cart_column column_3">
    <el-select v-model="cart.expire_id" size="mini" placeholder="请选择购买有效期" class="my_el_select">
        <el-option :label="expire.expire_text" :value="expire.id" :key="expire.id" v-for="(expire,expire_index) in cart.expire_list"></el-option>
    </el-select>
</div>

2.根据有效期不同切换价格

1.有效期切换更改redis中数据

cart/views.py

class AddCartView:
        def change_expire(self,request):

            user_id = request.user.id
            course_id = request.data.get('course_id')
            expire_id = request.data.get('expire_id')
            
           # 检验课程id是否有效
            try:
                course_obj = models.Course.objects.get(id=course_id)
            except:
                return Response({'msg':'课程不存在'},status=status.HTTP_400_BAD_REQUEST)
            # 检验有效期id是否有效
            try:
                if expire_id > 0:
                 expire_object = models.CourseExpire.objects.get(id=expire_id)
            except:
                return Response({'msg':'课程有效期不存在'},status=status.HTTP_400_BAD_REQUEST)

            # 计算xx课程xx有效期的真实价格
            real_price = course_obj.real_price(expire_id)

            # 更改了新的有效期,要将更改之后的有效期存放到redis中
            conn = get_redis_connection('cart')
            conn.hset('cart_%s' % user_id, course_id, expire_id)

            return Response({'msg':'切换成功!', 'real_price': real_price})

cart/urls.py

urlpatterns = [
    ......
    path('expires/', views.AddCartView.as_view({'put':'change_expire'}))
]

2.有效期切换让页面价格变化

course/models.py

class Course:
        def real_price(self,expire_id=0): # 增加expire_id=0参数 让真实价格根据不同的有效期显示不同的价格
            price = float(self.price)
            '''
            1.如果不是永久有效(expire_id>0),那就从course_expire获取该有效期对应的价格
            2.如果是永久有效(expire_id=0),那么真实价格就等于它原来的价格
            '''
            if expire_id > 0:
                expire_obj = self.course_expire.get(id=expire_id)
                price = float(expire_obj.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)

                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
                            if price >= float(a):
                                dis_list.append(float(b))

                        max_dis = max(dis_list)
                        r_price = price - max_dis

            return r_price

cartitem.vue

// js
 watch:{
      ......
      // 当用户选择的课程有效期发生变化时(在前端下拉框选择了别的有效期)
      'cart.expire_id':function (){
        let token = localStorage.token || sessionStorage.token;

          this.$axios.put(`${this.$settings.Host}/cart/expires/`,{
            // 将课程id和课程对应的有效期id提交到后端
            course_id: this.cart.course_id,
            expire_id:this.cart.expire_id,
          },{
            headers:{
              'Authorization':'jwt ' + token
            }

      }
      ).then((res)=>{
        this.$message.success(res.data.msg);
              
        // 修改有效期成功,将真实价格返回给前端
        this.cart.real_price = res.data.real_price;
              
        // 修改有效期成功,触发cart父组件重新计算总价格的事件
        this.$emit('change_expire_handler',) 

          }).catch((error)=>{
            this.$message.error(error.response.data.msg)
          })
    }
},

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" @change_expire_handler="cal_total_price" @delete_course_handler="delete_c(index)"></CartItem>

</div>

3.页面刷新 应该重置有效期

cart/views.py

class AddCartView:
    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')
                
                # 当用户查看购物车页面时->默认显示永久有效
                conn.hset('cart_%s' % user_id, course_id, 0)
                expire_id = 0
                
                course_obj = models.Course.objects.get(id=course_id)

                cart_data_list.append({
                    'course_id':course_obj.id,
                    'name':course_obj.name,
                    'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
                    'price':course_obj.price,
                    'real_price':course_obj.real_price(),
                    'expire_id':expire_id,
                    'expire_list':course_obj.get_expire(),
                    '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})
    

3.购物车删除操作

cartitem.vue

<div class="cart_column column_4" id="delete" @click="delete_course">删除</div>
delete_course(){
        let token = localStorage.token || sessionStorage.token;

          this.$axios.delete(`${this.$settings.Host}/cart/add_cart/`,{
            params:{
              course_id: this.cart.course_id,  // request.query_params.get('course_id')
            },
            headers:{
              'Authorization':'jwt ' + token
            }
          })
        .then((res)=>{
          this.$message.success(res.data.msg);
              
          // 当用户要删除购物车中的某条课程记录时 执行Cart父组件的删除课程事件
          this.$emit('delete_course_handler')
        })
        .catch((error)=>{
          this.$message.error(error.response.data.msg);
        })
      }

1.后端redis删除

cart/urls.py

urlpatterns = [
    path('add_cart/', views.AddCartView.as_view(
        {'post':'add','get':'cart_list',
         'patch':'change_select','put':'cancel_select',
         'delete':'delete_course'
         })),
    path('expires/', views.AddCartView.as_view({'put':'change_expire'}))

]

cart/views.py

class AddCartView:
    def delete_course(self,request):
        user_id = request.user.id
        course_id = request.query_params.get('course_id')
        
        conn = get_redis_connection('cart')
        pipe = conn.pipeline()
        pipe.hdel('cart_%s' % user_id, course_id)
        pipe.srem('selected_cart_%s' % user_id, course_id)
        pipe.execute()

        return Response({'msg':'删除成功'})

2.前端同步实现删除效果

用户删除一条购物车数据 后端已经删除了 但是前端页面也要同步删除

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" @change_expire_handler="cal_total_price" @delete_course_handler="delete_c(index)"></CartItem>

</div>
delete_c(index){
        this.cart_data_list.splice(index,1)
        this.cal_total_price() // 删除之后 重新触发计算总价格的方法
      }

Cartitem.vue

delete_course(){
        let token = localStorage.token || sessionStorage.token;

          this.$axios.delete(`${this.$settings.Host}/cart/add_cart/`,{
            params:{
              course_id: this.cart.course_id, 
            },
            headers:{
              'Authorization':'jwt ' + token
            }
          })
        .then((res)=>{ // 删除时 触发Cart父组件的删除事件
          this.$message.success(res.data.msg);
          this.$emit('delete_course_handler')
        })
        .catch((error)=>{
          this.$message.error(error.response.data.msg);
        })
      }
  }

4.价格结算

1.价格结算页面-准备工作

1.结算初始页面

<!-- 结算页面初始界面 -->

2.配置路由

index.js

import Vue from 'vue'
import Order from "@/components/Order"

Vue.use(Router)
export default new Router({
  mode:'history',
  routes: [
    ......
    {
      path: '/order/',   
      component: Order
    },
  ]
})

3.点击去结算按钮 看到页面

cart.vue

<span class="goto_pay"><router-link to="/order/">去结算</router-link></span>          

2.价格结算页面-后端

cart/urls.py

urlpatterns = [
    ...
    path('expires/', views.AddCartView.as_view({'get':'show_pay_info'}))

]

cart/views.py

def show_pay_info(self,request):
    
    user_id = request.user.id
    
    conn = get_redis_connection('cart')
    
    # 获取用户购物车选中的所有课程id
    select_list = conn.smembers('selected_cart_%s' % user_id)
    data = []

    # 获取用户购物车的课程id:有效期id
    ret = conn.hgetall('cart_%s' % user_id)  # dict {b'1': b'0', b'2': b'0'}

    
    for cid, eid in ret.items():
        expire_id = int(eid.decode('utf-8'))
        if cid in select_list: # 如果课程是被勾选的

            course_id = int(cid.decode('utf-8'))
            # 获取这个'被勾选的课程'model对象
            course_obj = models.Course.objects.get(id=course_id)
           
            # 如果课程的有效期不是永久有效
            if expire_id > 0:
                expire_obj = models.CourseExpire.objects.get(id=expire_id)
                data.append({
                    'course_id':course_obj.id,
                    'name':course_obj.name,
                    'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
                    'real_price':course_obj.real_price(expire_id),
                    'expire_text':expire_obj.expire_text,
                })
                
            # 如果课程的有效期是永久有效
            else:
                data.append({
                    'course_id': course_obj.id,
                    'name': course_obj.name,
                    'course_img': contains.SERVER_ADDR + course_obj.course_img.url,
                    'real_price': course_obj.real_price(expire_id),
                    'expire_text': '永久有效',
                })


    return Response({'data':data})

3.价格结算页面-前端

order.vue

// js
methods: {
      get_order_data(){
        let token = localStorage.token || sessionStorage.token;
        this.$axios.get(`${this.$settings.Host}/cart/expires/`,{
          headers:{
              'Authorization':'jwt ' + token
            }
        }).then((res)=>{
          this.course_list = res.data.data;
        })
      },
<!-- html --> 
<div class="cart-item" v-for="(course,index) in course_list">
          <el-row>
             <el-col :span="2" class="checkbox">&nbsp;&nbsp;</el-col>
             <el-col :span="10" class="course-info">
               <img :src="course.course_img" alt="">
                <span>{{course.name}}</span>
             </el-col>
             <el-col :span="8"><span>{{course.expire_text}}</span></el-col>
             <el-col :span="4" class="course-price">¥{{course.real_price}}</el-col>
           </el-row>
        </div>

4.切换支付方式-微信/支付宝

其实就是几张图片 点击显示图片而已

Order.vue

<el-col :span="8">
    <span class="alipay"  v-if="pay_type===1"><img src="../../static/img/alipay2.png"  alt=""></span>
    <span class="alipay" @click="pay_type=1"  v-else><img src="../../static/img/alipay.png" alt=""></span>
    <span class="alipay wechat" v-if="pay_type===2"><img src="../../static/img/wechat2.png" alt="" ></span>
    <span class="alipay wechat"  @click="pay_type=2" v-else><img src="../../static/img/wechat.png" alt=""></span>

</el-col>

5.订单页面-初始化

order/models.py

from django.db import models
# Create your models here.
from lyapi.utils.models import BaseModel
from users.models import User
from course.models import Course
class Order(BaseModel):
"""订单模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(0, '支付宝'),
(1, '微信支付'),
)
order_title = models.CharField(max_length=150,verbose_name="订单标题")
total_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="订单总价", default=0)
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="实付金额", default=0)
order_number = models.CharField(max_length=64,verbose_name="订单号")
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
credit = models.IntegerField(default=0, verbose_name="使用的积分数量")
coupon = models.IntegerField(null=True, verbose_name="用户优惠券ID")
order_desc = models.TextField(max_length=500, verbose_name="订单描述",null=True,blank=True)
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING,verbose_name="下单用户")
class Meta:
db_table="ly_order"
verbose_name= "订单记录"
verbose_name_plural= "订单记录"
def __str__(self):
return "%s,总价: %s,实付: %s" % (self.order_title, self.total_price, self.real_price)
class OrderDetail(BaseModel):
"""
订单详情
"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单ID")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程ID")
expire = models.IntegerField(default='0', verbose_name="有效期周期",help_text="0表示永久有效")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
discount_name = models.CharField(max_length=120,default="",verbose_name="优惠类型")
class Meta:
db_table="ly_order_detail"
verbose_name= "订单详情"
verbose_name_plural= "订单详情"
def __str__(self):
return "%s" % (self.course.name)

订单表结构设计

order/adminx.py

import xadmin
from .models import Order
class OrderModelAdmin(object):
"""订单模型管理类"""
pass
xadmin.site.register(Order, OrderModelAdmin)
from .models import OrderDetail
class OrderDetailModelAdmin(object):
"""订单详情模型管理类"""
pass
xadmin.site.register(OrderDetail, OrderDetailModelAdmin)

order/__init__.py

default_app_config = "order.apps.OrderConfig"

order/app.py

from django.apps import AppConfig
class OrderConfig(AppConfig):
name = 'order'
verbose_name = '订单管理'

order/urls.py

from django.urls import path,re_path
from . import views
urlpatterns = [
path('add_money/',views.OrderView.as_view(),)
]

order/views.py

from django.shortcuts import render
from rest_framework.generics import CreateAPIView
# Create your views here.
from . import models
from .serializers import OrderModelSerializer
from rest_framework.permissions import IsAuthenticated
class OrderView(CreateAPIView):
queryset = models.Order.objects.filter(is_deleted=False,is_show=True)
serializer_class = OrderModelSerializer
permission_classes = [IsAuthenticated, ]

order/serializers.py

import datetime
from rest_framework import serializers
from . import models
from django_redis import get_redis_connection
from course.models import Course
from course.models import CourseExpire
from django.db import transaction
class OrderModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Order
fields = ['id', 'order_number', 'pay_type', 'coupon', 'credit']
'''
1.用户在订单页面需要提交过来的数据:
支付类型:微信/支付宝
优惠券
积分
2.用户提交数据成功后,前端页面需要返回过来的数据:
id
订单号
'''
extra_kwargs = {
'id':{'read_only':True},
'order_number':{'read_only':True},
'pay_type':{'write_only':True},
'coupon':{'write_only':True},
'credit':{'write_only':True},
}
def validate(self, attrs):
# 获取用户使用的支付方式
pay_type = int(attrs.get('pay_type',0))  #
        
# 验证用户使用的支付方式是否是支付宝或微信中的一种
if pay_type not in [i[0] for i in models.Order.pay_choices]:
raise serializers.ValidationError('支付方式不对!')
# todo 优惠券校验,看看是否过期了等等
# todo 积分上限校验
return attrs
def create(self, validated_data):
try:
# 生成订单号  [日期,用户id,自增数据]
current_time = datetime.datetime.now()
now = current_time.strftime('%Y%m%d%H%M%S')
user_id = self.context['request'].user.id
conn = get_redis_connection('cart')
num = conn.incr('num')
# 生成一个唯一的订单号
order_number = now + "%06d" % user_id + "%06d" % num
with transaction.atomic():  # 添加事务
# 生成订单
order_obj = models.Order.objects.create(**{
'order_title': '31期订单',
'total_price': 0,
'real_price': 0,
'order_number': order_number,
'order_status': 0,
'pay_type': validated_data.get('pay_type', 0),
'credit': 0,
'coupon': 0,
'order_desc': '女朋友',
'pay_time': current_time,
'user_id': user_id,
# 'user':user_obj,
                })
select_list = conn.smembers('selected_cart_%s' % user_id)
ret = conn.hgetall('cart_%s' % user_id)  # dict {b'1': b'0', b'2': b'0'}
for cid, eid in ret.items():
expire_id = int(eid.decode('utf-8'))
if cid in select_list:
course_id = int(cid.decode('utf-8'))
course_obj = Course.objects.get(id=course_id)
if expire_id > 0:
expire_text = CourseExpire.objects.get(id=expire_id).expire_text
# 生成订单详情
models.OrderDetail.objects.create(**{
'order': order_obj,
'course': course_obj,
'expire': expire_id,
'price': course_obj.price,
'real_price': course_obj.real_price(expire_id),
'discount_name': course_obj.discount_name(),
})
# print('xxxxx')
except Exception:
raise models.Order.DoesNotExist
return order_obj