day86:luffy:前端發送請求生成訂單&結算頁面優惠劵的實現

目錄

1.前端發送請求生成訂單

  1.前端點擊支付按鈕生成訂單

  2.結算成功之後應該清除結算頁面的數據

  3.後端計算結算頁面總原價格和總的真實價格並存到資料庫訂單表中

2.優惠劵

  1.準備工作

  2.前端展示優惠券資訊-初始化

  3.優惠券-前端獲取優惠券數據+後端介面

  4.結算頁面計算真實總價格

  5.優惠券是否真的能夠使用+優惠劵前端計算

    1.優惠劵的選中效果

    2.不可點擊的不應該具有點擊效果

    3.優惠券箭頭收縮之後 取消優惠券的選中狀態

    4.當選中的優惠劵發生變化時 重新計算總價

  6.優惠劵後端對優惠劵進行校驗

1.前端發送請求生成訂單

1.前端點擊支付按鈕生成訂單

昨日講到了後端如何生成訂單(order/add_money)[傳送門:後端生成訂單的介面]  但是前端的請求還沒有發,現在來做一下前端發送請求來生成訂單。

點擊支付按鈕,觸發生成訂單事件,生成一個訂單

order.vue

<!-- html -->
<!-- 給支付按鈕綁定一個事件 該事件向後端發起請求來生成訂單 -->
<el-col :span="4" class="cart-pay"><span @click="payhander">支付</span></el-col>
// js
// 生成訂單
payhander(){
    let token = localStorage.token || sessionStorage.token;
    this.$axios.post(`${this.$settings.Host}/order/add_money/`,{
        
        // 生成訂單需要提交的支付類型、優惠券、積分
        "pay_type":this.pay_type,
        "coupon":this.current_coupon,
        "credit":0

    },{
        headers:{
            'Authorization':'jwt ' + token
        }
    }).then((res)=>{
        this.$message.success('訂單已經生成,馬上跳轉支付頁面')
    }).catch((error)=>{
        this.$message.error(error.response.data.msg);
    })



}

2.結算成功之後應該清除結算頁面的數據

結算成功之後應該清除結算介面的數據,如果用戶還想購買課程的話,應該去購物車去再次選中自己想要購買的商品再購買,然後再重新生成訂單。

所以應該redis中刪除用戶選中的課程id,也就是selected_cart的數據

order/serializers.py

# serializers.py
    def create(self, validated_data):
        
        # 結算成功之後,再清除
        conn = get_redis_connection('cart')
        conn.delete('selected_cart_%s' % user_id)
        
        return order_obj

3.後端計算結算頁面總原價格和總的真實價格並存到資料庫訂單表中

order/serializers.py

def create(self, validated_data):
    try:
        # 生成訂單號  [日期,用戶id,自增數據]

        total_price = 0  # 總原價
        total_real_price = 0  # 總真實價格

        with transaction.atomic():  # 添加事務
            # 生成訂單,保存到資料庫中
           ......
            # 生成訂單詳情
           ......
            # 計算所有課程的總原價
            total_price += course_obj.price

            # 計算所有課程的總真實價格
            total_real_price += course_obj.real_price(expire_id)

            # 將訂單總原價和總真實價格存到資料庫表中
            order_obj.total_price = total_price
            order_obj.real_price = total_real_price
            order_obj.save()

            except Exception:
                raise models.Order.DoesNotExist

            return order_obj

2.優惠劵

1.準備工作

1.創建coupon應用,並配置INSTALLAPP

python3 ../../manage.py startapp coupon

2.coupon/models.py

from django.db import models
from lyapi.utils.models import BaseModel
from users.models import User
from datetime import timedelta

# Create your models here.
class Coupon(BaseModel):
    """優惠券"""
    coupon_choices = (
        (0, '折扣優惠'),
        (1, '減免優惠')
    )
    name = models.CharField(max_length=32, verbose_name="優惠券標題")
    coupon_type = models.SmallIntegerField(choices=coupon_choices, default=0, verbose_name="優惠券類型")
    timer = models.IntegerField(verbose_name="優惠券有效期", default=7, help_text="默認當前優惠券7天有效,如果設置值為-1則表示當前優惠券永久有效")
    condition = models.IntegerField(blank=True, default=0, verbose_name="滿足使用優惠券的價格條件,如果設置值為0,則表示沒有任何條件")
    sale = models.TextField(verbose_name="優惠公式", help_text="""
        *號開頭表示折扣價,例如*0.82表示八二折;<br>
        -號開頭表示減免價,例如-10表示在總價基礎上減免10元<br>    
        """)

    class Meta:
        db_table = "ly_coupon"
        verbose_name="優惠券"
        verbose_name_plural="優惠券"

    def __str__(self):
        return "%s" % (self.name)


class UserCoupon(BaseModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="coupons", verbose_name="用戶")
    coupon = models.ForeignKey(Coupon, on_delete=models.CASCADE, related_name="users", verbose_name="優惠券")
    start_time = models.DateTimeField(verbose_name="優惠策略的開始時間")
    is_use = models.BooleanField(default=False,verbose_name="優惠券是否使用過")

    class Meta:
        db_table = "ly_user_coupon"
        verbose_name = "用戶的優惠券"
        verbose_name_plural = "用戶的優惠券"

    def __str__(self):
        return "優惠券:%s,用戶:%s" % (self.coupon.name, self.user.username)


    @property
    def end_time(self):
        s_time = self.start_time
        timer = self.coupon.timer  #天數

        return s_time + timedelta(days=timer)

優惠劵表結構設計

3.資料庫遷移指令

python3 ../../manage.py makemigrations
python3 ../../manage.py migrate

4.adminx註冊

coupon/adminx.py

import xadmin
from .models import Coupon
class CouponModelAdmin(object):
    """優惠券模型管理類"""
    list_display = ["name","coupon_type","timer"]
xadmin.site.register(Coupon, CouponModelAdmin)


from .models import UserCoupon
class UserCouponModelAdmin(object):
    """我的優惠券模型管理類"""
    list_display = ["user","coupon","start_time","is_use"]

xadmin.site.register(UserCoupon, UserCouponModelAdmin)

5.插入一些數據

INSERT INTO `ly_coupon` VALUES (1,1,1,0,'2019-08-21 15:59:04.568037','2019-08-21 15:59:04.568061','十元優惠券',1,30,10,'-10'),(2,2,1,0,'2019-08-21 15:59:33.764807','2019-08-21 15:59:33.764830','五十元優惠券',1,30,50,'-50'),(3,3,1,0,'2019-08-21 16:00:10.090100','2019-08-21 16:00:10.090126','9折優惠券',2,7,0,'*0.9');


INSERT INTO `ly_user_coupon` VALUES
(1,1,1,0,'2019-08-21 16:00:40.823977','2019-08-23 19:23:58.117600','2019-08-21 01:00:00.000000',1,3,1),
(2,2,1,0,'2019-08-21 16:00:49.868597','2019-08-22 09:37:46.010037','2019-10-01 01:00:00.000000',0,2,1),
(3,3,1,0,'2019-08-21 16:01:09.051862','2019-08-23 19:31:02.605253','2019-08-21 01:01:00.000000',1,1,1),
(4,5,1,0,'2019-08-22 08:48:56.406671','2019-08-22 08:48:56.406694','2019-08-22 17:48:00.000000',0,2,1);

2.前端展示優惠券資訊-初始化

<!-- html -->
<div class="discount">
    <div id="accordion">
        <div class="coupon-box">
            <div class="icon-box">
                <span class="select-coupon">使用優惠劵:</span>
                <a class="select-icon unselect" :class="use_coupon?'is_selected':''" @click="use_coupon=!use_coupon"><img class="sign is_show_select" src="../../static/img/12.png" alt=""></a>
                <span class="coupon-num">有0張可用</span>
            </div>
            <p class="sum-price-wrap">商品總金額:<span class="sum-price">0.00元</span></p>
        </div>
        <div id="collapseOne" v-if="use_coupon">
            <ul class="coupon-list"  v-if="coupon_list.length>0">
                <li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">
                    <p class="coupon-name">8.5折優惠券</p>
                    <p class="coupon-condition">滿0元可以使用</p>
                    <p class="coupon-time start_time">開始時間:</p>
                    <p class="coupon-time end_time">過期時間:</p>
                </li>

            </ul>
            <div class="no-coupon" v-if="coupon_list.length<1">
                <span class="no-coupon-tips">暫無可用優惠券</span>
            </div>
        </div>
    </div>
    <div class="credit-box">
        <label class="my_el_check_box"><el-checkbox class="my_el_checkbox" v-model="use_credit"></el-checkbox></label>
        <p class="discount-num1" v-if="!use_credit">使用我的貝里</p>
        <p class="discount-num2" v-else><span>總積分:100,已抵扣 ¥0.00,本次花費0積分</span></p>
    </div>
    <p class="sun-coupon-num">優惠券抵扣:<span>0.00元</span></p>
</div>

前端展示優惠劵資訊-HTML

/* css */
.coupon-box{
  text-align: left;
  padding-bottom: 22px;
  padding-left:30px;
  border-bottom: 1px solid #e8e8e8;
}
.coupon-box::after{
  content: "";
  display: block;
  clear: both;
}
.icon-box{
  float: left;
}
.icon-box .select-coupon{
  float: left;
  color: #666;
  font-size: 16px;
}
.icon-box::after{
  content:"";
  clear:both;
  display: block;
}
.select-icon{
  width: 20px;
  height: 20px;
  float: left;
}
.select-icon img{
  max-height:100%;
  max-width: 100%;
  margin-top: 2px;
  transform: rotate(-90deg);
  transition: transform .5s;
}
.is_show_select{
  transform: rotate(0deg)!important;
}
.coupon-num{
    height: 22px;
    line-height: 22px;
    padding: 0 5px;
    text-align: center;
    font-size: 12px;
    float: left;
    color: #fff;
    letter-spacing: .27px;
    background: #fa6240;
    border-radius: 2px;
    margin-left: 20px;
}
.sum-price-wrap{
    float: right;
    font-size: 16px;
    color: #4a4a4a;
    margin-right: 45px;
}
.sum-price-wrap .sum-price{
  font-size: 18px;
  color: #fa6240;
}

.no-coupon{
  text-align: center;
  width: 100%;
  padding: 50px 0px;
  align-items: center;
  justify-content: center; /* 文本兩端對其 */
  border-bottom: 1px solid rgb(232, 232, 232);
}
.no-coupon-tips{
  font-size: 16px;
  color: #9b9b9b;
}
.credit-box{
  height: 30px;
  margin-top: 40px;
  display: flex;
  align-items: center;
  justify-content: flex-end
}
.my_el_check_box{
  position: relative;
}
.my_el_checkbox{
  margin-right: 10px;
  width: 16px;
  height: 16px;
}
.discount{
    overflow: hidden;
}
.discount-num1{
  color: #9b9b9b;
  font-size: 16px;
  margin-right: 45px;
}
.discount-num2{
  margin-right: 45px;
  font-size: 16px;
  color: #4a4a4a;
}
.sun-coupon-num{
  margin-right: 45px;
  margin-bottom:43px;
  margin-top: 40px;
  font-size: 16px;
  color: #4a4a4a;
  display: inline-block;
  float: right;
}
.sun-coupon-num span{
  font-size: 18px;
  color: #fa6240;
}
.coupon-list{
  margin: 20px 0;
}
.coupon-list::after{
  display: block;
  content:"";
  clear: both;
}
.coupon-item{
  float: left;
  margin: 15px 8px;
  width: 180px;
  height: 100px;
  padding: 5px;
  background-color: #fa3030;
  cursor: pointer;
}
.coupon-list .active{
  background-color: #fa9000;
}
.coupon-list .disable{
  cursor: not-allowed;
  background-color: #fa6060;
}
.coupon-condition{
  font-size: 12px;
  text-align: center;
  color: #fff;
}
.coupon-name{
  color: #fff;
  font-size: 24px;
  text-align: center;
}
.coupon-time{
  text-align: left;
  color: #fff;
  font-size: 12px;
}
.unselect{
  margin-left: 0px;
  transform: rotate(-90deg);
}
.is_selected{
  transform: rotate(-1turn)!important;
}
  .coupon-item p{
    margin: 0;
    padding: 0;
  }

前端展示優惠劵資訊樣式-CSS

3.優惠券-前端獲取優惠券數據+後端介面

1.優惠劵後端介面

coupon/urls.py

# coupon/urls.py
from django.urls import path,re_path
from . import views


urlpatterns = [
    re_path(r'list/', views.CouponView.as_view(),),

]

lyapi/urls.py

# lyapi/urls.py

from xadmin.plugins import xversion
xversion.register_models()

urlpatterns = [
    ......
    path(r'coupon/',include('coupon.urls')),

]

coupon/views.py

# coupon/views.py
from django.shortcuts import render
from rest_framework.generics import ListAPIView
from . import models
from rest_framework.permissions import IsAuthenticated

from .serializers import UserCouponModelSerializer


class CouponView(ListAPIView):

    serializer_class = UserCouponModelSerializer
    permission_classes = [IsAuthenticated, ]
    def get_queryset(self):

        return models.UserCoupon.objects.filter(is_show=True,is_deleted=False,is_use=False, user_id=self.request.user.id)

coupon/serializers.py

# coupon/serializers.py
from rest_framework import serializers
from .models import Coupon, UserCoupon
class CouponModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = Coupon
        fields = ("name","coupon_type","timer","condition","sale")


class UserCouponModelSerializer(serializers.ModelSerializer):
    coupon = CouponModelSerializer()
    class Meta:
        model = UserCoupon
        fields = ("id","start_time","coupon","end_time")

2.間接計算優惠劵的結束時間

# coupon/models.py
class UserCoupon(BaseModel):
    ......A
    @property # 調用類中該方法時不需要加大括弧 將其視作為屬性
    def end_time(self):
        s_time = self.start_time
        timer = self.coupon.timer  #天數
        return s_time + timedelta(days=timer)

3.前端發送請求獲取優惠券數據

order.vue

// order.vue -js
get_user_coupon(){
    let token = localStorage.token || sessionStorage.token;
    this.$axios.get(`${this.$settings.Host}/coupon/list/`,{
        headers:{
            'Authorization':'jwt ' + token
        }
    }).then((res)=>{
        this.coupon_list = res.data; // 獲取到的優惠劵數據
    }).catch((error)=>{
        this.$message.error('優惠券獲取錯誤')
    })


      },
<!-- order.vue  html  -->
 <ul class="coupon-list"  v-if="coupon_list.length>0">
     <li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">
         <p class="coupon-name">{{coupon.coupon.name}}</p>
         <p class="coupon-condition">滿{{coupon.coupon.condition}}元可以使用</p>
         <p class="coupon-time start_time">開始時間:{{coupon.start_time.replace('T',' ')}}</p>
         <p class="coupon-time end_time">過期時間:{{coupon.end_time.replace('T',' ')}}</p>
     </li>

</ul>

4.結算頁面計算真實總價格

之前的結算頁面只差真實的總價格沒有計算了。現在通過後端計算真實總價格然後發送給前端。

1.後端計算好結算頁面的總價格和總真實價格

cart/views.py

# cart/views.py

# 結算頁面數據
def show_pay_info(self,request):
    user_id = request.user.id
    conn = get_redis_connection('cart')
    select_list = conn.smembers('selected_cart_%s' % user_id)
    data = []

    ret = conn.hgetall('cart_%s' % user_id)  # dict {b'1': b'0', b'2': b'0'}

    total_price = 0
    total_real_price = 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 = models.Course.objects.get(id=course_id)

            if expire_id > 0:
                expire_obj = models.CourseExpire.objects.get(id=expire_id)
                
                # 查詢到每個已勾選課程的真實價格
                course_real_price = course_obj.real_price(expire_id)
                
                # 計算出所有課程的總真實價格
                total_real_price += course_real_price
                
                data.append({
                    'course_id':course_obj.id,
                    'name':course_obj.name,
                    'course_img':contains.SERVER_ADDR + course_obj.course_img.url ,
                    
                    # 結算頁面的每條數據都顯示為每個課程的真實價格
                    'real_price':course_real_price,
                    
                    'expire_text':expire_obj.expire_text,
                })
                else:
                    course_real_price = course_obj.real_price(expire_id)
                    total_real_price += course_real_price
                    data.append({
                        'course_id': course_obj.id,
                        'name': course_obj.name,
                        'course_img': contains.SERVER_ADDR + course_obj.course_img.url,
                        'real_price': course_real_price,
                        'expire_text': '永久有效',
                    })


                    return Response({'data':data,'total_real_price':total_real_price})

2.前端發送請求 獲取結算頁面的數據、總價格、總真實價格

order.vue

// Order.vue
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;
        
        // 獲取到後端發送過來的總真實價格
        this.total_real_price = res.data.total_real_price;
        
        // 獲取到後端發送過來的總原價格
        this.total_price = res.data.total_real_price;
    })
},

5.優惠券是否真的能夠使用+優惠劵前端計算

1.優惠劵的選中效果

不在活動範圍內的優惠劵應該設置不可選中的效果

如果選中了當期優惠劵,則應該給優惠劵設置為選中的效果

order.vue

// order.vue  js
select_coupon(index,coupon_id){
    
        // 拿到你當前點擊的那條優惠劵數據
        let current_c = this.coupon_list[index]
        if (this.total_real_price < current_c.coupon.condition){
          return 'disable'
        }
    
        // '/1000'拿到時間戳
        let current_time = new Date() / 1000;
        let s_time = new Date(current_c.start_time) / 1000
        let e_time = new Date(current_c.end_time) / 1000

        // 如果優惠劵不在活動時間範圍內 則設置效果為不可選中
        if (current_time < s_time || current_time > e_time){
          return 'disable'
        }
        
        // 如果優惠劵是當前被選中的優惠劵 則設置效果為選中
        if (this.current_coupon === coupon_id){
          return 'active'
        }

        return ''
      },
<!-- order.vue  html -->
<li class="coupon-item" :class="select_coupon(index,coupon.id)" v-for="(coupon,index) in coupon_list" @click="change_coupon(index,coupon.id)">

2.不可點擊的不應該具有點擊效果

change_coupon(index,coupon_id){
    let current_c = this.coupon_list[index]
    
    // 如果優惠劵不符合條件 則優惠劵是不可點擊的
    if (this.total_real_price < current_c.coupon.condition){
        return false
    }
    let current_time = new Date() / 1000;
    let s_time = new Date(current_c.start_time) / 1000
    let e_time = new Date(current_c.end_time) / 1000
    
    // 如果優惠劵不符合條件 則優惠劵是不可點擊的
    if (current_time < s_time || current_time > e_time){
        return false
    }

    this.current_coupon = coupon_id;
    this.coupon_obj = current_c;

},

3.優惠券箭頭收縮之後 取消優惠券的選中狀態

order.vue

watch:{
    use_coupon(){
        // 如果點擊箭頭收縮 那麼就將優惠劵的選中狀態取消
        if (this.use_coupon === false){
            this.current_coupon = 0;

        }
    },

4.當選中的優惠劵發生變化時 重新計算總價

order.vue

// 當選中的優惠券發生變化時,重新計算總價
current_coupon(){
    this.cal_total_price();
}

 methods: {

      cal_total_price(){
        // 當用戶選中了某個優惠劵
        if (this.current_coupon !== 0){
          
          // 1.獲取用戶使用優惠劵前的真實價格
          let tt = this.total_real_price;
          
          // 2.獲取當前優惠劵的優惠公式
          let sales = this.coupon_obj.coupon.sale;
            
          // 3.取出優惠公式的數字部分
          let d = parseFloat(this.coupon_obj.coupon.sale.substr(1));
          if (sales[0] === '-'){
            tt = this.total_real_price - d
          }else if (sales[0] === '*'){
            tt = this.total_real_price * d
          }
          this.total_price = tt;

        }

      },

6.優惠劵後端對優惠劵進行校驗

前端雖然對優惠劵進行校驗,但是前端所顯示的都是虛假的,後端也要對優惠劵進行校驗。

class OrderModelSerializers:
    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('支付方式不對!')


        #  優惠券校驗,看看是否過期了等等
        coupon_id = attrs.get('coupon', 0)
        if coupon_id > 0:
            try:
                user_conpon_obj = UserCoupon.objects.get(id=coupon_id)
            except:
                raise serializers.ValidationError('訂單創建失敗,優惠券id不對')
            # 校驗優惠劵是否在活動時間範圍內
            now = datetime.datetime.now().timestamp()
            start_time = user_conpon_obj.start_time.timestamp()
            end_time = user_conpon_obj.end_time.timestamp()
            if now < start_time or now > end_time:
                raise serializers.ValidationError('訂單創建失敗,優惠券不在使用範圍內,滾犢子')


        # todo 積分上限校驗



        return attrs