目录

一、模块需求分析

二、课程预览

1.课程预览需求分析

2.使用Freemarker模板引擎渲染前端页面

3.定义接口

4.视频播放页面接口开发

三、课程审核

1.业务流程

2.接口定义

3.在课程发布CoursePublishService类中定义接口:

4.实现Service接口

四、课程发布

1.业务需求分析

2.分布式事务技术方案

3.接口定义

4.Service接口开发,在CoursePublishService类中添加课程发布接口

5.实现Service接口

6.消息处理SDK

7.页面静态化

五、课程搜索

1.业务流程分析

2.搭建ES环境

3.索引管理

4.搜索


一、模块需求分析

课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后学习者在网站可以搜索到课程,然后查看课程的详细信息,进一步选课、支付、在线学习。

教学机构确认课程内容无误,提交审核,平台运营人员对课程内容审核,审核通过后教学机构人员发布课程成功。

课程发布模块共包括三块功能:课程预览、课程审核、课程发布。

二、课程预览

1.课程预览需求分析

课程预览就是把课程的相关信息进行整合,在课程详情界面进行展示,通过课程预览页面查看信息是否存在问题。

说明如下:

1.1、点击课程预览,通过Nginx、后台服务网关请求内容管理服务进行课程预览。

1.2、内容管理服务查询课程相关信息进行整合,并通过模板引擎技术在服务端渲染生成页面,返回给浏览器。

1.3、通过课程预览页面点击”马上学习“打开视频播放页面。

1.4、视频播放页面通过Nginx请求后台服务网关,查询课程信息展示课程计划目录,请求媒资服务查询课程计划绑定的视频文件地址,在线浏览播放视频。

2.使用Freemarker模板引擎渲染前端页面

2.1、在nacos为内容管理接口层配置freemarker,公用配置组新加一个freemarker-config-dev.yaml

spring:freemarker:enabled: truecache: false   #关闭模板缓存,方便测试settings:template_update_delay: 0suffix: .ftl   #页面模板后缀名charset: UTF-8template-loader-path: classpath:/templates/   #页面模板位置(默认为 classpath:/templates/)resources:add-mappings: false   #关闭项目中的静态资源映射(static、resources文件夹下的资源)

2.2、在内容管理接口工程添加freemarker-config-dev.yaml

          - data-id: freemarker-config-dev.yamlgroup: xuecheng-plus-commonrefresh: true

2.3、添加模板 ,从课程资料目录下获取课程预览页面course_template.html,拷贝至内容管理的接口工程的resources/templates下,并将其在本目录复制一份命名为course_template.ftl

3.定义接口

3.1、接口层Controller调用Service方法获取模板引擎需要的模型数据

    @GetMapping("/coursepreview/{courseId}")public ModelAndView preview(@PathVariable("courseId") Long courseId) {ModelAndView modelAndView = new ModelAndView();//查询课程的信息作为模型数据CoursePreviewDto coursePreviewInfo = coursePublishService.getCoursePreviewInfo(courseId);//指定模型modelAndView.addObject("model", coursePreviewInfo);//指定模板modelAndView.setViewName("course_template");//根据视图名称加.ftl找到模板return modelAndView;}

3.2、定义数据模型

package com.xuecheng.content.model.dto;import lombok.Data;
import java.util.List;/*** @description 课程预览模型类*/
@Data
public class CoursePreviewDto {//课程基本信息,营销信息private CourseBaseInfoDto courseBase;//课程计划信息private List<TeachplanDto> teachplans;//课程师资信息...}

3.3、定义Service接口

在CoursePublishService接口中添加获取课程预览信息的方法

 /*** @description 获取课程预览信息* @param courseId 课程id* @return com.xuecheng.content.model.dto.CoursePreviewDto*/public CoursePreviewDto getCoursePreviewInfo(Long courseId);

3.4、实现Service方法

    @Overridepublic CoursePreviewDto getCoursePreviewInfo(Long courseId) {CoursePreviewDto coursePreviewDto = new CoursePreviewDto();//课程基本信息,营销信息CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);coursePreviewDto.setCourseBase(courseBaseInfo);//课程计划信息List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);coursePreviewDto.setTeachplans(teachplanTree);return coursePreviewDto;}

4.视频播放页面接口开发

在此页面需要从后台获取课程信息、根据课程计划获取对应的视频地址,这两个接口编写如下:

4.1、获取课程信息接口:/open/content/course/whole/{courseId}

package com.xuecheng.content.api;import com.alibaba.fastjson.JSON;
import com.xuecheng.content.model.dto.CourseBaseInfoDto;
import com.xuecheng.content.model.dto.CoursePreviewDto;
import com.xuecheng.content.model.dto.TeachplanDto;
import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.content.service.CoursePublishService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;import java.util.List;/*** @description 课程发布相关接口*/
@Controller
public class CoursePublishController {@AutowiredCoursePublishService coursePublishService;@ApiOperation("获取课程发布信息")@ResponseBody@GetMapping("/course/whole/{courseId}")public CoursePreviewDto getCoursePublish(@PathVariable("courseId") Long courseId) {//封装数据CoursePreviewDto coursePreviewDto = new CoursePreviewDto();//查询课程发布表
//        CoursePublish coursePublish = coursePublishService.getCoursePublish(courseId);//先从缓存查询,缓存中有直接返回,没有再查询数据库CoursePublish coursePublish = coursePublishService.getCoursePublishCache(courseId);if(coursePublish == null){return coursePreviewDto;}//开始向coursePreviewDto填充数据CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();BeanUtils.copyProperties(coursePublish,courseBaseInfoDto);//课程计划信息String teachplanJson = coursePublish.getTeachplan();//转成List<TeachplanDto>List<TeachplanDto> teachplanDtos = JSON.parseArray(teachplanJson, TeachplanDto.class);coursePreviewDto.setCourseBase(courseBaseInfoDto);coursePreviewDto.setTeachplans(teachplanDtos);return coursePreviewDto;}
}

4.2、根据课程计划获取视频地址接口:/open/media/preview/{mediaId},在媒资管理服务media-api工程定义MediaOpenController类

package com.xuecheng.media.api;import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
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;@Api(value = "媒资文件管理接口", tags = "媒资文件管理接口")
@RestController
@RequestMapping("/open")
public class MediaOpenController {@AutowiredMediaFileService mediaFileService;@ApiOperation("预览文件")@GetMapping("/preview/{mediaId}")public RestResponse<String> getPlayUrlByMediaId(@PathVariable String mediaId) {//查询媒资文件信息MediaFiles mediaFiles = mediaFileService.getFileById(mediaId);if (mediaFiles == null) {return RestResponse.validfail("找不到视频");}//取出视频播放地址String url = mediaFiles.getUrl();if (StringUtils.isEmpty(url)) {return RestResponse.validfail("该视频正在处理中");}return RestResponse.success(mediaFiles.getUrl());}
}

三、课程审核

1.业务流程

根据模块需求分析,课程发布前要先审核,审核通过方可发布。下图是课程审核及发布的流程图:

下边是课程状态的转化关系:

说明如下:

1.1、一门课程新增后它的审核状为”未提交“,发布状态为”未发布“。

1.2、课程信息编辑完成,教学机构人员执行”提交审核“操作。此时课程的审核状态为”已提交“。

1.3、当课程状态为已提交时运营平台人员对课程进行审核。

1.4、运营平台人员审核课程,结果有两个:审核通过、审核不通过。

1.5、课程审核过后不管状态是通过还是不通过,教学机构可以再次修改课程并提交审核,此时课程状态为”已提交“。此时运营平台人员再次审核课程。

1.6、课程审核通过,教学机构人员可以发布课程,发布成功后课程的发布状态为”已发布“。

1.7、课程发布后通过”下架“操作可以更改课程发布状态为”下架“

1.8、课程下架后通过”上架“操作可以再次发布课程,上架后课程发布状态为“发布”。

2.接口定义

定义提交课程审核的接口,在课程发布Controller中定义接口。

    @ResponseBody@PostMapping("/courseaudit/commit/{courseId}")public void commitAudit(@PathVariable("courseId") Long courseId) {Long companyId = 1232141425L;coursePublishService.commitAudit(companyId, courseId);}

3.在课程发布CoursePublishService类中定义接口:

 /*** @description 提交审核* @param courseId  课程id* @return void*/public void commitAudit(Long companyId,Long courseId);

4.实现Service接口

    @Transactional@Overridepublic void commitAudit(Long companyId, Long courseId) {CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);if (courseBaseInfo == null) {XueChengPlusException.cast("课程找不到");}//审核状态String auditStatus = courseBaseInfo.getAuditStatus();//如果课程的审核状态为已提交则不允许提交if (auditStatus.equals("202003")) {XueChengPlusException.cast("课程已提交请等待审核");}//本机构只能提交本机构的课程//todo:本机构只能提交本机构的课程//课程的图片、计划信息没有填写也不允许提交String pic = courseBaseInfo.getPic();if (StringUtils.isEmpty(pic)) {XueChengPlusException.cast("请求上传课程图片");}//查询课程计划//课程计划信息List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);if (teachplanTree == null || teachplanTree.size() == 0) {XueChengPlusException.cast("请编写课程计划");}//查询到课程基本信息、营销信息、计划等信息插入到课程预发布表CoursePublishPre coursePublishPre = new CoursePublishPre();BeanUtils.copyProperties(courseBaseInfo, coursePublishPre);//设置机构idcoursePublishPre.setCompanyId(companyId);//营销信息CourseMarket courseMarket = courseMarketMapper.selectById(courseId);//转jsonString courseMarketJson = JSON.toJSONString(courseMarket);coursePublishPre.setMarket(courseMarketJson);//计划信息//转jsonString teachplanTreeJson = JSON.toJSONString(teachplanTree);coursePublishPre.setTeachplan(teachplanTreeJson);//状态为已提交coursePublishPre.setStatus("202003");//提交时间coursePublishPre.setCreateDate(LocalDateTime.now());//查询预发布表,如果有记录则更新,没有则插入CoursePublishPre coursePublishPreObj = coursePublishPreMapper.selectById(courseId);if (coursePublishPreObj == null) {//插入coursePublishPreMapper.insert(coursePublishPre);} else {//更新coursePublishPreMapper.updateById(coursePublishPre);}//更新课程基本信息表的审核状态为已提交CourseBase courseBase = courseBaseMapper.selectById(courseId);courseBase.setAuditStatus("202003");//审核状态为已提交courseBaseMapper.updateById(courseBase);}

四、课程发布

1.业务需求分析

教学机构人员在课程审核通过后即可发布课程,课程发布后会公开展示在网站上供学生查看、选课和学习。

在网站上展示课程信息需要解决课程信息显示的性能问题,如果速度慢(排除网速)会影响用户的体验性。

为了提高网站的速度需要将课程信息进行缓存,并且要将课程信息加入索引库方便搜索,下图显示了课程发布后课程信息的流转情况:

1.1、向内容管理数据库的课程发布表存储课程发布信息,更新课程基本信息表中发布状态为已发布。

1.2、向Redis存储课程缓存信息。

1.3、向Elasticsearch存储课程索引信息。

1.4、请求分布文件系统存储课程静态化页面(即html页面),实现快速浏览课程详情页面。

1.5、redis中的课程缓存信息是将课程发布表中的数据转为json进行存储。

1.6、elasticsearch中的课程索引信息是根据搜索需要将课程名称、课程介绍等信息进行索引存储。

1.7、MinIO中存储了课程的静态化页面文件(html网页),查看课程详情是通过文件系统去浏览课程详情页面。

2.分布式事务技术方案

一次课程发布操作需要向数据库、redis、elasticsearch、MinIO写四份数据,这里存在分布式事务问题。

课程发布操作后,先更新数据库中的课程发布状态,更新后向redis、elasticsearch、MinIO写课程信息,只要在一定时间内最终向redis、elasticsearch、MinIO写数据成功即可。       

2.1、在内容管理服务的数据库中添加一个消息表,消息表和课程发布表在同一个数据库。

2.2、点击课程发布通过本地事务向课程发布表写入课程发布信息,同时向消息表写课程发布的消息。通过数据库进行控制,只要课程发布表插入成功消息表也插入成功,消息表的数据就记录了某门课程发布的任务。

2.3、启动任务调度系统定时调度内容管理服务去定时扫描消息表的记录。

2.4、当扫描到课程发布的消息时即开始完成向redis、elasticsearch、MinIO同步数据的操作。

2.5、同步数据的任务完成后删除消息表记录。

3.接口定义

根据课程发布的分布式事务控制方案,课程发布操作首先通过本地事务向课程发布表写入课程发布信息并向消息表插入一条消息,这里定义的课程发布接口要实现该功能。

在内容管理接口工程中定义课程发布接口。

    @ApiOperation("课程发布")@ResponseBody@PostMapping ("/coursepublish/{courseId}")public void coursepublish(@PathVariable("courseId") Long courseId){Long companyId = 1232141425L;coursePublishService.publish(companyId,courseId);}

4.Service接口开发,在CoursePublishService类中添加课程发布接口

 /*** @description 课程发布接口* @param companyId 机构id* @param courseId 课程id* @return void*/public void publish(Long companyId,Long courseId);

5.实现Service接口

    @Transactional@Overridepublic void publish(Long companyId, Long courseId) {//查询预发布表CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);if (coursePublishPre == null) {XueChengPlusException.cast("课程没有审核记录,无法发布");}//状态String status = coursePublishPre.getStatus();//课程如果没有审核通过不允许发布if (!status.equals("202004")) {XueChengPlusException.cast("课程没有审核通过不允许发布");}//本机构只允许提交本机构的课程if(!coursePublishPre.getCompanyId().equals(companyId)){XueChengPlusException.cast("不允许提交其它机构的课程。");}//向课程发布表写入数据CoursePublish coursePublish = new CoursePublish();BeanUtils.copyProperties(coursePublishPre, coursePublish);//先查询课程发布,如果有则更新,没有再添加CoursePublish coursePublishObj = coursePublishMapper.selectById(courseId);if (coursePublishObj == null) {coursePublishMapper.insert(coursePublish);} else {coursePublishMapper.updateById(coursePublish);}//向消息表写入数据saveCoursePublishMessage(courseId);//将预发布表数据删除coursePublishPreMapper.deleteById(courseId);}
    /*** @param courseId 课程id* @return void* @description 保存消息表记录*/private void saveCoursePublishMessage(Long courseId) {MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);if (mqMessage == null) {XueChengPlusException.cast(CommonError.UNKOWN_ERROR);}}

6.消息处理SDK

本项目将对消息表相关的处理做成一个SDK组件供各微服务使用

6.1、在内容管理数据库创建消息表和消息历史表

6.2、拷贝课程资料中的xuecheng-plus-message-sdk到工程目录

6.3、课程发布任务处理,在内容管理服务添加消息处理sdk的依赖即可使用它,实现sdk中的MessageProcessAbstract类,重写execte方法。

package com.xuecheng.content.service.jobhandler;import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.content.feignclient.CourseIndex;
import com.xuecheng.content.feignclient.SearchServiceClient;
import com.xuecheng.content.mapper.CoursePublishMapper;
import com.xuecheng.content.model.dto.CoursePreviewDto;
import com.xuecheng.content.model.po.CoursePublish;
import com.xuecheng.content.service.CoursePublishService;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MessageProcessAbstract;
import com.xuecheng.messagesdk.service.MqMessageService;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.io.File;/*** @description 课程发布的任务类*/
@Slf4j
@Component
public class CoursePublishTask extends MessageProcessAbstract {@AutowiredCoursePublishService coursePublishService;@AutowiredSearchServiceClient searchServiceClient;@AutowiredCoursePublishMapper coursePublishMapper;//任务调度入口@XxlJob("CoursePublishJobHandler")public void coursePublishJobHandler() throws Exception {// 分片参数int shardIndex = XxlJobHelper.getShardIndex();//执行器的序号,从0开始int shardTotal = XxlJobHelper.getShardTotal();//执行器总数//调用抽象类的方法执行任务process(shardIndex,shardTotal, "course_publish",30,60);}//执行课程发布任务的逻辑,如果此方法抛出异常说明任务执行失败@Overridepublic boolean execute(MqMessage mqMessage) {//从mqMessage拿到课程idLong courseId = Long.parseLong(mqMessage.getBusinessKey1());//课程静态化上传到miniogenerateCourseHtml(mqMessage,courseId);//向elasticsearch写索引数据saveCourseIndex(mqMessage,courseId);//向redis写缓存//返回true表示任务完成return true;}//生成课程静态化页面并上传至文件系统private void generateCourseHtml(MqMessage mqMessage,long courseId){//消息idLong taskId = mqMessage.getId();MqMessageService mqMessageService = this.getMqMessageService();//做任务幂等性处理//查询数据库取出该阶段执行状态int stageOne = mqMessageService.getStageOne(taskId);if(stageOne>0){log.debug("课程静态化任务完成,无需处理...");return ;}//开始进行课程静态化 生成html页面File file = coursePublishService.generateCourseHtml(courseId);if(file == null){XueChengPlusException.cast("生成的静态页面为空");}// 将html上传到miniocoursePublishService.uploadCourseHtml(courseId,file);//..任务处理完成写任务状态为完成mqMessageService.completedStageOne(taskId);}//保存课程索引信息 第二个阶段任务private void saveCourseIndex(MqMessage mqMessage,long courseId){//任务idLong taskId = mqMessage.getId();MqMessageService mqMessageService = this.getMqMessageService();//取出第二个阶段状态int stageTwo = mqMessageService.getStageTwo(taskId);//任务幂等性处理if(stageTwo>0){log.debug("课程索引信息已写入,无需执行...");return;}//查询课程信息,调用搜索服务添加索引接口//从课程发布表查询课程信息CoursePublish coursePublish = coursePublishMapper.selectById(courseId);CourseIndex courseIndex = new CourseIndex();BeanUtils.copyProperties(coursePublish,courseIndex);//远程调用Boolean add = searchServiceClient.add(courseIndex);if(!add){XueChengPlusException.cast("远程调用搜索服务添加课程索引失败");}//完成本阶段的任务mqMessageService.completedStageTwo(taskId);}
}

MessageProcessAbstract类代码如下:

package com.xuecheng.messagesdk.service;import com.xuecheng.messagesdk.model.po.MqMessage;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.concurrent.*;/*** @description 消息处理抽象类*/
@Slf4j
@Data
public abstract class MessageProcessAbstract {@AutowiredMqMessageService mqMessageService;/*** @param mqMessage 执行任务内容* @return boolean true:处理成功,false处理失败* @description 任务处理*/public abstract boolean execute(MqMessage mqMessage);/*** @description 扫描消息表多线程执行任务* @param shardIndex 分片序号* @param shardTotal 分片总数* @param messageType  消息类型* @param count  一次取出任务总数* @param timeout 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒* @return void*/public void process(int shardIndex, int shardTotal,  String messageType,int count,long timeout) {try {//扫描消息表获取任务清单List<MqMessage> messageList = mqMessageService.getMessageList(shardIndex, shardTotal,messageType, count);//任务个数int size = messageList.size();log.debug("取出待处理消息"+size+"条");if(size<=0){return ;}//创建线程池ExecutorService threadPool = Executors.newFixedThreadPool(size);//计数器CountDownLatch countDownLatch = new CountDownLatch(size);messageList.forEach(message -> {threadPool.execute(() -> {log.debug("开始任务:{}",message);//处理任务try {boolean result = execute(message);if(result){log.debug("任务执行成功:{})",message);//更新任务状态,删除消息表记录,添加到历史表int completed = mqMessageService.completed(message.getId());if (completed>0){log.debug("任务执行成功:{}",message);}else{log.debug("任务执行失败:{}",message);}}} catch (Exception e) {e.printStackTrace();log.debug("任务出现异常:{},任务:{}",e.getMessage(),message);}finally {//计数countDownLatch.countDown();}log.debug("结束任务:{}",message);});});//等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务countDownLatch.await(timeout,TimeUnit.SECONDS);System.out.println("结束....");} catch (InterruptedException e) {e.printStackTrace();}}}

6.4、开启任务调度

6.4.1、在nacos中在content-service-dev.yaml中配置

  job:admin: addresses: http://192.168.101.65:8088/xxl-job-adminexecutor:appname: coursepublish-jobaddress: ip: port: 8999logpath: /data/applogs/xxl-job/jobhandlerlogretentiondays: 30accessToken: default_token

6.4.2、从媒资管理服务层工程中拷贝一个XxlJobConfig配置类到内容管理service工程中。

6.4.3、在xxl-job-admin控制台中添加执行器

6.4.4、在xxl-job添加任务

7.页面静态化

根据课程发布的操作流程,执行课程发布后要将课程详情信息页面静态化,生成html页面上传至文件系统。下边使用freemarker技术对页面静态化生成html页面。课程静态化包括两部分工作:生成课程静态化页面,上传静态页面到文件系统。

7.1、Service层的接口定义

在CoursePublishService类中添加课程静态化接口,

 /*** @description 课程静态化* @param courseId  课程id* @return File 静态化文件*/public File generateCourseHtml(Long courseId);

在CoursePublishService类中添加课程静态化接口,

 /*** @description 上传课程静态化页面* @param file  静态化文件* @return void*/public void  uploadCourseHtml(Long courseId, File file);

7.2、实现Service层接口

    @Overridepublic File generateCourseHtml(Long courseId) {Configuration configuration = new Configuration(Configuration.getVersion());//最终的静态文件File htmlFile = null;try {//拿到classpath路径
//            String classpath = this.getClass().getResource("/").getPath();
//            //指定模板的目录
//            configuration.setDirectoryForTemplateLoading(new File(classpath+"/templates/"));//更改为如下方式configuration.setTemplateLoader(new ClassTemplateLoader(this.getClass().getClassLoader(), "/templates"));//指定编码configuration.setDefaultEncoding("utf-8");//得到模板Template template = configuration.getTemplate("course_template.ftl");//准备数据CoursePreviewDto coursePreviewInfo = this.getCoursePreviewInfo(courseId);HashMap<String, Object> map = new HashMap<>();map.put("model", coursePreviewInfo);//Template template 模板, Object model 数据String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);//输入流InputStream inputStream = IOUtils.toInputStream(html, "utf-8");htmlFile = File.createTempFile("coursepublish", ".html");//输出文件FileOutputStream outputStream = new FileOutputStream(htmlFile);//使用流将html写入文件IOUtils.copy(inputStream, outputStream);} catch (Exception ex) {log.error("页面静态化出现问题,课程id:{}", courseId, ex);ex.printStackTrace();}return htmlFile;}@Overridepublic void uploadCourseHtml(Long courseId, File file) {try {//将file转成MultipartFileMultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);//远程调用得到返回值String upload = mediaServiceClient.upload(multipartFile, "course/" + courseId + ".html");if (upload == null) {log.debug("远程调用走降级逻辑得到上传的结果为null,课程id:{}", courseId);XueChengPlusException.cast("上传静态文件过程中存在异常");}} catch (Exception ex) {ex.printStackTrace();XueChengPlusException.cast("上传静态文件过程中存在异常");}}

注意:在内容管理api工程的启动类上配置FeignClient

@EnableFeignClients(basePackages={"com.xuecheng.content.feignclient"})

五、课程搜索

1.业务流程分析

搜索功能是一个系统的重要功能,是信息查询的方式。课程搜索模块包括课程索引、课程搜索两部分。

1.1、在课程发布操作执行后通过消息处理方式创建课程索引,本项目使用elasticsearch作为索引及搜索服务。如下图:

1.2、课程索引创建完成,用户才可以通过前端搜索课程信息。

2.搭建ES环境

2.1、虚拟中已经在docker容器中安装了elasticsearch和kibana。

2.2、修改虚拟机中的启动脚本restart.sh添加

docker stop elasticsearch
docker stop kibanadocker start elasticsearch
docker start kibana

2.3、通过浏览器访问 http://192.168.101.65:5601/app/dev_tools#/console进入kibana的开发工具界面。

可通过命令:GET /_cat/indices?v  查看所有的索引,通过此命令判断kibana是否正常连接elasticsearch

索引相当于MySQL中的表,Elasticsearch与MySQL之间概念的对应关系见下表:

2.4、创建索引,并指定Mapping

PUT /course-publish

{"settings": {"number_of_shards": 1,"number_of_replicas": 0},"mappings": {"properties": {"id": {"type": "keyword"},"companyId": {"type": "keyword"},"companyName": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"name": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"users": {"index": false,"type": "text"},"tags": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"mt": {"type": "keyword"},"mtName": {"type": "keyword"},"st": {"type": "keyword"},"stName": {"type": "keyword"},"grade": {"type": "keyword"},"teachmode": {"type": "keyword"},"pic": {"index": false,"type": "text"},"description": {"analyzer": "ik_max_word","search_analyzer": "ik_smart","type": "text"},"createDate": {"format": "yyyy-MM-dd HH:mm:ss","type": "date"},"status": {"type": "keyword"},"remark": {"index": false,"type": "text"},"charge": {"type": "keyword"},"price": {"type": "scaled_float","scaling_factor": 100},"originalPrice": {"type": "scaled_float","scaling_factor": 100},"validDays": {"type": "integer"}}}
}

2.5、拷贝课程资料中的xuecheng-plus-search搜索工程到自己的工程目录。

3.索引管理

索引创建好就可以向其中添加文档,此时elasticsearch会根据索引的mapping配置对有些字段进行分词,当课程发布时请求添加课程接口添加课程信息到索引,当课程下架时请求删除课程接口从索引中删除课程信息,这里先实现添加课程接口。。

3.1、接口定义

package com.xuecheng.search.controller;import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @description 课程索引接口*/
@Api(value = "课程信息索引接口", tags = "课程信息索引接口")
@RestController
@RequestMapping("/index")
public class CourseIndexController {@Value("${elasticsearch.course.index}")private String courseIndexStore;@AutowiredIndexService indexService;@ApiOperation("添加课程索引")@PostMapping("course")public Boolean add(@RequestBody CourseIndex courseIndex) {Long id = courseIndex.getId();if(id==null){XueChengPlusException.cast("课程id为空");}Boolean result = indexService.addCourseIndex(courseIndexStore, String.valueOf(id), courseIndex);if(!result){XueChengPlusException.cast("添加课程索引失败");}return result;}
}

3.2、根据索引的mapping结构创建po类:

package com.xuecheng.search.po;import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** <p>* 课程索引信息* </p>** @author itcast*/
@Data
public class CourseIndex implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/private Long id;/*** 机构ID*/private Long companyId;/*** 公司名称*/private String companyName;/*** 课程名称*/private String name;/*** 适用人群*/private String users;/*** 标签*/private String tags;/*** 大分类*/private String mt;/*** 大分类名称*/private String mtName;/*** 小分类*/private String st;/*** 小分类名称*/private String stName;/*** 课程等级*/private String grade;/*** 教育模式*/private String teachmode;/*** 课程图片*/private String pic;/*** 课程介绍*/private String description;/*** 发布时间*/@JSONField(format="yyyy-MM-dd HH:mm:ss")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createDate;/*** 状态*/private String status;/*** 备注*/private String remark;/*** 收费规则,对应数据字典--203*/private String charge;/*** 现价*/private Float price;/*** 原价*/private Float originalPrice;/*** 课程有效期天数*/private Integer validDays;}

3.3、定义Service接口,请求elasticsearch添加课程信息。为了适应其它文档信息,将添加文档定义为通用的添加文档接口,此接口不仅适应添加课程还适应添加其它信息。

package com.xuecheng.search.service;import com.xuecheng.search.po.CourseIndex;/*** @description 课程索引service*/
public interface IndexService {/*** @param indexName 索引名称* @param id 主键* @param object 索引对象* @return Boolean true表示成功,false失败* @description 添加索引*/public Boolean addCourseIndex(String indexName,String id,Object object);
}

3.4、Service接口实现

package com.xuecheng.search.service.impl;import com.alibaba.fastjson.JSON;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.IndexService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;/*** @description 课程索引管理接口实现*/
@Slf4j
@Service
public class IndexServiceImpl implements IndexService {@AutowiredRestHighLevelClient client;@Overridepublic Boolean addCourseIndex(String indexName, String id, Object object) {String jsonString = JSON.toJSONString(object);IndexRequest indexRequest = new IndexRequest(indexName).id(id);//指定索引文档内容indexRequest.source(jsonString, XContentType.JSON);//索引响应对象IndexResponse indexResponse = null;try {indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);} catch (IOException e) {log.error("添加索引出错:{}", e.getMessage());e.printStackTrace();XueChengPlusException.cast("添加索引出错");}String name = indexResponse.getResult().name();System.out.println(name);return name.equalsIgnoreCase("created") || name.equalsIgnoreCase("updated");}
}

4.搜索

4.1、根据搜索界面可知需求如下:

4.1.1、根据一级分类、二级分类搜索课程信息。

4.1.2、根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容。

4.1.3、根据难度等级搜索课程。

4.1.4、搜索结点分页显示。

4.2、技术点:

4.2.1、整体采用布尔查询。

4.2.2、根据关键字搜索,采用MultiMatchQuery,搜索name、description字段。

4.2.3、根据分类、课程等级搜索采用过滤器实现。

4.2.4、分页查询。

4.2.5、高亮显示。

4.3、接口定义

package com.xuecheng.search.controller;import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;/*** @description 课程搜索接口*/
@Api(value = "课程搜索接口", tags = "课程搜索接口")
@RestController
@RequestMapping("/course")
public class CourseSearchController {@AutowiredCourseSearchService courseSearchService;@ApiOperation("课程搜索列表")@GetMapping("/list")public SearchPageResultDto<CourseIndex> list(PageParams pageParams, SearchCourseParamDto searchCourseParamDto) {return courseSearchService.queryCoursePubIndex(pageParams, searchCourseParamDto);}
}

4.4、定义搜索条件DTO类

package com.xuecheng.search.dto;import lombok.Data;
import lombok.ToString;/*** @description 搜索课程参数dtl*/@Data@ToString
public class SearchCourseParamDto {//关键字private String keywords;//大分类private String mt;//小分类private String st;//难度等级private String grade;}

4.5、定义搜索结果类,为了适应后期的扩展,让它继承PageResult

package com.xuecheng.search.dto;import com.xuecheng.base.model.PageResult;
import lombok.Data;
import lombok.ToString;import java.util.List;@Data
@ToString
public class SearchPageResultDto<T> extends PageResult {//大分类列表List<String> mtList;//小分类列表List<String> stList;public SearchPageResultDto(List<T> items, long counts, long page, long pageSize) {super(items, counts, page, pageSize);}}

4.6、Service接口定义

package com.xuecheng.search.service;import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;/*** @description 课程搜索service*/
public interface CourseSearchService {/*** @description 搜索课程列表* @param pageParams 分页参数* @param searchCourseParamDto 搜索条件* @return com.xuecheng.base.model.PageResult<com.xuecheng.search.po.CourseIndex> 课程列表*/SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto searchCourseParamDto);}

4.7、实现Service接口

package com.xuecheng.search.service.impl;import com.alibaba.fastjson.JSON;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.search.dto.SearchCourseParamDto;
import com.xuecheng.search.dto.SearchPageResultDto;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.CourseSearchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;/*** @author Mr.M* @version 1.0* @description 课程搜索service实现类* @date 2022/9/24 22:48*/
@Slf4j
@Service
public class CourseSearchServiceImpl implements CourseSearchService {@Value("${elasticsearch.course.index}")private String courseIndexStore;@Value("${elasticsearch.course.source_fields}")private String sourceFields;@AutowiredRestHighLevelClient client;@Overridepublic SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto courseSearchParam) {//设置索引SearchRequest searchRequest = new SearchRequest(courseIndexStore);SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//source源字段过虑String[] sourceFieldsArray = sourceFields.split(",");searchSourceBuilder.fetchSource(sourceFieldsArray, new String[]{});if(courseSearchParam==null){courseSearchParam = new SearchCourseParamDto();}//关键字if(StringUtils.isNotEmpty(courseSearchParam.getKeywords())){//匹配关键字MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(courseSearchParam.getKeywords(), "name", "description");//设置匹配占比multiMatchQueryBuilder.minimumShouldMatch("70%");//提升另个字段的Boost值multiMatchQueryBuilder.field("name",10);boolQueryBuilder.must(multiMatchQueryBuilder);}//过虑if(StringUtils.isNotEmpty(courseSearchParam.getMt())){boolQueryBuilder.filter(QueryBuilders.termQuery("mtName",courseSearchParam.getMt()));}if(StringUtils.isNotEmpty(courseSearchParam.getSt())){boolQueryBuilder.filter(QueryBuilders.termQuery("stName",courseSearchParam.getSt()));}if(StringUtils.isNotEmpty(courseSearchParam.getGrade())){boolQueryBuilder.filter(QueryBuilders.termQuery("grade",courseSearchParam.getGrade()));}//分页Long pageNo = pageParams.getPageNo();Long pageSize = pageParams.getPageSize();int start = (int) ((pageNo-1)*pageSize);searchSourceBuilder.from(start);searchSourceBuilder.size(Math.toIntExact(pageSize));//布尔查询searchSourceBuilder.query(boolQueryBuilder);//高亮设置HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.preTags("<font class='eslight'>");highlightBuilder.postTags("</font>");//设置高亮字段highlightBuilder.fields().add(new HighlightBuilder.Field("name"));searchSourceBuilder.highlighter(highlightBuilder);//请求搜索searchRequest.source(searchSourceBuilder);//聚合设置buildAggregation(searchRequest);SearchResponse searchResponse = null;try {searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);} catch (IOException e) {e.printStackTrace();log.error("课程搜索异常:{}",e.getMessage());return new SearchPageResultDto<CourseIndex>(new ArrayList(),0,0,0);}//结果集处理SearchHits hits = searchResponse.getHits();SearchHit[] searchHits = hits.getHits();//记录总数TotalHits totalHits = hits.getTotalHits();//数据列表List<CourseIndex> list = new ArrayList<>();for (SearchHit hit : searchHits) {String sourceAsString = hit.getSourceAsString();CourseIndex courseIndex = JSON.parseObject(sourceAsString, CourseIndex.class);//取出sourceMap<String, Object> sourceAsMap = hit.getSourceAsMap();//课程idLong id = courseIndex.getId();//取出名称String name = courseIndex.getName();//取出高亮字段内容Map<String, HighlightField> highlightFields = hit.getHighlightFields();if(highlightFields!=null){HighlightField nameField = highlightFields.get("name");if(nameField!=null){Text[] fragments = nameField.getFragments();StringBuffer stringBuffer = new StringBuffer();for (Text str : fragments) {stringBuffer.append(str.string());}name = stringBuffer.toString();}}courseIndex.setId(id);courseIndex.setName(name);list.add(courseIndex);}SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);//获取聚合结果List<String> mtList= getAggregation(searchResponse.getAggregations(), "mtAgg");List<String> stList = getAggregation(searchResponse.getAggregations(), "stAgg");pageResult.setMtList(mtList);pageResult.setStList(stList);return pageResult;}private void buildAggregation(SearchRequest request) {request.source().aggregation(AggregationBuilders.terms("mtAgg").field("mtName").size(100));request.source().aggregation(AggregationBuilders.terms("stAgg").field("stName").size(100));}private List<String> getAggregation(Aggregations aggregations, String aggName) {// 4.1.根据聚合名称获取聚合结果Terms brandTerms = aggregations.get(aggName);// 4.2.获取bucketsList<? extends Terms.Bucket> buckets = brandTerms.getBuckets();// 4.3.遍历List<String> brandList = new ArrayList<>();for (Terms.Bucket bucket : buckets) {// 4.4.获取keyString key = bucket.getKeyAsString();brandList.add(key);}return brandList;}
}

黑马学成在线-课程分布相关推荐

  1. 微服务实战项目-学成在线-课程发布模块

    学成在线-课程发布模块 1 模块需求分析 1.1 模块介绍 课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后学习者在网站可以搜索到课程,然后查看课程的详细信息,进一步选课.支付.在 ...

  2. 学成在线--课程发布模块

    完整版请移步至我的个人博客查看:https://cyborg2077.github.io/ 学成在线–项目环境搭建 学成在线–内容管理模块 学成在线–媒资管理模块 学成在线–课程发布模块 学成在线–认 ...

  3. 学成在线 课程 页面

    学成在线 课程 页面 先把剩下两个页面中比较复杂的,也许要超链接的页面做掉了. 大部分布局上的功能都实现了,就剩下一些细节上的:例如说没有补上的注释,以及字体大小颜色上的细节方面.div都分了,cla ...

  4. 学成在线 课程列表 页面

    学成在线 课程列表 页面 在美好的礼拜天,正好我们不调休,把最后一个页面给完成了. 和其他用浮动座位布局方式比起来,课程类表页面最终还是使用了flexbox去布局,一来主要内容最下面的 分页 部分用浮 ...

  5. 黑马学成在线--项目环境搭建

    完整版请移步至我的个人博客查看:https://cyborg2077.github.io/ 学成在线–项目环境搭建 学成在线–内容管理模块 学成在线–媒资管理模块 学成在线–课程发布模块 学成在线–认 ...

  6. 黑马学成在线-项目搭建

    一.开发环境搭建 1.开发工具版本 开发工具 版本号 IntelliJ-IDEA 2021.x以上版本 Java JDK-1.8.x Maven 3.6.x以上版本 Mysql 8.x VMware- ...

  7. 黑马学成在线-内容管理

    目录 一.内容管理的业务流程 1.教学机构人员的业务流程如下: 2.运营人员的业务流程如下: 二.内容管理模块的工程结构 1.业务流程 三.课程查询 1.业务流程 2.分页查询开发,把分页查询配置到b ...

  8. 黑马学成在线-媒资管理

    目录 一.媒资需求分析 二.搭建Nacos 三.分布式文件系统介绍 四.上传图片 1.需求分析 ,上传课程图片总体上包括两部分: 2.环境准备 3.接口定义 4.数据模型开发 5.业务层开发 五.上传 ...

  9. 学成在线-课程详情页面优化

    文章目录 前言 一.模板修改 二.测试 前言 课程详情页面目录的跳转播放 本人技术有限写的不好的地方敬请原谅,如有什么问题或者更好的解决方案大家可以留言或者私信 一.模板修改 课程详情页面是采用fre ...

最新文章

  1. 探索JAVA并发 - 并发容器全家福!
  2. Volley框架学习
  3. 《廖雪峰 . Git 教程》学习总结
  4. slidingmenu阻碍沉浸式实现的原理讲解,demo下载地址在github
  5. SRS性能、内存优化工具用法
  6. leetcode468. 验证IP地址
  7. 前端开发有哪些技术栈要掌握_为什么要掌握前端开发的这四个主要概念
  8. 二级c语言基础题库100题,二级C语言上题库100题.doc
  9. flash绘制荷花多个图层_Flash鼠绘入门第八课:绘制脱俗荷花
  10. opencv之划痕缺陷检测
  11. Ehcache整合spring配置
  12. 【渝粤教育】国家开放大学2018年春季 0603-21T建筑工程管理与实务 参考试题
  13. CSDN每日打卡已经2周,进展如何?,【2021Python最新学习路线】
  14. Microsemi Libero免费版License申请教程(2022年)
  15. NISP管理中心|NISP二级证书介绍
  16. 车用计算机电路板,使用车充、LED头灯电路板制作1.5V电源模块(可代替1号电池)...
  17. 美国又搞事,芯片工程师们怒了
  18. 奢侈品电商,压死趣店的最后一根稻草?
  19. 台式计算机usb口不识别鼠标,如何解决计算机无法识别鼠标的问题
  20. 有什么便宜好用的电容笔推荐?超实惠电容笔测评

热门文章

  1. OpenStack 网络项目(Neutron)的历史、现状与未来
  2. 倍福--控制电缸的配置
  3. 035-JAVA语言实现下拉菜单与弹出菜单功能
  4. 【Linux】进程控制 —— 进程创建/终止/等待
  5. Qt下Unix时间10进制格式和实际时间的相互转换
  6. java内部枚举类_内部类和枚举类
  7. 使用hive+hbase做数据分析
  8. 考了这个PMP证书到底有什么好处?
  9. vue中引入高德js
  10. 免费好用的思维导图工具,让你事半功倍