匹配系统开发要点

封装请求响应结果和异常处理

响应数据封装

接口响应给前端的数据统一风格

{
  "code": 200,
  "data": true,
  "msg": "ok",
  "description": null
}

代码实现:使用泛型传入返回 data 里的数据类型

@Data
public class BaseResponse<T> implements Serializable {

    private static final long serialVersionUID = -1053725934018053785L;
    private int code;
    private T data;
    private String msg;
    private  String description;


    public BaseResponse(int code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }
    public BaseResponse(int code, T data) {
        this(code,data,"");
    }

    public BaseResponse(int code, T data, String msg, String description) {
        this.code = code;
        this.data = data;
        this.msg = msg;
        this.description = description;
    }

    public BaseResponse() {
    }
    public BaseResponse(ErrorCode errorCode){
        this(errorCode.getCode(),null,errorCode.getMsg(),errorCode.getDescription());
    }
}

封装自己的状态码枚举类型

package com.jin.partner.common;

/**
 * @author fantasy
 */

public enum ErrorCode {
  SUCCESS(200,"ok",""),
  PARAMS_ERROR(40000,"params error","参数错误"),
  NULL_ERROR(40001,"null error","请求数据为空"),
  NO_LOGIN(40100,"no login","没有登陆"),
  NO_AUTH(40101,"no auth","无权限"),
  SYSTEM_ERROR(5000,"SYSTEM ERROR","系统内部错误")
  ;
  private final int code;
  private final String msg;
  private final String description;

  ErrorCode(int code, String msg, String description) {
    this.code = code;
    this.msg = msg;
    this.description = description;
  }

  public int getCode() {
    return code;
  }

  public String getMsg() {
    return msg;
  }

  public String getDescription() {
    return description;
  }
}

封装返回工具类

public class ResultUtil {
  /**
     * 成功 200 ok
     * @param data
     * @param <T>
     * @return
     */
  public static <T> BaseResponse<T> success(T data) {
    return new BaseResponse<>(200, data, "ok");
  }
  /**
     * 失败 data:null
     * @param errorCode
     * @param <T>
     * @return
     */
  public static <T> BaseResponse<T> error(ErrorCode errorCode) {
    return new BaseResponse<>(errorCode);
  }

  public static BaseResponse error(int code, String msg, String dsc) {
    return new BaseResponse<>(code, null, msg, dsc);
  }

  public static BaseResponse error(ErrorCode errorCode, String msg, String dsc) {
    return new BaseResponse<>(errorCode.getCode(), null, msg, dsc);
  }

  public static BaseResponse error(ErrorCode errorCode, String dsc) {
    return new BaseResponse<>(errorCode.getCode(), null, errorCode.getMsg(), dsc);
  }
}

使用例子

// 返回类型:BaseResponse<UserVo> 
// 使用工具类返回  ResultUtil.success(safetyUser);
@GetMapping("/current")
public BaseResponse<UserVo> getCurrentUser(HttpServletRequest request){
  User user = userService.getLoginUser(request);
  if(user==null){
    throw new BusinessException(ErrorCode.NULL_ERROR);
  }
  Long userId = user.getId();
  User byIdUser = userService.getById(userId);
  UserVo safetyUser = userService.getSafetyUser(byIdUser);
  return ResultUtil.success(safetyUser);
}

全局异常响应封装

如果我们代码出问题了,抛出异常,不能直接把系统的异常信息都响应给前端(不安全)

所以封装自定义异常,明确什么是异常,再捕获代码的异常 集中处理,处理后再响应给前端请求

封装异常

public class BusinessException extends RuntimeException {
  private static final long serialVersionUID = -8228470992635963684L;
  private final int code;
  private final String description;
  public BusinessException(String message, int code, String description) {
    super(message);
    this.code = code;
    this.description = description;
  }
  public BusinessException(ErrorCode errorCode) {
    super(errorCode.getMsg());
    this.code = errorCode.getCode();
    this.description = errorCode.getDescription();
  }
  public BusinessException(ErrorCode errorCode ,String description) {
    super(errorCode.getMsg());
    this.code = errorCode.getCode();
    this.description = description;
  }

  public int getCode() {
    return code;
  }

  public String getDescription() {
    return description;
  }
}
异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
  @ExceptionHandler(BusinessException.class)
  public BaseResponse businessExceptionHandler(BusinessException e){
    log.error("BusinessException: "+ e.getMessage(),e);
    // 发生异常;利用上面封装的响应工具类响应给前端
    return ResultUtil.error(e.getCode(),e.getMessage(), e.getDescription());
  }
  @ExceptionHandler(RuntimeException.class)
  public BaseResponse runtimeExceptionHandler(BusinessException e){
    log.error("runtimeException: "+ e);
    return ResultUtil.error(ErrorCode.SYSTEM_ERROR,e.getMessage(), "");
  }
}

查询数据库数据

查询用户标签

sql 处理

实现简单,sql 拆分查询处理

如果参数可以分析,根据用户的参数去选择查询方式,比如标签数

/**
* sql 模糊查询标签
* @param tagNameList 用户标签
* @return 用户列表
*/
private List<User> searchTagsSql(List<String> tagNameList) {
  // 查询
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  //拼接tag
  // like '%Java%' and like '%Python%'
  for (String tagList : tagNameList) {
    queryWrapper = queryWrapper.like("tag", tagList);
  }
  List<User> users = userMapper.selectList(queryWrapper);
  List<User> userList = users.stream().collect(Collectors.toList());
  return userList;
}

内存查询

灵活通过并发优化

如果参数不可分析,并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁

还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag

/**
* 搜索标签内存处理
*
* @param tagNameList 标记名称列表
* @return {@link List}<{@link User}>
*/
private Page<User> searchTagsMemory(List<String> tagNameList){
  // 查询所有用户
  QueryWrapper<User> wrapper = new QueryWrapper<User>().ne("tag", "[]");
  List<User> userList = userMapper.selectList(wrapper);
  // 内存判断
  Gson gson = new Gson();
  Page<User> page = new Page<>(pageListRequest.getPageNum(), pageListRequest.getPageSize());

  Page<User> userPage = page.setRecords(userList.stream().filter(user -> {
    String tag = user.getTag();
    // json 反系列化
    Set<String> tempTagNameSet = gson.fromJson(tag, new TypeToken<Set<String>>() {
    }.getType());
    // 判空
    tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
    for (String tagName : tagNameList) {
      // 判断查询出来的标签用户是否拥有
      // noneMath 遍历流中的元素,一旦发现有任何一个元素满足指定的条件,就会立即返回 false
      if (tempTagNameSet
          .stream()
          .noneMatch(tempTagName -> tempTagName.equalsIgnoreCase(tagName))) {
        return false;
      }
    }
    return true;
  }).collect(Collectors.toList()));
  return userPage;
}

分布式 session

如果我们在服务器 A 登陆后,后面请求发送到服务器 B,会请求不到之前存储的 session 域的用户信息

原因:我们把用户信息存储到服务器 A 内存上

解决方法:不要把用户信息存在单个服务器内存上,而是存在公共存储上

使用 redis 公共存储用户信息

Redis(基于内存的 K / V 数据库)

springboot 使用 redis

  1. 引用 redis 依赖
<!-- session-data-redis -->
  <!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>2.6.3</version>
 </dependency>
  <!-- session-data-redis:spring-session 和 redis 的整合-->
  <!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>2.6.3</version>
</dependency>
  1. 修改 session 存储位置

    默认是 none,表示存储在单台服务器

    store-type: redis,表示从 redis 读写 session

    spring:
      session:
        store-type: redis
      redis:
        port: 6379
        host: localhost
        database: 1
        password: 123456 #默认没密码
  2. domain注意点:

    比如两个域名:

    aaa.yupi.com

    bbb.yupi.com

    如果要共享 cookie,可以种一个更高层的公共域名,比如 yupi.com

server:
    session:
      cookie:
       domain: yupi.com

查看是否配置成功:开两个服务,8080,8081 在8080登陆,8081是否可以获取用户信息,查看redis是否多了redis 的数据

数据缓存 Redis

缓存可使用:

  • Redis(分布式缓存)
  • memcached(分布式)
  • Etcd(云原生架构的一个分布式存储,存储配置,扩容能力)
  • ehcache(单机)
  • 本地缓存(Java 内存 Map)
  • Caffeine(Java 内存缓存,高性能)
  • Google Guava

实现方法

  • Spring-Data-Redis

  • Spring Data:

  • 通用的数据访问框架,定义了一组 增删改查 的接口mysql、redis、jpa

  • Jedis:

  • (独立于 Spring 操作 Redis 的 Java 客户端,要配合 Jedis Pool 使用)

  • Lettuce

  • 高阶的操作 Redis 的 Java 客户端

  • 异步、连接池

  • Redisson

    • 分布式操作 Redis 的 Java 客户端,让你像在使用本地的集合一样操作 Redis(分布式 Redis 数据网格)

使用 spring-data-redis

依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
  <version>2.6.4</version>
</dependency>

配置文件

spring:
  redis:
    port: 6379
    host: localhost
    database: 1
    password: 123456 #默认没密码

自定义序列化

@Configuration
public class RedisTemplateConfig {
  @Bean
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    //创建RedisTemplate对象
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    //设置连接工厂
    redisTemplate.setConnectionFactory(connectionFactory);
    // 自定义序列化 key
    redisTemplate.setKeySerializer(RedisSerializer.string());
    return redisTemplate;
  }
}

缓存数据

缓存数据 key :例如 Jin.user.recommend;不能与其他冲突

redis 内存不能无限增加,一定要设置过期时间

@Resource
private RedisTemplate redisTemplate;


@GetMapping("/recommend")
public BaseResponse<IPage<UserVo>> recommendUserList(PageListRequest pageListRequest, HttpServletRequest request){
  User loginUser = userService.getLoginUser(request);
  // 缓存 key
  String redisKey = String.format("jin:user:recommend:%s",loginUser.getId());
  ValueOperations valueOperations = redisTemplate.opsForValue();
  //如果有缓存,直接读取
  Page<UserVo> userPage = (Page<UserVo>) valueOperations.get(redisKey);
  if (userPage != null){
    return ResultUtil.success(userPage);
  }
  // 无缓存查数据库
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Page<User> pageInfo = new Page<>(pageListRequest.getPageNum(), pageListRequest.getPageSize());
  Page<User> userList = userService.page(pageInfo, queryWrapper);
  // 数据脱敏
  IPage<UserVo> userVoIPage = userList.convert(e -> {
    UserVo userVo = new UserVo();
    BeanUtils.copyProperties(e, userVo);
    return userVo;
  });
  //写缓存,10分钟过期
  try {
    valueOperations.set(redisKey,userVoIPage,600000, TimeUnit.MILLISECONDS);
  } catch (Exception e) {
    e.printStackTrace();
  }
  return ResultUtil.success(userVoIPage);
}

定时任务缓存数据

用定时任务,每天刷新所有用户的推荐列表

注意点:

  1. 缓存预热的意义(新增少、总用户多)
  2. 缓存的空间不能太大,要预留给其他缓存空间
  3. 缓存数据的周期(此处每天一次)

使用 Spring Scheduler(spring boot 默认整合了)

使用方式:

  1. 主类开启 @EnableScheduling
  2. 给要定时执行的方法添加 @Scheduling 注解,指定 cron 表达式或者执行频率
@Component
@Slf4j
public class PreCacheJob {
  @Resource
  private UserService userService;

  @Resource
  private RedisTemplate<String, Object> redisTemplate;

  // 重点用户
  private List<Long> mainUserList = Arrays.asList(1L);

  // 每天执行,预热推荐用户
  @Scheduled(cron = "0 12 1 * * *")   //自己设置时间测试
  public void doCacheRecommendUser() {
    //查数据库
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    Page<User> userPage = userService.page(new Page<>(1,10),queryWrapper);

    String redisKey = String.format("jin:user:recommend:%s",mainUserList);
    ValueOperations valueOperations = redisTemplate.opsForValue();
    // 数据脱敏
    IPage<UserVo> userVoIPage = userPage.convert(e -> {
      UserVo userVo = new UserVo();
      BeanUtils.copyProperties(e, userVo);
      return userVo;
    });
    //写缓存,10分钟过期
    try {
      valueOperations.set(redisKey,userVoIPage,600000, TimeUnit.MILLISECONDS);
    } catch (Exception e){
      log.error("redis set key error",e);
    }
  }
}

cron 表达式

redis 实现分布式锁

使用 Redisson 是一个 java 操作 Redis 的客户端

直接引用

添加依赖

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.19.1</version>
</dependency>  

配置文件

/**
 * Redisson 配置
 */
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
  private String host;
  private String port;
  @Bean
  public RedissonClient redissonClient() {
    // 1. 创建配置
    Config config = new Config();
    String redisAddress = String.format("redis://%s:%s", host, port);
    //  使用单个Redis,没有开集群 useClusterServers()  设置地址和使用库
    config.useSingleServer().setAddress(redisAddress).setDatabase(1);
    // 2. 创建实例
    RedissonClient redisson = Redisson.create(config);
    return redisson;
  }
}

简单使用

@SpringBootTest
public class RedissonTest {
  @Resource
  private RedissonClient redissonClient;
  @Test
  void test() {
    // list,数据存在本地 JVM 内存中
    List<String> list = new ArrayList<>();
    list.add("yupi");
    System.out.println("list:" + list.get(0));
    list.remove(0);

    // 数据存在 redis 的内存中
    RList<String> rList = redissonClient.getList("test-list");
    rList.add("yupi");
    System.out.println("rlist:" + rList.get(0));
    rList.remove(0);
    // map
    Map<String, Integer> map = new HashMap<>();
    map.put("yupi", 10);
    map.get("yupi");
    RMap<Object, Object> map1 = redissonClient.getMap("test-map");
    // set
    // stack
  }
}

分布式锁实现

实现方法:锁+定时任务

waitTime 设置为 0,只抢一次,抢不到就放弃

@Component
@Slf4j
public class PreCacheJob {

  @Resource
  private UserService userService;

  @Resource
  private RedisTemplate<String, Object> redisTemplate;

  @Resource
  private RedissonClient redissonClient;
  // 重点用户
  private List<Long> mainUserList = Arrays.asList(1L);

  // 每天执行,预热推荐用户
  @Scheduled(cron = "0 12 1 * * *")   //自己设置时间测试
  public void doCacheRecommendUser() {
    RLock lock = redissonClient.getLock("jin:precachejob:docache:lock");
    try {
      // 只有一个线程能获取到锁
      if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
        // System.out.println("getLock: " + Thread.currentThread().getId());
        for (Long userId : mainUserList) {
          //查数据库
          QueryWrapper<User> queryWrapper = new QueryWrapper<>();
          Page<User> userPage = userService.page(new Page<>(1, 10), queryWrapper);
          String redisKey = String.format("shayu:user:recommend:%s", mainUserList);
          ValueOperations valueOperations = redisTemplate.opsForValue();
          // 数据脱敏
          IPage<UserVo> userVoIPage = userPage.convert(e -> {
            UserVo userVo = new UserVo();
            BeanUtils.copyProperties(e, userVo);
            return userVo;
          });
          //写缓存,30s过期
          try {
            valueOperations.set(redisKey, userVoIPage, 30000, TimeUnit.MILLISECONDS);
          } catch (Exception e) {
            log.error("redis set key error", e);
          }
        }
      }
    } catch (InterruptedException e) {
      log.error("doCacheRecommendUser error", e);
    } finally {
      // 只能释放自己的锁
      if (lock.isHeldByCurrentThread()) {
        System.out.println("unLock: " + Thread.currentThread().getId());
        lock.unlock();
      }
    }

  }
}

看门狗机制(逾期问题)

开一个监听线程,如果方法还没执行完,就帮你重置 redis 锁的过期时间。

原理:

  1. 监听当前线程,默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
  2. 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期

参考连接:https://blog.csdn.net/qq_26222859/article/details/79645203

匹配算法

匹配方法:根据用户标签去匹配相似度,共同标签

  1. 找到有共同标签最多的用户(TopN)
  2. 共同标签越多,分数越高,越排在前面
  3. 如果没有匹配的用户,随机推荐几个(降级方案)

编辑距离算法https://blog.csdn.net/DBC_121/article/details/104198838

最小编辑距离:字符串 1 通过最少多少次增删改字符的操作可以变成字符串 2

余弦相似度算法:https://blog.csdn.net/m0_55613022/article/details/125683937(如果需要带权重计算,比如学什么方向最重要,性别相对次要)

最短距离算法

编辑距离算法(用于计算最相似的两组标签)

原理:https://blog.csdn.net/DBC_121/article/details/104198838

package com.yupi.usercenter.utlis;

import java.util.List;
import java.util.Objects;

/**
* 算法工具类
*
* @author yupi
*/
public class AlgorithmUtils {

  /**
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
* @param tagList1
* @param tagList2
* @return
*/
  public static int minDistance(List<String> tagList1, List<String> tagList2) {
    int n = tagList1.size();
    int m = tagList2.size();

    if (n * m == 0) {
      return n + m;
    }

    int[][] d = new int[n + 1][m + 1];
    for (int i = 0; i < n + 1; i++) {
      d[i][0] = i;
    }

    for (int j = 0; j < m + 1; j++) {
      d[0][j] = j;
    }

    for (int i = 1; i < n + 1; i++) {
      for (int j = 1; j < m + 1; j++) {
        int left = d[i - 1][j] + 1;
        int down = d[i][j - 1] + 1;
        int left_down = d[i - 1][j - 1];
        if (!Objects.equals(tagList1.get(i - 1), tagList2.get(j - 1))) {
          left_down += 1;
        }
        d[i][j] = Math.min(left, Math.min(down, left_down));
      }
    }
    return d[n][m];
  }

  /**
* 编辑距离算法(用于计算最相似的两个字符串)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
* @param word1
* @param word2
* @return
*/
  public static int minDistance(String word1, String word2) {
    int n = word1.length();
    int m = word2.length();

    if (n * m == 0) {
      return n + m;
    }

    int[][] d = new int[n + 1][m + 1];
    for (int i = 0; i < n + 1; i++) {
      d[i][0] = i;
    }

    for (int j = 0; j < m + 1; j++) {
      d[0][j] = j;
    }

    for (int i = 1; i < n + 1; i++) {
      for (int j = 1; j < m + 1; j++) {
        int left = d[i - 1][j] + 1;
        int down = d[i][j - 1] + 1;
        int left_down = d[i - 1][j - 1];
        if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
          left_down += 1;
        }
        d[i][j] = Math.min(left, Math.min(down, left_down));
      }
    }
    return d[n][m];
  }
}

实现代码

public IPage<UserVo> getMatchUser(PageListRequest pageInfo, User loginUser) {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.isNotNull("tag");
  queryWrapper.ne("tag", "[]");
  queryWrapper.select("id", "tag");
  // 获取当前标签
  String loginUserTag = loginUser.getTag();
  Gson gson = new Gson();
  List<String> tagList = gson.fromJson(loginUserTag, new TypeToken<List<String>>() {}.getType());
  // 计算所有用户的相似度,并将用户及其相似度保存在一个 Map 中
  // 获取用户处理
  HashMap<User, Integer> userScoreMap = new HashMap<>();
  this.list(queryWrapper).stream()
    .filter(user -> !user.getId().equals(loginUser.getId())) // 排除当前登录用户
    .forEach(user -> userScoreMap.put(user, AlgorithmUtils.minDistance(gson.fromJson(user.getTag(), new TypeToken<List<String>>() {
    }.getType()), tagList)));
  // 对 Map 中的用户进行排序
  List<User> sortedUserList = sortMap(userScoreMap);
  List<Long> validUserIdData = sortedUserList.stream().map(User::getId).collect(Collectors.toList());
  // 分页查询用户
  Page<User> page = new Page<>(pageInfo.getPageNum(), pageInfo.getPageSize());
  QueryWrapper<User> userQueryWrapper = new QueryWrapper<User>();
  // 根据上面 validUserIdData 数组查询相关 id 的用户,
  // 在数组 validUserIdData 中的顺序进行升序排序
  // `FIELD()` 函数,该根据 `id` 值在 `validUserIdData` 数组中的顺序进行升序排列
  userQueryWrapper
    .in("id", validUserIdData)
    .orderByAsc("FIELD(id, " + StringUtils.join(validUserIdData, ",") + ")");

  Page<User> userPageList = this.page(page, userQueryWrapper);
  if (userPageList == null) {
    throw new BusinessException(ErrorCode.NULL_ERROR, "查询失败");
  }
  return userPageList.convert(this::getSafetyUser);
}

/**
     * map 排序
     *
     * @param map 需要排序的map 集合
     * @return 排好序的list
     */
public static List<User> sortMap(Map<User, Integer> map) {
  //利用Map的entrySet方法,转化为list进行排序
  List<Map.Entry<User, Integer>> entryList = new ArrayList<>(map.entrySet());
  //利用Collections的sort方法对list排序
  entryList.sort(Comparator.comparingInt(Map.Entry::getValue));

  List<User> userList = new ArrayList<>();
  for (Map.Entry<User, Integer> e : entryList) {
    userList.add(e.getKey());
  }
  return userList;
}

实用工具

json 解析

序列化:java对象转成 json

反序列化:把 json 转为 java 对象

  1. gson(google )
  2. fastjson alibaba(阿里 出品,快,但是漏洞太多)
  3. jackson
  4. kryo

gson 使用方法

参考连接:https://juejin.cn/post/7106058260163067934

接口文档

Swagger + Knife4j :https://doc.xiaominfo.com/knife4j/documentation/get_start.html

导入依赖

<!-- knife4j 接口文档 -->
<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-spring-boot-starter</artifactId>
  <version>2.0.7</version>
</dependency>

创建配置文件 swaggerConfig

@Configuration
@EnableSwagger2WebMvc
@Profile({"dev", "test"})   //版本控制访问
public class SwaggerConfig {
    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                // 这里一定要标注你控制器的位置
                .apis(RequestHandlerSelectors.basePackage("com.jin.usercenter.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    /**
     * api 信息
     * @return
     */
    // 基本信息设置
    private ApiInfo apiInfo() {
        Contact contact = new Contact(
                "jin", // 作者姓名
                "blog.luckjin.top", // 作者网址
                "1768606916@qq.com"); // 作者邮箱
        return new ApiInfoBuilder()
                .title("匹配系统接口文档") // 标题
                .description("匹配系统接口文档") // 描述
                .termsOfServiceUrl("") // 跳转连接
                .version("1.0") // 版本
                .contact(contact)
                .build();
    }
}

springboot配置文件

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

接口的描述配置

在 controller 方法上添加:

  • @Api(value = “/team”, tags = {“队伍模块”})
  • @ApiImplicitParam(name = “name”,value = “姓名”,required = true)
  • @ApiOperation(value = “向客人问好”) 等注解来自定义生成的接口描述信息

推荐使用idea 插件 swagger 自动生成接口描述的注解

https://plugins.jetbrains.com/plugin/14130-swagger-tools

例如下面的代码

/**
*  队伍列表分页查询
* @param teamQuery 查询条件
* @return 返回列表
*/
@ApiOperation(value = "队伍列表分页查询", notes = "队伍列表分页查询", httpMethod = "GET")
@GetMapping("/list/page")
public BaseResponse<Page<TeamUserVO>> getTeamListPage(TeamQuery teamQuery,HttpServletRequest request){
  if(ObjectUtils.isEmpty(teamQuery)){
    throw new BusinessException(ErrorCode.PARAMS_ERROR);
  }
  User loginUser = userService.getLoginUser(request);
  Page<TeamUserVO> teamUserVOS = teamService.teamList(teamQuery,loginUser);

  return ResultUtil.success(teamUserVOS);
}

esay excel

java 处理 excel 表格的数据

官方文档:https://easyexcel.opensource.alibaba.com/index.html

redis 管理工具 quick_redis

https://gitee.com/quick123official/quick_redis_blog