项目4
购物车实现
创建子应用cart
cd luffyapi/apps
python ../../manage.py startapp cart
注册子应用cart
INSTALLED_APPS = [ 'ckeditor', # 富文本编辑器 'ckeditor_uploader', # 富文本编辑器上传图片模块 'home', 'users', 'courses', 'cart', ]
因为购物车中的商品(课程)信息会经常被用户操作,所以为了减轻mysql服务器的压力,可以选择把购物车信息通过redis来存储.
dev.py
# 设置redis缓存 CACHES = { # 默认缓存 .... "cart":{ "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } }, }
接下来商品信息存储以下内容:
购物车商品信息格式:
商品数量[因为目前路飞学城的商品是视频,所以没有数量,如果以后做到真实商品,则必须有数量]
商品id
用户id
课程有效期
商品勾选状态
五种数据类型
string字符串
键:值
hash哈希字典
键:{
域:值,
域:值,
}
list列表
键:[值1,值2,....]
set集合
键:{值1,值2,....}
zset有序集合
键:{
权重值1:值,
权重值2:值,
}
经过比较可以发现没有一种数据类型,可以同时存储4个字段数据的,所以我们才有2种数据结构来保存购物车数据
可以发现,上面5种数据类型中,哈希hash可以存储的数据量是最多的。
hash:
键[用户ID]:{
域[商品ID]:值[课程有效期],
域[商品ID]:值[课程有效期],
域[商品ID]:值[课程有效期],
域[商品ID]:值[课程有效期],
}
set:
键[用户ID]:{商品ID1,商品ID2....}
实现课程商品到购物车的api接口
cart/views.py视图,代码:
from rest_framework.viewsets import ViewSet from rest_framework.permissions import IsAuthenticated from courses.models import Course from rest_framework.response import Response from rest_framework import status from django_redis import get_redis_connection class CartAPIViewSet(ViewSet): # permission_classes = [IsAuthenticated] def add_cart(self,request): """添加商品到购物车""" # 获取用户ID user_id = 1 # request.user.id # 获取客户端发送过来的course_id course_id = request.data.get("course_id") # 验证课程是否存在 try: Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: return Response({"message":"对不起,添加的商品不存在"}, status=status.HTTP_400_BAD_REQUEST) # 设置有效期和勾选状态的默认值 expire = 0 # 0表示没有设置默认值,或者将来我们完成课程的有效期时,重新定义0代表的意思。 # 打开redis的链接 redis_conn = get_redis_connection("cart") # 保存数据到redis中 # 把商品ID和商品的有效期存放到hash中 pipe = redis_conn.pipeline() pipe.multi() pipe.hset("cart_%s" % user_id, course_id, expire ) pipe.sadd("selected_%s" % user_id, course_id ) # 默认勾选的状态,如果用户不希望将来这个商品进行结算状态,我们在购物车商品列表中提供按钮给用户去掉 pipe.execute() # 返回响应结果 return Response({"message":"成功添加商品到购物车"})
redis的异常处理,utils/exceptions.py,代码:(不写)
from rest_framework.views import exception_handler from django.db import DatabaseError from rest_framework.response import Response from rest_framework import status from redis import RedisError import logging logger = logging.getLogger('django') def custom_exception_handler(exc, context): """ 自定义异常处理 :param exc: 异常类 :param context: 抛出异常的上下文 :return: Response响应对象 """ # 调用drf框架原生的异常处理方法 response = exception_handler(exc, context) if response is None: view = context['view'] if isinstance(exc, DatabaseError) or isinstance(exc, RedisError): # 数据库异常 logger.error('[%s] %s' % (view, exc)) response = Response({'message': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE) return response
总路由,代码:
urlpatterns = [ ... path('cart/', include("cart.urls") ), ]
子应用路由cart/urls.py,代码:
from django.urls import path,re_path from . import views urlpatterns = [ path("", views.CartAPIView.as_view({"post":"add_cart"})), ]
代码还原,把注释符号去掉:
views.py
class CartAPIViewSet(ViewSet): permission_classes = [IsAuthenticated] def add_cart(self,request): """添加商品到购物车""" # 获取用户ID user_id = request.user.id
Detail.vue
<template> <div class="detail"> <Header/> <div class="main"> <div class="course-info"> <div class="wrap-left"> <videoPlayer class="video-player vjs-custom-skin" ref="videoPlayer" :playsinline="true" :options="playerOptions" @play="onPlayerPlay($event)" @pause="onPlayerPause($event)" /> </div> <div class="wrap-right"> <h3 class="course-name">{{course.name}}</h3> <p class="data">{{course.students}}人在学 课程总时长:{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':`已更新${course.pub_lessons}课时`}} 难度:初级</p> <div class="sale-time"> <p class="sale-type">限时免费</p> <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span> 秒</p> </div> <p class="course-price"> <span>活动价</span> <span class="discount">¥0.00</span> <span class="original">¥{{course.price}}</span> </p> <div class="buy"> <div class="buy-btn"> <button class="buy-now">立即购买</button> <button class="free">免费试学</button> </div> <div class="add-cart" @click="add_cart"><img src="/static/image/cart-yellow.svg" alt="">加入购物车</div> </div> </div> </div> <div class="course-tab"> <ul class="tab-list"> <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li> <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li> <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li> <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li> </ul> </div> <div class="course-content"> <div class="course-tab-list"> <div class="tab-item" v-if="tabIndex==1"> <div v-html="url_format(course.brief)"></div> </div> <div class="tab-item" v-if="tabIndex==2"> <div class="tab-item-title"> <p class="chapter">课程章节</p> <p class="chapter-length">共11章 147个课时</p> </div> <div class="chapter-item" v-for="chapter in chapter_list"> <p class="chapter-title"><img src="/static/image/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p> <ul class="lesson-list"> <li class="lesson-item" v-for="lesson in chapter.lessons"> <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span class="free" v-if="lesson.free_trail">免费</span></p> <p class="time">{{lesson.duration}} <img src="/static/image/chapter-player.svg"></p> <button class="try" v-if="lesson.free_trail">立即试学</button> <button class="try" v-else>立即购买</button> </li> </ul> </div> </div> <div class="tab-item" v-if="tabIndex==3"> 用户评论 </div> <div class="tab-item" v-if="tabIndex==4"> 常见问题 </div> </div> <div class="course-side"> <div class="teacher-info"> <h4 class="side-title"><span>授课老师</span></h4> <div class="teacher-content"> <div class="cont1"> <img :src="teacher.image"> <div class="name"> <p class="teacher-name">{{teacher.name}}</p> <p class="teacher-title">{{teacher.title}} {{teacher.signature}}</p> </div> </div> <p class="narrative" >{{teacher.brief}}</p> </div> </div> </div> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" // 1.加载播放器组件 import {videoPlayer} from 'vue-video-player'; export default { name: "Detail", data(){ return { course_id: 0, // 课程ID course: {}, // 课程信息 teacher:'', chapter_list: [], // 章节列表 tabIndex:2, // 当前选项卡显示的下标 playerOptions:{ playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度 autoplay: false, //如果true,则自动播放 muted: false, // 默认情况下将会消除任何音频。 loop: false, // 循环播放 preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持) language: 'zh-CN', aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3") fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。 sources: [{ // 播放资源和资源格式 type: "video/mp4", src: "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4" //你的视频地址(必填) }], poster: "../static/image/course-cover.jpeg", //视频封面图 width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度 notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。 } } }, created(){ // 接受地址路径参数 // console.log( '路径参数: id=',this.$route.params.id ); // 接受地址的查询字符串参数 // console.log( '查询字符串: uid=', this.$route.query.uid ); this.course_id = this.$route.params.id; this.get_course(); this.get_chapter(); }, methods: { onPlayerPlay(){ alert("开始播放视频,关闭广告"); }, onPlayerPause(){ alert("暂停播放广告"); }, get_course(){ // 获取课程信息 this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{ this.course = response.data; this.teacher=response.data.teacher }).catch(error=>{ let self = this; this.$alert("无法获取当前课程信息,请联系客服工作人员","路飞学城",{ callback(){ self.$router.back(); // 等同于 self.$router.go(-1); } }); }) }, get_chapter(){ // 获取课程相关的章节课时信息 this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{ // params:{ // "course": this.course_id, // } }).then(response=>{ this.chapter_list = response.data; console.log(this.chapter_list); }).catch(error=>{ this.$message.info("没有获取到当前课程的章节信息"); }); }, url_format(data){ console.log(data); while( data.search('="/media') != -1 ){ data = data.replace('="/media',`="${this.$settings.Host}/media`); } return data; }, add_cart(){ // 添加商品课程到购物车中 // 1. 先判断用户是否登录 let user_token = this.$settings.check_user_login(); if( !user_token ){ let self = this; this.$confirm('对不起,您尚未登录,请登录后继续购买, 是否继续?', '路飞学城', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击确定按钮 self.$router.push("/login"); }).catch(() => { }); return ; } this.$axios.post(`${this.$settings.Host}/cart/`,{ course_id: this.course_id },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + user_token, } }).then(response=>{ this.$message.info(response.data.message); }).catch(error=>{ if(error.response.status == 401){ // 没有登录或者登录超时 let self = this; this.$confirm('对不起,您尚未登录,请登录后继续购买, 是否继续?', '路飞学城', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击确定按钮 self.$router.push("/login"); }).catch(() => { }); return ; }else{ this.$message.error("购物车添加商品失败!请联系客服工作人员!"); } }); }, }, components:{ Header, Footer, videoPlayer, // 注册播放器组件到当前页面中 } } </script> <style scoped> .main{ background: #fff; padding-top: 30px; } .course-info{ width: 1200px; margin: 0 auto; overflow: hidden; } .wrap-left{ float: left; width: 690px; height: 388px; background-color: #000; } .wrap-right{ float: left; position: relative; height: 388px; } .course-name{ font-size: 20px; color: #333; padding: 10px 23px; letter-spacing: .45px; } .data{ padding-left: 23px; padding-right: 23px; padding-bottom: 16px; font-size: 14px; color: #9b9b9b; } .sale-time{ width: 464px; background: #fa6240; font-size: 14px; color: #4a4a4a; padding: 10px 23px; overflow: hidden; } .sale-type { font-size: 16px; color: #fff; letter-spacing: .36px; float: left; } .sale-time .expire{ font-size: 14px; color: #fff; float: right; } .sale-time .expire .second{ width: 24px; display: inline-block; background: #fafafa; color: #5e5e5e; padding: 6px 0; text-align: center; } .course-price{ background: #fff; font-size: 14px; color: #4a4a4a; padding: 5px 23px; } .discount{ font-size: 26px; color: #fa6240; margin-left: 10px; display: inline-block; margin-bottom: -5px; } .original{ font-size: 14px; color: #9b9b9b; margin-left: 10px; text-decoration: line-through; } .buy{ width: 464px; padding: 0px 23px; position: absolute; left: 0; bottom: 20px; overflow: hidden; } .buy .buy-btn{ float: left; } .buy .buy-now{ width: 125px; height: 40px; border: 0; background: #ffc210; border-radius: 4px; color: #fff; cursor: pointer; margin-right: 15px; outline: none; } .buy .free{ width: 125px; height: 40px; border-radius: 4px; cursor: pointer; margin-right: 15px; background: #fff; color: #ffc210; border: 1px solid #ffc210; } .add-cart{ float: right; font-size: 14px; color: #ffc210; text-align: center; cursor: pointer; margin-top: 10px; } .add-cart img{ width: 20px; height: 18px; margin-right: 7px; vertical-align: middle; } .course-tab{ width: 100%; background: #fff; margin-bottom: 30px; box-shadow: 0 2px 4px 0 #f0f0f0; } .course-tab .tab-list{ width: 1200px; margin: auto; color: #4a4a4a; overflow: hidden; } .tab-list li{ float: left; margin-right: 15px; padding: 26px 20px 16px; font-size: 17px; cursor: pointer; } .tab-list .active{ color: #ffc210; border-bottom: 2px solid #ffc210; } .tab-list .free{ color: #fb7c55; } .course-content{ width: 1200px; margin: 0 auto; background: #FAFAFA; overflow: hidden; padding-bottom: 40px; } .course-tab-list{ width: 880px; height: auto; padding: 20px; background: #fff; float: left; box-sizing: border-box; overflow: hidden; position: relative; box-shadow: 0 2px 4px 0 #f0f0f0; } .tab-item{ width: 880px; background: #fff; padding-bottom: 20px; box-shadow: 0 2px 4px 0 #f0f0f0; } .tab-item-title{ justify-content: space-between; padding: 25px 20px 11px; border-radius: 4px; margin-bottom: 20px; border-bottom: 1px solid #333; border-bottom-color: rgba(51,51,51,.05); overflow: hidden; } .chapter{ font-size: 17px; color: #4a4a4a; float: left; } .chapter-length{ float: right; font-size: 14px; color: #9b9b9b; letter-spacing: .19px; } .chapter-title{ font-size: 16px; color: #4a4a4a; letter-spacing: .26px; padding: 12px; background: #eee; border-radius: 2px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; } .chapter-title img{ width: 18px; height: 18px; margin-right: 7px; vertical-align: middle; } .lesson-list{ padding:0 20px; } .lesson-list .lesson-item{ padding: 15px 20px 15px 36px; cursor: pointer; justify-content: space-between; position: relative; overflow: hidden; } .lesson-item .name{ font-size: 14px; color: #666; float: left; } .lesson-item .index{ margin-right: 5px; } .lesson-item .free{ font-size: 12px; color: #fff; letter-spacing: .19px; background: #ffc210; border-radius: 100px; padding: 1px 9px; margin-left: 10px; } .lesson-item .time{ font-size: 14px; color: #666; letter-spacing: .23px; opacity: 1; transition: all .15s ease-in-out; float: right; } .lesson-item .time img{ width: 18px; height: 18px; margin-left: 15px; vertical-align: text-bottom; } .lesson-item .try{ width: 86px; height: 28px; background: #ffc210; border-radius: 4px; font-size: 14px; color: #fff; position: absolute; right: 20px; top: 10px; opacity: 0; transition: all .2s ease-in-out; cursor: pointer; outline: none; border: none; } .lesson-item:hover{ background: #fcf7ef; box-shadow: 0 0 0 0 #f3f3f3; } .lesson-item:hover .name{ color: #333; } .lesson-item:hover .try{ opacity: 1; } .course-side{ width: 300px; height: auto; margin-left: 20px; float: right; } .teacher-info{ background: #fff; margin-bottom: 20px; box-shadow: 0 2px 4px 0 #f0f0f0; } .side-title{ font-weight: normal; font-size: 17px; color: #4a4a4a; padding: 18px 14px; border-bottom: 1px solid #333; border-bottom-color: rgba(51,51,51,.05); } .side-title span{ display: inline-block; border-left: 2px solid #ffc210; padding-left: 12px; } .teacher-content{ padding: 30px 20px; box-sizing: border-box; } .teacher-content .cont1{ margin-bottom: 12px; overflow: hidden; } .teacher-content .cont1 img{ width: 54px; height: 54px; margin-right: 12px; float: left; } .teacher-content .cont1 .name{ float: right; } .teacher-content .cont1 .teacher-name{ width: 188px; font-size: 16px; color: #4a4a4a; padding-bottom: 4px; } .teacher-content .cont1 .teacher-title{ width: 188px; font-size: 13px; color: #9b9b9b; white-space: nowrap; } .teacher-content .narrative{ font-size: 14px; color: #666; line-height: 24px; } </style>
显示购物车商品列表页面
购物车页面有两部分构成:
Cart.vue,代码:
<template> <div class="cart"> <Header></Header> <div class="cart_info"> <div class="cart_title"> <span class="text">我的购物车</span> <span class="total">共4门课程</span> </div> <div class="cart_table"> <div class="cart_head_row"> <span class="doing_row"></span> <span class="course_row">课程</span> <span class="expire_row">有效期</span> <span class="price_row">单价</span> <span class="do_more">操作</span> </div> <div class="cart_course_list"> <CartItem></CartItem> <CartItem></CartItem> <CartItem></CartItem> <CartItem></CartItem> </div> <div class="cart_footer_row"> <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span> <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span> <span class="goto_pay">去结算</span> <span class="cart_total">总计:¥0.0</span> </div> </div> </div> <Footer></Footer> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { checked: false, } }, methods:{ }, components:{ Header, Footer, CartItem, } } </script> <style scoped> .cart_info{ width: 1200px; margin: 0 auto 200px; } .cart_title{ margin: 25px 0; } .cart_title .text{ font-size: 18px; color: #666; } .cart_title .total{ font-size: 12px; color: #d0d0d0; } .cart_table{ width: 1170px; } .cart_table .cart_head_row{ background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; padding-right: 30px; } .cart_table .cart_head_row::after{ content: ""; display: block; clear: both; } .cart_table .cart_head_row .doing_row, .cart_table .cart_head_row .course_row, .cart_table .cart_head_row .expire_row, .cart_table .cart_head_row .price_row, .cart_table .cart_head_row .do_more{ padding-left: 10px; height: 80px; float: left; } .cart_table .cart_head_row .doing_row{ width: 78px; } .cart_table .cart_head_row .course_row{ width: 530px; } .cart_table .cart_head_row .expire_row{ width: 188px; } .cart_table .cart_head_row .price_row{ width: 162px; } .cart_table .cart_head_row .do_more{ width: 162px; } .cart_footer_row{ padding-left: 30px; background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; } .cart_footer_row .cart_select span{ font-size: 18px; color: #666; } .cart_footer_row .cart_delete{ margin-left: 58px; } .cart_delete .el-icon-delete{ font-size: 18px; } .cart_delete span{ margin-left: 15px; cursor: pointer; font-size: 18px; color: #666; } .cart_total{ float: right; margin-right: 62px; font-size: 18px; color: #666; } .goto_pay{ float: right; width: 159px; height: 80px; outline: none; border: none; background: #ffc210; font-size: 18px; color: #fff; text-align: center; cursor: pointer; } </style>
Cartitem.vue,代码:
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="checked"></el-checkbox> </div> <div class="cart_column column_2"> <img src="/static/image/course_demo.png" alt=""> <span><router-link to="/course/detail/1">爬虫从入门到进阶</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option label="1个月有效" value="30" key="30"></el-option> <el-option label="2个月有效" value="60" key="60"></el-option> <el-option label="3个月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="10000" key="10000"></el-option> </el-select> </div> <div class="cart_column column_4">¥499.0</div> <div class="cart_column column_4">删除</div> </div> </template> <script> export default { name: "CartItem", data(){ return { checked:false, expire: "1个月有效", } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
前端路由:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) // @ 表示src目录 // ... import Cart from "@/components/Cart" // .... export default new Router({ mode:"history", routes: [ // .... { path: '/cart', name: 'Cart', component: Cart, }, // .... ] })
cart/views.py,代码:
"""功能分析: 购物车中的商品信息和商品勾选状态是分开存储的,但是我们现在需要一个接口提供所有数据,所以我们需要整合/重构数据的结构 cart_<user_id>:{ <course_id>:<expire>, <course_id>:<expire>, ... } selected_<user_id>:{<course_id>,<course_id>,....} 目标:把数据整合成一个列表,列表中每一个成员就是商品字典: data = [ { course_id:<course_id>, expire:<expire>, selected:<selected>, }, { course_id:<course_id>, expire:<expire>, selected:<selected>, }, { course_id:<course_id>, expire:<expire>, selected:<selected>, }, ] """ from rest_framework.viewsets import ViewSet from rest_framework.permissions import IsAuthenticated from courses.models import Course from rest_framework.response import Response from rest_framework import status from django_redis import get_redis_connection class CartAPIViewSet(ViewSet): permission_classes = [IsAuthenticated] def add_cart(self,request): """添加商品到购物车""" # 获取用户ID user_id = request.user.id # 获取客户端发送过来的course_id course_id = request.data.get("course_id") # 验证课程是否存在 try: Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: return Response({"message":"对不起,添加的商品不存在"}, status=status.HTTP_400_BAD_REQUEST) # 设置有效期和勾选状态的默认值 expire = 0 # 0表示没有设置默认值,或者将来我们完成课程的有效期时,重新定义0代表的意思。 # 打开redis的链接 redis_conn = get_redis_connection("cart") # 保存数据到redis中 # 把商品ID和商品的有效期存放到hash中 pipe = redis_conn.pipeline() pipe.multi() pipe.hset("cart_%s" % user_id, course_id, expire ) pipe.sadd("selected_%s" % user_id, course_id ) # 默认勾选的状态,如果用户不希望将来这个商品进行结算状态,我们在购物车商品列表中提供按钮给用户去掉 pipe.execute() # 返回响应结果 return Response({"message":"成功添加商品到购物车"}) def list(self, request): """购物车商品列表""" # 获取登录用户id user_id = request.user.id # 1. 打开redis链接 redis_conn = get_redis_connection("cart") # 2. 从redis读取购物车信息[[id,course_img,name,price]]和勾选商品集合 cart_dict_bytes = redis_conn.hgetall("cart_%s" % user_id) print(cart_dict_bytes) # {b'1': b'0', b'3': b'0', b'4': b'0'} selected_set_bytes = redis_conn.smembers("selected_%s" % user_id) # 3. 把勾选状态信息和商品信息组合成一个数据列表 data = [] for course_id_bytes, expire_bytes in cart_dict_bytes.items(): course_id = course_id_bytes.decode() expire = expire_bytes.decode() try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: course = None if course: data.append({ "course_id": course.id, "course_name": course.name, "course_img": course.course_img.url, "price": "%.2f" % course.price, # 将来替换成真实价格 "expire": expire, "selected": course_id_bytes in selected_set_bytes, # 勾选状态 }) # 4. 返回数据 return Response(data)
更改路由
cart/urls.py
from django.urls import path from . import views urlpatterns = [ path("", views.CartAPIViewSet.as_view({"post":"add_cart","get":"list"}) ), ]
Cart.vue
<template> <div class="cart"> <Header></Header> <div class="cart_info"> <div class="cart_title"> <span class="text">我的购物车</span> <span class="total">共4门课程</span> </div> <div class="cart_table"> <div class="cart_head_row"> <span class="doing_row"></span> <span class="course_row">课程</span> <span class="expire_row">有效期</span> <span class="price_row">单价</span> <span class="do_more">操作</span> </div> <div class="cart_course_list"> <CartItem :key="key" v-for="course,key in course_list" :course="course"></CartItem> </div> <div class="cart_footer_row"> <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span> <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span> <span class="goto_pay">去结算</span> <span class="cart_total">总计:¥0.0</span> </div> </div> </div> <Footer></Footer> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { user_token: "", checked: false, // 实现全选功能的 course_list: [], // 购物车中的商品列表 } }, created(){ // 验证登录 this.user_token = this.$settings.check_user_login(); if(!this.user_token){ let self = this; this.$alert("请登录以后再访问!","路飞学城",{ callback(){ self.$router.push("/login"); } }); return ; } // 获取购物车商品列表 this.get_cart(); }, methods:{ get_cart(){ // 获取购物车中的商品列表 this.$axios.get(`${this.$settings.Host}/cart/`,{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; }).catch(error=>{ this.$message.error("无法获取购物车商品列表,请联系客服工作人员!"); }); } }, components:{ Header, Footer, CartItem, } } </script> <style scoped> .cart_info{ width: 1200px; margin: 0 auto 200px; } .cart_title{ margin: 25px 0; } .cart_title .text{ font-size: 18px; color: #666; } .cart_title .total{ font-size: 12px; color: #d0d0d0; } .cart_table{ width: 1170px; } .cart_table .cart_head_row{ background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; padding-right: 30px; } .cart_table .cart_head_row::after{ content: ""; display: block; clear: both; } .cart_table .cart_head_row .doing_row, .cart_table .cart_head_row .course_row, .cart_table .cart_head_row .expire_row, .cart_table .cart_head_row .price_row, .cart_table .cart_head_row .do_more{ padding-left: 10px; height: 80px; float: left; } .cart_table .cart_head_row .doing_row{ width: 78px; } .cart_table .cart_head_row .course_row{ width: 530px; } .cart_table .cart_head_row .expire_row{ width: 188px; } .cart_table .cart_head_row .price_row{ width: 162px; } .cart_table .cart_head_row .do_more{ width: 162px; } .cart_footer_row{ padding-left: 30px; background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; } .cart_footer_row .cart_select span{ font-size: 18px; color: #666; } .cart_footer_row .cart_delete{ margin-left: 58px; } .cart_delete .el-icon-delete{ font-size: 18px; } .cart_delete span{ margin-left: 15px; cursor: pointer; font-size: 18px; color: #666; } .cart_total{ float: right; margin-right: 62px; font-size: 18px; color: #666; } .goto_pay{ float: right; width: 159px; height: 80px; outline: none; border: none; background: #ffc210; font-size: 18px; color: #fff; text-align: center; cursor: pointer; } </style>
CartItem.vue
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option label="1个月有效" value="30" key="30"></el-option> <el-option label="2个月有效" value="60" key="60"></el-option> <el-option label="3个月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="10000" key="10000"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], data(){ return { checked:false, expire: "1个月有效", } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
后端提供修改勾选状态的接口
视图代码
from rest_framework.viewsets import ViewSet from rest_framework.permissions import IsAuthenticated from courses.models import Course from rest_framework.response import Response from rest_framework import status from django_redis import get_redis_connection class CartAPIViewSet(ViewSet): permission_classes = [IsAuthenticated] def add_cart(self,request): // # ... def list(self, request): """购物车商品列表""" // # ... def change_select_status(self,request): """切换商品勾选状态""" # 获取用户ID # 获取登录用户id user_id = 1 # request.user.id # 获取客户端提交过来的勾选状态 selected = request.data.get("selected") # 获取课程ID course_id = request.data.get("course_id") # 验证数据 try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: return Response({"message":"对不起,操作的商品不存在"}, status=status.HTTP_400_BAD_REQUEST) # 链接redis,修改指定商品的勾选状态 redis_conn = get_redis_connection("cart") if selected: """添加商品的勾选状态""" redis_conn.sadd("selected_%s" % user_id, course_id) else: """移除商品的勾选状态""" redis_conn.srem("selected_%s" % user_id, course_id) # 返回响应结果 return Response({"message":"切换勾选状态成功!"})
路由代码:
from django.urls import path from . import views urlpatterns = [ path("", views.CartAPIViewSet.as_view({"post":"add_cart", "get":"list", "patch":"change_select_status"}) ), ]
CartItem.vue
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option label="1个月有效" value="30" key="30"></el-option> <el-option label="2个月有效" value="60" key="60"></el-option> <el-option label="3个月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="10000" key="10000"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); } }, data(){ return { checked:false, expire: "1个月有效", } }, methods:{ change_selected_status(){ this.user_token = this.$settings.check_user_login(); // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
注意:
drf中,接受put,patch和post,接收请求体都使用request.data
接受get,delete,接收参数,只能使用request.query_params,
原因是: http请求中.get,delete是没有请求体的.而post,put和patch有请求体
视图,代码:
from rest_framework.viewsets import ViewSet from rest_framework.permissions import IsAuthenticated from courses.models import Course from rest_framework.response import Response from rest_framework import status from django_redis import get_redis_connection class CartAPIViewSet(ViewSet): # permission_classes = [IsAuthenticated] def add_cart(self,request): """添加商品到购物车""" # .... def list(self, request): """购物车商品列表""" # .... def change_select_status(self,request): """切换商品勾选状态""" # ... def delete(self, request): """"删除购物车中的商品信息""" # 接受数据 user_id = request.user.id course_id = request.query_params.get("course_id") # 验证数据 try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: return Response({"message":"对不起,操作的商品不存在"}, status=status.HTTP_400_BAD_REQUEST) # 操作redis redis_conn = get_redis_connection("cart") pipe = redis_conn.pipeline() pipe.multi() pipe.hdel("cart_%s" % user_id, course_id) pipe.srem("selected_%s" % user_id, course_id) pipe.execute() # 响应结果 return Response({"message":"购物车删除商品成功!"}, status=status.HTTP_204_NO_CONTENT)
路由代码:
from django.urls import path from . import views urlpatterns = [ path("", views.CartAPIViewSet.as_view({"post":"add_cart", "get":"list", "patch":"change_select_status","delete":"delete"}) ), ]
分析: "删除"按钮是在子组件中,但是我们不能在子组件内部删除自己,所以需要通知父组件删除自己 需要使用vue组件中子组件传递参数给给父组件
子组件CartItem.vue,代码:
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option label="1个月有效" value="30" key="30"></el-option> <el-option label="2个月有效" value="60" key="60"></el-option> <el-option label="3个月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="10000" key="10000"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4" @click="cart_delete">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); } }, data(){ return { checked:false, expire: "1个月有效", } }, created(){ this.user_token = this.$settings.check_user_login(); }, methods:{ change_selected_status(){ // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); }, cart_delete(){ // 让用户确认是否删除商品 this.$confirm(`您确定要删除当前商品<${this.course.course_name}>么?`, '路飞学城', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击删除 this.deleteHandler(); }).catch(() => { }); }, deleteHandler(){ // 购物车删除商品 this.$axios.delete(`${this.$settings.Host}/cart/`,{ params:{ course_id: this.course.course_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 通知父组件删除当前课程内容 this.$emit("delete_cart"); this.$message.success("操作成功!"); }).catch(error=>{ this.$message.error("操作失败!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
父组件Cart.vue,代码:
<template> <div class="cart"> <Header></Header> <div class="cart_info"> <div class="cart_title"> <span class="text">我的购物车</span> <span class="total">共4门课程</span> </div> <div class="cart_table"> <div class="cart_head_row"> <span class="doing_row"></span> <span class="course_row">课程</span> <span class="expire_row">有效期</span> <span class="price_row">单价</span> <span class="do_more">操作</span> </div> <div class="cart_course_list"> <CartItem :key="key" v-for="course,key in course_list" :course="course" @delete_cart="deleteHandler(key)"></CartItem> </div> <div class="cart_footer_row"> <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span> <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span> <span class="goto_pay">去结算</span> <span class="cart_total">总计:¥0.0</span> </div> </div> </div> <Footer></Footer> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { user_token: "", checked: false, // 实现全选功能的 course_list: [], // 购物车中的商品列表 } }, created(){ // 验证登录 this.user_token = this.$settings.check_user_login(); if(!this.user_token){ let self = this; this.$alert("请登录以后再访问!","路飞学城",{ callback(){ self.$router.push("/login"); } }); return ; } // 获取购物车商品列表 this.get_cart(); }, methods:{ get_cart(){ // 获取购物车中的商品列表 this.$axios.get(`${this.$settings.Host}/cart/`,{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; }).catch(error=>{ this.$message.error("无法获取购物车商品列表,请联系客服工作人员!"); }); }, deleteHandler(key){ // 前端实现删除商品信息 this.course_list.splice(key,1); } }, components:{ Header, Footer, CartItem, } } </script> <style scoped> .cart_info{ width: 1200px; margin: 0 auto 200px; } .cart_title{ margin: 25px 0; } .cart_title .text{ font-size: 18px; color: #666; } .cart_title .total{ font-size: 12px; color: #d0d0d0; } .cart_table{ width: 1170px; } .cart_table .cart_head_row{ background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; padding-right: 30px; } .cart_table .cart_head_row::after{ content: ""; display: block; clear: both; } .cart_table .cart_head_row .doing_row, .cart_table .cart_head_row .course_row, .cart_table .cart_head_row .expire_row, .cart_table .cart_head_row .price_row, .cart_table .cart_head_row .do_more{ padding-left: 10px; height: 80px; float: left; } .cart_table .cart_head_row .doing_row{ width: 78px; } .cart_table .cart_head_row .course_row{ width: 530px; } .cart_table .cart_head_row .expire_row{ width: 188px; } .cart_table .cart_head_row .price_row{ width: 162px; } .cart_table .cart_head_row .do_more{ width: 162px; } .cart_footer_row{ padding-left: 30px; background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; } .cart_footer_row .cart_select span{ font-size: 18px; color: #666; } .cart_footer_row .cart_delete{ margin-left: 58px; } .cart_delete .el-icon-delete{ font-size: 18px; } .cart_delete span{ margin-left: 15px; cursor: pointer; font-size: 18px; color: #666; } .cart_total{ float: right; margin-right: 62px; font-size: 18px; color: #666; } .goto_pay{ float: right; width: 159px; height: 80px; outline: none; border: none; background: #ffc210; font-size: 18px; color: #fff; text-align: center; cursor: pointer; } </style>
在后端模型中声明一个课程和课程有效期价格关系表模型CourseExpire
class CourseExpire(BaseModel): course = models.ForeignKey("Course", related_name='courseexpire', on_delete=models.CASCADE, verbose_name="课程ID") timer = models.CharField(max_length=100,verbose_name="课程有效期(天)",help_text="课程有效期") price = models.DecimalField(max_digits=6,decimal_places=2, verbose_name="课程价格",default=0) text = models.CharField(max_length=64, verbose_name="课程有效期文本格式", help_text="课程有效期文本格式") class Meta: db_table = "ly_course_expire" verbose_name = "课程与有效期" verbose_name_plural = verbose_name def __str__(self): return "%s[%s]" % (self.course, self.text)
对模型进行数据迁移
python manage.py makemigrations
python manage.py migrate
courses/adminx.py,代码:
from .models import CourseExpire class CourseExpireModelAdmin(object): """课程与有效期模型管理类""" list_display = ["course","text","price"] xadmin.site.register(CourseExpire, CourseExpireModelAdmin)
为了方便我们在课程中直接获取课程有效期的数据,所以在课程模型中,新增一个属性方法:
class Course(BaseModel): // ... @property def expire_list(self): """课程有效期选项列表""" data_list = self.courseexpire.all() data = [] # 如果课程模型中,有存在永久价格,则在有效期选项列表中,新增永久有效的选项 if self.price > 0: data.append({ "time": "0", "text": "永久有效", "price": "%.2f" % self.price }) for item in data_list: data.append({ "time": item.timer, "text": item.text, "price": "%.2f" % item.price }) return data
在购物车的商品列表接口中返回每一个商品对应的有效期列表,cart/views.py,视图代码:
def list(self, request): """购物车商品列表""" # 获取登录用户id user_id = request.user.id # 1. 打开redis链接 redis_conn = get_redis_connection("cart") # 2. 从redis读取购物车信息[[id,course_img,name,price]]和勾选商品集合 cart_dict_bytes = redis_conn.hgetall("cart_%s" % user_id) print(cart_dict_bytes) # {b'1': b'0', b'3': b'0', b'4': b'0'} selected_set_bytes = redis_conn.smembers("selected_%s" % user_id) # 3. 把勾选状态信息和商品信息组合成一个数据列表 data = [] for course_id_bytes, expire_bytes in cart_dict_bytes.items(): course_id = course_id_bytes.decode() expire = expire_bytes.decode() try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: course = None if course: data.append({ "course_id": course.id, "course_name": course.name, "course_img": course.course_img.url, "price": "%.2f" % course.real_price(expire), # 真实价格 "expire": expire, # 用户购物车中的有效期选项 "expire_list": course.expire_list, # 有效期选项列表 "selected": course_id_bytes in selected_set_bytes, # 勾选状态 }) # 4. 返回数据 return Response(data)
前端展示购物车的课程商品列表时,显示有效期
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="course.expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option v-for="item in course.expire_list" :label="item.text" :value="item.time" :key="item.time"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4" @click="cart_delete">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); } }, data(){ return { checked: false, } }, created(){ this.user_token = this.$settings.check_user_login(); }, methods:{ change_selected_status(){ // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); }, cart_delete(){ // 让用户确认是否删除商品 this.$confirm(`您确定要删除当前商品<${this.course.course_name}>么?`, '路飞学城', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击删除 this.deleteHandler(); }).catch(() => { }); }, deleteHandler(){ // 购物车删除商品 this.$axios.delete(`${this.$settings.Host}/cart/`,{ params:{ course_id: this.course.course_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 通知父组件删除当前课程内容 this.$emit("delete_cart"); this.$message.success("操作成功!"); }).catch(error=>{ this.$message.error("操作失败!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
在用户切换商品的有效期选项以后,价格产生对应的变化,
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="course.expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option v-for="item in course.expire_list" :label="item.text" :value="item.time" :key="item.time"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4" @click="cart_delete">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); } }, data(){ return { checked: false, } }, created(){ this.user_token = this.$settings.check_user_login(); }, watch:{ "course.expire": function(){ this.change_course_expire(); }, }, methods:{ change_course_expire(){ // 从expire_list中选出对应的课程价格 for(let item of this.course.expire_list){ // for ... of 是es6的语法,可以在循环中获取数组的成员,而不是原来的for ..in 里面的下表索引 if( this.course.expire == item.time ){ this.course.price = item.price; } } // 把当前本次选中的有效期选项同步到redis中 }, change_selected_status(){ // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); }, cart_delete(){ // 让用户确认是否删除商品 this.$confirm(`您确定要删除当前商品<${this.course.course_name}>么?`, '路飞学城', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击删除 this.deleteHandler(); }).catch(() => { }); }, deleteHandler(){ // 购物车删除商品 this.$axios.delete(`${this.$settings.Host}/cart/`,{ params:{ course_id: this.course.course_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 通知父组件删除当前课程内容 this.$emit("delete_cart"); this.$message.success("操作成功!"); }).catch(error=>{ this.$message.error("操作失败!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
我们这里使用put方法。
from rest_framework.viewsets import ViewSet from rest_framework.permissions import IsAuthenticated from courses.models import Course from rest_framework.response import Response from rest_framework import status from django_redis import get_redis_connection class CartAPIViewSet(ViewSet): permission_classes = [IsAuthenticated] # // .... def change_course_expire(self,request): """切换课程有效期""" # 获取登录用户id user_id = request.user.id # 获取客户端提交过来的课程有效期时间 time = request.data.get("time") # 获取课程ID course_id = request.data.get("course_id") # 验证数据 try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) if int(time) != 0: course_expire = course.courseexpire.get(timer=time, course_id=course.id) else: if float(course.price) == 0: raise Exception("对应的价格选项不存在!") except: return Response({"message":"对不起,操作的商品不存在"}, status=status.HTTP_400_BAD_REQUEST) # 链接redis,修改指定商品的勾选状态 redis_conn = get_redis_connection("cart") redis_conn.hset("cart_%s" % user_id, course_id, time) return Response({"messsage":"切换有效期成功!"})
cart/urls.py,路由代码:
from django.urls import path from . import views urlpatterns = [ path("", views.CartAPIViewSet.as_view({ "post":"add_cart", "get":"list", "patch":"change_select_status", "delete":"delete", "put": "change_course_expire", }) ), ]
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="course.expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option v-for="item in course.expire_list" :label="item.text" :value="item.time" :key="item.time"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4" @click="cart_delete">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); }, "course.expire": function(){ this.change_course_expire(); }, }, data(){ return { checked: false, } }, created(){ this.user_token = this.$settings.check_user_login(); }, methods:{ change_course_expire(){ // 从expire_list中选出对应的课程价格 for(let item of this.course.expire_list){ // for ... of 是es6的语法,可以在循环中获取数组的成员,而不是原来的for ..in 里面的下表索引 if( this.course.expire == item.time ){ this.course.price = item.price; } } // 把当前本次选中的有效期选项同步到redis中 this.$axios.put(`${this.$settings.Host}/cart/`,{ course_id: this.course.course_id, time: this.course.expire },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ console.log(response.data); }).catch(error=>{ this.$message.error("切换有效期选项失败!无法请求服务端,请联系客服工作人员!"); }) }, change_selected_status(){ // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); }, cart_delete(){ // 让用户确认是否删除商品 this.$confirm(`您确定要删除当前商品<${this.course.course_name}>么?`, '路飞学城', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击删除 this.deleteHandler(); }).catch(() => { }); }, deleteHandler(){ // 购物车删除商品 this.$axios.delete(`${this.$settings.Host}/cart/`,{ params:{ course_id: this.course.course_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 通知父组件删除当前课程内容 this.$emit("delete_cart"); this.$message.success("操作成功!"); }).catch(error=>{ this.$message.error("操作失败!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
我们应该在购物车中商品被删除时,重新计算购物车商品总价!
我们应该在商品的有效期选项发生改变时,重新计算购物车商品总价!
子组件CartItem.vue,代码:
<template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="course.selected"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="$settings.Host+course.course_img" alt=""> <span><router-link :to="`/courses/${course.course_id}`">{{course.course_name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="course.expire" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option v-for="item in course.expire_list" :label="item.text" :value="item.time" :key="item.time"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{course.price}}</div> <div class="cart_column column_4" @click="cart_delete">删除</div> </div> </template> <script> export default { name: "CartItem", props:["course"], data(){ return { checked: false, } }, created(){ this.user_token = this.$settings.check_user_login(); }, watch:{ "course.selected": function(){ // 切换商品的勾选状态 this.change_selected_status(); }, "course.expire": function(){ this.change_course_expire(); }, }, methods:{ change_course_expire(){ // 从expire_list中选出对应的课程价格 for(let item of this.course.expire_list){ // for ... of 是es6的语法,可以在循环中获取数组的成员,而不是原来的for ..in 里面的下表索引 if( this.course.expire == item.time ){ this.course.price = item.price; } } // 把当前本次选中的有效期选项同步到redis中 this.$axios.put(`${this.$settings.Host}/cart/`,{ course_id: this.course.course_id, time: this.course.expire },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 在切换勾选状态时,通知父组件条件总价格 this.$emit("change_course", this.course); }).catch(error=>{ this.$message.error("切换有效期选项失败!无法请求服务端,请联系客服工作人员!"); }) }, change_selected_status(){ // 切换商品的勾选状态 this.$axios.patch(`${this.$settings.Host}/cart/`,{ "course_id": this.course.course_id, "selected": this.course.selected, },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 在切换勾选状态时,通知父组件条件总价格 this.$emit("change_course", this.course); }).catch(error=>{ this.$message.error("勾选状态切换失败!请刷新页面后重新尝试!"); }); }, cart_delete(){ // 让用户确认是否删除商品 this.$confirm(`您确定要删除当前商品<${this.course.course_name}>么?`, '路飞学城', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击删除 this.deleteHandler(); }).catch(() => { }); }, deleteHandler(){ // 购物车删除商品 this.$axios.delete(`${this.$settings.Host}/cart/`,{ params:{ course_id: this.course.course_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 通知父组件删除当前课程内容 this.$emit("delete_cart"); this.$message.success("操作成功!"); }).catch(error=>{ this.$message.error("操作失败!"); }); } } } </script> <style scoped> .cart_item::after{ content: ""; display: block; clear: both; } .cart_column{ float: left; height: 250px; } .cart_item .column_1{ width: 88px; position: relative; } .my_el_checkbox{ position: absolute; left: 0; right: 0; bottom: 0; top: 0; margin: auto; width: 16px; height: 16px; } .cart_item .column_2 { padding: 67px 10px; width: 520px; height: 116px; } .cart_item .column_2 img{ width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; } .cart_item .column_3{ width: 197px; position: relative; padding-left: 10px; } .my_el_select{ width: 117px; height: 28px; position: absolute; top: 0; bottom: 0; margin: auto; } .cart_item .column_4{ padding: 67px 10px; height: 116px; width: 142px; line-height: 116px; } </style>
父组件,Cart.vue,代码:
<template> <div class="cart"> <Header></Header> <div class="cart_info"> <div class="cart_title"> <span class="text">我的购物车</span> <span class="total">共4门课程</span> </div> <div class="cart_table"> <div class="cart_head_row"> <span class="doing_row"></span> <span class="course_row">课程</span> <span class="expire_row">有效期</span> <span class="price_row">单价</span> <span class="do_more">操作</span> </div> <div class="cart_course_list"> <CartItem :key="key" v-for="course,key in course_list" :course="course" @delete_cart="deleteHandler(key)" @change_course="change_course" ></CartItem> </div> <div class="cart_footer_row"> <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span> <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span> <span class="goto_pay">去结算</span> <span class="cart_total">总计:¥{{total.toFixed(2)}}</span> </div> </div> </div> <Footer></Footer> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { user_token: "", checked: false, // 实现全选功能的 course_list: [], // 购物车中的商品列表 total: 0, // 购物车商品总价 } }, created(){ // 验证登录 this.user_token = this.$settings.check_user_login(); if(!this.user_token){ let self = this; this.$alert("请登录以后再访问!","路飞学城",{ callback(){ self.$router.push("/login"); } }); return ; } // 获取购物车商品列表 this.get_cart(); }, methods:{ get_cart(){ // 获取购物车中的商品列表 this.$axios.get(`${this.$settings.Host}/cart/`,{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; // 计算购物车商品总价格 this.calc_cart_total_price(); }).catch(error=>{ this.$message.error("无法获取购物车商品列表,请联系客服工作人员!"); }); }, deleteHandler(key){ // 前端实现删除商品信息 this.course_list.splice(key,1); }, calc_cart_total_price(){ // 计算购物车中所有商品的总价格 let total_price = 0; for(let course of this.course_list){ if( course.selected ){ total_price += parseFloat(course.price); } } this.total = total_price; }, change_course(course){ this.course_list.forEach((item,key)=>{ if(item.course_id == course.course_id){ this.course_list[key] = course; } }); // 计算总价格 this.calc_cart_total_price(); }, }, components:{ Header, Footer, CartItem, } } </script> <style scoped> .cart_info{ width: 1200px; margin: 0 auto 200px; } .cart_title{ margin: 25px 0; } .cart_title .text{ font-size: 18px; color: #666; } .cart_title .total{ font-size: 12px; color: #d0d0d0; } .cart_table{ width: 1170px; } .cart_table .cart_head_row{ background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; padding-right: 30px; } .cart_table .cart_head_row::after{ content: ""; display: block; clear: both; } .cart_table .cart_head_row .doing_row, .cart_table .cart_head_row .course_row, .cart_table .cart_head_row .expire_row, .cart_table .cart_head_row .price_row, .cart_table .cart_head_row .do_more{ padding-left: 10px; height: 80px; float: left; } .cart_table .cart_head_row .doing_row{ width: 78px; } .cart_table .cart_head_row .course_row{ width: 530px; } .cart_table .cart_head_row .expire_row{ width: 188px; } .cart_table .cart_head_row .price_row{ width: 162px; } .cart_table .cart_head_row .do_more{ width: 162px; } .cart_footer_row{ padding-left: 30px; background: #F7F7F7; width: 100%; height: 80px; line-height: 80px; } .cart_footer_row .cart_select span{ font-size: 18px; color: #666; } .cart_footer_row .cart_delete{ margin-left: 58px; } .cart_delete .el-icon-delete{ font-size: 18px; } .cart_delete span{ margin-left: 15px; cursor: pointer; font-size: 18px; color: #666; } .cart_total{ float: right; margin-right: 62px; font-size: 18px; color: #666; } .goto_pay{ float: right; width: 159px; height: 80px; outline: none; border: none; background: #ffc210; font-size: 18px; color: #fff; text-align: center; cursor: pointer; } </style>
经过上面的处理以后,客户端更换有效期选项和勾选状态时,都已经可以准确的计算价格了。但是页面一旦刷新,则会出现,购物车商品列表的中商品价格不是有效期选项对应的价格而是来自于课程信息中的原价。
def list(self, request): """购物车商品列表""" # 获取登录用户id user_id = request.user.id # 1. 打开redis链接 redis_conn = get_redis_connection("cart") # 2. 从redis读取购物车信息[[id,course_img,name,price]]和勾选商品集合 cart_dict_bytes = redis_conn.hgetall("cart_%s" % user_id) print(cart_dict_bytes) # {b'1': b'0', b'3': b'0', b'4': b'0'} selected_set_bytes = redis_conn.smembers("selected_%s" % user_id) # 3. 把勾选状态信息和商品信息组合成一个数据列表 data = [] for course_id_bytes, expire_bytes in cart_dict_bytes.items(): course_id = course_id_bytes.decode() expire = expire_bytes.decode() try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: course = None if course: data.append({ "course_id": course.id, "course_name": course.name, "course_img": course.course_img.url, "price": "%.2f" % course.real_price(expire), # 真实价格 "expire": expire, # 用户购物车中的有效期选项 "expire_list": course.expire_list, # 有效期选项列表 "selected": course_id_bytes in selected_set_bytes, # 勾选状态 }) # 4. 返回数据 return Response(data)
模型中新增获取真实价格的方法,courses/models.py,代码:
class Course(BaseModel): """ 实战课程 """ # // ... def real_price(self, time): """获取真实原价""" if time == "0": price = self.price else: courseexpire = self.courseexpire.get(timer=time) price = courseexpire.price # 根据优惠计算真实价格 return price
价格优惠活动类型名称: 限时免费, 限时折扣, 限时减免, 满减, 积分抵扣, 优惠券 公式: 限时免费 原价-原价 限时折扣 原价*0.8 限时减免 原价-减免价 满减 原价-(满减计算后换算价格) 积分抵扣 原价-(积分计算后换算价格) ->> 积分换算比率 优惠券 原价-优惠券价格 -->> 优惠券
courses/models.py,模型代码:
# Create your models here. from django.db import models from luffyapi.utils.models import BaseModel # Create your models here. class CourseCategory(BaseModel): """ 课程分类 """ name = models.CharField(max_length=64, unique=True, verbose_name="分类名称") class Meta: db_table = "ly_course_category" verbose_name = "课程分类" verbose_name_plural = "课程分类" def __str__(self): return "%s" % self.name from ckeditor.fields import RichTextField from ckeditor_uploader.fields import RichTextUploadingField from datetime import datetime class Course(BaseModel): """ 实战课程 """ course_type = ( (0, '付费'), (1, 'VIP专享'), (2, '学位课程'), ) level_choices = ( (0, '初级'), (1, '中级'), (2, '高级'), ) status_choices = ( (0, '上线'), (1, '下线'), (2, '预上线'), ) name = models.CharField(max_length=128, verbose_name="课程名称") course_img = models.ImageField(upload_to="course", max_length=255, verbose_name="封面图片", blank=True, null=True) course_video = models.FileField(upload_to="course_video", max_length=255, verbose_name="封面视频", blank=True, null=True) course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型") # 使用这个字段的原因 brief = RichTextUploadingField(verbose_name="详情介绍", null=True, blank=True) level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级") pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True) period = models.IntegerField(verbose_name="建议学习周期(day)", default=7) attachment_path = models.FileField(max_length=128, verbose_name="课件路径", blank=True, null=True) status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态") course_category = models.ForeignKey("CourseCategory", on_delete=models.CASCADE, null=True, blank=True,verbose_name="课程分类") students = models.IntegerField(verbose_name="学习人数",default = 0) lessons = models.IntegerField(verbose_name="总课时数量",default = 0) pub_lessons = models.IntegerField(verbose_name="课时更新数量",default = 0) price = models.DecimalField(max_digits=6,decimal_places=2, verbose_name="课程原价",default=0) teacher = models.ForeignKey("Teacher",on_delete=models.DO_NOTHING, null=True, blank=True,verbose_name="授课老师") class Meta: db_table = "ly_course" verbose_name = "实战课程" verbose_name_plural = "实战课程" @property def teacher_demo(self): return "测试数据" @property def recomment_lessons(self): """课程列表的推荐课时""" lesson_list = self.lesson_list.filter(recomment=True, is_show=True, is_delete=False).order_by("orders","id") # data = [] # for lesson in lesson_list: # data.append({ # "id": lesson.id, # "name": lesson.name, # "free_trail": lesson.free_trail, # }) # return data # 列表生成式 return [{"id":lesson.id, "name": lesson.name, "free_trail": lesson.free_trail} for lesson in lesson_list] @property def level_name(self): """难度等级的文本格式""" return self.level_choices[self.level][1] @property def expire_list(self): """课程有效期选项列表""" data_list = self.courseexpire.all() data = [] # 如果课程模型中,有存在永久价格,则在有效期选项列表中,新增永久有效的选项 if self.price > 0: data.append({ "time": "0", "text": "永久有效", "price": "%.2f" % self.price }) for item in data_list: data.append({ "time": item.timer, "text": item.text, "price": "%.2f" % item.price }) return data def real_price(self, time=None): """获取真实原价""" if time == "0": price = self.price else: courseexpire = self.courseexpire.get(timer=time) price = courseexpire.price # 根据优惠计算真实价格 return price @property def discount_name(self): """折扣类型""" # 判断 当前时间>活动开始时间 and 当前时间<活动结束时间 try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return "" return activity.discount.discount_type.name def discount_price(self,price=None): """折扣后的价格""" if price is None: # 如果没有传递参数,则默认采用当前原价 price = self.price price = float(price) # 默认获取到的数据时Decimal类型的 try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return price # 计算参与活动后的真实价格 condition = activity.discount.condition # 判断当前课程原价是否满足活动的参与条件 if condition > price: return price sale = activity.discount.sale if sale == "0": """限时免费""" price = 0 elif sale[0] == "*": """限时折扣""" num = float( sale[1:] ) # 折扣数值 price = price * num elif sale[0] == "-": """限时减免""" num = float( sale[1:] ) # 减免数值 price = price - num elif sale[0] == "满": """限时满减""" num_list = sale.split("\r\n") sale_list = [] for item in num_list: sale_condition,sale_num = item[1:].split("-") if price > float(sale_condition): sale_list.append( float(sale_num) ) current_sale = max(sale_list) price = price - current_sale return "%.2f" % price @property def has_time(self): try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return 0 cur_timestamp = datetime.now().timestamp() end_timestamp = activity.activity.end_time.timestamp() time = end_timestamp - cur_timestamp return int(time) def __str__(self): return "%s" % self.name class Teacher(BaseModel): """讲师、导师表""" role_choices = ( (0, '讲师'), (1, '导师'), (2, '班主任'), ) name = models.CharField(max_length=32, verbose_name="讲师昵称") role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="讲师身份") title = models.CharField(max_length=64, verbose_name="职位、职称") signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True) image = models.ImageField(upload_to="teacher", null=True, blank=True, verbose_name = "讲师封面") brief = models.TextField(max_length=1024, verbose_name="讲师描述") class Meta: db_table = "ly_teacher" verbose_name = "讲师导师" verbose_name_plural = "讲师导师" @property def role_name(self): return self.role_choices[self.role][1] def __str__(self): return "%s" % self.name class CourseChapter(BaseModel): """课程章节""" course = models.ForeignKey("Course", related_name='chapters', on_delete=models.CASCADE, verbose_name="课程名称") chapter = models.SmallIntegerField(verbose_name="第几章", default=1) name = models.CharField(max_length=128, verbose_name="章节标题") summary = models.TextField(verbose_name="章节介绍", blank=True, null=True) pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True) class Meta: db_table = "ly_course_chapter" verbose_name = "课程章节" verbose_name_plural = "课程章节" def __str__(self): return "%s:(第%s章)%s" % (self.course, self.chapter, self.name) class CourseLesson(BaseModel): """课程课时""" lesson_type_choices = ( (0, '文档'), (1, '练习'), (2, '视频') ) chapter = models.ForeignKey("CourseChapter", related_name='lessons', on_delete=models.CASCADE,verbose_name="章节") course = models.ForeignKey("Course", related_name="lesson_list", on_delete=models.CASCADE, verbose_name="课程") name = models.CharField(max_length=128,verbose_name = "课时标题") lesson_type = models.SmallIntegerField(default=2, choices=lesson_type_choices, verbose_name="课时种类") lesson_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接", help_text = "若是video,填vid,若是文档,填link") duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32) # 仅在前端展示使用 pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True) free_trail = models.BooleanField(verbose_name="是否可试看", default=False) recomment = models.BooleanField(verbose_name="是否推荐到课程列表") class Meta: db_table = "ly_course_lesson" verbose_name = "课程课时" verbose_name_plural = "课程课时" def __str__(self): return "%s-%s" % (self.chapter, self.name) class CourseExpire(BaseModel): course = models.ForeignKey("Course", related_name='courseexpire', on_delete=models.CASCADE, verbose_name="课程ID") timer = models.CharField(max_length=100,verbose_name="课程有效期(天)",help_text="课程有效期") price = models.DecimalField(max_digits=6,decimal_places=2, verbose_name="课程价格",default=0) text = models.CharField(max_length=64, verbose_name="课程有效期文本格式", help_text="课程有效期文本格式") class Meta: db_table = "ly_course_expire" verbose_name = "课程与有效期" verbose_name_plural = verbose_name def __str__(self): return "%s[%s]" % (self.course, self.text) """价格相关的模型""" class PriceDiscountType(BaseModel): """课程优惠类型""" name = models.CharField(max_length=32, verbose_name="类型名称") remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息") class Meta: db_table = "ly_price_discount_type" verbose_name = "课程优惠类型" verbose_name_plural = verbose_name def __str__(self): return "%s" % (self.name) class PriceDiscount(BaseModel): """课程优惠策略模型""" discount_type = models.ForeignKey("PriceDiscountType", on_delete=models.CASCADE, related_name='pricediscounts', verbose_name="优惠类型") condition = models.IntegerField(blank=True, default=0, verbose_name="满足优惠的价格条件") sale = models.TextField(verbose_name="优惠公式", help_text=""" 0表示免费;<br> *号开头加上浮点数表示折扣价,例如*0.82表示八二折;<br> -号开头加上数值表示减免价,例如-50表示减免50;<br> 如果要表示限时满减,则需要使用 原价-优惠价格,例如表示,课程价格大于100,优惠10;大于200,优惠20,格式如下:<br> 满100-10<br> 满200-20<br> """) class Meta: db_table = "ly_price_discount" verbose_name = "价格优惠策略" verbose_name_plural = verbose_name def __str__(self): return "%s: 优惠条件:%s,优惠值:%s" % (self.discount_type.name, self.condition, self.sale) class Activity(BaseModel): """优惠活动模型""" name = models.CharField(max_length=64, verbose_name="活动名称") start_time = models.DateTimeField(verbose_name="活动的开始时间", help_text="活动的开始时间") end_time = models.DateTimeField(verbose_name="活动的结束时间", help_text="活动的结束时间") class Meta: db_table = "ly_activity" verbose_name = "优惠活动表" verbose_name_plural = verbose_name def __str__(self): return "%s[%s-%s]" % (self.name, self.start_time, self.end_time) class CourseActivity(BaseModel): """课程与活动的关系模型""" course = models.ForeignKey("Course", on_delete=models.CASCADE, related_name="course_prices", verbose_name="课程") activity = models.ForeignKey("Activity", on_delete=models.CASCADE, related_name="activity_courses") discount = models.ForeignKey("PriceDiscount", on_delete=models.CASCADE, related_name="discount_courses", verbose_name="优惠活动") class Meta: db_table = "ly_course_activity" verbose_name = "课程与优惠活动的关系" verbose_name_plural = "课程与优惠策略的关系" def __str__(self): return "%s-%s-%s" % (self.course.name, self.activity.name, self.discount.discount_type.name)
python manage.py makemigrations
python manage.py migrate
添加测试数据
courses/adminx.py
"""优惠活动的相关模型注册""" from .models import Activity class ActivityModelAdmin(object): """优惠活动模型管理类""" list_display = ["name","start_time","end_time"] xadmin.site.register(Activity, ActivityModelAdmin) from .models import PriceDiscountType class PriceDiscountTypeModelAdmin(object): """优惠类型模型管理类""" list_display = ["name","remark"] xadmin.site.register(PriceDiscountType, PriceDiscountTypeModelAdmin) from .models import PriceDiscount class PriceDiscountModelAdmin(object): """优惠策略公式模型管理类""" pass xadmin.site.register(PriceDiscount, PriceDiscountModelAdmin) from .models import CourseActivity class CourseActivityModelAdmin(object): """课程与活动的关系模型管理类""" pass xadmin.site.register(CourseActivity, CourseActivityModelAdmin)
后端在模型中计算课程真实价格
因为课程的优惠是具有时效性的,所以我们计算价格的时候需要先判断当前优惠是否过期了。
USE_TZ = False
courses/models.py,代码:
from ckeditor.fields import RichTextField from ckeditor_uploader.fields import RichTextUploadingField from datetime import datetime class Course(BaseModel): """ 实战课程 """ # // .... def real_price(self, time=None): """获取真实原价""" if time == "0": price = self.price else: courseexpire = self.courseexpire.get(timer=time) price = courseexpire.price # 根据优惠计算真实价格 return price @property def discount_name(self): """折扣类型""" # 判断 当前时间>活动开始时间 and 当前时间<活动结束时间 try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return "" return activity.discount.discount_type.name def discount_price(self,price=None): """折扣后的价格""" if price is None: # 如果没有传递参数,则默认采用当前原价 price = self.price price = float(price) # 默认获取到的数据时Decimal类型的 try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return price # 计算参与活动后的真实价格 condition = activity.discount.condition # 判断当前课程原价是否满足活动的参与条件 if condition > price: return price sale = activity.discount.sale if sale == "0": """限时免费""" price = 0 elif sale[0] == "*": """限时折扣""" num = float( sale[1:] ) # 折扣数值 price = price * num elif sale[0] == "-": """限时减免""" num = float( sale[1:] ) # 减免数值 price = price - num elif sale[0] == "满": """限时满减""" num_list = sale.split("\r\n") sale_list = [] for item in num_list: sale_condition,sale_num = item[1:].split("-") if price > float(sale_condition): sale_list.append( float(sale_num) ) current_sale = max(sale_list) price = price - current_sale return "%.2f" % price def __str__(self): return "%s" % self.name
修改序列化器courses/serializers.py,增加返回2个字段[优惠类型和优惠策略]
from .models import Course class CourseModelSerializer(serializers.ModelSerializer): """课程列表""" teacher = TeacherModelSerializer() class Meta: model = Course fields = ["id","name","course_img","students","lessons","pub_lessons","price","teacher","recomment_lessons","discount_name","discount_price"]
Course.vue
<div class="pay-box"> <span class="discount-type" v-if="course.discount_name">{{course.discount_name}}</span> <span class="discount-price" v-if="course.discount_name">¥{{course.discount_price}}元</span> <span class="discount-price" v-else>¥{{course.price}}元</span> <span class="original-price" v-if="course.discount_name">原价:{{course.price}}元</span> <span class="buy-now">立即购买</span> </div>
后端序列化器增加返回字段信息
class CourseRetrieveModelSerializer(serializers.ModelSerializer): """课程详情""" teacher = TeacherModelSerializer() class Meta: model = Course fields = ["id", "name", "course_img", "course_video", "students", "lessons", "pub_lessons", "price", "teacher", "brief", "level_name","discount_name","discount_price","has_time"]
模型中新增返回活动的剩余时间字段方法,代码:
@property def has_time(self): try: activity = self.course_prices.get(activity__start_time__lt=datetime.now(), activity__end_time__gt=datetime.now(),is_show=True,is_delete=False) except: return 0 cur_timestamp = datetime.now().timestamp() end_timestamp = activity.activity.end_time.timestamp() time = end_timestamp - cur_timestamp return int(time)
<template> <div class="detail"> <Header/> <div class="main"> <div class="course-info"> <div class="wrap-left"> <videoPlayer v-if="course.course_video" class="video-player vjs-custom-skin" ref="videoPlayer" :playsinline="true" :options="playerOptions" @play="onPlayerPlay($event)" @pause="onPlayerPause($event)" /> <img v-else :src="course.course_img" alt=""> </div> <div class="wrap-right"> <h3 class="course-name">{{course.name}}</h3> <p class="data">{{course.students}}人在学 课程总时长:{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':`已更新${course.pub_lessons}课时`}} 难度:初级</p> <div class="sale-time" v-if="course.has_time>0"> <p class="sale-type">{{course.discount_name}}</p> <p class="expire">距离结束:仅剩 {{parseInt(course.has_time/86400) | padding0}}天 {{parseInt(course.has_time/3600%24) | padding0}}小时 {{parseInt(course.has_time/60%60) | padding0}}分 <span class="second">{{course.has_time%60 | padding0 }}</span> 秒</p> </div> <p class="course-price" v-if="course.has_time>0"> <span>活动价</span> <span class="discount">¥{{course.discount_price}}</span> <span class="original">¥{{course.price}}</span> </p> <div class="sale-time" v-else> <p class="sale-type">价格 ¥ {{course.price}}</p> </div> <div class="buy"> <div class="buy-btn"> <button class="buy-now">立即购买</button> <button class="free">免费试学</button> </div> <div class="add-cart" @click="add_cart"><img src="/static/image/cart-yellow.svg" alt="" >加入购物车</div> </div> </div> </div> <div class="course-tab"> <ul class="tab-list"> <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li> <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li> <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li> <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li> </ul> </div> <div class="course-content"> <div class="course-tab-list"> <div class="tab-item" v-if="tabIndex==1"> <div v-html="url_format(course.brief)"></div> </div> <div class="tab-item" v-if="tabIndex==2"> <div class="tab-item-title"> <p class="chapter">课程章节</p> <p class="chapter-length">共11章 147个课时</p> </div> <div class="chapter-item" v-for="chapter in chapter_list"> <p class="chapter-title"><img src="/static/image/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p> <ul class="lesson-list"> <li class="lesson-item" v-for="lesson in chapter.lessons"> <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span class="free" v-if="lesson.free_trail">免费</span></p> <p class="time">{{lesson.duration}} <img src="/static/image/chapter-player.svg"></p> <button class="try" v-if="lesson.free_trail">立即试学</button> <button class="try" v-else>立即购买</button> </li> </ul> </div> </div> <div class="tab-item" v-if="tabIndex==3"> 用户评论 </div> <div class="tab-item" v-if="tabIndex==4"> 常见问题 </div> </div> <div class="course-side"> <div class="teacher-info"> <h4 class="side-title"><span>授课老师</span></h4> <div class="teacher-content"> <div class="cont1"> <img :src="course.teacher.image"> <div class="name"> <p class="teacher-name">{{course.teacher.name}}</p> <p class="teacher-title">{{course.teacher.title}} {{course.teacher.signature}}</p> </div> </div> <p class="narrative" >{{course.teacher.brief}}</p> </div> </div> </div> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" // 1.加载播放器组件 import {videoPlayer} from 'vue-video-player'; export default { name: "Detail", data(){ return { course_id: 0, // 课程ID course: { // 课程信息 teacher:{} }, chapter_list: [], // 章节列表 tabIndex:2, // 当前选项卡显示的下标 playerOptions:{ playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度 autoplay: false, //如果true,则自动播放 muted: false, // 默认情况下将会消除任何音频。 loop: false, // 循环播放 preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持) language: 'zh-CN', aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3") fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。 sources: [{ // 播放资源和资源格式 type: "video/mp4", src: "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4" //你的视频地址(必填) }], poster: "../static/image/course-cover.jpeg", //视频封面图 width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度 notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。 } } }, filters:{ padding0(data){ if(data>=10){ return data; } return "0" + data } }, created(){ // 接受地址路径参数 // console.log( '路径参数: id=',this.$route.params.id ); // 接受地址的查询字符串参数 // console.log( '查询字符串: uid=', this.$route.query.uid ); this.course_id = this.$route.params.id; this.get_course(); this.get_chapter(); }, methods: { onPlayerPlay(){ alert("开始播放视频,关闭广告"); }, onPlayerPause(){ alert("暂停播放广告"); }, get_course(){ // 获取课程信息 this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{ this.course = response.data; console.log(response.data.course_video); this.playerOptions.sources[0].src = response.data.course_video; // 进行活动的倒计时 let t = setInterval(()=>{ if(this.course.has_time > 0){ this.course.has_time-=1; }else{ clearInterval(t); } },1000) }).catch(error=>{ let self = this; this.$alert("无法获取当前课程信息,请联系客服工作人员","路飞学城",{ callback(){ self.$router.back(); // 等同于 self.$router.go(-1); } }); }) }, get_chapter(){ // 获取课程相关的章节课时信息 this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{ params:{ "course": this.course_id, } }).then(response=>{ this.chapter_list = response.data; }).catch(error=>{ this.$message.info("没有获取到当前课程的章节信息"); }); }, url_format(data){ console.log(data); while( data.search('="/media') != -1 ){ data = data.replace('="/media',`="${this.$settings.Host}/media`); } return data; }, add_cart(){ // 添加商品课程到购物车中 // 1. 先判断用户是否登录 let user_token = this.$settings.check_user_login(); if( !user_token ){ let self = this; this.$confirm('对不起,您尚未登录,请登录后继续购买, 是否继续?', '路飞学城', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击确定按钮 self.$router.push("/login"); }).catch(() => { }); return ; } this.$axios.post(`${this.$settings.Host}/cart/`,{ course_id: this.course_id },{ headers:{ // 访问需要登录认证权限的api接口必须附带token到请求头中 "Authorization": "jwt " + user_token, } }).then(response=>{ this.$message.info(response.data.message); }).catch(error=>{ if(error.response.status == 401){ // 没有登录或者登录超时 let self = this; this.$confirm('对不起,您尚未登录,请登录后继续购买, 是否继续?', '路飞学城', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 点击确定按钮 self.$router.push("/login"); }).catch(() => { }); return ; }else{ this.$message.error("购物车添加商品失败!请联系客服工作人员!"); } }); } }, components:{ Header, Footer, videoPlayer, // 注册播放器组件到当前页面中 } } </script> <style scoped> .main{ background: #fff; padding-top: 30px; } .course-info{ width: 1200px; margin: 0 auto; overflow: hidden; } .wrap-left{ float: left; width: 690px; height: 388px; background-color: #000; } .wrap-right{ float: left; position: relative; height: 388px; } .course-name{ font-size: 20px; color: #333; padding: 10px 23px; letter-spacing: .45px; } .data{ padding-left: 23px; padding-right: 23px; padding-bottom: 16px; font-size: 14px; color: #9b9b9b; } .sale-time{ width: 464px; background: #fa6240; font-size: 14px; color: #4a4a4a; padding: 10px 23px; overflow: hidden; } .sale-type { font-size: 16px; color: #fff; letter-spacing: .36px; float: left; } .sale-time .expire{ font-size: 14px; color: #fff; float: right; } .sale-time .expire .second{ width: 24px; display: inline-block; background: #fafafa; color: #5e5e5e; padding: 6px 0; text-align: center; } .course-price{ background: #fff; font-size: 14px; color: #4a4a4a; padding: 5px 23px; } .discount{ font-size: 26px; color: #fa6240; margin-left: 10px; display: inline-block; margin-bottom: -5px; } .original{ font-size: 14px; color: #9b9b9b; margin-left: 10px; text-decoration: line-through; } .buy{ width: 464px; padding: 0px 23px; position: absolute; left: 0; bottom: 20px; overflow: hidden; } .buy .buy-btn{ float: left; } .buy .buy-now{ width: 125px; height: 40px; border: 0; background: #ffc210; border-radius: 4px; color: #fff; cursor: pointer; margin-right: 15px; outline: none; } .buy .free{ width: 125px; height: 40px; border-radius: 4px; cursor: pointer; margin-right: 15px; background: #fff; color: #ffc210; border: 1px solid #ffc210; } .add-cart{ float: right; font-size: 14px; color: #ffc210; text-align: center; cursor: pointer; margin-top: 10px; } .add-cart img{ width: 20px; height: 18px; margin-right: 7px; vertical-align: middle; } .course-tab{ width: 100%; background: #fff; margin-bottom: 30px; box-shadow: 0 2px 4px 0 #f0f0f0; } .course-tab .tab-list{ width: 1200px; margin: auto; color: #4a4a4a; overflow: hidden; } .tab-list li{ float: left; margin-right: 15px; padding: 26px 20px 16px; font-size: 17px; cursor: pointer; } .tab-list .active{ color: #ffc210; border-bottom: 2px solid #ffc210; } .tab-list .free{ color: #fb7c55; } .course-content{ width: 1200px; margin: 0 auto; background: #FAFAFA; overflow: hidden; padding-bottom: 40px; } .course-tab-list{ width: 880px; height: auto; padding: 20px; background: #fff; float: left; box-sizing: border-box; overflow: hidden; position: relative; box-shadow: 0 2px 4px 0 #f0f0f0; } .tab-item{ width: 880px; background: #fff; padding-bottom: 20px; box-shadow: 0 2px 4px 0 #f0f0f0; } .tab-item-title{ justify-content: space-between; padding: 25px 20px 11px; border-radius: 4px; margin-bottom: 20px; border-bottom: 1px solid #333; border-bottom-color: rgba(51,51,51,.05); overflow: hidden; } .chapter{ font-size: 17px; color: #4a4a4a; float: left; } .chapter-length{ float: right; font-size: 14px; color: #9b9b9b; letter-spacing: .19px; } .chapter-title{ font-size: 16px; color: #4a4a4a; letter-spacing: .26px; padding: 12px; background: #eee; border-radius: 2px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; } .chapter-title img{ width: 18px; height: 18px; margin-right: 7px; vertical-align: middle; } .lesson-list{ padding:0 20px; } .lesson-list .lesson-item{ padding: 15px 20px 15px 36px; cursor: pointer; justify-content: space-between; position: relative; overflow: hidden; } .lesson-item .name{ font-size: 14px; color: #666; float: left; } .lesson-item .index{ margin-right: 5px; } .lesson-item .free{ font-size: 12px; color: #fff; letter-spacing: .19px; background: #ffc210; border-radius: 100px; padding: 1px 9px; margin-left: 10px; } .lesson-item .time{ font-size: 14px; color: #666; letter-spacing: .23px; opacity: 1; transition: all .15s ease-in-out; float: right; } .lesson-item .time img{ width: 18px; height: 18px; margin-left: 15px; vertical-align: text-bottom; } .lesson-item .try{ width: 86px; height: 28px; background: #ffc210; border-radius: 4px; font-size: 14px; color: #fff; position: absolute; right: 20px; top: 10px; opacity: 0; transition: all .2s ease-in-out; cursor: pointer; outline: none; border: none; } .lesson-item:hover{ background: #fcf7ef; box-shadow: 0 0 0 0 #f3f3f3; } .lesson-item:hover .name{ color: #333; } .lesson-item:hover .try{ opacity: 1; } .course-side{ width: 300px; height: auto; margin-left: 20px; float: right; } .teacher-info{ background: #fff; margin-bottom: 20px; box-shadow: 0 2px 4px 0 #f0f0f0; } .side-title{ font-weight: normal; font-size: 17px; color: #4a4a4a; padding: 18px 14px; border-bottom: 1px solid #333; border-bottom-color: rgba(51,51,51,.05); } .side-title span{ display: inline-block; border-left: 2px solid #ffc210; padding-left: 12px; } .teacher-content{ padding: 30px 20px; box-sizing: border-box; } .teacher-content .cont1{ margin-bottom: 12px; overflow: hidden; } .teacher-content .cont1 img{ width: 54px; height: 54px; margin-right: 12px; float: left; } .teacher-content .cont1 .name{ float: right; } .teacher-content .cont1 .teacher-name{ width: 188px; font-size: 16px; color: #4a4a4a; padding-bottom: 4px; } .teacher-content .cont1 .teacher-title{ width: 188px; font-size: 13px; color: #9b9b9b; white-space: nowrap; } .teacher-content .narrative{ font-size: 14px; color: #666; line-height: 24px; } </style>
courses/views.py,代码:
def list(self, request): """购物车商品列表""" # 获取登录用户id user_id = request.user.id # 1. 打开redis链接 redis_conn = get_redis_connection("cart") # 2. 从redis读取购物车信息[[id,course_img,name,price]]和勾选商品集合 cart_dict_bytes = redis_conn.hgetall("cart_%s" % user_id) print(cart_dict_bytes) # {b'1': b'0', b'3': b'0', b'4': b'0'} selected_set_bytes = redis_conn.smembers("selected_%s" % user_id) # 3. 把勾选状态信息和商品信息组合成一个数据列表 data = [] for course_id_bytes, expire_bytes in cart_dict_bytes.items(): course_id = course_id_bytes.decode() expire = expire_bytes.decode() try: course = Course.objects.get(pk=course_id, is_show=True, is_delete=False) except: course = None if course: data.append({ "course_id": course.id, "course_name": course.name, "course_img": course.course_img.url, "price": course.discount_price( course.real_price(expire) ), # 真实价格 "expire": expire, # 用户购物车中的有效期选项 "expire_list": course.expire_list, # 有效期选项列表 "selected": course_id_bytes in selected_set_bytes, # 勾选状态 }) # 4. 返回数据 return Response(data)
courses/models.py,代码:
@property def expire_list(self): """课程有效期选项列表""" data_list = self.courseexpire.all() data = [] # 如果课程模型中,有存在永久价格,则在有效期选项列表中,新增永久有效的选项 if self.price > 0: data.append({ "time": "0", "text": "永久有效", "price": self.discount_price(self.price) }) for item in data_list: data.append({ "time": item.timer, "text": item.text, "price": self.discount_price(item.price) }) return data
完成购物车功能以后,那么我们可以让用户点击“去结算”按钮时,在后端提供一个查询勾选商品的API接口给客户端,展示数据在结算页面中。
def get_selected_course(self, request): """获取已经勾选的商品信息""" # 获取登录用户id user_id = 1 # request.user.id # 从redis提取购物车信息 # 连接redis redis = get_redis_connection("cart") course_set = redis.smembers("selected_%s" % user_id) cart_list = redis.hgetall("cart_%s" % user_id) # 对购物车信息进行筛选 # 计算所有商品的总价 total_price = 0 # 创建一个空的列表用于保存商品课程列表 course_list = [] for course_id_bytes in course_set: course_expire_bytes = cart_list[course_id_bytes] expire_time = course_expire_bytes.decode() course_id = int(course_id_bytes.decode()) try: course = Course.objects.get(pk=course_id) except: return Response({"message":"对不起,商品课程不存在!"}, status=status.HTTP_400_BAD_REQUEST) if expire_time == "0": expire_text = "永久有效" else: try: # 获取当前本次用户购买商品的有效期选项文本格式 expire_obj = course.courseexpire.get(timer=expire_time) expire_text = expire_obj.text except: return Response({"message": "对不起,商品课程不存在!"}, status=status.HTTP_400_BAD_REQUEST) # 对购物车中信息进行补充和完善 course_list.append({ "course_id": course.id, "course_img": course.course_img.url, "name": course.name, "discount_name": course.discount_name, "expire": expire_text, "price": course.real_price(expire_time), # 没有打折的价格 "discount_price": course.discount_price(course.real_price( expire_time )), }) return Response(course_list)
路由,代码:
from django.urls import path from . import views urlpatterns = [ path("", views.CartAPIViewSet.as_view({ "post":"add_cart", "get":"list", "patch":"change_select_status", "delete":"delete", "put": "change_course_expire", }) ), path("selected/", views.CartAPIViewSet.as_view({"get":"get_selected_course"})), ]
前端完成页面从购物车页面跳转到结算页面。
<span class="goto_pay"><router-link to="/buy">去结算</router-link></span>
增加显示结算页面的路由和组件代码:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) // @ 表示src目录 import Order from "@/components/Order" import AliPlayer from "@/components/AliPlayer" export default new Router({ mode:"history", routes: [ { path: '/buy', name: 'Order', component: Order, }, { path: '/aliPlayer', name: 'AliPlayer', component: AliPlayer, } ] })
components/Order.vue,页面代码:
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共1门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img src="/static/image/course-cover.jpeg" alt=""> <span class="course-title">redis入门课程<br>限时免费</span> </el-col> <el-col :span="8"><span>永久有效</span></el-col> <el-col :span="4" class="course-price">¥0.0<br><span>原价 ¥99.00</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有3张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">99.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="{disable:total_price<item.coupon.condition || check_duration(item.start_time,item.coupon.duration),active:coupon==item.id}" @click="select_coupon(item)" :key="key" v-for="item,key in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-condition" v-if="item.coupon.condition>0">满{{item.coupon.condition}}元可以使用</p> <p class="coupon-time start_time">开始时间:{{item.start_time.replace("T"," ")}}</p> <p class="coupon-time end_time">过期时间:{{get_end_time(item.start_time,item.coupon.duration)}}</p> </li> </ul> <div class="no-coupon" v-else> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_real_price}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="payhander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, methods: { payhander(){ } } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>
在组件中获取购物车勾选商品的数据
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row :key="key" v-for="course,key in course_list"> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="$settings.Host+course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.discount_price}}<br><span>原价 ¥{{course.price}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有3张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">99.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="{disable:total_price<item.coupon.condition || check_duration(item.start_time,item.coupon.duration),active:coupon==item.id}" @click="select_coupon(item)" :key="key" v-for="item,key in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-condition" v-if="item.coupon.condition>0">满{{item.coupon.condition}}元可以使用</p> <p class="coupon-time start_time">开始时间:{{item.start_time.replace("T"," ")}}</p> <p class="coupon-time end_time">过期时间:{{get_end_time(item.start_time,item.coupon.duration)}}</p> </li> </ul> <div class="no-coupon" v-else> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_real_price}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="payhander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_token: "", // 用户的jwt token course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, created(){ this.user_token = this.$settings.check_user_login(); this.get_selected_course(); }, methods: { get_selected_course(){ // 获取勾选的商品信息 this.$axios.get(`${this.$settings.Host}/cart/selected/`,{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; }).catch(error=>{ this.$message.error("无法获取商品信息!"); }); }, payhander(){ } } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>
所以为了方便开发,和以后项目的维护,我们再次创建子应用orders来完成接下来的订单和订单支付功能。
cd luffyapi/apps
python ../../manage.py startapp orders
注册子应用,settings/dev.py,代码:
INSTALLED_APPS = [ # 子应用 。。。 'orders', ]
订单模型
订单模型分析:
订单模型: 优惠券ID,积分使用数量,订单总价格,实付价格,订单标题,订单支付时间,用户ID,支付状态,订单有效时间,订单号,支付方式,
订单详情模型: 商品ID,商品原价,商品实价,商品有效期,商品优惠方式
商品购买记录:
优惠券模型:
积分流水模型:
为什么有订单号?
原因是支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。
所以订单号是支付平台那边强制要求在支付时提供给平台的。
订单模型的代码:
from django.db import models from luffyapi.utils.models import BaseModel from users.models import User from courses.models import Course class Order(BaseModel): """订单基本信息模型""" status_choices = ( (0, '未支付'), (1, '已支付'), (2, '已取消'), (3, '超时取消'), ) pay_choices = ( (1, '支付宝'), (2, '微信支付'), ) order_title = models.CharField(max_length=150,verbose_name="订单标题") total_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="订单总价", default=0) real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="实付金额", default=0) order_number = models.CharField(max_length=64,verbose_name="订单号") order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态") pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式") credit = models.IntegerField(default=0, verbose_name="使用的积分数量") coupon = models.IntegerField(default=0, verbose_name="用户优惠券ID") order_desc = models.TextField(max_length=500, verbose_name="订单描述") pay_time = models.DateTimeField(null=True, verbose_name="支付时间") user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING,verbose_name="下单用户") class Meta: db_table="ly_order" verbose_name= "订单记录" verbose_name_plural= "订单记录" def __str__(self): return "%s,总价: %s,实付: %s" % (self.order_title, self.total_price, self.real_price) class OrderDetail(BaseModel): """ 订单详情 """ order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单") course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程") expire = models.IntegerField(default='-1', verbose_name="有效期周期") price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价") real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价") discount_name = models.CharField(max_length=120,default="",verbose_name="优惠类型") class Meta: db_table="ly_order_detail" verbose_name= "订单详情" verbose_name_plural= "订单详情" def __str__(self): return "%s" % (self.course.name)
数据迁移:
python manage.py makemigrations
python manage.py migrate
在当前子应用下创建adminx.py,代码:
import xadmin from .models import Order class OrderModelAdmin(object): """订单模型管理类""" pass xadmin.site.register(Order, OrderModelAdmin) from .models import OrderDetail class OrderDetailModelAdmin(object): """订单详情模型管理类""" pass xadmin.site.register(OrderDetail, OrderDetailModelAdmin)
例子:
from rest_framework import serializers from .models import Order,OrderDetail from datetime import datetime import random from django.db import transaction from django_redis import get_redis_connection from courses.models import Course class OrderModelSerializer(serializers.ModelSerializer): """订单序列化器""" class Meta: model = Order fields = [ "id","order_title", "total_price","real_price","order_number","order_status", "pay_type","pay_time", "credit","coupon", ] extra_kwargs = { "id":{ "read_only":True,}, "order_title":{"read_only":True,}, "total_price":{"read_only":True,}, "real_price":{"read_only":True,}, "order_number":{"read_only":True,}, "order_status":{"read_only":True,}, "pay_time":{"read_only":True,}, "pay_type":{"required":True,}, "credit":{"required":True,"min_value":0}, "coupon":{"required":True,"min_value":0}, } def create(self, validated_data): """生成订单""" # 接受客户端提交的数据 pay_type = validated_data.get("pay_type") credit = validated_data.get("credit", 0) coupon = validated_data.get("coupon",0) # 生成必要参数 user_id = 1 # todo 回头我们学习怎么在序列化器中获取视图中的数据 order_title = "路飞学城课程购买" order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%06d" % user_id) + ("%04d" % random.randint(0, 9999)) order_status = 0 # 开始编写保存订单信息和订单详情模型的代码
上面我们使用了redis的事务操作保证数据的原子性。但是mysql里面我们也是在进行多表操作,所以也是需要使用事务来保证数据的原子性的。
事务: 在完成一个整体功能时,操作到了多个表数据,或者同一个表的多条记录,如果要保证这些SQL语句操作作为一个整体保存到数据库中,那么可以使用事务(transation), 事务具有4个特性,5个隔离等级 四个特性:一致性,原子性,隔离性,持久性 # 隔离性:两个事务的隔离性,隔离性的修改可以通过数据库的配置文件my.ini进行修改 五个隔离级别: 串行隔离,可重复读,已提交读,未提交读,没有隔离级别 原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation)[事务隔离级别->幻读,脏读] 持久性(Durability) 在mysql中有专门的SQl语句来完成事务的操作,事务操作一般有3个步骤: 设置事务开始 transation start; 事务的处理[增删改] 设置事务的回滚或者提交 rollback / commit; 在 django等web框架中,只要ORM模型,一般都会实现了事务操作封装 所以在django中我们可以直接使用ORM模型提供的事务操作方法即可完成事务的操作
django的事务操作方法主要通过 django.db.transation模块完成的。
启用事务用法1:
from django.db import transaction from rest_framework.views import APIView class OrderAPIView(APIView): @transaction.atomic # 开启事务,当方法执行完成以后,自动提交事务 def post(self,request): ....
启用事务用法2:
from django.db import transaction from rest_framework.views import APIView class OrderAPIView(APIView): def post(self,request): .... with transation.atomic(): # 开启事务,当with语句执行完成以后,自动提交事务 # 数据库操作
在使用事务过程中, 有时候会出现异常,当出现异常的时候,我们需要让程序停止下来,同时需要回滚事务。
from django.db import transaction from rest_framework.views import APIView class OrderAPIView(APIView): def post(self,request): .... with transation.atomic(): # 设置事务回滚的标记点 sid1 = transation.savepoint() .... # 增删改等数据库操作 try: .... except: transation.savepoint_rallback(sid1)
视图代码:
from rest_framework import serializers from .models import Order,OrderDetail from datetime import datetime import random from django.db import transaction from django_redis import get_redis_connection from courses.models import Course class OrderModelSerializer(serializers.ModelSerializer): """订单序列化器""" class Meta: model = Order fields = [ "id","order_title", "total_price","real_price","order_number","order_status", "pay_type","pay_time", "credit","coupon", ] extra_kwargs = { "id":{ "read_only":True,}, "order_title":{"read_only":True,}, "total_price":{"read_only":True,}, "real_price":{"read_only":True,}, "order_number":{"read_only":True,}, "order_status":{"read_only":True,}, "pay_time":{"read_only":True,}, "pay_type":{"required":True,}, "credit":{"required":True,"min_value":0}, "coupon":{"required":True,"min_value":0}, } def create(self, validated_data): """生成订单""" # 接受客户端提交的数据 pay_type = validated_data.get("pay_type") credit = validated_data.get("credit", 0) coupon = validated_data.get("coupon",0) # 生成必要参数 user_id = 1 # todo 回头我们学习怎么在序列化器中获取视图中的数据 order_title = "路飞学城课程购买" order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%06d" % user_id) + ("%04d" % random.randint(0, 9999)) order_status = 0 with transaction.atomic(): # 设置事务回滚点 sid = transaction.savepoint() # 保存订单基本信息 order = Order.objects.create({ "order_title": order_title, "total_price": 0, # 订单总价格 "real_price": 0, # 实付价格 "order_number": order_number, "order_status": order_status, "pay_type": pay_type, "credit": credit, "coupon": coupon, "order_desc": "", "user_id": user_id, }) # 保存订单相关商品信息 redis_conn = get_redis_connection("cart") # 从购物车中提取勾选的商品信息 cart_list = redis_conn.hgetall("cart_%s" % user_id) course_set = redis_conn.smembers("selected_%s" % user_id) # 订单总价,默认为0,在循环中逐个加上课程的价格 total_price = 0 for course_id_bytes in course_set: """在循环中把每一个勾选商品同步到订单详情记录中""" course_expire_bytes = cart_list[course_id_bytes] expire = course_expire_bytes.decode() course_id = int( course_id_bytes.decode() ) try: course = Course.objects.get(pk=course_id) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("对不起,商品课程不存在!") # 提取课程的有效期选项对应的原价 if expire > 0: course_expire = course.courseexpire.get(timer=expire) price = course_expire.price expire_text = course_expire.text else: price = course.price expire_text = "永久有效" # 保存商品信息到OrderDetail中 try: order_detail = OrderDetail.objects.create( order=order, course=course, expire= int(expire_time), # 有效期选项的时间 price = Decimal(price), real_price = Decimal( course.discount_price(price) ), discount_name = course.discount_name, ) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") # 计算订单总价 total_price += order_detail.real_price try: # 保存总价到数据库中 order.total_price = total_price # 将来这里补充关于积分和优惠券的处理 order.real_price = total_price order.save() except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") return order
一旦购物车中选中的商品被转移到了购物车中,则购物车中原来被选中的商品是否要删除?
from rest_framework import serializers from .models import Order,OrderDetail from datetime import datetime import random from django.db import transaction from django_redis import get_redis_connection from courses.models import Course class OrderModelSerializer(serializers.ModelSerializer): """订单序列化器""" class Meta: model = Order fields = [ "id","order_title", "total_price","real_price","order_number","order_status", "pay_type","pay_time", "credit","coupon", ] extra_kwargs = { "id":{ "read_only":True,}, "order_title":{"read_only":True,}, "total_price":{"read_only":True,}, "real_price":{"read_only":True,}, "order_number":{"read_only":True,}, "order_status":{"read_only":True,}, "pay_time":{"read_only":True,}, "pay_type":{"required":True,}, "credit":{"required":True,"min_value":0}, "coupon":{"required":True,"min_value":0}, } def create(self, validated_data): """生成订单""" # 接受客户端提交的数据 pay_type = validated_data.get("pay_type") credit = validated_data.get("credit", 0) coupon = validated_data.get("coupon",0) # 生成必要参数 user_id = 1 # todo 回头我们学习怎么在序列化器中获取视图中的数据 order_title = "路飞学城课程购买" order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%06d" % user_id) + ("%04d" % random.randint(0, 9999)) order_status = 0 with transaction.atomic(): # 设置事务回滚点 sid = transaction.savepoint() # 保存订单基本信息 order = Order.objects.create(**{ "order_title": order_title, "total_price": 0, # 订单总价格 "real_price": 0, # 实付价格 "order_number": order_number, "order_status": order_status, "pay_type": pay_type, "credit": credit, "coupon": coupon, "order_desc": "", "user_id": user_id, }) # 保存订单相关商品信息 redis_conn = get_redis_connection("cart") # 从购物车中提取勾选的商品信息 cart_list = redis_conn.hgetall("cart_%s" % user_id) course_set = redis_conn.smembers("selected_%s" % user_id) if course_set is None: transaction.savepoint_rollback(sid) return serializers.ValidationError("对不起,购物车中没有商品,赶紧去买买买!") # 订单总价,默认为0,在循环中逐个加上课程的价格 total_price = 0 # 开启redis的事务操作[管道操作] pipe = redis_conn.pipeline() pipe.multi() for course_id_bytes in course_set: """在循环中把每一个勾选商品同步到订单详情记录中""" course_expire_bytes = cart_list[course_id_bytes] expire = course_expire_bytes.decode() course_id = int( course_id_bytes.decode() ) try: course = Course.objects.get(pk=course_id) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("对不起,商品课程不存在!") # 提取课程的有效期选项对应的原价 if int(expire) > 0: course_expire = course.courseexpire.get(timer=expire) price = course_expire.price expire_time = course_expire.timer else: price = course.price expire_time = 0 # 保存商品信息到OrderDetail中 from decimal import Decimal try: order_detail = OrderDetail.objects.create( order=order, course=course, expire= int(expire_time), # 有效期选项的时间 price = Decimal(price), real_price = Decimal( course.discount_price(price) ), discount_name = course.discount_name, ) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") # 计算订单总价 total_price += order_detail.real_price # 从购物车中删除订单对应的商品 pipe.hdel("cart_%s" % user_id, course.id) pipe.srem("selected_%s" % user_id, course.id) # 执行redis的管道操作 pipe.execute() try: # 保存总价到数据库中 order.total_price = total_price # 将来这里补充关于积分和优惠券的处理 order.real_price = total_price order.save() except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") return order
提供api视图并设置路由
from django.urls import path,re_path from . import views urlpatterns = [ path("",views.OrderCreateAPIView.as_view()), ]
总路由,代码:
path('orders/', include("orders.urls") ),
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row :key="key" v-for="course,key in course_list"> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="$settings.Host+course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.discount_price}}<br><span>原价 ¥{{course.price.toFixed(2)}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有3张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">{{total_price}}元</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="{disable:total_price<item.coupon.condition || check_duration(item.start_time,item.coupon.duration),active:coupon==item.id}" @click="select_coupon(item)" :key="key" v-for="item,key in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-condition" v-if="item.coupon.condition>0">满{{item.coupon.condition}}元可以使用</p> <p class="coupon-time start_time">开始时间:{{item.start_time.replace("T"," ")}}</p> <p class="coupon-time end_time">过期时间:{{get_end_time(item.start_time,item.coupon.duration)}}</p> </li> </ul> <div class="no-coupon" v-else> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_price}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="ordeHander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_token: "", // 用户的jwt token course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, created(){ this.user_token = this.$settings.check_user_login(); this.get_selected_course(); }, methods: { get_selected_course(){ // 获取勾选的商品信息 this.$axios.get(`${this.$settings.Host}/cart/selected/`,{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; let total_price = 0; for(let course of this.course_list){ total_price = total_price + parseFloat(course.discount_price) } this.total_price = total_price.toFixed(2); }).catch(error=>{ this.$message.error("无法获取商品信息!"); }); }, ordeHander(){ // 生成订单 this.$axios.post(`${this.$settings.Host}/orders/`,{ "pay_type": this.pay_type, "credit": 0, // 积分 "coupon": 0 // 优惠券 },{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 去支付 console.log(response.data); }).catch(error=>{ this.$message.error("生成订单失败!"); }) }, payhander(){ // 去支付 }, } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>
完善后端生成订单时,在序列化器中要接受客户端用户的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
服务端序列化器代码:
from rest_framework import serializers from .models import Order,OrderDetail from datetime import datetime import random from django.db import transaction from django_redis import get_redis_connection from courses.models import Course class OrderModelSerializer(serializers.ModelSerializer): """订单序列化器""" class Meta: model = Order fields = [ "id","order_title", "total_price","real_price","order_number","order_status", "pay_type","pay_time", "credit","coupon", ] extra_kwargs = { "id":{ "read_only":True,}, "order_title":{"read_only":True,}, "total_price":{"read_only":True,}, "real_price":{"read_only":True,}, "order_number":{"read_only":True,}, "order_status":{"read_only":True,}, "pay_time":{"read_only":True,}, "pay_type":{"required":True,}, "credit":{"required":True,"min_value":0}, "coupon":{"required":True,"min_value":0}, } def create(self, validated_data): """生成订单""" # 接受客户端提交的数据 pay_type = validated_data.get("pay_type") credit = validated_data.get("credit", 0) coupon = validated_data.get("coupon",0) # 生成必要参数 user_id = self.context["request"].user.id # 获取来自于视图的request对象,通过request对象获取本次操作的用户id order_title = "路飞学城课程购买" order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%06d" % user_id) + ("%04d" % random.randint(0, 9999)) order_status = 0 with transaction.atomic(): # 设置事务回滚点 sid = transaction.savepoint() # 保存订单基本信息 order = Order.objects.create(**{ "order_title": order_title, "total_price": 0, # 订单总价格 "real_price": 0, # 实付价格 "order_number": order_number, "order_status": order_status, "pay_type": pay_type, "credit": credit, "coupon": coupon, "order_desc": "", "user_id": user_id, }) # 保存订单相关商品信息 redis_conn = get_redis_connection("cart") # 从购物车中提取勾选的商品信息 cart_list = redis_conn.hgetall("cart_%s" % user_id) course_set = redis_conn.smembers("selected_%s" % user_id) if course_set is None: transaction.savepoint_rollback(sid) return serializers.ValidationError("对不起,购物车中没有商品,赶紧去买买买!") # 订单总价,默认为0,在循环中逐个加上课程的价格 total_price = 0 # 开启redis的事务操作[管道操作] pipe = redis_conn.pipeline() pipe.multi() for course_id_bytes in course_set: """在循环中把每一个勾选商品同步到订单详情记录中""" course_expire_bytes = cart_list[course_id_bytes] expire = course_expire_bytes.decode() course_id = int( course_id_bytes.decode() ) try: course = Course.objects.get(pk=course_id) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("对不起,商品课程不存在!") # 提取课程的有效期选项对应的原价 if int(expire) > 0: course_expire = course.courseexpire.get(timer=expire) price = course_expire.price expire_time = course_expire.timer else: price = course.price expire_time = 0 # 保存商品信息到OrderDetail中 from decimal import Decimal try: order_detail = OrderDetail.objects.create( order=order, course=course, expire= int(expire_time), # 有效期选项的时间 price = Decimal(price), real_price = Decimal( course.discount_price(price) ), discount_name = course.discount_name, ) except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") # 计算订单总价 total_price += order_detail.real_price # 从购物车中删除订单对应的商品 pipe.hdel("cart_%s" % user_id, course.id) pipe.srem("selected_%s" % user_id, course.id) # 执行redis的管道操作 pipe.execute() try: # 保存总价到数据库中 order.total_price = total_price # 将来这里补充关于积分和优惠券的处理 order.real_price = total_price order.save() except: transaction.savepoint_rollback(sid) return serializers.ValidationError("生成订单失败!") return order
前段代码调整
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row :key="key" v-for="course,key in course_list"> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="$settings.Host+course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.discount_price}}<br><span>原价 ¥{{course.price.toFixed(2)}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有3张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">{{total_price}}元</span></p> </div> <div id="collapseOne" v-if="use_coupon"> <ul class="coupon-list"> <li class="coupon-item disable"> <p class="coupon-name">五十元优惠券</p> <p class="coupon-condition">满100元可以使用</p> <p class="coupon-time start_time">开始时间:2019-12-12 00:00:00</p> <p class="coupon-time end_time">过期时间:2019-12-12 00:00:00</p> </li> <li class="coupon-item active"> <p class="coupon-name">五十元优惠券</p> <p class="coupon-condition">满100元可以使用</p> <p class="coupon-time start_time">开始时间:2019-12-12 00:00:00</p> <p class="coupon-time end_time">过期时间:2019-12-12 00:00:00</p> </li> </ul> <div class="no-coupon" v-if="false"> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>0.00元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_price}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="ordeHander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_token: "", // 用户的jwt token course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [1,2,3], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, created(){ this.user_token = this.$settings.check_user_login(); this.get_selected_course(); }, methods: { get_selected_course(){ // 获取勾选的商品信息 this.$axios.get(`${this.$settings.Host}/cart/selected/`,{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; let total_price = 0; for(let course of this.course_list){ total_price = total_price + parseFloat(course.discount_price) } this.total_price = total_price.toFixed(2); }).catch(error=>{ this.$message.error("无法获取商品信息!"); }); }, ordeHander(){ // 生成订单 this.$axios.post(`${this.$settings.Host}/orders/`,{ "pay_type": this.pay_type, "credit": 0, // 积分 "coupon": 0 // 优惠券 },{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 去支付 console.log(response.data); }).catch(error=>{ this.$message.error("生成订单失败!"); }) }, payhander(){ // 去支付 }, } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>
优惠券
创建一个coupon子应用.
cd luffyapi/apps
python ../../manage.py startapp coupon
注册子应用
INSTALLED_APPS = [ # 子应用 。。。 'coupon', ]
模型分析:
from django.db import models from luffyapi.utils.models import BaseModel # Create your models here. class Coupon(BaseModel): """优惠券""" coupon_choices = ( (1, '减免优惠'), (2, '折扣优惠'), ) name = models.CharField(max_length=32, verbose_name="优惠券标题") coupon_type = models.SmallIntegerField(choices=coupon_choices, default=1, verbose_name="优惠券类型") duration = models.IntegerField(verbose_name="优惠券有效期", default=7, help_text="优惠券有效期(天)") condition = models.IntegerField(blank=True, default=0, verbose_name="满足使用优惠券的价格条件") 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) from users.models import User 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)
数据迁移
cd ../../
python manage.py makemigrations
python manage.py migrate
注册到xadmin,添加测试数据[1.添加优惠券,给用户发放优惠券]
import xadmin from .models import Coupon class CouponModelAdmin(object): """优惠券模型管理类""" list_display = ["name","coupon_type","duration"] 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)
添加测试数据
序列化器,代码:
from rest_framework import serializers from .models import UserCoupon,Coupon class CouponModelSerializer(serializers.ModelSerializer): """优惠券序列化器""" class Meta: model = Coupon fields = ["name","coupon_type","duration","condition","sale"] class UserCouponModelSerializer(serializers.ModelSerializer): """我的优惠券序列化器""" coupon = CouponModelSerializer() class Meta: model = UserCoupon fields = ["id","start_time","coupon"]
视图,代码:
# Create your views here. from rest_framework.generics import ListAPIView from .models import UserCoupon from .serializers import UserCouponModelSerializer from django_filters.rest_framework import DjangoFilterBackend from rest_framework.permissions import IsAuthenticated class UserCouponListAPIView(ListAPIView): queryset = UserCoupon.objects.filter(is_delete=False,is_show=True,is_use=False) serializer_class = UserCouponModelSerializer permission_classes = [IsAuthenticated] filter_backends = [DjangoFilterBackend] filter_fields = ['user_id']
子应用路由,代码:
from django.urls import path,re_path from . import views urlpatterns = [ path("", views.UserCouponListAPIView.as_view()), ]
总路由,代码:
path('coupon/', include("coupon.urls")),
Order.vue
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item" :key="key" v-for="course,key in course_list"> <el-row> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.real_price}}<br><span>原价 ¥{{course.price}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有{{coupon_list.length}}张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">{{total_price}}元</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="coupon==item.id?'active':''" @click="coupon=item.id" :key="key" v-for="item,key in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-time start_time">开始时间:{{item.start_time}}</p> <p class="coupon-time end_time">过期时间:{{item.start_time}}</p> </li> </ul> <div class="no-coupon" v-else> <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-if="use_credit"><span>可用0个已抵扣 ¥0</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>0元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay"><img src="../../static/image/alipay2.png" alt=""></span> <span class="alipay wechat"><img src="../../static/image/wechat.png" alt=""></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_price}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="payhander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_id: localStorage.user_id || sessionStorage.user_id, course_list: [], total_price: 0, use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID } }, components:{ Header, Footer, }, created(){ this.get_cart_goods(); this.get_user_coupon(); }, methods: { get_user_coupon(){ // 获取用户的优惠券 let token = localStorage.token || sessionStorage.token; if( !token ){ this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{ this.$router.push("/login/"); }); return false; // 阻止代码往下执行 } this.$axios.get(`${this.$settings.Host}/coupon/`,{ params:{ user_id: this.user_id, }, headers:{ "Authorization":"jwt " + token, } }).then(response=>{ this.coupon_list = response.data }).catch(error=>{ console.log(error.response); }) }, get_cart_goods(){ // 获取用户勾选的商品 let token = localStorage.token || sessionStorage.token; if( !token ){ this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{ this.$router.push("/login/"); }); return false; // 阻止代码往下执行 } // 获取购物车的勾选商品信息 this.$axios.get(`${this.$settings.Host}/cart/buy/`,{ headers:{ "Authorization":"jwt " + token, } }).then(response=>{ this.course_list = response.data.course_list; this.total_price = response.data.total_price; }).catch(error=>{ console.log(error.response); }); }, payhander(){ let token = localStorage.token || sessionStorage.token; // 立即支付,发送请求到后端生成订单,并根据订单信息生成支付链接地址,让用户可以跳转到第三方支付平台进行支付 // 注意,现有的电脑网站支付,不同的第三方支付平台,服务端操作步骤几乎一致,但是前端处理会有所不同! // 京东支付,支付宝,会让后端生成一个支付地址,用户点击支付地址,来到支付平台进行支付 // 微信支付,会让后端生成一个包含了支付地址的二维码,用户需要使用手机扫码进行支付! this.$axios.post(`${this.$settings.Host}/orders/`,{ pay_type:1, use_credit: false, credit: 0, use_coupon: false },{ headers:{ "Authorization":"jwt " + token, } }).then(response=>{ console.log(response.data); }).catch(error=>{ console.log(error.response); }) } } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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: #fa6060; cursor: pointer; } .coupon-list .active{ background-color: #fa0000; } .coupon-name{ color: #fff; font-size: 24px; text-align: center; } .coupon-time{ text-align: left; color: #fff; font-size: 12px; } </style>
前端用户勾选使用优惠券以后,会自动调整订单实付价格
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row :key="key" v-for="course,key in course_list"> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="$settings.Host+course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.discount_price}}<br><span>原价 ¥{{course.price.toFixed(2)}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有{{coupon_list.length}}张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">{{total_price.toFixed(2)}}元</span></p> </div> <div id="collapseOne" v-if="use_coupon"> <ul class="coupon-list"> <li class="coupon-item" :class="coupon==item.id?'active':''" @click="coupon=item.id" v-for="item in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-condition">满{{item.coupon.condition}}元可以使用</p> <p class="coupon-time start_time">开始时间:{{item.start_time.replace("T"," ")}}</p> <p class="coupon-time end_time">过期时间:{{has_time(item.start_time,item.coupon.duration)}}</p> </li> </ul> <div class="no-coupon" v-if="false"> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>{{sale_price.toFixed(2)}}元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_real_price.toFixed(2)}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="ordeHander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_token: "", // 用户的jwt token course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 sale_price: 0, // 优惠券抵扣价格 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, created(){ this.user_token = this.$settings.check_user_login(); this.get_selected_course(); this.get_user_coupon(); }, watch:{ coupon(){ if(this.coupon > 0){ // 点选优惠券 for(let user_coupon of this.coupon_list){ if(user_coupon.id == this.coupon){ let sale = parseFloat(user_coupon.coupon.sale.substr(1)); if( user_coupon.coupon.sale[0]=="*" ){ // 折扣 this.total_real_price = this.total_price * sale; this.sale_price = this.total_price * (1-sale); }else{ // 假面 this.total_real_price = this.total_price - sale; this.sale_price = sale; } } } }else{ this.total_real_price = this.total_price; this.sale_price = 0; } }, use_coupon(){ // 是否使用了优惠券 if(!this.use_coupon){ this.coupon = 0; } } }, methods: { has_time(start_time, timer){ let start_timestamp = (new Date(start_time) - 0)/1000; let timer_timestamp = timer * 24 * 3600; let endtime_timestamp = start_timestamp + timer_timestamp; let endtime = new Date(endtime_timestamp * 1000); let year = endtime.getFullYear(); let month = endtime.getMonth()+1; month = month>=10?month:"0"+month; let date = endtime.getDate(); date = date >=10?date:"0"+date; let hour = endtime.getHours(); hour = hour >=10?hour:"0"+hour; let min = endtime.getMinutes(); min = min >= 10?min:"0"+min; let sec = endtime.getSeconds(); sec = sec >= 10?sec:"0"+sec; let time = `${year}-${month}-${date} ${hour}:${min}:${sec}`; return time; }, get_user_coupon(){ // 获取登录的用户 let user_id = localStorage.user_id || sessionStorage.user_id; // 获取当前用户的优惠券 this.$axios.get(`${this.$settings.Host}/coupon/`,{ params:{ user_id, // 等同于 user_id:user_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.coupon_list = response.data; }).catch(error=>{ this.$message.error("获取优惠券列表失败!"); }); }, get_selected_course(){ // 获取勾选的商品信息 this.$axios.get(`${this.$settings.Host}/cart/selected/`,{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; let total_price = 0; for(let course of this.course_list){ total_price = total_price + parseFloat(course.discount_price); } this.total_price = total_price; this.total_real_price = total_price; }).catch(error=>{ this.$message.error("无法获取商品信息!"); }); }, ordeHander(){ // 生成订单 this.$axios.post(`${this.$settings.Host}/orders/`,{ "pay_type": this.pay_type, "credit": 0, // 积分 "coupon": 0 // 优惠券 },{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 去支付 console.log(response.data); }).catch(error=>{ this.$message.error("生成订单失败!"); }) }, payhander(){ // 去支付 }, } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>
在前端Order.vue代码中,调整并加入对优惠券的起用时间判断
如果 当前时间 < 过期时间 , 则优惠券可以使用 如果 当前时间 > 起用时间 , 则优惠券可以使用 但是我们判断的时候,必须两个时间同时判断: 起用时间 < 当前时间 < 过期时间,优惠券可以使用
check_duration(start_time,duration){ // 判断优惠券是否已经过期 let now = new Date() - 0; let duration_end = new Date(start_time) - 0 + duration * 24 * 3600 * 1000; // 过期时间戳 let duration_start = new Date(start_time); // 起用时间戳 return duration_start > now || duration_end<=now; },
在前端Order.vue代码中,调整并加入对优惠券的起用时间判断
如果 当前时间 < 过期时间 , 则优惠券可以使用 如果 当前时间 > 起用时间 , 则优惠券可以使用 但是我们判断的时候,必须两个时间同时判断: 起用时间 < 当前时间 < 过期时间,优惠券可以使用
检查优惠券是否到有效起用时间:
check_duration(start_time,duration){ // 判断优惠券是否已经过期 let now = new Date() - 0; let duration_end = new Date(start_time) - 0 + duration * 24 * 3600 * 1000; // 过期时间戳 let duration_start = new Date(start_time); // 起用时间戳 return duration_start > now || duration_end<=now; },
在点击优惠券时,如果遇到禁用的优惠间,不纳入总价计算:
click_coupon(user_coupon){ let result = this.check_coupon(user_coupon.start_time, user_coupon.coupon.duration) || this.total_price < user_coupon.coupon.condition; if(!result){ this.coupon=user_coupon.id; } }
Order.vue组件,代码:
<template> <div class="cart"> <Header/> <div class="cart-info"> <h3 class="cart-top">购物车结算 <span>共{{course_list.length}}门课程</span></h3> <div class="cart-title"> <el-row> <el-col :span="2"> </el-col> <el-col :span="10">课程</el-col> <el-col :span="8">有效期</el-col> <el-col :span="4">价格</el-col> </el-row> </div> <div class="cart-item"> <el-row :key="key" v-for="course,key in course_list"> <el-col :span="2" class="checkbox"> </el-col> <el-col :span="10" class="course-info"> <img :src="$settings.Host+course.course_img" alt=""> <span class="course-title">{{course.name}}<br>{{course.discount_name}}</span> </el-col> <el-col :span="8"><span>{{course.expire}}</span></el-col> <el-col :span="4" class="course-price">¥{{course.discount_price}}<br><span>原价 ¥{{course.price.toFixed(2)}}</span></el-col> </el-row> </div> <div class="discount"> <div id="accordion"> <div class="coupon-box"> <div class="icon-box"> <span class="select-coupon">使用优惠劵:</span> <a class="select-icon" @click="use_coupon=!use_coupon"><img class="sign" :class="use_coupon?'is_show_select':''" src="../../static/image/12.png" alt=""></a> <span class="coupon-num">有{{coupon_list.length}}张可用</span> </div> <p class="sum-price-wrap">商品总金额:<span class="sum-price">{{total_price.toFixed(2)}}元</span></p> </div> <div id="collapseOne" v-if="use_coupon"> <ul class="coupon-list"> <li class="coupon-item" :class="{active:coupon==item.id,disable:check_coupon(item.start_time, item.coupon.duration) || total_price < item.coupon.condition }" @click="click_coupon(item)" v-for="item in coupon_list"> <p class="coupon-name">{{item.coupon.name}}</p> <p class="coupon-condition">满{{item.coupon.condition}}元可以使用</p> <p class="coupon-time start_time">开始时间:{{item.start_time.replace("T"," ")}}</p> <p class="coupon-time end_time">过期时间:{{has_time(item.start_time,item.coupon.duration)}}</p> </li> </ul> <div class="no-coupon" v-if="false"> <span class="no-coupon-tips">暂无可用优惠券</span> </div> </div> </div> <div class="credit-box" v-if="total_real_price>0"> <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-if="use_credit"><span>总积分:12000,已抵扣 ¥0,本次花费0积分</span></p> </div> <p class="sun-coupon-num">优惠券抵扣:<span>{{sale_price.toFixed(2)}}元</span></p> </div> <div class="calc"> <el-row class="pay-row"> <el-col :span="4" class="pay-col"><span class="pay-text">支付方式:</span></el-col> <el-col :span="8"> <span class="alipay" @click="pay_type=1" v-if="pay_type!=1"><img src="../../static/image/alipay.png" alt="支付宝"></span> <span class="alipay" v-if="pay_type==1"><img src="../../static/image/alipay2.png" alt="支付宝"></span> <span class="alipay wechat" @click="pay_type=2" v-if="pay_type!=2"><img src="../../static/image/wechat.png" alt="微信支付"></span> <span class="alipay wechat" v-if="pay_type==2"><img src="../../static/image/wechat2.png" alt="微信支付"></span> </el-col> <el-col :span="8" class="count">实付款: <span>¥{{total_real_price.toFixed(2)}}</span></el-col> <el-col :span="4" class="cart-pay"><span @click="ordeHander">立即支付</span></el-col> </el-row> </div> </div> <Footer/> </div> </template> <script> import Header from "./common/Header" import Footer from "./common/Footer" export default { name:"Order", data(){ return { user_token: "", // 用户的jwt token course_list: [], // 勾选商品列表 total_price: 0, // 课程总价 total_real_price: 0, // 实付总价 sale_price: 0, // 优惠券抵扣价格 use_coupon: false, // 是否使用优惠券 use_credit: false, // 是否使用积分 coupon_list: [], // 用户的优惠券列表 coupon: 0, // 当前用户选择的优惠券ID coupon_price: 0, // 优惠券抵扣价格 my_credit: 0, // 用户拥有的积分 current_credit: 0, // 本次订单可以使用的积分 credit_rmb: 0, // 积分兑换比率 credit_price: 0, // 积分折算价格 pay_type: 1, // 支付方式 } }, components:{ Header, Footer, }, created(){ this.user_token = this.$settings.check_user_login(); this.get_selected_course(); this.get_user_coupon(); }, watch:{ coupon(){ if(this.coupon > 0){ // 点选优惠券 for(let user_coupon of this.coupon_list){ if(user_coupon.id == this.coupon){ let sale = parseFloat(user_coupon.coupon.sale.substr(1)); if( user_coupon.coupon.sale[0]=="*" ){ // 折扣 this.total_real_price = this.total_price * sale; this.sale_price = this.total_price * (1-sale); }else{ // 假面 this.total_real_price = this.total_price - sale; this.sale_price = sale; } } } }else{ this.total_real_price = this.total_price; this.sale_price = 0; } }, use_coupon(){ // 是否使用了优惠券 if(!this.use_coupon){ this.coupon = 0; } }, }, methods: { has_time(start_time, timer){ let start_timestamp = (new Date(start_time) - 0)/1000; let timer_timestamp = timer * 24 * 3600; let endtime_timestamp = start_timestamp + timer_timestamp; let endtime = new Date(endtime_timestamp * 1000); let year = endtime.getFullYear(); let month = endtime.getMonth()+1; month = month>=10?month:"0"+month; let date = endtime.getDate(); date = date >=10?date:"0"+date; let hour = endtime.getHours(); hour = hour >=10?hour:"0"+hour; let min = endtime.getMinutes(); min = min >= 10?min:"0"+min; let sec = endtime.getSeconds(); sec = sec >= 10?sec:"0"+sec; let time = `${year}-${month}-${date} ${hour}:${min}:${sec}`; return time; }, get_user_coupon(){ // 获取登录的用户 let user_id = localStorage.user_id || sessionStorage.user_id; // 获取当前用户的优惠券 this.$axios.get(`${this.$settings.Host}/coupon/`,{ params:{ user_id, // 等同于 user_id:user_id, }, headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.coupon_list = response.data; }).catch(error=>{ this.$message.error("获取优惠券列表失败!"); }); }, get_selected_course(){ // 获取勾选的商品信息 this.$axios.get(`${this.$settings.Host}/cart/selected/`,{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ this.course_list = response.data; let total_price = 0; for(let course of this.course_list){ total_price = total_price + parseFloat(course.discount_price); } this.total_price = total_price; this.total_real_price = total_price; }).catch(error=>{ this.$message.error("无法获取商品信息!"); }); }, ordeHander(){ // 生成订单 this.$axios.post(`${this.$settings.Host}/orders/`,{ "pay_type": this.pay_type, "credit": 0, // 积分 "coupon": 0 // 优惠券 },{ headers:{ "Authorization": "jwt " + this.user_token, } }).then(response=>{ // 去支付 console.log(response.data); }).catch(error=>{ this.$message.error("生成订单失败!"); }) }, payhander(){ // 去支付 }, check_coupon(start_time,duration){ // 判断优惠券是否已经过期 let now = new Date() - 0; let duration_end = new Date(start_time) - 0 + duration * 24 * 3600 * 1000; // 过期时间戳 let duration_start = new Date(start_time); // 起用时间戳 return duration_start > now || duration_end<=now; }, click_coupon(user_coupon){ let result = this.check_coupon(user_coupon.start_time, user_coupon.coupon.duration) || this.total_price < user_coupon.coupon.condition; if(!result){ this.coupon=user_coupon.id; } } } } </script> <style scoped> .cart{ margin-top: 80px; } .cart-info{ overflow: hidden; width: 1200px; margin: auto; } .cart-top{ font-size: 18px; color: #666; margin: 25px 0; font-weight: normal; } .cart-top span{ font-size: 12px; color: #d0d0d0; display: inline-block; } .cart-title{ background: #F7F7F7; height: 70px; } .calc{ margin-top: 25px; margin-bottom: 40px; } .calc .count{ text-align: right; margin-right: 10px; vertical-align: middle; } .calc .count span{ font-size: 36px; color: #333; } .calc .cart-pay{ margin-top: 5px; width: 110px; height: 38px; outline: none; border: none; color: #fff; line-height: 38px; background: #ffc210; border-radius: 4px; font-size: 16px; text-align: center; cursor: pointer; } .cart-item{ height: 120px; line-height: 120px; margin-bottom: 30px; } .course-info img { width: 175px; height: 115px; margin-right: 35px; vertical-align: middle; float: left; } .course-title{ float: left; line-height: 32px; margin-top: 26px; } .course-price{ line-height: 32px; margin-top: 32px; } .course-price span{ text-decoration: line-through; color: #5e5e5e; } .alipay{ display: inline-block; height: 48px; cursor: pointer; } .alipay img{ height: 100%; width:auto; } .pay-text{ display: block; text-align: right; height: 100%; line-height: 100%; vertical-align: middle; margin-top: 20px; } .discount{ text-align: right; margin-top: 30px; } .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-left: 10px; 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-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; } .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; } </style>