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

我的订单

打通头部子组件的链接,代码:
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 CourseExpire

class 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_text
            except:
                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 data

class OrderDetail(BaseModel):
    """订单详情"""
    order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单")
    course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程")
    expire = models.IntegerField(default='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 = Order
        fields = ["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 = UserOrderModelSerializer
    permission_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:27
            let 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 os

from 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 os

from 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_TIMEOUT
    timeout = 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 = 3
        order.save()

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

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

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

视频播放

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

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

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

注意:

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

公司使用酷播尊享版

[外链图片转存失败(img-G959S8z0-1566913902007)(assets/1557993340983.png)]

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

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

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

[外链图片转存失败(img-MAVpJ5A9-1566913902009)(assets/1557993716875.png)]

配置视频上传加密.

[外链图片转存失败(img-yxUlfn3q-1566913902011)(assets/1557993844826.png)]

上传视频并记录视频的VID

[外链图片转存失败(img-haVZWXpQ-1566913902012)(assets/1557994524173.png)]

后端获取保利威的视频播放授权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 hashlib

class PolyvPlayer(object):
    def __init__(self,userId,secretkey,tokenUrl):
        """初始化,提供用户id和秘钥"""
        self.userId = userId
        self.secretKey = secretkey
        self.tokenUrl = tokenUrl

    def tomd5(self, value):
        """取md5值"""
        return hashlib.md5(value.encode()).hexdigest()

    # 获取视频数据的token
    def 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")  # 视频播放ID
        remote_addr = request.META.get("REMOTE_ADDR")  # 用户的IP
        user_id = request.user.id      # 用户ID
        user_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 = Course
        fields = ["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, // 当前课程信息的ID
        course_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(){
        // 获取地址栏上面的课程ID
        this.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; // 阻止代码往下执行
        }

        // 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
        this.$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 = CourseLesson
        fields = ["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, // 当前课程信息的ID
        course_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(){
        // 获取地址栏上面的课程ID
        this.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; // 阻止代码往下执行
        }

        // 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
        this.$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时,进行数据库查询,这个用户是否购买了课程,并且课程在有效期范围内!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值