SAAS-HRM-day5(ElasticSearch)

1. 课程上下线业务

1.1 业务描述

  1. 上线

    在系统中,我们添加了一个课程,用户不能立即就搜索到,需要上线以后才行。

  2. 下线

    当某个课程不想卖的时候,就要下线.当课程下线后,用户不能搜索到,但是数据库是还有的。

1.2 技术方案

1.2.1 技术方案一:数据库状态做判断-->不可取

上线后,修改状态为”上线”,用户搜索时只能搜索到上线状态的.如果不想卖了,执行下线时,修改状态下线.

--->垃圾(每次都要操作数据库)

1.2.2 技术方案二:全文检索服务器-->可取

上线时把课程数据同步到es,用户查询直接从es查询.也就意味着没有上线的课程用户查询不到,因为没有放到es库.

下线时把es库课程数据删除掉.用户就查询不到了.

--->牛B(以基于索引搜索代替数据查询)

优点:

  • (1)降低数据库压力
  • (2)提高了查询速度,增强用户体验-基于索引搜索,效率远远数据库搜索

2. 课程上下线实现

2.1 技术架构

//TODO 结构图以后自己画上补充

简单描述:

用户查询直接从ES库中查

  1. 管理员:
    • (1)添加课程。管理员将课程添加到db
    • (2)课程上线。把需要上线的课程查出来,同步到ES库。
    • (3)删除或修改课程。同步操作ES库和DB库。
    • (4)查询课程。这是后台的查询,就直接从数据库查询。
  2. 用户:用户查询直接从ES库中查
    • 减少数据库压力
    • 提高查询效率,用户体验更佳

2.2 实现+分析

课程服务调用搜索服务-服务内部调用feign

步骤分析:

  1. 搭建搜索服务
  2. 课程的上下线处理

2.2.1 搭建搜索服务

2.2.1.1 步骤分析

  1. 创建项目
  2. 导包
  3. 配置
  4. 入口类
  5. doc准备
  6. repository准备-service
  7. query准备-interface
  8. IESCourseService接口-service
  9. ESCourseServiceImpl(实现上面那个接口)-service
  10. ESCourseController-service
  11. client-interface
  12. 生成文档映射-test-service
  13. 测试
  14. 日志集成
  15. 网关集成
  16. 本项目swagger集成
  17. 网关swagger集成
  18. 启动测试网关、日志、swagger

2.2.1.2 步骤实现

  1. 创建项目

    在二级子模块hrm_basic_parent下创建三级子模块hrm_basic_es_interface和三级子模块hrm_basic_es_service

  2. 导包

  • hrm_basic_es_interface
 <!--公共工具依赖-->
        <dependency>
            <groupId>cn.wangningbo.hrm</groupId>
            <artifactId>hrm_basic_util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--不能直接依赖starter,有自动配置,而消费者是不需要额。-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--测试场景-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--客户端feign支持-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--spring-data-elasticsearch 注意没有starter,否则要报错-->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-elasticsearch</artifactId>
            <version>3.0.10.RELEASE</version>
        </dependency>
  • hrm_basic_es_service
<dependency>
            <groupId>cn.wangningbo.hrm</groupId>
            <artifactId>hrm_basic_es_interface</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web场景-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--测试场景-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Eureka 客户端依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--引入swagger支持-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--配置中心支持-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <!--springboot 对spring data es支持-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
  1. 配置(application.yml)-在service端
server:
  port: 9004
spring:
  application:
    name: hrm-es
  data:
    elasticsearch:
      cluster-name: elasticsearch
      cluster-nodes: 127.0.0.1:9300 #9200是图形界面端,9300代码端
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    prefer-ip-address: true
  1. 入口类-在service端
package cn.wangningbo.hrm;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class ElasticSearch9004Application {
    public static void main(String[] args) {
        SpringApplication.run(ElasticSearch9004Application.class, args);
    }
}
  1. doc准备-interface

    根据业务和表设计这里的doc

package cn.wangningbo.hrm.doc;

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.math.BigDecimal;
import java.util.Date;

@Document(indexName = "hrm", type = "course")
public class ESCourse {
    @Id
    private Long id;
    private String name;
    private String users;
    private Long courseTypeId;
    private String courseTypeName;
    private Long gradeId;
    private String gradeName;
    private Integer status;
    private Long tenantId;
    private String tenantName;
    private Long userId;
    private String userName;
    private Date startTime;
    private Date endTime;
    private String intro;
    private String resources; //图片
    private Date expires; //过期时间
    private BigDecimal priceOld; //原价
    private BigDecimal price; //原价
    private String qq; //原价

    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
    private String all;

    public String getAll() {
        String tmp = name
                + " " + users
                + " " + courseTypeName
                + " " + gradeName
                + " " + tenantName
                + " " + userName
                + " " + intro;
        return tmp;
    }

    public void setAll(String all) {
        this.all = all;
    }
    //提供get、set和toString方法
}
  1. repository准备-service

    由于需要操作es,所需需要repository操作

package cn.wangningbo.hrm.repository;


import cn.wangningbo.hrm.doc.ESCourse;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface CourseRepository extends ElasticsearchRepository<ESCourse, Long> {
}
  1. query准备-interface
public class ESCourseQuery extends BaseQuery {
}
  1. IESCourseService接口-service
package cn.wangningbo.hrm.service;


import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.PageList;

import java.util.List;

/**
 * @author wangningbo
 * @since 2019-09-06
 */
public interface IESCourseService {
    //添加
    void insert(ESCourse esCourse);

    //修改
    void updateById(ESCourse esCourse);

    //删除
    void deleteById(Long id);

    //查询一个
    ESCourse selectById(Long id);

    //查询所有
    List<ESCourse> selectList(Object o);

    //dsl高级查询
    PageList<ESCourse> selectListPage(ESCourseQuery query);

}
  1. ESCourseServiceImpl(实现上面那个接口)-service
@Service
public class ESCourseServiceImpl implements IESCourseService {

    @Autowired
    private CourseRepository courseRepository;

    @Override
    public void insert(ESCourse esCourse) {
        courseRepository.save(esCourse);
    }

    @Override
    public void updateById(ESCourse esCourse) {
        courseRepository.save(esCourse);
    }

    @Override
    public void deleteById(Long id) {
        courseRepository.deleteById(id);
    }

    @Override
    public ESCourse selectById(Long id) {
        return courseRepository.findById(id).get();
    }

    @Override
    public List<ESCourse> selectList(Object o) {
        Page page = (Page) courseRepository.findAll();
        return page.getContent();
    }

    @Override
    public PageList<ESCourse> selectListPage(ESCourseQuery query) {
        NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
        BoolQueryBuilder bool = QueryBuilders.boolQuery();
        //模糊查询 @TODO
        bool.must(QueryBuilders.matchQuery("intro", "zhang"));
        //精确过滤 @TODO
        List<QueryBuilder> filters = bool.filter();
        filters.add(QueryBuilders.rangeQuery("age").gte(0).lte(200));
        builder.withQuery(bool); //query bool must(filter)
        //排序 @TODO
        builder.withSort(SortBuilders.fieldSort("age").order(SortOrder.ASC));
        //分页 当前页从0开始
        builder.withPageable(PageRequest.of(query.getPage() - 1, query.getRows()));
        //构造查询条件
        NativeSearchQuery esQuery = builder.build();
        //查询
        Page<ESCourse> page = courseRepository.search(esQuery);
        return new PageList<>(page.getTotalElements(), page.getContent());
    }
}
  1. ESCourseController-service
package cn.wangningbo.hrm.web.controller;

import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.service.IESCourseService;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/esCourse")
public class ESCourseController {
    @Autowired
    public IESCourseService esCourseService;

    /**
     * 保存和修改公用的
     *
     * @param esCourse 传递的实体
     * @return Ajaxresult转换结果
     */
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public AjaxResult save(@RequestBody ESCourse esCourse) {
        try {
            if (esCourse.getId() != null) {
                esCourseService.updateById(esCourse);
            } else {
                esCourseService.insert(esCourse);
            }
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setMessage("保存对象失败!" + e.getMessage());
        }
    }

    /**
     * 删除
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public AjaxResult delete(@PathVariable("id") Long id) {
        try {
            esCourseService.deleteById(id);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setMessage("删除对象失败!" + e.getMessage());
        }
    }

    //获取用户
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ESCourse get(@PathVariable("id") Long id) {
        return esCourseService.selectById(id);
    }


    /**
     * 查看所有信息
     *
     * @return
     */
    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public List<ESCourse> list() {

        return esCourseService.selectList(null);
    }


    /**
     * 分页查询数据
     *
     * @param query 查询对象
     * @return PageList 分页对象
     */
    @RequestMapping(value = "/json", method = RequestMethod.POST)
    public PageList<ESCourse> json(@RequestBody ESCourseQuery query) {
        return esCourseService.selectListPage(query);
    }
}
  1. client-interface

    注意点:@FeignClient的value = "HRM-ES",自己服务端的名字,@RequestMapping("/esCourse")要与自己服务端controller的一样

package cn.wangningbo.hrm.client;


import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@FeignClient(value = "HRM-ES", configuration = FeignClientsConfiguration.class,
        fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
@RequestMapping("/esCourse")
public interface ESCourseClient {
    /**
     * 保存和修改公用的
     *
     * @param esCourse 传递的实体
     * @return Ajaxresult转换结果
     */
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    AjaxResult save(ESCourse esCourse);

    /**
     * 删除
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
    AjaxResult delete(@PathVariable("id") Integer id);

    //获取用户
    @RequestMapping("/{id}")
    ESCourse get(@RequestParam(value = "id", required = true) Long id);


    /**
     * 查看所有信息
     *
     * @return
     */
    @RequestMapping("/list")
    public List<ESCourse> list();

    /**
     * 分页查询数据
     *
     * @param query 查询对象
     * @return PageList 分页对象
     */
    @RequestMapping(value = "/json", method = RequestMethod.POST)
    PageList<ESCourse> json(@RequestBody ESCourseQuery query);
}
package cn.wangningbo.hrm.client;

import cn.wangningbo.hrm.doc.ESCourse;
import cn.wangningbo.hrm.query.ESCourseQuery;
import cn.wangningbo.hrm.util.AjaxResult;
import cn.wangningbo.hrm.util.PageList;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author wangningbo
 * @date 2019/09/06
 */
@Component
public class EsCourseClientHystrixFallbackFactory implements FallbackFactory<ESCourseClient> {

    @Override
    public ESCourseClient create(Throwable throwable) {
        return new ESCourseClient() {
            @Override
            public AjaxResult save(ESCourse esCourse) {
                return null;
            }

            @Override
            public AjaxResult delete(Integer id) {
                return null;
            }

            @Override
            public ESCourse get(Long id) {
                return null;
            }

            @Override
            public List<ESCourse> list() {
                return null;
            }

            @Override
            public PageList<ESCourse> json(ESCourseQuery query) {
                return null;
            }
        };
    }
}
  1. 生成文档映射-test-service
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ElasticSearch9004Application.class)
public class IESCourseServiceTest {
    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    @Test
    public void testInit() throws Exception {
        elasticsearchTemplate.createIndex(ESCourse.class);
        elasticsearchTemplate.putMapping(ESCourse.class);
    }
}
  1. 测试

    http://localhost:9004/esCourse/list

  2. 日志集成

    resources下存放一个名字为logback-spring.xml的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
    <!-- 定义日志的根目录 -->
    <property name="LOG_HOME" value="/hrm/" />
    <!-- 定义日志文件名称 -->
    <property name="appName" value="hrm-es"></property>
    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!--
        日志输出格式:
            %d表示日期时间,
            %thread表示线程名,
            %-5level:级别从左显示5个字符宽度
            %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 
            %msg:日志消息,
            %n是换行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
    <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日志文件的名称
           /hrm/hrm-course/hrm-course.log
        -->
        <file>${LOG_HOME}/${appName}/${appName}.log</file>
        <!--
        当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
        TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--
            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 
            %i:当文件大小超过maxFileSize时,按照i进行文件滚动
            -->
            <fileNamePattern>${LOG_HOME}/${appName}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 
            可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
            且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
            那些为了归档而创建的目录也会被删除。
            -->
            <MaxHistory>365</MaxHistory>
            <!-- 
            当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
            -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 日志输出格式: -->     
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 
        logger主要用于存放日志对象,也可以定义日志类型、级别
        name:表示匹配的logger类型前缀,也就是包的前半部分
        level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
        additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
        false:表示只用当前logger的appender-ref,true:
        表示当前logger的appender-ref和rootLogger的appender-ref都有效
    -->
    <!-- hibernate logger -->
    <logger name="cn.wangningbo" level="debug" />
    <!-- Spring framework logger -->
    <logger name="org.springframework" level="debug" additivity="false"></logger>



    <!-- 
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 
    -->
    <root level="info">
        <appender-ref ref="stdout" />
        <appender-ref ref="appLogAppender" />
    </root>
</configuration> 
  1. 网关集成

    在网关的配置文件中zuul.routes中再新加两行,配置es

    es.serviceId: hrm-es # 服务名
    es.path: /es/** # 把es打头的所有请求都转发给hrm-es
  1. 本项目swagger集成
package cn.wangningbo.hrm.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2 {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //对外暴露服务的包,以controller的方式暴露,所以就是controller的包.
                .apis(RequestHandlerSelectors.basePackage("cn.wangningbo.hrm.web.controller"))
                .paths(PathSelectors.any())
                .build();
    }


    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("分布式全文检索api")
                .description("分布式全文检索接口文档说明")
                .contact(new Contact("wangningbo", "", "wang_ning_bo163@163.com"))
                .version("1.0")
                .build();
    }
}
  1. 网关swagger集成

    在网关的DocumentationConfig配置类里面新加个

resources.add(swaggerResource("分布式全文检索", "/services/es/v2/api-docs", "2.0"));
  1. 启动测试网关、日志、swagger

2.2.2 课程上下线处理

架构分析图(后续补上)

2.2.2.1 步骤分析

  1. 改造商品的删除和修改(索引库也要进行对应的操作)
  2. 课程的上下线逻辑
  3. client

2.2.2.2 步骤实现

  1. 改造商品的删除和修改(索引库也要进行对应的操作)

    由于要使用es,这里操作删除和修改的时候也要操作es库,所以要覆写删除和修改方法!

    @Override
    public boolean deleteById(Serializable id) {
        //删除数据库的同时也要判断状态是否要删除es库
        courseMapper.deleteById(id);
        Course course = courseMapper.selectById(id);
        if (course.getStatus() == 1)
            esCourseClient.delete(Integer.valueOf(id.toString()));
        return true;
    }

    // @TODO 不同服务,反3Fn设计冗余字段
    // @TODO 相同服务,关联查询
    //根据自己的需求和库中的表设计
    private ESCourse course2EsCourse(Course course) {
        ESCourse result = new ESCourse();
        result.setId(course.getId());
        result.setName(course.getName());
        result.setUsers(course.getUsers());
        result.setCourseTypeId(course.getCourseTypeId());
        //type-同库
        if (course.getCourseType() != null)
            result.setCourseTypeName(course.getCourseType().getName());
        //跨服务操作
        result.setGradeId(course.getGrade());
        result.setGradeName(null);
        result.setStatus(course.getStatus());
        result.setTenantId(course.getTenantId());
        result.setTenantName(course.getTenantName());
        result.setUserId(course.getUserId());
        result.setUserName(course.getUserName());
        result.setStartTime(course.getStartTime());
        result.setEndTime(course.getEndTime());
        //Detail
        result.setIntro(null);
        //resource
        result.setResources(null);
        //market
        result.setExpires(null);
        result.setPrice(null);
        result.setPriceOld(null);
        result.setQq(null);
        return result;
    }

    @Override
    public boolean updateById(Course entity) {
        //修改数据库的时候也要根据状态判断是否操作修改es库
        courseMapper.updateById(entity);
        Course course = courseMapper.selectById(entity.getId());
        if (course.getStatus() == 1)
            esCourseClient.save(course2EsCourse(entity));
        return true;
    }
  1. 课程的上下线逻辑

    简单逻辑:前端会发起上线或下线请求!到我这里的controller接口。我这里进行一层一层的实现逻辑!既要操作db库,也要操作es库!

    controller层新增两个方法,上线和下线。我这里先实现上线,再写下线(下线比较简单)!

==(上线---------------------->)==

controller

//由于前端可能是批量操作,所以我这里使用数组接收参数
    @PostMapping("/onLine")
    public AjaxResult onLine(@RequestBody Long[] ids) {
        try {
            courseService.onLine(ids);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("online failed!"+e);
            return AjaxResult.me().setSuccess(false)
                    .setMessage("上线失败!"+e.getMessage());
        }
    }

IService

    void onLine(Long[] ids);

ServiceImpl

    /**
     * 商品课程上线
     * @param ids
     */
    @Override
    public void onLine(Long[] ids) {
        //批量操作数据库状态字段 //类似于这种update t_course set status = 1,start_time=xxx where id in (1,2,3)
        ArrayList<Map<String, Object>> listMap = new ArrayList<>();
        if (ids != null || ids.length > 0) {
            for (Long id : ids) {
                HashMap<String, Object> map = new HashMap<>();
                map.put("id", id);
                map.put("start_time", new Date());
                listMap.add(map);
            }
        }
        //批量修改db状态字段
        courseMapper.batchOnline(listMap);
        //批量操作索引库
        //从数据库中查出来需要上线的商品课程
        List<Course> courseList = courseMapper.selectBatchIds(Arrays.asList(ids));
        //把从数据库中查出来的商品课程转化为es的doc
        List<ESCourse> esCourseList = courseList2EsCourse(courseList);
        //批量操作把doc添加到es库 //批量添加到es库中的方法没有,自己实现一个
        esCourseClient.batchSave(esCourseList);
    }
    
        /**
     * db库的domain转化为es的doc
     *
     * @param courseList
     * @return
     */
    private List<ESCourse> courseList2EsCourse(List<Course> courseList) {
        ArrayList<ESCourse> list = new ArrayList<>();
        courseList.forEach(course -> list.add(course2EsCourse(course)));
        return list;
    }

上面这个serviceImpl主要是做2步操作,第一步是批量修改db库的状态字段为上线,第二步是操作es库,批量把上线的商品课程添加es库中。

①先说操作db库的字段修改为上线的逻辑步骤

Mapper.java

void batchOnline(ArrayList<Map<String,Object>> listMap);

Mapper.xml

    <!--批量上线商品课程 void batchOnline(ArrayList<Map<String,Object>> listMap);-->
    <update id="batchOnline" parameterType="arrayList">
        UPDATE t_course set status =1,start_time=now() where id IN
        <foreach collection="list" item="item" open="(" close=")" separator=",">
            #{item.id}
        </foreach>
    </update>

②再说操作es库,把上线的商品课程添加到es库

es-->client

    //批量上线
    @PostMapping("/online")
    AjaxResult batchSave(List<ESCourse> esCourseList);

es-->ClientHystrixFallbackFactory

            @Override
            public AjaxResult batchSave(List<ESCourse> esCourseList) {
                return null;
            }

es-->controller

    /**
     * 批量保存到es库,批量上线
     * @param esCourseList
     * @return
     */
    @PostMapping("/online")
    AjaxResult batchSave(@RequestBody List<ESCourse> esCourseList){
        try {
            esCourseService.batchSave(esCourseList);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("批量添加失败!"+e.getMessage());
        }
    }

es-->IService

    //批量保存
    void batchSave(List<ESCourse> ids);

es-->ServiceImpl

    //批量保存
    @Override
    public void batchSave(List<ESCourse> esCourseList) {
        courseRepository.saveAll(esCourseList);
    }

==注意:这时候调用es的那个模块的入口类就要打上注解@EnableFeignClients== 入口类获得feign支持

测试是否成功

==(下线---------------------->)==

下线的简单逻辑分析:管理员下线商品课程只需要做2步,第一步:修改db库中的商品课程字段为下线状态,第二步:删除es库中的商品课程

controller

    /**
     * 商品课程下线
     *
     * @param ids
     * @return
     */
    @PostMapping("/offLine")
    public AjaxResult offLine(@RequestBody Long[] ids) {
        try {
            courseService.offLine(ids);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("offLine failed!"+e);
            return AjaxResult.me().setSuccess(false)
                    .setMessage("下线失败!"+e.getMessage());
        }
    }

IService

    void offLine(Long[] ids);

ServiceImpl

    @Override
    public void offLine(Long[] ids) {
        //批量修改db库中商品课程的状态为下线状态
        courseMapper.batchOffline(Arrays.asList(ids));
        //批量删除es库中的商品课程 //时间方面是使用mysql的语法生成的
        List<Course> courseList = courseMapper.selectBatchIds(Arrays.asList(ids));
        List<ESCourse> esCourseList = courseList2EsCourse(courseList);
        esCourseClient.batchDel(esCourseList);
    }

上面这个serviceImpl主要是做2步操作,第一步是批量修改db库的状态字段为下线状态,第二步是操作es库,批量把下线的商品课程从es库中删除掉。

①批量修改db库的状态字段为下线状态

Mapper.java

    void batchOffline(List<Long> longs);

Mapper.xml

    <!--批量下线商品课程void batchOffline(List<Long> longs);-->
    <update id="batchOffline" parameterType="arrayList">
        UPDATE t_course set status =0,end_time=now() where id IN
        <foreach collection="list" item="item" open="(" close=")" separator=",">
            #{item}
        </foreach>
    </update>

es-->client

    //批量下线
    @PostMapping("/offline")
    void batchDel(List<ESCourse> esCourseList);

es-->ClientHystrixFallbackFactory

            @Override
            public void batchDel(List<ESCourse> esCourseList) {

            }

es-->controller

    @PostMapping("/offline")
    AjaxResult batchDel(@RequestBody List<ESCourse> esCourseList){
        try {
            esCourseService.batchDel(esCourseList);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("批量删除失败!"+e.getMessage());
        }
    }

es-->Iservice

    //批量删除
    void batchDel(List<ESCourse> esCourseList);

es-->ServiceImpl

    //批量删除
    @Override
    public void batchDel(List<ESCourse> esCourseList) {
        courseRepository.deleteAll(esCourseList);
    }

测试是否成功

3. 易错点总结

  1. ESClient那里的注解@FeignClient的参数。value = "HRM-ES"的值指向自己注册到eureka的服务。以下面为例
@FeignClient(value = "HRM-ES",configuration = FeignClientsConfiguration.class,
        fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
  1. ESClient那里的注@RequestMapping的参数值要与controller的一致。以下面的为例
//client
@FeignClient(value = "HRM-ES",configuration = FeignClientsConfiguration.class,
        fallbackFactory = EsCourseClientHystrixFallbackFactory.class)
@RequestMapping("/esCourse")
public interface EsCourseClient {}

//controller
@RestController
@RequestMapping("/esCourse")
public class EsCourseController {}
  1. 其他模块调用es的时候,作为es的客户端,在入口类上要加入feign的支持。也就是注解@EnableFeignClients。以下面为例
package cn.wangningbo.hrm;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@MapperScan("cn.wangningbo.hrm.mapper")
@EnableFeignClients
public class Course9002Application {
    public static void main(String[] args) {
        SpringApplication.run(Course9002Application.class, args);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容