文章目录

  • 我的订单
    • 后端提供查询当前登录用户的订单列表信息
    • 前端请求获取当前登录用户的订单信息
    • 订单状态显示分析
    • 使用Celery的定时任务来完成订单超时功能
  • 视频播放
    • 使用保利威云视频服务来对视频进行加密
    • 后端获取保利威的视频播放授权token,提供接口api给前端
    • 客户端请求token并播放视频
    • 完善课程详情页的视频内容显示
    • 完善点击课程详情页的立即试学按钮跳转到视频播放页面,并发送视频的播放ID vid
    • 完善API接口的身份认证

我的订单

打通头部子组件的链接,代码:
Header.vue

            <div class="login-box login-box1 full-left"><router-link to="">学习中心</router-link><el-menu width="200" class="member el-menu-demo" mode="horizontal"><el-submenu index="2"><template slot="title"><router-link to=""><img src="/static/image/logo@2x.png" alt=""></router-link></template><el-menu-item index="2-1">我的账户</el-menu-item><el-menu-item index="2-2"><router-link to="/user/order">我的订单</router-link></el-menu-item><el-menu-item index="2-3">我的优惠卷</el-menu-item><el-menu-item index="2-3"><span @click="logoutHander">退出登录</span></el-menu-item></el-submenu></el-menu></div>

前端显示我的订单页面,代码:
user/Order.vue

<template><div class="user-order"><Header/><div class="main"><div class="banner"></div><div class="profile"><div class="profile-info"><div class="avatar"><img class="newImg" width="100%" alt="" src="../../../static/image/logo@2x.png"></div><span class="user-name">Mixtea</span><span class="user-job">深圳市 | 程序员</span></div><ul class="my-item"><li>我的账户</li><li class="active">我的订单</li><li>个人资料</li><li>账号安全</li></ul></div><div class="user-data"><ul class="nav"><li class="order-info">订单</li><li class="course-expire">有效期</li><li class="course-price">课程价格</li><li class="real-price">实付金额</li><li class="order-status">交易状态</li><li class="order-do">交易操作</li></ul><div class="my-order-item"><div class="user-data-header"><span class="order-time">2019-04-02 10:27:49</span><span class="order-num">订单号:<span class="my-older-number">20190402102749606</span></span></div><ul class="nav user-data-list"><li class="order-info"><img src="../../../static/image/course-cover.jpeg" alt=""><div class="order-info-title"><p class="course-title">Pycharm使用秘籍</p><p class="price-service">限时免费</p></div></li><li class="course-expire">永久有效</li><li class="course-price">977.00</li><li class="real-price">577.00</li><li class="order-status">交易成功</li><li class="order-do"><span class="btn btn2">去学习</span></li></ul></div></div></div><Footer/></div>
</template><script>import Header from "../common/Header"import Footer from "../common/Footer"export default{name:"MyOrder",data(){return {};},created(){this.check_login();},methods:{check_login(){// 检查当前访问者是否登录了!let token = localStorage.user_token || sessionStorage.user_token;if( !token ){this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/user/login");});return false; // 阻止代码往下执行}return token;},get_user_order(){// 获取当前登录用户的所有订单},},components:{Header,Footer,}}
</script><style scoped>
.main .banner{width: 100%;height: 324px;background: url(../../../static/image/my_bkging.0648ebe.png) no-repeat;background-size: cover;z-index: 1;
}
.profile{width: 1200px;margin: 0 auto;
}
.profile-info{text-align: center;margin-top: -80px;
}
.avatar{width: 120px;height: 120px;border-radius: 60px;overflow: hidden;margin: 0 auto;
}
.user-name{display: block;font-size: 24px;color: #4a4a4a;margin-top: 14px;
}
.user-job{display: block;font-size: 11px;color: #9b9b9b;}
.my-item{list-style: none;line-height: 1.42857143;color: #333;width: 474px;height: 31px;display: -ms-flexbox;display: flex;cursor: pointer;margin: 41px auto 0;-ms-flex-pack: justify;justify-content: space-between;
}
.my-item .active{border-bottom: 1px solid #000;
}
.user-data{width: 1200px;height: auto;margin: 0 auto;padding-top: 30px;border-top: 1px solid #e8e8e8;margin-bottom: 63px;
}
.nav{width: 100%;height: 60px;background: #e9e9e9;display: -ms-flexbox;display: flex;-ms-flex-align: center;align-items: center;
}
.nav li{margin-left: 20px;margin-right: 28px;height: 60px;line-height: 60px;list-style: none;font-size: 13px;color: #333;border-bottom: 1px solid #e9e9e9;width: 160px;
}
.nav .order-info{ width: 325px; }
.nav .course-expire{ width: 60px; }
.nav .course-price{ width: 130px; }
.user-data-header{display: flex;height: 44px;color: #4a4a4a;font-size: 14px;background: #f3f3f3;-ms-flex-align: center;align-items: center;
}
.order-time{font-size: 12px;display: inline-block;margin-left: 20px;
}
.order-num{font-size: 12px;display: inline-block;margin-left: 29px;
}
.user-data-list{height: 100%;display: flex;
}
.user-data-list{background: none;
}
.user-data-list li{height: 60px;line-height: 60px;
}
.user-data-list .order-info{display: flex;align-items: center;margin-right: 28px;
}
.user-data-list .order-info img{max-width: 100px;max-height: 75px;margin-right: 22px;
}
.course-title{width: 203px;font-size: 13px;color: #333;line-height: 20px;margin-top: -10px;
}
.order-info-title .price-service{line-height: 18px;
}
.price-service{font-size: 12px;color: #fa6240;padding: 0 5px;border: 1px solid #fa6240;border-radius: 4px;margin-top: 4px;position: absolute;
}
.order-info-title{margin-top: -10px;
}
.user-data-list .course-expire{font-size: 12px;color: #ff5502;width: 60px;text-align: center;
}
.btn {width: 100px;height: 32px;font-size: 14px;color: #fff;background: #ffc210;border-radius: 4px;border: none;outline: none;transition: all .25s ease;display: inline-block;line-height: 32px;text-align: center;cursor: pointer;
}
</style>

路由注册:
router/index.js

import Vue from "vue"
import Router from "vue-router"// 这里导入可以让让用户访问的组件
// vue 中提供了@符号,表示src路径
import Home from "@/components/Home"
import Login from "@/components/Login"
import Register from "@/components/Register"
import Course from "@/components/Course"
import Detail from "@/components/Detail"
import Cart from "@/components/Cart"
import Order from "@/components/Order"
import Success from "@/components/Success"
import UserOrder from "@/components/user/Order"
Vue.use(Router);export default new Router({// 设置路由模式为‘history’,去掉默认的#mode: "history",routes:[// 路由列表{path:"/",name:"Home",component:Home},{path:"/home",name:"Home",component:Home},{path:"/user/login",name:"Login",component: Login},{path:"/user/register",name:"Register",component: Register},{path:"/course",name:"Course",component: Course},{path:"/course/:course",name:"Detail",component: Detail},{path:"/cart",name:"Cart",component: Cart},{path:"/order",name:"Order",component: Order},{path:"/pay/result",name:"Success",component: Success},{path:"/user/order",name:"UserOrder",component: UserOrder}]
});

后端提供查询当前登录用户的订单列表信息

orders/models.py,模型新增返回订单状态的文本格式

from django.db import models
from luffyapi.utils.models import BaseModel
from users.models import User
from courses.models import Course
from django.conf import settings
from courses.models import CourseExpireclass Order(BaseModel):"""订单模型"""...def status(self):"""返回订单状态的文本提示"""return self.status_choices[self.order_status][1]def course_list(self):"""订单详情的课程列表"""order_detail = self.order_courses.all()data = []for item in order_detail:# 获取本地订单中用户购买的课程有效期try:course_expire = CourseExpire.objects.get(expire_time=item.expire,course=item.course)expire = course_expire.expire_textexcept:if item.expire == 0:expire = "永久有效"else:raise CourseExpire.DoesNotExist()data.append({"id": item.course.id,"course_name": item.course.name,"course_img": settings.DOMAIL_IMAGE_URL[:-1]+item.course.course_img.url,"expire": expire,"price": item.price,"real_price": item.real_price,"discount_name": item.discount_name})return dataclass 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='0', 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,null=True, default="",blank="", verbose_name="优惠类型")class Meta:db_table="ly_order_detail"verbose_name= "订单详情"verbose_name_plural= "订单详情"def __str__(self):return "%s" % (self.course.name)

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

from orders.models import Order
class UserOrderModelSerializer(serializers.ModelSerializer):class Meta:model = Orderfields = ["id","created_time","order_number","status","order_status","pay_time","course_list"]

users/views.py,视图代码:

from rest_framework.generics import ListAPIView
from orders.models import Order
from .seriazliers import UserOrderModelSerializer
from rest_framework.permissions import IsAuthenticated
class UserOrderAPIView(ListAPIView):"""用户的订单列表"""queryset = Order.objects.filter()serializer_class = UserOrderModelSerializerpermission_classes = [IsAuthenticated]def list(self, request, *args, **kwargs):# 重写列表查询方法,在数据查询的时候新增当前用户的过滤条件queryset = self.filter_queryset(self.get_queryset().filter(user=request.user))page = self.paginate_queryset(queryset)if page is not None:serializer = self.get_serializer(page, many=True)return self.get_paginated_response(serializer.data)serializer = self.get_serializer(queryset, many=True)return Response(serializer.data)

users/urls.py,路由代码:

    path("orders/", views.UserOrderAPIView.as_view() ),

前端请求获取当前登录用户的订单信息

user/Order.vue

<template><div class="user-order"><Header/><div class="main"><div class="banner"></div><div class="profile"><div class="profile-info"><div class="avatar"><img class="newImg" width="100%" alt="" src="../../../static/image/logo@2x.png"></div><span class="user-name">{{user_name}}</span><span class="user-job">深圳市 | 程序员</span></div><ul class="my-item"><li>我的账户</li><li class="active">我的订单</li><li>个人资料</li><li>账号安全</li></ul></div><div class="user-data"><ul class="nav"><li class="order-info">订单</li><li class="course-expire">有效期</li><li class="course-price">课程价格</li><li class="real-price">实付金额</li><li class="order-status">交易状态</li><li class="order-do">交易操作</li></ul><div class="my-order-item" v-for="order in order_list"><div class="user-data-header"><span class="order-time">{{order.created_time|timeformat}}</span><span class="order-num">订单号:<span class="my-older-number">{{order.order_number}}</span></span></div><ul class="nav user-data-list" v-for="course in order.course_list"><li class="order-info"><img :src="course.course_img" :alt="course.course_name"><div class="order-info-title"><p class="course-title">{{course.course_name}}</p><p class="price-service" v-if="course.discount_name">{{course.discount_name}}</p></div></li><li class="course-expire">{{course.expire_text}}</li><li class="course-price">{{course.price.toFixed(2)}}</li><li class="real-price">{{course.real_price.toFixed(2)}}</li><li class="order-status">{{order.status}}</li><li class="order-do"><span class="btn btn2" v-if="order.order_status==1">去学习</span><span class="btn btn2" v-if="order.order_status==0" @click="get_alipay_payment_url(order.order_number)">去支付</span></li></ul></div></div></div><Footer/></div>
</template><script>import Header from "../common/Header"import Footer from "../common/Footer"export default{name:"MyOrder",data(){return {user_name: localStorage.user_name || sessionStorage.user_name,order_list:[]};},created(){this.check_login();this.get_user_order();},methods:{check_login(){// 检查当前访问者是否登录了!let token = localStorage.user_token || sessionStorage.user_token;if( !token ){this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/user/login");});return false; // 阻止代码往下执行}return token;},get_user_order(){// 获取当前登录用户的所有订单this.$axios.get(`${this.$settings.Host}/user/orders/`,{headers:{"Authorization": "jwt " + this.check_login(),}}).then(response=>{this.order_list = response.data;console.log(this.order_list);}).catch(error=>{console.log(error.response);});},get_alipay_payment_url(order_number){// 获取支付宝的支付地址this.$axios.post(`${this.$settings.Host}/payments/${order_number}/alipay/`).then(response=>{console.log(response.data.pay_url);// 页面跳转location.href=response.data.pay_url;}).catch(error=>{console.log(error.response);})}},filters:{timeformat(time){// 时间格式化// 2019/04/02 10:27let current_obj = new Date(time);// 年份let Y = current_obj.getFullYear();// 月份let m = current_obj.getMonth()+1;m = m<10?"0"+m:m;// 日期let d = current_obj.getDate();d = d<10?"0"+d:d;// 小时let H = current_obj.getHours();H = H<10?"0"+H:H;// 分钟let i = current_obj.getMinutes();i = i<10?"0"+i:i;// 秒let s = current_obj.getSeconds();s = s<10?"0"+s:s;return `${Y}/${m}/${d} ${H}:${i}:${s}`;}},components:{Header,Footer,}}
</script>

订单状态显示分析

根据订单状态显示:
1. 如果未支付[order.order_stauts=0],则显示"去支付"按钮
2. 如果已支付[order.order_stauts=1],则显示"去学习"按钮
3. 如果未支付,并超过指定时间[12个小时],则显示"超时取消" [Celery / Django-crontab 定时任务 ]用户下单在12小时以后自动判断订单状态如果是0,则直接改成3定时任务[crontab],主要是依靠:操作系统的定时计划或者第三方软件的定时执行
定时任务的常见场景:1. 订单超时2. 生日邮件[例如,每天凌晨检查当天有没有用户生日,有则发送一份祝福邮件]3. 财务统计[例如,每个月的1号,把当月的订单进行统计,生成一个财务记录,保存到数据库中]4. 页面缓存[例如,把首页设置为每隔5分钟生成一次缓存]

使用Celery的定时任务来完成订单超时功能

Celery官方文档中关于定时任务使用的说明:

http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html

在实现定时任务之前,我们需要先简单使用一下。

我们需要新增一个任务目录,例如order

main.py中,注册任务目录【注意,接下来后面我们使用django的模型处理,所以必须对django的配置进行引入】

import osfrom celery import Celery# 1. 创建示例对象
app = Celery("luffy")# 2. 加载配置
app.config_from_object("celery_tasks.config")
# 3. 注册任务[自动搜索并加载任务]
# 参数必须必须是一个列表,里面的每一个任务都是任务的路径名称
# app.autodiscover_tasks(["任务1","任务2"])
app.autodiscover_tasks(["celery_tasks.sms","celery_tasks.order"])# 4. 在终端下面运行celery命令启动celery
# celery -A 主程序 worker --loglevel=info
# celery -A celery_tasks.main worker --loglevel=info

接下来,在order任务目录下, 创建固定名字的任务文件tasks.py,代码:

from celery_tasks.main import app@app.task(name="check_order")
def check_order():print("检查订单是否过期!!!")

接下来,我们需要把这个任务设置定时任务,所以需要借助Celery本身提供的Crontab模块。

在配置文件中,对定时任务进行注册:
config.py

# 任务队列的链接地址
broker_url = 'redis://127.0.0.1:6379/15'
# 结果队列的链接地址
result_backend = 'redis://127.0.0.1:6379/14'from celery.schedules import crontab
from .main import app
# 定时任务的调度列表,用于注册定时任务
app.conf.beat_schedule = {# Executes every Monday morning at 7:30 a.m.'check_order_outtime': {# 本次调度的任务'task': 'check_order', # 这里的任务名称必须先到main.py中注册# 定时任务的调度周期# 'schedule': crontab(minute=0, hour=0),   # 每周凌晨00:00'schedule': crontab(),   # 每分钟# 'args': (16, 16),  # 注意:任务就是一个函数,所以如果有参数则需要传递},
}

接下来,我们就可以重启Celery并启用Celery的定时任务调度器

先在终端下,运行celery的定时任务程序,以下命令:

celery -A celery_tasks.main beat  # ycelery.main 是celery的主应用文件

然后再新建一个终端,运行以下命令,上面的命令必须先指定:

celery -A celery_tasks.main worker --loglevel=info

注意,使用的时候,如果有时区必须先配置好系统时区。

经过上面的测试以后,我们接下来只需改造上面的任务函数,用于判断修改订单是否超时。

要完成订单的任务功能,如果需要调用django框架的模型操作,那么必须针对django框架进行配置加载和初始化。

main.py,代码:

import osfrom celery import Celery# 1. 创建示例对象
app = Celery("luffy")# 把celery和django进行组合,识别和加载django的配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev')# 在当前clery中启动django框架,对django框架进行进行初始化
import django
django.setup()# 2. 加载配置
app.config_from_object("celery_tasks.config")
# 3. 注册任务[自动搜索并加载任务]
# 参数必须必须是一个列表,里面的每一个任务都是任务的路径名称
# app.autodiscover_tasks(["任务1","任务2"])
app.autodiscover_tasks(["celery_tasks.sms","celery_tasks.order"])# 4. 在终端下面运行celery命令启动celery
# celery -A 主程序 worker --loglevel=info
# celery -A celery_tasks.main worker --loglevel=info

注意,因为在django中是有时区配置的,所以,我们在django框架配置中也要修改时区配置。

任务代码tasks.py的实现:

from celery_tasks.main import app
from orders.models import Order
from datetime import datetime
from django.conf import settings@app.task(name="check_order")
def check_order():# 查询出所有已经超时的订单# 超时条件: 当前时间 > (订单生成时间 + 超时时间)   =====>>>>  (当前时间 - 超时时间) > 订单生成时间now = datetime.now().timestamp()timeout_number = now - settings.ORDER_TIMEOUTtimeout = datetime.fromtimestamp(timeout_number)timeout_order_list = Order.objects.filter(order_status=0, created_time__lte=timeout)for order in timeout_order_list:order.order_status = 3order.save()

配置文件,settings/dev.py,代码:

# 设置订单超时超时的时间[单位: 秒]
ORDER_TIMEOUT = 12 * 60 * 60

重新启动celery的定时任务模块和celery的主应用程序。

视频播放

项目中有两种视频:收费视频[需要加密]和免费视频

使用保利威云视频服务来对视频进行加密

官方网址: http://www.polyv.net/vod/

注意:

开发时通过免费试用注册体验版账号【测试账号的测试有效期是一周】

公司使用酷播尊享版

开发文档地址: http://dev.polyv.net/2017/videoproduct/v-playerapi/html5player/html5-docs/

要开发播放保利威的加密视频功能,需要在用户中心->设置->API接口和加密设置.

http://my.polyv.net/secure/setting/api

配置视频上传加密.

上传视频并记录视频的VID

后端获取保利威的视频播放授权token,提供接口api给前端

参考文档:http://dev.polyv.net/2019/videoproduct/v-api/v-api-play/create-playsafe-token/

根据官方文档的案例,已经有其他人开源了,针对polvy的token生成的python版本了,我们可以直接拿来使用.

libs下创建polyv.py,编写token生成工具函数

from django.conf import settings
import time
import requests
import hashlibclass PolyvPlayer(object):def __init__(self,userId,secretkey,tokenUrl):"""初始化,提供用户id和秘钥"""self.userId = userIdself.secretKey = secretkeyself.tokenUrl = tokenUrldef tomd5(self, value):"""取md5值"""return hashlib.md5(value.encode()).hexdigest()# 获取视频数据的tokendef get_video_token(self, videoId, viewerIp, viewerId=None, viewerName='', extraParams='HTML5'):""":param videoId: 视频id:param viewerId: 看视频用户id:param viewerIp: 看视频用户ip:param viewerName: 看视频用户昵称:param extraParams: 扩展参数:param sign: 加密的sign:return: 返回点播的视频的token"""ts = int(time.time() * 1000)  # 时间戳plain = {"userId": self.userId,'videoId': videoId,'ts': ts,'viewerId': viewerId,'viewerIp': viewerIp,'viewerName': viewerName,'extraParams': extraParams}# 按照ASCKII升序 key + value + key + value... + value 拼接plain_sorted = {}key_temp = sorted(plain)for key in key_temp:plain_sorted[key] = plain[key]print(plain_sorted)plain_string = ''for k, v in plain_sorted.items():plain_string += str(k) + str(v)print(plain_string)# 首尾拼接上秘钥sign_data = self.secretKey + plain_string + self.secretKey# 取sign_data的md5的大写sign = self.tomd5(sign_data).upper()# 新的带有sign的字典plain.update({'sign': sign})# python 提供的发送http请求的模块result = requests.post(url=self.tokenUrl,headers={"Content-type": "application/x-www-form-urlencoded"},data=plain).json()token = {} if isinstance(result, str) else result.get("data", {})return token

配置文件settings/dev.py,代码:

# 保利威视频加密服务
POLYV_CONFIG = {"userId":"62d***3f","secretkey":"h6*****RMU","tokenUrl":"https://hls.videocc.net/service/v1/token",
}

courses/views.py,视图代码:

from luffyapi.libs.polyv import PolyvPlayer
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class PolyvAPIView(APIView):permission_classes = [IsAuthenticated]def get(self, request):"""获取保利威云视频加密播放的token""""""接受客户端的请求参数"""vid = request.query_params.get("vid")  # 视频播放IDremote_addr = request.META.get("REMOTE_ADDR")  # 用户的IPuser_id = request.user.id      # 用户IDuser_name = request.user.username  # 用户名polyv = PolyvPlayer(settings.POLYV_CONFIG["userId"],settings.POLYV_CONFIG["secretkey"],settings.POLYV_CONFIG["tokenUrl"],)data = polyv.get_video_token(vid,remote_addr,user_id,user_name)return Response(data)

courses/urls.py,路由代码:

path(r"polyv/token/",views.PolyvAPIView.as_view()),

客户端请求token并播放视频

在 vue项目的入口文件index.html 中加载保利威视频播放器的js核心类库

<script src='https://player.polyv.net/script/polyvplayer.min.js'></script>

创建视频播放页面的组件Player.vue,代码:

<template><div class="player"><div id="player"></div></div>
</template><script>
export default {name:"Player",data () {return {}},methods: {},computed: {}
}
</script><style scoped>
</style>

前端路由,router/index.js代码:

      {name:"Player",path:"/player",component: Player,},

引入保利威前端HTML5视频播放器代码,Player.vue

<template><div class="player"><div id="player"></div></div>
</template><script>
export default {name:"Player",data () {return {}},methods: {check_login(){// 检查当前访问者是否登录了!let token = localStorage.user_token || sessionStorage.user_token;if( !token ){this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/user/login");});return false; // 阻止代码往下执行}return token;},},mounted(){// 验证用户是否登录let token = this.check_login();let user_name = localStorage.user_name || sessionStorage.user_name;let _this = this;let vid = "d6f2d2d505673e0a75cef00f8d5284f6_d";var player = polyvObject('#player').videoPlayer({wrap: '#player',width: document.documentElement.clientWidth-260, // 页面宽度height: document.documentElement.clientHeight, // 页面高度forceH5: true,vid: vid,code: user_name, // 一般是用户昵称// 视频加密播放的配置playsafe: function (vid, next) { // 向后端发送请求获取加密的token_this.$axios.get(`${_this.$settings.Host}/course/polyv/token/`,{params:{vid: vid,},headers:{"Authorization":"jwt " + token,}}).then(function (response) {console.log(response);next(response.data.token);})}});},computed: {}}
</script><style scoped>
</style>

完善课程详情页的视频内容显示

1. 课程详情中有封面视频播放,所以我们需要在后端的课程模型中新增一个字段course_video
2. 在序列化器中返回的内容增加course_video
3. 在课程详情页组件中显示封面视频或者封面图片

课程详情中有封面视频播放,所以我们需要在后端的课程模型中新增一个字段course_video

courses/models.py,代码:

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", max_length=255, verbose_name="封面视频", blank=True, null=True)# ....

执行数据迁移,

python manage.py makemigrations
python manage.py migrate

在序列化器中返回的内容增加course_video

class CourseRetrieveSerializer(serializers.ModelSerializer):# 课程详情的序列化器teacher = TeacherSerializer()class Meta:model = Coursefields = ["id","name","course_img","course_video","students","lessons","pub_lessons","price","teacher","brief_text","level_name","active_time","discount_type","real_price",]

在课程详情页组件中显示封面视频或者封面图片

<template><div class="detail"><Header/><div class="main"><div class="course-info"><div class="wrap-left"><videoPlayer v-if="course_info.course_video" class="video-player vjs-custom-skin"ref="videoPlayer":playsinline="true":options="playerOptions"@play="onPlayerPlay($event)"@pause="onPlayerPause($event)"></videoPlayer><img v-else class="course_img" :src="course_info.course_img" alt=""></div><div class="wrap-right"><h3 class="course-name">{{course_info.name}}</h3><p class="data">{{course_info.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_info.lessons}}课时/{{course_info.pub_lessons}}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course_info.level_name}}</p><div v-if="course_info.active_time>0"><div class="sale-time"><p class="sale-type">{{course_info.discount_type}}</p><p class="expire">距离结束:仅剩{{day}}天 {{hour}}小时 {{minute}}分 <span class="second">{{second}}</span> 秒</p></div><p class="course-price"><span>活动价</span><span class="discount">¥{{course_info.real_price}}</span><span class="original">¥{{course_info.price}}</span></p></div><div v-else class="sale-time"><p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p><p class="expire"></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(course_info.id)"><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 class="course-brief" v-html="course_info.brief_text"></div></div><div class="tab-item" v-if="tabIndex==2"><div class="tab-item-title"><p class="chapter">课程章节</p><p class="chapter-length">共{{course_chapters.length}}章 {{course_info.lessons}}个课时</p></div><div class="chapter-item" v-for="chapter in course_chapters"><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.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="/static/image/chapter-player.svg"></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_info.teacher.image"><div class="name"><p class="teacher-name">{{course_info.teacher.name}} {{course_info.teacher.title}}</p><p class="teacher-title">{{course_info.teacher.signature}}</p></div></div><p class="narrative" >{{course_info.teacher.brief}}</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 {tabIndex:2,   // 当前选项卡显示的下标course_id: 0, // 当前课程信息的IDcourse_info: {teacher:{},}, // 课程信息course_chapters:[], // 课程的章节课时列表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: "" //你的视频地址(必填)}],poster: "", //视频封面图width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。}}},computed:{day(){let day = parseInt( this.course_info.active_time/ (24*3600));if(day < 10){return '0'+day;}else{return day;}},hour(){let rest = parseInt( this.course_info.active_time % (24*3600) );let hours = parseInt(rest/3600);if(hours < 10){return '0'+hours;}else{return hours;}},minute(){let rest = parseInt( this.course_info.active_time % 3600 );let minute = parseInt(rest/60);if(minute < 10){return '0'+minute;}else{return minute;}},second(){let second = this.course_info.active_time % 60;if(second < 10){return '0'+second;}else{return second;}}},created(){this.get_course_id();this.get_course_data();this.get_chapter();},methods: {onPlayerPlay(event){// 当视频播放时,执行的方法alert("关闭广告")},onPlayerPause(event){// 当视频暂停播放时,执行的方法alert("显示广告");},get_course_id(){// 获取地址栏上面的课程IDthis.course_id = this.$route.params.course;if( this.course_id < 1 ){let _this = this;_this.$alert("对不起,当前视频不存在!","警告",{callback(){_this.$router.go(-1);}});}},get_course_data(){// ajax请求课程信息this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{// console.log(response.data);this.course_info = response.data;this.playerOptions.poster = response.data.course_img;// 在服务端中新增一个模型字段 course_video,如果有视频,则显示到播放器中,如果没有则显示一张封面图片if(response.data.course_video){// this.playerOptions.sources[0].src = "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4";this.playerOptions.sources[0].src = response.data.course_video;}// 在获取到剩余的活动时间以后,就要进入倒计时let timer = setInterval(()=>{if(this.course_info.active_time<1){clearInterval(timer);}else{--this.course_info.active_time;}},1000);}).catch(response=>{this.$message({message:"对不起,访问页面出错!请联系客服工作人员!"});})},get_chapter(){// 获取当前课程对应的章节课时信息this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{params:{"course": this.course_id,}}).then(response=>{this.course_chapters = response.data;}).catch(error=>{console.log(error.response);})},add_cart(course_id){// 添加商品到购物车// 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车let token = localStorage.token || sessionStorage.token;if( !token ){this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/login/");});return false; // 阻止代码往下执行}// 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttokenthis.$axios.post(`${this.$settings.Host}/cart/`,{"course_id": course_id,},{headers:{"Authorization":"jwt " + token,}}).then(response=>{this.$message({message:response.data.message,});// 购物车中的商品数量let total = response.data.total;this.$store.commit("change_total",total)}).catch(error=>{this.$message({message:error.response.data})})}},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-left .course_img{width: 100%;height: 100%;
}
.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>

完善点击课程详情页的立即试学按钮跳转到视频播放页面,并发送视频的播放ID vid

在序列化器中,新增返回2个字段表示当前课时的类型和课时的视频/课件/练习题链接

class CourseLessonSerializer(serializers.ModelSerializer):class Meta:model = CourseLessonfields = ["id","name","duration","free_trail","section_type","section_link"]

Detail.vue,代码:

课时章节:

<template><div class="detail"><Header/><div class="main"><div class="course-info"><div class="wrap-left"><videoPlayer v-if="course_info.course_video" class="video-player vjs-custom-skin"ref="videoPlayer":playsinline="true":options="playerOptions"@play="onPlayerPlay($event)"@pause="onPlayerPause($event)"></videoPlayer><img v-else class="course_img" :src="course_info.course_img" alt=""></div><div class="wrap-right"><h3 class="course-name">{{course_info.name}}</h3><p class="data">{{course_info.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_info.lessons}}课时/{{course_info.pub_lessons}}小时&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course_info.level_name}}</p><div v-if="course_info.active_time>0"><div class="sale-time"><p class="sale-type">{{course_info.discount_type}}</p><p class="expire">距离结束:仅剩{{day}}天 {{hour}}小时 {{minute}}分 <span class="second">{{second}}</span> 秒</p></div><p class="course-price"><span>活动价</span><span class="discount">¥{{course_info.real_price}}</span><span class="original">¥{{course_info.price}}</span></p></div><div v-else class="sale-time"><p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p><p class="expire"></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(course_info.id)"><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 class="course-brief" v-html="course_info.brief_text"></div></div><div class="tab-item" v-if="tabIndex==2"><div class="tab-item-title"><p class="chapter">课程章节</p><p class="chapter-length">共{{course_chapters.length}}章 {{course_info.lessons}}个课时</p></div><div class="chapter-item" v-for="chapter in course_chapters"><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.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="/static/image/chapter-player.svg"></p><button class="try" v-if="lesson.free_trail"><router-link v-if="lesson.section_type==0" :to="{path: '/document',query:{'vid':lesson.section_link}}">立即试学</router-link><router-link v-if="lesson.section_type==1" :to="{path: '/exam',query:{'vid':lesson.section_link}}">立即试学</router-link><router-link v-if="lesson.section_type==2" :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_info.teacher.image"><div class="name"><p class="teacher-name">{{course_info.teacher.name}} {{course_info.teacher.title}}</p><p class="teacher-title">{{course_info.teacher.signature}}</p></div></div><p class="narrative" >{{course_info.teacher.brief}}</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 {tabIndex:2,   // 当前选项卡显示的下标course_id: 0, // 当前课程信息的IDcourse_info: {teacher:{},}, // 课程信息course_chapters:[], // 课程的章节课时列表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: "" //你的视频地址(必填)}],poster: "", //视频封面图width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。}}},computed:{day(){let day = parseInt( this.course_info.active_time/ (24*3600));if(day < 10){return '0'+day;}else{return day;}},hour(){let rest = parseInt( this.course_info.active_time % (24*3600) );let hours = parseInt(rest/3600);if(hours < 10){return '0'+hours;}else{return hours;}},minute(){let rest = parseInt( this.course_info.active_time % 3600 );let minute = parseInt(rest/60);if(minute < 10){return '0'+minute;}else{return minute;}},second(){let second = this.course_info.active_time % 60;if(second < 10){return '0'+second;}else{return second;}}},created(){this.get_course_id();this.get_course_data();this.get_chapter();},methods: {onPlayerPlay(event){// 当视频播放时,执行的方法alert("关闭广告")},onPlayerPause(event){// 当视频暂停播放时,执行的方法alert("显示广告");},get_course_id(){// 获取地址栏上面的课程IDthis.course_id = this.$route.params.course;if( this.course_id < 1 ){let _this = this;_this.$alert("对不起,当前视频不存在!","警告",{callback(){_this.$router.go(-1);}});}},get_course_data(){// ajax请求课程信息this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{// console.log(response.data);this.course_info = response.data;this.playerOptions.poster = response.data.course_img;// 在服务端中新增一个模型字段 course_video,如果有视频,则显示到播放器中,如果没有则显示一张封面图片if(response.data.course_video){// this.playerOptions.sources[0].src = "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4";this.playerOptions.sources[0].src = response.data.course_video;}// 在获取到剩余的活动时间以后,就要进入倒计时let timer = setInterval(()=>{if(this.course_info.active_time<1){clearInterval(timer);}else{--this.course_info.active_time;}},1000);}).catch(response=>{this.$message({message:"对不起,访问页面出错!请联系客服工作人员!"});})},get_chapter(){// 获取当前课程对应的章节课时信息this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{params:{"course": this.course_id,}}).then(response=>{this.course_chapters = response.data;}).catch(error=>{console.log(error.response);})},add_cart(course_id){// 添加商品到购物车// 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车let token = localStorage.token || sessionStorage.token;if( !token ){this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/login/");});return false; // 阻止代码往下执行}// 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttokenthis.$axios.post(`${this.$settings.Host}/cart/`,{"course_id": course_id,},{headers:{"Authorization":"jwt " + token,}}).then(response=>{this.$message({message:response.data.message,});// 购物车中的商品数量let total = response.data.total;this.$store.commit("change_total",total)}).catch(error=>{this.$message({message:error.response.data})})}},components:{Header,Footer,videoPlayer, // 注册组件}
}
</script>

Player.vue,代码:

获取vid视频ID

<template><div class="player"><div id="player"></div></div>
</template><script>
export default {name:"Player",data () {return {}},methods: {check_login(){// 检查当前访问者是否登录了!let token = localStorage.token || sessionStorage.token;if( !token ){this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{this.$router.push("/login/");});return false; // 阻止代码往下执行}return token;},},mounted(){// 验证用户是否登录let token = this.check_login();let user_name = localStorage.user_name || sessionStorage.user_name;let _this = this;let vid = this.$route.query.vid;var player = polyvObject('#player').videoPlayer({wrap: '#player',width: document.documentElement.clientWidth-260, // 页面宽度height: document.documentElement.clientHeight, // 页面高度forceH5: true,vid: vid,code: user_name, // 一般是用户昵称// 视频加密播放的配置playsafe: function (vid, next) { // 向后端发送请求获取加密的token_this.$axios.get(`${_this.$settings.Host}/courses/polyv/token/`,{params:{vid: vid,},headers:{"Authorization":"jwt " + token,}}).then(function (response) {console.log(response);next(response.data.token);})}});},computed: {}}
</script><style scoped>
</style>

完善API接口的身份认证

试学必须在用户登录以后才能进行,所以后端的tokenAPI接口必须保证用户登陆以后,我们已经完成了。

但是有些视频必须购买了以后才能播放!对于这种情况,在用户点击播放的时候,我们在后端返回token时,进行数据库查询,这个用户是否购买了课程,并且课程在有效期范围内!

Python笔记_84_我的订单_使用celery完成订单超时_视频播放_使用保利威视频加密相关推荐

  1. 保利威(polyv)加密视频的使用详解---python(基于drf/vue)

    0. 注册账号,新人会免费用7天 polyv官网地址http://www.polyv.net/vod/ 一. 根据图片步骤,创建视频分类并上传测试视频 a.    b. c.  d.  二. 参数介绍 ...

  2. java filebody bitmap对象_处理 | 保利威帮助中心 - Part 5

    一.简介 目前部分网页使用WebView控件来播放Polyv点播视频或直播视频时,会产生以下问题,在此提供demo供参考方便快速解决问题. 安卓webview demo描述 - demo中实现了点击全 ...

  3. python 录制web视频_Python django框架 web端视频加密的实例详解

    视频加密流程图: 后端获取保利威的视频播放授权token,提供接口api给前端 参考文档:http://dev.polyv.net/2019/videoproduct/v-api/v-api-play ...

  4. Python笔记_81_结算页面_订单模型_优惠券

    文章目录 结算页面 订单模型 把当前子应用注册到xadmin中 后端实现生成订单的api接口 使用django提供的mysql事务操作保证下单过程中的数据一致性 前端请求生成订单 前端请求后端的订单信 ...

  5. python 网页版笔记_【Python笔记】Python网页正文抽取工具

    本文信息本文由方法SEO顾问发表于2016-05-2018:48:27,共 1153 字,转载请注明:[Python笔记]Python网页正文抽取工具_[方法SEO顾问],如果我网站的文章对你有所帮助 ...

  6. python中成绩分析函数_自学Python笔记:用Python做成绩分析(1)

    有朋友会问,刚学了一周,什么是面向对象都还不清楚就可以写程序?还有Python不是写"爬虫"吗? Python是面向对象的语言,函数.模块.数字.字符串都是对象,并且完全支持继承. ...

  7. python笔记_第三周

    python笔记_第三周 第十天 回调函数 回调函数就是一个被作为参数传递的函数把函数a当做一个值 赋值给函数b的形参, 在调用函数b的时候 在函数体内 适当的实际调用函数a, 这个函数a就是回调函数 ...

  8. c++ 冒泡排序_干货|python笔记1-冒泡排序

    面试的时候经常有面试官喜欢问如何进行冒泡排序?这个问题相信可以难倒一批的同学,本篇就详细讲解如何用python进行冒泡排序. 基本原理 01概念: 冒泡排序是一种交换排序,它的基本思想是:两两比较相邻 ...

  9. python微信公众号翻译功能_自学Python笔记:给微信公众号搭建“成绩查询”功能...

    原标题:自学Python笔记:给微信公众号搭建"成绩查询"功能 期末考试 临近年末,全国各地都在上演一场大戏<期末考试>,考完试无论什么样的结果总想尽快看到自己一个学期 ...

最新文章

  1. 抛开设计模式,软件设计的微思考
  2. 二十七、深入==与equals的区别(下篇)
  3. aspxgridview 增加行号
  4. 可持久化平衡树(FHQ Treap)
  5. tc溜溜865手机投屏卡_溜溜 TC Games 官网_专题
  6. leveldb登山之路——cache
  7. 动态规划-hdoj-4832-百度之星2014初赛第二场
  8. http://blog.csdn.net/evankaka/article/details/45155047
  9. linux 命令打印,Linux终端打印命令使用介绍
  10. dicom文件的后缀_DCM文件扩展名 - 什么是.dcm以及如何打开? - ReviverSoft
  11. 2022年烷基化工艺找解析及烷基化工艺考试总结
  12. java 过滤script_Java过滤任意(script,html,style)标签符,返回纯文本--封装类
  13. 优秀的流程图是如何制作的?简单的教程讲解
  14. 程序员的一天:硅谷女程序员 mayuko
  15. 掩码、子网掩码及子网划分
  16. [Office] 段落间距调整
  17. 计算机段落格式解释,职称计算机考试Word教程:Word段落格式
  18. 微信高级群发之预览接口
  19. Apache基于域名、端口、IP的虚拟主机配置(Centos 6.5)
  20. java使用jxl生成excel表格,jsp使用js下载excel文件xls

热门文章

  1. Eclipse安装SVN插件和项目迁入迁出SVN
  2. 苹果wifi网速慢怎么办_通恒泛信助您全屋WiFi覆盖,全家信号满满~
  3. 如何从ip服务器所用系统,如何从0开始打造一个深度学习服务器?
  4. uniapp开发APP实现导航栏顶部搜索功能
  5. 会声会影2023旗舰版中文版永久功能介绍,会声会影版本系统要求配置及使用技巧
  6. 透过实例demo带你认识gRPC
  7. VS2008安装盘整合sp1补丁
  8. 适合 JS 新手学习的开源项目——在 GitHub 学编程
  9. 又发闪电贷价格操纵攻击?—— welnance.finance事件分析
  10. py217-基于Python+django的服装销售商城网站#毕业设计