学习页面查询课程计划
后端开发
修改xc-service-searcj
服务中的代码
API定义
@ApiOperation("根据id查询课程信息")
Map<String, EsCoursePub> getAll(String id);
EsCourseController
@Override
@GetMapping("getall/{id}")
public Map<String, EsCoursePub> getAll(@PathVariable String id) {
return esCourseService.getAll(id);
}
EsCourseService
/**
* 查询课程信息
*
* @param id 课程id
* @return Map<String, EsCoursePub>
*/
public Map<String, EsCoursePub> getAll(String id) {
Map<String, EsCoursePub> result = new HashMap<>();
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 查询条件
nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery("id", id));
AggregatedPage<EsCoursePub> queryForPage = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), EsCoursePub.class);
queryForPage.getContent().forEach(coursePub -> result.put(coursePub.getId(), coursePub));
return result;
}
测试
前端开发
learning_video.vue
主要修改learning_video.vue
文件中的create()
区
created(){
//当前请求的url
this.url = window.location
//课程id
this.courseId = this.$route.params.courseId
//章节id
this.chapter = this.$route.params.chapter
//取出课程Id
systemApi.course_view(this.courseId).then((view_course)=>{
console.log(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.name
if(courseInfo.teachplan){
let teachplan = JSON.parse(courseInfo.teachplan);
this.teachplanList = teachplan.children;
}
})
}
Nginx配置文件
截至目前为止,nginx配置文件如下
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
# cms页面预览
upstream cms_server_pool{
server 127.0.0.1:31001 weight=10;
}
# 静态资源服务
upstream static_server_pool{
server 127.0.0.1:91 weight=10;
}
# 前端动态门户
upstream dynamic_portal_server_pool{
server 127.0.0.1:10000 weight=10;
}
# 搜索接口
upstream search_server_pool{
server 127.0.0.1:40100 weight=10;
}
#媒体服务
upstream video_server_pool{
server 127.0.0.1:90 weight=10;
}
server{
listen 80;
server_name www.xuecheng.com;
ssi on;
ssi_silent_errors on;
location / {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/;
index index.html;
}
# 静态资源,包括系统所需要的图片,js、css等静态资源
location /static/img/ {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/img/;
}
location /static/css/ {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/css/;
}
location /static/js/ {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/js/;
}
location /static/plugins/ {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/plugins/;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET;
}
location /plugins/ {
alias F:/xcEdu/xcEdu_ui/xc-ui-pc-static-portal/plugins/;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET;
}
# 页面预览
location /cms/preview/ {
proxy_pass http://cms_server_pool/cms/preview/;
}
location /static/company/ {
proxy_pass http://static_server_pool;
}
location /static/teacher/ {
proxy_pass http://static_server_pool;
}
location /static/stat/ {
proxy_pass http://static_server_pool;
}
location /course/detail/ {
proxy_pass http://static_server_pool;
}
# 前端门户课程搜索
location ^~ /course/search {
proxy_pass http://dynamic_portal_server_pool;
}
# 后端搜索服务
location /openapi/search/ {
proxy_pass http://search_server_pool/search/;
}
# 分类信息
location /static/category/ {
proxy_pass http://static_server_pool;
}
# 开发环境Webpack定时加载文件
location ^~ /__webpack_hmr {
proxy_pass http://dynamic_portal_server_pool/__webpack_hmr;
}
# 开发环境nuxt访问
location ^~ /_nuxt/ {
proxy_pass http://dynamic_portal_server_pool/_nuxt/;
}
}
#学成网媒体服务代理
map $http_origin $origin_list{
default http://www.xuecheng.com;
"~http://www.xuecheng.com" http://www.xuecheng.com;
"~http://ucenter.xuecheng.com" http://ucenter.xuecheng.com;
}
#学成网媒体服务代理
server {
listen 80;
server_name video.xuecheng.com;
location /video {
proxy_pass http://video_server_pool;
add_header Access-Control-Allow-Origin $origin_list;
#add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods GET;
}
}
#学成网用户中心
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/;
}
}
#前端ucenter
upstream ucenter_server_pool{
server 127.0.0.1:13000 weight=10;
}
# 学成网静态资源
server {
listen 91;
server_name localhost;
# 公司信息
location /static/company/ {
alias F:/xcEdu/xcEdu_ui/static/company/;
}
# 老师信息
location /static/teacher/ {
alias F:/xcEdu/xcEdu_ui/static/teacher/;
}
# 统计信息
location /static/stat/ {
alias F:/xcEdu/xcEdu_ui/static/stat/;
}
# 课程静态页
location /course/detail/ {
alias F:/xcEdu/xcEdu_ui/static/course/detail/;
}
# 分类信息
location /static/category/ {
alias F:/xcEdu/xcEdu_ui/static/category/;
}
}
#学成网媒体服务
server {
listen 90;
server_name localhost;
#视频目录
location /video/ {
alias E:/nginx/xcEdu/video/;
}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
测试
课程计划展示成功。
学习页面获取视频播放地址
需求分析
在线学习视频播放流程图如下:
- 用户进入在线学习页面,页面请求搜索服务获取课程信息(包括课程计划信息)并且在页面展示。
- 在线学习请求学习服务获取视频播放地址。
- 学习服务校验当前用户是否有权限学习,如果没有权限学习则提示用户。
- 学习服务校验通过,请求搜索服务获取课程媒资信息。
- 搜索服务请求
ElasticSearch
获取课程媒资信息。
保存课程媒资数据
实体类
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修改
-
TeachplanMediaPubRepository
package com.xuecheng.manage_course.dao; import com.xuecheng.framework.domain.course.TeachplanMediaPub; import org.springframework.data.jpa.repository.JpaRepository; public interface TeachplanMediaPubRepository extends JpaRepository<TeachplanMediaPub, String> { //根据课程id删除课程计划媒资信息 long deleteByCourseId(String courseId); }
-
TeachplanMediaRepository新增方法
List<TeachplanMedia> findByCourseId(String courseId);
CourseService
新增保存课程媒资方法并在发布课程时调用
@Autowired
private TeachplanMediaPubRepository teachplanMediaPubRepository;
/**
* 保存指定课程的课程计划媒资信息到索引表中
*
* @param id 课程ID
*/
private void saveTeachplanMediaPub(String id) {
// 查询课程媒资信息
List<TeachplanMedia> teachplanMediaList = teachplanMediaRepository.findByCourseId(id);
// 删除原有数据
teachplanMediaPubRepository.deleteByCourseId(id);
// 将课程计划媒资信息存储待索引表
List<TeachplanMediaPub> teachplanMediaPubList = new ArrayList<>();
teachplanMediaList.forEach(teachplanMedia -> {
TeachplanMediaPub teachplanMediaPub = new TeachplanMediaPub();
BeanUtils.copyProperties(teachplanMedia, teachplanMediaPub);
teachplanMediaPubList.add(teachplanMediaPub);
});
teachplanMediaPubRepository.saveAll(teachplanMediaPubList);
}
Logstash导入数据到索引库
创建索引
PUT xc_course_media/doc/_mapping
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
}
}
创建映射
POST 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"
}
}
}
创建模板文件
创建xc_course_media_template.json
文件
{
"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"
}
}
Logstash数据导入脚本
与课程发布信息导入类似,只是执行的sql
脚本不同和导入的映射不同
input {
stdin {
}
jdbc {
jdbc_connection_string => "jdbc:mysql://192.168.136.110:3306/xc_course?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC"
# the user we wish to excute our statement as
jdbc_user => "root"
jdbc_password => "123456"
# the path to our downloaded jdbc driver
jdbc_driver_library => "/usr/share/logstash/config/mysql-connector-java-8.0.13.jar"
# the name of the driver class for mysql
jdbc_driver_class => "com.mysql.cj.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 => true
last_run_metadata_path => "/usr/share/logstash/config/logstash_metadata"
}
}
filter{
json{
source => "message"
remove_field => ["message"]
}
}
output {
elasticsearch {
#ES的ip地址和端口
hosts => "192.168.136.110:9200"
#hosts => ["localhost:9200","localhost:9202","localhost:9203"]
#ES索引库名称
index => "xc_course_media"
document_id => "%{courseid}"
document_type => "doc"
template => "/usr/share/logstash/config/xc_course_media_template.json"
template_name => "xc_course_media"
template_overwrite => "true"
}
stdout {
#日志输出
codec => json_lines
}
}
查看导入数据
课程媒资接口
在xc-service-search
中添加相关代码
ES实体类定义
package com.xuecheng.framework.domain.search;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "xc_course_media", type = "doc", shards = 1)
public class EsTeachplanMediaPub {
@Id
private String courseid;
@Field
private String media_fileoriginalname;
@Field
private String media_id;
@Field
private String media_url;
@Field
private String teachplan_id;
}
appliction.yml
新增配置
elasticsearch:
es_course_source_field: id,name,grade,mt,st,charge,valid,pic,qq,price,price_old,status,studymodel,teachmode,expires,pub_time,start_time,end_time
es_course_media_source_field: courseid,media_id,media_url,teachplan_id,media_fileoriginalname
配置类
package com.xuecheng.search.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticsearchConfig {
private String esCourseSourceField;
private String esCourseMediaSourceField;
}
API定义
@ApiOperation("根据课程计划查询媒资信息")
EsTeachplanMediaPub getMedia(String teachplanId);
EsCourseController
@Override
@GetMapping("getmedia/{teachplanId}")
public EsTeachplanMediaPub getMedia(@PathVariable String teachplanId) {
//将课程计划id放在数组中,为调用service作准备
String[] teachplanIds = new String[]{teachplanId};
//通过service查询ES获取课程媒资信息
List<EsTeachplanMediaPub> esTeachplanMediaPubList = esCourseService.getMedia(teachplanIds);
return esTeachplanMediaPubList.isEmpty() ? null : esTeachplanMediaPubList.get(0);
}
EsCourseService
/**
* 查询课程媒资信息
*
* @param teachplanIds 课程计划ID
*/
public List<EsTeachplanMediaPub> getMedia(String[] teachplanIds) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
// 结果过滤
nativeSearchQueryBuilder.withSourceFilter(
new FetchSourceFilter(elasticsearchConfig.getEsCourseMediaSourceField().split(","), null));
// 查询条件
nativeSearchQueryBuilder.withQuery(QueryBuilders.termQuery("teachplan_id", Arrays.stream(teachplanIds).reduce((a, b) -> a + "," +b).get()));
return elasticsearchTemplate.queryForList(nativeSearchQueryBuilder.build(), EsTeachplanMediaPub.class);
}
在线学习
微服务项目导入(省略)
OPEN API
开放搜索微服务的API
,修改搜索微服务相关代码
引入eureka依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml配置
新增eureka
配置
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/,http://localhost:50102/eureka/}
instance:
prefer-ip-address: true #将自己的ip地址注册到Eureka服务中
ip-address: ${IP_ADDRESS:127.0.0.1}
instance-id: ${spring.application.name}:${server.port} #指定实例id
ribbon:
MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试,如果eureka中找不到服务则直接走断路器
MaxAutoRetriesNextServer: 3 #切换实例的重试次数
OkToRetryOnAllOperations: false #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
ConnectTimeout: 5000 #请求连接的超时时间
ReadTimeout: 6000 #请求处理的超时时间
启动类
在启动类上添加@EnableDiscoveryClient
编写Api Client
在学习微服务中编写Feign Client
package com.xuecheng.learning.client;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.search.EsTeachplanMediaPub;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = XcServiceList.XC_SERVICE_SEARCH)
public interface CourseSearchClient {
@GetMapping(value = "search/course/getmedia/{teachplanId}")
EsTeachplanMediaPub getMedia(@PathVariable("teachplanId") String teachplanId);
}
在线学习接口后端
返回结果实体类
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;
}
API定义
package com.xuecheng.api.learning;
import com.xuecheng.framework.domain.learning.response.GetMediaResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@Api(value = "录播课程学习管理", description = "录播课程学习管理")
public interface CourseLearningControllerApi {
@ApiOperation("获取课程学习地址")
GetMediaResult getMedia(String courseId, String teachplanId);
}
CourseLearningController
package com.xuecheng.learning.controller;
import com.xuecheng.api.learning.CourseLearningControllerApi;
import com.xuecheng.framework.domain.learning.response.GetMediaResult;
import com.xuecheng.learning.service.LearningService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("learning")
public class CourseLearningController implements CourseLearningControllerApi {
@Autowired
LearningService learningService;
@Override
@GetMapping("getmedia/{courseId}/{teachplanId}")
public GetMediaResult getMedia(@PathVariable String courseId, @PathVariable String teachplanId) {
//获取课程学习地址
return learningService.getMedia(courseId, teachplanId);
}
}
CourseLearningService
package com.xuecheng.learning.service;
import com.xuecheng.framework.domain.learning.response.GetMediaResult;
import com.xuecheng.framework.domain.learning.response.LearningCode;
import com.xuecheng.framework.domain.search.EsTeachplanMediaPub;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.learning.client.CourseSearchClient;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class LearningService {
@Autowired
CourseSearchClient courseSearchClient;
/**
* 获取课程视频信息
*
* @param courseId 课程ID
* @param teachplanId 课程计划ID
* @return GetMediaResult
*/
public GetMediaResult getMedia(String courseId, String teachplanId) {
// TODO 校验学生的学习权限, 是否资费
// 调用搜索服务查询
EsTeachplanMediaPub teachplanMediaPub = courseSearchClient.getMedia(teachplanId);
if (teachplanMediaPub == null || StringUtils.isEmpty(teachplanMediaPub.getMedia_url())) {
//获取视频播放地址出错
ExceptionCast.cast(LearningCode.LEARNING_GETMEDIA_ERROR);
}
return new GetMediaResult(CommonCode.SUCCESS, teachplanMediaPub.getMedia_url());
}
}
在线学习接口前端
learning_video.vue
-
在
methods
中新增方法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.fileUrl //播放视频 this.playvideo(fileUrl) } else if(res.message) { this.$message.error(res.message) } else { this.$message.error("播放视频失败,请刷新页面重试") } }).catch(res => { this.$message.error("播放视频失败,请刷新页面重试") }); }
-
修改
created
中拿到课程ID后回调逻辑if(!this.chapter || this.chapter == '0'){ //取出第一个教学计划 this.chapter = this.getFirstTeachplan() console.log(this.chapter) } //开始学习 this.study(this.chapter)
测试
成功点播视频。