课程发布
需求分析
课程发布后将生成正式的课程详情页面,课程发布后用户即可浏览课程详情页面,并开始课程的学习。
课程发布生成课程详情页面的流程与课程预览业务流程相同,如下:
1、用户进入教学管理中心,进入某个课程的管理界面
2、点击课程发布,前端请求到课程管理服务
3、课程管理服务远程调用CMS生成课程发布页面,CMS将课程详情页面发布到服务器
4、课程管理服务修改课程发布状态为 “已发布”,并向前端返回发布成功
5、用户在教学管理中心点击“课程详情页面”链接,查看课程详情页面内容
CMS 一键发布接口
根据需求分析内容,需要在 cms服务增加页面发布接口供课程管理服务调用,此接口的功能如下:
1、接收课程管理服务发布的页面信息
2、将页面信息添加到 数据库(mongodb)
3、对页面信息进行静态化
4、将页面信息发布到服务器
接口定义
1、创建响应结果类型
页面发布成功cms返回页面的url
页面Url= cmsSite.siteDomain+cmsSite.siteWebPath+ cmsPage.pageWebPath + cmsPage.pageName
@Data @NoArgsConstructor//无参构造器注解 public class CmsPostPageResult extends ResponseResult { String pageUrl; public CmsPostPageResult(ResultCode resultCode,String pageUrl) { super(resultCode); this.pageUrl = pageUrl; } }
2、在api工程定义页面发布接口
@ApiOperation("一键发布页面") public CmsPostPageResult postPageQuick(CmsPage cmsPage);
Dao
1、站点dao
接口中需要获取站点的信息(站点域名、站点访问路径等
public interface CmsSiteRepository extends MongoRepository{ }
Service
1、添加页面,如果已存在则更新页面
//添加页面,如果已存在则更新页面 public CmsPageResult save(CmsPage cmsPage){ //校验页面是否存在,根据页面名称、站点Id、页面webpath查询 CmsPage cmsPage1 = cmsPageRepository.findByPageNameAndSiteIdAndPageWebPath(cmsPage.getPageName(), cmsPage.getSiteId(), cmsPage.getPageWebPath()); if(cmsPage1 !=null){ //更新 return this.update(cmsPage1.getPageId(),cmsPage); }else{ //添加 return this.add(cmsPage); } }
2、页面发布方法
//一键发布页面 public CmsPostPageResult postPageQuick(CmsPage cmsPage){ //添加页面 CmsPageResult save = this.save(cmsPage); if(!save.isSuccess()){ return new CmsPostPageResult(CommonCode.FAIL,null); } CmsPage cmsPage1 = save.getCmsPage(); //要布的页面id String pageId = cmsPage1.getPageId(); //发布页面 ResponseResult responseResult = this.postPage(pageId); if(!responseResult.isSuccess()){ return new CmsPostPageResult(CommonCode.FAIL,null); } //得到页面的url //页面url=站点域名+站点webpath+页面webpath+页面名称 //站点id String siteId = cmsPage1.getSiteId(); //查询站点信息 CmsSite cmsSite = findCmsSiteById(siteId); //站点域名 String siteDomain = cmsSite.getSiteDomain(); //站点web路径 String siteWebPath = cmsSite.getSiteWebPath(); //页面web路径 String pageWebPath = cmsPage1.getPageWebPath(); //页面名称 String pageName = cmsPage1.getPageName(); //页面的web访问地址 String pageUrl = siteDomain+siteWebPath+pageWebPath+pageName; return new CmsPostPageResult(CommonCode.SUCCESS,pageUrl); } //根据id查询站点信息 public CmsSite findCmsSiteById(String siteId){ Optionaloptional = cmsSiteRepository.findById(siteId); if(optional.isPresent()){ return optional.get(); } return null; }
Controller
@Override @PostMapping("/postPageQuick") public CmsPostPageResult postPageQuick(@RequestBody CmsPage cmsPage) { return pageService.postPageQuick(cmsPage); }
课程发布接口
Api接口
此Api接口由课程管理提供,由课程管理前端调用此Api接口,实现课程发布。
在api工程下课程管理包下定义接口:
@ApiOperation("发布课程") public CoursePublishResult publish(@PathVariable String id);
创建Feign Client
在课程管理工程创建CMS服务页面发布的Feign Client
@FeignClient(value = XcServiceList.XC_SERVICE_MANAGE_CMS) public interface CmsPageClient { //一键发布页面 @PostMapping("/cms/page/postPageQuick") public CmsPostPageResult postPageQuick(CmsPage cmsPage); }
Service
1、配置课程发布页面参数
在application.yml中配置
course‐publish: siteId: 5b30cba5f58b4411fc6cb1e5 templateId: 5ad9a24d68db5239b8fef199 previewUrl: http://www.xuecheng.com/cms/preview/ pageWebPath: /course/detail/ pagePhysicalPath: /course/detail/ dataUrlPre: http://localhost:31200/course/courseview/
siteId:站点id
templateId:模板id
dataurlPre:数据url的前缀
pageWebPath: 页面的web访问路径
pagePhysicalPath:页面的物理存储路径。
2、Service方法如下
@Value("${course‐publish.dataUrlPre}") private String publish_dataUrlPre; @Value("${course‐publish.pagePhysicalPath}") private String publish_page_physicalpath; @Value("${course‐publish.pageWebPath}") private String publish_page_webpath; @Value("${course‐publish.siteId}") private String publish_siteId; @Value("${course‐publish.templateId}") private String publish_templateId; @Value("${course‐publish.previewUrl}") private String previewUrl; @Autowired CmsPageClient cmsPageClient; //课程发布 @Transactional public CoursePublishResult publish(String courseId){ //课程信息 CourseBase one = this.findCourseBaseById(courseId); //发布课程详情页面 CmsPostPageResult cmsPostPageResult = publish_page(courseId); if(!cmsPostPageResult.isSuccess()){ ExceptionCast.cast(CommonCode.FAIL); } //更新课程状态 CourseBase courseBase = saveCoursePubState(courseId); //课程索引... //课程缓存... //页面url String pageUrl = cmsPostPageResult.getPageUrl(); return new CoursePublishResult(CommonCode.SUCCESS,pageUrl); } //更新课程发布状态 private CourseBase saveCoursePubState(String courseId){ CourseBase courseBase = this.findCourseBaseById(courseId); //更新发布状态 courseBase.setStatus("202002"); CourseBase save = courseBaseRepository.save(courseBase); return save; } //发布课程正式页面 public CmsPostPageResult publish_page(String courseId){ CourseBase one = this.findCourseBaseById(courseId); //发布课程预览页面 CmsPage cmsPage = new CmsPage(); //站点 cmsPage.setSiteId(publish_siteId);//课程预览站点 //模板 cmsPage.setTemplateId(publish_templateId); //页面名称 cmsPage.setPageName(courseId+".html"); //页面别名 cmsPage.setPageAliase(one.getName()); //页面访问路径 cmsPage.setPageWebPath(publish_page_webpath); //页面存储路径 cmsPage.setPagePhysicalPath(publish_page_physicalpath); //数据url cmsPage.setDataUrl(publish_dataUrlPre+courseId); //发布页面 CmsPostPageResult cmsPostPageResult = cmsPageClient.postPageQuick(cmsPage); return cmsPostPageResult; }
Controller
@Override @PostMapping("/publish/{id}") public CoursePublishResult publish(@PathVariable String id) { return courseService.publish(id); }
新增站点和模板
1、新增课程详情页面的站点信息
如果已增加课程详情页面的站点则忽略此步骤。
向cms_site中新增如下信息
{ "_id" : ObjectId("5b30b052f58b4411fc6cb1cf"), "_class" : "com.xuecheng.framework.domain.cms.CmsSite", "siteName" : "课程详情站点", "siteDomain" : "http://www.xuecheng.com", "sitePort" : "80", "siteWebPath" : "", "siteCreateTime" : ISODate("2018‐02‐03T02:34:19.113+0000") }
2、新增课程详情模板信息
可直接使用前边章节制作的课程详情信息模板。
可以GridFS的测试代码添加模板,如果已添加则不用重复添加。
使用测试GridFS Api将模板文件存储到mongodb:
//文件存储2 @Test public void testStore2() throws FileNotFoundException { File file = new File("C:\\Users\\admin\\Desktop\\coursedetail_t.html"); FileInputStream inputStream = new FileInputStream(file); //保存模版文件内容 GridFSFile gridFSFile = gridFsTemplate.store(inputStream, "测试文件",""); String fileId = gridFSFile.getId().toString(); System.out.println(fileId); }
单元测试
1、启动RabbitMQ服务
2、启动cms服务
3、启动cms_client,注意配置routingKey和队列名称
xuecheng:
mq:
#cms客户端监控的队列名称(不同的客户端监控的队列不能重复)
queue: queue_cms_postpage_03
routingKey: 5b30b052f58b4411fc6cb1cf #此routingKey为门户站点ID
全文检索 Elasticearch 研究
ElasticSearch 介绍
1、elasticsearch是一个基于Lucene的高扩展的分布式搜索服务器,支持开箱即用。
2、elasticsearch隐藏了Lucene的复杂性,对外提供Restful 接口来操作索引、搜索。
突出优点:
1. 扩展性好,可部署上百台服务器集群,处理PB级数据。
2.近实时的去索引数据、搜索数据。
es和solr选择哪个?
1.如果你公司现在用的solr可以满足需求就不要换了。
2.如果你公司准备进行全文检索项目的开发,建议优先考虑elasticsearch,因为像Github这样大规模的搜索都在用
它。
es在项目中的应用方式:
1)用户在前端搜索关键字
2)项目前端通过http方式请求项目服务端
3)项目服务端通过Http RESTful方式请求ES集群进行搜索
4)ES集群从索引库检索数据。
ES 快速入门
创建索引库
ES的索引库是一个逻辑概念,它包括了分词列表及文档列表,同一个索引库中存储了相同类型的文档。它就相当于
MySQL中的表,或相当于Mongodb中的集合。
关于索引这个语:
索引(名词):ES是基于Lucene构建的一个搜索服务,它要从索引库搜索符合条件索引数据。
索引(动词):索引库刚创建起来是空的,将数据添加到索引库的过程称为索引。
1)使用postman或curl这样的工具创建:
put http://localhost:9200/索引库名称
{ "settings":{ "index":{ "number_of_shards":1, "number_of_replicas":0 } } }
number_of_shards:设置分片的数量,在集群中通常设置多个分片,表示一个索引库将拆分成多片分别存储不同
的结点,提高了ES的处理能力和高可用性,入门程序使用单机环境,这里设置为1。
number_of_replicas:设置副本的数量,设置副本是为了提高ES的高可靠性,单机环境设置为0.
2)使用head插件创建
创建映射
在索引中每个文档都包括了一个或多个field,创建映射就是向索引库中创建field的过程,下边是document和field
与关系数据库的概念的类比:
文档(Document)----------------Row记录
字段(Field)-------------------Columns 列
我们要把课程信息存储到ES中,这里我们创建课程信息的映射,先来一个简单的映射,如下:
发送:post http://localhost:9200/索引库名称 /类型名称/_mapping
创建类型为xc_course的映射,共包括三个字段:name、description、studymondel
由于ES6.0版本还没有将type彻底删除,所以暂时把type起一个没有特殊意义的名字。
post 请求:http://localhost:9200/xc_course/doc/_mapping
表示:在 xc_course索引库下的doc类型下创建映射。doc是类型名,可以自定义,在ES6.0中要弱化类型的概念,
给它起一个没有具体业务意义的名称。
{ "properties": { "name": { "type": "text" }, "description": { "type": "text" }, "studymodel": { "type": "keyword" } } }
映射创建成功,查看head界面:
创建文档
ES中的文档相当于MySQL数据库表中的记录。
发送:put 或Post http://localhost:9200/xc_course/doc/id值
(如果不指定id值ES会自动生成ID)
http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
{ "name":"Bootstrap开发框架", "description":"Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包 含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的 精美界面效果。", "studymodel":"201001" }
使用postman测试:
搜索文档
1、根据课程id查询文档
发送:get http://localhost:9200/xc_course/doc/4028e58161bcf7f40161bcf8b77c0000
使用 postman测试:
IK 分词器
在添加文档时会进行分词,索引中存放的就是一个一个的词(term),当你去搜索时就是拿关键字去匹配词,最终
找到词关联的文档。
测试当前索引库使用的分词器:
post 发送:localhost:9200/_analyze
{"text":"测试分词器,后边是测试内容:spring cloud实战"}
结果如下:
会发现分词的效果将 “测试” 这个词拆分成两个单字“测”和“试”,这是因为当前索引库使用的分词器对中文就是单字
分词。
两种分词模式
ik分词器有两种分词模式:ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、
华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
2、ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
测试两种分词模式:
发送: post localhost:9200/_analyze
{"text":"中华人民共和国人民大会堂","analyzer":"ik_smart" }
索引管理
搭建工程
ES客户端
本教程准备采用 Java High Level REST Client,如果它有不支持的功能,则使用Java Low Level REST Client。
添加依赖:
org.elasticsearch.client elasticsearch‐rest‐high‐level‐client 6.2.1 org.elasticsearch elasticsearch 6.2.1
创建搜索工程
创建搜索工程(maven工程):xc-service-search,添加RestHighLevelClient依赖及junit依赖。
pom.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema‐instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven‐4.0.0.xsd"> xc‐framework‐parent com.xuecheng 1.0‐SNAPSHOT ../xc‐framework‐parent/pom.xml 4.0.0 xc‐service‐search com.xuecheng xc‐framework‐model 1.0‐SNAPSHOT com.xuecheng xc‐framework‐common 1.0‐SNAPSHOT com.xuecheng xc‐service‐api 1.0‐SNAPSHOT org.springframework.boot spring‐boot‐starter‐web org.springframework.boot spring‐boot‐starter‐web org.elasticsearch.client elasticsearch‐rest‐high‐level‐client 6.2.1 org.elasticsearch elasticsearch 6.2.1 org.springframework.boot spring‐boot‐starter‐test test com.alibaba fastjson org.apache.commons commons‐io org.apache.commons commons‐lang3
2、配置文件
application.yml
server: port: ${port:40100} spring: application: name: xc‐search‐service xuecheng: elasticsearch: hostlist: ${eshostlist:127.0.0.1:9200} #多个结点中间用逗号分隔
3、配置类
创建com.xuecheng.search.config包
在其下创建配置类
package com.xuecheng.search.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ElasticsearchConfig { @Value("${xuecheng.elasticsearch.hostlist}") private String hostlist; @Bean public RestHighLevelClient restHighLevelClient(){ //解析hostlist配置信息 String[] split = hostlist.split(","); //创建HttpHost数组,其中存放es主机和端口的配置信息 HttpHost[] httpHostArray = new HttpHost[split.length]; for(int i=0;i){ String item = split[i]; httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":") [1]), "http"); } //创建RestHighLevelClient客户端 return new RestHighLevelClient(RestClient.builder(httpHostArray)); } //项目主要使用RestHighLevelClient,对于低级的客户端暂时不用 @Bean public RestClient restClient(){ //解析hostlist配置信息 String[] split = hostlist.split(","); //创建HttpHost数组,其中存放es主机和端口的配置信息 HttpHost[] httpHostArray = new HttpHost[split.length]; for(int i=0;i ){ String item = split[i]; httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":") [1]), "http"); } return RestClient.builder(httpHostArray).build(); } }
4、启动类
@SpringBootApplication @EntityScan("com.xuecheng.framework.domain.search")//扫描实体类 @ComponentScan(basePackages={"com.xuecheng.api"})//扫描接口 @ComponentScan(basePackages={"com.xuecheng.search"})//扫描本项目下的所有类 @ComponentScan(basePackages={"com.xuecheng.framework"})//扫描common下的所有类 public class SearchApplication { public static void main(String[] args) throws Exception { SpringApplication.run(SearchApplication.class, args); } }
创建索引库
Java Client
@SpringBootTest @RunWith(SpringRunner.class) public class TestIndex { @Autowired RestHighLevelClient client; @Autowired RestClient restClient; //创建索引库 @Test public void testCreateIndex() throws IOException { //创建索引请求对象,并设置索引名称 CreateIndexRequest createIndexRequest = new CreateIndexRequest("xc_course"); //设置索引参数 createIndexRequest.settings(Settings.builder().put("number_of_shards",1) .put("number_of_replicas",0)); //设置映射 createIndexRequest.mapping("doc"," {\n" + " \t\"properties\": {\n" + " \"name\": {\n" + " \"type\": \"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"search_analyzer\":\"ik_smart\"\n" + " },\n" + " \"description\": {\n" + " \"type\": \"text\",\n" + " \"analyzer\":\"ik_max_word\",\n" + " \"search_analyzer\":\"ik_smart\"\n" + " },\n" + " \"studymodel\": {\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"price\": {\n" + " \"type\": \"float\"\n" + " }\n" + " }\n" + "}", XContentType.JSON); //创建索引操作客户端 IndicesClient indices = client.indices(); //创建响应对象 CreateIndexResponse createIndexResponse = indices.create(createIndexRequest); //得到响应结果 boolean acknowledged = createIndexResponse.isAcknowledged(); System.out.println(acknowledged); } //删除索引库 @Test public void testDeleteIndex() throws IOException { //删除索引请求对象 DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("xc_course"); //删除索引 DeleteIndexResponse deleteIndexResponse = client.indices().delete(deleteIndexRequest); //删除索引响应结果 boolean acknowledged = deleteIndexResponse.isAcknowledged(); System.out.println(acknowledged); } }
添加文档
//添加文档 @Test public void testAddDoc() throws IOException { //准备json数据 MapjsonMap = new HashMap<>(); jsonMap.put("name", "spring cloud实战"); jsonMap.put("description", "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。"); jsonMap.put("studymodel", "201001"); SimpleDateFormat dateFormat =new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss"); jsonMap.put("timestamp", dateFormat.format(new Date())); jsonMap.put("price", 5.6f); //索引请求对象 IndexRequest indexRequest = new IndexRequest("xc_course","doc"); //指定索引文档内容 indexRequest.source(jsonMap); //索引响应对象 IndexResponse indexResponse = client.index(indexRequest); //获取响应结果 DocWriteResponse.Result result = indexResponse.getResult(); System.out.println(result); }
查询文档
//查询文档 @Test public void getDoc() throws IOException { GetRequest getRequest = new GetRequest( "xc_course", "doc", "4028e581617f945f01617f9dabc40000"); GetResponse getResponse = client.get(getRequest); boolean exists = getResponse.isExists(); MapsourceAsMap = getResponse.getSourceAsMap(); System.out.println(sourceAsMap); }