😎 知识点概览
为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。
本章节为【学成在线】项目的 day15
的内容
目录
内容会比较多,小伙伴门可以根据目录进行按需查阅。
文章目录
- 😎 知识点概览
- 目录
- 一、学习页面:查询课程计划
-
- 0x01 需求分析
- 0x02 Api接口
- 0x03 服务端开发
-
- 0x04 前端开发
-
- 配置NGINX虚拟主机
- 前端 API 方法
- 前端 API 方法调用
- 测试
- 二、学习页面:获取视频播放地址
-
- 0x01 需求分析
- 0x02 课程发布:储存媒资信息
-
- 0x03 Logstash:扫描课程计划媒资
-
- 创建索引
- 创建模板文件
- 配置 mysql.conf
- 启动 logstash.bat
- Logstash多实例运行
- 0x04 搜素服务:查询课程媒资接口
-
- 需求分析
- Api接口定义
- Service
- Controller
- 测试
- 三、在线学习:接口开发
-
- 0x01 需求分析
- 0x02 搭建开发环境
- 0x03 Api接口
- 0x04 服务端开发
-
- 需求分析
- 搜索服务注册Eureka
- 搜索服务客户端
- 自定义错误代码
- Service
- Controller
- 测试
- 0x05 前端开发
-
- 需求分析
- api方法
- 配置代理
- 视频播放页面
- 简单的测试
- 完整的测试
-
- 1、上传文件
-
- 一些问题
- ~~方案1:删除本地分块文件重新尝试上传~~
- 方案2:检查前端提交的MD5值是否正确
- 2、为课程计划选择媒资信息
- 3、前端门户测试
- 四、待完善的一些功能
- 😁 认识作者
一、学习页面:查询课程计划
0x01 需求分析
到目前为止,我们已可以编辑课程计划信息并上传课程视频,下一步我们要实现在线学习页面动态读取章节对应的视频并进行播放。在线学习页面所需要的信息有两类:
- 课程计划信息
- 课程学习信息(视频地址、学习进度等)
如下图:

在线学习集成媒资管理的需求如下:
1、在线学习页面显示课程计划
2、点击课程计划播放该课程计划对应的视频
本章节实现学习页面动态显示课程计划,进入不同课程的学习页面右侧动态显示当前课程的课程计划。
0x02 Api接口
课程计划信息从哪里获取?
在课程发布完成后会自动发布到一个 course_pub
的表中,logstash
会自动将课程发布后的信息自动采集到 ES
索引库中,这些信息也包含课程计划信息。
所以考虑性能要求,课程发布后对课程的查询统一从 ES
索引库中查询。
前端通过请求 搜索服务
获取课程信息,需要单独在 搜索服务
中定义课程信息查询接口。
本接口接收课程id,查询课程所有信息返回给前端。
我们在搜素服务 API
下添加以下方法
@ApiOperation("根据id搜索课程发布信息")
public Map<String,CoursePub> getdetail(String id);
返回的课程信息为 json
结构:key
为课程id,value
为课程内容。
0x03 服务端开发
在搜索服务中开发查询课程信息接口。
Controller
在搜素服务下添加以下方法
@Override
@GetMapping("/getdetail/{id}")
public Map<String, CoursePub> getdetail(@PathVariable("id")String id) {return esCourseService.getdetail(id);
}
Service
public Map<String, CoursePub> getdetail(String id) {SearchRequest searchRequest = new SearchRequest(es_index);searchRequest.types(es_type);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();searchSourceBuilder.query(QueryBuilders.termQuery("id",id));searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = null;try {searchResponse = restHighLevelClient.search(searchRequest);} catch (IOException e) {e.printStackTrace();}SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits(); Map<String,CoursePub> map = new HashMap<>();for (SearchHit hit: searchHits) {Map<String, Object> sourceAsMap = hit.getSourceAsMap();String courseId = (String) sourceAsMap.get("id");String name = (String) sourceAsMap.get("name");String grade = (String) sourceAsMap.get("grade");String charge = (String) sourceAsMap.get("charge");String pic = (String) sourceAsMap.get("pic");String description = (String) sourceAsMap.get("description");String teachplan = (String) sourceAsMap.get("teachplan");CoursePub coursePub = new CoursePub();coursePub.setId(courseId);coursePub.setName(name);coursePub.setPic(pic);coursePub.setGrade(grade);coursePub.setTeachplan(teachplan);coursePub.setDescription(description);map.put(courseId,coursePub);}return map;
}
测试
使用 swagger-ui
或 postman
测试查询课程信息接口。

0x04 前端开发
配置NGINX虚拟主机
学习中心的二级域名为 ucenter.xuecheng.com
,我们在 nginx
中配置 ucenter
虚拟主机。
#学成网用户中心
server {listen 80;server_name ucenter.xuecheng.com;#个人中心location / {proxy_pass http://ucenter_server_pool;}
}
#前端ucenter
upstream ucenter_server_pool{#server 127.0.0.1:7081 weight=10;server 127.0.0.1:13000 weight=10;
}
在学习中心要调用搜索的 API
,使用 Nginx
解决代理,如下图:

在 ucenter
虚拟主机下配置搜索 Api
代理路径
#后台搜索(公开api)
upstream search_server_pool{server 127.0.0.1:40100 weight=10;
}
#学成网用户中心
server {listen 80;server_name ucenter.xuecheng.com;#个人中心location / {proxy_pass http://ucenter_server_pool;}#后端搜索服务location /openapi/search/ {proxy_pass http://search_server_pool/search/;}
}
前端 API 方法
在学习中心 xc-ui-pc-leanring
对课程信息的查询属于基础常用功能,所以我们将课程查询的 api
方法定义在base
模块下,如下图:

在system.js
中定义课程查询方法:
import http from './public'
export const course_view = id => {return http.requestGet('/openapi/search/course/getdetail/'+id);
}
前端 API 方法调用
在 learning_video.vue
页面中调用课程信息查询接口得到课程计划,将课程计划json
串转成对象。
xc-ui-pc-leanring/src/module/course/page/learning_video.vue
1、定义视图
课程计划
<div class="navCont"><div class="course-weeklist"><div class="nav nav-stacked" v-for="(teachplan_first, index) in teachplanList"><div class="tit nav-justified text-center"><i class="pull-left glyphicon glyphicon-th-list"></i>{{teachplan_first.pname} }<i class="pull-right"></i></div><li v-if="teachplan_first.children!=null" v-for="(teachplan_second, index) in teachplan_first.children"><i class="glyphicon glyphicon-check"></i><a :href="url" @click="study(teachplan_second.id)">{{teachplan_second.pname} }</a></li></div></div>
</div>
课程名称
<div class="top text-center">
{{coursename} }
</div>
定义数据对象
data() {return {url:'',courseId:'',chapter:'',coursename:'',coursepic:'',teachplanList:[],playerOptions: {autoplay: false,controls: true,sources: [{type: "application/x-mpegURL",src: ''}]},}
}
在 created
钩子方法中获取课程信息
created(){this.url = window.locationthis.courseId = this.$route.params.courseIdthis.chapter = this.$route.params.chaptersystemApi.course_view(this.courseId).then((view_course)=>{if(!view_course || !view_course[this.courseId]){this.$message.error("获取课程信息失败,请重新进入此页面!")return ;} let courseInfo = view_course[this.courseId]console.log(courseInfo)this.coursename = courseInfo.nameif(courseInfo.teachplan){let teachplan = JSON.parse(courseInfo.teachplan);this.teachplanList = teachplan.children;}})
},
测试
在浏览器请求:http://ucenter.xuecheng.com/#/learning/4028e581617f945f01617f9dabc40000/0
4028e581617f945f01617f9dabc40000
:第一个参数为课程 id
,测试时从 ES
索引库找一个课程 id
- 0:第二个参数为课程计划
id
,此参数用于点击课程计划播放视频。

如果出现跨域问题,但是确定已经配置了跨域,请尝试结束所以 nginx.exe 的进程 和 清空浏览器缓存。
如果还没有解决?重启电脑试试。
二、学习页面:获取视频播放地址
0x01 需求分析
用户进入在线学习页面,点击课程计划将播放该课程计划对应的教学视频。
业务流程如下:

业务流程说明:
1、用户进入在线学习页面,页面请求搜索服务获取课程信息(包括课程计划信息)并且在页面展示。
2、在线学习请求学习服务获取视频播放地址。
3、学习服务校验当前用户是否有权限学习,如果没有权限学习则提示用户。
4、学习服务校验通过,请求搜索服务获取课程媒资信息。
5、搜索服务请求ElasticSearch获取课程媒资信息。
为什么要请求 ElasticSearch
查询课程媒资信息?
出于性能的考虑,公开查询课程信息从搜索服务查询,分摊 mysql
数据库的访问压力。
什么时候将课程媒资信息存储到 ElasticSearch
中?
课程媒资信息是在课程发布的时候存入 ElasticSearch
,因为课程发布后课程信息将基本不再修改。
0x02 课程发布:储存媒资信息
需求分析
课程媒资信息是在课程发布的时候存入 ElasticSearch
索引库,因为课程发布后课程信息将基本不再修改,具体的业务流程如下。
1、课程发布,向课程媒资信息表写入数据。
1)根据课程 id
删除 teachplanMediaPub
中的数据
2)根据课程 id
查询 teachplanMedia
数据
3)将查询到的 teachplanMedia
数据插入到 teachplanMediaPub
中
2、Logstash 定时扫描课程媒资信息表,并将课程媒资信息写入索引库。
数据模型
在 xc_course
数据库创建课程计划媒资发布表:
CREATE TABLE `teachplan_media_pub` (`teachplan_id` varchar(32) NOT NULL COMMENT '课程计划id',`media_id` varchar(32) NOT NULL COMMENT '媒资文件id',`media_fileoriginalname` varchar(128) NOT NULL COMMENT '媒资文件的原始名称',`media_url` varchar(256) NOT NULL COMMENT '媒资文件访问地址',`courseid` varchar(32) NOT NULL COMMENT '课程Id',`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT'logstash使用',PRIMARY KEY (`teachplan_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
数据模型类如下:
package com.xuecheng.framework.domain.course;import lombok.Data;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;@Data
@ToString
@Entity
@Table(name="teachplan_media_pub")
@GenericGenerator(name = "jpa-assigned", strategy = "assigned")
public class TeachplanMediaPub implements Serializable {private static final long serialVersionUID = -916357110051689485L;@Id@GeneratedValue(generator = "jpa-assigned")@Column(name="teachplan_id")private String teachplanId;@Column(name="media_id")private String mediaId;@Column(name="media_fileoriginalname")private String mediaFileOriginalName;@Column(name="media_url")private String mediaUrl;@Column(name="courseid")private String courseId;@Column(name="timestamp")private Date timestamp;
}
Dao
创建 TeachplanMediaPub
表的 Dao
,向 TeachplanMediaPub
存储信息采用先删除该课程的媒资信息,再添加该课程的媒资信息,所以这里定义根据课程 id
删除课程计划媒资方法:
public interface TeachplanMediaPubRepository extends JpaRepository<TeachplanMediaPub, String> {long deleteByCourseId(String courseId);
}
从TeachplanMedia查询课程计划媒资信息
public interface TeachplanMediaRepository extends JpaRepository<TeachplanMedia, String> {List<TeachplanMedia> findByCourseId(String courseId);
}
Service
编写保存课程计划媒资信息方法,并在课程发布时调用此方法。
1、保存课程计划媒资信息方法
本方法采用先删除该课程的媒资信息,再添加该课程的媒资信息,在 CourseService
下定义该方法
private void saveTeachplanMediaPub(String courseId){List<TeachplanMedia> byCourseId = teachplanMediaRepository.findByCourseId(courseId);if(byCourseId == null) return; teachplanMediaPubRepository.deleteByCourseId(courseId);List<TeachplanMediaPub> teachplanMediaPubList = new ArrayList<>();for (TeachplanMedia teachplanMedia: byCourseId) {TeachplanMediaPub teachplanMediaPub = new TeachplanMediaPub();BeanUtils.copyProperties(teachplanMedia, teachplanMediaPub);teachplanMediaPubList.add(teachplanMediaPub);}teachplanMediaPubRepository.saveAll(teachplanMediaPubList);
}
2、课程发布时调用此方法
修改课程发布的 coursePublish
方法:
....
saveTeachplanMediaPub(courseId);
String pageUrl = cmsPostPageResult.getPageUrl();
return new CoursePublishResult(CommonCode.SUCCESS,pageUrl);
.....
测试
测试课程发布后是否成功将课程媒资信息存储到 teachplan_media_pub
中,测试流程如下:
1、指定一个课程
2、为课程计划添加课程媒资
3、执行课程发布
4、观察课程计划媒资信息是否存储至 teachplan_media_pub
中
注意:由于此测试仅用于测试发布课程计划媒资信息的功能,可暂时将 cms
页面发布的功能暂时屏蔽,提高测试效率。
测试结果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vrzs5589-1595567273126)(https://qnoss.codeyee.com/20200704_15/image7)]
0x03 Logstash:扫描课程计划媒资
Logstash
定时扫描课程媒资信息表,并将课程媒资信息写入索引库。
创建索引
1、创建 xc_course_media
索引

2、并向此索引创建如下映射
POST: http://localhost:9200/xc_course_media/doc/_mapping
{"properties" : {"courseid" : {"type" : "keyword"},"teachplan_id" : {"type" : "keyword"},"media_id" : {"type" : "keyword"},"media_url" : {"index" : false,"type" : "text"},"media_fileoriginalname" : {"index" : false,"type" : "text"}}
}
索引创建成功

创建模板文件
在 logstach
的 config
目录文件 xc_course_media_template.json
文件路径为 %ES_ROOT_DIR%/logstash6.8.8/config/xc_course_media_template.json
%ES_ROOT_DIR% 为 ElasticSearch 和 logstash 的安装目录
内容如下:
{"mappings" : {"doc" : {"properties" : {"courseid" : {"type" : "keyword"},"teachplan_id" : {"type" : "keyword"},"media_id" : {"type" : "keyword"},"media_url" : {"index" : false,"type" : "text"},"media_fileoriginalname" : {"index" : false,"type" : "text"}}},"template" : "xc_course_media"}
}
配置 mysql.conf
在logstash的 config
目录下配置 mysql_course_media.conf
文件供 logstash
使用,logstash
会根据
mysql_course_media.conf
文件的配置的地址从 MySQL
中读取数据向 ES
中写入索引。
参考https://www.elastic.co/guide/en/logstash/current/plugins-inputs-jdbc.html
配置输入数据源和输出数据源。
input {stdin {} jdbc {jdbc_connection_string => "jdbc:mysql://localhost:3306/xc_course?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC"# 数据库信息jdbc_user => "root"jdbc_password => "123123"# MYSQL 驱动地址,修改为maven仓库对应的位置jdbc_driver_library => "D:/soft/apache-maven-3.5.4/repository/mysql/mysql-connector-java/5.1.40/mysql-connector-java-5.1.40.jar"# the name of the driver class for mysqljdbc_driver_class => "com.mysql.jdbc.Driver"jdbc_paging_enabled => "true"jdbc_page_size => "50000"#要执行的sql文件#statement_filepath => "/conf/course.sql"statement => "select * from teachplan_media_pub where timestamp > date_add(:sql_last_value,INTERVAL 8 HOUR)"#定时配置schedule => "* * * * *"record_last_run => truelast_run_metadata_path => "D:/soft/elasticsearch/logstash-6.8.8/config/xc_course_media_metadata"}
}
output {elasticsearch {#ES的ip地址和端口hosts => "localhost:9200"#hosts => ["localhost:9200","localhost:9202","localhost:9203"]#ES索引库名称index => "xc_course_media"document_id => "%{teachplan_id}"document_type => "doc"template => "D:/soft/elasticsearch/logstash-6.8.8/config/xc_course_media_template.json"template_name =>"xc_course_media"template_overwrite =>"true"} stdout {#日志输出codec => json_lines}
}
启动 logstash.bat
启动 logstash.bat
采集 teachplan_media_pub
中的数据,向 ES
写入索引。
logstash.bat -f ../config/mysql_course_media.conf
课程发布成功后,Logstash 会自动参加 teachplan_media_pub
表中新增的数据,效果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILPBxfXi-1595567273134)(https://qnoss.codeyee.com/20200704_15/image10)]

Logstash多实例运行
由于之前我们还启动了一个 Logstash
对课程的发布信息进行采集,所以如果想两个 logstash
实例同时运行,因为每个实例都有一个.lock文件,所以不能使用同一个目录来存放数据,所以我们需要使用 --path.data=
为每个实例指定单独的数据目录,具体的代码如下:
该配置是在windows下进行的
课程发布实例
logstash_start_course_pub.bat
@title logstash in course_pub
logstash.bat -f ..\config\mysql.conf --path.data=../data/course_pub
课程计划媒体发布实例
logstash_start_teachplan_media.bat
@title logstash i n teachplan_media_pub
logstash.bat -f ../config/mysql_course_media.conf --path.data=../data/teachplan_media/
同时运行效果如下

0x04 搜素服务:查询课程媒资接口
需求分析
搜索服务
提供查询课程媒资接口,此接口供学习服务调用。
Api接口定义
@ApiOperation("根据课程计划查询媒资信息")
public TeachplanMediaPub getmedia(String teachplanId);
Service
1、配置课程计划媒资索引库等信息
在 application.yml
中配置
xuecheng:elasticsearch:hostlist: ${eshostlist:127.0.0.1:9200} course:index: xc_coursetype: docsource_field: id,name,grade,mt,st,charge,valid,pic,qq,price,price_old,status,studymodel,teachmode,expires,pub_time,start_time,end_timemedia:index: xc_course_mediatype: docsource_field: courseid,media_id,media_url,teachplan_id,media_fileoriginalname
2、service 方法开发
在 课程搜索服务
中定义课程媒资查询接口,为了适应后续需求,service
参数定义为数组,可一次查询多个课程计划的媒资信息。
public QueryResponseResult<TeachplanMediaPub> getmedia(String [] teachplanIds){SearchRequest searchRequest = new SearchRequest(media_index);searchRequest.types(media_type);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();String[] media_index_arr = media_field.split(",");searchSourceBuilder.fetchSource(media_index_arr, new String[]{});searchSourceBuilder.query(QueryBuilders.termsQuery("teachplan_id", teachplanIds));searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = null;try {searchResponse = restHighLevelClient.search(searchRequest);} catch (IOException e) {e.printStackTrace();}SearchHits hits = searchResponse.getHits();long totalHits = hits.getTotalHits();SearchHit[] searchHits = hits.getHits();List<TeachplanMediaPub> teachplanMediaPubList = new ArrayList<>();for(SearchHit hit:searchHits){TeachplanMediaPub teachplanMediaPub =new TeachplanMediaPub();Map<String, Object> sourceAsMap = hit.getSourceAsMap();String courseid = (String) sourceAsMap.get("courseid");String media_id = (String) sourceAsMap.get("media_id");String media_url = (String) sourceAsMap.get("media_url");String teachplan_id = (String) sourceAsMap.get("teachplan_id");String media_fileoriginalname = (String) sourceAsMap.get("media_fileoriginalname");teachplanMediaPub.setCourseId(courseid);teachplanMediaPub.setMediaUrl(media_url);teachplanMediaPub.setMediaFileOriginalName(media_fileoriginalname);teachplanMediaPub.setMediaId(media_id);teachplanMediaPub.setTeachplanId(teachplan_id);teachplanMediaPubList.add(teachplanMediaPub);}QueryResult<TeachplanMediaPub> queryResult = new QueryResult<>();queryResult.setList(teachplanMediaPubList);queryResult.setTotal(totalHits);return new QueryResponseResult<TeachplanMediaPub>(CommonCode.SUCCESS,queryResult);}
Controller
@GetMapping(value="/getmedia/{teachplanId}")
@Override
public TeachplanMediaPub getmedia(@PathVariable("teachplanId") String teachplanId) {String[] teachplanIds = new String[]{teachplanId};QueryResponseResult<TeachplanMediaPub> mediaPubQueryResponseResult = esCourseService.getmedia(teachplanIds);QueryResult<TeachplanMediaPub> queryResult = mediaPubQueryResponseResult.getQueryResult();if(queryResult!=null&& queryResult.getList()!=null&& queryResult.getList().size()>0){return queryResult.getList().get(0);} return new TeachplanMediaPub();
}
测试
使用 swagger-ui
和 postman
测试课程媒资查询接口。

三、在线学习:接口开发
0x01 需求分析
根据下边的业务流程,本章节完成前端学习页面请求学习服务获取课程视频地址,并自动播放视频。

0x02 搭建开发环境
1、创建数据库
创建 xc_learning
数据库,学习数据库将记录学生的选课信息、学习信息。
导入:资料/xc_learning.sql
2、创建学习服务工程
参考课程管理服务工程结构,创建学习服务工程:
导入:资料/xc-service-learning.zip
项目工程结构如下

0x03 Api接口
此 api
接口是课程学习页面请求学习服务获取课程学习地址。
定义返回值类型:
package com.xuecheng.framework.domain.learning.response;import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.model.response.ResultCode;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;@Data
@ToString
@NoArgsConstructor
public class GetMediaResult extends ResponseResult {public GetMediaResult(ResultCode resultCode, String fileUrl) {super(resultCode);this.fileUrl = fileUrl;}private String fileUrl;
}
定义接口,学习服务根据传入课程 ID
、章节 Id
(课程计划 ID
)来取学习地址。
@Api(value = "录播课程学习管理",description = "录播课程学习管理")
public interface CourseLearningControllerApi {@ApiOperation("获取课程学习地址")public GetMediaResult getMediaPlayUrl(String courseId,String teachplanId);
}
0x04 服务端开发
需求分析
学习服务根据传入课程ID、章节Id(课程计划ID)请求搜索服务获取学习地址。
搜索服务注册Eureka
学习服务要调用搜索服务查询课程媒资信息,所以需要将搜索服务注册到 eureka
中。
1、查看服务名称是否为 xc-service-search
spring:application:name: xc‐service‐search
2、配置搜索服务的配置文件 application.yml
,加入 Eureka
配置 如下:
eureka:client:registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/,http://localhost:50102/eureka/}instance:prefer-ip-address: true ip-address: ${IP_ADDRESS:127.0.0.1}instance-id: ${spring.application.name}:${server.port}
ribbon:MaxAutoRetries: 2 MaxAutoRetriesNextServer: 3 OkToRetryOnAllOperations: false ConnectTimeout: 5000 ReadTimeout: 6000
3、添加 eureka
依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId>
</dependency>
4、修改启动类,在class上添加如下注解:
@EnableDiscoveryClient
搜索服务客户端
在 学习服务
创建搜索服务的客户端接口,此接口会生成代理对象,调用搜索服务:
package com.xuecheng.learning.client;
import com.xuecheng.framework.domain.course.TeachplanMediaPub;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;@FeignClient(value = "xc‐service‐search")
public interface CourseSearchClient {@GetMapping(value="/getmedia/{teachplanId}")public TeachplanMediaPub getmedia(@PathVariable("teachplanId") String teachplanId);
}
自定义错误代码
我们在 com.xuecheng.framework.domain.learning.response
包下自定义一个错误消息模型
package com.xuecheng.framework.domain.learning.response;import com.xuecheng.framework.model.response.ResultCode;
import lombok.ToString;@ToString
public enum LearningCode implements ResultCode {LEARNING_GET_MEDIA_ERROR(false,23001,"学习中心获取媒资信息错误!");boolean success;int code;String message;private LearningCode(boolean success, int code, String message){this.success = success;this.code = code;this.message = message;}@Overridepublic boolean success() {return success;}@Overridepublic int code() {return code;}@Overridepublic String message() {return message;}
}
该消息模型基于 ResultCode
来实现,代码如下
package com.xuecheng.framework.model.response;
public interface ResultCode {boolean success();int code();String message();
从 ResultCode
中我们可以看出,我们约定了用户中心的错误代码使用 23000
,所以我们定义的一些错误信息的代码就从 23000 开始计数。
Service
在学习服务中定义 service
方法,此方法远程请求课程管理服务、媒资管理服务获取课程学习地址。
package com.xuecheng.learning.service.impl;import com.netflix.discovery.converters.Auto;
import com.xuecheng.framework.domain.course.TeachplanMediaPub;
import com.xuecheng.framework.domain.learning.response.GetMediaResult;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.learning.client.CourseSearchClient;
import com.xuecheng.learning.service.LearningService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class LearningServiceImpl implements LearningService {@AutowiredCourseSearchClient courseSearchClient;@Overridepublic GetMediaResult getMediaPlayUrl(String courseId, String teachplanId) {TeachplanMediaPub mediaPub = courseSearchClient.getmedia(teachplanId);if(mediaPub == null) ExceptionCast.cast(CommonCode.FAIL);return new GetMediaResult(CommonCode.SUCCESS, mediaPub.getMediaUrl());}
}
Controller
调用 service
根据课程计划 id
查询视频播放地址:
@RestController
@RequestMapping("/learning/course")
public class CourseLearningController implements CourseLearningControllerApi {@AutowiredLearningService learningService;@Override@GetMapping("/getmedia/{courseId}/{teachplanId}")public GetMediaResult getMediaPlayUrl(@PathVariable String courseId, @PathVariable String teachplanId) {return learningService.getMedia(courseId, teachplanId);}
}
测试
使用 swagger-ui
或postman
测试学习服务查询课程视频地址接口。

0x05 前端开发
需求分析
需要在学习中心前端页面需要完成如下功能:
1、进入课程学习页面需要带上 课程 Id
参数及课程计划Id的参数,其中 课程 Id
参数必带,课程计划 Id
可以为空。
2、进入页面根据 课程 Id
取出该课程的课程计划显示在右侧。
3、进入页面后判断如果请求参数中有课程计划 Id
则播放该章节的视频。
4、进入页面后判断如果 课程计划id
为0则需要取出本课程第一个 课程计划的Id
,并播放第一个课程计划的视频。
进入到模块 xc-ui-pc-leanring/src/module/course
api方法
let sysConfig = require('@/../config/sysConfig')
let apiUrl = sysConfig.xcApiUrlPre;
export const get_media = (courseId,chapter) => {return http.requestGet(apiUrl+'/api/learning/course/getmedia/'+courseId+'/'+chapter);
}
配置代理
在 Nginx
中的 ucenter.xuecheng.com
虚拟主机中配置 /api/learning/
的路径转发,此url
请转发到学习服务。
#学习服务
upstream learning_server_pool{server 127.0.0.1:40600 weight=10;
}#学成网用户中心
server {listen 80;server_name ucenter.xuecheng.com;#个人中心location / {proxy_pass http://ucenter_server_pool;}#后端搜索服务location /openapi/search/ {proxy_pass http://search_server_pool/search/; }#学习服务location ^~ /api/learning/ {proxy_pass http://learning_server_pool/learning/;}
}
视频播放页面
1、如果传入的课程计划id为0则取出第一个课程计划id
在 created
钩子方法中完成
created(){this.url = window.locationthis.courseId = this.$route.params.courseIdthis.chapter = this.$route.params.chaptersystemApi.course_view(this.courseId).then((view_course)=>{if(!view_course || !view_course[this.courseId]){this.$message.error("获取课程信息失败,请重新进入此页面!")return ;}let courseInfo = view_course[this.courseId]console.log(courseInfo)this.coursename = courseInfo.nameif(courseInfo.teachplan){console.log("准备开始播放视频")let teachplan = JSON.parse(courseInfo.teachplan);this.teachplanList = teachplan.children;if(this.chapter == "0" || !this.chapter){this.chapter = this.getFirstTeachplan();console.log("第一个教学计划id为 ",this.chapter);this.study(this.chapter);}else{this.study(this.chapter);}}})
},
取出第一个章节 id
,用户未输入课程计划 id
或者输入为 0
时,播放第一个。
getFirstTeachplan(){for(var i=0;i<this.teachplanList.length;i++){let firstTeachplan = this.teachplanList[i];if(firstTeachplan.children && firstTeachplan.children.length>0){let secondTeachplan = firstTeachplan.children[0];return secondTeachplan.id;}}return ;
},
开始学习:
study(chapter){courseApi.get_media(this.courseId,chapter).then((res)=>{if(res.success){let fileUrl = sysConfig.videoUrl + res.fileUrlthis.playvideo(fileUrl)}else if(res.message){this.$message.error(res.message)}else{this.$message.error("播放视频失败,请刷新页面重试")}}).catch(res=>{this.$message.error("播放视频失败,请刷新页面重试")});
},
2、点击右侧课程章节切换播放
在原有代码基础上添加 click
事件,点击调用开始学习方法(study
)。
<li v‐if="teachplan_first.children!=null" v‐for="(teachplan_second, index) in
teachplan_first.children"><i class="glyphicon glyphicon‐check"></i>
<a :href="url" @click="study(teachplan_second.id)">
{{teachplan_second.pname} }
</a>
</li>
3、地址栏路由url变更
这里需要注意一个问题,在用户点击课程章节切换播放时,地址栏的 url
也应该同步改变为当前所选择的课程计划 id
4、在线学习按钮
将 learnstatus
默认更改为 1
,这样就能显示出马上学习的按钮,方便我们后续的集成测试。
文件路径为 xc-ui-pc-static-portal/include/course_detail_dynamic.html
部分代码块如下
<script>var body= new Vue({ el: "#body", data: {editLoading: false,title:'测试',courseId:'',charge:'',learnstatus: 1 ,course:{},companyId:'template',company_stat:[],course_stat:{"s601001":"","s601002":"","s601003":""}},
简单的测试
访问在线学习页面:http://ucenter.xuecheng.com/#/learning/课程id/课程计划id
通过 url
传入两个参数:课程id
和 课程计划id
如果没有课程计划则传入0
测试项目如下:
1、传入正确的课程id、课程计划id,自动播放本章节的视频
2、传入正确的课程id、课程计划id传入0,自动播放第一个视频
3、传入错误的课程id 或 课程计划id,提示错误信息。
4、通过右侧章节目录切换章节及播放视频。
访问: http://ucenter.xuecheng.com/#/learning/4028e58161bcf7f40161bcf8b77c0000/4028e58161bd18ea0161bd1f73190008
传入正确的课程id、课程计划id,自动播放本章节的视频
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ef0xxym7-1595567273153)(https://qnoss.codeyee.com/20200704_15/image17)]
传入正确的课程id、课程计划id传入0,自动播放第一个视频
访问 http://ucenter.xuecheng.com/#/learning/4028e58161bcf7f40161bcf8b77c0000/0
识别出第一个课程计划的 id

需要注意的是这里的 chapter
参数是我自己在 study
函数里加上去的,可以忽略。
传入错误的课程id或课程计划id,提示错误信息。

通过右侧章节目录切换章节及播放视频。
点击章节即可播放,但是点击制定章节后 url
没有发生改变,这个问题暂时还没有解决,关注笔记后面的内容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TOGdxwb4-1595567273158)(https://qnoss.codeyee.com/20200704_15/image20)]
完整的测试
准备工作
我们整理一下测试的流程
- 上传两个媒资视频文件,用于测试
- 进入到课程管理,为课程计划选择媒资信息
- 发布课程,等待
logstash
将数据采集到 ElasticSearch
的索引库中
- 进入学成网主页,点击课程,进入到搜索门户页面
- 搜索课程,进入到课程详情页面
- 点击开始学习,进入到课程学习页面,选择课程计划中的一个章节进行学习。
1、上传文件
首先我们使用之前开发的媒资管理模块,上传两个视频文件用于测试。
第一个文件上传成功

一些问题
在上传第二个文件时,发生了错误,我们来检查一下问题出在了哪里

在媒体服务的控制台中可以看到,在 mergeChunks
方法在校验文件 md5
时候抛出了异常
我们在 MD5
校验这里打个断点,重新上传文件,分析一下问题所在。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OpEMZGI8-1595567273166)(https://qnoss.codeyee.com/20200704_15/image23)]
单步调试后发现,合并文件后的MD5值与用户上传的源文件值不相等

方案1:删除本地分块文件重新尝试上传
考虑到可能是在用户上传完 视频的分块文件时发生了一些问题,导致合并文件后与源文件的大小不等,导致MD5也不相同,这里我们把这个视频上传到本地的文件全部删除,在媒资上传页面重新上传文件。
对比所有分块文件的字节大小和本地源文件的大小,完全是相等的

删除所有文件后重新上传,md5值还是不等,考虑从调试一下文件合并的代码。
方案2:检查前端提交的MD5值是否正确
在查阅是否有其他的MD5值获取方案时,发现了一个使用 windows
本地命令获取文件MD5值的方法
certutil -hashfile .\19-在线学习接口-集成测试.avi md5
惊奇的发现,TM的原来是前端那边转换的MD5值不正确,后端这边是没有问题的。

从前面的图可以看出,本地和后端转换的都是以一个 f6f0
开头的MD5值

那么问题就出现在前端了,还需要花一些时间去分析一下,这里暂时就先告一段落,因为上传了几个文件测试中只有这一个文件出现了问题。
2、为课程计划选择媒资信息
进入到一个课程的管理页面
http://localhost:12000/#/course/manage/baseinfo/4028e58161bcf7f40161bcf8b77c0000
将刚才我们上传的媒资文件的信息和课程计划绑定

选择效果如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-epKaqzCD-1595567273178)(https://qnoss.codeyee.com/20200704_15/image29)]
2、发布课程,等待 logstash
从 course_pub
以及 teachplan_media_pub
表中采集数据到 ElasticSearch
当中

发布成功后,我们可以从 teachplan_media_pub
表中看到刚才我们发布的媒资信息

再观察 Logstash 的控制台,发现两个 Logstash 的实例都对更新的课程发布信息进行了采集
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hTUve2ik-1595567273183)(https://qnoss.codeyee.com/20200704_15/image32)]
3、前端门户测试
打开我们的门户主站 http://www.xuecheng.com/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wZe9R84-1595567273185)(https://qnoss.codeyee.com/20200704_15/image33)]
点击导航栏的课程,进入到我们的搜索门户页面
如果无法进入到搜索门户,请检查你的 xc-ui-pc-portal 前端工程是否已经启动
进入到搜索门户后,可以看到一些初始化时搜索的课程数据,默认是搜索第一页的数据,每页2个课程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJ1AKoJb-1595567273187)(https://qnoss.codeyee.com/20200704_15/image34)]
我们可以测试搜索一下前面我们选择媒资信息时所用的课程

点击课程,进入到课程详情页面,然后再点击开始学习。

点击马上学习后,会进入到该课程的在线学习页面,默认自动播放我们第一个课程计划中的视频。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcuLWnf2-1595567273193)(https://qnoss.codeyee.com/20200704_15/image37)]
我们可以在右侧的目录中选择第二个课程计划,会自动播放所选的课程计划所对应的媒资视频播放地址,该 播放地址正是我们刚才通过 Logstash
自动采集到 ElasticSearch
的索引信息,效果图如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cvi9Dr0Y-1595567273195)(https://qnoss.codeyee.com/20200704_15/image38)]
四、待完善的一些功能
😁 认识作者
作者:👦 LCyee ,全干型代码🐕
自建博客:https://www.codeyee.com
记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。
CSDN 博客:https://blog.csdn.net/codeyee
记录和分享一些开发过程中遇到的问题以及解决的思路。
欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。
