首页 > 其他分享 >下单支付&订单处理--下单结算

下单支付&订单处理--下单结算

时间:2022-11-01 21:03:14浏览次数:31  
标签:box course -- pay price cart 订单 下单 coupons

下单结算

客户端展示下单结算界面展示

views/Cart.vue,代码:

<div class="li-3"><router-link to="/order" class="btn">去结算</router-link></div>
View Code

 

router/index.js,代码:

// 路由列表
const routes = [
    {
      meta:{
        title: "luffy2.0-首页",
        keepAlive: true
      },
      path:'/',         // uri访问地址
      component: ()=> import("../views/Home.vue")
    },{
      meta:{
        title: "登录",
        keepAlive: true
      },
      path: '/login',
      name: "Login",            // 路由名称
      component: ()=> import("../views/Login.vue"),         // uri绑定的组件页面
    },{
      meta:{
        title: "注册",
        keepAlive: true
      },
      path: '/register',
      name: "Register",            // 路由名称
      component: ()=> import("../views/Register.vue"),         // uri绑定的组件页面
    },{
      meta:{
        title: "项目课",
        keepAlive: true
      },
      path: '/project',
      name: "Course",            // 路由名称
      component: ()=> import("../views/Course.vue"),         // uri绑定的组件页面
    },{
      meta:{
        title: "项目课",
        keepAlive: true
      },
      path: '/project/:id',
      name: "Info",
      component: ()=> import("../views/Info.vue"),
    },{
      meta:{
        title: "购物车",
        keepAlive: true
      },
      path: '/cart',
      name: "Cart",
      component: ()=> import("../views/Cart.vue"),
    },{
      meta:{
        title: "确认下单",
        keepAlive: true
      },
      path: '/order',
      name: "Order",
      component: ()=> import("../views/Order.vue"),
    }
]
View Code

 

views/Order.vue,代码:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-main">
      <div class="cart-header">
        <div class="cart-header-warp">
          <div class="cart-title left">
            <h1 class="left">确认订单</h1>
          </div>
          <div class="right">
            <div class="">
              <span class="left"><router-link class="myorder-history" to="/cart">返回购物车</router-link></span>
            </div>
          </div>
        </div>
      </div>
      <div class="cart-body" id="cartBody">
        <div class="cart-body-title"><p class="item-1 l">课程信息</p></div>
        <div class="cart-body-table">
          <div class="item">
              <div class="item-2">
                  <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
                  <dl class="l has-package">
                    <dt>【实战课程】3天Typescript精修 </dt>
                    <p class="package-item">减免价</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price"><em>¥</em><span>998.00</span></p>
                      <p class="original-price"><em>¥</em><span>800.00</span></p>
                  </div>
              </div>
          </div>
          <div class="item">
              <div class="item-2">
                  <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
                  <dl class="l has-package">
                    <dt>【实战课程】3天Typescript精修 </dt>
                    <p class="package-item">减免价</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price"><em>¥</em><span>998.00</span></p>
                      <p class="original-price"><em>¥</em><span>800.00</span></p>
                  </div>
              </div>
          </div>
          <div class="item">
              <div class="item-2">
                  <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
                  <dl class="l has-package">
                    <dt>【实战课程】3天Typescript精修 </dt>
                    <p class="package-item">减免价</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price"><em>¥</em><span>998.00</span></p>
                      <p class="original-price"><em>¥</em><span>800.00</span></p>
                  </div>
              </div>
          </div>
          <div class="item">
              <div class="item-2">
                  <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
                  <dl class="l has-package">
                    <dt>【实战课程】3天Typescript精修 </dt>
                    <p class="package-item">减免价</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price"><em>¥</em><span>998.00</span></p>
                      <p class="original-price"><em>¥</em><span>800.00</span></p>
                  </div>
              </div>
          </div>
          <div class="item">
              <div class="item-2">
                  <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
                  <dl class="l has-package">
                    <dt>【实战课程】3天Typescript精修 </dt>
                    <p class="package-item">减免价</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price"><em>¥</em><span>998.00</span></p>
                      <p class="original-price"><em>¥</em><span>800.00</span></p>
                  </div>
              </div>
          </div>
        </div>
        <div class="coupons-box">
          <div class="coupon-title-box">
            <p class="coupon-title">
              使用优惠券/积分
                <span v-if="state.use_coupon" @click="state.use_coupon=!state.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></span>
                <span v-else @click="state.use_coupon=!state.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg></span>
<!--                <i :class="state.use_coupon?'el-icon-arrow-up':'el-icon-arrow-down'" @click="state.use_coupon=!state.use_coupon"></i>-->
            </p>
          </div>
          <transition name="el-zoom-in-top">
          <div class="coupon-del-box" v-if="state.use_coupon">
            <div class="coupon-switch-box">
              <div class="switch-btn ticket" :class="{'checked': state.discount_type===0}" @click="state.discount_type=0">优惠券 (4)<em><i class="imv2-check"></i></em></div>
              <div class="switch-btn code" :class="{'checked': state.discount_type===1}" @click="state.discount_type=1">积分<em><i class="imv2-check"></i></em></div>
            </div>
            <div class="coupon-content ticket" v-if="state.discount_type===0">
              <p class="no-coupons" v-if="state.coupon_list.length<1">暂无可用优惠券</p>
              <div class="coupons-box" v-else>
               <div class="content-box">
                <ul class="nouse-box">
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l select">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="use-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="overdue-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
               </div>
              </div>
            </div>
            <div class="coupon-content code" v-else>
                <div class="input-box">
                  <el-input-number placeholder="10积分=1元" v-model="state.credit" :step="1" :min="0" :max="1000"></el-input-number>
                  <a class="convert-btn">兑换</a>
                </div>
                <div class="converted-box">
                  <p>使用积分:<span class="code-num">200</span></p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                </div>
                <p class="error-msg">本次订单最多可以使用1000积分,您当前拥有200积分。(10积分=1元)</p>
                <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
              </div>
          </div>
          </transition>
        </div>
        <div class="pay-type">
          <p class="title">选择支付方式</p>
          <div class="list">
            <img :src="state.pay_type==0?'/src/assets/alipay2.png':'/src/assets/alipay1.png'" @click="state.pay_type=0" alt="支付宝">
            <img :src="state.pay_type==1?'/src/assets/wechat2.png':'/src/assets/wechat1.png'" @click="state.pay_type=1" alt="微信">
            <img :src="state.pay_type==2?'/src/assets/yue2.png':'/src/assets/yue1.png'"  @click="state.pay_type=2" alt="余额">
          </div>
        </div>
        <div class="pay-box" :class="{fixed:state.fixed}">
      <div class="row-bottom">
            <div class="row">
              <div class="goods-total-price-box">
                <p class="r rw price-num"><em>¥</em><span>1811.00</span></p>
                <p class="r price-text"><span>共<span>5</span>件商品,</span>商品总金额:</p>
              </div>
            </div>
            <div class="coupons-discount-box">
              <p class="r rw price-num">-<em>¥</em><span>60.00</span></p>
              <p class="r price-text">优惠券/积分抵扣:</p>
            </div>
            <div class="pay-price-box clearfix">
              <p class="r rw price"><em>¥</em><span id="js-pay-price">1751.00</span></p>
              <p class="r price-text">应付:</p>
            </div>
            <span class="r btn btn-red submit-btn">提交订单</span>
</div>
          <div class="pay-add-sign">
            <ul class="clearfix">
              <li>支持花呗</li>
              <li>可开发票</li>
              <li class="drawback">7天可退款</li>
            </ul>
          </div>
          </div>
      </div>
    </div>
    <Footer/>
  </div>
</template>
​
<script setup>
import {reactive,watch} from "vue"
import Header from "../components/Header.vue"
import Footer from "../components/Footer.vue"
import {useStore} from "vuex";
​
let store = useStore()
​
let state = reactive({
  course_list: [],     // 购物车中的商品课程列表
  total_price: 0,      // 勾选商品的总价格
  use_coupon: false,   // 用户是否使用优惠
  discount_type: 0,    // 0表示优惠券,1表示积分
  coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券
  credit: 0,           // 当前用户选择抵扣的积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
})
​
// 监听用户选择的支付方式
watch(
    ()=>state.pay_type,
    ()=>{
      console.log(state.pay_type)
    }
)
​
// 底部订单总价信息固定浮动效果
window.onscroll = ()=>{
  let cart_body_table = document.querySelector(".cart-body-table")
  let offsetY = window.scrollY
  let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  state.fixed = offsetY < maxY
}
</script>
​
<style scoped>
.cart-header {
height: 160px;
background-color: #e3e6e9;
background: url("/src/assets/cart-header-bg.jpeg") repeat-x;
background-size: 38%;
}
​
.cart-header .cart-header-warp {
width: 1500px;
height: 120px;
line-height: 120px;
margin-left: auto;
margin-right: auto;
font-size: 14px
}
​
.cart-header .cart-header-warp .myorder-history {
font-weight: 200
}
​
.cart-header .left {
float: left
}
​
.cart-header .right {
float: right
}
​
.cart-header .cart-title {
color: #4d555d;
font-weight: 200;
font-size: 14px
}
​
.cart-header .cart-title h1 {
font-size: 32px;
line-height: 115px;
margin-right: 25px;
color: #07111b;
font-weight: 200
}
​
.cart-header .cart-title span {
margin: 0 4px
}
​
.l {
  float: left;
}
.r {
  float: right;
}
.cart-body {
width: 1500px;
padding: 0 36px 32px;
background-color: #fff;
margin-top: -40px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 8px 16px 0 rgba(7,17,27, .1);
border-radius: 8px;
box-sizing: border-box
}
​
.cart-body .left {
float: left!important
}
​
.cart-body .right {
float: right!important
}
​
.cart-body .cart-body-title {
min-height: 88px;
line-height: 88px;
border-bottom: 1px solid #b7bbbf;
box-sizing: border-box
}
​
body {
background: #f8fafc
}
​
.cart-body .cart-body-title span {
font-size: 14px
}
​
.cart-body .cart-body-title .item-1>span,
.cart-body .cart-body-title .item-2>span,
.cart-body .cart-body-title .item-3>span{
display: inline-block;
font-size: 14px;
line-height: 24px;
color: #4d555d
}
​
.cart-body .cart-body-title .item-1>span {
color: #93999f
}
​
.cart-body .cart-body-title .item-2>span {
margin-left: 40px
}
​
.cart-body .item {
height: 88px;
padding: 24px 0;
background: #f3f5f7;
}
.cart-body .cart-body-table {
    padding-bottom: 36px;
    border-bottom: 1px solid #d9dde1;
}
.cart-body .item>div {
float: left
}
​
.cart-body .item .item-1 {
padding-top: 34px;
position: relative;
z-index: 1
}
​
.cart-body .item:last-child>.item-1::after {
display: none
}
​
.cart-body .item-1 {
width: 120px
}
​
.cart-body .item-1 i {
margin-left: 12px;
margin-right: 8px;
font-size: 24px
}
​
.cart-body .item-2 {
width: 1020px;
  position:relative;
}
.cart-body .item-2>span{
  line-height: 88px;
}
.cart-body .item-2 dl {
width: 464px;
margin-left: 24px;
padding-top: 12px
}
​
.cart-body .item-2 dl a {
display: block;
}
​
.cart-body .item-2 dl.has-package {
padding-top: 4px;
}
​
.cart-body .item-2 dl.has-package .package-item {
display: inline-block;
padding: 0 12px;
margin-top: 4px;
font-size: 12px;
color: rgba(240,20,20, .6);
line-height: 24px;
background: rgba(240,20,20, .08);
border-radius: 12px;
cursor: pointer
}
​
.cart-body .item-2 dl.has-package .package-item:hover {
color: #fff;
background: rgba(240,20,20, .2)
}
​
.cart-body .item-2 dt {
font-size: 16px;
color: #07111b;
line-height: 24px;
margin-bottom: 4px
}
​
.cart-body .item-2 .img-box {
display: block;
  margin-left: 42px;
}
.cart-body .item-2 .img-box img{
  height: 94px;
}
.cart-body .item-2 dd {
font-size: 12px;
color: #93999f;
line-height: 24px;
font-weight: 200
}
​
.cart-body .item-2 dd a {
display: inline-block;
margin-left: 12px;
color: rgba(240,20,20, .4)
}
​
.cart-body .item-2 dd a:hover {
color: #f01414
}
​
.cart-body .item-3 {
width: 280px;
margin-left: 48px;
  position: relative;
}
​
.cart-body .item-3 .price {
display: inline-block;
height: 46px;
width: 96px;
  padding-top: 24px;
  padding-bottom: 24px;
  color: #f01414;
}
.cart-body .item-3 .price em,
.cart-body .item-3 .price span{
  font-size: 18px;
}
.cart-body .item-3 .price .original-price em,
.cart-body .item-3 .price .original-price span{
  font-size: 15px;
  color: #aaa;
  text-decoration: line-through;
}
​
.cart-body .cart-body-bot li {
float: left
}
​
.cart-body .cart-body-bot .li-1 em,
.cart-body .cart-body-bot .li-3 em {
font-style: normal;
color: red
}
​
.cart-body .cart-body-bot .li-2 .price {
font-size: 16px;
color: #f01414;
line-height: 24px;
font-weight: 700
}
​
.coupons-box::after{
  display: block;
  content: "";
  overflow: hidden;
  clear: both;
}
.coupons-box .coupon-title-box {
margin: 27px 0 0 12px
}
​
.coupons-box .coupon-title-box .coupon-title {
color: #07111b;
font-size: 16px;
line-height: 34px
}
​
.coupons-box .coupon-title-box .coupon-title svg {
position: relative;
    width: 26px;
    height: 26px;
top: 5px;
margin-left: 12px;
font-size: 24px;
color: #999;
cursor: pointer
}
​
​
.coupons-box .coupon-del-box {
width: 100%;
padding-top: 24px;
box-sizing: border-box
}
​
.coupons-box .coupon-del-box .coupon-switch-box {
margin-bottom: 16px
}
​
.coupons-box .coupon-del-box .coupon-switch-box .switch-btn {
position: relative;
display: inline-block;
width: 138px;
height: 58px;
line-height: 20px;
border: 1px solid #d9dde1;
border-radius: 8px;
padding: 18px 0;
color: #1c1f21;
text-align: center;
font-size: 16px;
margin-right: 16px;
box-sizing: border-box;
cursor: pointer
}
​
.coupons-box .coupon-del-box .coupon-switch-box .switch-btn em {
display: none;
position: absolute;
bottom: 0;
right: 0;
width: 0;
height: 0;
line-height: 54px;
border-left-width: 20px;
border-left-style: solid;
border-left-color: transparent;
border-bottom-width: 20px;
border-bottom-style: solid;
border-bottom-color: #f01414
}
​
.coupons-box .coupon-del-box .coupon-switch-box .switch-btn em i {
color: #fff;
position: absolute;
bottom: -20px;
right: 0;
font-size: 12px
}
​
.coupons-box .coupon-del-box .coupon-switch-box .switch-btn.checked {
border: 2px solid #f01414
}
​
.coupons-box .coupon-del-box .coupon-switch-box .switch-btn.checked em {
display: block
}
​
.coupons-box .coupon-del-box .coupon-content {
position: relative;
background: #f3f5f7;
border-radius: 8px;
padding: 24px
}
​
.coupons-box .coupon-del-box .coupon-content:before {
content: "";
display: block;
position: absolute;
top: -7px;
left: 62px;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-bottom: 7px solid #f3f5f7
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li {
padding-top: 8px;
box-sizing: border-box;
width: 320px;
background-color: #fff6f0;
cursor: pointer;
margin: 12px
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li .more-del-box {
padding: 16px 22px 24px 22px;
width: 100%;
box-sizing: border-box;
background-repeat: no-repeat
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li .price-box {
height: 32px;
line-height: 32px
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li .price-box .price {
font-size: 30px;
margin-right: 4px
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li .price-box .price sub {
font-size: 24px;
letter-spacing: -5px
}
​
.coupons-box .coupon-del-box .coupon-content.ticket li .price-box .use-inst {
font-size: 12px;
margin-top: 5px;
}
​
.coupons-box .coupon-del-box .coupon-content.ticket .active .price,
.coupons-box .coupon-del-box .coupon-content.ticket .active .use-inst {
color: #fff
}
​
.coupons-box .coupon-del-box .coupon-content.ticket .active i {
position: absolute;
top: 12px;
right: 12px;
color: #fff;
font-size: 24px
}
​
.coupons-box .coupon-del-box .coupon-content.ticket .no-coupons {
font-size: 14px;
color: #4d555d;
line-height: 14px
}
​
.coupons-box .coupon-del-box .coupon-content.code {
padding-left: 38px
}
​
.coupons-box .coupon-del-box .coupon-content.code:before {
left: 216px
}
​
.coupons-box .coupon-del-box .coupon-content.code .input-box {
position: relative;
left: -12px;
margin-top: 12px
}
​
.coupons-box .coupon-del-box .coupon-content.code .input-box .convert-input {
background: #fff;
border: 1px solid #9199a1;
width: 356px;
height: 48px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
color: #07111b;
letter-spacing: 2px;
line-height: 24px;
padding: 12px 16px;
box-sizing: border-box;
vertical-align: middle
}
​
.coupons-box .coupon-del-box .coupon-content.code .input-box .convert-btn {
display: inline-block;
width: 124px;
height: 48px;
line-height: 22px;
font-size: 16px;
color: #fff;
padding: 12px;
background: #f01414;
border-radius: 8px;
margin-left: 24px;
box-sizing: border-box;
text-align: center;
cursor: pointer
}
​
.coupons-box .coupon-del-box .coupon-content.code .converted-box p {
line-height: 24px;
font-size: 16px;
color: #07111b;
  margin-top: 10px;
}
​
.coupons-box .coupon-del-box .coupon-content.code .converted-box .c_name,
.coupons-box .coupon-del-box .coupon-content.code .converted-box .code-num {
padding-left: 8px
}
​
.coupons-box .coupon-del-box .coupon-content.code .converted-box .cancel-btn {
background: #fff;
border: 1px solid #d9dde1;
line-height: 20px;
padding: 2px 12px;
text-align: center;
border-radius: 4px;
color: #f01414;
font-size: 14px;
margin-left: 16px;
cursor: pointer
}
​
.coupons-box .coupon-del-box .coupon-content.code .converted-box .course-title {
font-size: 14px;
color: #07111b;
font-weight: 600;
margin-top: 12px
}
​
.coupons-box .coupon-del-box .coupon-content.code .converted-box .course-title .discount-cash {
margin-left: 12px;
color: #f01414
}
​
.coupons-box .coupon-del-box .coupon-content.code .error-msg {
font-size: 14px;
color: #f01414;
margin-top: 8px;
line-height: 20px;
height: 20px
}
​
.coupons-box .coupon-del-box .coupon-content.code .tip {
font-size: 14px;
color: #93999f;
margin-top: 8px;
line-height: 20px
}
​
​
.coupons-box .content-box ul {
width: 100%
}
.coupons-box .content-box .nouse-box::after,
.coupons-box .content-box .overdue-box::after,
.coupons-box .content-box .use-box::after {
  display: block;
  content: "";
  overflow: hidden;
  clear: both;
}
.coupons-box .content-box .nouse-box li,
.coupons-box .content-box .overdue-box li,
.coupons-box .content-box .use-box li {
position: relative;
padding: 24px 32px;
margin-right: 16px;
margin-bottom: 16px;
width: 320px;
height: 144px;
border-radius: 8px;
box-sizing: border-box;
background-color: #fff;
box-shadow: 0 8px 16px 0 rgba(7,17,27, .2);
background-repeat: no-repeat;
background-size: 320px 144px;
}
.coupons-box .content-box .nouse-box li.select{
  background-color: orangered;
}
.coupons-box .content-box .nouse-box li .detail-box,
.coupons-box .content-box .overdue-box li .detail-box,
.coupons-box .content-box .use-box li .detail-box {
width: 100%;
height: 100%
}
​
.coupons-box .content-box .nouse-box li .detail-box .price-box,
.coupons-box .content-box .overdue-box li .detail-box .price-box,
.coupons-box .content-box .use-box li .detail-box .price-box {
margin-bottom: 8px;
height: 40px;
color: #93999f;
line-height: 40px;
font-weight: 700
}
​
.coupons-box .content-box .nouse-box li .detail-box .price-box .coupon-price,
.coupons-box .content-box .overdue-box li .detail-box .price-box .coupon-price,
.coupons-box .content-box .use-box li .detail-box .price-box .coupon-price {
margin-right: 12px;
font-size: 36px;
  margin-top: 5px;
}
​
.coupons-box .content-box .nouse-box li .detail-box .price-box .use-inst,
.coupons-box .content-box .overdue-box li .detail-box .price-box .use-inst,
.coupons-box .content-box .use-box li .detail-box .price-box .use-inst {
font-size: 14px
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box,
.coupons-box .content-box .use-box li .detail-box .use-detail-box {
font-size: 12px;
color: #93999f;
line-height: 24px
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box,
.coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box {
position: relative
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box i,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box i,
.coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box i {
position: relative;
top: 3px;
left: 0;
font-size: 16px;
color: #93999f;
line-height: 24px;
cursor: pointer
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a,
.coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a {
padding: 16px 0;
width: 100%;
display: block;
font-size: 12px;
color: #4d555d;
line-height: 20px;
border-bottom: 1px solid #d9dde1;
box-sizing: border-box
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover,
.coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover {
color: #07111b
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child,
.coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child,
.coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child {
border-bottom: none
}
​
.coupons-box .content-box li {
background-image: url(/src/assets/coupons_bg.png)
}
​
.coupons-box .content-box .nouse-box li .detail-box .price-box .coupon-price {
color: #f01414
}
​
.coupons-box .content-box .nouse-box li .detail-box .price-box .use-inst {
color: #f01414
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box {
color: #07111b
}
​
.coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box i {
color: #4d555d
}
​
.coupons-box .content-box .nouse-box li.wait-use {
background-image: url(/src/assets/coupon_start_bg.png)
}
​
.coupons-box .content-box .use-box li {
background-image: url(/src/assets/coupons_used_bg.png)
}
​
.coupons-box .content-box .use-box li.useing {
background-image: url(/src/assets/coupon_useing_bg.png)
}
​
.coupons-box .content-box .overdue-box li {
background-image: url(/src/assets/coupons_overdue.png)
}
​
.tip-box ol {
margin-top: 16px;
width: 100%;
list-style: decimal;
margin-left: 14px;
box-sizing: border-box
}
​
.tip-box ol li {
font-size: 12px
}
​
.pay-box {
margin-top: 36px;
position: relative
}
​
.pay-box::after,
.goods-total-price-box::after,
.package-discount-box::after,
.pay-price-box::after,
.coupons-discount-box::after{
  display: block;
  content: "";
  clear: both;
  overflow: hidden;
}
​
.pay-box .rw {
width: 140px;
box-sizing: border-box;
text-align: right
}
​
.pay-box .bargain-discount-box,.pay-box .coupons-discount-box,.pay-box .goods-total-price-box,.pay-box .package-discount-box,.pay-box .redpackage-discount-box,.pay-box .student-discount-box {
margin-bottom: 12px;
line-height: 26px
}
​
.pay-box .bargain-discount-box .price-num,.pay-box .coupons-discount-box .price-num,.pay-box .goods-total-price-box .price-num,.pay-box .package-discount-box .price-num,.pay-box .redpackage-discount-box .price-num,.pay-box .student-discount-box .price-num {
position: relative;
font-size: 14px;
color: #07111b
}
​
.pay-box .bargain-discount-box .price-text,.pay-box .coupons-discount-box .price-text,.pay-box .goods-total-price-box .price-text,.pay-box .package-discount-box .price-text,.pay-box .redpackage-discount-box .price-text,.pay-box .student-discount-box .price-text {
text-align: right;
font-size: 14px;
color: #07111b
}
​
.pay-box .bargain-discount-box .price-text span,.pay-box .coupons-discount-box .price-text span,.pay-box .goods-total-price-box .price-text span,.pay-box .package-discount-box .price-text span,.pay-box .redpackage-discount-box .price-text span,.pay-box .student-discount-box .price-text span {
margin-left: 4px;
margin-right: 4px
}
​
.pay-box .pay-add-sign {
text-align: right;
position: absolute;
top: -10px
}
​
.pay-box .pay-add-sign li {
float: left;
padding: 0 12px;
height: 26px;
line-height: 26px;
border: 1px solid #f01414;
border-radius: 18px;
font-size: 12px;
color: #f01414;
margin-right: 15px
}
​
.pay-box .pay-add-sign li.drawback {
position: relative
}
​
.pay-box .pay-add-sign li.drawback .imv2-ques {
position: absolute;
top: -4px;
right: -2px;
background: #fff;
color: #d7dbdf;
font-size: 14px;
display: inline-block;
width: 14px;
height: 14px;
cursor: pointer
}
​
.pay-box .pay-add-sign li.drawback .imv2-ques:hover {
color: #f20d0d
}
​
.pay-box .pay-add-sign a.checkbackbtn {
display: none;
color: #fff;
font-size: 12px;
text-align: center;
border-radius: 8px;
vertical-align: top;
position: absolute;
left: 100%;
top: -12px;
background: rgba(28,31,33,.25);
width: 100px;
height: 26px;
line-height: 26px;
margin-left: 8px
}
​
.pay-box .pay-add-sign a.checkbackbtn i.arrow {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-right: 5px solid;
border-bottom: 5px solid transparent;
position: absolute;
left: -5px;
top: 8px;
border-right-color: rgba(28,31,33,.25)
}
​
.pay-box .pay-price-box {
color: #07111b
}
​
.pay-box .pay-price-box .price {
position: relative;
color: #f01414;
font-size: 24px;
font-weight: 700;
  line-height: 36px;
  height: 36px;
}
.pay-box .pay-price-box .price-text{
  line-height: 36px;
  height: 36px;
}
.pay-box .pay-price-box .price span {
float: none;
font-weight: 700
}
​
.pay-box .pay-account {
font-size: 12px;
color: #93999f;
line-height: 24px;
margin-bottom: 20px;
margin-top: 15px
}
​
.pay-box .submit-btn {
padding: 0;
width: 140px;
height: 40px;
margin-top: 12px;
text-align: center;
font-size: 14px;
line-height: 40px;
border-radius: 24px
}
​
.pay-box .disabled {
background: #ccc;
cursor: not-allowed;
border: none
}
​
.pay-box .presale-wrap {
text-align: right
}
​
.pay-box .presale-wrap .submit-btn {
margin-top: 24px
}
​
.pay-box .presale-box {
display: inline-block;
font-size: 0;
text-align: left
}
​
.pay-box .presale-box .step {
width: 213px;
padding-bottom: 10px;
position: relative
}
​
.pay-box .presale-box .step .title {
font-size: 14px;
color: #07111b;
line-height: 26px
}
​
.pay-box .presale-box .step .title .price {
color: #93999f;
float: right
}
​
.pay-box .presale-box .step .title .price.active {
color: #f01414
}
​
.pay-box .presale-box .step .desc {
font-size: 12px;
color: #93999f;
line-height: 16px
}
​
.pay-box .presale-box .step:nth-child(3) .price {
color: #f01414;
font-size: 24px;
font-weight: 700
}
​
.pay-box .presale-box .step .step-line {
position: absolute;
top: 8px;
left: -16px;
width: 9px;
display: flex;
flex-direction: column;
align-items: center
}
​
.pay-box .presale-box .step .step-line .circle {
width: 9px;
height: 9px;
border-radius: 50%;
background: rgba(147,153,159,.3)
}
​
.pay-box .presale-box .step .step-line .circle.active {
background: #f01414
}
​
.pay-box .presale-box .step .step-line .line {
height: 43px;
border-left: 1px dashed rgba(147,153,159,.3)
}
​
.pay-box .presale-box .step .step-line .line.short {
height: 27px
}
​
.pay-box.fixed {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
line-height: 80px;
background-color: #fff;
z-index: 300;
box-shadow: 10px -2px 12px rgba(7,17,27,.2);
  padding-top: 10px;
}
​
.pay-box.fixed .row-bottom {
max-width: 1500px;
position: relative;
margin: 0 auto;
}
​
.pay-box.fixed .row-bottom .row {
float: left
}
​
.pay-box.fixed .row-bottom .bargain-discount-box,.pay-box.fixed .row-bottom .coupons-discount-box,.pay-box.fixed .row-bottom .js-total-hide,.pay-box.fixed .row-bottom .package-discount-box {
display: none
}
​
.pay-box.fixed .bargain-discount-box,.pay-box.fixed .coupons-discount-box,.pay-box.fixed .goods-total-price-box,.pay-box.fixed .package-discount-box,.pay-box.fixed .pay-add-sign,.pay-box.fixed .pay-price-box,.pay-box.fixed .redpackage-discount-box {
float: left;
margin-bottom: 0
}
​
.pay-box.fixed .coupons-discount-box,.pay-box.fixed .package-discount-box,.pay-box.fixed .redpackage-discount-box {
margin-left: 20px
}
​
.pay-box.fixed .goods-total-price-box {
width: auto
}
​
.pay-box.fixed .rw {
text-align: left;
width: auto
}
​
.pay-box.fixed .price,.pay-box.fixed .price-num,.pay-box.fixed .price-text {
line-height: 80px
}
​
.pay-box.fixed .pay-add-sign {
position: static!important;
margin-left: 20px
}
​
.pay-box.fixed .pay-add-sign li {
float: left;
padding: 0 12px;
height: 26px;
line-height: 26px;
border: 1px solid #f01414;
border-radius: 18px;
font-size: 12px;
color: #f01414;
margin: 27px 20px 27px 0
}
​
.pay-box.fixed .pay-price-box {
width: auto;
margin-left: 20px
}
​
.pay-box.fixed .submit-btn {
margin-top: 16px;
width: 148px;
height: 48px;
line-height: 48px;
font-size: 16px;
border-radius: 24px
}
​
.pay-box.fixed .presale-wrap {
float: left;
text-align: left
}
​
.pay-box.fixed .presale-wrap .presale-box {
height: 80px;
display: flex;
align-items: center
}
​
.pay-box.fixed .presale-wrap .presale-box .step {
padding-right: 38px;
padding-bottom: 0;
width: auto;
min-width: 118px;
height: 45px
}
​
.pay-box.fixed .presale-wrap .presale-box .step:nth-child(3) {
height: auto
}
​
.pay-box.fixed .presale-wrap .presale-box .step .title {
float: none;
background: #fff
}
​
.pay-box.fixed .presale-wrap .presale-box .step .title .price {
line-height: 26px;
float: none
}
​
.pay-box.fixed .presale-wrap .presale-box .step .step-line {
flex-direction: row;
width: 100%;
left: -14px
}
​
.pay-box.fixed .presale-wrap .presale-box .step .step-line .line {
border-left: none;
border-top: 1px dashed rgba(147,153,159,.3);
width: 30px;
height: 1px;
position: absolute;
right: 5px
}
​
.pay-box.fixed .presale-wrap .presale-box .step .step-line .circle:nth-child(3) {
position: absolute;
right: -10px
}
​
.btn {
  position: relative;
  display: inline-block;
  margin-bottom: 0;
  text-align: center;
  vertical-align: middle;
  touch-action: manipulation;
  text-decoration: none;
  box-sizing: border-box;
  background-image: none;
  -webkit-appearance: none;
  white-space: nowrap;
  outline: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  border-style: solid;
  border-width: 1px;
  cursor: pointer;
  transition: all .3s;
  color: #545c63;
  background-color: transparent;
  border-color: #9199a1;
  opacity: 1;
  padding: 7px 16px;
  font-size: 14px;
  line-height: 1.42857143;
  border-radius: 18px;
}
​
.btn-red {
  border-style: solid;
  border-width: 1px;
  cursor: pointer;
  -moz-transition: all .3s;
  transition: all .3s;
  color: #fff;
  background-color: #f20d0d;
  border-color: #f20d0d;
  opacity: 1;
}
.btn-red:hover {
  color: #fff;
  border-color: #c20a0a;
  background: #c20a0a;
  opacity: 1;
}
.pay-type {
  margin-top: 28px;
  margin-left: 12px;
}
.pay-type .title {
  margin-top: 28px;
}
.pay-type .list {
  padding-top: 20px;
}
​
.pay-type .list img {
  margin-right: 10px;
}
</style>
View Code

 

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 客户端展示下单结算页面"
git push

 

 

展示购物车勾选商品列表

服务端实现购物车勾选商品列表的api接口

cart/views,视图,代码:

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django_redis import get_redis_connection
from courses.models import Course
​
​
# .... 中间代码省略
​
class CartOrderAPIView(APIView):
    """购物车确认下单接口"""
    # 保证用户必须是登录状态才能调用当前视图
    permission_classes = [IsAuthenticated]
​
    def get(self,request):
        """获取勾选商品列表"""
        # 查询购物车中的商品课程ID列表
        user_id = request.user.id
        redis = get_redis_connection("cart")
        cart_hash = redis.hgetall(f"cart_{user_id}")
        """
        cart_hash = {
            # b'商品课程ID': b'勾选状态', 
            b'2': b'1', 
            b'4': b'1', 
            b'5': b'1'
        }
        """
        if len(cart_hash) < 1:
            return Response({"errmsg": "购物车没有任何商品。"}, status=status.HTTP_204_NO_CONTENT)
​
        # 把redis中的购物车勾选课程ID信息转换成普通列表
        cart_list = [int(course_id.decode()) for course_id, selected in cart_hash.items() if selected == b'1']
​
        course_list = Course.objects.filter(pk__in=cart_list, is_deleted=False, is_show=True).all()
​
        # 把course_list进行遍历,提取课程中的信息组成列表
        data = []
        for course in course_list:
            data.append({
                "id": course.id,
                "name": course.name,
                "course_cover": course.course_cover.url,
                "price": float(course.price),
                "discount": course.discount,
                "course_type": course.get_course_type_display(),
            })
​
        # 返回客户端
        return Response({"errmsg": "ok!", "cart": data})
​
View Code

 

cart/urls.py,路由,代码:

from django.urls import path
from . import views
urlpatterns = [
    path("", views.CartAPIView.as_view()),
    path("order/", views.CartOrderAPIView.as_view()),
]
​
 
View Code

 

客户端获取购物车勾选商品的数据

api/cart.js,代码:

import http from "../utils/http";
import {reactive, ref} from "vue"
​
const cart = reactive({
    // ... 中间代码省略
    select_course_list: [], // 购物车中被勾选的商品磕碜列表
    // ... 中间代码省略
    get_select_course(token){
        // 获取购物车中被勾选的商品列表
        return http.get("/cart/order/", {
            headers:{
                Authorization: "jwt " + token,
            }
        })
    }
})
​
export default cart;
View Code

 

api/order.js,代码:

import http from "../utils/http";
import {reactive} from "vue";
​
const order = reactive({
  total_price: 0,      // 勾选商品的总价格
  use_coupon: false,   // 用户是否使用优惠
  discount_type: 0,    // 0表示优惠券,1表示积分
  coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券下标,-1表示没有选择
  credit: 0,           // 当前用户选择抵扣的积分,0表示没有使用积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
})
​
export default order;
View Code

 

views/Order.vue,代码:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-main">
      <div class="cart-header">
        <div class="cart-header-warp">
          <div class="cart-title left">
            <h1 class="left">确认订单</h1>
          </div>
          <div class="right">
            <div class="">
              <span class="left"><router-link class="myorder-history" to="/cart">返回购物车</router-link></span>
            </div>
          </div>
        </div>
      </div>
      <div class="cart-body" id="cartBody">
        <div class="cart-body-title"><p class="item-1 l">课程信息</p></div>
        <div class="cart-body-table">
          <div class="item" v-for="course_info in cart.select_course_list">
              <div class="item-2">
                  <router-link :to="`/project/${course_info.id}`" class="img-box l"><img :src="course_info.course_cover"></router-link>
                  <dl class="l has-package">
                    <dt>【{{course_info.course_type}}】{{course_info.name}} </dt>
                    <p class="package-item" v-if="course_info.discount.type">{{course_info.discount.type}}</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price" v-if="course_info.discount.price>=0"><em>¥</em><span>{{course_info.discount.price.toFixed(2)}}</span></p>
                      <p :class="{'original-price': course_info.discount.price>=0}"><em>¥</em><span>{{course_info.price.toFixed(2)}}</span></p>
                  </div>
              </div>
          </div>
        </div>
        <div class="coupons-box">
          <div class="coupon-title-box">
            <p class="coupon-title">
              使用优惠券/积分
                <span v-if="order.use_coupon" @click="order.use_coupon=!order.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></span>
                <span v-else @click="order.use_coupon=!order.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg></span>
<!--                <i :class="order.use_coupon?'el-icon-arrow-up':'el-icon-arrow-down'" @click="order.use_coupon=!order.use_coupon"></i>-->
            </p>
          </div>
          <transition name="el-zoom-in-top">
          <div class="coupon-del-box" v-if="order.use_coupon">
            <div class="coupon-switch-box">
              <div class="switch-btn ticket" :class="{'checked': order.discount_type===0}" @click="order.discount_type=0">优惠券 (4)<em><i class="imv2-check"></i></em></div>
              <div class="switch-btn code" :class="{'checked': order.discount_type===1}" @click="order.discount_type=1">积分<em><i class="imv2-check"></i></em></div>
            </div>
            <div class="coupon-content ticket" v-if="order.discount_type===0">
              <p class="no-coupons" v-if="order.coupon_list.length<1">暂无可用优惠券</p>
              <div class="coupons-box" v-else>
               <div class="content-box">
                <ul class="nouse-box">
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l select">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="use-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="overdue-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
               </div>
              </div>
            </div>
            <div class="coupon-content code" v-else>
                <div class="input-box">
                  <el-input-number placeholder="10积分=1元" v-model="order.credit" :step="1" :min="0" :max="1000"></el-input-number>
                  <a class="convert-btn">兑换</a>
                </div>
                <div class="converted-box">
                  <p>使用积分:<span class="code-num">200</span></p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                </div>
                <p class="error-msg">本次订单最多可以使用1000积分,您当前拥有200积分。(10积分=1元)</p>
                <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
              </div>
          </div>
          </transition>
        </div>
        <div class="pay-type">
          <p class="title">选择支付方式</p>
          <div class="list">
            <img :src="order.pay_type==0?'/src/assets/alipay2.png':'/src/assets/alipay1.png'" @click="order.pay_type=0" alt="支付宝">
            <img :src="order.pay_type==1?'/src/assets/wechat2.png':'/src/assets/wechat1.png'" @click="order.pay_type=1" alt="微信">
            <img :src="order.pay_type==2?'/src/assets/yue2.png':'/src/assets/yue1.png'"  @click="order.pay_type=2" alt="余额">
          </div>
        </div>
        <div class="pay-box" :class="{fixed:order.fixed}">
      <div class="row-bottom">
            <div class="row">
              <div class="goods-total-price-box">
                <p class="r rw price-num"><em>¥</em><span>1811.00</span></p>
                <p class="r price-text"><span>共<span>5</span>件商品,</span>商品总金额:</p>
              </div>
            </div>
            <div class="coupons-discount-box">
              <p class="r rw price-num">-<em>¥</em><span>60.00</span></p>
              <p class="r price-text">优惠券/积分抵扣:</p>
            </div>
            <div class="pay-price-box clearfix">
              <p class="r rw price"><em>¥</em><span id="js-pay-price">1751.00</span></p>
              <p class="r price-text">应付:</p>
            </div>
            <span class="r btn btn-red submit-btn">提交订单</span>
</div>
          <div class="pay-add-sign">
            <ul class="clearfix">
              <li>支持花呗</li>
              <li>可开发票</li>
              <li class="drawback">7天可退款</li>
            </ul>
          </div>
          </div>
      </div>
    </div>
    <Footer/>
  </div>
</template>
<script setup>
import {reactive,watch} from "vue"
import Header from "../components/Header.vue"
import Footer from "../components/Footer.vue"
import {useStore} from "vuex";
import cart from "../api/cart"
import order from "../api/order";
​
// let store = useStore()
​
const get_select_course = ()=>{
    // 获取购物车中的勾选商品列表
    let token = sessionStorage.token || localStorage.token;
    cart.get_select_course(token).then(response=>{
        cart.select_course_list = response.data.cart
    })
}
​
get_select_course();
​
​
// 监听用户选择的支付方式
watch(
    ()=>order.pay_type,
    ()=>{
      console.log(order.pay_type)
    }
)
​
// 底部订单总价信息固定浮动效果
window.onscroll = ()=>{
  let cart_body_table = document.querySelector(".cart-body-table")
  let offsetY = window.scrollY
  let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  order.fixed = offsetY < maxY
}
</script>
View Code

 

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 确认下单页面中展示购物车勾选商品列表"
git push
 

 

订单生成

创建订单子应用

完成了勾选商品列表展示以后,因为优惠券或积分属于增值业务,所以可以先把优惠券功能和积分功能延后处理,先完成主流程中的订单生成功能。同时,为了方便以后项目的代码管理和维护,我们再次创建子应用orders来完成接下来的订单功能。

# 确认前面功能已经开发完整,review代码结束,向公司申请合并分支,开发合并分支
git checkout master
git merge feature/cart
# 推送到远程master
git push origin master
​
# 查看线上本地所有的分支列表,可以看到本地的feature/user分支已经删除,但是线上的依然存在。
git branch --all
git branch -d feature/cart
# 本地删除了分支以后,线上分支也要同步一下。
git push origin --delete feature/cart
# 因为属于一个较大功能的开发合并,往往项目中都会打一个标签
git tag v0.0.4
# 提交标签版本
git push --tag
# git push origin v0.0.4
​
# 后续的功能属于购物流程里面的订单生成部分了
git checkout -b feature/order
​
# 创建订单子应用
cd luffycityapi/luffycityapi/apps
python ../../manage.py startapp orders

 

注册子应用,settings/dev.py,代码:

INSTALLED_APPS = [
    # 子应用
    。。。
    
    'orders',
]
View Code

 

子路由,orders/urls.py,代码:

from django.urls import path
from . import views
urlpatterns = [
    
]
View Code

 

总路由,luffycityapi/urls.py,代码:

    path("orders/", include("orders.urls")),
 
View Code

 

订单模型

订单相关的模型分析:

订单基本信息:订单ID,支付方式,订单状态,支付时间,订单总价格,实付价格,订单标题,订单号,用户ID等等
订单项详情(订单与商品的关系):商品ID,商品原价、商品实价,优惠方式,订单ID等等
​
用户课程(用户与课程的关系):用户ID,课程ID,学习总时长等等
用户学习课程的进度跟踪记录(用户与课时的关系):用户ID,课时ID,课程ID,章节ID,学习进度(视频进度),学习时间等等
​
优惠券:优惠券标题、优惠券面额、优惠券优惠方式、优惠类型、领取方式(用户领取,系统发放)、起用时间、过期时间等等
用户的优惠券(用户与优惠券的关系):  用户ID,优惠券ID,领取时间等等。(我们采用redis来记录)
优惠券的使用记录(用户的优惠券与订单的关系):用户ID,优惠券ID、使用状态、订单ID等等。
积分流水:操作方式、积分面值、用户ID、订单ID等等。
余额流水:操作方式、货币面值、用户ID、订单ID等等。

 

为什么有订单号?

原因是支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。
所以订单号是支付平台那边强制要求在支付时提供给平台的。用于对账。

 

`orders/models.py,订单模型,代码:

from models import BaseModel,models
from users.models import User
from courses.models import Course
# Create your models here.
​
​
class Order(BaseModel):
    """订单基本信息模型"""
    status_choices = (
        # 模型对象.<字段名>                   获取元组的第一个成员
        # 模型对象.get_<字段名>_display()     获取元组的第二个成员
        (0, '未支付'),
        (1, '已支付'),
        (2, '已取消'),
        (3, '超时取消'),
    )
    pay_choices = (
        (0, '支付宝'),
        (1, '微信'),
        (2, '余额'),
    )
​
    total_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="订单总价")
    real_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="实付金额")
    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="支付方式")
    order_desc = models.TextField(null=True, blank=True, max_length=500, verbose_name="订单描述")
    pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付时间")
    user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")
​
    class Meta:
        db_table = "ly_order"
        verbose_name = "订单记录"
        verbose_name_plural = verbose_name
​
    def __str__(self):
        return "%s,总价: %s,实付: %s" % (self.name, self.total_price, self.real_price)
​
​
class OrderDetail(BaseModel):
    """
    订单详情
    """
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="订单")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False, verbose_name="课程")
    price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="课程原价")
    real_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="课程实价")
    discount_name = models.CharField(max_length=120,default="",verbose_name="优惠类型")
​
    class Meta:
        db_table = "ly_order_course"
        verbose_name = "订单详情"
        verbose_name_plural = verbose_name
​
    def __str__(self):
        return "%s" % self.course.name
​
View Code

 

数据迁移:

cd ../../
python manage.py makemigrations
python manage.py migrate

 

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:订单子应用创建以及订单信息和订单项模型的创建"
git push --set-upstream origin feature/order
 

 

 

把订单子应用相关的模型注册到admin管理站点

orders/admin.py,代码:

from django.contrib import admin
from .models import Order, OrderDetail
​
​
# class OrderDetailInLine(admin.StackedInline):
class OrderDetailInLine(admin.TabularInline):
    """订单项的内嵌类"""
    model = OrderDetail
    fields = ["course", "price", "real_price", "discount_name"]
    # readonly_fields = ["discount_name"]
​
​
class OrderModelAdmin(admin.ModelAdmin):
    """订单信息的模型管理器"""
    list_display = ["id","order_number","user","total_price","total_price","order_status"]
    inlines = [OrderDetailInLine, ]
​
​
admin.site.register(Order, OrderModelAdmin)
View Code
 

orders/apps.py,代码:

from django.apps import AppConfig
​
class OrdersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'orders'
    verbose_name = "订单管理"
    verbose_name_plural = verbose_name
View Code

 

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:把订单子应用相关的模型注册到admin管理站点"
git push
 

 

服务端提供创建订单的api接口

orders/views.py,代码:

from rest_framework.generics import CreateAPIView
from .models import Order
from .serializers import OrderModelSerializer
from rest_framework.permissions import IsAuthenticated
​
​
# Create your views here.
class OrderCreateAPIView(CreateAPIView):
    """创建订单"""
    permission_classes = [IsAuthenticated]
    queryset = Order.objects.all()
    serializer_class = OrderModelSerializer
​
View Code

 

子路由,orders/urls.py,代码:

from django.urls import path
from . import views
​
urlpatterns = [
    path("", views.OrderCreateAPIView.as_view()),
]
View Code

 

序列化器,orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from .models import Order, OrderDetail, Course
​
​
class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)
​
    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }
​
    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1
​
        # 创建订单记录
        order = Order.objects.create(
            name="购买课程",  # 订单标题
            user_id=user_id,  # 当前下单的用户ID
            # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
            order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
            pay_type=validated_data.get("pay_type"),  # 支付方式
        )
​
        # 记录本次下单的商品列表
        cart_hash = redis.hgetall(f"cart_{user_id}")
        if len(cart_hash) < 1:
            raise serializers.ValidationError(detail="购物车没有要下单的商品")
​
        # 提取购物车中所有勾选状态为b'1'的商品
        course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
​
        # 添加订单与课程的关系
        course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
        detail_list = []
        total_price = 0 # 本次订单的总价格
        real_price = 0  # 本次订单的实付总价
​
        for course in course_list:
            discount_price = float(course.discount.get("price", 0)) # 获取课程原价
            discount_name = course.discount.get("type", "")
            detail_list.append(OrderDetail(
                order=order,
                course=course,
                name=course.name,
                price=course.price,
                real_price=discount_price,
                discount_name=discount_name,
            ))
​
            # 统计订单的总价和实付总价
            total_price += float(course.price)
            real_price += discount_price if discount_price > 0 else float(course.price)
​
        # 一次性批量添加本次下单的商品记录
        OrderDetail.objects.bulk_create(detail_list)
​
        # 保存订单的总价格和实付价格
        order.total_price = total_price
        order.real_price = real_price
        order.save()
​
        # todo 支付链接地址[后面实现支付功能的时候,再做]
        order.pay_link = ""
        return order
​
View Code

 

生成订单时,在序列化器中要接收客户端用户的user_id

用户ID在序列化器中接收到视图中的数据,那么在序列化器初始化的时候,其实有3个参数可以填写:
   1. instance 模型对象,数据模型,
   2. data     字典,客户端提交数据,
   3. context  字典,额外参数[执行上下文],如果要自定义参数,可以直接通过字典格式声明,然后到context
   
   OrderModerSerializer(instance="模型对象",data="客户端数据", context={})
    
利用序列化器初始化时提供的第三个参数就可以调用到视图类的
   context的属性          描述                       序列化器中的调用代码
       request    本次客户端的请求对象            self.context["request"]
       format     本次服务器响应的数据格式         self.context["format"]
       view       调用当前序列化器的视图类          self.context["view"]
   
因此,我们要在序列化器中提取用户的id,代码如下:
   user_id = self.context["request"].user.id

 

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:服务端提供创建订单的API接口"
git push

 

 

上面我们完成了订单信息的添加,但是下单不是一个数据记录而已,而是多张表记录的同时添加操作。所以针对这种多个记录或者多张表连贯进行的操作,为了保证数据的完整性和一致性以及原子性,我们要使用数据库的事务(Transaction)来完成,当然我们这个项目中不需要使用到数据库原生的事务语句,而是使用django的ORM提供的事务模块即可。

事务(Transaction),是以功能或业务作为逻辑单位,把一条或多条SQL语句组成一个不可分割的操作序列来执行的数据库特性。
在完成一个整体功能时,操作到了多个表数据,或者同一个表的多条记录,如果要保证这些SQL语句操作作为一个整体保存到数据库中,那么可以使用事务(transaction),保证这些操作作为不可分割的整体,要么一起成功,要么一起失败。
​
事务具有4个特性(ACID),5个隔离等级
  
  四个特性:一致性,原子性,隔离性,持久性
  # 隔离性:两个事务的隔离性,隔离性的修改可以通过数据库的配置文件mysqld.cnf进行修改,默认mysql是属于可重复级别
  五个隔离级别(从高到低): 串行隔离,可重复读,已提交读,未提交读,没有隔离
    原子性(Atomicity)
    一致性(Consistency)
    隔离性(Isolation)[事务隔离级别->幻读,脏读, 不可重复读]
    持久性(Durability)
​
  在mysql中有专门的SQl语句来完成事务的操作,事务的代码操作一般有3个步骤:
     设置事务开始  begin;
     事务的处理[mysql:增删改]
         redis.sadd()
         事务的处理[mysql:增删改]
     设置事务的回滚或者提交 rollback / commit;  # 这个事务过程中,事务无法对mysql数据库以外的其他类型的数据库操作进行管理和回滚
mysql中底层的事务是如何实现事务的回滚操作:undo.log重做日志
在ORM框架一般都会实现了事务操作封装,所以我们可以直接使用ORM框架即可完成事务的操作
 

 

django框架本身就提供了2种事务操作的写法,主要都是通过 django.db.transaction模块完成的。

启用事务写法1:基于装饰器对函数或方法进行事务管理:

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    @transaction.atomic          # 开启事务,当函数/方法执行完成以后,自动提交事务
    def post(self,request):      # 不一定是视图方法,也可以是其他函数方法。
        ....  # 在整个函数或者方法中,进行的所有SQL数据写操作[增删改],都属于同一个事务操作

 

启用事务写法2,基于with上下文管理器进行事务管理:

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    def post(self,request):
        .... # 事务以外的,其他的SQL数据操作
        with transation.atomic(): # 开启事务,当with语句执行完成以后,自动提交事务
            # 数据库操作【DML增删改】
            
        .... # with语句以外的其他的SQL数据操作,无法被上面事务管理

 

在使用事务过程中, 有时候会出现异常,当出现异常时我们需要回滚事务。

from django.db import transaction
from rest_framework.generics import CreateAPIView
class OrderCreateAPIView(CreateAPIView):
    def post(self,request):
        ....
        with transaction.atomic():
            # 1、设置事务回滚的标记点【一个事物中可以设置多个回滚标记】
            sid1 = transaction.savepoint()
            try:
                .... # 增删改等数据库操作
                ....
            except:
                transaction.savepoint_rallback(sid1)
​
        .... # 数据库操作,注意,如果这里被执行,因为没有在with里面,所以是不会被上面的事务操作影响。

 

django的事务操作是支持嵌套事务的,但是mysql本身不支持嵌套事务。

from django.db import transaction
from rest_framework.generics import CreateAPIView
class OrderCreateAPIView(CreateAPIView):
    def post(self,request):
        ....
        with transaction.atomic():
            # 1、设置事务回滚的标记点【一个事物中可以设置多个回滚标记】
            sid1 = transaction.savepoint()
​
            try:
                .... # 增删改等数据库操作
                ....
                with transaction.atomic():
                    # 2. 设置回滚点
                    sid2 = transaction.savepoint()
                    try:
                        .... # 其他内部数据库处理
                        ....
                    except:
                        transaction.savepoint_rallback(sid2)
            except:
                transaction.savepoint_rallback(sid1)
            
        .... # 数据库操作,注意,如果这里被执行,因为没有在with里面,所以是不会被上面的事务操作影响。
 

 

使用Django的ORM提供的mysql事务操作保证下单过程中的数据原子性

orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from django.db import transaction
from .models import Order, OrderDetail, Course
import logging
​
logger = logging.getLogger("django")
​
​
class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)
​
    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }
​
    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1
​
        # 开启事务操作,保证下单过程中的所有数据库的原子性
        with transaction.atomic():
            # 设置事务的回滚点标记
            t1 = transaction.savepoint()
            try:
                # 创建订单记录
                order = Order.objects.create(
                    name="购买课程",  # 订单标题
                    user_id=user_id,  # 当前下单的用户ID
                    # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
                    order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
                    pay_type=validated_data.get("pay_type"),  # 支付方式
                )
​
                # 记录本次下单的商品列表
                cart_hash = redis.hgetall(f"cart_{user_id}")
                if len(cart_hash) < 1:
                    raise serializers.ValidationError(detail="购物车没有要下单的商品")
​
                # 提取购物车中所有勾选状态为b'1'的商品
                course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
​
                # 添加订单与课程的关系
                course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
                detail_list = []
                total_price = 0 # 本次订单的总价格
                real_price = 0  # 本次订单的实付总价
​
                for course in course_list:
                    discount_price = float(course.discount.get("price", 0)) # 获取课程原价
                    discount_name = course.discount.get("type", "")
                    detail_list.append(OrderDetail(
                        order=order,
                        course=course,
                        name=course.name,
                        price=course.price,
                        real_price=discount_price,
                        discount_name=discount_name,
                    ))
​
                    # 统计订单的总价和实付总价
                    total_price += float(course.price)
                    real_price += discount_price if discount_price > 0 else float(course.price)
​
                # 一次性批量添加本次下单的商品记录
                OrderDetail.objects.bulk_create(detail_list)
​
                # 保存订单的总价格和实付价格
                order.total_price = total_price
                order.real_price = real_price
                order.save()
​
                # todo 支付链接地址[后面实现支付功能的时候,再做]
                order.pay_link = ""
                return order
            except Exception as e:
                # 1. 记录日志
                logger.error(f"订单创建失败:{e}")
                # 2. 事务回滚
                transaction.savepoint_rollback(t1)
                # 3. 抛出异常,通知视图返回错误提示
                raise serializers.ValidationError(detail="订单创建失败!")
​
View Code

 

购物车中选中的商品被记录到了订单中,那么购物车中原来的勾选商品是否要删除?

如果不删除,那么订单中的商品与购物车中就重复了,所以要删除,购物车中只需要保留没有勾选过的商品。

orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from django.db import transaction
from .models import Order, OrderDetail, Course
import logging
​
logger = logging.getLogger("django")
​
​
class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)
​
    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }
​
    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1
​
        # 开启事务操作,保证下单过程中的所有数据库的原子性
        with transaction.atomic():
            # 设置事务的回滚点标记
            t1 = transaction.savepoint()
            try:
                # 创建订单记录
                order = Order.objects.create(
                    name="购买课程",  # 订单标题
                    user_id=user_id,  # 当前下单的用户ID
                    # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
                    order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
                    pay_type=validated_data.get("pay_type"),  # 支付方式
                )
​
                # 记录本次下单的商品列表
                cart_hash = redis.hgetall(f"cart_{user_id}")
                if len(cart_hash) < 1:
                    raise serializers.ValidationError(detail="购物车没有要下单的商品")
​
                # 提取购物车中所有勾选状态为b'1'的商品
                course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
​
                # 添加订单与课程的关系
                course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
                detail_list = []
                total_price = 0 # 本次订单的总价格
                real_price = 0  # 本次订单的实付总价
​
                for course in course_list:
                    discount_price = float(course.discount.get("price", 0)) # 获取课程原价
                    discount_name = course.discount.get("type", "")
                    detail_list.append(OrderDetail(
                        order=order,
                        course=course,
                        name=course.name,
                        price=course.price,
                        real_price=discount_price,
                        discount_name=discount_name,
                    ))
​
                    # 统计订单的总价和实付总价
                    total_price += float(course.price)
                    real_price += discount_price if discount_price > 0 else float(course.price)
​
                # 一次性批量添加本次下单的商品记录
                OrderDetail.objects.bulk_create(detail_list)
​
                # 保存订单的总价格和实付价格
                order.total_price = total_price
                order.real_price = real_price
                order.save()
​
                # todo 支付链接地址[后面实现支付功能的时候,再做]
                order.pay_link = ""
​
                # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
                cart = {key: value for key, value in cart_hash.items() if value == b'0'}
                pipe = redis.pipeline()
                pipe.multi()
                # 删除原来的购物车
                pipe.delete(f"cart_{user_id}")
                # 重新把未勾选的商品记录到购物车中
                pipe.hmset(f"cart_{user_id}", cart)  # hset 在新版本的redis中实际上hmset已经被废弃了,改用hset替代hmset
                pipe.execute()
​
                return order
            except Exception as e:
                # 1. 记录日志
                logger.error(f"订单创建失败:{e}")
                # 2. 事务回滚
                transaction.savepoint_rollback(t1)
                # 3. 抛出异常,通知视图返回错误提示
                raise serializers.ValidationError(detail="订单创建失败!")
View Code

 


提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:服务端基于事务保证订单生成操作的原子性"
git push
 

 

客户端请求生成订单

api/order.js,代码:

import http from "../utils/http";
import {reactive} from "vue";
​
const order = reactive({
  total_price: 0,      // 勾选商品的总价格
  use_coupon: false,   // 用户是否使用优惠
  discount_type: 0,    // 0表示优惠券,1表示积分
  coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券下标,-1表示没有选择
  credit: 0,           // 当前用户选择抵扣的积分,0表示没有使用积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
  create_order(token){
    // 生成订单
    return http.post("/orders/",{
        pay_type: this.pay_type
    },{
        headers:{
            Authorization: "jwt " + token,
        }
    })
  }
})
​
export default order;
View Code

 

views/Order.vue,代码:

<span class="r btn btn-red submit-btn" @click="commit_order">提交订单</span>
<script setup>
import {reactive,watch} from "vue"
import Header from "../components/Header.vue"
import Footer from "../components/Footer.vue"
import {useStore} from "vuex";
import cart from "../api/cart"
import order from "../api/order";
import {ElMessage} from "element-plus";
import router from "../router";
​
// let store = useStore()
​
const get_select_course = ()=>{
    // 获取购物车中的勾选商品列表
    let token = sessionStorage.token || localStorage.token;
    cart.get_select_course(token).then(response=>{
        cart.select_course_list = response.data.cart
        if(response.data.cart.length === 0){
          ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
          router.back();
        }
    }).catch(error=>{
    if(error?.response?.status===400){
      ElMessage.error("登录超时!请重新登录后再继续操作~");
    }
  })
}
​
get_select_course();
​
​
const commit_order = ()=>{
    // 生成订单
    let token = sessionStorage.token || localStorage.token;
    order.create_order(token).then(response=>{
    console.log(response.data.order_number)  // todo 订单号
    console.log(response.data.pay_link)      // todo 支付链接
    // 成功提示
    ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
    // 扣除掉被下单的商品数量,更新购物车中的商品数量
    store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  }).catch(error=>{
    if(error?.response?.status===400){
          ElMessage.success("登录超时!请重新登录后再继续操作~");
    }
  })
}
​
​
// 监听用户选择的支付方式
watch(
    ()=>order.pay_type,
    ()=>{
      console.log(order.pay_type)
    }
)
​
// 底部订单总价信息固定浮动效果
window.onscroll = ()=>{
  let cart_body_table = document.querySelector(".cart-body-table")
  let offsetY = window.scrollY
  let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  order.fixed = offsetY < maxY
}
</script>
View Code

 

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:客户端请求生成订单"
git push

 

 

标签:box,course,--,pay,price,cart,订单,下单,coupons
From: https://www.cnblogs.com/erhuoyuan/p/16849131.html

相关文章

  • Linux学习笔记(第一篇)零散命令
    ​​Linux学习笔记(第零篇)计算机基础​​Linux学习笔记(第零篇)Linux文件系统及ShellLinux学习笔记(第一篇)零散命令Linux学习笔记(第二篇)目录操作命令Linux学习笔记(第三篇)文件操......
  • Linux学习笔记(第二篇)目录操作命令
    ​​Linux学习笔记(第零篇)计算机基础​​Linux学习笔记(第零篇)Linux文件系统及ShellLinux学习笔记(第一篇)零散命令Linux学习笔记(第二篇)目录操作命令Linux学习笔记(第三篇)文件操......
  • Linux学习笔记(第四篇)文件内容操作命令+正则表达式+重定向和管道
    ​​Linux学习笔记(第零篇)计算机基础​​Linux学习笔记(第零篇)Linux文件系统及ShellLinux学习笔记(第一篇)零散命令Linux学习笔记(第二篇)目录操作命令Linux学习笔记(第三篇)文件操......
  • CSP 2022 游记
    高一老年人拉,还有最后一个月的OI生涯。初赛乱打,反正是过了(去杭州的路上在借py的手机打元,上一次打元还是中考回去时候,那次加特林技能一开狂暴5s秒杀Boss(。CSP前......
  • KVM环境SRIOV子卡同宿主机bridge网卡无法相互通信问题的解决办法
    一.SRIOV是啥。请百度。二.怎么配置。建议看intel官方文档. https://www.intel.com/content/www/us/en/developer/articles/technical/configure-sr-iov-network-virt......
  • Docker容器和宿主机文件互传
    #从容器里面拷文件到宿主机前:容器文件路径后:宿主机路径dockercp容器ID/容器名:容器文件路径宿主机文件路径#从宿主机拷文件到容器里面前:宿主机文件路径后:容......
  • 洛谷-P2015 二叉苹果树
    二叉苹果树树形dp设计状态:\(dp[u][i]\),表示以结点\(u\)为根的子树,保留\(i\)条边的最大苹果数状态转移:遍历每一个子节点\(v\)保留和\(v\)相连的边:\(dp[u][i]=......
  • 【Azure 应用服务】部署WAR包到App Service访问出现404错误的解决方式
    问题描述在Linux的AppService上,通过FTP把war文件和HTML静态文件上传到wwwroot目录下,静态文件访问成功,但是java应用中的请求都返回404错误 问题解决因为FTP上传文件只......
  • Linux学习笔记(第三篇)文件操作命令
    ​​Linux学习笔记(第零篇)计算机基础​​Linux学习笔记(第零篇)Linux文件系统及ShellLinux学习笔记(第一篇)零散命令Linux学习笔记(第二篇)目录操作命令Linux学习笔记(第三篇)文件操......
  • L - Intersection and Union Gym - 103993L (线段树)
    题意思路思路很巧妙,首先是枚举每个值的贡献,然后找到了规律,下次做题的时候线分析每个题有啥好规律,然后根据规律做题。再就是线段树的这个思路,感觉很巧妙,通过设置每一段的......