硅谷课堂第十三天-公众号点播课程和直播管理
一、实现公众号点播课程相关功能
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)]
- 首先是主播方,它是产生视频流的源头,由一系列流程组成:第一,通过一定的设备来采集数据;第二,将采集的这些视频进行一系列的处理,比如水印、美颜和特效滤镜等处理;第三,将处理后的结果视频编码压缩成可观看可传输的视频流;第四,分发推流,即将压缩后的视频流通过网络通道传输出去。
- 其次是播放端,播放端功能有两个层面,第一个层面是关键性的需求;另一层面是业务层面的。先看第一个层面,它涉及到一些非常关键的指标,比如秒开,在很多场景当中都有这样的要求,然后是对于一些重要内容的版权保护。为了达到更好的效果,我们还需要配合服务端做智能解析,这在某些场景下也是关键性需求。再来看第二个层面也即业务层面的功能,对于一个社交直播产品来说,在播放端,观众希望能够实时的看到主播端推过来的视频流,并且和主播以及其他观众产生一定的互动,因此它可能包含一些像点赞、聊天和弹幕这样的功能,以及礼物这样更高级的道具。
- 我们知道,内容产生方和消费方一般都不是一一对应的。对于一个直播产品来讲,最直观的体现就是一个主播可能会有很多粉丝。因此,我们不能直接让主播端和所有播放端进行点对点通信,这在技术上是做不到或者很有难度。主播方播出的视频到达播放端之前,需要经过一系列的中间环节,也就是我们这里讲的直播服务器端。
- 直播服务器端提供的最核心功能是收集主播端的视频推流,并将其放大后推送给所有观众端。除了这个核心功能,还有很多运营级别的诉求,比如鉴权认证,视频连线和实时转码,自动鉴黄,多屏合一,以及云端录制存储等功能。另外,对于一个主播端推出的视频流,中间需要经过一些环节才能到达播放端,因此对中间环节的质量进行监控,以及根据这些监控来进行智能调度,也是非常重要的诉求。
- 实际上无论是主播端还是播放端,他们的诉求都不会仅仅是拍摄视频和播放视频这么简单。在这个核心诉求被满足之后,还有很多关键诉求需要被满足。比如,对于一个消费级的直播产品来说,除了这三大模块之外,还需要实现一个业务服务端来进行推流和播放控制,以及所有用户状态的维持。如此,就构成了一个消费级可用的直播产品。
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、体验总结
上面的体验完全能够满足我们业务的需要,硅谷课堂的需求是定期推出直播课程,方便学员与名师之间的交流互动,在直播间老师可以推荐点播课程(类似直播带货),学员可以点赞交流,购买推荐的点播课程。
直播平台只是做了直播相关的业务,不能与我们的业务进行衔接,我们期望是在硅谷课堂的管理后台管理直播相关的业务,那么怎么做呢?对接直播业务接口,直播平台有对应的直播接口,我们直接对接即可。