day13-硅谷课堂-公众号点播课程和直播管理模块

硅谷课堂第十三天-公众号点播课程和直播管理

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

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

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

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

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

(1)创建CourseApiController

@Api(tags = "课程")
@RestController
@RequestMapping("/api/vod/course")
public class CourseApiController {

    @Autowired
    private CourseService courseService;

    @Autowired
    private ChapterService chapterService;

   //根据课程分类查询课程列表(分页)用于微信公众号里的查询
    @ApiOperation("根据课程分类查询课程列表")
    @GetMapping("{subjectParentId}/{page}/{limit}")
    public Result findPageCourse(@ApiParam(value = "课程一级分类ID", required = true) @PathVariable Long subjectParentId,
                                 @ApiParam(name = "page", value = "当前页码", required = true) @PathVariable Long page,
                                 @ApiParam(name = "limit", value = "每页记录数", required = true) @PathVariable Long limit) {
        //封装条件
        CourseQueryVo courseQueryVo = new CourseQueryVo();
        courseQueryVo.setSubjectParentId(subjectParentId);
        //创建page对象
        Page<Course> pageParam = new Page<>(page,limit);
        Map<String, Object> map= courseService.findPage(pageParam,courseQueryVo);
        return Result.ok(map);
    }

    //根据课程ID查询课程详情信息
    @ApiOperation("根据ID查询课程")
    @GetMapping("getInfo/{courseId}")
    public Result getInfo(
            @ApiParam(value = "课程ID", required = true)
            @PathVariable Long courseId){
        //没有专门实体类或者不好确定返回什么类型的数据就返回Map<String, Object>这要取值放值方便
        Map<String, Object> map = courseService.getInfoById(courseId);
        return Result.ok(map);
    }

}

(2)编写CourseService

 	//根据课程分类查询课程列表(分页)用于微信公众号里的查询
    Map<String, Object>  findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo);
    //根据课程ID查询课程详情信息
    Map<String, Object> getInfoById(Long courseId);

(3)编写CourseServiceImpl

//根据课程分类查询课程列表(分页)用于微信公众号里的查询
    @Override
    public Map<String, Object> findPage(Page<Course> pageParam, CourseQueryVo courseQueryVo) {
        //获取条件值
        String title = courseQueryVo.getTitle();//课程名称
        Long subjectId = courseQueryVo.getSubjectId();//二级分类
        Long subjectParentId = courseQueryVo.getSubjectParentId();//一级分类
        Long teacherId = courseQueryVo.getTeacherId();//讲师
        //判断条件值是否为空,封装条件(其实就传了一个subjectid大可不必判空)
        QueryWrapper<Course> wrapper = new QueryWrapper<>();
        if(!StringUtils.isEmpty(title)) {
            wrapper.like("title",title);
        }
        if(!StringUtils.isEmpty(subjectId)) {
            wrapper.eq("subject_id",subjectId);
        }
        if(!StringUtils.isEmpty(subjectParentId)) {
            wrapper.eq("subject_parent_id",subjectParentId);
        }
        if(!StringUtils.isEmpty(teacherId)) {
            wrapper.eq("teacher_id",teacherId);
        }
        //调用方法进行条件分页查询
        Page<Course> pages = baseMapper.selectPage(pageParam, wrapper);
        //获取分页数据
        long totalCount = pages.getTotal();//总记录数
        long totalPage = pages.getPages();//总页数
        long currentPage = pages.getCurrent();//当前页
        long size = pages.getSize();//每页记录数
        //每页数据集合
        List<Course> records = pages.getRecords();

        //封装其他数据(获取讲师名称和课程分类名称)
        records.stream().forEach(item -> {
            this.getTeacherOrSubjectName(item);//调用下边方法获取讲师和分类名称
        });
        Map<String,Object> map = new HashMap<>();
        map.put("totalCount",totalCount);
        map.put("totalPage",totalPage);
        map.put("records",records);
        return map;
    }
    //获取讲师和分类名称
    private Course getTeacherOrSubjectName(Course course) {
        //获取讲师名称
        Long teacherId=course.getTeacherId();
        //根据id查询讲师信息
        Teacher teacher = teacherService.getById(teacherId);
        if(teacher != null) {
            course.getParam().put("teacherName",teacher.getName());
        }
        //获取课程一级分类名称
        Long subjectParentId=course.getSubjectParentId();
        //根据一级分类id查询一级分类信息
        Subject subjectOne = subjectService.getById(subjectParentId);
        if(subjectOne != null) {
            course.getParam().put("subjectParentTitle",subjectOne.getTitle());
        }
        //根据二级分类id查询二级分类信息
        Long subjectId=course.getSubjectId();
        Subject subjectTwo = subjectService.getById(subjectId);
        if(subjectTwo != null) {
            course.getParam().put("subjectTitle",subjectTwo.getTitle());
        }
        return course;
    }

    //根据课程ID查询课程详情信息
    @Override
    public Map<String, Object> getInfoById(Long courseId) {
        //更新浏览量  view_count表字段流量+1
        Course course = baseMapper.selectById(courseId);
        course.setViewCount(course.getViewCount() + 1);
        baseMapper.updateById(course);
        //根据课程id查询课程详情数据(自己写的方法在自己的实现层就调用basemapper)
        CourseVo courseVo = baseMapper.selectCourseVoById(courseId);
        //课程章节小节数据(之前写的方法)大纲列表(章节和小节列表也叫课时)树形结构显示
        List<ChapterVo> chapterVoList = chapterService.getTreeList(courseId);
        //课程描述信息
        CourseDescription courseDescription = courseDescriptionService.getById(courseId);
        //课程所属信息讲师信息
        Teacher teacher = teacherService.getById(course.getTeacherId());

        //TODO后续完善
        Boolean isBuy = false;
        //封装map集合,返回
        Map<String, Object> map = new HashMap<>();
        map.put("courseVo", courseVo);
        map.put("chapterVoList", chapterVoList);
        map.put("description", null != courseDescription ?
                courseDescription.getDescription() : "");
        map.put("teacher", teacher);
        map.put("isBuy", isBuy);//是否购买
        return map;
    }

(4)编写CourseMapper

public interface CourseMapper extends BaseMapper<Course> {
    /**
     * 根据课程id获取课程发布信息
     * @param id
     * @return
     */
    CoursePublishVo selectCoursePublishVoById(Long id);
    //根据课程id查询课程详情(与上面方法一致只不过所要返回字段变多了)
    CourseVo selectCourseVoById(Long courseId);
}

coursevo

@ApiModel("课程对象")
@Data
public class CourseVo {

    @ApiModelProperty(value = "课程ID")
    private String id;
    @ApiModelProperty(value = "课程标题")
    private String title;
    @ApiModelProperty(value = "一级分类标题")
    private String subjectParentTitle;
    @ApiModelProperty(value = "二级分类标题")
    private String subjectTitle;
    @ApiModelProperty(value = "讲师id")
    private Long teacherId;
    @ApiModelProperty(value = "讲师姓名")
    private String teacherName;
    @ApiModelProperty(value = "总课时")
    private Integer lessonNum;
    @ApiModelProperty(value = "课程销售价格")
    private String price;//只用于显示
    @ApiModelProperty(value = "课程封面图片路径")
    private String cover;
    @ApiModelProperty(value = "销售数量")
    private Long buyCount;
    @ApiModelProperty(value = "浏览数量")
    private Long viewCount;
    @ApiModelProperty(value = "课程状态")
    private String status;
    @ApiModelProperty(value = "课程发布时间")
    private String publishTime;
}

(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.atguigu.ggkt.vod.mapper.CourseMapper">

    <select id="selectCoursePublishVoById" resultType="com.atguigu.ggkt.vo.vod.CoursePublishVo">
        SELECT
        c.id,
        c.title,
        c.cover,
        c.lesson_num AS lessonNum,
        c.price,
        t.name AS teacherName,
        s1.title AS subjectParentTitle,
        s2.title AS subjectTitle
        FROM
        <include refid="tables" />
        WHERE c.id = #{id}
    </select>

    <select id="selectCourseVoById" resultType="com.atguigu.ggkt.vo.vod.CourseVo">
        SELECT
        <include refid="columns" />
        FROM
        <include refid="tables" />
        WHERE c.id = #{id}
    </select>
    
    <sql id="columns">
        c.id,
        c.title,
        c.lesson_num AS lessonNum,
        c.price,
        c.cover,
        c.buy_count AS buyCount,
        c.view_count AS viewCount,
        c.status,
        c.publish_time AS publishTime,
        c.teacher_id as teacherId,
        t.name AS teacherName,
        s1.title AS subjectParentTitle,
        s2.title AS subjectTitle
    </sql>
    
    <sql id="tables">
        course c
        LEFT JOIN teacher t ON c.teacher_id = t.id
        LEFT JOIN subject s1 ON c.subject_parent_id = s1.id
        LEFT JOIN subject s2 ON c.subject_id = s2.id
    </sql>
</mapper>
1.3、整合课程列表和详情前端

(1)查看路由文件

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

import request from '@/utils/request'
const api_name = '/api/vod/course'
export default {
  // 课程分页列表
  findPage(subjectParentId, pageNo, pageSize) {
    return request({
      url: `${api_name}/${subjectParentId}/${pageNo}/${pageSize}`,
      method: 'get'
    })
  },
  // 课程详情
  getInfo(courseId) {
    return request({
      url: `${api_name}/getInfo/${courseId}`,
      method: 'get'
    })
  }
}

(3)编写页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4FTS9wZD-1677738012823)(.\images\image-20220304095631556.png)]

course.vue

<template>
    <div>
        <van-image width="100%" height="200" src="https://cdn.uviewui.com/uview/swiper/1.jpg"/>
        <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.pages;
                this.loading = false;
                if(this.pageNo >= this.pages) {
                    this.finished = true;
                }
                this.pageNo++;
            });
        },
        info(id) {
            this.$router.push({path: '/courseInfo/' + 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'
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;
      });
    },
    buy() {
      this.$router.push({ path: '/trade/'+this.courseId })
    },
    play(video) {
      
    },
    see() {
      this.$router.push({ path: '/play/'+this.courseId+'/0' })
    }
  }
};
</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

@Api(tags = "腾讯视频点播")
@RestController
@RequestMapping("/api/vod")
public class VodApiController {

    @Autowired
    private VodService vodService;

    /**
     * 点播视频播放接口
     * @param courseId
     * @param videoId
     * @return
     */
    @GetMapping("getPlayAuth/{courseId}/{videoId}")
    public Result getPlayAuth(
            @ApiParam(value = "课程id", required = true)
            @PathVariable Long courseId,
            @ApiParam(value = "小节id", required = true)
            @PathVariable Long videoId) {
        Map<String,Object> map=vodService.getPlayAuth(courseId, videoId);
        return  Result.ok(map);
    }
}

(3)application.properties添加

tencent.video.appid=1312624373

(3)VodService创建方法

//获取视频播放凭证  点播视频播放接口
Map<String,Object> getPlayAuth(Long courseId, Long videoId);

(4)VodServiceImpl实现方法

@Value("${tencent.video.appid}")//读取配置文件定义的数据
private String appId;

//点播视频播放接口
@Override
public Map<String, Object> getPlayAuth(Long courseId, Long videoId) {
    //根据小节id获取小节对象,获取腾讯云视频id
    Video video = videoService.getById(videoId);
    if(video == null) {
        throw new GgktException(20001,"小节信息不存在");
    }

    Map<String, Object> map = new HashMap<>();
    map.put("videoSourceId",video.getVideoSourceId());
    map.put("appId",appId);//配置文件中的appid
    return map;
}
1.2、整合点播视频播放前端

(1)创建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方法

    play(video) {
      let videoId = video.id;
      let isFree = video.isFree;
      this.$router.push({ path: '/play/'+this.courseId+'/'+videoId })
    },

(3)index.html引入文件

在这里插入图片描述

<link href="//cloudcache.tencent-cloud.com/open/qcloud/video/tcplayer/tcplayer.css" rel="stylesheet">
<!-- 如需在IE8、9浏览器中初始化播放器,浏览器需支持Flash并在页面中引入 -->
<!--[if lt IE 9]>
<script src="//cloudcache.tencent-cloud.com/open/qcloud/video/tcplayer/ie8/videojs-ie8.js"></script>
<![endif]-->
<!-- 如果需要在 Chrome 和 Firefox 等现代浏览器中通过 H5 播放 HLS 格式的视频,需要在 tcplayer.v4.1.min.js 之前引入 hls.min.0.13.2m.js -->
<script src="//imgcache.qq.com/open/qcloud/video/tcplayer/libs/hls.min.0.13.2m.js"></script>
<!-- 引入播放器 js 文件 -->
<script src="//imgcache.qq.com/open/qcloud/video/tcplayer/tcplayer.v4.1.min.js"></script>

(4)创建play.vue页面

<template>
    <div>
        <video id="player-container-id" preload="auto" width="600" height="400" 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;

                //获取第一个播放视频id
                // this.firstVideo = this.chapterVoList[0].children[0]
                // if(this.videoSourceId == '0') {
                //     this.see(this.firstVideo);
                // }
                this.loading = false;
            });
        },

        see(video) {
            let videoId = video.id;
            let isFree = video.isFree;
            //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) {
            var player = TCPlayer("player-container-id", { /**player-container-id 为播放器容器ID,必须与html中一致*/
                fileID: data.videoSourceId, /**请传入需要播放的视频fileID 必须 */
                appID: data.appId, /**请传入点播账号的子应用appID 必须 */
                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

@RestController
@RequestMapping("api/order/orderInfo")
public class OrderInfoApiController {

    @Autowired
    private OrderInfoService orderInfoService;

    @ApiOperation("新增点播课程订单")
    @PostMapping("submitOrder")
    public Result submitOrder(@RequestBody OrderFormVo orderFormVo, HttpServletRequest request) {
        //返回订单id
        Long orderId = orderInfoService.submitOrder(orderFormVo);
        return Result.ok(orderId);
    }
}

OrderFormVo

@RestController
@RequestMapping("api/order/orderInfo")
public class OrderInfoApiController {

    @Autowired
    private OrderInfoService orderInfoService;

    /**
     * 生成订单方法
     * @param orderFormVo
     * @param request
     * @return
     */
    @ApiOperation("新增点播课程订单")
    @PostMapping("submitOrder")
    public Result submitOrder(@RequestBody OrderFormVo orderFormVo, HttpServletRequest request) {
        //返回订单id
        Long orderId = orderInfoService.submitOrder(orderFormVo);
        return Result.ok(orderId);
    }
}

(2)编写Service

OrderInfoService

//生成点播课程订单
Long submitOrder(OrderFormVo orderFormVo);

实现类:
在这里插入图片描述
在这里插入图片描述

3.3、创建获取课程信息接口

操作service_vod模块

(1)CourseApiController添加方法

 //根据课程id查询课程信息(返回course不返回result是因为这要远程调用取值方便)
    @ApiOperation("根据ID查询课程")
    @GetMapping("inner/getById/{courseId}")
    public Course getById(
            @ApiParam(value = "课程ID", required = true)
            @PathVariable Long courseId){
        return courseService.getById(courseId);
    }

(2)service_course_client定义方法

@ApiOperation("根据ID查询课程")
@GetMapping("/api/vod/course/inner/getById/{courseId}")
Course getById(@PathVariable Long courseId);
3.4、创建获取优惠券接口

操作service_activity模块

(1)创建CouponInfoApiController

@Api(tags = "优惠券接口")
@RestController
@RequestMapping("/api/activity/couponInfo")
public class CouponInfoApiController {

    @Autowired
    private CouponInfoService couponInfoService;
    //根据优惠券id查询(不返回reslut的原因是因为这要好取值)
    @ApiOperation(value = "获取优惠券")
    @GetMapping(value = "inner/getById/{couponId}")
    public CouponInfo getById(@PathVariable("couponId") Long couponId) {
        return couponInfoService.getById(couponId);
    }
    //更新优惠券使用状态
    @ApiOperation(value = "更新优惠券使用状态")
    @GetMapping(value = "inner/updateCouponInfoUseStatus/{couponUseId}/{orderId}")
    public Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId) {
        couponInfoService.  updateCouponInfoUseStatus(couponUseId, orderId);
        return true;
    }
}

(2)编写CouponInfoService

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

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

@FeignClient(value = "service-activity")
public interface CouponInfoFeignClient {

    @ApiOperation(value = "获取优惠券")
    @GetMapping(value = "/api/activity/couponInfo/inner/getById/{couponId}")
    CouponInfo getById(@PathVariable("couponId") Long couponId);
    
    /**
     * 更新优惠券使用状态
     */
    @GetMapping(value = "/api/activity/couponInfo/inner/updateCouponInfoUseStatus/{couponUseId}/{orderId}")
    Boolean updateCouponInfoUseStatus(@PathVariable("couponUseId") Long couponUseId, @PathVariable("orderId") Long orderId);

}
3.5、获取当前用户id

在这里插入图片描述

绿色部分为前端做法,localStorage相当于后端cookie
灰色部分为后端做法

(1)common模块引入依赖

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

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</dependency>

(2)复制工具类到common下的service_utils模块

(3)前端实现方式

......
// http request 拦截器
service.interceptors.request.use(config => {
        //获取localStorage里面的token值
        let token = window.localStorage.getItem('token') || '';
        if (token != '') {
            //把token值放到header里面
            config.headers['token'] = token; 
        }
        return config
    },
    err => {
        return Promise.reject(err);
    })
.......
3.6、生成订单Service

(1)service_order引入依赖(远程调用模块在此模块中引入进去)

<dependencies>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>service_course_client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>service_user_client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.atguigu</groupId>
        <artifactId>service_activity_client</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

(2)OrderInfoServiceImpl

@Autowired
private CourseFeignClient courseFeignClient;

@Autowired
private UserInfoFeignClient userInfoFeignClient;

@Autowired
private CouponInfoFeignClient couponInfoFeignClient;

    //生成订单方法
    @Override
    public Long submitOrder(OrderFormVo orderFormVo) {
        //1获取生成订单条件值
        Long courseId = orderFormVo.getCourseId();
        Long couponId = orderFormVo.getCouponId();
        //引入的那个工具类去获取用户id
        Long userId = AuthContextHolder.getUserId();

        //2判断当前用户是否已经生成订单
        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getCourseId, courseId);
        queryWrapper.eq(OrderDetail::getUserId, userId);

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

        //3 根据课程id查询课程信息(远程调用)
        Course course = courseFeignClient.getById(courseId);
        if (course == null) {
            throw new GgktException(ResultCodeEnum.DATA_ERROR.getCode(),
                    ResultCodeEnum.DATA_ERROR.getMessage());
        }
        //4根据用户id查询用户信息(远程调用)
        UserInfo userInfo = userInfoFeignClient.getById(userId);
        if (userInfo == null) {
            throw new GgktException(ResultCodeEnum.DATA_ERROR.getCode(),
                    ResultCodeEnum.DATA_ERROR.getMessage());
        }
        //5根据优惠券id查询优惠券信息(远程调用)
        //涉及金额要用BigDecimal
        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);
        //实际价格等于初始价格-优惠券价格  subtract为减
        orderInfo.setFinalAmount(orderInfo.getOriginAmount().subtract(orderInfo.getCouponReduce()));
        //流水号(订单号)
        orderInfo.setOutTradeNo(OrderNoUtils.getOrderNo());
        orderInfo.setTradeBody(course.getTitle());
        orderInfo.setOrderStatus("0");
        this.save(orderInfo);
        //6.1 封装数据到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));
        //实际价格等于初始价格-优惠券价格  subtract为减
        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();
    }

AuthContextHolder工具类:

/**
 * 获取登录用户信息类
 *
 */
public class AuthContextHolder {

    //后台管理用户id
    private static ThreadLocal<Long> adminId = new ThreadLocal<Long>();
    //会员用户id
    private static ThreadLocal<Long> userId = new ThreadLocal<Long>();

    public static Long getAdminId() {
        return adminId.get();
    }

    public static void setAdminId(Long _adminId) {
        adminId.set(_adminId);
    }

    public static Long getUserId(){
        return userId.get();
    }

    public static void setUserId(Long _userId){
        userId.set(_userId);
    }

}

OrderNoUtils(订单号工具类)

/**
* 订单号工具类
*
* @author qy
* @since 1.0
*/
public class OrderNoUtils {

   /**
    * 获取订单号
    * @return
    */
   public static String getOrderNo() {
       SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
       String newDate = sdf.format(new Date());
       String result = "";
       Random random = new Random();
       for (int i = 0; i < 3; i++) {
           result += random.nextInt(10);
       }
       return newDate + result;
   }

}

ResultCodeEnum(统一返回结果状态信息类)

/**
* 统一返回结果状态信息类
*
*/
@Getter
public enum ResultCodeEnum {

   SUCCESS(200,"成功"),
   FAIL(201, "失败"),
   SERVICE_ERROR(2012, "服务异常"),
   DATA_ERROR(204, "数据异常"),
   ILLEGAL_REQUEST(205, "非法请求"),
   REPEAT_SUBMIT(206, "重复提交"),

   LOGIN_AUTH(208, "未登陆"),
   PERMISSION(209, "没有权限"),

   PHONE_CODE_ERROR(211, "手机验证码错误"),

   MTCLOUD_ERROR(210, "直播接口异常"),

   COUPON_GET(220, "优惠券已经领取"),
   COUPON_LIMIT_GET(221, "优惠券已发放完毕"),

   FILE_UPLOAD_ERROR( 21004, "文件上传错误"),
   FILE_DELETE_ERROR( 21005, "文件刪除错误"),

   VOD_PALY_ERROR(209, "请购买后观看"),;

   private Integer code;

   private String message;

   private ResultCodeEnum(Integer code, String message) {
       this.code = code;
       this.message = message;
   }
}
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

@Api(tags = "微信支付接口")
@RestController
@RequestMapping("/api/order/wxPay")
public class WXPayController {

    @Autowired
    private WXPayService wxPayService;

    @ApiOperation(value = "下单 小程序支付")
    @GetMapping("/createJsapi/{orderNo}")
    public Result createJsapi(
            @ApiParam(name = "orderNo", value = "订单No", required = true)
            @PathVariable("orderNo") String orderNo) {
        return Result.ok(wxPayService.createJsapi(orderNo));
    }
}

(2)创建WXPayService

public interface WXPayService {
	Map createJsapi(String orderNo);
}

(3)service_order引入依赖

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

(4)创建WXPayServiceImpl

@Service
@Slf4j
public class WXPayServiceImpl implements WXPayService {

    @Autowired
    private OrderInfoService orderInfoService;
    @Resource
    private UserInfoFeignClient userInfoFeignClient;
    //微信支付(要根据官方文档去开发这里写的只是一种场景)
    @Override
    public Map<String, String> createJsapi(String orderNo) {
        try {
            //封装微信支付需要参数,使用map集合
            Map<String, String> paramMap = new HashMap();
            //1、设置参数(最好写到配置文件,然后@Value去获取值)
            //正式服务号id:(根据自己的改)
            paramMap.put("appid", "wxf913bfa3a2c7eeeb");
            //服务号商户号(根据自己的改)
            paramMap.put("mch_id", "1481962542");
            //引入依赖WXPayUtil
            paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
            //支付时弹框微信显示的内容,这里写的是test根据实际场景自己定义去
            paramMap.put("body", "test");
            //订单号
            paramMap.put("out_trade_no", orderNo);
            //支付金额为了测试支付0.01元
            paramMap.put("total_fee", "1");
            //支付客户端的id,这里是本地所以写的是本地根据实际场景写
            paramMap.put("spbill_create_ip", "127.0.0.1");
            //支付之后的跳转
            paramMap.put("notify_url", "http://glkt.atguigu.cn/api/order/wxPay/notify");
            //支付类型,按照生成固定金额支付,自己看文档去选择特定的支付类型
            paramMap.put("trade_type", "JSAPI");
            /**
             * 设置参数值当前微信用户openid
             * 目前实现逻辑:1 根据订单号获取userid  2根据userid获取openid(注释掉的那部分代码原理)
             *
             *
             * 因为当前使用测试号,测试号不支持支付功能为了使用正式服务号测试,
             * 采用下面写法(正式项目不这么写,是注释掉的那些代码写法)获取正式服务号微信openid
             * 通过其他方式获取正式服务号openid,直接设置
             */
//			paramMap.put("openid", "o1R-t5trto9c5sdYt6l1ncGmY5Y");
            //UserInfo userInfo = userInfoFeignClient.getById(paymentInfo.getUserId());
//			paramMap.put("openid", "oepf36SawvvS8Rdqva-Cy4flFFg");
            paramMap.put("openid", "oQTXC56lAy3xMOCkKCImHtHoLL");

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

            //client设置参数(商户keyMXb72b9RfshXZD4FRGV5KLqmv5bx9LT9根据自己的改)
            //因为使用setXmlParam方法所以要把map集合转为xml格式WXPayUtil.generateSignedXml方法实现
            client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9"));
            //表示支持https协议
            client.setHttps(true);
            //请求
            client.post();
            //3、微信支付接口返回第三方的数据
            String xml = client.getContent();
            //WXPayUtil.xmlToMap转为map集合
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            if(null != resultMap.get("result_code")  && !"SUCCESS".equals(resultMap.get("result_code"))) {
                System.out.println("error1");
            }

            //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, String> 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);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap<>();
        }
    }
}

HttpClientUtils(工具类)

/**
 * http请求客户端
 */
public class HttpClientUtils {
	private String url;
	private Map<String, String> param;
	private int statusCode;
	private String content;
	private String xmlParam;
	private boolean isHttps;

	public boolean isHttps() {
		return isHttps;
	}

	public void setHttps(boolean isHttps) {
		this.isHttps = isHttps;
	}

	public String getXmlParam() {
		return xmlParam;
	}

	public void setXmlParam(String xmlParam) {
		this.xmlParam = xmlParam;
	}

	public HttpClientUtils(String url, Map<String, String> param) {
		this.url = url;
		this.param = param;
	}

	public HttpClientUtils(String url) {
		this.url = url;
	}

	public void setParameter(Map<String, String> map) {
		param = map;
	}

	public void addParameter(String key, String value) {
		if (param == null)
			param = new HashMap<String, String>();
		param.put(key, value);
	}

	public void post() throws ClientProtocolException, IOException {
		HttpPost http = new HttpPost(url);
		setEntity(http);
		execute(http);
	}

	public void put() throws ClientProtocolException, IOException {
		HttpPut http = new HttpPut(url);
		setEntity(http);
		execute(http);
	}

	public void get() throws ClientProtocolException, IOException {
		if (param != null) {
			StringBuilder url = new StringBuilder(this.url);
			boolean isFirst = true;
			for (String key : param.keySet()) {
				if (isFirst) {
					url.append("?");
					isFirst = false;
				}else {
					url.append("&");
				}
				url.append(key).append("=").append(param.get(key));
			}
			this.url = url.toString();
		}
		HttpGet http = new HttpGet(url);
		execute(http);
	}

	/**
	 * set http post,put param
	 */
	private void setEntity(HttpEntityEnclosingRequestBase http) {
		if (param != null) {
			List<NameValuePair> nvps = new LinkedList<NameValuePair>();
			for (String key : param.keySet())
				nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
			http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
		}
		if (xmlParam != null) {
			http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
		}
	}

	private void execute(HttpUriRequest http) throws ClientProtocolException,
			IOException {
		CloseableHttpClient httpClient = null;
		try {
			if (isHttps) {
				SSLContext sslContext = new SSLContextBuilder()
						.loadTrustMaterial(null, new TrustStrategy() {
							// 信任所有
							public boolean isTrusted(X509Certificate[] chain,
									String authType)
									throws CertificateException {
								return true;
							}
						}).build();
				SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
						sslContext);
				httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
						.build();
			} else {
				httpClient = HttpClients.createDefault();
			}
			CloseableHttpResponse response = httpClient.execute(http);
			try {
				if (response != null) {
					if (response.getStatusLine() != null)
						statusCode = response.getStatusLine().getStatusCode();
					HttpEntity entity = response.getEntity();
					// 响应内容
					content = EntityUtils.toString(entity, Consts.UTF_8);
				}
			} finally {
				response.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			httpClient.close();
		}
	}

	public int getStatusCode() {
		return statusCode;
	}

	public String getContent() throws ParseException, IOException {
		return content;
	}

}

@Slf4j

4.4、服务号测试过程

(1)修改service-user模块配置文件(这是正式号的以前用的测试号,这里只是用正式号去测试一下所以改了)

wechat.mpAppId: wxf913bfa3a2c7eeeb
## 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: cd360d429e5c8db0c638d5ef9df74f6d

(2)service-user模块创建controller(用来之前WXPayServiceImpl)

@Controller
@RequestMapping("/api/user/openid")
public class GetOpenIdController {

    @Autowired
    private WxMpService wxMpService;

    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
        String userInfoUrl =
                "http://ggkt.vipgz1.91tunnel.com/api/user/openid/userInfo";
        String redirectURL = wxMpService
                .oauth2buildAuthorizationUrl(userInfoUrl,
                                             WxConsts.OAUTH2_SCOPE_USER_INFO,
                                             URLEncoder.encode(returnUrl.replace("guiguketan", "#")));
        return "redirect:" + redirectURL;
    }

    @GetMapping("/userInfo")
    @ResponseBody
    public String userInfo(@RequestParam("code") String code,
                           @RequestParam("state") String returnUrl) throws Exception {
        WxMpOAuth2AccessToken wxMpOAuth2AccessToken = this.wxMpService.oauth2getAccessToken(code);
        String openId = wxMpOAuth2AccessToken.getOpenId();
        System.out.println("【微信网页授权】openId={}"+openId);
        return openId;
    }
}

(3)修改前端App.vue

......
if (token == '') {
    let url = window.location.href.replace('#', 'guiguketan')
    //修改认证controller路径
    window.location = 'http://ggkt.vipgz1.91tunnel.com/api/user/openid/authorize?returnUrl=' + url
}
......

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

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

(1)trade.vue

<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() {
            debugger
            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() {
            //debugger
            couponApi.findCouponInfo().then(response => {
                // console.log(response.data);
                this.coupons = response.data.abledCouponsList;
                this.disabledCoupons = response.data.disabledCouponsList;
            });
        },

        sureOrder() {
            //debugger
            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>

(2)pay.vue

<template>

    <div>
        <van-image width="100%" height="200" src="https://cdn.uviewui.com/uview/swiper/1.jpg"/>

        <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 == 'UNPAID' ? '未支付' : '已支付' }}</div>
        </div>
        <div class="course_teacher_price_box" v-if="orderInfo.orderStatus == 'PAID'">
            <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;
                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', {
                   'appId': data.appId,     //公众号ID,由商户传入
                    'timeStamp': data.timeStamp,         //时间戳,自1970年以来的秒数
                    '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: '/courseInfo/' + 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添加方法

//根据订单id获取订单信息
    @ApiOperation(value = "获取")
    @GetMapping("getInfo/{id}")
    public Result getInfo(@PathVariable Long id) {
        OrderInfoVo orderInfoVo = orderInfoService.getOrderInfoVoById(id);
        return Result.ok(orderInfoVo);
    }

(2)OrderInfoServiceImpl实现方法

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

        OrderInfoVo orderInfoVo = new OrderInfoVo();
        BeanUtils.copyProperties(orderInfo, orderInfoVo);
        orderInfoVo.setCourseId(orderDetail.getCourseId());
        orderInfoVo.setCourseName(orderDetail.getCourseName());
        return orderInfoVo;
    }

OrderInfoVo:

@Data
public class OrderInfoVo extends OrderInfo {

	@ApiModelProperty(value = "课程id")
	private Long courseId;

	@ApiModelProperty(value = "课程名称")
	private String courseName;

	@ApiModelProperty(value = "课程封面图片路径")
	private String cover;

	@ApiModelProperty(value = "总时长:分钟")
	private Integer durationSum;

	@ApiModelProperty(value = "观看进度总时长:分钟")
	private Integer progressSum;

	@ApiModelProperty(value = "观看进度")
	private Integer progress;
}

orderinfo

@Data
@ApiModel(description = "OrderInfo")
@TableName("order_info")
public class OrderInfo extends BaseEntity {

	private static final long serialVersionUID = 1L;

	@ApiModelProperty(value = "用户id")
	@TableField("user_id")
	private Long userId;

	@ApiModelProperty(value = "昵称")
	@TableField("nick_name")
	private String nickName;

	@TableField("phone")
	private String phone;

	@ApiModelProperty(value = "原始金额")
	@TableField("origin_amount")
	private BigDecimal originAmount;

	@ApiModelProperty(value = "优惠券减免")
	@TableField("coupon_reduce")
	private BigDecimal couponReduce;

	@ApiModelProperty(value = "最终金额")
	@TableField("final_amount")
	private BigDecimal finalAmount;

	@ApiModelProperty(value = "订单状态")
	@TableField("order_status")
	private String orderStatus;

	@ApiModelProperty(value = "订单交易编号(第三方支付用)")
	@TableField("out_trade_no")
	private String outTradeNo;

	@ApiModelProperty(value = "订单描述(第三方支付用)")
	@TableField("trade_body")
	private String tradeBody;

	@ApiModelProperty(value = "session id")
	@TableField("session_id")
	private String sessionId;

	@ApiModelProperty(value = "地区id")
	@TableField("province")
	private String province;

	@ApiModelProperty(value = "支付时间")
	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	@TableField("pay_time")
	private Date payTime;

	@ApiModelProperty(value = "失效时间")
	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	@TableField("expire_time")
	private Date expireTime;

}
4.7、查询支付结果

在这里插入图片描述
所以自己添加一个方法去验证
(1)WXPayController添加方法

/**
     * 查询支付状态
     * @param orderNo
     * @return
     */
    @ApiOperation(value = "查询支付状态")
    @GetMapping("/queryPayStatus/{orderNo}")
    public Result queryPayStatus(
            @ApiParam(name = "orderNo", value = "订单No", required = true)
            @PathVariable("orderNo") String orderNo) {

        System.out.println("orderNo:"+orderNo);
        //调用订单号调用微信接口查询支付状态
        Map<String, String> resultMap = wxPayService.queryPayStatus(orderNo);
       //判断支付是否成功:根据微信支付状态接口判断
        if (resultMap == null) {//出错
            return Result.fail(null).message("支付出错");
        }
        if ("SUCCESS".equals(resultMap.get("trade_state"))) {//如果成功
            //更改订单状态(已经支付),处理支付结果
            String out_trade_no = resultMap.get("out_trade_no");
            System.out.println("out_trade_no:"+out_trade_no);
            orderInfoService.updateOrderStatus(out_trade_no);
            return Result.ok(null).message("支付成功");
        }
        return Result.ok(null).message("支付中");
    }

(2)WXPayServiceImpl实现方法

  //更改订单状态(已经支付),处理支付结果
    @Override
    public Map<String, String> queryPayStatus(String orderNo) {
        try {
            //1、封装微信接口需要参数,使用map
            Map paramMap = new HashMap<>();
            //正式服务号id:(根据自己的改)
            paramMap.put("appid", "wxf913bfa3a2c7eeeb");
            //服务号商户号(根据自己的改)
            paramMap.put("mch_id", "1481962542");
            /**
             * 以下为推荐写法放入配置类读取
             */
//            //正式服务号id:(根据自己的改)
//            paramMap.put("appid", wxPayAccountConfig.getAppId());
//            //服务号商户号(根据自己的改)
//            paramMap.put("mch_id", wxPayAccountConfig.getMchId());
            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");
            //商户keyMXb72b9RfshXZD4FRGV5KLqmv5bx9LT9根据自己的改)
            WXPayUtil.generateSignature(paramMap, "MXb72b9RfshXZD4FRGV5KLqmv5bx9LT9");
            /**
             * 以下为推荐写法放入配置类读取
             */
            //client.setXmlParam(WXPayUtil.generateSignedXml(paramMap, wxPayAccountConfig.getKey()));
            client.setHttps(true);
            client.post();
            //3、返回第三方的数据(转成map)
            String xml = client.getContent();
            Map<String, String> resultMap = WXPayUtil.xmlToMap(xml);
            return resultMap;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

(3)OrderInfoServiceImpl实现方法

 //更改订单状态(已经支付),处理支付结果
    @Override
    public void updateOrderStatus(String out_trade_no) {
        //根据out_trade_no查询订单
        LambdaQueryWrapper<OrderInfo> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(OrderInfo::getOutTradeNo,out_trade_no);
        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、通用直播模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VYJfOl6-1677738012830)(./images/20200912172809543.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KX93WAVW-1677738012831)(./images/20200912172228526.png)]

  1. 首先是主播方,它是产生视频流的源头,由一系列流程组成:第一,通过一定的设备来采集数据;第二,将采集的这些视频进行一系列的处理,比如水印、美颜和特效滤镜等处理;第三,将处理后的结果视频编码压缩成可观看可传输的视频流;第四,分发推流,即将压缩后的视频流通过网络通道传输出去。
  2. 其次是播放端,播放端功能有两个层面,第一个层面是关键性的需求;另一层面是业务层面的。先看第一个层面,它涉及到一些非常关键的指标,比如秒开,在很多场景当中都有这样的要求,然后是对于一些重要内容的版权保护。为了达到更好的效果,我们还需要配合服务端做智能解析,这在某些场景下也是关键性需求。再来看第二个层面也即业务层面的功能,对于一个社交直播产品来说,在播放端,观众希望能够实时的看到主播端推过来的视频流,并且和主播以及其他观众产生一定的互动,因此它可能包含一些像点赞、聊天和弹幕这样的功能,以及礼物这样更高级的道具。
  3. 我们知道,内容产生方和消费方一般都不是一一对应的。对于一个直播产品来讲,最直观的体现就是一个主播可能会有很多粉丝。因此,我们不能直接让主播端和所有播放端进行点对点通信,这在技术上是做不到或者很有难度。主播方播出的视频到达播放端之前,需要经过一系列的中间环节,也就是我们这里讲的直播服务器端。
  4. 直播服务器端提供的最核心功能是收集主播端的视频推流,并将其放大后推送给所有观众端。除了这个核心功能,还有很多运营级别的诉求,比如鉴权认证,视频连线和实时转码,自动鉴黄,多屏合一,以及云端录制存储等功能。另外,对于一个主播端推出的视频流,中间需要经过一些环节才能到达播放端,因此对中间环节的质量进行监控,以及根据这些监控来进行智能调度,也是非常重要的诉求。
  5. 实际上无论是主播端还是播放端,他们的诉求都不会仅仅是拍摄视频和播放视频这么简单。在这个核心诉求被满足之后,还有很多关键诉求需要被满足。比如,对于一个消费级的直播产品来说,除了这三大模块之外,还需要实现一个业务服务端来进行推流和播放控制,以及所有用户状态的维持。如此,就构成了一个消费级可用的直播产品。
2.2、如何快速开发完整直播
2.2.1、利用第三方SDK开发
  • 七牛云:七牛直播云是专为直播平台打造的全球化直播流服务和一站式实现SDK端到端直播场景的企业级直播云服务平台.
    ☞ 熊猫TV,龙珠TV等直播平台都是用的七牛云
  • 网易视频云:基于专业的跨平台视频编解码技术和大规模视频内容分发网络,提供稳定流畅、低延时、高并发的实时音视频服务,可将视频直播无缝对接到自身App.
  • 阿里云视频直播解决方案
    直播推流 SDK(iOS/Android)
    直播播放器 SDK(iOS/Android)
  • 欢拓云直播平台:欢拓是一家以直播技术为核心的网络平台,旨在帮助人们通过网络也能实现真实互动通讯。
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、体验总结

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

直播平台只是做了直播相关的业务,不能与我们的业务进行衔接,我们期望是在硅谷课堂的管理后台管理直播相关的业务,那么怎么做呢?对接直播业务接口,直播平台有对应的直播接口,我们直接对接即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值