媒资管理系统集成
学习页面查询课程计划
后端开发
修改xc-service-searcj
服务中的代码
API定义
1 2
| @ApiOperation("根据id查询课程信息") Map<String, EsCoursePub> getAll(String id);
|
EsCourseController
1 2 3 4 5
| @Override @GetMapping("getall/{id}") public Map<String, EsCoursePub> getAll(@PathVariable String id) { return esCourseService.getAll(id); }
|
EsCourseService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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()
区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| created(){ this.url = window.location this.courseId = this.$route.params.courseId this.chapter = this.$route.params.chapter 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配置文件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
| #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
获取课程媒资信息。
保存课程媒资数据
实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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修改
CourseService
新增保存课程媒资方法并在发布课程时调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Autowired private TeachplanMediaPubRepository teachplanMediaPubRepository;
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导入数据到索引库
创建索引
1 2 3 4 5 6 7
| PUT xc_course_media/doc/_mapping { "settings": { "number_of_shards": 1, "number_of_replicas": 0 } }
|
创建映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "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
脚本不同和导入的映射不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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实体类定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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
新增配置
1 2 3
| 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
|
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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定义
1 2
| @ApiOperation("根据课程计划查询媒资信息") EsTeachplanMediaPub getMedia(String teachplanId);
|
EsCourseController
1 2 3 4 5 6 7 8 9 10
| @Override @GetMapping("getmedia/{teachplanId}") public EsTeachplanMediaPub getMedia(@PathVariable String teachplanId) { String[] teachplanIds = new String[]{teachplanId}; List<EsTeachplanMediaPub> esTeachplanMediaPubList = esCourseService.getMedia(teachplanIds);
return esTeachplanMediaPubList.isEmpty() ? null : esTeachplanMediaPubList.get(0); }
|
EsCourseService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
|
application.yml配置
新增eureka
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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
|
启动类
在启动类上添加@EnableDiscoveryClient
编写Api Client
在学习微服务中编写Feign Client
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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);
}
|
在线学习接口后端
返回结果实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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定义
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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;
public GetMediaResult getMedia(String courseId, String teachplanId) {
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
中新增方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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后回调逻辑
1 2 3 4 5 6 7
| if(!this.chapter || this.chapter == '0'){ this.chapter = this.getFirstTeachplan() console.log(this.chapter) }
this.study(this.chapter)
|
测试
成功点播视频。
代码获取
代码获取
写在这里
学成在线去年年底就开始在做了,后面因为一些原因(因为懒……),停止在了这个阶段,后面还剩下:
现在捡起来继续做,先把之前写好的笔记发出来(再不发灰都要堆成山了)。
等把这个做完,准备开始搞搞更深层次的东西了,老实讲,应用层面的东西更多的是在业务,对实际工作中的架构和设计帮助甚少。
愿与你共勉!!!!!