07购物车实现
创建子应用 cart
cd luffy/apps
python ../../manage.py startapp cart
注册子应用cart
INSTALLED_APPS = [
'ckeditor', # 富文本编辑器
'ckeditor_uploader', # 富文本编辑器上传图片模块
'home',
'users',
'courses',
'cart',
]
因为购物车中的商品(课程)信息会经常被用户操作,所以为了减轻服务器的压力,可以选择把购物车信息通过redis来存储.
配置信息
# 设置redis缓存
CACHES = {
# 默认缓存
....
"cart":{
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/3",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
}
接下来商品信息存储以下内容:
course_id 购物车中的商品ID
course_expire 购物车中商品的有效期
is_selected 购物车中对应商品的勾选状态
# 上面三个数据,实际上存储到redis中,要以什么类型来存储呢?
# redis一共5种数据类型,我们就应该考虑到哪种数据类型保存上面的数据最方便我们读写.
cart_<user_id>: {
"商品ID1":"有效期",
"商品ID1":"有效期",
"商品ID1":"有效期",
}
# 把已经勾选的商品ID记录到无序集合中
cart_selected_<user_id>:{
"商品ID1",
"商品ID2",
"商品ID3",
}
添加课程商品到购物车的API接口实现
cart/views.py视图,代码:
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
class CartAPIView(APIView):
permission_classes = [IsAuthenticated]
"""购物车视图"""
def post(self,request):
"""购物车添加商品"""
# 获取客户端发送过来的课程ID
course_id = request.data.get("course_id")
# 验证课程ID是否有效
try:
Course.objects.get(pk=course_id,is_delete=False,is_show=True)
except Course.DoesNotExist:
return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
# 组装基本数据[课程ID,有效期]保存到redis
redis = get_redis_connection("cart")
# user_id = 1
user_id = request.user.id
try:
# 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
# hset(名称,键,值)
redis.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
# 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
# sadd(名称,成员)
redis.sadd("cart_selected_%s" % user_id, course_id )
except:
return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
# 返回结果
return Response({"message":"成功添加课程到购物车!"},status=status.HTTP_200_OK)
提供访问路由
总路由,代码:
urlpatterns = [
...
path('cart/', include("cart.urls")),
]
子应用路由cart/urls.py,代码:
from django.urls import path, re_path
from . import views
urlpatterns = [
path(r"course/",views.CartAPIView.as_view()),
]
默认用户添加课程到购物车就已经勾选了商品,视图代码:
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
class CartAPIView(APIView):
permission_classes = [IsAuthenticated]
"""购物车视图"""
def post(self,request):
"""购物车添加商品"""
# 获取客户端发送过来的课程ID
course_id = request.data.get("course_id")
# 验证课程ID是否有效
try:
Course.objects.get(pk=course_id,is_delete=False,is_show=True)
except Course.DoesNotExist:
return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
# 组装基本数据[课程ID,有效期]保存到redis
redis = get_redis_connection("cart")
# user_id = 1
user_id = request.user.id
# transation: 事务
# 作用: 可以设置多个数据库操作看成一个整体,这个整理里面每一条数据库操作都成功了,事务才算成功,
# 如果出现其中任意一个数据库操作失败,则整体一起失败!
# 事务可以提供 提交事务 和 回滚事务 的功能
# 不仅mysql中存在事务,在redis中也有事务的概念,但是叫"管道 pipeline"
try:
# 创建事务[管道]对象
pipeline = redis.pipeline()
# 开启事务
pipeline.multi()
# 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
# hset(名称,键,值)
pipeline.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
# 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
# sadd(名称,成员)
pipeline.sadd("cart_selected_%s" % user_id, course_id )
# 提交事务[如果不提交,则事务会自动回滚]
pipeline.execute()
except:
return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
# 返回结果
return Response({"message": "成功添加课程到购物车!"}, status=status.HTTP_200_OK)
前端提交课程到后端添加购物车数据
Detail.vue
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<video-player class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)"
>
</video-player>
</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+"课程")}} 难度:{{course.course_level}}</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="cartAddHandle"><img src="/src/assets/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="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">共{{chapter_list.length}}章 147个课时</p>
</div>
<div class="chapter-item" v-for="chapter in chapter_list">
<p class="chapter-title"><img src="" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
<ul class="lesson-list">
<li class="lesson-item" v-for="lesson in chapter.coursesections">
<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=""></p>
<button class="try" v-if="lesson.free_trail"><router-link :to="{path: '/player',
query:{'vid':lesson.section_link}}">立即试学</router-link></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}} {{course.teacher.title}}</p>
<p class="teacher-title">{{course.teacher.signature}}</p>
</div>
</div>
<p class="narrative" >Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
data(){
return {
token:sessionStorage.token || localStorage.token,
user_id:sessionStorage.id || localStorage.id,
user_name:sessionStorage.username || localStorage.username,
tabIndex:1, // 当前选项卡显示的下标
course_id:0, // 当前页面对应的课程ID
course: {
teacher: {},
}, // 课程详情信息
chapter_list:{},
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/courses/675076.jpeg", //视频封面图
width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
}
}
},
watch:{
course(data){
// 替换视频地址
this.playerOptions.sources[0].src = data.video;
// 替换视频封面
this.playerOptions.poster = data.course_img;
// 替换科恒信息中的详情介绍里面的图片路径
while(data.brief.search(`"/media`) != -1 ){
data.brief = data.brief.replace(`"/media`,`"${this.$settings.Host}/media`)
}
},
tabIndex(data){
if(data==2){
//获取当前课程对应的章节列表和课时列表
this.$axios.get(`${this.$settings.Host}/courses/chapters/?course=${this.course_id}`).then(response=>{
this.chapter_list = response.data;
}).catch(error=>{
console.log(error.response)
})
}
}
},
created(){
// 获取当前课程ID
this.course_id = this.$route.query.id - 0;
// 判断ID基本有效性
let _this = this;
if( isNaN(this.course_id) || this.course_id < 1 ){
_this.$alert("无效的课程ID!","错误",{
callback(){
_this.$router.go(-1);
}});
}
// 发送请求获取后端课程数据
this.$axios.get(this.$settings.Host+`/courses/detail/${this.course_id}/`).then(response=>{
this.course = response.data;
// 修改视频中的封面图片
this.playerOptions.poster = this.course.course_img;
}).catch(error=>{
console.log(error.response)
});
},
methods: {
// 视频播放事件
onPlayerPlay(player) {
alert("play");
},
// 视频暂停播放事件
onPlayerPause(player){
alert("pause");
},
// 视频插件初始化
player() {
return this.$refs.videoPlayer.player;
},
cartAddHandle(){
// 判断客户是否登录
if(!this.token){
this.$confirm("对不起,您尚未登录!请登录", "提示").then(()=>{
this.$router.push("/login");
}).catch();}
// 发起请求just
console.log('just click it');
this.$axios.post(this.$settings.Host + `/cart/course/`, {
course_id: this.course_id,
}, {
headers: {
// 注意:jwt后面必须有且只有一个空格!!!!
"Authorization": "jwt "+this.token
}
}).then(response=>{
// 获取购物车中商品总数
// 添加购物车成功!
this.$message(response.data.message, '提示', {
duration: 2000, // 单位:毫秒
});
}).catch(error=>{
console.log(error.response);
})
}
},
components:{
Header,
Footer,
videoPlayer,
}
}
</script>
后端增加返回一个购物车的商品课程总数
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
class CartAPIView(APIView):
permission_classes = [IsAuthenticated]
"""购物车视图"""
def post(self,request):
"""购物车添加商品"""
# 获取客户端发送过来的课程ID
course_id = request.data.get("course_id")
# 验证课程ID是否有效
try:
Course.objects.get(pk=course_id,is_delete=False,is_show=True)
except Course.DoesNotExist:
return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
# 组装基本数据[课程ID,有效期]保存到redis
redis = get_redis_connection("cart")
# user_id = 1
user_id = request.user.id
# transation: 事务
# 作用: 可以设置多个数据库操作看成一个整体,这个整理里面每一条数据库操作都成功了,事务才算成功,
# 如果出现其中任意一个数据库操作失败,则整体一起失败!
# 事务可以提供 提交事务 和 回滚事务 的功能
# 不仅mysql中存在事务,在redis中也有事务的概念,但是叫"管道 pipeline"
try:
# 创建事务[管道]对象
pipeline = redis.pipeline()
# 开启事务
pipeline.multi()
# 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
# hset(名称,键,值)
pipeline.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
# 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
# sadd(名称,成员)
pipeline.sadd("cart_selected_%s" % user_id, course_id )
# 提交事务[如果不提交,则事务会自动回滚]
pipeline.execute()
except:
return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
# 返回结果,返回购物车中的商品数量
count = redis.hlen("cart_%s" % user_id)
return Response({
"message": "成功添加课程到购物车!",
"count": count,
}, status=status.HTTP_200_OK)
前端展示商品课程的总数
获取商品总数是在头部组件中使用到,并展示出来,但是我们后面可以在购物车中,或者商品课程的详情页中修改购物车中商品总数,因为对于一些数据,需要在多个组件中共享,这种情况,我们可以使用本地存储来完成,但是也可以通过vuex组件来完成这个功能。
安装vuex
npm install -S vuex
把vuex注册到vue中
在src目录下创建store目录,并在store目录下创建一个index.js文件,index.js文件代码:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); export const store = new Vuex.Store({ // 数据仓库,类似vue里面的data state: { }, // 数据操作方法,类似vue里面的methods mutations: { } });
把上面index.js中创建的store对象注册到main.js的vue中。
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './routers/index';
import store from './store/index';
// 手动的自定义全局配置
import settings from "./settings"
Vue.prototype.$settings = settings;
// elementUI 导入
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
// 调用插件
Vue.use(ElementUI);
import "../static/css/reset.css"
import axios from 'axios'; // 从node_modules目录中导入包
// 允许ajax发送请求时附带cookie
axios.defaults.withCredentials = true;
Vue.prototype.$axios = axios; // 把对象挂载vue中
Vue.config.productionTip = false;
// 导入gt极验
import '../static/js/gt.js';
// vue-video视频播放插件
require('video.js/dist/video-js.css');
require('vue-video-player/src/custom-theme.css');
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
接下来,我们就可以在组件使用到store中state里面保存的共享数据了.
先到vuex中添加数据,store/index.js,代码
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
// 数据仓库,类似vue里面的data
state: {
// 购物车数据
cart:{
count:0,
}
},
// 数据操作方法,类似vue里面的methods
mutations: {
}
});
在Header.vue头部组件中,直接读取store里面的数据
<b class="goods-number">{{this.$store.state.cart.count}}</b>
// this是可以省略不写。
<b class="goods-number">{{$store.state.cart.count}}</b>
我们可以在Detail.vue课程详情的组件中, 修改商品总数。
// 添加商品课程到购物车
cartAddHander(){
// 1. 判断用户是否已经登录了.
if(!this.token){
this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
this.$router.push("/login");
});
}
// 2. 发起请求
this.$axios.post(this.$settings.Host+`/carts/course/`,{
course_id: this.course_id,
},{
headers:{
// 注意:jwt后面必须有且只有一个空格!!!!
"Authorization":"jwt " + this.token
}
}).then(response=>{
// 获取购物城中商品总数
// this.$store.state.cart.count = response.data.count;
this.$store.commit("addcart",response.data);
// 添加购物车成功!
this.$message(response.data.message,"提示!",{
duration: 2000, // 单位: 毫秒
});
}).catch(error=>{
console.log(error.response);
})
}
在store/index.js中新增mutations的方法,代码:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
// 数据仓库,类似vue里面的data
state: {
// 购物车数据
cart:{
count: 0,
// course_list: [], // 购物车里面的商品列表信息
}
},
// 数据操作方法,类似vue里面的 methods
mutations: {
// data是调用方法,传递的购物车相关的参数
addcart(state,data){
// 修改商品课程的总数
state.cart.count = data.count;
// state.cart.course_list = data.course_list;
}
}
});
显示购物车商品列表数据
后端提供获取购物车课程信息
def get(self,request):
"""获取购物车中的所有课程信息"""
# 获取当前登陆用户
user_id = request.user.id
# 从redis中获取所有的课程信息和勾选状态
redis = get_redis_connection("cart")
course_list = redis.hgetall("cart_%s" % user_id)
selected_list = redis.smembers("cart_select_%s" % user_id )
# 组装数据
"""
cart_<user_id>:{
课程id: 有效期,
课程id: 有效期,
课程id: 有效期,
}
cart_select_<user_id>:{课程id,课程id}
上面两个数据最终要合并到一个数组中,一起返回给客户端
[
{
course_id:课程id,
course_expire:有效期,
selected:True/False, // 勾选状态
},
]
"""
data = []
for course_id,price in course_list.items():
course_id = course_id.decode()
price = price.decode()
try:
course_info = Course.objects.get(pk=course_id)
except:
return Response({"message":"请求有误,请联系客服"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
data.append({
"id": course_id,
"price":price,
"selected": course_id.encode() in selected_list,
"course_img": course_info.course_img.url,
"name": course_info.name,
})
# 返回给客户端
return Response(data,status=status.HTTP_200_OK)
购物车页面有两部分构成:
Cart.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="4">有效期</el-col>
<el-col :span="4">单价</el-col>
<el-col :span="4">操作</el-col>
</el-row>
</div>
<CartItem v-for="item in course_list" :course="item"/>
<div class="calc">
<el-row>
<el-col :span="2"> </el-col>
<el-col :span="3">
<el-checkbox label="全选" name="type"></el-checkbox></el-col>
<el-col :span="2" class="del"><i class="el-icon-delete"></i>删除</el-col>
<el-col :span="12" class="count">总计:¥0.0</el-col>
<el-col :span="3" class="cart-calc">去结算</el-col>
</el-row>
</div>
</div>
<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 {
token: localStorage.token || sessionStorage.token,
id: localStorage.id || sessionStorage.id,
course_list:[]
}
},
components:{
Header,
Footer,
CartItem,
},
created(){
// 判断用户是否已经登陆了。
if( !this.token || !this.id ){
this.$router.push("/login");
}
let _this = this;
// 发起请求获取购物车中的商品信息
}
}
</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;
}
.cart-title .el-row,.cart-title .el-col{
height: 80px;
font-size: 14px;
color: #333;
line-height: 80px;
}
.calc .el-col{
height: 80px;
line-height: 80px;
}
.calc .el-row span{
font-size: 18px!important;
}
.calc .el-row{
font-size: 18px;
color: #666;
margin-bottom: 300px;
margin-top: 50px;
background: #F7F7F7;
}
.calc .del{
}
.calc .el-icon-delete{
margin-right: 15px;
font-size: 20px;
}
.calc .count{
text-align: right;
margin-right:62px;
}
.calc .cart-calc{
width: 159px;
height: 80px;
border: none;
background: #ffc210;
font-size: 18px;
color: #fff;
text-align: center;
cursor: pointer;
}
</style>
前端请求并显示课程信息
Cart.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="4">有效期</el-col>
<el-col :span="4">单价</el-col>
<el-col :span="4">操作</el-col>
</el-row>
</div>
<CartItem v-for="item in course_list" :course="item"/>
<div class="calc">
<el-row>
<el-col :span="2"> </el-col>
<el-col :span="3">
<el-checkbox label="全选" name="type"></el-checkbox></el-col>
<el-col :span="2" class="del"><i class="el-icon-delete"></i>删除</el-col>
<el-col :span="12" class="count">总计:¥0.0</el-col>
<el-col :span="3" class="cart-calc">去结算</el-col>
</el-row>
</div>
</div>
<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 {
token: localStorage.token || sessionStorage.token,
user_id: localStorage.user_id || sessionStorage.user_id,
course_list:[] // 购物车中所有商品信息
}
},
components:{
Header,
Footer,
CartItem,
},
created(){
// 判断用户是否已经登陆了。
if( !this.token || !this.user_id ){
alert("对不起,请登录")
this.$router.push("/login");
}
// 发起请求获取购物车中的商品信息
this.$axios.get(this.$settings.host+"/cart/course/").then(response=>{
this.course_list = 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;
}
.cart-title .el-row,.cart-title .el-col{
height: 80px;
font-size: 14px;
color: #333;
line-height: 80px;
}
.calc .el-col{
height: 80px;
line-height: 80px;
}
.calc .el-row span{
font-size: 18px!important;
}
.calc .el-row{
font-size: 18px;
color: #666;
margin-bottom: 300px;
margin-top: 50px;
background: #F7F7F7;
}
.calc .del{
}
.calc .el-icon-delete{
margin-right: 15px;
font-size: 20px;
}
.calc .count{
text-align: right;
margin-right:62px;
}
.calc .cart-calc{
width: 159px;
height: 80px;
border: none;
background: #ffc210;
font-size: 18px;
color: #fff;
text-align: center;
cursor: pointer;
}
</style>
CartItem.vue
<template>
<div class="cart-item">
<el-row>
<el-col :span="2" class="checkbox"><el-checkbox v-model="course.selected" :checked="course.selected" name="type"></el-checkbox></el-col>
<el-col :span="10" class="course-info">
<img :src="this.$settings.host+course.course_img" alt="">
<span>{{course.name}}</span>
</el-col>
<el-col :span="4">
<el-select v-model="course.course_expire">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-col>
<el-col :span="4" class="course-price">¥{{course.price}}</el-col>
<el-col :span="4" class="course-delete">删除</el-col>
</el-row>
</div>
</template>
<script>
export default {
name:"CartItem",
props:["course"],
data(){
return {
options:[ // 有效期
{value:30,label:"一个月有效"},
{value:60,label:"二个月有效"},
{value:90,label:"三个月有效"},
{value:0,label:"永久有效"},
]
}
},
watch:{
"course.course_expire": function(value){
// 切换当前课程的有效期
console.log("有效期:"+value)
},
"course.selected": function (value) {
// 切换当前课程的勾选状态
console.log("勾选:" + value)
}
}
}
</script>
<style scoped>
.cart-item{
height: 250px;
}
.cart-item .el-row{
height: 100%;
}
.course-delete{
font-size: 14px;
color: #ffc210;
cursor: pointer;
}
.el-checkbox,.el-select,.course-price,.course-delete{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.el-checkbox{
padding-top: 55px;
}
.el-select{
padding-top: 45px;
width: 118px;
height: 28px;
font-size: 12px;
color: #666;
line-height: 18px;
}
.course-info img{
width: 175px;
height: 115px;
margin-right: 35px;
vertical-align: middle;
}
.cart-item .el-col{
padding: 67px 10px;
vertical-align: middle!important;
}
.course-info{
}
</style>
切换勾选状态和课程有效期
后端提供修改勾选状态的接口
视图代码:
def put(self,request):
"""修改购物车中的商品信息"""
user_id = request.user.id
course_id = request.data.get("course_id")
try:
course_info = Course.objects.get(pk=course_id)
except:
return Response({"message": "请求有误,请联系客服"}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
redis = get_redis_connection("cart")
# 操作商品信息之前,必须先确保当前课程在购物车中
try:
rs = redis.hget("cart_%s" % user_id, course_id)
if rs is None:
raise Exception
except:
return Response({"message": "请求有误,请联系客服"}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
# 修改勾选状态
selected = request.data.get("selected",None)
if selected is not None:
if selected == True:
redis.sadd("cart_select_%s" % user_id, course_info.id)
else:
redis.srem("cart_select_%s" % user_id, course_info.id)
# 修改有效期
course_expire = request.data.get("course_expire",None)
if course_expire is not None:
redis.hset("cart_%s" % user_id, course_info.id, course_expire )
return Response({"message":"ok"}, status=status.HTTP_200_OK)
CartItem.vue
<template>
<div class="cart-item">
<el-row>
<el-col :span="2" class="checkbox"><el-checkbox v-model="course.selected" :checked="course.selected" name="type"></el-checkbox></el-col>
<el-col :span="10" class="course-info">
<img :src="this.$settings.host+course.course_img" alt="">
<span>{{course.name}}</span>
</el-col>
<el-col :span="4">
<el-select v-model="course.course_expire">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-col>
<el-col :span="4" class="course-price">¥{{course.price}}</el-col>
<el-col :span="4" class="course-delete"><span @click="removeCartHander">删除</span></el-col>
</el-row>
</div>
</template>
<script>
export default {
name:"CartItem",
props:["course"],
data(){
return {
token: localStorage.token || sessionStorage.token,
user_id: localStorage.user_id || sessionStorage.user_id,
options:[ // 有效期
{value:30,label:"一个月有效"},
{value:60,label:"二个月有效"},
{value:90,label:"三个月有效"},
{value:0,label:"永久有效"},
]
}
},
methods:{
removeCartHander(){
// 从购物车中删除商品信息
this.$axios.delete(this.$settings.host+"/course/cart/",{
course_id: this.course.course_id,
},{
headers:{
// 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格
'Authorization':'JWT '+this.token
},
}).then(response=>{
console.log(response.data)
}).catch(error=>{
console.log(error.response)
});
},
},
watch:{
"course.course_expire": function(value){
// 切换当前课程的有效期
this.$axios.put(this.$settings.host+"/cart/course/",{
course_id: this.course.course_id,
course_expire: value
},{
headers:{
// 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格
'Authorization':'JWT '+this.token
},
}).then(response=>{
console.log(response.data)
}).catch(error=>{
console.log(error.response)
});
},
"course.selected": function (value) {
// 切换当前课程的勾选状态
this.$axios.put(this.$settings.host+"/cart/course/",{
course_id: this.course.course_id,
selected: value
},{
headers:{
// 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格
'Authorization':'JWT '+this.token
},
}).then(response=>{
console.log(response.data)
}).catch(error=>{
console.log(error.response)
});
}
}
}
</script>
<style scoped>
.cart-item{
height: 250px;
}
.cart-item .el-row{
height: 100%;
}
.course-delete{
font-size: 14px;
color: #ffc210;
cursor: pointer;
}
.el-checkbox,.el-select,.course-price,.course-delete{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.el-checkbox{
padding-top: 55px;
}
.el-select{
padding-top: 45px;
width: 118px;
height: 28px;
font-size: 12px;
color: #666;
line-height: 18px;
}
.course-info img{
width: 175px;
height: 115px;
margin-right: 35px;
vertical-align: middle;
}
.cart-item .el-col{
padding: 67px 10px;
vertical-align: middle!important;
}
.course-info{
}
</style>
修复添加商品课程到购物车的前端BUG
用户在课程详情页中退出登录时,如果不刷新页面,则可以继续进行购物。
出现问题的原因:添加购物车时,判断用户是否登录依靠的是当前组件中data的token数据,而token数据是通过sessionStorage或者localStorage赋值过来的,但是当用户退出登录时,我们删除了sessionStorage或者localStorage,但是并没有删除token值。因为形成了BUG。
解决方案:退出登录进行页面跳转.或者添加商品课程到购物车时,重新对token赋值。
logout(){
this.token = false;
this.user_id=false;
this.user_name=false;
sessionStorage.removeItem("token");
sessionStorage.removeItem("user_id");
sessionStorage.removeItem("user_name");
localStorage.removeItem("token");
localStorage.removeItem("user_id");
localStorage.removeItem("user_name");
let _this = this;
_this.$alert('退出登录成功!', '路飞学城', {
callback(){
_this.$router.push("/");
}
});
显示购物车商品列表数据
前端显示购物车页面组件
<template>
<div class="cart">
<Header/>
<div class="main">
<div class="cart-title">
<h3>我的购物车 <span> 共2门课程</span></h3>
</div>
<div class="cart-info">
<el-table :data="courseData" style="width:100%">
<el-table-column type="selection" label="" width="87"></el-table-column>
<el-table-column label="课程" width="540">
<template slot-scope="scope">
<div class="course-box">
<img :src="scope.row.img" alt="">
{{scope.row.title}}
</div>
</template>
</el-table-column>
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-form ref="form" label-width="60px">
<el-form-item>
<el-select v-model="expire" placeholder="请选择有效期">
<el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column label="单价" width="162">
<template slot-scope="scope">¥{{ scope.row.price }}</template>
</el-table-column>
<el-table-column label="操作" width="162">
<a href="">删除</a>
</el-table-column>
</el-table>
</div>
<div class="cart-bottom">
<div class="select-all"><el-checkbox>全选</el-checkbox></div>
<div class="delete-any"><img src="../../static/img/ico3.png" alt="">删除</div>
<div class="cart-bottom-right">
<span class="total">总计:¥<span>0.0</span></span>
<span class="go-pay">去结算</span>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data(){
return{
expire:3,
expire_list:[
{title:"一个月有效",id:1},
{title:"两个月有效",id:2},
{title:"三个月有效",id:3},
{title:"永久有效",id:4},
],
courseData:[
{img:"../../static/course/1544059695.jpeg",title:"课程标题一",expire:"2016",price:"12.00"},
{img:"../../static/course/1544059695.jpeg",title:"课程标题一",expire:"2016",price:"12.00"},
]
}
},
components:{Header,Footer}
}
</script>
<style scoped>
.main{
width: 1200px;
margin: 0 auto;
overflow: hidden; /* 解决body元素和标题之间的上下外边距的塌陷问题 */
}
.cart-title h3{
font-size: 18px;
color: #666;
margin: 25px 0;
}
.cart-title h3 span{
font-size: 12px;
color: #d0d0d0;
display: inline-block;
}
.course-box img{
max-width: 175px;
max-height: 115px;
margin-right: 35px;
vertical-align: middle;
}
.cart-bottom{
overflow: hidden;
height: 80px;
background: #F7F7F7;
margin-bottom: 300px;
margin-top: 50px;
}
.select-all{
float: left;
margin-right: 58px;
line-height: 80px;
}
.delete-any{
cursor: pointer;
float: left;
line-height: 80px;
}
.delete-any img{
width:18px;
height: 18px;
vertical-align: middle;
padding-right: 15px;
}
.cart-bottom-right{
float: right;
text-align: right; /* 文本右对齐 */
}
.total{
margin-right: 62px;
font-size: 18px;
color: #666;
}
.go-pay{
outline: none;
background: #ffc210;
display: inline-block;
padding: 27px 50px;
font-size: 18px;
cursor: pointer;
color: #fff;
}
</style>
路由routers/index.js,注册:
// 导入页面组件
...
import Cart from "../components/Cart"
Vue.use(Router);
export default new Router({
// 设置路由模式为‘history’,去掉默认的#
mode: "history",
routes:[
// 路由列表
。。。
,{
name:"Cart",
path:"/cart",
component: Cart,
},
]
})
后端提供获取购物车课程信息
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from django.conf import settings
class CartAPIView(APIView):
permission_classes = [IsAuthenticated]
"""购物车视图"""
def get(self,request):
"""获取购物车商品课程列表"""
# 获取当前用户ID
# user_id = 1
user_id = request.user.id
# 通过用户ID获取购物车中的商品信息
redis = get_redis_connection("cart")
cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
cart_goods_selectsfrom rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
class CartAPIView(APIView):
"""购物车视图"""
permission_classes = [IsAuthenticated]
def get(self, request):
"""获取购物车中的所有课程信息"""
# 获取当前登录用户
# user_id = 1
user_id = request.user.id
# 通过用户ID,从redis中获取所有的课程信息和勾选状态
# redis里面的所有数据最终都是以bytes类型的字符串保存的
redis = get_redis_connection("cart")
cart_courses_list = redis.hgetall("cart_%s" % user_id) # 商品课程列表
cart_courses_selected = redis.smembers("cart_selected_%s" % user_id)
print('++++++++++++++++++++++', cart_courses_list, cart_courses_selected,
"++++++++++++++++++++++", sep='\n')
"""
++++++++++++++++++++++
{b'1': b'-1', b'4': b'-1', b'3': b'-1', b'2': b'-1'}
{b'3', b'4', b'2'}
++++++++++++++++++++++
"""
# 组装数据
"""
cart_<user_id>:{
课程id: 有效期,
课程id: 有效期,
课程id: 有效期,
}
cart_select_<user_id>:{课程id,课程id}
上面两个数据最终要合并到一个数组中,一起返回给客户端
[
{
course_id:课程id,
course_expire:有效期,
selected:True/False, // 勾选状态
},
]
"""
# 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
data_list = []
try:
for course_id_bytes, expire_bytes in cart_courses_list.items():
course_id = int(course_id_bytes.decode())
expire = expire_bytes.decode()
course = Course.objects.get(pk=course_id)
data_list.append({
"id": course_id,
"expire": expire,
"course_img": course.course_img.url,
"name": course.name,
"price": course.get_course_price(),
"is_select": course_id_bytes in cart_courses_selected
})
except:
pass
# 返回客户端
print(data_list)
return Response(data_list, status=status.HTTP_200_OK) = redis.smembers("cart_selected_%s" % user_id)
# redis里面的所有数据最终都是以bytes类型的字符串保存的
# print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
# print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
# 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
data_list = []
try:
for course_id_bytes,expire_bytes in cart_goods_list.items():
course_id = int( course_id_bytes.decode() )
expire = expire_bytes.decode()
course = Course.objects.get(pk=course_id)
data_list.append({
"id": course_id,
"expire":expire,
"course_img": course.course_img.url,
"name": course.name,
"price": course.get_course_price(),
"is_select": course_id_bytes in cart_goods_selects
})
except:
return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# print(data_list)
# 返回查询结果
return Response(data_list,status=status.HTTP_200_OK)
前端请求显示课程信息
Cart.vue,代码:
<template>
<div class="cart">
<Header/>
<div class="main">
<div class="cart-title">
<h3>我的购物车 <span> 共{{$store.state.cart.count}}门课程</span></h3>
</div>
<div class="cart-info">
<el-table
:data="courseData"
style="width:100%"
ref="multipleTable"
@select="currentSelected"
>
<el-table-column type="selection" width="87"></el-table-column>
<el-table-column label="课程" width="540">
<template slot-scope="scope">
<div class="course-box">
<img :src="$settings.Host + scope.row.course_img" alt="">
{{scope.row.name}}
</div>
</template>
</el-table-column>
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-select @change="changeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
<el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="单价" width="162">
<template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="操作" width="162">
<template slot-scope="scope">
<a @click="cartDel(scope.row, scope.row.name)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
<div class="cart-bottom">
<div class="select-all">
<el-checkbox>全选</el-checkbox>
</div>
<div class="delete-any"><img src="" alt="">删除</div>
<div class="cart-bottom-right">
<span class="total">总计:¥<span>0.0</span></span>
<span class="go-pay">去结算</span>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data() {
return {
expire: 3,
expire_list: [
{title: "一个月有效", id: 1},
{title: "两个月有效", id: 2},
{title: "三个月有效", id: 3},
{title: "永久有效", id: -1},
],
courseData: []
}
},
components: {Header, Footer},
created(){
// 判断是否登录
this.token = sessionStorage.token || localStorage.token;
if(!this.token){
this.$confirm("对不起,您尚未登录!请登录", "Attention").then(()=>{
this.$router.push("/login");
}).catch(()=>{
this.$router.go(-1);
})
}else{
// 获取购物车商品数据
this.$axios.get(this.$settings.Host+"/cart/course/", {
headers: {
// 注意空格
"Authorization": "jwt " + this.token
}
}).then(response=>{
this.courseData = response.data;
console.log("courseData",response.data);
// 更新在vuex中的数据
this.$store.state.cart.count = response.data.length;
// setTimeout(()=>{
// let text_expire_list = [];
// for(let i=0; i<this.expire_list.length; i++){
// text_expire_list[this.expire_list[i].id] = this.expire_list[i].title;
//
// }
// console.log(text_expire_list);
//
// for(let i=0; i< this.courseData.length; i++){
// // 设置选中效果
// this.$refs.multipleTable.toggleRowSelection(this.courseData[i],
// this.courseData[i].is_select);
//
// // 修改有效期显示值
// this.courseData[i].expire = text_expire_list[this.courseData[i].expire];
// }
// }, 0)
// 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
setTimeout(()=>{
let expire_data = [];
this.expire_list.forEach(row=>{
expire_data[row.id] = row.title;
});
console.log("expire_data", expire_data);
// row 就是字典数据[json]
this.courseData.forEach(row=>{
if(row.is_select){
this.$refs.multipleTable.toggleRowSelection(row);
}
// 调整有效期选项中数值变成文本内容
row.expire = expire_data[row.expire];
})
}, 0)
}).catch(error=>{
console.log(error.response);
let status = error.response.status;
if(status === 401){
this.token = null;
sessionStorage.removeItem("token");
localStorage.removeItem("token");
let _this = this;
this.$alert("您尚未登录或登录超时!请重新登录", 'Warning', {
callback(){
_this.$router.push("/login");
}
})
}
})
}
},
methods: {},
}
</script>
修复商品课程的默认选中和有效期的文本显示效果
<template>
<div class="cart">
<Header/>
<div class="main">
<div class="cart-title">
<h3>我的购物车 <span> 共{{$store.state.cart.count}}门课程</span></h3>
</div>
<div class="cart-info">
<el-table
:data="courseData"
style="width:100%"
ref="multipleTable"
@select="currentSelected"
>
<el-table-column type="selection" width="87"></el-table-column>
<el-table-column label="课程" width="540">
<template slot-scope="scope">
<div class="course-box">
<img :src="$settings.Host + scope.row.course_img" alt="">
{{scope.row.name}}
</div>
</template>
</el-table-column>
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-select @change="changeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
<el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="单价" width="162">
<template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="操作" width="162">
<template slot-scope="scope">
<a @click="cartDel(scope.row, scope.row.name)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
<div class="cart-bottom">
<div class="select-all">
<el-checkbox>全选</el-checkbox>
</div>
<div class="delete-any"><img src="" alt="">删除</div>
<div class="cart-bottom-right">
<span class="total">总计:¥<span>0.0</span></span>
<span class="go-pay">去结算</span>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data() {
return {
expire: 3,
expire_list: [
{title: "一个月有效", id: 1},
{title: "两个月有效", id: 2},
{title: "三个月有效", id: 3},
{title: "永久有效", id: -1},
],
courseData: []
}
},
components: {Header, Footer},
created(){
// 判断是否登录
this.token = sessionStorage.token || localStorage.token;
if(!this.token){
this.$confirm("对不起,您尚未登录!请登录", "Attention").then(()=>{
this.$router.push("/login");
}).catch(()=>{
this.$router.go(-1);
})
}else{
// 获取购物车商品数据
this.$axios.get(this.$settings.Host+"/cart/course/", {
headers: {
// 注意空格
"Authorization": "jwt " + this.token
}
}).then(response=>{
this.courseData = response.data;
console.log("courseData",response.data);
// 更新在vuex中的数据
this.$store.state.cart.count = response.data.length;
// setTimeout(()=>{
// let text_expire_list = [];
// for(let i=0; i<this.expire_list.length; i++){
// text_expire_list[this.expire_list[i].id] = this.expire_list[i].title;
//
// }
// console.log(text_expire_list);
//
// for(let i=0; i< this.courseData.length; i++){
// // 设置选中效果
// this.$refs.multipleTable.toggleRowSelection(this.courseData[i],
// this.courseData[i].is_select);
//
// // 修改有效期显示值
// this.courseData[i].expire = text_expire_list[this.courseData[i].expire];
// }
// }, 0)
// 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
setTimeout(()=>{
let expire_data = [];
this.expire_list.forEach(row=>{
expire_data[row.id] = row.title;
});
console.log("expire_data", expire_data);
// row 就是字典数据[json]
this.courseData.forEach(row=>{
if(row.is_select){
this.$refs.multipleTable.toggleRowSelection(row);
}
// 调整有效期选项中数值变成文本内容
row.expire = expire_data[row.expire];
})
}, 0)
}).catch(error=>{
console.log(error.response);
let status = error.response.status;
if(status === 401){
this.token = null;
sessionStorage.removeItem("token");
localStorage.removeItem("token");
let _this = this;
this.$alert("您尚未登录或登录超时!请重新登录", 'Warning', {
callback(){
_this.$router.push("/login");
}
})
}
})
}
},
methods: {},
}
</script>
切换勾选状态
后端提供修改勾选状态的接口
视图代码:
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
class CartAPIView(APIView):
"""购物车视图"""
# permission_classes = [IsAuthenticated]
def get(self, request):
"""获取购物车中的所有课程信息"""
......
return Response(data_list, status=status.HTTP_200_OK)
def post(self, request):
"""购物车添加商品"""
......
return Response({"message": "成功添加课程到购物车",
"count": count,
}, status=status.HTTP_200_OK)
def put(self, request):
"""修改购物车中商品的信息"""
# 获取当前登录用户ID
user_id = request.user.id
# 接受课程ID,判断课程ID是否存在
course_id = request.data.get("course_id")
try:
Course.objects.get(pk=course_id, is_delete=False, is_show=True)
except:
return Response({"message": "请求有误"}, status=status.HTTP_400_BAD_REQUEST)
redis = get_redis_connection('cart')
# 操作商品信息之前,必须先确保当前课程在购物车中
try:
rs = redis.hget("cart_%s" % user_id, course_id)
if rs is None:
raise Exception
except:
return Response({"message": "请求有误,请联系客服"}, status=status.HTTP_507_INSUFFICIENT_STORAGE)
# 获取勾选状态
is_select = request.data.get("is_select")
# 链接redis
redis = get_redis_connection('cart')
# 修改购物车中指定的商品课程的信息
if is_select:
# 从勾选set中增加一个课程ID
redis.sadd("cart_selected_%s" % user_id, course_id)
else:
redis.srem("cart_selected_%s" % user_id, course_id)
return Response({"message": "Okay!"},
status=status.HTTP_200_OK)
前端发送更新勾选状态的请求
template代码:
<el-table
:data="courseData"
style="width:100%"
ref="multipleTable"
@select="currentSelected"
>
script新增一个 操作方法:
methods: {
// 切换课程勾选状态
currentSelected(selection, row){
// selection 表示所有被勾选的信息
// row 当前操作的数据
let is_select = true;
if(selection.indexOf(row)===-1){
is_select = false;
}
// 获取当前课程ID
let course_id = row.id;
// 发送put请求,切换勾选状态
this.$axios.put(this.$settings.Host+"/cart/course/", {
course_id: course_id,
is_select: is_select,
}, {
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
this.$message(response.data.message, "Attention");
}).catch(error=>{
console.log(error.response);
})
},
}
课程有效期
后端提供修改勾选状态的接口
因为前面实现修改购物车商品课程的勾选状态已经使用put方法提供API接口了,所以我们现在要修改课程有效期,业务上来说也是更新购物车中商品信息,所以我们可以继续在put里面通过判断是否有expire参数来执行不同代码,也可以使用http的patch操作来完成更新课程有效期的功能。
我们这里使用patch方法。
def patch(self,request):
"""更新购物城中的商品信息"""
# 获取当前登录的用户ID
# user_id = 1
user_id = request.user.id
# 获取当前操作的课程ID
course_id = request.data.get("course_id")
# 获取新的有效期
expire = request.data.get("expire")
# 获取redis链接
redis = get_redis_connection("cart")
# 更新购物中商品课程的有效期
redis.hset("cart_%s" % user_id,course_id, expire)
return Response({
"message": "修改购物车信息成功!"
}, status=status.HTTP_200_OK)
前端提交修改商品课程的有效期请求
<template>
<div class="cart">
<Header/>
<div class="main">
<div class="cart-title">
<h3>我的购物车 <span> 共{{$store.state.cart.count}}门课程</span></h3>
</div>
<div class="cart-info">
<el-table
:data="courseData"
style="width:100%"
ref="multipleTable"
@select="currentSelected"
>
<el-table-column type="selection" width="87"></el-table-column>
<el-table-column label="课程" width="540">
<template slot-scope="scope">
<div class="course-box">
<img :src="$settings.Host + scope.row.course_img" alt="">
{{scope.row.name}}
</div>
</template>
</el-table-column>
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-select @change="changeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
<el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="单价" width="162">
<template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="操作" width="162">
<template slot-scope="scope">
<a @click="CartDel(scope.row.id,scope.row.name)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
<div class="cart-bottom">
<div class="select-all">
<el-checkbox>全选</el-checkbox>
</div>
<div class="delete-any"><img src="" alt="">删除</div>
<div class="cart-bottom-right">
<span class="total">总计:¥<span>0.0</span></span>
<span class="go-pay">去结算</span>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data() {
return {
expire: 3,
expire_list: [
{title: "一个月有效", id: 1},
{title: "两个月有效", id: 2},
{title: "三个月有效", id: 3},
{title: "永久有效", id: -1},
],
courseData: []
}
},
components: {Header, Footer},
created(){......},
methods: {
// 切换课程勾选状态
currentSelected(selection, row){......},
// 切换课程的有效期
changeExpire(course){
let course_id = course.id;
let expire = course.expire;
// 发送patch请求更新有效期
this.$axios.patch(this.$settings.Host+"/cart/course/", {
course_id: course_id,
expire, // expire <==> expire: expire,
}, {
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
this.$message(response.data.message, "Attention");
}).catch(error=>{
console.log(error.response);
})
}
},
}
</script>
删除购物车中的商品信息
后端提供从购物车中删除商品课程的API接口
def delete(self,request):
"""从购物车中删除数据"""
# 获取当前登录用户ID
# user_id = 1
user_id = request.user.id
# 获取课程ID
course_id = request.query_params.get("course_id")
redis = get_redis_connection("cart")
pipeline = redis.pipeline()
pipeline.multi()
# 从购物车中删除指定商品课程
pipeline.hdel("cart_%s" % user_id, course_id)
# 从勾选集合中移除指定商品课程
pipeline.srem("cart_selected_%s" % user_id, course_id )
pipeline.execute()
# 返回操作结果
return Response({"message":"删除商品课程成功!"},status=status.HTTP_204_NO_CONTENT)
前端请求删除商品课程
tempalte,代码:
<template slot-scope="scope">
<a @click="CartDel(scope.row,scope.row.name)">删除</a>
</template>
script标签代码:
methods: {
// 切换课程勾选状态
currentSelected(selection, row){......},
// 切换课程的有效期
changeExpire(course){.....},
// 删除课程
cartDel(course, course_name){
this.$confirm(`您确定要从购物车删除《${course_name}》`, "Attention").then(()=>{
let course_id = course.id;
// 发送请求
this.$axios.delete(this.$settings.Host+"/cart/course/", {
params: {
course_id: course_id,
},
headers: {
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
let index = this.courseData.indexOf(course);
this.courseData.splice(index, 1);
console.log("courseData", this.courseData);
this.$message("删除成功!")
}).catch(error=>{
console.log(error.response);
});
}).catch(()=>{
// 取消操作
})
},
},
}
购物车的价格统计
<template>
<div class="cart">
<Header/>
<div class="main">
<div class="cart-title">
<h3>我的购物车 <span> 共2门课程</span></h3>
</div>
<div class="cart-info">
<el-table
:data="courseData"
style="width:100%"
ref="multipleTable"
@select="currentSelected"
@selection-change="SelectionChange"
>
<el-table-column type="selection" width="87"></el-table-column>
<el-table-column label="课程" width="540">
<template slot-scope="scope">
<div class="course-box">
<img :src="$settings.Host + scope.row.course_img" alt="">
<router-link :to="'/detail?id='+scope.row.id">{{scope.row.name}}</router-link>
</div>
</template>
</el-table-column>
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-select @change="ChangeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
<el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="单价" width="162">
<template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
</el-table-column>
<el-table-column label="操作" width="162">
<template slot-scope="scope">
<a @click="CartDel(scope.row,scope.row.name)">删除</a>
</template>
</el-table-column>
</el-table>
</div>
<div class="cart-bottom">
<div class="select-all"><el-checkbox>全选</el-checkbox></div>
<div class="delete-any"><img src="../../static/img/ico3.png" alt="">删除</div>
<div class="cart-bottom-right">
<span class="total">总计:¥<span>{{total_price}}</span></span>
<span class="go-pay">去结算</span>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data(){
return{
expire:3,
expire_list:[
{title:"一个月有效",id:1},
{title:"两个月有效",id:2},
{title:"三个月有效",id:3},
{title:"永久有效",id:-1},
],
courseData:[], // 购物车中的商品信息
selection:[], // 购物车中被勾选的商品信息
total_price:0.00,
}
},
watch:{
selection(){
// 当课程勾选状态发生变化时核算价格
this.getTotalPrice();
},
courseData(){
// 当课程数量发生变化时核算价格
this.getTotalPrice();
},
},
created(){
// 判断是否登录
this.token = sessionStorage.token || localStorage.token;
if( !this.token ){
this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
this.$router.push("/login");
}).catch(()=>{
this.$router.go(-1);
});
}else{
// 获取购物车商品数据
this.$axios.get(this.$settings.Host+"/carts/course/",{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
}
}).then(response=>{
this.courseData = response.data;
// 更新在vuex里面的数据
this.$store.state.cart.count = response.data.length;
// 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
setTimeout(()=>{
let expire_data = [];
this.expire_list.forEach(row=>{
expire_data[row.id] = row.title;
});
// row 就是字典数据[json]
this.courseData.forEach(row => {
// 设置商品课程的选中状态
if(row.is_select){
this.$refs.multipleTable.toggleRowSelection(row);
}
// 调整有效期选项中数值变成文本内容
row.expire = expire_data[row.expire];
});
},0)
}).catch(error=>{
let status = error.response.status;
if( status == 401 ){
this.token = null;
sessionStorage.removeItem("token");
localStorage.removeItem("token");
let _this = this;
this.$alert("您尚未登录或登录超时!请重新登录","警告",{
callback(){
_this.$router.push("/login");
}
});
}
})
}
},
methods:{
getTotalPrice(){
// 核算购物车中所有勾选商品的总价格
let total = 0;
this.selection.forEach(row=>{
total += row.price;
});
// 保留2个小数位
this.total_price = total.toFixed(2);
},
CartDel(course,course_name){
this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
let course_id = course.id;
// 发送请求
this.$axios.delete(this.$settings.Host+"/carts/course/",{
params:{
course_id:course_id,
},
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
let index = this.courseData.indexOf(course);
this.courseData.splice(index,1);
this.$message("删除成功!");
}).catch(error=>{
console.log(error.response);
});
}).catch(()=>{
// 取消操作
});
},
currentSelected(selection,row){
// selection 表示所有被勾选的信息
// row 当前操作的数据
let is_select = true;
if( selection.indexOf(row) == -1 ){
is_select = false;
}
// 获取当前课程ID
let course_id = row.id;
// 切换勾选状态
// 发送请求
this.$axios.put(this.$settings.Host+"/carts/course/",{
course_id: course_id,
is_select: is_select,
},{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
this.$message(response.data.message,"提示");
}).catch(error=>{
console.log(error.response)
})
},
// 更新课程的有效期
ChangeExpire(course){
// 获取课程ID和有效期
let course_id = course.id;
let expire = course.expire
// 发送patch请求更新有效期
this.$axios.patch(this.$settings.Host+"/carts/course/",{
course_id,
expire, // 这里是简写,相当于 expire:expire,
},{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
this.$message(response.data.message,"提示");
});
},
// 获取勾选过的商品课程列表
SelectionChange(data){
this.selection = data;
}
},
components:{Header,Footer}
}
</script>
显示当前课程所属的真实价格
价格策略模型
价格优惠活动类型名称: 限时免费, 限时折扣, 限时减免, 积分抵扣, 满减, 优惠券
公式:
限时免费 原价-原价
限时折扣 原价*0.8
限时减免 原价-减免价
积分抵扣 原价-(积分计算后换算价格) ->> 积分换算比率
满减 原价-(满减计算后换算价格)
优惠券 原价-优惠券价格 -->> 优惠券
模型代码:
from django.db import models
from luffy.utils.models import BaseModel
from datetime import datetime
from decimal import Decimal
# 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_uploader.fields import RichTextUploadingField
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_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
# 使用这个字段的原因
video = models.FileField(upload_to="video", null=True,blank=True,default=None, verbose_name="封面视频")
brief = RichTextUploadingField(max_length=2048, 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 = "专题课程"
def __str__(self):
return "%s" % self.name
def get_course_discount_type(self):
now = datetime.now()
try:
course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
# 获取优惠活动类型
return course_prices.discount.discount_type.name
except:
return ""
def get_course_price(self):
# 获取当前课程的真实价格
# 获取当前课程的价格策略
now = datetime.now()
try:
course_prices = self.prices.get( start_time__lte=now,end_time__gte=now, is_delete=False,is_show=True)
# 价格优惠条件判断,原价大于优惠条件才参与活动
if self.price < course_prices.discount.condition:
return self.price
# 当优惠公式为多行文本,则表示满减
if course_prices.discount.sale[0]=="满":
sale_list = course_prices.discount.sale.split("\r\n")
sale_price_list = []
# 通过遍历提取所有策略项的优惠价格值
for sale_item in sale_list:
sale = int( sale_item[1: sale_item.index("-")] )
sele_price = int( sale_item[sale_item.index("-")+1:] )
if self.price >= sale:
sale_price_list.append( sele_price )
# 当前课程只能享受一个最大优惠
return self.price - max( sale_price_list )
# 当优惠公式为-1,则表示真实价格为0
if course_prices.discount.sale == "-1":
return 0
# 当优惠公式为*开头,则表示折扣
if course_prices.discount.sale[0] == "*":
sale = Decimal( course_prices.discount.sale[1:] )
return self.price * sale
# 当优惠公式为负数,则表示减免
if course_prices.discount.sale[0] == "-":
sale = Decimal( course_prices.discount.sale[1:] )
real_price = self.price - sale
return real_price if real_price > 0 else 0
except:
print("---没有优惠---")
return self.price
def lesson_list(self):
"""获取当前课程的前8个课时展示到列表中"""
# 获取所有章节
chapters_list = self.coursechapters.filter(is_delete=False,is_show=True)
lesson_list = []
if chapters_list:
for chapter in chapters_list:
lessons = chapter.coursesections.filter(is_delete=False,is_show=True)[:4]
if lessons:
for lesson in lessons:
lesson_list.append({
"id":lesson.id,
"name":lesson.name,
"free_trail":lesson.free_trail
})
return lesson_list[:4]
def course_level(self):
"""把课程难度数值转换成文本"""
return self.level_choices[self.level][1]
class Teacher(BaseModel):
"""讲师、导师表"""
role_choices = (
(0, '讲师'),
(1, '导师'),
(2, '班主任'),
)
name = models.CharField(max_length=32, verbose_name="讲师title")
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 = "讲师导师"
def __str__(self):
return "%s" % self.name
class CourseChapter(BaseModel):
"""课程章节"""
course = models.ForeignKey("Course", related_name='coursechapters', 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):
"""课程课时"""
section_type_choices = (
(0, '文档'),
(1, '练习'),
(2, '视频')
)
chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,verbose_name="课程章节")
name = models.CharField(max_length=128,verbose_name = "课时标题")
orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
section_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)
class Meta:
db_table = "ly_course_lesson"
verbose_name = "课程课时"
verbose_name_plural = "课程课时"
def __str__(self):
return "%s-%s" % (self.chapter, self.name)
"""价格相关的模型"""
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 = "课程优惠类型"
def __str__(self):
return "%s" % (self.name)
class PriceDiscount(BaseModel):
"""课程优惠模型"""
discount_type = models.ForeignKey("PriceDiscountType",on_delete=models.CASCADE, related_name='pricediscounts',verbose_name="优惠类型")
discount_name = models.CharField(max_length=150,verbose_name="优惠活动名称")
condition = models.IntegerField(blank=True,default=0,verbose_name="满足优惠的价格条件")
sale = models.TextField(verbose_name="优惠公式",help_text="""
-1表示免费;<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 = "价格优惠策略"
def __str__(self):
return "价格优惠:%s,优惠条件:%s,优惠值:%s" % (self.discount_type.name,self.condition,self.sale)
class CoursePriceDiscount(BaseModel):
"""课程与优惠策略的关系表"""
course = models.ForeignKey("Course",on_delete=models.CASCADE, related_name="prices",verbose_name="课程")
discount = models.ForeignKey("PriceDiscount",on_delete=models.CASCADE,related_name="courses",verbose_name="优惠活动")
start_time = models.DateTimeField(verbose_name="优惠策略的开始时间")
end_time = models.DateTimeField(verbose_name="优惠策略的结束时间")
class Meta:
db_table = "ly_course_price_dicount"
verbose_name="课程与优惠策略的关系表"
verbose_name_plural="课程与优惠策略的关系表"
def __str__(self):
return "优惠: %s,开始时间:%s,结束时间:%s" % (self.discount.discount_name, self.start_time,self.end_time)
执行数据迁移
python manage.py makemigrations
python manage.py migrate
添加测试数据
后端在模型中计算课程真实价格
因为课程的优惠是具有时效性的,所以我们计算价格的时候需要先判断当前优惠是否过期了。
settings/dev.py,代码:
USE_TZ = False
courses/models.py,代码:
from django.db import models
from luffy.utils.models import BaseModel
from datetime import datetime
from decimal import Decimal
# Create your models here.
class CourseCategory(BaseModel):
"""
课程分类
"""
。。。
from ckeditor_uploader.fields import RichTextUploadingField
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_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
# 使用这个字段的原因
video = models.FileField(upload_to="video", null=True,blank=True,default=None, verbose_name="封面视频")
brief = RichTextUploadingField(max_length=2048, 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 = "专题课程"
def __str__(self):
return "%s" % self.name
def get_course_discount_type(self):
now = datetime.now()
try:
course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
# 获取优惠活动类型
return course_prices.discount.discount_type.name
except:
return ""
def get_course_price(self):
# 获取当前课程的真实价格
# 获取当前课程的价格策略
now = datetime.now()
try:
course_prices = self.prices.get( start_time__lte=now,end_time__gte=now, is_delete=False,is_show=True)
# 价格优惠条件判断,原价大于优惠条件才参与活动
if self.price < course_prices.discount.condition:
return self.price
# 当优惠公式为多行文本,则表示满减
if course_prices.discount.sale[0]=="满":
sale_list = course_prices.discount.sale.split("\r\n")
sale_price_list = []
# 通过遍历提取所有策略项的优惠价格值
for sale_item in sale_list:
sale = int( sale_item[1: sale_item.index("-")] )
sele_price = int( sale_item[sale_item.index("-")+1:] )
if self.price >= sale:
sale_price_list.append( sele_price )
# 当前课程只能享受一个最大优惠
return self.price - max( sale_price_list )
# 当优惠公式为-1,则表示真实价格为0
if course_prices.discount.sale == "-1":
return 0
# 当优惠公式为*开头,则表示折扣
if course_prices.discount.sale[0] == "*":
sale = Decimal( course_prices.discount.sale[1:] )
return self.price * sale
# 当优惠公式为负数,则表示减免
if course_prices.discount.sale[0] == "-":
sale = Decimal( course_prices.discount.sale[1:] )
real_price = self.price - sale
return real_price if real_price > 0 else 0
except:
print("---没有优惠---")
return self.price
def lesson_list(self):
"""获取当前课程的前8个课时展示到列表中"""
# 获取所有章节
chapters_list = self.coursechapters.filter(is_delete=False,is_show=True)
lesson_list = []
if chapters_list:
for chapter in chapters_list:
lessons = chapter.coursesections.filter(is_delete=False,is_show=True)[:4]
if lessons:
for lesson in lessons:
lesson_list.append({
"id":lesson.id,
"name":lesson.name,
"free_trail":lesson.free_trail
})
return lesson_list[:4]
def course_level(self):
"""把课程难度数值转换成文本"""
return self.level_choices[self.level][1]
class Teacher(BaseModel):
"""讲师、导师表"""
。。。
class CourseChapter(BaseModel):
"""课程章节"""
。。。
class CourseLesson(BaseModel):
"""课程课时"""
。。。
"""价格相关的模型"""
class PriceDiscountType(BaseModel):
"""课程优惠类型"""
。。。
class PriceDiscount(BaseModel):
"""课程优惠模型"""
。。。
class CoursePriceDiscount(BaseModel):
"""课程与优惠策略的关系表"""
。。。
修改序列化器,增加返回字段[优惠类型和优惠策略]
from rest_framework import serializers
from .models import CourseCategory,Course
class CourseCategoryModelSerializer(serializers.ModelSerializer):
class Meta:
model = CourseCategory
fields = ("id","name")
from .models import Teacher
class TeacherModelSerializer(serializers.ModelSerializer):
class Meta:
model = Teacher
fields = ("id","name","title")
class CourseModelSerializer(serializers.ModelSerializer):
"""课程列表页的序列化器"""
# 默认情况,序列化器转换模型数据时,默认会把外键直接转成主键ID值
# 所以我们需要重新设置在序列化器中针对外键的序列化
# 这种操作就是一个序列器里面调用另一个序列化器了.叫"序列化器嵌套"
teacher = TeacherModelSerializer()
# coursechapters = CourseChapterModelSerializer(many=True)
class Meta:
model = Course
fields = ("id","name","course_img","students","lessons","pub_lessons","price","teacher","lesson_list","get_course_price","get_course_discount_type")
class TeacherDetailModelSerializer(serializers.ModelSerializer):
。。。。
class CourseDetailModelSerializer(serializers.ModelSerializer):
"""课程详情页的序列化器"""
teacher = TeacherDetailModelSerializer()
class Meta:
model = Course
# fields = ("id","name", "video", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief")
fields = ("id","name", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief","get_course_price","get_course_discount_type")
前端课程列表页展示真实课程价格
判断是否有课程类型,如果有,则显示优惠价格。没有则显示课程原价。
Course.vue
<template>
<div class="course">
<Header/>
<div class="main">
<!-- 筛选功能 -->
<div class="top">
<ul class="condition condition1">
<li class="cate-condition">课程分类:</li>
<li class="item" :class="query_params.course_category==0?'current':''" @click="query_params.course_category=0">全部</li>
<li :class="query_params.course_category==catetory.id?'current':''" @click="query_params.course_category=catetory.id" v-for="catetory in catetory_list" :data-key="catetory.id" class="item">{{catetory.name}}</li>
</ul>
<ul class="condition condition2">
<li class="cate-condition">筛 选:</li>
<li class="item" :class="(query_params.ordering=='-id' || query_params.ordering=='id')?'current':''" @click="select_ordering('id')">默认</li>
<li class="item" :class="(query_params.ordering=='-students' || query_params.ordering=='students')?'current':''" @click="select_ordering('students')">人气</li>
<li class="item" :class="query_params.ordering=='price'?'current price':(query_params.ordering=='-price'?'current price2':'')" @click="select_ordering('price')">价格</li>
<li class="course-length">共21个课程</li>
</ul>
</div>
<!-- 课程列表 --->
<div class="list">
<ul>
<li class="course-item" v-for="course in course_list">
<router-link :to="{path: '/detail',query:{id:course.id}}" class="course-link">
<div class="course-cover">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<div class="course-title">
<h3>{{course.name}}</h3>
<span>{{course.students}}人已加入学习</span>
</div>
<p class="teacher">
<span class="info">{{course.teacher.name}} {{course.teacher.title}}</span>
<span class="lesson">共{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':('已更新'+course.pub_lessons+"课时")}}</span>
</p>
<ul class="lesson-list">
<li v-for="lesson,key in course.lesson_list">
<p class="lesson-title">0{{key+1}} | {{lesson.name}}</p>
<span v-if="lesson.free_trail" class="free">免费</span>
</li>
</ul>
<div class="buy-info">
<div v-if="course.get_course_discount_type">
<span class="discount">{{course.get_course_discount_type}}</span>
<span class="present-price">¥{{course.get_course_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="present-price">¥{{course.price}}元</span>
<button class="buy-now">立即购买</button>
</div>
</div>
</router-link>
</li>
</ul>
</div>
<div class="pagination">
<el-pagination
@current-change="handleCurrentChange"
:current-page="query_params.current_page"
background
layout="prev, pager, next"
:page-size="course_page_size"
:total="course_count">
</el-pagination>
</div>
</div>
<Footer/>
</div>
</template>
前端课程详情页展示真实课程的价格
Detail.vue
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<video-player class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)"
>
</video-player>
</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+"课程")}} 难度:{{course.course_level}}</p>
<div v-if="course.get_course_discount_type">
<div class="sale-time">
<p class="sale-type">{{course.get_course_discount_type}}</p>
<p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span> 秒</p>
</div>
<p class="course-price">
<span>活动价</span>
<span class="discount">¥{{course.get_course_price}}</span>
<span class="original">¥{{course.price}}</span>
</p>
</div>
<div v-else>
<div class="sale-time">
<p class="sale-type">价格: ¥{{course.price}}</p>
</div>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买</button>
<button class="free">免费试学</button>
</div>
<div @click="cartAddHander" class="add-cart"><img src="@/assets/cart-yellow.svg" alt="">加入购物车</div>
</div>
</div>
</div>
。。。。
</div>
<Footer/>
</div>
</template>
购物车结算
实现课程详情页倒计时功能
模型返回当前课程优惠的剩余时间戳
courses/models.py中,Course模型新增计算剩余时间的方法,代码:
from ckeditor_uploader.fields import RichTextUploadingField
class Course(BaseModel):
"""
专题课程
"""
。。。
def has_time(self):
"""计算活动的剩余时间"""
now = datetime.now()
try:
course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
# 把 活动结束时间 - 当前时间 = 剩余时间
return int( course_prices.end_time.timestamp() - now.timestamp() )
except:
print("---活动过期了----")
return 0
序列化器,新增返回字段
class CourseDetailModelSerializer(serializers.ModelSerializer):
"""课程详情页的序列化器"""
teacher = TeacherDetailModelSerializer()
class Meta:
model = Course
# fields = ("id","name", "video", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief")
fields = ("id","name", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief","get_course_price","get_course_discount_type","has_time")
前端使用定时器setInterval完成倒计时功能
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
。。。。
<div class="sale-time">
<p class="sale-type">{{course.get_course_discount_type}}</p>
<p class="expire">距离结束:仅剩 {{Math.floor(course.has_time/86400)}}天 {{Math.floor(course.has_time%86400/3600)}}小时 {{Math.floor(course.has_time%86400%3600/60)}}分 <span class="second">{{Math.floor(course.has_time%86400%3600%60)}}</span> 秒</p>
</div>
<p class="course-price">
<span>活动价</span>
<span class="discount">¥{{course.get_course_price}}</span>
<span class="original">¥{{course.price}}</span>
</p>
</div>
<div v-else>
<div class="sale-time">
<p class="sale-type">价格: ¥{{course.price}}</p>
</div>
</div>
。。。。
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
。。。。
created(){
// 获取当前课程ID
this.course_id = this.$route.query.id - 0;
// 判断ID基本有效性
let _this = this;
if( isNaN(this.course_id) || this.course_id < 1 ){
_this.$alert("无效的课程ID!","错误",{
callback(){
_this.$router.go(-1);
}});
}
// 发送请求获取后端课程数据
this.$axios.get(this.$settings.Host+`/courses/detail/${this.course_id}/`).then(response=>{
this.course = response.data;
// 修改视频中的封面图片
this.playerOptions.poster = this.course.course_img;
// 倒计时
if(this.course.has_time > 1){
let timer = setInterval(()=>{
if( this.course.has_time > 1 ){
this.course.has_time-=1;
}else{
clearInterval(timer);
location.reload();
}
},1000);
}
}).catch(error=>{
console.log(error.response)
});
},
。。。。
}
</script>
根据课程有效期调整价格
后端实现提供课程有效期的API
模型代码:
"""课程有效期"""
class CourseTime(BaseModel):
"""课程有效期表"""
timer = models.IntegerField(verbose_name="购买周期",default=30,help_text="单位:天<br>建议按月书写,例如:1个月,则为30.")
title = models.CharField(max_length=150, null=True, blank=True, verbose_name="购买周期的文本提示", default="1个月有效", help_text="要根据上面的购买周期,<br>声明对应的提示内容,<br>展示在购物车商品列表中")
course = models.ForeignKey("Course", on_delete=models.CASCADE, related_name="coursetimes", verbose_name="课程")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
class Meta:
db_table = "ly_course_time"
verbose_name = "课程有效期表"
verbose_name_plural = "课程有效期表"
def __str__(self):
return "课程:%s,周期:%s,价格:%s" % (self.course, self.timer, self.price)
数据迁移
python manage.py makemigrations
python manage.py migrate
把模型注册到xadmin中。
from .models import CourseTime
class CourseTimeModelAdmin(object):
"""课程与价格优惠关系模型管理类"""
list_display = ["course","title","timer","price"]
xadmin.site.register(CourseTime, CourseTimeModelAdmin)
添加测试数据
购物车视图中,返回购买课程的周期列表,cart/views.py,代码
class CartAPIView(APIView):
permission_classes = [IsAuthenticated]
"""购物车视图"""
def get(self,request):
"""获取购物车商品课程列表"""
# 获取当前用户ID
# user_id = 1
user_id = request.user.id
# 通过用户ID获取购物车中的商品信息
redis = get_redis_connection("cart")
cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
# redis里面的所有数据最终都是以bytes类型的字符串保存的
# print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
# print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
# 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
data_list = []
try:
for course_id_bytes,expire_bytes in cart_goods_list.items():
course_id = int( course_id_bytes.decode() )
expire = expire_bytes.decode()
course = Course.objects.get(pk=course_id)
# 获取购买的课程的周期价格列表
expires = course.coursetimes.all()
# 默认具有永久价格
expire_list = [{
"title": "永久有效",
"timer": -1,
"price": course.price
}]
for item in expires:
expire_list.append({
"title":item.title,
"timer":item.timer,
"price":item.price,
})
data_list.append({
"id": course_id,
"expire":expire,
"course_img": course.course_img.url,
"name": course.name,
"price": course.get_course_price(),
"is_select": course_id_bytes in cart_goods_selects,
"expire_list": expire_list,
})
except:
return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# print(data_list)
# 返回查询结果
return Response(data_list,status=status.HTTP_200_OK)
前端展示购物车中每一个商品课程的购买周期,代码:
<template>
。。。。
<el-table-column label="有效期" width="216">
<template slot-scope="scope">
<el-select @change="ChangeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
<el-option v-for="item in scope.row.expire_list" :key="item.timer" :label="item.title" :value="item.timer"></el-option>
</el-select>
</template>
</el-table-column>
。。。。
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
name: "Cart",
data(){
return{
// 注释掉原来的有效周期测试数据
// expire:3,
// expire_list:[]
courseData:[], // 购物车中的商品信息
selection:[], // 购物车中被勾选的商品信息
total_price:0.00,
}
},
。。。。
created(){
// 判断是否登录
this.token = sessionStorage.token || localStorage.token;
if( !this.token ){
this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
this.$router.push("/login");
}).catch(()=>{
this.$router.go(-1);
});
}else{
// 获取购物车商品数据
this.$axios.get(this.$settings.Host+"/carts/course/",{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
}
}).then(response=>{
this.courseData = response.data;
// 更新在vuex里面的数据
this.$store.state.cart.count = response.data.length;
// 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
setTimeout(()=>{
let expire_data = [];
this.courseData.forEach(course=>{
course.expire_list.forEach(row=>{
expire_data[row.timer] = row.title;
});
});
// row 就是字典数据[json]
this.courseData.forEach(row => {
// 设置商品课程的选中状态
if(row.is_select){
this.$refs.multipleTable.toggleRowSelection(row);
}
// 调整有效期选项中数值变成文本内容
row.expire = expire_data[row.expire];
});
},0)
}).catch(error=>{
let status = error.response.status;
if( status == 401 ){
this.token = null;
sessionStorage.removeItem("token");
localStorage.removeItem("token");
let _this = this;
this.$alert("您尚未登录或登录超时!请重新登录","警告",{
callback(){
_this.$router.push("/login");
}
});
}
})
}
},
。。。
}
</script>
当切换课程周期时,后端重新计算价格并返回
模型中计算真实价格时,增加一个原价字段,通过原价字段,判断本次计算是计算周期还是计算永久有效。
from ckeditor_uploader.fields import RichTextUploadingField
class Course(BaseModel):
"""
专题课程
"""
。。。。
def get_course_price(self,price=0):
# 获取当前课程的真实价格
self.price = price if price != 0 else self.price # 判断调用当前方法时,是否定义了价格
。。。。
视图中修改patch方法,在用户切换购买课程周期时,重新计算真实课程价格,代码:
def patch(self,request):
"""更新购物城中的商品信息[切换课程有效期]"""
# 获取当前登录的用户ID
# user_id = 1
user_id = request.user.id
# 获取当前操作的课程ID
course_id = request.data.get("course_id")
# 获取新的有效期
expire = request.data.get("expire")
# 获取redis链接
redis = get_redis_connection("cart")
# 更新购物中商品课程的有效期
redis.hset("cart_%s" % user_id,course_id, expire)
# 根据新的课程有效期获取新的课程原价
try:
coursetime = CourseTime.objects.get(course=course_id, timer=expire)
# 根据新的课程价格,计算真实课程价格
price = coursetime.course.get_course_price(coursetime.price)
except:
# 这里给price设置一个默认值,当值-1,则前段不许要对价格进行调整
course = Course.objects.get(pk=course_id)
price = course.get_course_price()
return Response({
"price": price,
"message": "修改购物车信息成功!"
}, status=status.HTTP_200_OK)
前端获取课程有效期列表
// 更新课程的有效期
ChangeExpire(course){
// 获取课程ID和有效期
let course_id = course.id;
let expire = course.expire;
// 发送patch请求更新有效期
this.$axios.patch(this.$settings.Host+"/carts/course/",{
course_id,
expire, // 这里是简写,相当于 expire:expire,
},{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
// 更新购买的商品课程的价格
course.price = response.data.price;
this.$message(response.data.message,"提示");
});
},
完成上面的步骤以后,切换购买周期时,价格就发生了变化,但是购物车页面刷新时,发现价格还原成"永久有效"的价格。所以我们需要在后端的购物车商品列表api接口中针对价格的购买周期进行判断。
carts/views.py的CartAPIView
def get(self,request):
"""获取购物车商品课程列表"""
# 获取当前用户ID
# user_id = 1
user_id = request.user.id
# 通过用户ID获取购物车中的商品信息
redis = get_redis_connection("cart")
cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
# redis里面的所有数据最终都是以bytes类型的字符串保存的
# print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
# print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
# 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
data_list = []
try:
for course_id_bytes,expire_bytes in cart_goods_list.items():
course_id = int( course_id_bytes.decode() )
expire = expire_bytes.decode()
course = Course.objects.get(pk=course_id)
# 获取购买的课程的周期价格列表
expires = course.coursetimes.all()
# 默认具有永久价格
expire_list = [{
"title": "永久有效",
"timer": -1,
"price": course.price
}]
for item in expires:
expire_list.append({
"title":item.title,
"timer":item.timer,
"price":item.price,
})
try:
# 根据课程有效期传入课程原价
coursetime = CourseTime.objects.get(course=course_id, timer=expire)
# 根据新的课程价格,计算真实课程价格
price= coursetime.price
except:
price = 0
data_list.append({
"id": course_id,
"expire":expire,
"course_img": course.course_img.url,
"name": course.name,
"price": course.get_course_price(price),
"is_select": course_id_bytes in cart_goods_selects,
"expire_list": expire_list,
})
except:
return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# print(data_list)
# 返回查询结果
return Response(data_list,status=status.HTTP_200_OK)
最后,修复在用户切换购买周期时,前端需要重新计算购物车中所有商品的总价格。
// 更新课程的有效期
ChangeExpire(course){
// 获取课程ID和有效期
let course_id = course.id;
let expire = course.expire;
// 发送patch请求更新有效期
this.$axios.patch(this.$settings.Host+"/carts/course/",{
course_id,
expire, // 这里是简写,相当于 expire:expire,
},{
headers:{
// 注意下方的空格!!!
"Authorization":"jwt " + this.token
},
}).then(response=>{
// 更新购买的商品课程的价格
course.price = response.data.price;
// 重新计算购物车中的商品总价
this.getTotalPrice();
this.$message(response.data.message,"提示");
});
},