硅谷课堂-智慧星球 Day 13~Day 14——尚硅谷项目笔记 2022 年

硅谷课堂-智慧星球 Day 13~Day 14——尚硅谷项目笔记 2022 年

文章目录

Day 13-公众号点播课程和直播管理

一、实现公众号点播课程相关功能

1、课程列表和详情
1.1、需求说明

(1)点击课程中的分类,根据分类查询课程列表。

需求说明

需求说明

(2)点击去看看,进入课程详情页面。

需求说明

1.2、编写课程列表和详情接口

(1)创建 CourseApiController。

package com.myxh.smart.planet.vod.api;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@Tag(name = "课程 API", description = "课程 API 接口")
@RestController
@RequestMapping("/api/vod/course")
public class CourseApiController
{
    @Autowired
    private CourseService courseService;

    /**
     * 根据课程分类查询课程列表(分页)
     *
     * @param subjectParentId 课程一级分类 id
     * @param current         当前页码
     * @param limit           每页记录数
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据课程分类查询课程列表(分页)", description = "根据课程分类查询课程列表(分页)")
    @GetMapping("{subjectParentId}/{current}/{limit}")
    public Result<Map<String, Object>> findCoursePage(@Parameter(name = "subjectParentId", description = "课程一级分类 ID", required = true) @PathVariable Long subjectParentId,
                                                      @Parameter(name = "current", description = "当前页码", required = true) @PathVariable Long current,
                                                      @Parameter(name = "limit", description = "每页记录数", required = true) @PathVariable Long limit)
    {
        // 封装条件
        CourseQueryVo courseQueryVo = new CourseQueryVo();
        courseQueryVo.setSubjectParentId(subjectParentId);

        // 创建 page 对象
        Page<Course> coursePageParam = new Page<>(current, limit);
        Map<String, Object> coursePageMap = courseService.findCoursePage(coursePageParam, courseQueryVo);
        return Result.ok(coursePageMap);
    }

    /**
     * 根据课程 id 查询课程详情信息
     *
     * @param courseId 课程 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据课程 id 查询课程详情信息", description = "根据课程 id 查询课程详情信息")
    @GetMapping("get/info/{courseId}")
    public Result<Map<String, Object>> getCourseInfo(
            @Parameter(name = "courseId", description = "课程ID", required = true) @PathVariable Long courseId)
    {
        Map<String, Object> courseInfoMap = courseService.getInfoById(courseId);

        return Result.ok(courseInfoMap);
    }
}

(2)编写 CourseService。

package com.myxh.smart.planet.vod.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务类
 * </p>
 */
public interface CourseService extends IService<Course>
{
    /**
     * 根据课程分类查询课程列表(分页)
     *
     * @param coursePageParam 课程页面参数
     * @param courseQueryVo   查询对象
     * @return coursePageMap 课程页面
     */
    Map<String, Object> findCoursePage(Page<Course> coursePageParam, CourseQueryVo courseQueryVo);

    /**
     * 根据课程 id 查询课程详情信息
     *
     * @param courseId 课程 id
     * @return courseInfoMap 课程详情信息
     */
    Map<String, Object> getInfoById(Long courseId);
}

(3)编写 CourseServiceImpl。

package com.myxh.smart.planet.vod.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.model.vod.CourseDescription;
import com.myxh.smart.planet.model.vod.Subject;
import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.vo.vod.ChapterVo;
import com.myxh.smart.planet.vo.vod.CourseQueryVo;
import com.myxh.smart.planet.vo.vod.CourseVo;
import com.myxh.smart.planet.vod.mapper.CourseMapper;
import com.myxh.smart.planet.vod.service.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 服务实现类
 * </p>
 */
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService
{
    @Autowired
    private SubjectService subjectService;

    @Autowired
    private TeacherService teacherService;

    @Autowired
    private CourseDescriptionService courseDescriptionService;

    @Autowired
    private ChapterService chapterService;

    /**
     * 根据课程分类查询课程列表(分页)
     *
     * @param coursePageParam 课程页面参数
     * @param courseQueryVo   查询对象
     * @return coursePageMap 课程页面
     */
    @Override
    public Map<String, Object> findCoursePage(Page<Course> coursePageParam, CourseQueryVo courseQueryVo)
    {
        // 获取条件值
        // 课程名称
        String title = courseQueryVo.getTitle();

        // 二级分类
        Long subjectId = courseQueryVo.getSubjectId();

        // 一级分类
        Long subjectParentId = courseQueryVo.getSubjectParentId();

        // 教师
        Long teacherId = courseQueryVo.getTeacherId();

        // 判断条件值是否为空,封装条件
        QueryWrapper<Course> wrapper = new QueryWrapper<>();

        if (StringUtils.hasLength(title))
        {
            wrapper.like("title", title);
        }

        if (!ObjectUtils.isEmpty(subjectId))
        {
            wrapper.eq("subject_id", subjectId);
        }

        if (!ObjectUtils.isEmpty(subjectParentId))
        {
            wrapper.eq("subject_parent_id", subjectParentId);
        }

        if (!ObjectUtils.isEmpty(teacherId))
        {
            wrapper.eq("teacher_id", teacherId);
        }

        // 调用方法进行条件分页查询
        Page<Course> pages = baseMapper.selectPage(coursePageParam, wrapper);

        // 获取分页数据
        // 总记录数
        long totalCount = pages.getTotal();

        // 总页数
        long totalPage = pages.getPages();

        // 每页数据集合
        List<Course> coursePageRecords = pages.getRecords();

        // 封装其他数据(获取教师名称和课程分类名称)
        coursePageRecords.stream().forEach(this::getTeacherAndSubjectName);

        Map<String, Object> coursePageMap = new HashMap<>();
        coursePageMap.put("totalCount", totalCount);
        coursePageMap.put("totalPage", totalPage);
        coursePageMap.put("records", coursePageRecords);

        return coursePageMap;
    }

    /**
     * 获取教师和分类名称
     *
     * @param course 课程数据
     * @return course 课程数据
     */
    private Course getTeacherAndSubjectName(Course course)
    {
        // 教师名称
        Long teacherId = course.getTeacherId();
        Teacher teacher = teacherService.getById(teacherId);

        if (teacher != null)
        {
            course.getParam().put("teacherName", teacher.getName());
        }

        // 课程分类名称
        Long subjectParentId = course.getSubjectParentId();
        Subject subjectOne = subjectService.getById(subjectParentId);

        if (subjectOne != null)
        {
            course.getParam().put("subjectParentTitle", subjectOne.getTitle());
        }

        Long subjectId = course.getSubjectId();
        Subject subjectTwo = subjectService.getById(subjectId);

        if (subjectTwo != null)
        {
            course.getParam().put("subjectTitle", subjectTwo.getTitle());
        }

        return course;
    }

    /**
     * 根据课程 id 查询课程详情信息
     *
     * @param courseId 课程 id
     * @return courseInfoMap 课程详情信息
     */
    @Override
    public Map<String, Object> getInfoById(Long courseId)
    {
        // 更新浏览数量
        Course course = baseMapper.selectById(courseId);
        course.setViewCount(course.getViewCount() + 1);
        baseMapper.updateById(course);

        // 课程详情数据
        CourseVo courseVo = baseMapper.selectCourseVoById(courseId);

        // 课程章节小节数据
        List<ChapterVo> chapterVoList = chapterService.getNestedTreeList(courseId);

        // 课程描述信息
        QueryWrapper<CourseDescription> wrapper = new QueryWrapper<>();
        wrapper.eq("course_id", courseId);
        CourseDescription courseDescription = courseDescriptionService.getOne(wrapper);

        // 课程所属教师信息
        Teacher teacher = teacherService.getById(course.getTeacherId());

        // 是否购买,后续完善
        Boolean isBuy = false;

        // 封装 map 集合,返回数据
        Map<String, Object> courseInfoMap = new HashMap<>();
        courseInfoMap.put("courseVo", courseVo);
        courseInfoMap.put("chapterVoList", chapterVoList);
        courseInfoMap.put("description", courseDescription != null ? courseDescription.getDescription() : "");
        courseInfoMap.put("teacher", teacher);

        // 是否购买
        courseInfoMap.put("isBuy", isBuy);

        return courseInfoMap;
    }
}

(4)编写 CourseMapper。

package com.myxh.smart.planet.vod.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vo.vod.CourseVo;

/**
 * @author MYXH
 * @date 2023/10/8
 *
 * <p>
 * 课程 Mapper 接口
 * </p>
 */
public interface CourseMapper extends BaseMapper<Course>
{
    /**
     * 根据课程 id 查询课程详情信息
     *
     * @param courseId 课程 id
     * @return courseInfoMap 课程详情信息
     */
    CourseVo selectCourseVoById(Long courseId);
}

(5)编写 CourseMapper.xml。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myxh.smart.planet.vod.mapper.CourseMapper">
    <select id="selectCourseVoById" resultType="com.myxh.smart.planet.vo.vod.CourseVo">
        SELECT
        <include refid="columns"/>
        FROM
        <include refid="tables"/>
        WHERE c.id = #{id}
    </select>

    <sql id="columns">
        c.id,
        c.teacher_id as teacherId,
        c.title,
        c.price,
        c.lesson_num AS lessonNum,
        c.cover,
        c.buy_count AS buyCount,
        c.view_count AS viewCount,
        c.status,
        c.publish_time AS publishTime,
        t.name AS teacherName,
        s1.title AS subjectParentTitle,
        s2.title AS subjectTitle
    </sql>

    <sql id="tables">
        `course` AS c
        LEFT OUTER JOIN `teacher` AS t ON c.teacher_id = t.id
        LEFT OUTER JOIN `subject` AS s1 ON c.subject_parent_id = s1.id
        LEFT OUTER JOIN `subject` AS s2 ON c.subject_id = s2.id
    </sql>
</mapper>
1.3、整合课程列表和详情前端

(1)查看路由文件。

查看路由文件

查看路由文件

(2)创建 js 文件定义接口。

创建 js 文件定义接口

import request from "@/utils/request";

const COURSE_API = "/api/vod/course";

export default {
  /**
   * 课程分页列表
   *
   * @param {number} subjectParentId 课程专业父级 id
   * @param {number} pageNo 页面号
   * @param {number} pageSize 页面大小
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  findPage(subjectParentId, pageNo, pageSize) {
    return request({
      url: `${COURSE_API}/${subjectParentId}/${pageNo}/${pageSize}`,
      method: "get",
    });
  },

  /**
   * 课程详情
   *
   * @param {number} courseId 课程 id
   * @returns {Promise} 返回一个 Promise 对象,表示操作的异步结果
   */
  getInfo(courseId) {
    return request({
      url: `${COURSE_API}/get/info/${courseId}`,
      method: "get",
    });
  },
};

(3)编写页面。

编写页面

course.vue

<template>
  <div>
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <van-image
      width="100%"
      height="200"
      src="https://img-blog.csdnimg.cn/b9f45932f6cf471aa53ee24e5555d9c2.png"
    />
    <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
      <!-- offset:滚动条与底部距离小于 offset 时触发load事件 默认300,因此要改小,否则首次进入一直触发  -->
      <van-list
        v-model="loading"
        :finished="finished"
        finished-text="没有更多了"
        offset="10"
        @load="onLoad"
      >
        <van-card
          v-for="(item, index) in list"
          :key="index"
          :price="item.price"
          :title="item.title"
          :thumb="item.cover"
        >
          <template #tags>
            <van-tag round plain color="#ffe1e1" text-color="#ad0000"
              >课时数:{{ item.lessonNum }}</van-tag
            >
            <br />
            <van-tag round plain color="#ffe1e1" text-color="#ad0000"
              >购买数:{{ item.buyCount }}</van-tag
            >
            <br />
            <van-tag round plain color="#ffe1e1" text-color="#ad0000"
              >访问量:{{ item.viewCount }}</van-tag
            >
          </template>
          <template #footer>
            <van-button size="mini" @click="info(item.id)">去看看</van-button>
          </template>
        </van-card>
      </van-list>
    </van-pull-refresh>
  </div>
</template>

<script>
  import courseAPI from "@/api/course";

  export default {
    name: "Course",

    data() {
      return {
        subjectParentId: 1,
        loading: false,
        finished: false,
        refreshing: false,
        pageNo: 1,
        pageSize: 5,
        pages: 1,
        list: [],
      };
    },

    created() {
      this.subjectParentId = this.$route.params.subjectId;
    },

    methods: {
      onLoad() {
        this.fetchData();
      },

      onRefresh() {
        // 清空列表数据
        this.finished = false;
        this.pageNo = 1;

        // 重新加载数据
        // 将 loading 设置为 true,表示处于加载状态
        this.loading = true;
        this.fetchData();
      },

      fetchData() {
        courseAPI
          .findPage(this.subjectParentId, this.pageNo, this.pageSize)
          .then((response) => {
            console.log(response.data);

            if (this.refreshing) {
              this.list = [];
              this.refreshing = false;
            }

            for (let i = 0; i < response.data.records.length; i++) {
              this.list.push(response.data.records[i]);
            }

            this.pages = response.data.totalPage;
            this.loading = false;

            if (this.pageNo >= this.pages) {
              this.finished = true;
            }

            this.pageNo++;
          });
      },

      info(id) {
        this.$router.push({ path: "/course/info/" + id });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .list {
    li {
      margin: 10px;
      padding-bottom: 5px;
      border-bottom: 1px solid #e5e5e5;

      h1 {
        font-size: 20px;
      }

      .list-box {
        display: flex;
        font-size: 14px;

        ul {
          flex: 1;
          margin: 0;

          li {
            margin: 0;
            border-bottom: none;
          }
        }

        p {
          margin: 0;
          width: 50px;
          align-items: center;
          align-self: flex-end;
        }
      }
    }
  }
</style>

courseInfo.vue

<template>
  <div>
    <van-image width="100%" height="200" :src="courseVo.cover" />
    <van-row>
      <van-col span="8">
        <div class="course_count">
          <h1>购买数</h1>
          <p>{{ courseVo.buyCount }}</p>
        </div>
      </van-col>
      <van-col span="8">
        <div class="course_count">
          <h1>课时数</h1>
          <p>{{ courseVo.lessonNum }}</p>
        </div>
      </van-col>
      <van-col span="8">
        <div class="course_count">
          <h1>浏览数</h1>
          <p>{{ courseVo.viewCount }}</p>
        </div>
      </van-col>
    </van-row>

    <h1 class="van-ellipsis course_title">{{ courseVo.title }}</h1>

    <div class="course_teacher_price_box">
      <div class="course_teacher_price">
        <div class="course_price">价格:</div>
        <div class="course_price_number">¥{{ courseVo.price }}</div>
      </div>
      <div>
        <van-button
          @click="see()"
          v-if="isBuy || courseVo.price == '0.00'"
          plain
          type="warning"
          size="mini"
          >立即观看</van-button
        >
        <van-button @click="buy" v-else plain type="warning" size="mini"
          >立即购买</van-button
        >
      </div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_teacher_box">
        <div class="course_teacher">教师: {{ teacher.name }}</div>
        <van-image :src="teacher.avatar" round width="50px" height="50px" />
      </div>
    </div>

    <div class="course_contents">
      <div class="course_title_font">课程详情</div>
      <van-divider :style="{ margin: '5px 0 ' }" />
      <div class="course_content" v-html="description"></div>

      <div class="course_title_font">课程大纲</div>
      <div class="gap"></div>
      <van-collapse v-model="activeNames">
        <van-collapse-item
          :title="item.title"
          :name="item.id"
          v-for="item in chapterVoList"
          :key="item.id"
        >
          <ul
            class="course_chapter_list"
            v-for="child in item.children"
            :key="child.id"
          >
            <h2>{{ child.title }}</h2>
            <p v-if="child.isFree == 1">
              <van-button @click="play(child)" type="warning" size="mini" plain
                >免费观看</van-button
              >
            </p>
            <p v-else>
              <van-button @click="play(child)" type="warning" size="mini" plain
                >观看</van-button
              >
            </p>
          </ul>
        </van-collapse-item>
      </van-collapse>
    </div>
    <van-loading vertical="true" v-show="loading">加载中...</van-loading>
  </div>
</template>

<script>
  import courseAPI from "@/api/course";
  import shareAPI from "@/api/share";
  import wxShare from "@/utils/wxShare";

  export default {
    data() {
      return {
        loading: false,
        courseId: null,
        courseVo: {},
        description: "",
        teacher: {},
        chapterVoList: [],
        isBuy: false,
        activeNames: ["1"],
      };
    },

    created() {
      this.courseId = this.$route.params.courseId;
      this.fetchData();
    },

    methods: {
      fetchData() {
        this.loading = true;
        courseAPI.getInfo(this.courseId).then((response) => {
          console.log(response.data);
          this.courseVo = response.data.courseVo;
          this.description = response.data.description;
          this.isBuy = response.data.isBuy;
          this.chapterVoList = response.data.chapterVoList;
          this.teacher = response.data.teacher;
          this.loading = false;

          //分享注册
          this.wxRegister();
        });
      },

      buy() {
        this.$router.push({ path: "/trade/" + this.courseId });
      },

      play(video) {
        let videoId = video.id;
        let isFree = video.isFree;

        if (isFree === 1 || this.isBuy || this.courseVo.price == "0.00") {
          this.$router.push({
            path: "/play/" + this.courseId + "/" + videoId,
          });
        } else {
          // this.$router.push({ path: "/play/" + this.courseId + "/" + videoId });

          if (window.confirm("购买了才可以观看, 是否继续?")) {
            this.buy();
          }
        }
      },

      see() {
        this.$router.push({ path: "/play/" + this.courseId + "/0" });
      },

      wxRegister() {
        // 说明:后台加密 url 必须与当前页面 url 一致
        let url = window.location.href.replace("#", "smartplanet");
        shareAPI.getSignature(url).then((response) => {
          console.log(response.data);
          // 记录分享用户
          let link = "";

          if (window.location.href.indexOf("?") != -1) {
            link =
              window.location.href + "&recommend=" + response.data.userEedId;
          } else {
            link =
              window.location.href + "?recommend=" + response.data.userEedId;
          }

          let option = {
            title: this.courseVo.title,
            desc: this.description,
            link: link,
            imgUrl: this.courseVo.cover,
          };

          wxShare.wxRegister(response.data, option);
        });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .gap {
    height: 10px;
  }
  ::v-deep.van-image {
    display: block;
  }
  .course_count {
    background-color: #82848a;
    color: white;
    padding: 5px;
    text-align: center;
    border-right: 1px solid #939393;
    h1 {
      font-size: 14px;
      margin: 0;
    }
    p {
      margin: 0;
      font-size: 16px;
    }
  }

  .course_title {
    font-size: 20px;
    margin: 10px;
  }

  .course_teacher_price_box {
    margin: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .course_teacher_price {
      display: flex;
      font-size: 14px;
      align-items: center;
      .course_price_number {
        color: red;
        font-size: 18px;
        font-weight: bold;
      }

      .course_teacher {
        margin-left: 20px;
      }
    }
    .course_teacher_box {
      display: flex;
      justify-content: center;
      align-items: center;

      .course_teacher {
        margin-right: 20px;
      }
    }
  }

  .course_contents {
    margin: 10px;
    .course_title_font {
      color: #68cb9b;
      font-weight: bold;
    }
    .course_content {
      margin-bottom: 20px;
    }
  }

  .course_chapter_list {
    display: flex;
    justify-content: space-between;
    align-items: center;
    h2 {
      font-size: 14px;
    }
    p {
      margin: 0;
    }
  }
</style>
2、点播视频播放

点播视频播放

1.1、获取视频播放参数

(1)创建 VodApiController。

package com.myxh.smart.planet.vod.api;

import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vod.service.VodService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/20
 */
@Tag(name = "腾讯云点播 API", description = "腾讯云点播 API 接口")
@RestController
@RequestMapping("/api/vod")
public class VodApiController
{
    @Autowired
    private VodService vodService;

    /**
     * 获取播放凭证
     *
     * @param courseId 课程 id
     * @param videoId  视频 id
     * @return Result 全局统一返回结果
     */
    @Operation()
    @GetMapping("get/play/auth/{courseId}/{videoId}")
    public Result<Map<String, Object>> getPlayAuth(
            @Parameter(name = "courseId", description = "课程ID", required = true)
            @PathVariable("courseId") Long courseId,
            @Parameter(name = "videoId", description = "视频ID", required = true)
            @PathVariable("videoId") Long videoId)
    {
        Map<String, Object> playAuthMap = vodService.getPlayAuth(courseId, videoId);

        return Result.ok(playAuthMap);
    }
}

(2)application.properties 添加。

# 设置点播账号的子应用 appID
tencent.video.appid=1315007088

(3)service-vod 引入依赖。

<!-- java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.0</version>
</dependency>

(4)创建 Psign 类。

package com.myxh.smart.planet.vod.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.HashMap;

/**
 * @author MYXH
 * @date 2023/10/21
 */
public class Psign
{
    public static String getPsign(Integer appId, String fileId)
    {
        // 点播应用 appId
        Integer AppId = appId;

        // 点播文件 ID
        String FileId = fileId;

        // 播放的音视频类型,可选值
        // RawAdaptive:未加密的,转自适应码流,输出。
        // ProtectedAdaptive:私有加密或 DRM 保护的,转自适应码流,输出
        // Transcode:转码后输出
        // Original:上传的原始音视频
        String AudioVideoType = "RawAdaptive";

        Integer RawAdaptiveDefinition = 10;
        Integer ImageSpriteDefinition = 10;

        // 派发签名当前 Unix 时间戳
        Integer CurrentTime = Math.toIntExact(System.currentTimeMillis() / 1000);

        // 播放密钥
        String PlayKey = "cw8EdW6d9indO6f3Uk6s";

        HashMap<String, Object> urlAccessInfo = new HashMap<>();

        HashMap<String, Object> contentInfo = new HashMap<>();
        contentInfo.put("audioVideoType", AudioVideoType);
        contentInfo.put("rawAdaptiveDefinition", RawAdaptiveDefinition);
        contentInfo.put("imageSpriteDefinition", ImageSpriteDefinition);


        Algorithm algorithm = Algorithm.HMAC256(PlayKey);

        String token = JWT.create()
                .withClaim("appId", AppId)
                .withClaim("fileId", FileId)
                .withClaim("contentInfo", contentInfo)
                .withClaim("currentTimeStamp", CurrentTime)
                .withClaim("urlAccessInfo", urlAccessInfo).sign(algorithm);

        return token;
    }
}

(5)VodService 创建方法。

package com.myxh.smart.planet.vod.service;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/11
 */
public interface VodService
{
    /**
     * 获取播放凭证
     *
     * @param courseId 课程 id
     * @param videoId  视频 id
     * @return playAuthMap 播放凭证
     */
    Map<String, Object> getPlayAuth(Long courseId, Long videoId);
}

(6)VodServiceImpl 实现方法。

package com.myxh.smart.planet.vod.service.impl;

import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.vod.Video;
import com.myxh.smart.planet.vod.service.VideoService;
import com.myxh.smart.planet.vod.service.VodService;
import com.myxh.smart.planet.vod.utils.Psign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/11
 */
@Service
public class VodServiceImpl implements VodService
{
    // @Lazy 注解可以延迟 Bean 的初始化,缓解循环引用
    @Lazy
    @Autowired
    private VideoService videoService;

    @Value("${tencent.video.appid}")
    private String appId;

    /**
     * 获取播放凭证
     *
     * @param courseId 课程 id
     * @param videoId  视频 id
     * @return playAuthMap 播放凭证
     */
    @Override
    public Map<String, Object> getPlayAuth(Long courseId, Long videoId)
    {
        // 根据小节 id 获取小节对象,获取腾讯云视频 id
        Video video = videoService.getById(videoId);

        if (video == null)
        {
            throw new SmartPlanetException(20001, "课程视频信息不存在");
        }

        String videoSourceId = video.getVideoSourceId();
        Integer id = Integer.valueOf(appId);
        String psign = Psign.getPsign(id, videoSourceId);

        Map<String, Object> playAuthMap = new HashMap<>();
        playAuthMap.put("videoSourceId", videoSourceId);
        playAuthMap.put("appId", appId);
        playAuthMap.put("psign", psign);

        return playAuthMap;
    }
}
1.2、整合点播视频播放前端

整合点播视频播放前端

整合点播视频播放前端

(1)创建 js 定义接口。

创建 js 定义接口

import request from "@/utils/request";
const api_name = "/api/vod";
export default {
  // 获取播放凭证
  getPlayAuth(courseId, videoId) {
    return request({
      url: `${api_name}/getPlayAuth/${courseId}/${videoId}`,
      method: "get",
    });
  },
};

(2)courseInfo.vue 修改 play 方法。

<template>
  <div>
    <van-image width="100%" height="200" :src="courseVo.cover" />
    <van-row>
      <van-col span="8">
        <div class="course_count">
          <h1>购买数</h1>
          <p>{{ courseVo.buyCount }}</p>
        </div>
      </van-col>
      <van-col span="8">
        <div class="course_count">
          <h1>课时数</h1>
          <p>{{ courseVo.lessonNum }}</p>
        </div>
      </van-col>
      <van-col span="8">
        <div class="course_count">
          <h1>浏览数</h1>
          <p>{{ courseVo.viewCount }}</p>
        </div>
      </van-col>
    </van-row>

    <h1 class="van-ellipsis course_title">{{ courseVo.title }}</h1>

    <div class="course_teacher_price_box">
      <div class="course_teacher_price">
        <div class="course_price">价格:</div>
        <div class="course_price_number">¥{{ courseVo.price }}</div>
      </div>
      <div>
        <van-button
          @click="see()"
          v-if="isBuy || courseVo.price == '0.00'"
          plain
          type="warning"
          size="mini"
          >立即观看</van-button
        >
        <van-button @click="buy" v-else plain type="warning" size="mini"
          >立即购买</van-button
        >
      </div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_teacher_box">
        <div class="course_teacher">教师: {{ teacher.name }}</div>
        <van-image :src="teacher.avatar" round width="50px" height="50px" />
      </div>
    </div>

    <div class="course_contents">
      <div class="course_title_font">课程详情</div>
      <van-divider :style="{ margin: '5px 0 ' }" />
      <div class="course_content" v-html="description"></div>

      <div class="course_title_font">课程大纲</div>
      <div class="gap"></div>
      <van-collapse v-model="activeNames">
        <van-collapse-item
          :title="item.title"
          :name="item.id"
          v-for="item in chapterVoList"
          :key="item.id"
        >
          <ul
            class="course_chapter_list"
            v-for="child in item.children"
            :key="child.id"
          >
            <h2>{{ child.title }}</h2>
            <p v-if="child.isFree == 1">
              <van-button @click="play(child)" type="warning" size="mini" plain
                >免费观看</van-button
              >
            </p>
            <p v-else>
              <van-button @click="play(child)" type="warning" size="mini" plain
                >观看</van-button
              >
            </p>
          </ul>
        </van-collapse-item>
      </van-collapse>
    </div>
    <van-loading vertical="true" v-show="loading">加载中...</van-loading>
  </div>
</template>

<script>
  import courseAPI from "@/api/course";
  import shareAPI from "@/api/share";
  import wxShare from "@/utils/wxShare";

  export default {
    data() {
      return {
        loading: false,
        courseId: null,
        courseVo: {},
        description: "",
        teacher: {},
        chapterVoList: [],
        isBuy: false,
        activeNames: ["1"],
      };
    },

    created() {
      this.courseId = this.$route.params.courseId;
      this.fetchData();
    },

    methods: {
      fetchData() {
        this.loading = true;
        courseAPI.getInfo(this.courseId).then((response) => {
          console.log(response.data);
          this.courseVo = response.data.courseVo;
          this.description = response.data.description;
          this.isBuy = response.data.isBuy;
          this.chapterVoList = response.data.chapterVoList;
          this.teacher = response.data.teacher;
          this.loading = false;

          //分享注册
          this.wxRegister();
        });
      },

      buy() {
        this.$router.push({ path: "/trade/" + this.courseId });
      },

      play(video) {
        let videoId = video.id;
        let isFree = video.isFree;

        if (isFree === 1 || this.isBuy || this.courseVo.price == "0.00") {
          this.$router.push({ path: "/play/" + this.courseId + "/" + videoId });
        } else {
          // this.$router.push({ path: "/play/" + this.courseId + "/" + videoId });

          if (window.confirm("购买了才可以观看, 是否继续?")) {
            this.buy();
          }
        }
      },

      see() {
        this.$router.push({ path: "/play/" + this.courseId + "/0" });
      },

      wxRegister() {
        // 说明:后台加密 url 必须与当前页面 url 一致
        let url = window.location.href.replace("#", "smartplanet");
        shareAPI.getSignature(url).then((response) => {
          console.log(response.data);
          // 记录分享用户
          let link = "";

          if (window.location.href.indexOf("?") != -1) {
            link =
              window.location.href + "&recommend=" + response.data.userEedId;
          } else {
            link =
              window.location.href + "?recommend=" + response.data.userEedId;
          }

          let option = {
            title: this.courseVo.title,
            desc: this.description,
            link: link,
            imgUrl: this.courseVo.cover,
          };

          wxShare.wxRegister(response.data, option);
        });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .gap {
    height: 10px;
  }
  ::v-deep.van-image {
    display: block;
  }
  .course_count {
    background-color: #82848a;
    color: white;
    padding: 5px;
    text-align: center;
    border-right: 1px solid #939393;
    h1 {
      font-size: 14px;
      margin: 0;
    }
    p {
      margin: 0;
      font-size: 16px;
    }
  }

  .course_title {
    font-size: 20px;
    margin: 10px;
  }

  .course_teacher_price_box {
    margin: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .course_teacher_price {
      display: flex;
      font-size: 14px;
      align-items: center;
      .course_price_number {
        color: red;
        font-size: 18px;
        font-weight: bold;
      }

      .course_teacher {
        margin-left: 20px;
      }
    }
    .course_teacher_box {
      display: flex;
      justify-content: center;
      align-items: center;

      .course_teacher {
        margin-right: 20px;
      }
    }
  }

  .course_contents {
    margin: 10px;
    .course_title_font {
      color: #68cb9b;
      font-weight: bold;
    }
    .course_content {
      margin-bottom: 20px;
    }
  }

  .course_chapter_list {
    display: flex;
    justify-content: space-between;
    align-items: center;
    h2 {
      font-size: 14px;
    }
    p {
      margin: 0;
    }
  }
</style>

(3)index.html 引入文件。

index.html 引入文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,,chrome=1" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no"
    />
    <title>智慧星球</title>
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />

    <script
      src="http://res2.wx.qq.com/open/js/jweixin-1.6.0.js"
      type="text/javascript"
    ></script>

    <!-- 引入播放器 css 文件 -->
    <link
      href="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/tcplayer.min.css"
      rel="stylesheet"
    />

    <!-- 如果需要在 Chrome 和 Firefox 等现代浏览器中通过 H5 播放 HLS 协议的视频,需要在 tcplayer.vx.x.x.min.js 之前引入 hls.min.x.xx.xm.js。 -->
    <script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/hls.min.1.1.6.js"></script>

    <!-- 如果需要在 Chrome 和 Firefox 等现代浏览器中通过 H5 播放 FLV 格式的视频,需要在 tcplayer.vx.x.x.min.js 之前引入 flv.min.x.x.x.js。 -->
    <script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/flv.min.1.6.3.js"></script>

    <!-- 如果需要在 Chrome 和 Firefox 等现代浏览器中通过 H5 播放 DASH 视频,需要在 tcplayer.vx.x.x.min.js 之前引入 dash.min.x.x.x.js。 -->
    <script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/libs/dash.all.min.4.5.2.js"></script>

    <!-- 引入播放器 js 文件 -->
    <script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.7.2/tcplayer.v4.7.2.min.js"></script>
  </head>

  <body>
    <script
      type="text/javascript"
      src="https://static-1.talk-fun.com/open/TalkFun_SDK_Pack/v7.0/TalkFunWebSDK-7.7.min.js"
    ></script>

    <noscript>
      <strong
        >We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
        properly without JavaScript enabled. Please enable it to
        continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

(4)创建 play.vue 页面。

<template>
  <div>
    <video
      id="player-container-id"
      preload="auto"
      width="400"
      height="280"
      playsinline
      webkit-playsinline
      x5-playsinline
    ></video>
    <h1 class="van-ellipsis course_title">{{ courseVo.title }}</h1>

    <div class="course_teacher_price_box">
      <div class="course_teacher_price">
        <div class="course_price">价格:</div>
        <div class="course_price_number">¥{{ courseVo.price }}</div>
        <div class="course_teacher">教师: {{ courseVo.teacherName }}</div>
      </div>
      <div>
        <van-button
          @click="getPlayAuth('0')"
          v-if="isBuy || courseVo.price == '0.00'"
          plain
          type="warning"
          size="mini"
          >立即观看</van-button
        >
        <van-button @click="buy" v-else plain type="warning" size="mini"
          >立即购买</van-button
        >
      </div>
    </div>

    <div class="course_contents">
      <div class="course_title_font">课程大纲</div>
      <div class="gap"></div>
      <van-collapse v-model="activeNames">
        <van-collapse-item
          :title="item.title"
          :name="item.id"
          v-for="item in chapterVoList"
          :key="item.id"
        >
          <ul
            class="course_chapter_list"
            v-for="child in item.children"
            :key="child.id"
          >
            <h2 :style="activeVideoId == child.id ? 'color:blue' : ''">
              {{ child.title }}
            </h2>
            <p v-if="child.isFree == 1">
              <van-button @click="see(child)" type="warning" size="mini" plain
                >免费观看</van-button
              >
            </p>
            <p v-else>
              <van-button @click="see(child)" type="warning" size="mini" plain
                >观看</van-button
              >
            </p>
          </ul>
        </van-collapse-item>
      </van-collapse>
    </div>

    <van-loading vertical="true" v-show="loading">加载中...</van-loading>
  </div>
</template>

<script>
  import courseAPI from "@/api/course";
  import vodAPI from "@/api/vod";
  import videoVisitorAPI from "@/api/videoVisitor";

  export default {
    data() {
      return {
        loading: false,
        courseId: null,
        videoId: null,
        courseVo: {},
        description: "",
        chapterVoList: [],
        isBuy: false,
        // firstVideo: null,
        activeNames: ["1"],

        // 记录当前正在播放的视频
        activeVideoId: 0,
        player: null,
      };
    },

    created() {
      this.courseId = this.$route.params.courseId;
      this.videoId = this.$route.params.videoId || "0";
      this.fetchData();
      this.getPlayAuth(this.videoId);
    },

    methods: {
      fetchData() {
        this.loading = true;
        courseAPI.getInfo(this.courseId).then((response) => {
          console.log(response.data);
          this.courseVo = response.data.courseVo;
          this.description = response.data.description;
          this.isBuy = response.data.isBuy;
          this.chapterVoList = response.data.chapterVoList;
          this.loading = false;
        });
      },

      see(video) {
        let videoId = video.id;
        let isFree = video.isFree;
        this.getPlayAuth(videoId);

        if (isFree === 1 || this.isBuy || this.courseVo.price == "0.00") {
          this.getPlayAuth(videoId);
        } else {
          if (window.confirm("购买了才可以观看, 是否继续?")) {
            this.buy();
          }
        }
      },

      buy() {
        this.$router.push({ path: "/trade/" + this.courseId });
      },

      getPlayAuth(videoId) {
        if (this.player != null) {
          // 是销毁之前的视频,不销毁的话,它会一直存在
          this.player.dispose();
        }

        vodAPI.getPlayAuth(this.courseId, videoId).then((response) => {
          console.log(response.data);
          this.play(response.data);

          // 展开章节
          this.activeNames = [response.data.chapterId];
          // 选中播放视频
          this.activeVideoId = response.data.videoId;
        });
      },

      // 视频播放
      play(data) {
        // window.location = './video.html?fileID='+data.videoSourceId+'&appID='+data.appId;
        var player = TCPlayer("player-container-id", {
          // player-container-id 为播放器容器 ID,必须与 html 中一致
          // 请传入需要播放的视频 fileID 必须
          fileID: data.videoSourceId,

          // 请传入点播账号的子应用 appID 必须
          appID: data.appId,

          psign: data.psign,

          // 其他参数请在开发文档中查看
        });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .gap {
    height: 10px;
  }

  ::v-deep.van-image {
    display: block;
  }

  .course_count {
    background-color: #82848a;
    color: white;
    padding: 5px;
    text-align: center;
    border-right: 1px solid #939393;

    h1 {
      font-size: 14px;
      margin: 0;
    }

    p {
      margin: 0;
      font-size: 16px;
    }
  }

  .course_title {
    font-size: 20px;
    margin: 10px;
  }

  .course_teacher_price_box {
    margin: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .course_teacher_price {
      display: flex;
      font-size: 14px;
      align-items: center;

      .course_price_number {
        color: red;
        font-size: 18px;
        font-weight: bold;
      }

      .course_teacher {
        margin-left: 20px;
      }
    }
  }

  .course_contents {
    margin: 10px;

    .course_title_font {
      color: #68cb9b;
      font-weight: bold;
    }

    .course_content {
      margin-bottom: 20px;
    }
  }

  .course_chapter_list {
    display: flex;
    justify-content: space-between;
    align-items: center;

    h2 {
      font-size: 14px;
    }

    p {
      margin: 0;
    }
  }
</style>
3、付费观看点播课程接口
3.1、需求介绍

(1)点击课程详情页立即购买。

需求介绍

(2)点击确认下单,生成课程订单。

需求介绍

3.2、编写创建订单接口

(1)创建 OrderInfoApiController。

package com.myxh.smart.planet.order.api;

import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.order.OrderFormVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "订单信息 API", description = "订单信息 API 接口")

@RestController
@RequestMapping("api/order/order/info")
public class OrderInfoApiController
{
    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 新增点播课程订单
     *
     * @param orderFormVo 订单信息
     * @param request     请求
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "新增点播课程订单", description = "新增点播课程订单")
    @PostMapping("submit/order")
    public Result<Long> submitOrder(@RequestBody OrderFormVo orderFormVo, HttpServletRequest request)
    {
        // 返回订单 id
        Long orderId = orderInfoService.submitOrder(orderFormVo);

        return Result.ok(orderId);
    }
}

(2)编写 Service。

OrderInfoService

package com.myxh.smart.planet.order.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.order.OrderInfo;
import com.myxh.smart.planet.vo.order.OrderFormVo;

/**
 * @author MYXH
 * @date 2023/10/14
 *
 * <p>
 * 订单信息 服务类
 * </p>
 */
public interface OrderInfoService extends IService<OrderInfo>
{
    /**
     * 新增点播课程订单
     *
     * @param orderFormVo 订单信息
     * @return orderId 订单 id
     */
    Long submitOrder(OrderFormVo orderFormVo);
}
3.3、创建获取课程信息接口

操作 service-vod 模块。

(1)CourseApiController 添加方法。

package com.myxh.smart.planet.vod.api;

import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.vod.service.CourseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@Tag(name = "课程 API", description = "课程 API 接口")
@RestController
@RequestMapping("/api/vod/course")
public class CourseApiController
{
    @Autowired
    private CourseService courseService;

    /**
     * 根据课程 id 查询课程信息
     *
     * @param courseId 课程 id
     * @return 课程信息
     */
    @Operation(summary = "根据课程 id 查询课程信息", description = "根据课程 id 查询课程信息")
    @GetMapping("inner/get/by/id/{courseId}")
    public Course getById(@Parameter(name = "courseId", description = "课程ID", required = true) @PathVariable("courseId") Long courseId)
    {
        Course course = courseService.getById(courseId);

        return course;
    }
}

(2)service-course-client 定义方法。

创建获取课程信息接口

package com.myxh.smart.planet.client.course;

import com.myxh.smart.planet.model.vod.Course;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@FeignClient("service-vod")
public interface CourseFeignClient
{
    @Operation(summary = "根据课程 id 查询课程信息", description = "根据课程 id 查询课程信息")
    @GetMapping("/api/vod/course/inner/get/by/id/{courseId}")
    Course getById(@Parameter(name = "courseId", description = "课程ID", required = true) @PathVariable("courseId") Long courseId);
}
3.4、创建获取优惠券接口

操作 service-activity 模块。

(1)创建 CouponInfoApiController。

package com.myxh.smart.planet.activity.api;

import com.myxh.smart.planet.activity.service.CouponInfoService;
import com.myxh.smart.planet.model.activity.CouponInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "优惠券信息 API", description = "优惠券信息 API 接口")
@RestController
@RequestMapping("/api/activity/coupon/info")
public class CouponInfoApiController
{
    @Autowired
    private CouponInfoService couponInfoService;

    /**
     * 根据优惠券 id 查询优惠券
     *
     * @param couponId 优惠券 id
     * @return 优惠券信息
     */
    @Operation(summary = "根据优惠券 id 查询优惠券", description = "根据优惠券 id 查询优惠券")
    @GetMapping(value = "inner/get/by/id/{couponId}")
    public CouponInfo getById(@PathVariable("couponId") Long couponId)
    {
        CouponInfo couponInfo = couponInfoService.getById(couponId);

        return couponInfo;
    }

    /**
     * 更新优惠券使用状态
     *
     * @param couponUseId 优惠券使用 id
     * @param orderId     订单 id
     * @return true 优惠券使用状态
     */
    @Operation(summary = "更新优惠券使用状态", description = "更新优惠券使用状态")
    @GetMapping(value = "inner/update/coupon/info/use/status/{couponUseId}/{orderId}")
    public Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId)
    {
        couponInfoService.updateCouponInfoUseStatus(couponUseId, orderId);

        return true;
    }
}

(2)编写 CouponInfoService。

package com.myxh.smart.planet.activity.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.activity.mapper.CouponInfoMapper;
import com.myxh.smart.planet.activity.service.CouponInfoService;
import com.myxh.smart.planet.activity.service.CouponUseService;
import com.myxh.smart.planet.model.activity.CouponInfo;
import com.myxh.smart.planet.model.activity.CouponUse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author MYXH
 * @date 2023/10/15
 *
 * <p>
 * 优惠券信息 服务实现类
 * </p>
 */
@Service
public class CouponInfoServiceImpl extends ServiceImpl<CouponInfoMapper, CouponInfo> implements CouponInfoService
{
    @Autowired
    private CouponUseService couponUseService;

    /**
     * 更新优惠券使用状态
     *
     * @param couponUseId 优惠券使用 id
     * @param orderId     订单 id
     */
    @Override
    public void updateCouponInfoUseStatus(Long couponUseId, Long orderId)
    {
        CouponUse couponUse = new CouponUse();
        couponUse.setId(couponUseId);
        couponUse.setOrderId(orderId);
        couponUse.setCouponStatus("1");
        couponUse.setUsingTime(new Date());
        couponUseService.updateById(couponUse);
    }
}

(3)创建 service-activity-client 模块定义接口。

package com.myxh.smart.planet.client.activity;

import com.myxh.smart.planet.model.activity.CouponInfo;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@FeignClient("service-activity")
public interface CouponInfoFeignClient
{
    @Operation(summary = "根据优惠券 id 查询优惠券", description = "根据优惠券 id 查询优惠券")
    @GetMapping(value = "/api/activity/coupon/info/inner/get/by/id/{couponId}")
    CouponInfo getById(@PathVariable("couponId") Long couponId);

    @Operation(summary = "更新优惠券使用状态", description = "更新优惠券使用状态")
    @GetMapping(value = "/api/activity/coupon/info/inner/update/coupon/info/use/status/{couponUseId}/{orderId}")
    Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId);
}
3.5、获取当前用户 id

(1)common 模块引入依赖。

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Spring 3.X 集成 Redis 所需 common-pool2 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.12.0</version>
</dependency>

(2)复制工具类到 common 下的 service-utils 模块。

复制工具类到 common 下的 service-utils 模块

(3)前端实现方式。

前端实现方式

import axios from "axios";

// 创建 axios 实例
const service = axios.create({
  // api 的 base_url
  baseURL: "http://smartplanet.free.idcfengye.com",
  // 请求超时时间
  timeout: 30000,
});

// http request 拦截器
service.interceptors.request.use(
  (config) => {
    // 获取 localStorage 里面的 token 值
    let token = window.localStorage.getItem("token") || "";

    if (token != "") {
      // 把 token 值放到 header 里面
      config.headers["token"] = token;

      // cookie.get('SmartPlanet');
    }

    return config;
  },

  (err) => {
    return Promise.reject(err);
  }
);

// http response 拦截器
service.interceptors.response.use(
  (response) => {
    if (response.data.code == 208) {
      // 替换 #,后台获取不到 # 后面的参数
      let url = window.location.href.replace("#", "smartplanet");

      window.location =
        "http://smartplanet.free.idcfengye.com/api/user/wechat/authorize?returnUrl=" +
        url;
    } else {
      if (response.data.code == 20000) {
        return response.data;
      } else {
        console.log("response.data:" + JSON.stringify(response.data));
        alert(response.data.message || "error");

        return Promise.reject(response);
      }
    }
  },

  (error) => {
    // 返回接口返回的错误信息
    return Promise.reject(error.response);
  }
);

export default service;
3.6、生成订单 Service

(1)service-order 引入依赖。

<!-- service-activity-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-activity-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- service-course-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-course-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- service-user-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-user-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

(2)OrderInfoServiceImpl

package com.myxh.smart.planet.order.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.activity.CouponInfoFeignClient;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.client.user.UserInfoFeignClient;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.model.activity.CouponInfo;
import com.myxh.smart.planet.model.order.OrderDetail;
import com.myxh.smart.planet.model.order.OrderInfo;
import com.myxh.smart.planet.model.user.UserInfo;
import com.myxh.smart.planet.model.vod.Course;
import com.myxh.smart.planet.order.AuthContextHolder;
import com.myxh.smart.planet.order.OrderNoUtils;
import com.myxh.smart.planet.order.mapper.OrderInfoMapper;
import com.myxh.smart.planet.order.service.OrderDetailService;
import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.result.ResultCodeEnum;
import com.myxh.smart.planet.vo.order.OrderFormVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @author MYXH
 * @date 2023/10/14
 *
 * <p>
 * 订单信息 服务实现类
 * </p>
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService
{
    @Autowired
    private OrderDetailService orderDetailService;

    @Autowired
    private CouponInfoFeignClient couponInfoFeignClient;

    @Autowired
    private CourseFeignClient courseFeignClient;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    /**
     * 新增点播课程订单
     *
     * @param orderFormVo 订单信息
     * @return orderId 订单 id
     */
    @Override
    public Long submitOrder(OrderFormVo orderFormVo)
    {
        // 1、获取生成订单条件值
        Long userId = AuthContextHolder.getUserId();
        Long courseId = orderFormVo.getCourseId();
        Long couponId = orderFormVo.getCouponId();

        // 2、判断当前用户是否已有当前课程的订单
        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getUserId, userId);
        queryWrapper.eq(OrderDetail::getCourseId, courseId);
        OrderDetail orderDetailExist = orderDetailService.getOne(queryWrapper);

        if (orderDetailExist != null)
        {
            // 如果订单已存在,则直接返回订单 id
            return orderDetailExist.getId();
        }

        // 3、根据课程 id 查询课程信息
        Course course = courseFeignClient.getById(courseId);

        if (course == null)
        {
            throw new SmartPlanetException(ResultCodeEnum.DATA_ERROR.getCode(), ResultCodeEnum.DATA_ERROR.getMessage());
        }

        // 4、根据用户 id 查询用户信息
        UserInfo userInfo = userInfoFeignClient.getUserInfoById(userId);

        if (userInfo == null)
        {
            throw new SmartPlanetException(ResultCodeEnum.DATA_ERROR.getCode(), ResultCodeEnum.DATA_ERROR.getMessage());
        }

        // 5、根据优惠券 id 查询优惠券信息
        // 优惠券金额
        BigDecimal couponReduce = new BigDecimal(0);

        if (null != couponId)
        {
            CouponInfo couponInfo = couponInfoFeignClient.getById(couponId);
            couponReduce = couponInfo.getAmount();
        }

        // 6、封装订单生成需要数据到对象,完成添加订单
        // 6.1、封装数据到 orderInfo 对象里面,添加订单基本信息表
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setUserId(userId);
        orderInfo.setNickName(userInfo.getNickName());
        orderInfo.setPhone(userInfo.getPhone());
        orderInfo.setProvince(userInfo.getProvince());
        orderInfo.setOriginAmount(course.getPrice());
        orderInfo.setCouponReduce(couponReduce);
        orderInfo.setFinalAmount(orderInfo.getOriginAmount().subtract(orderInfo.getCouponReduce()));
        orderInfo.setOutTradeNo(OrderNoUtils.getOrderNo());
        orderInfo.setTradeBody(course.getTitle());
        orderInfo.setOrderStatus("0");
        baseMapper.insert(orderInfo);

        // 6.2、封装数据到 orderDetail 对象里面,添加订单详情信息表
        OrderDetail orderDetail = new OrderDetail();
        orderDetail.setOrderId(orderInfo.getId());
        orderDetail.setUserId(userId);
        orderDetail.setCourseId(courseId);
        orderDetail.setCourseName(course.getTitle());
        orderDetail.setCover(course.getCover());
        orderDetail.setOriginAmount(course.getPrice());
        orderDetail.setCouponReduce(new BigDecimal(0));
        orderDetail.setFinalAmount(orderDetail.getOriginAmount().subtract(orderDetail.getCouponReduce()));
        orderDetailService.save(orderDetail);

        // 7、更新优惠券状态
        if (null != orderFormVo.getCouponUseId())
        {
            couponInfoFeignClient.updateCouponInfoUseStatus(orderFormVo.getCouponUseId(), orderInfo.getId());
        }

        // 8、返回订单 id
        return orderInfo.getId();
    }
}
4、微信支付
4.1、微信支付

接口文档:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1

4.2、公众号配置

(1)绑定域名。

与微信分享一致

先登录微信公众平台进入“设置与开发”,“公众号设置”的“功能设置”里填写“JS 接口安全域名”。

说明:因为测试号不支持支付功能,需要使用正式号才能进行测试。

绑定域名

(2)商户平台配置支付目录。

商户平台配置支付目录

4.3、创建订单支付接口

(1)创建 WXPayController。

package com.myxh.smart.planet.order.api;

import com.myxh.smart.planet.order.service.WXPayService;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "微信支付 API", description = "微信支付 API 接口")
@RestController
@RequestMapping("/api/order/wx/pay")
public class WXPayController
{
    @Autowired
    private WXPayService wxPayService;

    /**
     * 微信下单支付
     *
     * @param orderNo 订单号
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "微信下单支付", description = "微信下单支付")
    @GetMapping("/create/js/api/{orderNo}")
    public Result<Map<String, Object>> createJsapi(@Parameter(name = "orderNo", description = "订单号", required = true)
                                                   @PathVariable("orderNo") String orderNo)
    {
        Map<String, Object> map = wxPayService.createJsapi(orderNo);

        return Result.ok(map);
    }
}

(2)创建 WXPayService。

package com.myxh.smart.planet.order.service;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/21
 */
public interface WXPayService
{
    /**
     * 微信下单支付
     *
     * @param orderNo 订单号
     * @return result 结果
     */
    Map<String, Object> createJsapi(String orderNo);
}

(3)service-order 引入依赖。

<!-- wxpay-sdk -->
<dependency>
    <groupId>com.github.wxpay</groupId>
    <artifactId>wxpay-sdk</artifactId>
    <version>0.0.3</version>
</dependency>

(4)创建 WXPayServiceImpl。

package com.myxh.smart.planet.order.service.impl;

import com.github.wxpay.sdk.WXPayUtil;
import com.myxh.smart.planet.client.user.UserInfoFeignClient;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.order.HttpClientUtils;
import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.order.service.WXPayService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Service
public class WXPayServiceImpl implements WXPayService
{
    @Autowired
    private OrderInfoService orderInfoService;
    @Resource
    private UserInfoFeignClient userInfoFeignClient;

    /**
     * 微信下单支付
     *
     * @param orderNo 订单号
     * @return result 结果
     */
    @Override
    public Map<String, Object> createJsapi(String orderNo)
    {
        try
        {
            // 封装微信支付需要参数,使用 map 集合
            Map<String, String> paramMap = new HashMap<>();

            // 1、设置参数
            // 正式服务公众号商户id = wxf913bfa3a2c7eeeb(固定值)
            paramMap.put("appid", "wxf913bfa3a2c7eeeb");

            // 正式服务公众号商户号 = 1481962542(固定值)
            paramMap.put("mch_id", "1481962542");

            paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
            paramMap.put("body", "test");
            paramMap.put("out_trade_no", orderNo);

            // 为了参数,支付 0.01 元
            paramMap.put("total_fee", "1");
            paramMap.put("spbill_create_ip", "127.0.0.1");
            paramMap.put("notify_url", "http://smartplanet.free.idcfengye.com/api/order/wx/pay/notify");

            // 支付类型,按照生成固定金额支付
            paramMap.put("trade_type", "JSAPI");

            /*
            设置参数值当前微信用户 openid
            现实实现逻辑:第一步、根据订单号获取 userid
                       第二步、根据 userid 获取 openid

            因为当前使用测试号,测试号不支持支付功能,为了使用正式服务公众号进行测试,使用下面写法
            获取正式服务公众号微信 openid
            通过其他方式获取正式服务公众号 openid,直接设置
             */
            // 正式服务公众号 openid = oQTXC56lAy3xMOCkKCImHtHoLL(不定值)
            paramMap.put("openid", "oQTXC56lAy3xMOCkKCImHtHoLL");

            // 2、HTTPClient 来根据 URL 访问第三方接口并且传递参数,调用微信支付接口
            HttpClientUtils client = new HttpClientUtils("https://api.mch.weixin.qq.com/pay/unifiedorder");

            // client 设置请求参数
            // 正式服务公众号商户 key = MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9
            String paramXml = WXPayUtil.generateSignedXml(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9");
            client.setXmlParam(paramXml);
            client.setHttps(true);

            // 发送请求
            client.post();

            // 3、微信支付接口返回第三方的数据
            String contentXml = client.getContent();
            System.out.println("contentXml = " + contentXml);
            Map<String, String> resultMap = WXPayUtil.xmlToMap(contentXml);

            if ((resultMap.get("result_code") != null) && !"SUCCESS".equals(resultMap.get("result_code")))
            {
                throw new SmartPlanetException(20001, "支付失败");
            }

            // 4、再次封装参数
            Map<String, String> parameterMap = new HashMap<>();
            String prepayId = String.valueOf(resultMap.get("prepay_id"));
            String packages = "prepay_id=" + prepayId;
            parameterMap.put("appId", "wxf913bfa3a2c7eeeb");
            parameterMap.put("nonceStr", resultMap.get("nonce_str"));
            parameterMap.put("package", packages);
            parameterMap.put("signType", "MD5");
            parameterMap.put("timeStamp", String.valueOf(new Date().getTime()));
            String sign = WXPayUtil.generateSignature(parameterMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9");

            // 返回结果
            Map<String, Object> result = new HashMap<>();
            result.put("appId", "wxf913bfa3a2c7eeeb");
            result.put("timeStamp", parameterMap.get("timeStamp"));
            result.put("nonceStr", parameterMap.get("nonceStr"));
            result.put("signType", "MD5");
            result.put("paySign", sign);
            result.put("package", packages);
            System.out.println("result = " + result);

            return result;
        }
        catch (Exception e)
        {
            e.printStackTrace();

            return new HashMap<>();
        }
    }
}
4.4、服务号测试过程

(1)修改 service-user 模块配置文件。

# 公众号 id 和秘钥
# 智慧星球微信公众平台 appId
# wechat.appId=wxc23b80b9ffaac7bd
# 智慧星球微信公众平台 api 秘钥
# wechat.appSecret=5c0271622c4271753310c436b5cd3532

# 公众号 id 和秘钥(测试微信支付功能)
# 硅谷课堂微信公众平台 appId
wechat.appId=wxf913bfa3a2c7eeeb
# 硅谷课堂微信公众平台 api 秘钥
wechat.appSecret=cd360d429e5c8db0c638d5ef9df74f6d

(2)service-user 模块创建 controller。

package com.myxh.smart.planet.user.api;

import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "微信用户 API(测试微信支付功能)", description = "微信用户 API 接口(测试微信支付功能)")
@Controller
@RequestMapping("/api/user/openid")
public class GetOpenIdController
{
    @Autowired
    private WxMpService wxMpService;

    /**
     * 授权跳转
     *
     * @param returnUrl 返回 url
     * @param request   请求
     * @return redirectURL 重定向 url
     */
    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request)
    {
        String userInfoUrl = "http://smartplanet.free.idcfengye.com/api/user/openid/user/info";
        String redirectURL = wxMpService
                .getOAuth2Service().buildAuthorizationUrl(userInfoUrl,
                        WxConsts.OAuth2Scope.SNSAPI_USERINFO,
                        URLEncoder.encode(returnUrl.replace("#", "smartplanet"), StandardCharsets.UTF_8));

        return "redirect:" + redirectURL;
    }

    /**
     * 获取用户信息
     *
     * @param code      密码
     * @param returnUrl 返回 url
     * @return redirectURL 重定向 url
     */
    @GetMapping("/user/info")
    @ResponseBody
    public String userInfo(@RequestParam("code") String code,
                           @RequestParam("state") String returnUrl)
    {
        try
        {
            // 拿着 code 发送请求
            WxOAuth2AccessToken wxOAuth2AccessToken = null;

            wxOAuth2AccessToken = this.wxMpService.getOAuth2Service().getAccessToken(code);


            // 获取 openId
            String openId = wxOAuth2AccessToken.getOpenId();
            System.out.println("正式服务公众号微信网页授权 openId = " + openId);

            return openId;
        }
        catch (WxErrorException e)
        {
            e.printStackTrace();
        }

        return null;
    }
}

(3)修改前端 App.vue。

<template>
  <div id="app">
    <div id="nav">
      <!-- <router-link to="/">列表页</router-link> | -->
      <!-- <router-link to="/info">详情页</router-link> | -->
      <!--  <router-link to="/list">列表页</router-link> | -->
      <!--  <router-link to="/order">下单页</router-link> -->
      <van-button round block type="info" @click="clearData"
        >清空 localStorage</van-button
      >
    </div>
    <router-view />
  </div>
</template>

<script>
  import userInfoAPI from "@/api/userInfo";

  export default {
    data() {
      return {
        show: true,
      };
    },

    created() {
      // 处理微信授权登录
      this.wechatLogin();
    },

    methods: {
      wechatLogin() {
        // 处理微信授权登录
        let token = this.getQueryString("token") || "";

        if (token != "") {
          window.localStorage.setItem("token", token);
        }

        // 所有页面都必须登录,两次调整登录,这里与接口返回 208 状态
        token = window.localStorage.getItem("token") || "";

        if (token == "") {
          let url = window.location.href.replace("#", "smartplanet");

          /*
          window.location =
            "http://smartplanet.free.idcfengye.com/api/user/wechat/authorize?returnUrl=" +
            url;
           */

          // 测试微信支付功能
          window.location =
            "http://smartplanet.free.idcfengye.com/api/user/openid/authorize?returnUrl=" +
            url;
        }

        console.log("token:" + window.localStorage.getItem("token"));

        //绑定手机号处理
        /*
      if (token != "") {
        this.bindPhone();
      }
      */
      },

      bindPhone() {
        let userInfoString = window.localStorage.getItem("userInfo") || "";
        alert("userInfoString:" + userInfoString);

        if (userInfoString != "") {
          alert("userInfoString:" + userInfoString);
          let userInfo = JSON.parse(userInfoString);
          let phone = userInfo.phone || "";

          if (phone == "") {
            this.$router.push({ path: "/bind/first" });
          }
        } else {
          alert("userInfoString:" + userInfoString);
          userInfoAPI.getCurrentUserInfo().then((response) => {
            window.localStorage.setItem(
              "userInfo",
              JSON.stringify(response.data)
            );
            alert("data:" + JSON.stringify(response.data));
            let phone = response.data.phone || "";
            console.log("phone:" + phone);

            if (phone == "") {
              this.$router.push({ path: "/bind/first" });
            }
          });
        }
      },

      getQueryString(paramName) {
        if (window.location.href.indexOf("?") == -1) return "";

        let searchString = window.location.href.split("?")[1];
        let i,
          val,
          params = searchString.split("&");

        for (i = 0; i < params.length; i++) {
          val = params[i].split("=");

          if (val[0] == paramName) {
            return val[1];
          }
        }

        return "";
      },

      clearData() {
        window.localStorage.setItem("token", "");
        window.localStorage.setItem("userInfo", "");
        let token = window.localStorage.getItem("token");
        alert("token:" + token);
      },
    },
  };
</script>

<style lang="scss">
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    color: #2c3e50;
  }
</style>

(4)复制返回的 openid 到支付接口中测试。

复制返回的 openid 到支付接口中测试

4.5、整合点播视频支付前端

(1)trade.vue

<template>
  <template>
    <div>
      <van-image width="100%" height="200" :src="courseVo.cover" />

      <h1 class="van-ellipsis course_title">{{ courseVo.title }}</h1>

      <div class="course_teacher_price_box">
        <div class="course_teacher_price">
          <div class="course_price">价格:</div>
          <div class="course_price_number">¥{{ courseVo.price }}</div>
        </div>
      </div>
      <div class="course_teacher_price_box">
        <div class="course_teacher_box">
          <div class="course_teacher">教师: {{ teacher.name }}</div>
          <van-image :src="teacher.avatar" round width="50px" height="50px" />
        </div>
      </div>

      <van-loading vertical="true" v-show="loading">加载中...</van-loading>

      <div
        style="
        position: fixed;
        left: 0px;
        bottom: 50px;
        width: 100%;
        height: 50px;
        z-index: 999;
      "
      >
        <!-- 优惠券单元格 -->
        <van-coupon-cell
          :coupons="coupons"
          :chosen-coupon="chosenCoupon"
          @click="showList = true"
        />

        <!-- 优惠券列表 -->
        <van-popup
          v-model="showList"
          round
          position="bottom"
          style="height: 90%; padding-top: 4px"
        >
          <van-coupon-list
            :coupons="coupons"
            :chosen-coupon="chosenCoupon"
            :disabled-coupons="disabledCoupons"
            @change="onChange"
          />
        </van-popup>
      </div>

      <van-goods-action>
        <van-submit-bar
          :price="finalAmount"
          button-text="确认下单"
          @submit="sureOrder"
        />
      </van-goods-action>
    </div>
  </template>

  <script>
    import courseAPI from "@/api/course";
    import orderAPI from "@/api/order";
    import couponAPI from "@/api/coupon";

    export default {
      data() {
        return {
          loading: false,
          courseId: null,
          courseVo: {},
          teacher: {},
          orderId: null,
          showList: false,
          chosenCoupon: -1,
          coupons: [],
          disabledCoupons: [],
          couponId: null,
          couponUseId: null,
          couponReduce: 0,
          finalAmount: 0,
        };
      },

      created() {
        this.courseId = this.$route.params.courseId;
        this.fetchData();
        this.getCouponInfo();
      },

      methods: {
        onChange(index) {
          debugger;
          this.showList = false;
          this.chosenCoupon = index;
          this.couponId = this.coupons[index].id;
          this.couponUseId = this.coupons[index].couponUseId;
          this.couponReduce = this.coupons[index].value;
          this.finalAmount =
            parseFloat(this.finalAmount) - parseFloat(this.couponReduce);
        },

        fetchData() {
          this.loading = true;
          courseAPI.getInfo(this.courseId).then((response) => {
            // console.log(response.data);
            this.courseVo = response.data.courseVo;
            this.teacher = response.data.teacher;

            // 转换为分
            this.finalAmount = parseFloat(this.courseVo.price) * 100;
            this.loading = false;
          });
        },

        getCouponInfo() {
          couponAPI.findCouponInfo().then((response) => {
            // console.log(response.data);
            this.coupons = response.data.abledCouponsList;
            this.disabledCoupons = response.data.disabledCouponsList;
          });
        },

        sureOrder() {
          this.loading = true;
          let orderFormVo = {
            courseId: this.courseId,
            couponId: this.couponId,
            couponUseId: this.couponUseId,
          };

          orderAPI.submitOrder(orderFormVo).then((response) => {
            console.log(response.data);
            this.$router.push({ path: "/pay/" + response.data });
          });
        },
      },
    };
  </script>

  <style lang="scss" scoped>
    .gap {
      height: 10px;
    }

    ::v-deep.van-image {
      display: block;
    }

    .course_count {
      background-color: #82848a;
      color: white;
      padding: 5px;
      text-align: center;
      border-right: 1px solid #939393;

      h1 {
        font-size: 14px;
        margin: 0;
      }

      p {
        margin: 0;
        font-size: 16px;
      }
    }

    .course_title {
      font-size: 20px;
      margin: 10px;
    }

    .course_teacher_price_box {
      margin: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;

      .course_teacher_price {
        display: flex;
        font-size: 14px;
        align-items: center;

        .course_price_number {
          color: red;
          font-size: 18px;
          font-weight: bold;
        }
      }

      .course_teacher_box {
        display: flex;
        justify-content: center;
        align-items: center;

        .course_teacher {
          margin-right: 20px;
        }
      }
    }

    .course_contents {
      margin: 10px;

      .course_title_font {
        color: #68cb9b;
        font-weight: bold;
      }

      .course_content {
        margin-bottom: 20px;
      }
    }

    .course_chapter_list {
      display: flex;
      justify-content: space-between;
      align-items: center;

      h2 {
        font-size: 14px;
      }

      p {
        margin: 0;
      }
    }
  </style></template
>

(2)pay.vue

<template>
  <div>
    <head>
      <!-- 设置 referrer 为 no-referrer,用于绕过防盗链限制,从而正常显示图片 -->
      <meta name="referrer" content="no-referrer" />
    </head>

    <van-image
      width="100%"
      height="200"
      src="https://img-blog.csdnimg.cn/b9f45932f6cf471aa53ee24e5555d9c2.png"
    />

    <h1 class="van-ellipsis course_title">
      课程名称: {{ orderInfo.courseName }}
    </h1>

    <div class="course_teacher_price_box">
      <div class="course_price">订单号:{{ orderInfo.outTradeNo }}</div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_price">下单时间:{{ orderInfo.createTime }}</div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_price">
        支付状态:{{ orderInfo.orderStatus == "0" ? "未支付" : "已支付" }}
      </div>
    </div>
    <div class="course_teacher_price_box" v-if="orderInfo.orderStatus == '1'">
      <div class="course_price">支付时间:{{ orderInfo.payTime }}</div>
    </div>
    <van-divider />
    <div class="course_teacher_price_box">
      <div class="course_price">
        订单金额:<span style="color: red">¥{{ orderInfo.originAmount }}</span>
      </div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_price">
        优惠券金额:<span style="color: red"
          >¥{{ orderInfo.couponReduce }}</span
        >
      </div>
    </div>
    <div class="course_teacher_price_box">
      <div class="course_price">
        支付金额:<span style="color: red">¥{{ orderInfo.finalAmount }}</span>
      </div>
    </div>

    <van-goods-action>
      <van-goods-action-button
        type="danger"
        text="支付"
        @click="pay"
        v-if="orderInfo.orderStatus == '0'"
      />
      <van-goods-action-button
        type="warning"
        text="去观看"
        @click="see"
        v-else
      />
    </van-goods-action>

    <van-loading vertical="true" v-show="loading">加载中...</van-loading>
  </div>
</template>

<script>
  import orderAPI from "@/api/order";

  export default {
    data() {
      return {
        loading: false,
        orderId: null,
        orderInfo: {},
        showList: false,
        chosenCoupon: -1,
        coupons: [],
        disabledCoupons: [],
        couponReduce: 0,
        finalAmount: 0,
      };
    },

    created() {
      this.orderId = this.$route.params.orderId;
      this.fetchData();
    },

    methods: {
      fetchData() {
        this.loading = true;
        orderAPI.getInfo(this.orderId).then((response) => {
          this.orderInfo = response.data;
          // alert("orderStatus:" + this.orderInfo.orderStatus);
          this.finalAmount = parseFloat(this.orderInfo.finalAmount) * 100;

          this.loading = false;
        });
      },

      pay() {
        this.loading = true;
        orderAPI.createJsapi(this.orderInfo.outTradeNo).then((response) => {
          console.log(response.data);
          this.loading = false;
          this.onBridgeReady(response.data);
        });
      },

      onBridgeReady(data) {
        let that = this;
        console.log(data);
        WeixinJSBridge.invoke(
          "getBrandWCPayRequest",
          {
            //公众号 ID,由商户传入
            appId: data.appId,

            // 时间戳,自 1970 年以来的秒数
            timeStamp: data.timeStamp,

            // 随机串
            nonceStr: data.nonceStr,
            package: data.package,

            // 微信签名方式
            signType: data.signType,

            // 微信签名
            paySign: data.paySign,
          },

          function (res) {
            if (res.err_msg == "get_brand_wcpay_request:ok") {
              // 使用以上方式判断前端返回,微信团队郑重提示:
              // res.err_msg 将在用户支付成功后返回 ok,但并不保证它绝对可靠。
              console.log("支付成功");
              that.queryPayStatus();
            }
          }
        );
      },

      queryPayStatus() {
        // 回调查询
        orderAPI.queryPayStatus(this.orderInfo.outTradeNo).then((response) => {
          console.log(response.data);
          this.fetchData();
        });
      },

      see() {
        this.$router.push({ path: "/course/info/" + this.orderInfo.courseId });
      },
    },
  };
</script>

<style lang="scss" scoped>
  .gap {
    height: 10px;
  }

  ::v-deep.van-image {
    display: block;
  }

  .course_count {
    background-color: #82848a;
    color: white;
    padding: 5px;
    text-align: center;
    border-right: 1px solid #939393;

    h1 {
      font-size: 14px;
      margin: 0;
    }

    p {
      margin: 0;
      font-size: 16px;
    }
  }

  .course_title {
    font-size: 20px;
    margin: 10px;
  }

  .course_teacher_price_box {
    margin: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .course_teacher_price {
      display: flex;
      font-size: 14px;
      align-items: center;

      .course_price_number {
        color: red;
        font-size: 18px;
        font-weight: bold;
      }
    }

    .course_teacher_box {
      display: flex;
      justify-content: center;
      align-items: center;

      .course_teacher {
        margin-right: 20px;
      }
    }
  }

  .course_contents {
    margin: 10px;

    .course_title_font {
      color: #68cb9b;
      font-weight: bold;
    }

    .course_content {
      margin-bottom: 20px;
    }
  }

  .course_chapter_list {
    display: flex;
    justify-content: space-between;
    align-items: center;

    h2 {
      font-size: 14px;
    }

    p {
      margin: 0;
    }
  }
</style>
4.6、订单详情接口

(1)OrderInfoApiController 添加方法。

package com.myxh.smart.planet.order.api;

import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.order.OrderInfoVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "订单信息 API", description = "订单信息 API 接口")

@RestController
@RequestMapping("api/order/order/info")
public class OrderInfoApiController
{
    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 根据订单 id 获取订单信息
     *
     * @param id 订单 id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据订单 id 获取订单信息", description = "根据订单 id 获取订单信息")
    @GetMapping("get/info/{id}")
    public Result<OrderInfoVo> getInfo(@PathVariable("id") Long id)
    {
        OrderInfoVo orderInfoVo = orderInfoService.getOrderInfoVoById(id);

        return Result.ok(orderInfoVo);
    }
}

(2)OrderInfoServiceImpl 实现方法。

package com.myxh.smart.planet.order.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.order.OrderDetail;
import com.myxh.smart.planet.model.order.OrderInfo;
import com.myxh.smart.planet.order.mapper.OrderInfoMapper;
import com.myxh.smart.planet.order.service.OrderDetailService;
import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.vo.order.OrderInfoVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/14
 *
 * <p>
 * 订单信息 服务实现类
 * </p>
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService
{
    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 根据订单 id 获取订单信息
     *
     * @param id 订单 id
     * @return orderInfoVo 订单信息
     */
    @Override
    public OrderInfoVo getOrderInfoVoById(Long id)
    {
        //根据订单 id 查询订单基本信息和详情信息
        OrderInfo orderInfo = baseMapper.selectById(id);
        OrderDetail orderDetail = orderDetailService.getById(id);

        // 封装 OrderInfoVo
        OrderInfoVo orderInfoVo = new OrderInfoVo();
        BeanUtils.copyProperties(orderInfo, orderInfoVo);
        orderInfoVo.setCourseId(orderDetail.getCourseId());
        orderInfoVo.setCourseName(orderDetail.getCourseName());

        return orderInfoVo;
    }
}
4.7、查询支付结果

(1)WXPayController 添加方法。

package com.myxh.smart.planet.order.api;

import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.order.service.WXPayService;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Tag(name = "微信支付 API", description = "微信支付 API 接口")
@RestController
@RequestMapping("/api/order/wx/pay")
public class WXPayController
{
    @Autowired
    private WXPayService wxPayService;

    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 查询支付状态
     *
     * @param orderNo 订单号
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "查询支付状态", description = "查询支付状态")
    @GetMapping("/query/pay/status/{orderNo}")
    public Result<Object> queryPayStatus(@Parameter(name = "orderNo", description = "订单号", required = true)
                                         @PathVariable("orderNo") String orderNo)
    {
        // 根据订单号调用微信接口查询支付状态
        Map<String, String> resultMap = wxPayService.queryPayStatus(orderNo);

        // 判断支付是否成功,根据微信支付状态接口判断
        if (resultMap == null)
        {
            // 出错
            return Result.fail(null).message("支付出错");
        }

        if ("SUCCESS".equals(resultMap.get("trade_state")))
        {
            // 成功
            // 更改订单状态,处理支付结果
            String outTradeNo = resultMap.get("out_trade_no");
            System.out.println("out_trade_no = " + outTradeNo);
            orderInfoService.updateOrderStatus(outTradeNo);

            return Result.ok(null).message("支付成功");
        }

        return Result.ok(null).message("支付中");
    }
}

(2)WXPayServiceImpl 实现方法。

package com.myxh.smart.planet.order.service.impl;

import com.github.wxpay.sdk.WXPayUtil;
import com.myxh.smart.planet.client.user.UserInfoFeignClient;
import com.myxh.smart.planet.order.HttpClientUtils;
import com.myxh.smart.planet.order.service.OrderInfoService;
import com.myxh.smart.planet.order.service.WXPayService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * @author MYXH
 * @date 2023/10/21
 */
@Service
public class WXPayServiceImpl implements WXPayService
{
    @Autowired
    private OrderInfoService orderInfoService;
    @Resource
    private UserInfoFeignClient userInfoFeignClient;

    /**
     * 根据订单号调用微信接口查询支付状态
     *
     * @param orderNo 订单号
     * @return resultMap 结果
     */
    @Override
    public Map<String, String> queryPayStatus(String orderNo)
    {
        try
        {
            // 1、封装微信接口需要参数,使用 map
            Map paramMap = new HashMap<>();
            paramMap.put("appid", "wxf913bfa3a2c7eeeb");
            paramMap.put("mch_id", "1481962542");
            paramMap.put("out_trade_no", orderNo);
            paramMap.put("nonce_str", WXPayUtil.generateNonceStr());

            // 2、调用接口 Httpclient,设置请求
            HttpClientUtils client = new HttpClientUtils("https://api.mch.weixin.qq.com/pay/orderquery");
            client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9"));
            client.setHttps(true);
            client.post();

            // 3、封装返回第三方的数据
            String xml = client.getContent();
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);

            // 5、返回结果
            return resultMap;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return null;
    }
}

(3)OrderInfoServiceImpl 实现方法。

package com.myxh.smart.planet.order.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.model.order.OrderInfo;
import com.myxh.smart.planet.order.mapper.OrderInfoMapper;
import com.myxh.smart.planet.order.service.OrderInfoService;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/14
 *
 * <p>
 * 订单信息 服务实现类
 * </p>
 */
@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements OrderInfoService
{
    /**
     * 更改订单状态,处理支付结果
     *
     * @param outTradeNo 场外交易编号
     */
    @Override
    public void updateOrderStatus(String outTradeNo)
    {
        // 根据 订单号查询订单
        LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OrderInfo::getOutTradeNo, outTradeNo);
        OrderInfo orderInfo = baseMapper.selectOne(wrapper);

        // 更新订单状态,1 已经支付
        orderInfo.setOrderStatus("1");

        // 调用方法更新
        baseMapper.updateById(orderInfo);
    }
}

二、直播介绍

1、项目需求

智慧星球会定期推出直播课程,方便学生与名师之间的交流互动,在直播间老师可以推荐点播课程(类似直播带货),学生可以点赞交流,购买推荐的点播课程。

2、了解直播

一个完整直播实现流程:

​ 1.采集、2.滤镜处理、3.编码、4.推流、5.CDN 分发、6.拉流、7.解码、8.播放、9.聊天互动。

2.1、通用直播模型

通用直播模型

通用直播模型

  1. 首先是主播方,它是产生视频流的源头,由一系列流程组成:第一,通过一定的设备来采集数据;第二,将采集的这些视频进行一系列的处理,比如水印、美颜和特效滤镜等处理;第三,将处理后的结果视频编码压缩成可观看可传输的视频流;第四,分发推流,即将压缩后的视频流通过网络通道传输出去。

  2. 其次是播放端,播放端功能有两个层面,第一个层面是关键性的需求;另一层面是业务层面的。先看第一个层面,它涉及到一些非常关键的指标,比如秒开,在很多场景当中都有这样的要求,然后是对于一些重要内容的版权保护。为了达到更好的效果,还需要配合服务端做智能解析,这在某些场景下也是关键性需求。再来看第二个层面也即业务层面的功能,对于一个社交直播产品来说,在播放端,观众希望能够实时的看到主播端推过来的视频流,并且和主播以及其他观众产生一定的互动,因此它可能包含一些像点赞、聊天和弹幕这样的功能,以及礼物这样更高级的道具。

  3. 要知道,内容产生方和消费方一般都不是一一对应的。对于一个直播产品来讲,最直观的体现就是一个主播可能会有很多粉丝。因此,不能直接让主播端和所有播放端进行点对点通信,这在技术上是做不到或者很有难度。主播方播出的视频到达播放端之前,需要经过一系列的中间环节,也就是这里讲的直播服务器端。

  4. 直播服务器端提供的最核心功能是收集主播端的视频推流,并将其放大后推送给所有观众端。除了这个核心功能,还有很多运营级别的诉求,比如鉴权认证,视频连线和实时转码,自动鉴黄,多屏合一,以及云端录制存储等功能。另外,对于一个主播端推出的视频流,中间需要经过一些环节才能到达播放端,因此对中间环节的质量进行监控,以及根据这些监控来进行智能调度,也是非常重要的诉求。

  5. 实际上无论是主播端还是播放端,他们的诉求都不会仅仅是拍摄视频和播放视频这么简单。在这个核心诉求被满足之后,还有很多关键诉求需要被满足。比如,对于一个消费级的直播产品来说,除了这三大模块之外,还需要实现一个业务服务端来进行推流和播放控制,以及所有用户状态的维持。如此,就构成了一个消费级可用的直播产品。

2.2、如何快速开发完整直播
2.2.1、利用第三方 SDK 开发
  • 七牛云:七牛直播云是专为直播平台打造的全球化直播流服务和一站式实现 SDK 端到端直播场景的企业级直播云服务平台。

    • 熊猫 TV,龙珠 TV 等直播平台都是用的七牛云。
  • 网易视频云:基于专业的跨平台视频编解码技术和大规模视频内容分发网络,提供稳定流畅、低延时、高并发的实时音视频服务,可将视频直播无缝对接到自身 App。

  • 阿里云视频直播解决方案

  • 欢拓云直播平台:欢拓是一家以直播技术为核心的网络平台,旨在帮助人们通过网络也能实现真实互动通讯。

2.2.2、第三方 SDK 好处
  • 降低成本:

    • 使用好的第三方企业服务,将不用再花费大量的人力物力去研发。
  • 提升效率:

    • 第三方服务的专注与代码集成所带来的方便,所花费的时间可能仅仅是 1-2 个小时
      ,节约近 99% 的时间,足够换取更多的时间去和竞争对手斗智斗勇,增加更大的成功可能性。
  • 降低风险:

    • 借助专业的第三方服务,由于它的快速、专业、稳定等特点,能够极大地加强产品的
      竞争能力(优质服务、研发速度等),缩短试错时间,必将是创业中保命的手段之一。
  • 专业的事,找专业的人来做。

    • 服务最少是 10-20 人的团队专注地解决同一个问题,做同一件事情。
3、欢拓云直播

根据上面的综合对比和调研,最终选择了“欢拓与直播平台”,它提供了完整的可以直接使用的示例代码,方便开发对接。

欢拓是一家以直播技术为核心的网络平台,旨在帮助人们通过网络也能实现真实互动通讯。从 2010 年开始,欢拓就专注于音频、视频的采样、编码、后处理及智能传输研究,并于 2013 年底正式推出了针对企业/开发者的直播云服务系统,帮助开发者轻松实现真人互动。该系统适用场景包括在线教育、游戏语音、娱乐互动、远程会议(PC、移动均可)等等。针对应用场景,采用先进技术解决方案和产品形态,让客户和客户的用户满意!

官网:https://www.talk-fun.com/

接口文档地址:http://open.talk-fun.com/docs/getstartV2/document.html

欢拓云直播

三、直播对接

1、直播体验
1.1、开通账号

通过官网:https://www.talk-fun.com/ ,联系客户或 400 电话开通账号,开通 “生活直播” 权限。开通后注意使用有效期,一般一周左右,可以再次申请延期。

说明:官网免费试用,功能有限制,不建议使用

1.2、创建直播

1、在直播管理创建直播。

创建直播

2、创建直播,选择主播模式。

创建直播

3、配置直播,可以自行查看。

创建直播

1.3、开始直播

1、在直播列表,点击“直播入口”。

开始直播

开始直播

主播端下载“云直播客户端”,“频道 id 与密码”为直播客户端的登录账号;

下面还有管理员,主播进行直播时,助教可以在聊天时与观众互动。

2、网页端可以“一键开播”。

3、使用“频道 id 与密码”登录。

4、点击“开始直播”,打开摄像头即可开始直播。

开始直播

1.4、用户观看

1、在直播列表,点击“直播入口”。

2、在观众一栏点击进入,即可在网页端观看直播。

用户观看

1.5、体验总结

上面的体验完全能够满足业务的需要,智慧星球的需求是定期推出直播课程,方便学生与名师之间的交流互动,在直播间老师可以推荐点播课程(类似直播带货),学生可以点赞交流,购买推荐的点播课程。

直播平台只是做了直播相关的业务,不能与业务进行衔接,期望的是在智慧星球的管理后台管理直播相关的业务,那么怎么做呢?对接直播业务接口,直播平台有对应的直播接口,直接对接即可。

Day 14-直播管理模块

一、后台系统-直播管理

上面已经开通了“生活类直播”。

1、获取 openId 与 openToken

登录进入开放后台,后台首页即可获取 openId 与 openToken。

获取 openId 与 openToken

2、对接说明

1、使用 HTTP 协议进行信息交互,字符编码统一采用 UTF-8。

2、除非特殊说明,接口地址统一为:https://api.talk-fun.com/portal.php

3、除非特殊说明,同时支持 GET 和 POST 两种参数传递方式。

4、除非特殊说明,返回信息支持 JSON 格式。

5、除了 sign 外,其余所有请求参数值都需要进行 URL 编码。

6、参数表中,类型一栏声明的定义为:int 代表整数类型;string 代表字符串类型,如果后面有括号,括号中的数字代表该参数的最大长度;array / object 表示数组类型。

7、openID、openToken 参数的获取见对接流程说明

3、了解接口文档

接口文档地址:https://open.talk-fun.com/docs/getstartV2/api/live_dir.html

3.1、了解接口文档

根据接口文档,了解需要对接哪些接口。

了解接口文档

3.1.1、添加直播

api 名称:course.add,SDK 对应方法:courseAdd

添加直播是一定需要的。

3.1.2、更新直播信息

api 名称:course.update,SDK 对应方法courseUpdate

3.1.3、删除直播信息

api 名称:course.delete,SDK 对应方法:courseDelete

3.1.4、修改生活直播相关配置

api 名称:course.updateLifeConfig,SDK 对应方法:updateLifeConfig

设置功能很多,但是只需要几个即可,这个接口需要做如下设置:

1、界面模式:pageViewMode 界面模式,1 全屏模式 0 二分屏 2 课件模式。

​2、观看人数开关:number 观看人数开关;number.enable 是否开启;观看人数,0 否 1 是;示例:{“enable”:“1”}。

​3、商城开关(直播推荐课程):goodsListEdit 商品列表编辑,状态 goodsListEdit.status,0 覆盖,1 追加,不传默认为 0;示例:{“status”:1}。

直播设置最终效果:

修改生活直播相关配置

3.1.5、按照课程 ID 获取访客列表

改接口在:"访客/管理员列表"下面。

通过该接口统计课程观看人数信息。

直播访客 api 名称:course.visitor.list,SDK 对应方法:courseVisitorList

3.2、下载 SDK

直播平台准备了 SDK,直接使用。

下载地址:https://open.talk-fun.com/docs/getstartV2/api/introduce/sdkdownload.html

已下载:当前目录/MTCloud-java-sdk-1.6.zip

下载 SDK

5、搭建 service-live 模块
5.1、创建 service-live 模块

创建 service-live 模块

5.2、添加依赖

添加直播 SDK 需要的依赖。

<!-- mybatis-plus-generator -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3.1</version>
</dependency>

<!-- freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>

<!-- commons-httpclient  -->
<dependency>
    <groupId>commons-httpclient</groupId>
    <artifactId>commons-httpclient</artifactId>
    <version>3.0.1</version>
</dependency>

<!-- json-lib -->
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
    <classifier>jdk15</classifier>
</dependency>
5.3、集成代码

解压 MTCloud-java-sdk-1.6.zip,复制 MTCloud-java-sdk-1.6\MTCloud_java\src\com\mtcloud\sdk 下面的 java 文件到 com.myxh.smart.planet.live.mtcloud 包下,如图。

集成代码

集成代码

5.4、更改配置

更改 MTCloud 类配置。

说明:

​1、更改 openID 与 openToken。

​2、该类官方已经做了接口集成,可以直接使用。

/**
 * Copyright www.talk-fun.com
 */

package com.myxh.smart.planet.live.mtcloud;

import org.springframework.beans.factory.annotation.Value;

/**
 * @author MYXH
 * @date 2023/10/23
 */
public class MTCloud
{
    /**
     * 合作方 ID:合作方在欢拓平台的唯一 ID
     */
    @Value("${mtcloud.openId}")
    public String openID = "";

    /**
     * 合作方秘钥:合作方 ID 对应的参数加密秘钥
     */
    @Value("${mtcloud.openToken}")
    public String openToken = "";
}
5.5、创建配置文件和启动类

(1)application.properties

# 服务端口
server.port=8306

# 服务名
spring.application.name=service-live

# 环境设置:dev、test、prod
spring.profiles.active=dev

# MySQL 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/smart_planet_live?characterEncoding=utf-8&useSSL=false
spring.datasource.username=MYXH
spring.datasource.password=520.ILY!

# 返回 Json 的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

# MyBatis 日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# 设置 mapper.xml 的路径
mybatis-plus.mapper-locations=classpath:com/myxh/smart/planet/live/mapper/xml/*.xml

# nacos 服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# 设置 MTCloud 类的配置 openId 与 openToken
mtcloud.openId=61237
mtcloud.openToken=c2c502add3dabfbbacae160432ccb783

(2)启动类。

package com.myxh.smart.planet.live;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author MYXH
 * @date 2023/10/26
 */
@SpringBootApplication
@EnableTransactionManagement
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.myxh.smart.planet")
@MapperScan("com.myxh.smart.planet.live.mapper")
public class ServiceLiveApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(ServiceLiveApplication.class, args);
    }
}
5.6、生成相关代码

生成相关代码

6、功能实现-直播课程列表接口

功能实现-直播课程列表接口

根据直播平台与自身业务设计直播相关的业务表,如:smart_planet_live。

6.1、LiveCourseController 类
package com.myxh.smart.planet.live.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 条件查询直播课程分页列表
     *
     * @param current 当前页码
     * @param limit   每页记录数
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "条件查询直播课程分页列表", description = "条件查询直播课程分页列表")
    @GetMapping("find/query/page/{current}/{limit}")
    public Result<IPage<LiveCourse>> index(
            @Parameter(name = "current", description = "当前页码", required = true)
            @PathVariable("current") Long current,
            @Parameter(name = "limit", description = "每页记录数", required = true)
            @PathVariable("limit") Long limit)
    {
        Page<LiveCourse> LiveCoursePageParam = new Page<>(current, limit);
        IPage<LiveCourse> LiveCoursePageModel = liveCourseService.selectPage(LiveCoursePageParam);

        return Result.ok(LiveCoursePageModel);
    }
}
6.2、LiveCourseService 接口
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 条件查询直播课程分页列表
     *
     * @param liveCoursePageParam 直播课程页面参数
     * @return LiveCoursePageModel 直播课程页面
     */
    IPage<LiveCourse> selectPage(Page<LiveCourse> liveCoursePageParam);
}
6.3、service-vod 模块创建接口

(1)获取讲师信息。

获取讲师信息

package com.myxh.smart.planet.vod.controller;

import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.vod.service.TeacherService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/9/29
 *
 * <p>
 * 教师 前端控制器
 * </p>
 */
@Tag(name = "教师接口", description = "教师管理接口")
@RestController
@RequestMapping("/admin/vod/teacher")
public class TeacherController
{
    @Autowired
    private TeacherService teacherService;

    /**
     * 根据 id 查询教师
     *
     * @param id id
     * @return teacher 教师
     */
    @Operation(summary = "查询", description = "根据 id 查询教师")
    @GetMapping("inner/get/teacher/{id}")
    public Teacher getTeacherLive(@Parameter(name = "id", description = "ID", required = true) @PathVariable("id") Long id)
    {
        Teacher teacher = teacherService.getById(id);

        return teacher;
    }
}

(2)service-course-client 定义接口。

service-course-client 定义接口

package com.myxh.smart.planet.client.course;

import com.myxh.smart.planet.model.vod.Teacher;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author MYXH
 * @date 2023/10/17
 */
@FeignClient("service-vod")
public interface CourseFeignClient
{
    @Operation(summary = "查询", description = "根据 id 查询教师")
    @GetMapping("admin/vod/teacher/inner/get/teacher/{id}")
    Teacher getTeacherLive(@Parameter(name = "id", description = "ID", required = true) @PathVariable("id") Long id);
}
6.4、service-live 引入依赖
<!-- service-course-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-course-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- service-user-client -->
<dependency>
    <groupId>com.myxh.smart.planet</groupId>
    <artifactId>service-user-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
6.5、LiveCourseServiceImpl 实现
package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.vod.Teacher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private CourseFeignClient teacherFeignClient;

    /**
     * 条件查询直播课程分页列表
     *
     * @param liveCoursePageParam 直播课程页面参数
     * @return LiveCoursePageModel 直播课程页面
     */
    @Override
    public IPage<LiveCourse> selectPage(Page<LiveCourse> liveCoursePageParam)
    {
        // 分页查询
        IPage<LiveCourse> LiveCoursePageModel = baseMapper.selectPage(liveCoursePageParam, null);

        // 获取课程教师信息
        List<LiveCourse> liveCourseList = LiveCoursePageModel.getRecords();

        // 遍历获取直播课程 List 集合
        for (LiveCourse liveCourse : liveCourseList)
        {
            // 获取每个课程教师 id
            Long teacherId = liveCourse.getTeacherId();

            // 根据教师 id 查询教师信息
            Teacher teacher = teacherFeignClient.getTeacherLive(teacherId);

            // 进行封装
            liveCourse.getParam().put("teacherName", teacher.getName());
            liveCourse.getParam().put("teacherLevel", teacher.getLevel());
        }

        return LiveCoursePageModel;
    }
}
7、功能实现-直播课程添加接口

功能实现-直播课程添加接口

7.1、添加工具类

添加工具类

(1)MTCloudAccountConfig 类。

package com.myxh.smart.planet.live.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/26
 */
@Data
@Component
@ConfigurationProperties(prefix = "mtcloud")
public class MTCloudAccountConfig
{
    private String openId;
    private String openToken;
}

(2)MTCloudConfig 类。

package com.myxh.smart.planet.live.config;

import com.myxh.smart.planet.live.mtcloud.MTCloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author MYXH
 * @date 2023/10/26
 */
@Component
public class MTCloudConfig
{
    @Autowired
    private MTCloudAccountConfig mtCloudAccountConfig;

    @Bean
    public MTCloud mtCloudClient()
    {
        return new MTCloud(mtCloudAccountConfig.getOpenId(), mtCloudAccountConfig.getOpenToken());
    }
}
7.2、LiveCourseController 类
package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 添加直播课程
     *
     * @param liveCourseFormVo 直播课程
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "添加直播课程", description = "添加直播课程")
    @PostMapping("save")
    public Result<Void> save(@RequestBody LiveCourseFormVo liveCourseFormVo)
    {
        liveCourseService.saveLive(liveCourseFormVo);

        return Result.ok(null);
    }
}
7.3、LiveCourseService 接口
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 添加直播课程
     *
     * @param liveCourseFormVo 直播课程
     */
    void saveLive(LiveCourseFormVo liveCourseFormVo);
}
7.4、LiveCourseServiceImpl 实现
package com.myxh.smart.planet.live.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.mtcloud.CommonResult;
import com.myxh.smart.planet.live.mtcloud.MTCloud;
import com.myxh.smart.planet.live.service.LiveCourseAccountService;
import com.myxh.smart.planet.live.service.LiveCourseDescriptionService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseAccount;
import com.myxh.smart.planet.model.live.LiveCourseDescription;
import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;
import lombok.SneakyThrows;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private CourseFeignClient teacherFeignClient;

    @Autowired
    private MTCloud mtCloudClient;

    @Autowired
    private LiveCourseDescriptionService liveCourseDescriptionService;

    @Autowired
    private LiveCourseAccountService liveCourseAccountService;

    /**
     * 添加直播课程
     *
     * @param liveCourseFormVo 直播课程
     */
    @SneakyThrows
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void saveLive(LiveCourseFormVo liveCourseFormVo)
    {
        LiveCourse liveCourse = new LiveCourse();
        BeanUtils.copyProperties(liveCourseFormVo, liveCourse);

        // 获取教师信息
        Teacher teacher = teacherFeignClient.getTeacherLive(liveCourseFormVo.getTeacherId());

        // 调用方法添加直播课程
        // 创建 map 集合,封装直播课程其他参数
        HashMap<Object, Object> options = new HashMap<>();

        // 直播类型,1:教育直播,2:生活直播
        // 默认 1,说明:根据平台开通的直播类型填写
        options.put("scenes", 1);
        options.put("password", liveCourseFormVo.getPassword());

        /*
        course_name  课程名称
        bid      发起直播课程的主播账号
        start_time   课程开始时间,格式:2015-01-10 12:00:00
        end_time     课程结束时间,格式:2015-01-10 13:00:00
        nickname     昵称
        accountIntro 主播介绍
        options      其他参数
         */
        String res = mtCloudClient.courseAdd(liveCourse.getCourseName(),
                teacher.getId().toString(),
                new DateTime(liveCourse.getStartTime()).toString("yyyy-MM-dd HH:mm:ss"),
                new DateTime(liveCourse.getEndTime()).toString("yyyy-MM-dd HH:mm:ss"),
                teacher.getName(),
                teacher.getIntro(),
                options);
        System.out.println("res = " + res);
        CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);

        // 把创建之后返回结果进行判断
        if (Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS)
        {
            // 添加直播基本信息
            JSONObject object = commonResult.getData();

            // 直播课程 id
            Long courseId = object.getLong("course_id");
            liveCourse.setCourseId(courseId);
            baseMapper.insert(liveCourse);

            // 添加直播描述信息
            LiveCourseDescription liveCourseDescription = new LiveCourseDescription();
            liveCourseDescription.setLiveCourseId(liveCourse.getId());
            liveCourseDescription.setDescription(liveCourseFormVo.getDescription());
            liveCourseDescriptionService.save(liveCourseDescription);

            // 添加直播账号信息
            LiveCourseAccount liveCourseAccount = new LiveCourseAccount();
            liveCourseAccount.setLiveCourseId(liveCourse.getId());
            liveCourseAccount.setAnchorAccount(object.getString("bid"));
            liveCourseAccount.setAnchorPassword(liveCourseFormVo.getPassword());
            liveCourseAccount.setAdminKey(object.getString("admin_key"));
            liveCourseAccount.setUserKey(object.getString("user_key"));
            liveCourseAccount.setAnchorKey(object.getString("zhubo_key"));
            liveCourseAccountService.save(liveCourseAccount);
        }
        else
        {
            String msg = commonResult.getMsg();
            System.out.println("msg = " + msg);
            throw new SmartPlanetException(20001, "直播创建失败!" + msg);
        }
    }
}
8、功能实现-直播课程删除接口

功能实现-直播课程删除接口

8.1、LiveCourseController 类
package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 删除直播课程
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "删除直播课程", description = "删除直播课程")
    @DeleteMapping("remove/{id}")
    public Result<Void> remove(@PathVariable("id") Long id)
    {
        liveCourseService.removeLive(id);

        return Result.ok(null);
    }
}
8.2、LiveCourseService 接口
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 删除直播课程
     *
     * @param id id
     */
    void removeLive(Long id);
}
8.3、LiveCourseServiceImpl 实现
package com.myxh.smart.planet.live.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.mtcloud.CommonResult;
import com.myxh.smart.planet.live.mtcloud.MTCloud;
import com.myxh.smart.planet.live.service.LiveCourseAccountService;
import com.myxh.smart.planet.live.service.LiveCourseDescriptionService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseAccount;
import com.myxh.smart.planet.model.live.LiveCourseDescription;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private MTCloud mtCloudClient;

    @Autowired
    private LiveCourseDescriptionService liveCourseDescriptionService;

    @Autowired
    private LiveCourseAccountService liveCourseAccountService;

    /**
     * 删除直播课程
     *
     * @param id id
     */
    @SneakyThrows
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void removeLive(Long id)
    {
        // 根据 id 查询直播课程信息
        LiveCourse liveCourse = baseMapper.selectById(id);

        if (liveCourse != null)
        {
            // 获取直播 courseId
            Long courseId = liveCourse.getCourseId();

            // 调用方法删除平台直播课程
            String res = mtCloudClient.courseDelete(courseId.toString());
            System.out.println("res = " + res);
            CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);

            // 把删除之后返回结果进行判断
            if (Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS)
            {
                // 删除直播基本信息
                baseMapper.deleteById(id);

                // 删除直播描述信息
                liveCourseDescriptionService.remove(new LambdaQueryWrapper<LiveCourseDescription>().ge(LiveCourseDescription::getLiveCourseId, id));

                // 删除直播账号信息
                liveCourseAccountService.remove(new LambdaQueryWrapper<LiveCourseAccount>().ge(LiveCourseAccount::getLiveCourseId, id));
            }
            else
            {
                String msg = commonResult.getMsg();
                System.out.println("msg = " + msg);
                throw new SmartPlanetException(20001, "删除直播课程失败!" + msg);
            }
        }
    }
}
9、功能实现-直播课程修改接口

功能实现-直播课程修改接口

9.1、LiveCourseController 类
package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 根据 id 查询直播课程基本信息
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据 id 查询直播课程基本信息", description = "根据 id 查询直播课程基本信息")
    @GetMapping("get/{id}")
    public Result<LiveCourse> get(@PathVariable("id") Long id)
    {
        LiveCourse liveCourse = liveCourseService.getById(id);

        return Result.ok(liveCourse);
    }

    /**
     * 根据 id 查询直播课程基本信息和描述信息
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "根据 id 查询直播课程基本信息和描述信息", description = "根据 id 查询直播课程基本信息和描述信息")
    @GetMapping("get/info/{id}")
    public Result<LiveCourseFormVo> getInfo(@PathVariable("id") Long id)
    {
        LiveCourseFormVo liveCourseFormVo = liveCourseService.getLiveCourseFormVo(id);

        return Result.ok(liveCourseFormVo);
    }

    /**
     * 更新直播课程
     *
     * @param liveCourseFormVo 直播课程
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "更新直播课程", description = "更新直播课程")
    @PutMapping("update")
    public Result<Void> updateById(@RequestBody LiveCourseFormVo liveCourseFormVo)
    {
        liveCourseService.updateLiveById(liveCourseFormVo);

        return Result.ok(null);
    }
}
9.2、LiveCourseService 接口
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 根据 id 查询直播课程基本信息和描述信息
     *
     * @param id id
     * @return liveCourseFormVo 直播课程
     */
    LiveCourseFormVo getLiveCourseFormVo(Long id);

    /**
     * 更新直播课程
     *
     * @param liveCourseFormVo 直播课程
     */
    void updateLiveById(LiveCourseFormVo liveCourseFormVo);
}
9.3、LiveCourseServiceImpl 实现
package com.myxh.smart.planet.live.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.mtcloud.CommonResult;
import com.myxh.smart.planet.live.mtcloud.MTCloud;
import com.myxh.smart.planet.live.service.LiveCourseAccountService;
import com.myxh.smart.planet.live.service.LiveCourseDescriptionService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseAccount;
import com.myxh.smart.planet.model.live.LiveCourseDescription;
import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.vo.live.LiveCourseFormVo;
import lombok.SneakyThrows;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private CourseFeignClient teacherFeignClient;

    @Autowired
    private MTCloud mtCloudClient;

    @Autowired
    private LiveCourseDescriptionService liveCourseDescriptionService;

    @Autowired
    private LiveCourseAccountService liveCourseAccountService;

    /**
     * 根据 id 查询直播课程基本信息和描述信息
     *
     * @param id id
     * @return liveCourseFormVo 直播课程
     */
    @Override
    public LiveCourseFormVo getLiveCourseFormVo(Long id)
    {
        // 获取直播课程基本信息
        LiveCourse liveCourse = this.getById(id);

        // 获取直播课程描述信息
        LiveCourseDescription liveCourseDescription = liveCourseDescriptionService.getLiveCourseDescriptionByLiveCourseId(id);

        // 封装数据
        LiveCourseFormVo liveCourseFormVo = new LiveCourseFormVo();
        BeanUtils.copyProperties(liveCourse, liveCourseFormVo);
        liveCourseFormVo.setDescription(liveCourseDescription.getDescription());

        return liveCourseFormVo;
    }

    /**
     * 更新直播课程
     *
     * @param liveCourseFormVo 直播课程
     */
    @SneakyThrows
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void updateLiveById(LiveCourseFormVo liveCourseFormVo)
    {
        // 根据 id 获取直播课程基本信息
        LiveCourse liveCourse = baseMapper.selectById(liveCourseFormVo.getId());
        BeanUtils.copyProperties(liveCourseFormVo, liveCourse);

        // 获取教师信息
        Teacher teacher = teacherFeignClient.getTeacherLive(liveCourseFormVo.getTeacherId());

        /*
        course_id 课程ID
        course_name  课程名称
        bid      发起直播课程的主播账号
        start_time   课程开始时间,格式:2015-01-10 12:00:00
        end_time     课程结束时间,格式:2015-01-10 13:00:00
        nickname     昵称
        accountIntro 主播介绍
        options      其他参数
         */
        HashMap<Object, Object> options = new HashMap<>();

        String res = mtCloudClient.courseUpdate(liveCourse.getCourseId().toString(),
                teacher.getId().toString(),
                liveCourse.getCourseName(),
                new DateTime(liveCourse.getStartTime()).toString("yyyy-MM-dd HH:mm:ss"),
                new DateTime(liveCourse.getEndTime()).toString("yyyy-MM-dd HH:mm:ss"),
                teacher.getName(),
                teacher.getIntro(),
                options);
        System.out.println("res = " + res);

        // 返回结果转换,判断是否成功
        CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);

        if (Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS)
        {
            JSONObject object = commonResult.getData();

            // 更新直播课程基本信息
            liveCourse.setCourseId(object.getLong("course_id"));
            baseMapper.updateById(liveCourse);

            // 直播课程描述信息更新
            LiveCourseDescription liveCourseDescription = liveCourseDescriptionService.getLiveCourseDescriptionByLiveCourseId(liveCourse.getId());
            liveCourseDescription.setDescription(liveCourseFormVo.getDescription());
            liveCourseDescriptionService.update(liveCourseDescription, new LambdaQueryWrapper<LiveCourseDescription>().ge(LiveCourseDescription::getLiveCourseId, liveCourse.getId()));

            // 更新直播账号信息
            LiveCourseAccount liveCourseAccount = new LiveCourseAccount();
            liveCourseAccount.setLiveCourseId(liveCourse.getId());
            liveCourseAccount.setAnchorAccount(object.getString("bid"));
            liveCourseAccount.setAnchorPassword(liveCourseFormVo.getPassword());
            liveCourseAccount.setAdminKey(object.getString("admin_key"));
            liveCourseAccount.setUserKey(object.getString("user_key"));
            liveCourseAccount.setAnchorKey(object.getString("zhubo_key"));
            liveCourseAccountService.update(liveCourseAccount, new LambdaUpdateWrapper<LiveCourseAccount>().ge(LiveCourseAccount::getLiveCourseId, liveCourse.getId()));
        }
        else
        {
            String msg = commonResult.getMsg();
            System.out.println("msg = " + msg);
            throw new SmartPlanetException(20001, "修改直播课程失败!" + msg);
        }
    }
}
9.4、LiveCourseDescriptionService 添加方法
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourseDescription;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程简介 服务类
 * </p>
 */
public interface LiveCourseDescriptionService extends IService<LiveCourseDescription>
{
    /**
     * 获取直播课程描述信息
     *
     * @param id id
     * @return liveCourseDescription 直播课程描述信息
     */
    LiveCourseDescription getLiveCourseDescriptionByLiveCourseId(Long id);
}
9.5、LiveCourseDescriptionServiceImpl 实现方法
package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.live.mapper.LiveCourseDescriptionMapper;
import com.myxh.smart.planet.live.service.LiveCourseDescriptionService;
import com.myxh.smart.planet.model.live.LiveCourseDescription;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程简介 服务实现类
 * </p>
 */
@Service
public class LiveCourseDescriptionServiceImpl extends ServiceImpl<LiveCourseDescriptionMapper, LiveCourseDescription> implements LiveCourseDescriptionService
{
    /**
     * 获取直播课程描述信息
     *
     * @param id id
     * @return liveCourseDescription 直播课程描述信息
     */
    @Override
    public LiveCourseDescription getLiveCourseDescriptionByLiveCourseId(Long id)
    {
        LambdaQueryWrapper<LiveCourseDescription> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LiveCourseDescription::getLiveCourseId, id);
        LiveCourseDescription liveCourseDescription = baseMapper.selectOne(wrapper);

        return liveCourseDescription;
    }
}
10、功能实现-查看账号接口

功能实现-查看账号接口

10.1、LiveCourseController 类
package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseAccountService;
import com.myxh.smart.planet.model.live.LiveCourseAccount;
import com.myxh.smart.planet.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseAccountService liveCourseAccountService;

    /**
     * 获取直播账号信息
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取直播账号信息", description = "获取直播账号信息")
    @GetMapping("get/live/course/account/{id}")
    public Result<LiveCourseAccount> getLiveCourseAccount(@PathVariable("id") Long id)
    {
        LiveCourseAccount liveCourseAccount = liveCourseAccountService.getLiveCourseAccountByLiveCourseId(id);

        return Result.ok(liveCourseAccount);
    }
}
10.2、LiveCourseAccountService 接口
package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourseAccount;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程账号(受保护信息) 服务类
 * </p>
 */
public interface LiveCourseAccountService extends IService<LiveCourseAccount>
{
    /**
     * 获取直播账号信息
     *
     * @param id id
     * @return liveCourseAccount 直播账号信息
     */
    LiveCourseAccount getLiveCourseAccountByLiveCourseId(Long id);
}
10.3、LiveCourseAccountServiceImpl 实现
package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.live.mapper.LiveCourseAccountMapper;
import com.myxh.smart.planet.live.service.LiveCourseAccountService;
import com.myxh.smart.planet.model.live.LiveCourseAccount;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程账号(受保护信息) 服务实现类
 * </p>
 */
@Service
public class LiveCourseAccountServiceImpl extends ServiceImpl<LiveCourseAccountMapper, LiveCourseAccount> implements LiveCourseAccountService
{
    /**
     * 获取直播账号信息
     *
     * @param id id
     * @return liveCourseAccount 直播账号信息
     */
    @Override
    public LiveCourseAccount getLiveCourseAccountByLiveCourseId(Long id)
    {
        LambdaQueryWrapper<LiveCourseAccount> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LiveCourseAccount::getLiveCourseId, id);
        LiveCourseAccount liveCourseAccount = baseMapper.selectOne(wrapper);

        return liveCourseAccount;
    }
}
11、功能实现-配置和观看记录接口

功能实现-配置和观看记录接口

11.1、查看配置信息

(1)LiveCourseController 类。

package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 获取直播配置信息
     *
     * @param id id
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取直播配置信息", description = "获取直播配置信息")
    @GetMapping("get/course/config/{id}")
    public Result<LiveCourseConfigVo> getCourseConfig(@PathVariable("id") Long id)
    {
        LiveCourseConfigVo liveCourseConfigVo = liveCourseService.getCourseConfig(id);

        return Result.ok(liveCourseConfigVo);
    }
}

(2)LiveCourseService 添加方法。

package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 获取直播配置信息
     *
     * @param id id
     * @return liveCourseConfigVo 直播配置信息
     */
    LiveCourseConfigVo getCourseConfig(Long id);
}

(3)LiveCourseServiceImpl 实现。

package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.service.LiveCourseConfigService;
import com.myxh.smart.planet.live.service.LiveCourseGoodsService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseConfig;
import com.myxh.smart.planet.model.live.LiveCourseGoods;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private LiveCourseConfigService liveCourseConfigService;

    @Autowired
    private LiveCourseGoodsService liveCourseGoodsService;

    /**
     * 获取直播配置信息
     *
     * @param id id
     * @return liveCourseConfigVo 直播配置信息
     */
    @Override
    public LiveCourseConfigVo getCourseConfig(Long id)
    {
        LiveCourseConfigVo liveCourseConfigVo = new LiveCourseConfigVo();

        // 根据课程 id 查询直播课程配置信息
        LiveCourseConfig liveCourseConfig = liveCourseConfigService.getLiveCourseConfigByLiveCourseId(id);

        if (liveCourseConfig != null)
        {
            // 查询直播课程商品列表
            List<LiveCourseGoods> liveCourseGoodsList = liveCourseGoodsService.getLiveCourseGoodsByLiveCourseId(id);

            // 封装 LiveCourseConfigVo
            BeanUtils.copyProperties(liveCourseConfig, liveCourseConfigVo);

            // 封装商品列表
            liveCourseConfigVo.setLiveCourseGoodsList(liveCourseGoodsList);
        }

        return liveCourseConfigVo;
    }
}

(4)LiveCourseConfigService 添加方法。

package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourseConfig;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程配置 服务类
 * </p>
 */
public interface LiveCourseConfigService extends IService<LiveCourseConfig>
{
    /**
     * 根据课程 id 查询直播课程配置信息
     *
     * @param id id
     * @return liveCourseConfig 直播课程配置信息
     */
    LiveCourseConfig getLiveCourseConfigByLiveCourseId(Long id);
}

(5)LiveCourseConfigServiceImpl 实现方法。

package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.live.mapper.LiveCourseConfigMapper;
import com.myxh.smart.planet.live.service.LiveCourseConfigService;
import com.myxh.smart.planet.model.live.LiveCourseConfig;
import org.springframework.stereotype.Service;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程配置 服务实现类
 * </p>
 */
@Service
public class LiveCourseConfigServiceImpl extends ServiceImpl<LiveCourseConfigMapper, LiveCourseConfig> implements LiveCourseConfigService
{
    /**
     * 根据课程 id 查询直播课程配置信息
     *
     * @param id id
     * @return liveCourseConfig 直播课程配置信息
     */
    @Override
    public LiveCourseConfig getLiveCourseConfigByLiveCourseId(Long id)
    {
        LambdaQueryWrapper<LiveCourseConfig> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LiveCourseConfig::getLiveCourseId, id);
        LiveCourseConfig liveCourseConfig = baseMapper.selectOne(wrapper);

        return liveCourseConfig;
    }
}

(6)LiveCourseGoodsService 添加方法。

package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourseGoods;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程关联推荐 服务类
 * </p>
 */
public interface LiveCourseGoodsService extends IService<LiveCourseGoods>
{
    /**
     * 查询直播课程商品列表
     *
     * @param id id
     * @return liveCourseGoodsList 直播课程商品列表
     */
    List<LiveCourseGoods> getLiveCourseGoodsByLiveCourseId(Long id);
}

(7)LiveCourseGoodsServiceImpl 实现方法。

package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.live.mapper.LiveCourseGoodsMapper;
import com.myxh.smart.planet.live.service.LiveCourseGoodsService;
import com.myxh.smart.planet.model.live.LiveCourseGoods;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程关联推荐 服务实现类
 * </p>
 */
@Service
public class LiveCourseGoodsServiceImpl extends ServiceImpl<LiveCourseGoodsMapper, LiveCourseGoods> implements LiveCourseGoodsService
{
    /**
     * 查询直播课程商品列表
     *
     * @param id id
     * @return liveCourseGoodsList 直播课程商品列表
     */
    @Override
    public List<LiveCourseGoods> getLiveCourseGoodsByLiveCourseId(Long id)
    {
        LambdaQueryWrapper<LiveCourseGoods> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LiveCourseGoods::getLiveCourseId, id);
        List<LiveCourseGoods> liveCourseGoodsList = baseMapper.selectList(wrapper);

        return liveCourseGoodsList;
    }
}
11.2、修改直播配置信息

(1)LiveCourseController 添加方法。

package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 修改直播配置信息
     *
     * @param liveCourseConfigVo 直播配置信息
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "修改直播配置信息", description = "修改直播配置信息")
    @PutMapping("update/config")
    public Result<Void> updateConfig(@RequestBody LiveCourseConfigVo liveCourseConfigVo)
    {
        liveCourseService.updateCourseConfig(liveCourseConfigVo);

        return Result.ok(null);
    }
}

(2)LiveCourseService 添加方法。

package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 修改直播配置信息
     *
     * @param liveCourseConfigVo 直播配置信息
     */
    void updateCourseConfig(LiveCourseConfigVo liveCourseConfigVo);
}

(3)LiveCourseServiceImpl 实现方法。

package com.myxh.smart.planet.live.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.exception.SmartPlanetException;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.mtcloud.CommonResult;
import com.myxh.smart.planet.live.mtcloud.MTCloud;
import com.myxh.smart.planet.live.service.LiveCourseConfigService;
import com.myxh.smart.planet.live.service.LiveCourseGoodsService;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.live.LiveCourseConfig;
import com.myxh.smart.planet.model.live.LiveCourseGoods;
import com.myxh.smart.planet.vo.live.LiveCourseConfigVo;
import com.myxh.smart.planet.vo.live.LiveCourseGoodsView;
import lombok.SneakyThrows;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private MTCloud mtCloudClient;

    @Autowired
    private LiveCourseConfigService liveCourseConfigService;

    @Autowired
    private LiveCourseGoodsService liveCourseGoodsService;

    /**
     * 修改直播配置信息
     *
     * @param liveCourseConfigVo 直播配置信息
     */
    @Override
    @Transactional(rollbackFor = {Exception.class})
    public void updateCourseConfig(LiveCourseConfigVo liveCourseConfigVo)
    {
        // 1、修改直播配置表
        LiveCourseConfig liveCourseConfig = new LiveCourseConfig();
        BeanUtils.copyProperties(liveCourseConfigVo, liveCourseConfig);

        if (liveCourseConfigVo.getId() == null)
        {
            liveCourseConfigService.save(liveCourseConfig);
        }
        else
        {
            liveCourseConfigService.update(liveCourseConfig, new LambdaUpdateWrapper<LiveCourseConfig>().eq(LiveCourseConfig::getLiveCourseId, liveCourseConfigVo.getLiveCourseId()));
        }

        // 2、修改直播商品表
        // 根据课程 id 批量删除直播商品列表
        liveCourseGoodsService.remove(new LambdaQueryWrapper<LiveCourseGoods>().eq(LiveCourseGoods::getLiveCourseId, liveCourseConfigVo.getLiveCourseId()));

        // 添加商品列表
        if (!CollectionUtils.isEmpty(liveCourseConfigVo.getLiveCourseGoodsList()))
        {
            liveCourseGoodsService.saveBatch(liveCourseConfigVo.getLiveCourseGoodsList());
        }

        // 3、修改在线直播平台中的直播配置信息
        this.updateLiveConfig(liveCourseConfigVo);
    }

    /**
     * 上传直播配置,修改在线直播平台中的直播配置信息
     *
     * @param liveCourseConfigVo 直播配置信息
     */
    @SneakyThrows
    private void updateLiveConfig(LiveCourseConfigVo liveCourseConfigVo)
    {
        LiveCourse liveCourse = baseMapper.selectById(liveCourseConfigVo.getLiveCourseId());

        // 封装平台方法需要的参数
        // 参数设置
        HashMap<Object, Object> options = new HashMap<>();

        // 界面模式
        options.put("pageViewMode", liveCourseConfigVo.getPageViewMode());

        // 观看人数开关
        JSONObject number = new JSONObject();
        number.put("enable", liveCourseConfigVo.getNumberEnable());
        options.put("number", number.toJSONString());

        // 观看人数开关
        JSONObject store = new JSONObject();
        number.put("enable", liveCourseConfigVo.getStoreEnable());
        number.put("type", liveCourseConfigVo.getStoreType());
        options.put("store", number.toJSONString());

        // 商城列表
        List<LiveCourseGoods> liveCourseGoodsList = liveCourseConfigVo.getLiveCourseGoodsList();

        if (!CollectionUtils.isEmpty(liveCourseGoodsList))
        {
            List<LiveCourseGoodsView> liveCourseGoodsViewList = new ArrayList<>();

            for (LiveCourseGoods liveCourseGoods : liveCourseGoodsList)
            {
                LiveCourseGoodsView liveCourseGoodsView = new LiveCourseGoodsView();
                BeanUtils.copyProperties(liveCourseGoods, liveCourseGoodsView);
                liveCourseGoodsViewList.add(liveCourseGoodsView);
            }

            JSONObject goodsListEdit = new JSONObject();
            goodsListEdit.put("status", "0");
            options.put("goodsListEdit ", goodsListEdit.toJSONString());
            options.put("goodsList", JSON.toJSONString(liveCourseGoodsViewList));
        }

        String res = mtCloudClient.courseUpdateConfig(liveCourse.getCourseId().toString(), options);
        System.out.println("res = " + res);
        CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);

        if (Integer.parseInt(commonResult.getCode()) != MTCloud.CODE_SUCCESS)
        {
            String msg = commonResult.getMsg();
            System.out.println("msg = " + msg);
            throw new SmartPlanetException(20001, "修改配置信息失败!" + msg);
        }
    }
}
11.3、获取最近直播课程

(1)LiveCourseController 添加方法。

package com.myxh.smart.planet.live.controller;

import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.result.Result;
import com.myxh.smart.planet.vo.live.LiveCourseVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 前端控制器
 * </p>
 */
@Tag(name = "直播课程管理", description = "直播课程管理接口")
@RestController
@RequestMapping("/admin/live/live/course")
public class LiveCourseController
{
    @Autowired
    private LiveCourseService liveCourseService;

    /**
     * 获取最近的直播
     *
     * @return Result 全局统一返回结果
     */
    @Operation(summary = "获取最近的直播", description = "获取最近的直播")
    @GetMapping("find/lately/list")
    public Result<List<LiveCourseVo>> findLatelyList()
    {
        List<LiveCourseVo> liveCourseVoList = liveCourseService.getLatelyList();

        return Result.ok(liveCourseVoList);
    }
}

(2)LiveCourseService 添加方法。

package com.myxh.smart.planet.live.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseVo;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务类
 * </p>
 */
public interface LiveCourseService extends IService<LiveCourse>
{
    /**
     * 获取最近的直播
     *
     * @return liveCourseVoList 直播课程列表
     */
    List<LiveCourseVo> getLatelyList();
}

(3)LiveCourseServiceImpl 实现方法。

package com.myxh.smart.planet.live.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.myxh.smart.planet.client.course.CourseFeignClient;
import com.myxh.smart.planet.live.mapper.LiveCourseMapper;
import com.myxh.smart.planet.live.service.LiveCourseService;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.model.vod.Teacher;
import com.myxh.smart.planet.order.DateUtil;
import com.myxh.smart.planet.vo.live.LiveCourseVo;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 服务实现类
 * </p>
 */
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService
{
    @Autowired
    private CourseFeignClient teacherFeignClient;

    /**
     * 获取最近的直播
     *
     * @return liveCourseVoList 直播课程列表
     */
    @Override
    public List<LiveCourseVo> getLatelyList()
    {
        List<LiveCourseVo> liveCourseVoList = baseMapper.findLatelyList();

        for (LiveCourseVo liveCourseVo : liveCourseVoList)
        {
            // 封装开始和结束时间
            liveCourseVo.setStartTimeString(new DateTime(liveCourseVo.getStartTime()).toString("yyyy年MM月dd HH:mm:ss"));
            liveCourseVo.setEndTimeString(new DateTime(liveCourseVo.getEndTime()).toString("yyyy年MM月dd HH:mm:ss"));

            // 封装教师
            Long teacherId = liveCourseVo.getTeacherId();
            Teacher teacher = teacherFeignClient.getTeacherLive(teacherId);
            liveCourseVo.setTeacher(teacher);

            // 封装直播状态
            liveCourseVo.setLiveStatus(this.getLiveStatus(liveCourseVo));
        }

        return liveCourseVoList;
    }

    /**
     * 直播状态,0:未开始,1:直播中,2:直播结束
     *
     * @param liveCourse 直播课程
     * @return liveStatus 直播状态
     */
    private int getLiveStatus(LiveCourse liveCourse)
    {
        // 直播状态,0:未开始,1:直播中,2:直播结束
        int liveStatus;
        Date curTime = new Date();

        if (DateUtil.dateCompare(curTime, liveCourse.getStartTime()))
        {
            liveStatus = 0;
        }
        else if (DateUtil.dateCompare(curTime, liveCourse.getEndTime()))
        {
            liveStatus = 1;
        }
        else
        {
            liveStatus = 2;
        }

        return liveStatus;
    }
}

(4)LiveCourseMapper 添加方法。

package com.myxh.smart.planet.live.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.myxh.smart.planet.model.live.LiveCourse;
import com.myxh.smart.planet.vo.live.LiveCourseVo;

import java.util.List;

/**
 * @author MYXH
 * @date 2023/10/26
 *
 * <p>
 * 直播课程 Mapper 接口
 * </p>
 */
public interface LiveCourseMapper extends BaseMapper<LiveCourse>
{
    /**
     * 获取最近的直播
     *
     * @return liveCourseVoList 直播课程列表
     */
    List<LiveCourseVo> findLatelyList();
}

(5)LiveCourseMapper.xml 编写 sql 语句。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.myxh.smart.planet.live.mapper.LiveCourseMapper">
    <resultMap id="liveCourseMap" type="com.myxh.smart.planet.vo.live.LiveCourseVo" autoMapping="true">
    </resultMap>

    <!-- 用于 select 查询公用抽取的列 -->
    <sql id="columns">
        id, course_id, course_name, start_time, end_time, teacher_id, cover, create_time, update_time, is_deleted
    </sql>

    <select id="findLatelyList" resultMap="liveCourseMap">
        SELECT
        <include refid="columns"/>
        FROM `live_course`
        WHERE DATE(start_time) >= CURDATE()
        ORDER BY id
        LIMIT 5
    </select>
</mapper>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末影小黑xh

感谢朋友们对我的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值