【Java实战篇】Day7.在线教育网课平台

news2024/12/23 9:52:48

文章目录

  • 一、需求:课程审核
    • 1、需求分析
    • 2、建表与数据模型
    • 3、接口定义
    • 4、Mapper层开发
    • 5、Service层开发
    • 6、完善controller层
  • 二、需求:课程发布
    • 1、需求分析
    • 2、建表与数据模型
    • 3、`技术方案`
    • 4、接口定义
    • 5、消息处理SDK
    • 6、Mapper层开发
    • 7、Service层开发
    • 8、页面静态化
    • 9、`微服务远程调用`
    • 10、熔断降级
    • 11、发布任务代码完善
  • 三、需求:课程搜索
    • 1、需求分析
    • 2、全文检索
    • 3、定义模型类
    • 4、定义接口
    • 5、开发Mapper层
    • 6、开发Service层

一、需求:课程审核

1、需求分析

课程发布前要先审核,审核通过方可发布。
在这里插入图片描述
在课程基本表course_base表加审核状态字段。审核通过后,教学机构可发布(发布状态)。

在这里插入图片描述

2、建表与数据模型

思考:课程提交审核后教育方能修改课程吗?

  • 若不允许,万一提交完后发现少点啥,想改就得等审核完。如果审核周期较长,说不定等审核完,用户想修改啥自己都忘了,用户体验很差
  • 若允许改,如果审核方查数据和教学方改数据的表是同一份,会出现:审核员点击审核,获取到了视频A,觉得不错,准备审核通过,此时教学方修改了视频,而审核员获取到的还是旧数据,就有Bug

在这里插入图片描述
为解决上面的问题,设计课程预发布表来分开两方数据:

  • 教育方点击提交审核,汇总四张基本表中(当前这个时间点的)课程信息写入课程预发布表
  • 审核方从预发布表拿数据进行审核
  • 审核途中,教育方可以改数据,但改的是四张基本表,不写入预发布表
  • 审核通过,将预发布表的信息写入课程发布表

在这里插入图片描述

即修改后点击保存,是更改了基本信息表,只有点提交审核,才update预发布表。从而实现:

  • 提交后可修改
  • 修改后必须点击提交审核才能提交
  • 提交审核后必须等审核完才能再次将新的修改再次提交审核

课程预发布表:

在这里插入图片描述
注意这里,字段太多的信息直接用json格式,数据类型text

在这里插入图片描述

审核记录表:

在这里插入图片描述

审核后:

  • 更新课程基本信息表的课程审核状态为:已经提交
  • 课程审核后更新课程基本信息表的审核状态、课程预发布表的审核状态,并将审核结果写入课程审核记录

3、接口定义

//提交审核接口

@ResponseBody
@PostMapping ("/courseaudit/commit/{courseId}")
public void commitAudit(@PathVariable("courseId") Long courseId){

}

4、Mapper层开发

直接生成PO类、写Mapper接口继承BaseMapper<PO>

5、Service层开发

分析,在这里要实现的逻辑是:

  • 查询课程基本信息、课程营销信息、课程计划信息
  • 插入到,课程预发布表course_publish_pre。若已存在,则更新
  • 更新课程基本表course_base课程审核状态为:已提交

要做的业务校验(约束)有:

  • 对已提交审核的课程不允许提交审核(业务逻辑校验)
  • 本机构只允许提交本机构的课程(身份校验)
  • 没有上传图片或没有添加课程计划,不允许提交审核
Java
/**
 * @description 提交审核
 * @param courseId  课程id
*/
public void commitAudit(Long companyId,Long courseId);

写实现类:

@Override
@Transactional
public void commitAudit(Long companyId, Long courseId) {

	 CourseBase courseBase = courseBaseMapper.selectById(courseId);
	 //课程审核状态
	 String auditStatus = courseBase.getAuditStatus();
	 //当前审核状态为已提交不允许再次提交
	 if("202003".equals(auditStatus)){
	  MyException.cast("当前为等待审核状态,审核完成可以再次提交(只能保存修改)。");
	 }
	 //本机构只允许提交本机构的课程
	 if(!courseBase.getCompanyId().equals(companyId)){
	  MyException.cast("不允许提交其它机构的课程。");
	 }
	
	 //课程图片是否填写
	 if(StringUtils.isEmpty(courseBase.getPic())){
	  MyException.cast("提交失败,请上传课程图片");
	 }
	
	 //添加课程预发布记录
	 CoursePublishPre coursePublishPre = new CoursePublishPre();
	 //课程基本信息加部分营销信息
	 CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
	 BeanUtils.copyProperties(courseBaseInfo,coursePublishPre);
	 //课程营销信息
	 CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
	 //转为json
	 String courseMarketJson = JSON.toJSONString(courseMarket);
	 //将课程营销信息json数据放入课程预发布表
	 coursePublishPre.setMarket(courseMarketJson);
	
	 //查询课程计划信息
	 List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
	 if(teachplanTree.size()<=0){
	  XueChengPlusException.cast("提交失败,还没有添加课程计划");
	 }
	 //转json
	 String teachplanTreeString = JSON.toJSONString(teachplanTree);
	 coursePublishPre.setTeachplan(teachplanTreeString);
	
	 //设置预发布记录状态,已提交
	 coursePublishPre.setStatus("202003");
	 //教学机构id
	 coursePublishPre.setCompanyId(companyId);
	 //提交时间
	 coursePublishPre.setCreateDate(LocalDateTime.now());
	 CoursePublishPre coursePublishPreUpdate = coursePublishPreMapper.selectById(courseId);
	 if(coursePublishPreUpdate == null){
	  //添加课程预发布记录,不存在是插入,存在时更新
	  coursePublishPreMapper.insert(coursePublishPre);
	 }else{
	  coursePublishPreMapper.updateById(coursePublishPre);
	 }
	
	 //更新课程基本表的审核状态
	 courseBase.setAuditStatus("202003");
	 //同步更新状态
	 courseBaseMapper.updateById(courseBase);
}

注意这里引用类型对象转json:String objectJson = JSON.toJSONString(xxObject);

6、完善controller层

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

 }

以上为提交审核的接口。运营方审核,即:

  • 更改预发布表中的审核状态为审核通过202004
  • 更改课程基本表的审核状态为审核通过202004
  • 审核接口不再贴代码

二、需求:课程发布

1、需求分析

审核员审核通过后,教学方可以选择发布。
在这里插入图片描述

2、建表与数据模型

课程发布后,课程信息的展示有两个问题(海量用户的查看和教学方的预览不是一个问题):

  • 如何快速搜索课程
  • 打开课程 详情页时仍然去查MySQL数据库,性能不够

为了提高网站的速度需要将课程信息进行缓存,并且要将课程信息加入索引库方便搜索,静态页面也不能再每次模型渲染,而是直接存储静态页面:

在这里插入图片描述
新建课程发布表,和预发布表一样,状态字段是发布状态,不再是审核状态

在这里插入图片描述

3、技术方案

分布式事务

和之前的事务不同,课程发布操作后将数据写入数据库、redis、elasticsearch、MinIO四个地方,这四个地方已经不限制在一个数据库内,是由四个分散的服务去提供,与这四个服务去通信需要网络通信,而网络存在不可到达性,这种分布式系统环境下,通过与不同的服务进行网络通信去完成事务称之为分布式事务

【参考:本地事务和分布式事务的区别】

以下场景都存在分布式事务:

  • 微服务架构下:
    在这里插入图片描述
  • 单服务多数据库:
    在这里插入图片描述
  • 多服务单数据库
    在这里插入图片描述

先根据实际场景定要满足CP还是AP,再技术选型。此需求我们只需要数据的最终一致性,不用强一致,因此选择AP

课程发布的分布式事务控制实现

本地消息表+任务调度机制完成分布式事务最终数据一致性的控制

在这里插入图片描述

  • 在内容管理服务的数据库中添加一个消息表,消息表和课程发布表在同一个数据库
  • 课程发布通过本地事务向课程发布表写入课程发布信息,同时向消息表写课程发布的消息。通过数据库进行控制,只要课程发布表插入成功消息表也插入成功,消息表的数据就记录了某门课程发布的任务
  • 定时调度内容管理服务去定时扫描消息表的记录
  • 消息表中扫描到数据后向redis、elasticsearch、MinIO同步数据
  • 同步成功后删除消息表的这条记录

时序图:

在这里插入图片描述

4、接口定义

@Api(value = "课程预览发布接口",tags = "课程预览发布接口")
@Controller
public class CoursePublishController {
...
 @ApiOperation("课程发布")
 @ResponseBody
 @PostMapping ("/coursepublish/{courseId}")
public void coursepublish(@PathVariable("courseId") Long courseId){

}

5、消息处理SDK

关于信息表,有以下操作:

  • 新增消息表
  • 扫描信息表
  • 更新信息表
  • 删除信息表

以后其他服务也可能需要这一套针对信息表的处理逻辑
在这里插入图片描述
为了复用代码,考虑将它抽成一个通用服务,此时,该服务就要连接多个数据库,且涉及到和其他微服务的网络通信 ====> 不合理

考虑将消息表的逻辑处理做成一个SDK工具包,而不是通用服务

在这里插入图片描述

消息表的设计:

  • 将小任务作为任务的不同的阶段,如课程发布任务需要执行三个同步操作:存储课程到redis、存储课程到索引库,存储课程页面到文件系统。
  • 每完成一个阶段在相应的阶段状态字段打上完成标记,即使这个大任务没有完成再重新执行时,如果小阶段任务完成了也不会重复执行某个小阶段的任务

在这里插入图片描述
SDK提供的接口定义:

package com.xuecheng.messagesdk.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.xuecheng.messagesdk.model.po.MqMessage;

import java.util.List;

/**
 *  消息处理服务接口
 *  MqMessage即消息表的PO类
 */
public interface MqMessageService extends IService<MqMessage> {

    /**
     * @description 扫描消息表记录,采用与扫描视频处理表相同的思路
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param count 扫描记录数
     * @return java.util.List 消息记录
     */
    public List<MqMessage> getMessageList(int shardIndex, int shardTotal,  String messageType,int count);

    /**
     * @description 完成任务
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completed(long id);

    /**
     * @description 完成阶段任务
     * @param id 消息id
     * @return int 更新成功:1
     */
    public int completedStageOne(long id);
    public int completedStageTwo(long id);
    public int completedStageThree(long id);
    public int completedStageFour(long id);

    /**
     * @description 查询阶段状态
     * @param id
     * @return int
    */
    public int getStageOne(long id);
    public int getStageTwo(long id);
    public int getStageThree(long id);
    public int getStageFour(long id);

}

消息SDK提供消息处理抽象类,此抽象类供使用者去继承使用:

package com.xuecheng.messagesdk.service;

import com.xuecheng.messagesdk.model.po.MqMessage;
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 {

    @Autowired
    MqMessageService 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);
                    }
                    //计数
                    countDownLatch.countDown();
                    log.debug("结束任务:{}",message);

                });
            });

            //等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
            countDownLatch.await(timeout,TimeUnit.SECONDS);
            System.out.println("结束....");
        } catch (InterruptedException e) {
           e.printStackTrace();

        }

    }

}

继承上面的抽象类,写任务的执行方法

/**
 * @description 消息处理测试类,继承MessageProcessAbstract
 */
@Slf4j
@Component
public class MessageProcessClass extends MessageProcessAbstract {


    @Autowired
    MqMessageService mqMessageService;

    //执行任务
    @Override
    public boolean execute(MqMessage mqMessage) {
        Long id = mqMessage.getId();
        log.debug("开始执行任务:{}",id);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //取出阶段状态,判断这个阶段是否执行成功过
        int stageOne = mqMessageService.getStageOne(id);
        if(stageOne<1){
            log.debug("开始执行第一阶段任务");
            System.out.println();
            int i = mqMessageService.completedStageOne(id);
            if(i>0){
                log.debug("完成第一阶段任务");
            }

        }else{
            log.debug("无需执行第一阶段任务");
        }

        return true;
    }
}

集成上面的SDK,直接引入Maven坐标即可:

<dependency>
    <groupId>com.xuecheng</groupId>
    <artifactId>xuecheng-plus-message-sdk</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

6、Mapper层开发

自动生成信息表的PO类和Mapper继承BaseMapper

7、Service层开发

当前是课程审核通过后,教育方可以选择发布课程(上架商品),上架的数据流是:

  • 向课程发布表course_publish插入一条记录,记录来源于课程预发布表,如果存在则更新,发布状态为:已发布
  • 更新course_base表的课程发布状态为:已发布
  • 删除预发布表记录
  • 向信息表插入一条数据(任务),任务的处理由后续定时任务处理,发布接口插入这条数据即可
@Transactional
@Override
public void publish(Long companyId, Long courseId) {

	 //校验业务逻辑
	 //查询课程预发布表,预发布表没数据即都没提交
	 CoursePublishPre coursePublishPre = coursePublishPreMapper.selectById(courseId);
	 if(coursePublishPre == null){
	    XueChengPlusException.cast("请先提交课程审核,审核通过才可以发布");
	 }
	 //本机构只允许提交本机构的课程
	 if(!coursePublishPre.getCompanyId().equals(companyId)){
	  XueChengPlusException.cast("不允许提交其它机构的课程。");
	 }
	
	 //课程审核状态
	 String auditStatus = coursePublishPre.getStatus();
	 //审核通过方可发布
	 if(!"202004".equals(auditStatus)){
	  XueChengPlusException.cast("操作失败,课程审核通过方可发布。");
	 }
	 //保存课程发布信息
	 saveCoursePublish(courseId);
	
	 //保存消息表
	 saveCoursePublishMessage(courseId);
	
	//删除课程预发布表对应记录
	 coursePublishPreMapper.deleteById(courseId);

}

保存任务信息的saveCoursePublishMessage(courseId)方法:

Java
 /**
  * @description 保存消息表记录
  * @param courseId  课程id
  * @return void
  */
private void saveCoursePublishMessage(Long courseId){
	 MqMessage mqMessage = mqMessageService.addMessage("course_publish", String.valueOf(courseId), null, null);
	 if(mqMessage==null){
	  XueChengPlusException.cast(CommonError.UNKOWN_ERROR);
 	}
}

任务信息通过发布接口写入信息表后,需要通过定时任务进行:

  • 页面静态化并存储
  • 索引存入Elasticsearch
  • 课程缓存信息存入Redis

8、页面静态化

课程预览功能通过模板引擎在页面模板中填充数据来生成html页面,此过程是客户端请求服务器时,服务器才开始渲染填充出html,最后响应给服务器,而服务端的响应并发能力有限,这里的预览以后要给海量用户预览,而非发布课程前,教育方的一个人预览。
在这里插入图片描述

===>

因此考虑提前通过模板引擎技术生成html页面,而静态页面可以使用nginx、apache等高性能的web服务器,并发性能高

==>

页面静态化就是对于数据变化不频繁而又频繁请求的页面,直接生成html页面存起来。本需求中需要我完成两步:

  • 生成静态化页面
  • 上传html到文件系统

接口定义:

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

实现接口:

public class CoursePublishServiceImpl implements CoursePublishService{


	@Override
    public File generateCourseHtml(Long courseId) {

        //静态化文件
        File htmlFile  = null;

        try {
            //配置freemarker
            Configuration configuration = new Configuration(Configuration.getVersion());

            //加载模板
            //选指定模板路径,classpath下templates下
            //得到classpath路径
            String classpath = this.getClass().getResource("/").getPath();
            configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
            //设置字符编码
            configuration.setDefaultEncoding("utf-8");

            //指定模板文件名称
            Template template = configuration.getTemplate("course_template.ftl");

            //准备数据
            CoursePreviewDto coursePreviewInfo = this.getCoursePreviewInfo(courseId);

            Map<String, Object> map = new HashMap<>();
            map.put("model", coursePreviewInfo);

            //静态化
            //参数1:模板,参数2:数据模型
            String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
//            System.out.println(content);
            //将静态化内容输出到文件中
            InputStream inputStream = IOUtils.toInputStream(content);
            //创建临时静态化文件
            htmlFile = File.createTempFile("course",".html");
            log.debug("课程静态化,生成静态文件:{}",htmlFile.getAbsolutePath());
            //输出流
            FileOutputStream outputStream = new FileOutputStream(htmlFile);
            IOUtils.copy(inputStream, outputStream);
        } catch (Exception e) {
            log.error("课程静态化异常:{}",e.toString());
            XueChengPlusException.cast("课程静态化异常");
        }

        return htmlFile;
    }

    @Override
    public void uploadCourseHtml(Long courseId, File file) {
    	//上传生成的html需要调用媒资管理服务的上传接口
    	//看下面微服务调用
    }


}

9、微服务远程调用

内容管理服务对页面静态化生成html文件需要调用媒资管理服务的上传文件接口。在Spring Cloud中可以使用Feign进行远程调用。下面配置Feign实现HTTP请求的发送

  • 添加Maven依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud 微服务远程调用 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
<!--feign支持Multipart格式传参-->
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.8.0</version>
</dependency>

  • 在nacos配置feign-dev.yaml公用配置文件
feign:
  hystrix:
    enabled: true  # 开启熔断
  circuitbreaker:
    enabled: true
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000  #熔断超时时间
ribbon:
  ConnectTimeout: 60000 #连接超时时间
  ReadTimeout: 60000 #读超时时间
  MaxAutoRetries: 0 #重试次数
  MaxAutoRetriesNextServer: 1 #切换实例的重试次数

  • 在内容管理service工程引入这个公共配置
shared-configs:
  - data-id: feign-${spring.profiles.active}.yaml
    group: xuecheng-plus-common
    refresh: true

  • 在调用方编写feign接口
/**
 * 在调用方定义接口
 * @description 媒资管理服务远程接口
 * value即被调用方的服务名
 * configuration后面的类是配置feign支持Multipart
 */
 @FeignClient(value = "media-api",configuration = MultipartSupportConfig.class)
public interface MediaServiceClient {

 @RequestMapping(value = "/media/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
 String uploadFile(@RequestPart("filedata") MultipartFile upload,@RequestParam(value = "objectName",required=false) String objectName);
}

  • 在启动类添加@EnableFeignClients注解
@EnableFeignClients(basePackages={"com.xuecheng.content.feignclient"})
  • 测试
@SpringBootTest
public class FeignUploadTest {

    @Autowired
    MediaServiceClient mediaServiceClient;

    //远程调用,上传文件
    @Test
    public void test() {
    
        MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(new File("D:\\develop\\test.html"));
        mediaServiceClient.uploadFile(multipartFile,"course","test.html");
    }

}

10、熔断降级

微服务之间互相调用,当某一个服务异常,无法被正常调用,如果不去处理,可能会导致雪崩效应。如:以A服务异常开始:

在这里插入图片描述

以上问题的处理方案是–配置熔断和降级

熔断

当下游服务(被调用方)异常而断开与上游服务的交互,它就相当于保险丝,下游服务异常触发了熔断,从而保证上游服务不受影响。

在这里插入图片描述

降级

下游服务异常触发熔断后,上游服务不再调用异常的微服务而转去走另一套处理逻辑 ==> 降级处理逻辑 ==> 这个降级处理逻辑可以是一个服务本地的方法

在这里插入图片描述
熔断和降级,熔断是为了保护系统,是一种保护系统的手段,降级则是一种熔断后的处理方法

开启熔断

  • 开启熔断,在feign-dev.yaml中
feign:
  hystrix:
    enabled: true
  circuitbreaker:
    enabled: true

  • 设置熔断的超时时间,为了防止一次处理时间较长触发熔断
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000  #熔断超时时间
ribbon:
  ConnectTimeout: 60000 #连接超时时间
  ReadTimeout: 60000 #读超时时间
  MaxAutoRetries: 0 #重试次数
  MaxAutoRetriesNextServer: 1 #切换实例的重试次数

定义降级逻辑

实现方式一 =====> fallback

//定义一个fallback类
//实现在调用方编写feign接口(MediaServiceClient接口)
//熔断后走这里
public class MediaServiceClientFallback implements MediaServiceClient {

	@Override
	public String uploadFile( MultipartFile upload,String objectName){
		return null;
	}
}

在原调用方编写的feign接口上加入属性fallback

@FeignClient(value = "media-api",configuration = MultipartSupportConfig.class,fallback = MediaServiceClientFallback.class)
@RequestMapping("/media")
public interface MediaServiceClient{
...

此方式无法取出熔断所抛出的异常

实现方式二 =====> fallbackFactory

//定义MediaServiceClientFallbackFactory如下:
//实现FallbackFactory接口
//泛型指定为原调用方编写的feign接口

@Slf4j
@Component
public class MediaServiceClientFallbackFactory implements FallbackFactory<MediaServiceClient> {
	//拿到异常信息
    @Override
    public MediaServiceClient create(Throwable throwable) {
        return new MediaServiceClient(){

	//发生熔断后,上游方法执行这个方法来降级处理
    @Override
    public String uploadFile(MultipartFile upload, String objectName) {
        //降级方法
        log.debug("调用媒资管理服务上传文件时发生熔断,异常信息:{}",throwable.toString(),throwable);
        return null;
            }
        };
    }
}

这里返回一个啥自己定义,这里定义返回null,上游请求若得到一个null,就说明熔断降级处理了

11、发布任务代码完善

微服务调用结束后,将生成的HTML页面上传,完善上传方法:

@Override
public void uploadCourseHtml(Long courseId, File file) {
     MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
     String course = mediaServiceClient.uploadFile(multipartFile, "course/"+courseId+".html");
     if(course==null){
         XueChengPlusException.cast("上传静态文件异常");
     }
 }

完善定时任务执行代码:

//生成课程静态化页面并上传至文件系统
public void generateCourseHtml(MqMessage mqMessage,long courseId){
    log.debug("开始进行课程静态化,课程id:{}",courseId);
    //消息id
    Long id = mqMessage.getId();
    //消息处理的service
    MqMessageService mqMessageService = this.getMqMessageService();
    //消息幂等性处理
    int stageOne = mqMessageService.getStageOne(id);
    if(stageOne == 1){
        log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
        return ;
    }

    //生成静态化页面
    File file = coursePublishService.generateCourseHtml(courseId);
    //上传静态化页面
    if(file!=null){
        coursePublishService.uploadCourseHtml(courseId,file);
    }
    //保存第一阶段状态
    mqMessageService.completedStageOne(id);

}

最后在xxl-job中加入任务:

在这里插入图片描述
到此,剩Redis和索引信息没处理。

三、需求:课程搜索

1、需求分析

用户通过课程搜索找到课程信息,进一步去查看课程的详细信息,进行选课、支付、学习。

在这里插入图片描述

关键点分析:

  • 根据一级分类、二级分类搜索课程信息
  • 根据关键字搜索课程信息,搜索方式为全文检索,关键字需要匹配课程的名称、 课程内容
  • 根据难度等级搜索课程
  • 搜索结点分页显示
  • 结果中关键字高亮显示

2、全文检索

全文检索即扫描文章中的每一个词,对每一个词建立一个索引,并指明该词在文章中出现的次数和位置。当用户查询时,检索程序根据事先建立的索引进行查找,即通过索引来搜到文章。这里需要对课程信息建立索引。

在这里插入图片描述

全文检索的速度非常快,早期应用在搜索引擎技术中,比如:百度、google

在这里插入图片描述
Elasticsearch与MySQL之间概念的对应关系见下表:

在这里插入图片描述

3、定义模型类

搜索条件Dto:

/**
 * @description 搜索课程参数dtl
 */
 @Data
 @ToString
public class SearchCourseParamDto {

  //关键字
  private String keywords;

  //大分类
  private String mt;

  //小分类
  private String st;
  //难度等级
  private String grade;


}

4、定义接口

/**
 * @description 课程搜索接口
 * PageResult、PageParams是自己定义的分页相关的通用类
 */
@Api(value = "课程搜索接口",tags = "课程搜索接口")
@RestController
@RequestMapping("/course")
public class CourseSearchController {



   @ApiOperation("课程搜索列表")
   @GetMapping("/list")
   public PageResult<CourseIndex> list(PageParams pageParams, SearchCourseParamDto searchCourseParamDto){

    
   
   }
}

5、开发Mapper层

生成po类,创建Mapper接口并继承BaseMapper<PO>

6、开发Service层

Service层接口定义:

/**
 * @description 课程搜索service
 * @author Mr.M
 * @date 2022/9/24 22:40
 * @version 1.0
 */
public interface CourseSearchService {


    /**
     * @description 搜索课程列表
     * @param pageParams 分页参数
     * @param searchCourseParamDto 搜索条件
    */
    SearchPageResultDto<CourseIndex> queryCoursePubIndex(PageParams pageParams, SearchCourseParamDto searchCourseParamDto);

实现类:

/**
 * @description 课程搜索service实现类
 */
@Slf4j
@Service
public class CourseSearchServiceImpl implements CourseSearchService {

    @Value("${elasticsearch.course.index}")
    private String courseIndexStore;
    @Value("${elasticsearch.course.source_fields}")
    private String sourceFields;

    @Autowired
    RestHighLevelClient client;

    @Override
    public 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[]{});
        
        //分页
        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);
        
        //请求搜索
        searchRequest.source(searchSourceBuilder);
       
        
        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);
            list.add(courseIndex);

        }
        SearchPageResultDto<CourseIndex> pageResult = new SearchPageResultDto<>(list, totalHits.value,pageNo,pageSize);

        

        return pageResult;
    }


}

靠,没学过elasticsearch,这里有点吃力,先记个后端处理高亮的代码,后续工作遇到再细学吧。

@Override
public 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);

        //取出source
        Map<String, Object> sourceAsMap = hit.getSourceAsMap();

        //课程id
        Long 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/418311.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

unity,射手游戏

文章目录介绍一&#xff0c;制作玩家具体函数脚本PlayerCharacter三、 制作玩家控制脚本 PlayerController&#xff0c;调用上面的函数方法四、 制作子弹脚本 shell五、 给玩家挂载脚本六、 制作坦克脚本七、 给坦克添加组件八、 开始游戏&#xff0c;播放动画九、 下载介绍 3…

seata学习笔记

Seata 官网&#xff1a; https://seata.io/zh-cn/index.html 是什么&#xff1f; Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式&#xff0c;为用户打造一站式的分布式…

MySQL中的float类型慎用!慎用!慎用!

在前端输入一串数字后有时候展示值与输入的内容一致&#xff0c;有时候却不一致。经分析&#xff0c;原来是MySQL数据库中字该字段的类型是float&#xff0c;该字段的值超过6位有效数字后就会进行四舍五入截取&#xff0c;举例来说&#xff1a;假设float类型字段的输入值是1234…

十八、MySQL 变量、分支结构IF、CASE...WHEN详解

文章目录一、变量1.1 系统变量1.1.1 系统变量分类1.1.2 查看系统变量1.2 用户变量1.2.1 用户变量分类1.2.2 会话用户变量1.2.3 局部变量1.2.4 对比会话用户变量与局部变量二、定义条件与处理程序2.1 案例分析2.2 定义条件2.3 定义处理程序2.4 案例解决三、流程控制3.1 分支结构…

[C++]类与对象下篇

目录 类与对象下篇&#xff1a;&#xff1a; 1.再谈构造函数 2.static成员 3.友元 4.内部类 5.匿名对象 6.拷贝对象时的编译器优化 7.再次理解封装 8.求12...n(不能使用乘除法、循环、条件判断) 9.计算日期到天数的转换 10.日期差值 11.打印日期 12.累加天数 类与对象下篇&…

数据结构与算法七 堆

一 堆 1.1 堆定义 堆是计算机科学中一类特殊的数据结构的统称&#xff0c;堆通常可以被看做是一棵完全二叉树的数组对象。 堆的特性&#xff1a; 它是完全二叉树&#xff0c;除了树的最后一层结点不需要是满的&#xff0c;其它的每一层从左到右都是满的&#xff0c;如果最后…

4月最新编程排行出炉,第一名ChatGPT都在用~

作为一名合格的&#xff08;准&#xff09;程序员&#xff0c;必做的一件事是关注编程语言的热度&#xff0c;编程榜代表了编程语言的市场占比变化&#xff0c;它的变化更预示着未来的科技风向和机会&#xff01; 快跟着一起看看本月排行有何看点&#xff1a; 4月Tiobe排行榜前…

【CSS】使用 固定定位 实现顶部导航栏 ( 核心要点 | 固定定位元素居中设置 | 代码示例 )

文章目录一、核心要点分析1、顶部导航栏要点2、固定定位垂直居中设置二、代码示例一、核心要点分析 实现下图所示功能 : 上方有一个固定导航栏 , 水平居中设置 ;左右两侧各一个广告栏 , 垂直居中设置 ; 1、顶部导航栏要点 顶部导航栏要点 : 使用固定定位 , 上边偏移设置为 0 …

Linux Ubuntu虚拟机下载安装以及初始配置--VMware、Ubuntu、Xshell、Xftp

一、下载准备 Ubuntu系统下载链接&#xff08;系统本身&#xff09;&#xff1a;官网链接 VMware虚拟机下载链接&#xff08;搭载Ubuntu系统&#xff09;&#xff1a;网盘链接密码XMKD Xshell下载链接&#xff08;虚拟机远程连接&#xff09;&#xff1a;官网链接 Xftp下载…

MySQL索引数据结构入门

之前松哥写过一个 MySQL 系列&#xff0c;但是当时是基于 MySQL5.7 的&#xff0c;最近有空在看 MySQL8 的文档&#xff0c;发现和 MySQL5.7 相比还是有不少变化&#xff0c;同时 MySQL 又是小伙伴们在面试时一个非常重要的知识点&#xff0c;因此松哥打算最近再抽空和小伙伴们…

PyQt5学习笔记一、安装PyQt5和在PyCharm中配置工具

一、安装PyQt5 1. 可以在cmd窗口安装PyQt5和工具 可以在cmd窗口使用命令 pip install PyQt5 安装PyQt5&#xff0c;若指定版本使用命令 pip install PyQt5version&#xff0c;此时同时安装了PyQt5和sip。参考链接 在cmd命令窗口安装Python模块_Mr. 李大白的博客-CSDN博客htt…

potPlay——记忆播放位置、各种快捷键

potPlay——记忆播放位置、各种快捷键potPlay——各种快捷键简洁版完整版快捷键列表potPlay——记忆播放位置potPlay——各种快捷键 简洁版 Q 复位 亮度&#xff0c;对比度&#xff0c;色度复位键 W/E 调暗/调亮 R/T 对比度 Y/U 饱和度 I/O 色彩度 D 上一帧 F 下一帧 M 静音 …

Docker开启并配置远程安全访问

前言 在工作学习中&#xff0c;为了提高项目部署效率&#xff0c;一般会在Idea中直接使用Docker插件连接服务器Docker容器&#xff0c;然后将项目打包与DockerFile一起build成Docker镜像部署运行。但是不可能服务器总是跟着主机的&#xff0c;因此呢时常会面临的一个问题就是从…

【微信小程序】-- uni-app 项目--- 购物车 -- 配置 tabBar 效果(五十一)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

事务的ACID特性

1. 絮絮叨叨 重温Apache ORC时&#xff0c;发现ORC支持ACID想起自己之前一度不知道ACID是哪些单词的缩写&#xff0c;更别提面试中常提到的事物隔离级别等知识了因此&#xff0c;特地学习一下数据库中事务的ACID 2. ACID 2.1 What’s transaction&#xff1f; 考虑一个真实…

42.原型对象 prototype

目录 1 面向对象与面向过程 2 原型对象 prototype 3 在内置对象中添加方法 4 constructor 属性 5 实例对象原型 __proto__ 6 原型继承 7 原型链与instanceof 7.1 原型链 7.2 instanceof 8 案例-模态框 1 面向对象与面向过程 编程思想有 面向过程 与 面向…

几何-九种二次曲面类型

&#xff08;一&#xff09;椭圆锥面 &#xff08;1&#xff09;把z平方看成一个一直变大的常数&#xff0c;那么可以看出延z方向&#xff0c;是一个一直变大的椭圆。 &#xff08;2&#xff09;把一个x或y赋予0&#xff0c;显然是一个两条关于原点对称的直线。 由上即可判断…

不小心删除了文件能恢复吗 误删除文件怎么找回

电脑是我们平时工作或者生活、学习中使用频率非常高的电子设备&#xff0c;已经成为了我们日常生活中不可或缺的一部分。删除文件是电脑使用过程中常见的一种操作&#xff0c;因为电脑的储存空间是有限的&#xff0c;我们需要对电脑数据进行清理&#xff0c;避免电脑储存空间占…

视觉检测相比于人工目视检测有哪些优势

技术的发展可以给我们带来好的结果。 是其中之一。 这在现代工业生产中非常常见。 视觉检测设备可以更好地检测生产中的错误和产品质量问题&#xff0c;提高工业生产的效率和自动化水平&#xff0c;提高工业生产的准确性&#xff0c;加快工作进度&#xff0c;节约时间&#xff…

docker too many open files解决方式

1&#xff1a;问题描述 今天在环境上执行docker ps命令失败&#xff0c;如下提示 [rootcontrol02 ~]# docker ps -a lgrep nginx Cannot connect to the Docker daemon at unix:///var/run/docker.sock, Is the docker daemon running?2&#xff1a;查看节点docker状态 看信…