<repo_name>jungle8884/community<file_sep>/src/main/java/tech/turl/community/controller/LikeController.java
package tech.turl.community.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import tech.turl.community.entity.Event;
import tech.turl.community.entity.User;
import tech.turl.community.event.EventProducer;
import tech.turl.community.service.LikeService;
import tech.turl.community.util.CommunityConstant;
import tech.turl.community.util.CommunityUtil;
import tech.turl.community.util.HostHolder;
import tech.turl.community.util.RedisKeyUtil;

import java.util.HashMap;
import java.util.Map;

/**
 * @author zhengguohuang
 * @date 2021/03/21
 */
@Controller
public class LikeController implements CommunityConstant {
    @Autowired private HostHolder hostHolder;

    @Autowired private LikeService likeService;

    @Autowired private EventProducer eventProducer;
    @Autowired private RedisTemplate redisTemplate;

    /**
     * 点赞
     *
     * @param entityType
     * @param entityId
     * @return
     */
    @PostMapping("/like")
    @ResponseBody
    public String like(int entityType, int entityId, int entityUserId, int postId) {
        User user = hostHolder.getUser();
        // 点赞
        likeService.like(user.getId(), entityType, entityId, entityUserId);
        // 数量
        long likeCount = likeService.findEntityLikeCount(entityType, entityId);
        // 状态
        int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
        // 返回的结果
        Map<String, Object> map = new HashMap<>(16);
        map.put("likeCount", likeCount);
        map.put("likeStatus", likeStatus);
        // 触发事件
        if (likeStatus == 1) {
            Event event =
                    new Event()
                            .setTopic(TOPIC_LIKE)
                            .setUserId(user.getId())
                            .setEntityType(entityType)
                            .setEntityId(entityId)
                            .setEntityUserId(entityUserId)
                            .setData("postId", postId);
            eventProducer.fireEvent(event);
        }
        if (entityType == ENTITY_TYPE_POST) {
            // 计算帖子的分数
            String redisKey = RedisKeyUtil.getPostScoreKey();
            redisTemplate.opsForSet().add(redisKey, postId);
        }
        return CommunityUtil.getJSONString(0, null, map);
    }
}
<file_sep>/src/main/java/tech/turl/community/service/DataService.java
package tech.turl.community.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import tech.turl.community.util.RedisKeyUtil;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

/**
 * @author zhengguohuang
 * @date 2021/03/29
 */
@Service
public class DataService {
  @Autowired private RedisTemplate redisTemplate;
  private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");

  /**
   * 统计UV，将指定ip计入UV
   *
   * @param ip
   */
  public void recordUV(String ip) {
    String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
    redisTemplate.opsForHyperLogLog().add(redisKey, ip);
  }

  /**
   * 统计指定日期范围内的UV
   *
   * @param startDate
   * @param endDate
   * @return
   */
  public long calculateUV(Date startDate, Date endDate) {
    if (startDate == null || endDate == null) {
      throw new IllegalArgumentException("参数不能为空");
    }
    // 整理该日期范围内的Key
    List<String> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(startDate);
    while (!calendar.getTime().after(endDate)) {
      String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
      keyList.add(key);
      calendar.add(Calendar.DATE, 1);
    }
    // 合并这些数据
    String redisKey = RedisKeyUtil.getUVKey(df.format(startDate), df.format(endDate));
    redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
    // 统计数据
    return redisTemplate.opsForHyperLogLog().size(redisKey);
  }

  /**
   * 统计DAU，将指定用户计入DAU
   *
   * @param
   */
  public void recordDAU(int userId) {
    String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));
    redisTemplate.opsForValue().setBit(redisKey, userId, true);
  }

  /**
   * 统计指定日期范围内的DAU
   *
   * @param startDate
   * @param endDate
   * @return
   */
  public long calculateDAU(Date startDate, Date endDate) {
    // 整理该日期范围内的Key
    List<byte[]> keyList = new ArrayList<>();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(startDate);
    while (!calendar.getTime().after(endDate)) {
      String key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));
      keyList.add(key.getBytes());
      calendar.add(Calendar.DATE, 1);
    }
    // 合并这些数据
    String redisKey = RedisKeyUtil.getDAUKey(df.format(startDate), df.format(endDate));
    return (long)
        redisTemplate.execute(
            (RedisCallback)
                redisConnection -> {
                  redisConnection.bitOp(
                      RedisStringCommands.BitOperation.OR,
                      redisKey.getBytes(),
                      keyList.toArray(new byte[0][0]));
                  return redisConnection.bitCount(redisKey.getBytes());
                });
  }
}
<file_sep>/src/main/java/tech/turl/community/controller/SearchController.java
package tech.turl.community.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import tech.turl.community.entity.DiscussPost;
import tech.turl.community.entity.Page;
import tech.turl.community.service.ElasticsearchService;
import tech.turl.community.service.LikeService;
import tech.turl.community.service.UserService;
import tech.turl.community.util.CommunityConstant;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhengguohuang
 * @date 2021/03/26
 */
@Controller
public class SearchController implements CommunityConstant {

    @Autowired private ElasticsearchService elasticsearchService;

    @Autowired private UserService userService;

    @Autowired private LikeService likeService;

    /**
     * 搜索帖子
     *
     * @param keyword
     * @param page
     * @param model
     * @return
     */
    @GetMapping("/search")
    public String search(String keyword, Page page, Model model) {
        // 搜索帖子
        org.springframework.data.domain.Page<DiscussPost> searchResult =
                elasticsearchService.searchDiscussPost(
                        keyword, page.getCurrent() - 1, page.getLimit());
        // 聚合数据
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if (searchResult != null) {
            for (DiscussPost post : searchResult) {
                Map<String, Object> map = new HashMap<>(16);
                // 帖子
                map.put("post", post);
                // 作者
                map.put("user", userService.findUserById(post.getUserId()));
                // 点赞数量
                map.put(
                        "likeCount",
                        likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));
                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        model.addAttribute("keyword", keyword);
        page.setPath("/search?keyword=" + keyword);
        page.setRows(searchResult == null ? 0 : (int) searchResult.getTotalElements());
        return "site/search";
    }
}
<file_sep>/src/main/java/tech/turl/community/annotation/LoginRequired.java
package tech.turl.community.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author zhengguohuang
 * @date 2021/03/16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
<file_sep>/README.md
# 仿牛客网社区项目

![IDE](https://img.shields.io/badge/IDE-IntelliJ%20IDEA-brightgreen.svg) ![Java](https://img.shields.io/badge/Java-1.8-blue.svg) ![Database](https://img.shields.io/badge/Database-MySQL-lightgrey.svg)

## 项目简介

一个仿照牛客网实现的讨论区，不仅实现了基本的注册、登录、发帖、评论、回复、私信等功能，同时使用前缀树实现敏感词过滤；使用 Redis 实现点赞与关注；使用 Kafka 处理发送评论、点赞和关注等系统通知；使用 Elasticsearch 实现全文搜索，关键词高亮显示；使用 wkhtmltopdf 生成长图和 PDF；实现网站 UV 和 DAU 统计；并将用户头像等信息存于七牛云服务器。

## 在线演示地址

~~http://community.turl.tech~~

## 测试账号

| 用户类型 | 用户名 | 密码   |
| -------- | ------ | ------ |
| 普通用户 | user   | 123456 |
| 版主     | banzhu | 123456 |
| 管理员   | admin  | 123456 |

## 功能列表

- [x] 邮件发送
- [x] 注册
- [x] 验证码
- [x] 登录
- [ ] 登录出错限制
- [ ] 第三方登录
- [x] 修改头像
- [x] 修改密码
- [x] 敏感词过滤
- [x] 发布帖子
- [x] 我的帖子
- [x] 帖子详情
- [ ] 浏览量
- [x] 评论
- [x] 私信
- [x] 统一异常处理
- [x] 统一日志处理
- [x] 点赞
- [x] 关注
- [ ] 收藏
- [ ] 我的收藏
- [x] 系统通知
- [x] 搜索
- [x] 权限控制
- [x] 置顶、加精、删除
- [x] 网站统计
- [x] 定时执行任务计算热门帖子
- [x] 生成长图
- [x] 文件上传至七牛云
- [x] 监控

## 功能简介

- 使用 Redis 的 set 实现点赞，zset 实现关注，并使用 Redis 存储登录ticket和验证码，解决分布式 Session 问题，使用 Redis 的高级数据类型 HyperLogLog 统计 UV(Unique Visitor)，使用 Bitmap 统计 DAU(Daily Active User)。
- 使用 Kafka 处理发送评论、点赞、关注等系统通知、将新发布的帖子异步传输至Elasticsearch服务器，并使用事件进行封装，构建了强大的异步消息系统。
- 使用Elasticsearch做全局搜索，增加关键词高亮显示等功能。
- 热帖排行模块，使用本地缓存 Caffeine作为一级缓存和分布式缓存 Redis作为二级缓存构建多级缓存，避免了缓存雪崩，同时使用使用压测工具测试优化前后性能，将 QPS 提升了4倍(7.6/sec -> 33.5/sec)，大大提升了网站访问速度。并使用 Quartz 定时更新热帖排行。
- 使用 Spring Security 做权限控制，替代拦截器的拦截控制，并使用自己的认证方案替代 Security 认证流程，使权限认证和控制更加方便灵活。

## 技术栈

| 技术            | 链接                                                         | 版本           |
| --------------- | ------------------------------------------------------------ | -------------- |
| Spring Boot     | https://spring.io/projects/spring-boot                       | 2.4.3          |
| Spring          | https://spring.io/projects/spring-framework                  | 5.3.4          |
| Spring MVC      | https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#spring-web | 5.3.4          |
| MyBatis         | http://www.mybatis.org/mybatis-3                             | 3.5.1          |
| Redis           | https://redis.io/                                            | 5.0.3          |
| Kafka           | http://kafka.apache.org/                                     | 2.7.0          |
| Elasticsearch   | https://www.elastic.co/cn/elasticsearch/                     | 7.9.3          |
| Spring Security | https://spring.io/projects/spring-security                   | 5.4.5          |
| Spring Quartz   | https://www.baeldung.com/spring-quartz-schedule              | 2.3.2          |
| wkhtmltopdf     | https://wkhtmltopdf.org                                      | 0.12.6         |
| kaptcha         | https://github.com/penggle/kaptcha                           | 2.3.2          |
| Thymeleaf       | https://www.thymeleaf.org/                                   | 3.0.12.RELEASE |
| MySQL           | https://www.mysql.com/                                       | 5.7.17         |
| JDK             | https://www.oracle.com/java/technologies/javase-downloads.html | 1.8            |

## 系统架构

![image-20210331103427522](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331103427522.png)

![网站架构图](https://gitee.com/zhengguohuang/img/raw/master/img/%E7%BD%91%E7%AB%99%E6%9E%B6%E6%9E%84%E5%9B%BE.png)

## 数据库初始化

```sql
create database community;
use community;
source /path/to/sql/init_schema.sql;
source /path/to/sql/init_data.sql;
source /path/to/sql/tables_mysql_innodb.sql;
```

## 运行

1. 安装JDK，Maven

2. 克隆代码到本地

   ```bash
   git clone https://github.com/zhengguohuang/community.git
   ```

3. 运行打包命令

   ```bash
   mvn package
   ```

4. 运行项目

   ```bash
   java -jar xxx.jar
   ```

5. 访问项目

   ```
   http://localhost:8080
   ```

## 运行效果展示

#### 发帖

![image-20210331105217867](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105217867.png)

#### 帖子详情、评论

![image-20210331105241026](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105241026.png)

#### 私信

![image-20210331105316546](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105316546.png)

#### 系统通知

![image-20210331105333140](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105333140.png)

#### 搜索

![image-20210331105404130](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105404130.png)

#### 网站统计

![image-20210331105420047](https://gitee.com/zhengguohuang/img/raw/master/img/image-20210331105420047.png)

## 文档

https://easydoc.top/doc/70562937/SJuuLoYS/cI3Atvte

欢迎加群交流: 792364202

## 更新日志

* 2021-3-8 创建项目

<file_sep>/src/test/java/tech/turl/community/TestMessageMapper.java
package tech.turl.community;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import tech.turl.community.dao.MessageMapper;
import tech.turl.community.entity.Message;

import java.util.List;

/**
 * @author zhengguohuang
 * @date 2021/03/19
 */
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TestMessageMapper {
    @Autowired
    private MessageMapper messageMapper;

    @Test
    public void test() {
        List<Message> list = messageMapper.selectConversations(111, 0, 20);
        for (Message message : list) {
            System.out.println(message);
        }
        int count = messageMapper.selectConversationCount(111);
        System.out.println(count);

        list = messageMapper.selectLetters("111_112", 0, 10);
        for (Message message : list) {
            System.out.println(message);
        }
        count = messageMapper.selectLetterCount("111_112");
        System.out.println(count);

        count = messageMapper.selectLetterUnreadCount(131, "111_131");
        System.out.println(count);

    }
}
<file_sep>/src/main/resources/README.md
sensitive-words.txt为本项目收集的敏感词库，仅限用于合法的、积极向上的敏感词过滤使用，严禁用于从事违反法律法规、危害国家、危害人民、不道德的活动！！！<file_sep>/src/main/java/tech/turl/community/util/HostHolder.java
package tech.turl.community.util;

import org.springframework.stereotype.Component;
import tech.turl.community.entity.User;

/**
 * 持有用户信息，用于代替session对象
 *
 * @author zhengguohuang
 * @date 2021/3/15
 */
@Component
public class HostHolder {
    private ThreadLocal<User> users = new ThreadLocal<>();

    public void setUser(User user) {
        users.set(user);
    }

    public User getUser() {
        return users.get();
    }

    public void clear() {
        users.remove();
    }
}
<file_sep>/src/test/java/tech/turl/community/WkTests.java
package tech.turl.community;

import java.io.IOException;

/**
 * @author zhengguohuang
 * @date 2021/03/29
 */
public class WkTests {
    public static void main(String[] args) throws IOException {
        String cmd =
                "e:/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://baidu.com d:/work/data/wk-images/1.png";
        Runtime.getRuntime().exec(cmd);
        System.out.println("ok");
    }
}
<file_sep>/src/main/java/tech/turl/community/config/ThreadPoolConfig.java
package tech.turl.community.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

/**
 * @author zhengguohuang
 * @date 2021/03/29
 */
@Configuration
@EnableScheduling
@EnableAsync
public class ThreadPoolConfig {}
<file_sep>/src/test/java/tech/turl/community/TestLoginTicketMapper.java
package tech.turl.community;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import tech.turl.community.dao.LoginTicketMapper;
import tech.turl.community.entity.LoginTicket;

import java.util.Date;

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class TestLoginTicketMapper {

    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Test
    public void testInsert() {
        LoginTicket loginTicket = new LoginTicket();
        loginTicket.setUserId(1);
        loginTicket.setStatus(0);
        loginTicket.setTicket("dddd");
        loginTicket.setExpired(new Date());
        int ret = loginTicketMapper.insertLoginTicket(loginTicket);
        System.out.println(ret);
    }

    @Test
    public void testSelectByTicket() {
        LoginTicket loginTicket = loginTicketMapper.selectByTicket("dddd");
        System.out.println(loginTicket);
    }

    @Test
    public void testUpdateStatus() {
        int ret = loginTicketMapper.updateStatus("dddd", 1);
        System.out.println(ret);
    }

}
<file_sep>/src/main/java/tech/turl/community/controller/AlphaController.java
package tech.turl.community.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import tech.turl.community.dao.UserMapper;
import tech.turl.community.entity.Demo;
import tech.turl.community.service.AlphaService;
import tech.turl.community.service.DiscussPostService;
import tech.turl.community.util.CommunityUtil;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** @author zhengguohuang */
@Controller
@RequestMapping("/alpha")
public class AlphaController {
    @Autowired AlphaService alphaService;

    @Autowired private UserMapper userMapper;

    @Autowired private DiscussPostService discussPostService;

    @RequestMapping("/hello")
    @ResponseBody
    public String sayHello() {
        return "Hello Spring Boot.";
    }

    @RequestMapping("/data")
    @ResponseBody
    public String getData() {
        return alphaService.find();
    }

    /**
     * 带参数GET请求 /students?current=1&limit=20 只能处理GET请求
     *
     * @param current
     * @param limit
     * @return
     */
    @GetMapping("/students")
    @ResponseBody
    public String getStudents(
            @RequestParam(name = "current", required = false, defaultValue = "1") int current,
            @RequestParam(name = "limit", required = false, defaultValue = "20") int limit) {
        System.out.println(current);
        System.out.println(limit);
        return "some students";
    }

    /**
     * 路径中带参数 /student/123
     *
     * @param id
     * @return
     */
    @RequestMapping(path = "/student/{id}", method = RequestMethod.GET)
    @ResponseBody
    public String getStudent(@PathVariable("id") int id) {
        System.out.println(id);
        return "success";
    }

    /**
     * 响应表单请求
     *
     * @param name
     * @param age
     * @return
     */
    @RequestMapping(path = "/student", method = RequestMethod.POST)
    @ResponseBody
    public String saveStudent(String name, int age) {
        System.out.println(name);
        System.out.println(age);
        return "success";
    }

    /**
     * 响应HTML数据
     *
     * @return
     */
    @RequestMapping(path = "/teacher", method = RequestMethod.GET)
    public ModelAndView getTeacher() {
        ModelAndView mav = new ModelAndView();
        mav.addObject("name", "张三");
        mav.addObject("age", 30);
        mav.setViewName("demo/view");
        return mav;
    }

    @RequestMapping(path = "/school", method = RequestMethod.GET)
    public String getSchool(Model model) {
        model.addAttribute("name", "Beijing");
        model.addAttribute("age", 23);
        return "demo/view";
    }

    /**
     * 响应JSON数据(异步请求) Java对象 -> JSON字符串 -> JS对象
     *
     * @return
     */
    @RequestMapping(path = "/emp", method = RequestMethod.GET)
    @ResponseBody
    public Map<String, Object> getEmp() {
        Map<String, Object> emp = new HashMap<>(16);
        emp.put("name", "张三");
        emp.put("age", 23);
        emp.put("salary", 3000);
        return emp;
    }

    @RequestMapping(path = "/emps", method = RequestMethod.GET)
    @ResponseBody
    public List<Map<String, Object>> getEmps() {
        List<Map<String, Object>> list = new ArrayList<>();
        Map<String, Object> emp = new HashMap<>(16);
        emp.put("name", "张三");
        emp.put("age", 23);
        emp.put("salary", 3000);
        list.add(emp);
        emp = new HashMap<>(16);
        emp.put("name", "李四");
        emp.put("age", 24);
        emp.put("salary", 4000);
        list.add(emp);
        emp = new HashMap<>(16);
        emp.put("name", "John");
        emp.put("age", 23);
        emp.put("salary", 3000);
        list.add(emp);
        return list;
    }

    @GetMapping("/cookie/set")
    @ResponseBody
    public String setCookie(HttpServletResponse response) {
        // 创建cookie
        Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
        // 设置cookie生效范围
        cookie.setPath("/alpha");
        // 设置cookie的生存时间
        cookie.setMaxAge(60 * 10);
        // 发送cookie
        response.addCookie(cookie);
        return "set cookie";
    }

    @GetMapping("/cookie/get")
    @ResponseBody
    public String getCookie(@CookieValue("code") String code) {
        System.out.println(code);
        return "get cookie";
    }

    @GetMapping("/session/set")
    @ResponseBody
    public String setSession(HttpSession session) {
        session.setAttribute("id", 1);
        session.setAttribute("name", "hzg");

        return "set session";
    }

    @GetMapping("/session/get")
    @ResponseBody
    public String getSession(HttpSession session) {
        System.out.println(session.getAttribute("id"));
        System.out.println(session.getAttribute("name"));
        return "get cookie";
    }

    @GetMapping("/demo")
    @ResponseBody
    public Demo demo() {
        Demo demo = new Demo();
        demo.setAge(1);
        demo.setId(1);
        demo.setName("jjj");

        return demo;
    }

    @PostMapping("/ajax")
    @ResponseBody
    public String testAjax(String name, int age) {
        System.out.println(name);
        System.out.println(age);
        return CommunityUtil.getJSONString(0, "操作成功！");
    }
}
<file_sep>/src/test/java/tech/turl/community/CaffeineTests.java
package tech.turl.community;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import tech.turl.community.entity.DiscussPost;
import tech.turl.community.service.DiscussPostService;

import java.util.Date;

/**
 * @author zhengguohuang
 * @date 2021/03/30
 */
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class CaffeineTests {
    @Autowired private DiscussPostService postService;

    @Test
    public void initDataForTest() {
        for (int i = 0; i < 100000; i++) {
            DiscussPost post = new DiscussPost();
            post.setUserId(111);
            post.setTitle("互联网秋招暖春计划");
            post.setContent("xxxxxxxxxxxxxxxxxxxxx互联网XXX");
            post.setCreateTime(new Date());
            post.setScore(Math.random() * 2000);
            postService.addDiscussPost(post);
        }
    }

    @Test
    public void testCache() {
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
        System.out.println(postService.findDiscussPosts(0, 0, 10, 0));
    }
}
<file_sep>/src/main/java/tech/turl/community/service/ElasticsearchService.java
package tech.turl.community.service;

import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import tech.turl.community.dao.elasticsearch.DiscussPostRepository;
import tech.turl.community.entity.DiscussPost;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhengguohuang
 * @date 2021/03/26
 */
@Service
public class ElasticsearchService {
    @Autowired private DiscussPostRepository discussPostRepository;

    @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate;

    /**
     * 插入或修改帖子
     *
     * @param post
     */
    public void saveDiscussPost(DiscussPost post) {
        discussPostRepository.save(post);
    }

    /**
     * 删除帖子
     *
     * @param id
     */
    public void deleteDiscussPost(int id) {
        discussPostRepository.deleteById(id);
    }

    /**
     * 搜索并高亮分页显示
     *
     * @param keyword
     * @param current
     * @param limit
     * @return
     */
    public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
        Pageable pageable = PageRequest.of(current, limit);
        NativeSearchQuery searchQuery =
                new NativeSearchQueryBuilder()
                        .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
                        .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
                        .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
                        .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                        .withPageable(pageable)
                        .withHighlightFields(
                                new HighlightBuilder.Field("title")
                                        .preTags("<em>")
                                        .postTags("</em>"),
                                new HighlightBuilder.Field("content")
                                        .preTags("<em>")
                                        .postTags("</em>"))
                        .build();

        SearchHits<DiscussPost> searchHits =
                elasticsearchRestTemplate.search(searchQuery, DiscussPost.class);
        if (searchHits.getTotalHits() <= 0) {
            return null;
        }
        List<DiscussPost> list = new ArrayList<>();
        for (SearchHit<DiscussPost> hit : searchHits) {
            DiscussPost content = hit.getContent();
            DiscussPost post = new DiscussPost();
            BeanUtils.copyProperties(content, post);
            List<String> list1 = hit.getHighlightFields().get("title");
            if (list1 != null) {
                post.setTitle(list1.get(0));
            }
            List<String> list2 = hit.getHighlightFields().get("content");
            if (list2 != null) {
                post.setContent(list2.get(0));
            }
            list.add(post);
        }
        return new PageImpl<>(list, pageable, searchHits.getTotalHits());
    }
}
<file_sep>/src/main/java/tech/turl/community/dao/LoginTicketMapper.java
package tech.turl.community.dao;

import org.apache.ibatis.annotations.*;
import tech.turl.community.entity.LoginTicket;

/**
 * @author zhengguohuang
 * @date 2021/3/15
 */
@Mapper
@Deprecated
public interface LoginTicketMapper {
    /**
     * 插入login_ticket表
     *
     * @param loginTicket
     * @return
     */
    @Insert({
            "insert into login_ticket(user_id, ticket, status, expired) ",
            "values(#{userId}, #{ticket}, #{status}, #{expired})"
    })
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertLoginTicket(LoginTicket loginTicket);

    /**
     * 通过Ticket查找记录
     *
     * @param ticket
     * @return
     */
    @Select({
            "select id, user_id, ticket, status, expired ",
            "from login_ticket where ticket=#{ticket}"
    })
    LoginTicket selectByTicket(String ticket);

    /**
     * 更新login_ticket的status字段
     *
     * @param ticket
     * @param status
     * @return
     */
    @Update({
            "update login_ticket set status = #{status} where ticket=#{ticket} "
    })
    int updateStatus(@Param("ticket") String ticket, @Param("status") int status);

}
