欢迎光临
我们一直在努力

伙伴匹配系统复习

伙伴匹配系统

此系统是基于上一个用户中心系统而创作的

一、功能点:

1.根据标签查询用户:

在设计的时候考虑了两种实现方法

第一种:设置一个用户标签关联表,将其作为中间表来将用户和标签进行关联(优点是思维简单,但是它查询多影响我们系统的效率)

第二种:在用户的属性中增加一个String的属性tags,这个属性存放了这个用户的很多个标签

像这样[“java”,“c++”,“python”]

我们选择第二种实现方法。

第二种的实现也具有两个方案!!!

第一种为sql查询(实现简单)

在进行我们sql查询的时候先判断参数是否为空,然后在将标签取出来进行设置查询条件,再进行查询。最后将查询到的结果进行脱敏然后再返回用户列表

public List<User> searchUserByTags(List<String> tagsNameList) {
if (CollectionUtils.isEmpty(tagsNameList)){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"标签列表为空"); }
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
for (String tagName : tagsNameList){
queryWrapper.like("tags","%"+tagName+"%");
}
List<User> userList = userMapper.selectList(queryWrapper);
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}

第二种为内存查询(灵活)

这里引用了Gson的配置进行反序列化的操作,要在pom文件中配置一下。

这里的filter方法是若返回值为true则保留否则清除。

public List<User> searchUserByTags(List<String> tagsNameList) {
// 1.先查询所有用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<User> userList = userMapper.selectList(queryWrapper);
Gson gson = new Gson();

//2.在内存中判断是否包含要求的标签(内存查询)

return userList.stream().filter(user -> {
String tagStr = user.getTags();
if (StringUtils.isEmpty(tagStr)){
return false;
}
Set<String> temTagNameList = gson.fromJson(tagStr,new TypeToken<Set<String>>(){}.getType());
temTagNameSet = Optional.ofNullable(temTagNameSet).orElse(new HashSet<>());
for (String tagName : tagsNameList) {
if (!temTagNameList.contains(tagName)){
return false;
}
}
return true;

}).map(this::getSafetyUser).collect(Collectors.toList());

}
}

(1)代码解释:

Set temTagNameList = gson.fromJson(tagStr,new TypeToken<Set>(){}.getType());

为什么用set?

这行代码的核心是调用 Gson对象的 fromJson方法,它需要两个关键信息来完成转换:

数据来源:tagStr,即待解析的 JSON 字符串。它应该是一个字符串数组的格式,例如 ["标签1", "标签2", "标签3"]。

目标类型:new TypeToken<Set<String>>(){}.getType(),这部分代码的作用是明确告诉 Gson,你希望将 JSON 字符串转换成一个 Set<String>对象,而不仅仅是原始的 Set。

(2)代码解释: temTagNameSet = Optional.ofNullable(temTagNameSet).orElse(new HashSet<>());

这里用到了Optional的方法来判断是否为空,若为空则取orElse中的数据,这样的好处是减少if的使用。

理解 TypeToken 的作用

你可能会问,为什么不直接写成 Set.class呢?这主要是为了应对 Java 泛型的类型擦除 机制。

简单来说,Java 在编译后,泛型信息(如 <String>)会被擦除,程序在运行时只知道这是个 Set,但不知道它原本包含的是 String还是其他类型。Gson 如果只拿到 Set.class,就无法正确地将 JSON 数组中的元素转换为 String,可能导致类型转换异常 。

TypeToken的巧妙之处在于,它通过创建一个匿名内部类(new TypeToken<Set<String>>(){}),并将具体的泛型类型 Set<String>作为其泛型参数。这样,在运行时就可以通过反射从类的定义信息中获取到完整的泛型类型,从而精确地指导 Gson 进行反序列化 。

完整工作流程

你可以这样理解这行代码的执行过程:

  • 准备:Gson 根据 TypeToken提供的信息,确认目标类型是 Set<String>。

  • 解析:Gson 读取 tagStrJSON 字符串。

  • 转换:将 JSON 数组中的每个元素转换为 String类型。

  • 构建:将这些 String对象添加到一个新的 Set<String>集合中。

  • 返回:最终,将这个构建好的 Set<String>集合赋值给等号左边的 temTagNameList变量。

  • 如何去选择使用这两种方法呢?

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

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

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

    2.后端整合Swagger和Knife4j接口文档

    Knife4j:

    1.直接引入Knife4j

    <!knif4j>
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4jopenapi3jakartaspringbootstarter</artifactId>
    <version>4.4.0</version>
    </dependency>

    2.配置knife4j的yml属性

    ​ packages-to-scan:这个属性设置为我们启动类所在的包。

    # springdocopenapi项目配置
    springdoc:
    swaggerui:
    path: /swaggerui.html
    tagssorter: alpha
    operationssorter: alpha
    apidocs:
    path: /v3/apidocs
    enabled: true
    groupconfigs:
    group: 'default'
    pathstomatch: '/**'
    packagestoscan: com.example.partnermatch
    # knife4j的增强配置,不需要增强可以不配
    knife4j:
    enable: true
    setting:
    language: zh_cn

    效果如图所示:

    在这里插入图片描述

    3.用户登录功能

    (1)Session共享后端分布式登录

    进行本机分布式登录测试:

    1.先用maven的package的命令将项目打包成一个jar包

    2.再去terminal终端输入命令 java -jar jar包名称 –server.port=端口号

    在这里插入图片描述

    这样就可以在本地启动两个服务进行测试了。

    用户的身份如何确认?

    在点击登陆时,前端会给后端传一个Cookie,这个Cookie中带有一个属性叫做JSESSIONID如下图所示:

    在这里插入图片描述

    在之后的请求中就会根据这个JSESSIONID来确认用户身份。

    在这里插入图片描述

    但是如果只是这样的话,在测试的时候就会发现问题所在,当一个用户登陆后,在另一个后端进行获取用户信息的时候信息获取不到,这是因为这两个服务器的后端没有共享用户数据,我们可以设置一个Redis的缓存来进行存储我们的用户信息。

    在这里插入图片描述

    Redis实现

    1.添加redis配置和spring-session和redis的整合的配置,这个整合的配置可以自动将session存储到redis中。

    <! Redis >
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>springbootstarterdataredis</artifactId>
    <version>2.6.4</version>
    </dependency>
    <! Springsession与redis的整合 >
    <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>springsessiondataredis</artifactId>
    <version>2.6.3</version>
    </dependency>

    2.修改spring-session存储配置spring.session.redis

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

    SpringBoot3版本的配置如下所示:

    spring:
    datasource:
    driverclassname: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/partnermatch?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    session:
    timeout: 86400
    redis:
    flushmode: on_save
    namespace: spring:session
    data:
    redis:
    host: localhost
    port: 6379
    auth: root
    database: 10

    设置后我们再测试一下

    8080端口请求成功:

    在这里插入图片描述

    在这里插入图片描述

    Redis中有它的session缓存了:

    在这里插入图片描述

    8081端口也请求成功:

    在这里插入图片描述 在这里插入图片描述

    3.批量导入数据

    1.用可视化界面:适合一次性全部导入,数据量可控

    2.写程序:利用for循环,注意分批,而不是一次性全部导入(可以用接口来控制)要保证可控、幂等,要注意线上环境和测试环境是有区别的

    3.执行sql语句,适用于小数据量

    编写一次性任务:

    for循环插入数据的问题:

    1.建立和释放数据库连接

    2.for循环是绝对线性的

    可是使用批量插入提高效率:

    @SpringBootTest
    public class insertUsersTest {
    @Resource
    private UserMapper userMapper;
    @Resource
    private UserService userService;
    @Test
    public void doInsertUsers() {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    final int INSERT_NUM = 1000;
    List<User> userList = new ArrayList<>();
    for (int i = 0; i < INSERT_NUM; i++) {
    User user = new User();
    user.setUsername("假用户");
    user.setUserAccount("fakeuser");
    user.setAvatarUrl("");
    user.setGender(0);
    user.setUserPassword("12345678");
    user.setPhone("1234567890");
    user.setEmail("123165166");
    user.setUserStatus(0);
    user.setUserRole(0);
    user.setPlanetCode("65555");
    // userMapper.insert(user);

    }
    userService.saveBatch(userList,100);
    stopWatch.stop();
    System.out.println(stopWatch.getTotalTimeMillis());
    }
    }

    利用并发操作进行插入:

    @SpringBootTest
    public class insertUsersTest {
    @Resource
    private UserMapper userMapper;
    @Resource
    private UserService userService;
    //CPU密集型,分配的核心线程数 = cpu -1
    //IO密集型,分配核心线程数可以打于CPU核数

    private ExecutorService executorService = new ThreadPoolExecutor(60,100,10000, TimeUnit.MINUTES,new ArrayBlockingQueue<>(10000));//创建一个我们的自己的线程池
    @Test
    public void doInsertUsers() {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    List<CompletableFuture<Void>> futureList = new ArrayList<>();//创建一个List集合来存放任务。
    int j=0;
    for (int i = 0; i < 40; i++){
    List<User> userList = Collections.synchronizedList(new ArrayList<>());//在进行多线程操作的时候用到的要是线程安全的集合。
    while(true){
    j++;
    User user = new User();
    user.setUsername("假用户");
    user.setUserAccount("fakeuser");
    user.setAvatarUrl("");
    user.setGender(0);
    user.setUserPassword("12345678");
    user.setPhone("1234567890");
    user.setEmail("123165166");
    user.setUserStatus(0);
    user.setUserRole(0);
    user.setPlanetCode("65555");
    userList.add(user);
    if (j % 10000 == 0){
    break;
    }
    }

    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() ->{
    System.out.println("threadname"+Thread.currentThread().getName());
    userService.saveBatch(userList,10000);
    },executorService);
    futureList.add(completableFuture);
    }
    CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).join();

    stopWatch.stop();
    System.out.println(stopWatch.getTotalTimeMillis());
    }
    }

    代码解释:

    自定义线程池:

    private ExecutorService executorService = new ThreadPoolExecutor(60,100,10000, TimeUnit.MINUTES,new ArrayBlockingQueue<>(10000));

    参数当前设定的值含义和作用
    核心线程数 (corePoolSize) 40 线程池中长期保持存活的线程数量,即使它们处于空闲状态。
    最大线程数 (maximumPoolSize) 1000 线程池允许创建的线程总数上限(包含核心线程)。
    空闲线程存活时间 (keepAliveTime) 10000 当线程数超过核心线程数时,多余的空闲线程等待新任务的最长时间。
    时间单位 (Unit) TimeUnit.MINUTES 上述存活时间的单位,这里是分钟。
    任务队列 (workQueue) new ArrayBlockingQueue<>(10000) 用于存放等待执行任务的有界阻塞队列,容量为10000。

    // 异步执行
    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("threadName: " + Thread.currentThread().getName());
    userService.saveBatch(userList, batchSize);
    }, executorService);
    futureList.add(future);
    CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).join();

    创建异步任务:

    CompletableFuture.runAsync(() -> { … }, executorService)是核心方法。它接受两个参数:

    第一个参数(Runnable 任务): 这是一个Lambda表达式,定义了需要在后台线程中执行的具体逻辑。在这个例子里,它做了两件事:

    System.out.println("threadname"+Thread.currentThread().getName());: 打印出当前执行该任务的线程名称,便于调试和监控。

    userService.saveBatch(userList,10000);: 调用MyBatis-Plus提供的批量保存方法,将 userList中的用户数据插入数据库,10000表示每批处理的数据量。

    第二个参数(线程池): executorService是你自定义的线程池。它负责管理和调度执行任务的线程。使用自定义线程池而非默认的 ForkJoinPool.commonPool()是推荐做法,可以避免资源竞争,尤其适合IO密集型任务(如数据库操作),并能更好地控制并发行为。

    管理任务状态

    CompletableFuture<Void> completableFuture = …: runAsync方法会立即返回一个 CompletableFuture<Void>对象。这个对象就像是这个异步任务的**“提货单”**,你可以通过它来查询任务是否完成、等待任务完成,或者链式添加任务完成后的回调操作。因为 runAsync本身不返回业务结果,所以其泛型是 Void。

    futureList.add(completableFuture): 将这张“提货单”添加到一个 futureList集合中。这样做的目的是为了后续使用 CompletableFuture.allOf(futureList.toArray(new CompletableFuture[] {})).join()来等待所有添加到列表中的异步任务都执行完毕,然后再进行下一步操作(比如停止计时器)。

    CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).join();:

    格式转换:futureList.toArray(new CompletableFuture[]{})这部分的目的是进行简单的类型转换。因为你之前将所有的 CompletableFuture对象都添加到了一个 List<CompletableFuture<Void>> futureList中,而 allOf方法需要的是数组参数 (CompletableFuture<?>… cfs),所以这里通过 toArray方法将列表转换为数组 。

    组合监控:CompletableFuture.allOf(…)是核心。它不会开始新的任务,而是创建一个“总开关”式的 CompletableFuture<Void>对象。这个“总开关”的状态取决于传入的所有任务:只有当所有任务都完成(无论是正常结束还是异常结束)时,它自身才会标记为完成 。需要注意的是,它本身不包含各个任务的具体结果 。

    阻塞等待:.join()方法会阻塞当前线程(在你的代码中,就是执行测试的主线程),直到上一步创建的“总开关”Future 完成。一旦所有任务完成,join()方法就会返回,程序继续执行下一行代码(即停止计时并打印耗时)。这与 get()方法功能类似,但 join()不会抛出受检异常,写法更简洁 。

    4.缓存和分布式缓存

    缓存的实现:

    Redis(分布式缓存)

    memcached(分布式)

    Etcd(云原生架构的一个分布式存储)


    进程的缓存:

    ehcache(单机)

    本地缓存(JAVA内存Map)

    Caffeine(JAVA内存缓存,高性能)

    Google Guava

    Redis:

    NoSOL数据库

    实际上更像是一个key-value存储系统(它存储的是键值对)

    JAVA里的实现方式:
    Spring Data Redis:

    Spring Data: 通用的数据访问框架,定义了一组增删改查的接口

    mysql、redis、jpa

    1)引入redis:

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>springbootstarterdataredis</artifactId>
    </dependency>

  • 配置redis
  • data:
    redis:
    host: localhost
    port: 6379
    auth: root
    database: 10

    3)配置序列化

    因为redis存储数据的实际上是先将数据进行序列化然后再进行的存储,其使用的是原生的jdk序列化器,可能会出现存储不正确(乱码)的现象,那么就需要我们去配置一个redis配置类,值也想序列化的话需要对应的类实现Serializable.

    @Configuration
    public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    redisTemplate.setKeySerializer(RedisSerializer.string());
    return redisTemplate;
    }
    }

    设计缓存key:

    @GetMapping("/recommend")
    public BaseResult<Page<User>> recommendUsers(int pageSize, int pageNum, String username, HttpServletRequest request) {
    User user = userservice.getLoginUser(request);
    //如果有缓存,直接读取缓存
    String redisKey = String.format("partner:user:recommend:%s",user.getId());
    Page<User> userPage = (Page<User>) redisTemplate.opsForValue().get(redisKey);
    if (userPage != null) {
    return Result.success(userPage);
    }
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    Page<User> userList = userservice.page(new Page<>(pageNum, pageSize), queryWrapper);
    try {
    redisTemplate.opsForValue().set(redisKey,userList,10000);
    } catch (Exception e) {
    log.error("redis存储查询数据出错",e);
    }
    return Result.success(userList);
    }

    redis要设置过期时间,因为其内存不是无限增加的

    缓存预热:

    解决问题:

    第一个用户访问还是很慢(第一个人的加入),也能一定程度上保护数据库。

    缺点:

    1.增加开发成本(需要额外的开发)

    2.预热的时机或时间如果错了,可能缓存的数据不对

    3.需要占用空间

    定时任务实现:

    1.Spring Scheduler(spring boot 默认整合了)

    2.Quartz(独立于Spring存在的定时任务框架)

    3.XXL-job之类的分布式任务调度平台(界面+sdk)

    第一种方式:

    1.主类开启@EnableScheduling

    2.给要定时执行的方法添加@Scheduled注解

    @Component
    public class PreCacheJob {

    @Resource
    private UserService userservice;
    @Qualifier("redisTemplate")
    @Autowired
    private RedisTemplate redisTemplate;
    private List<Long> mainUserList = Arrays.asList(1L);
    //每天执行,缓存预热推荐用户
    @Scheduled(cron = "0 0/1 * * * ? ")
    public void doCacheRecommendUser(){

    for (Long userId : mainUserList) {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    Page<User> userList = userservice.page(new Page<>(1, 20), queryWrapper);
    String redisKey = String.format("partner:user:recommend:%s",userId);
    try {
    redisTemplate.opsForValue().set(redisKey,userList,10000);
    } catch (Exception e) {
    log.error("redis存储查询数据出错",e);
    }

    }

    }

    }

    对比:

    1.无过多定制化要求,可以使用Spring Data Redis, 最方便

    2.如果用的不是Spring,并且追求简单,无过高的性能要求,可以用Jedis+Jedis Pool

    3.如果用的不是Spring,并且追求高性能、高定制化,可以用Lettuce,支持异步、连接池


    4.如果项目是分布式的,需要一些分布式的特性,建议用redisson

    为什么需要控制定时任务的执行?

    ​ 1.浪费资源

    ​ 2.脏数据,比如重复插入

    如何控制定时任务再同一时间内只有一个服务器能运行

    1.分离定时任务程序和主程序,只在1个服务器运行定时任务。成本太大

    2.写死配置,每个服务器都执行定时任务,但是只有ip符合配置的服务器才真实执行业务逻辑,剩下的直接返回,成本最低;但是我们的ip可能是不固定的,把ip写太死了

    3.动态配置,配置可以很轻松,很方便更新(代码无需重启),但是只有ip符合配置的服务器才真实执行业务逻辑。

    ​ 数据库

    ​ redis

    ​ 配置中心(Nacos\\Apolio\\Spring Cloud config)

    4.分布式锁,只有抢到锁的服务器才能执行业务逻辑。好处:不用手动配置,多少个服务器都一样。坏处:增加成本。

    在资源有限的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源

    java实现锁:synchronized 关键字、并法包的类

    问题:只对单个JVM有效

    分布式锁

    抢锁机制:

    如何保证同一时间只有一个服务器能抢到锁?

    核心思想:先来的人先把数据改成自己的标识(服务器ip),后来的人发现标识已经存在,就抢锁失败,继续等待。

    先来的人执行方法结束,把标识清空,其他人继续抢锁。

    MySQL数据库:select for update行级锁(最简单),或者乐观锁

    redis实现:内存数据库,读写速度快。支持setnx、lua脚本,方便我们实现分布式锁

    setnx:set if not exists如果不存在,才会设置;只有设置成功才会返回true,否则返回false。

    注意事项:

    1)用完锁要释放

    2)锁要加过期时间

    3)如果方法执行时间过长,锁提前过期

    ​ 会导致问题:

    ​ 1.连锁效应:会释放掉别人的锁

    ​ 2.这样还是会存在多个方法同时执行的情况。

    解决方法:续期

    boolean end = false;

    new Thread(() -> {
    if (!end)}{
    续期
    })

    end = true;

    4)释放锁的时候,有可能先判断出是自己的锁,但是这时锁过期了,最后释放了别人的锁

    解决方案:Redis+lua脚本保证操作原子性

    boolean end = false;

    new Thread(() -> {
    if (!end)}{
    续期
    })

    end = true;

    5)Redis如果是集群用红锁

    Redisson实现分布式锁

    两种引入方式:

    1.spring boot starter引入

    2.直接引入:https://redisson.pro/docs/getting-started/

    先创建配置类:

    @Configuration
    @ConfigurationProperties(prefix = "spring.data.redis")
    @Data

    public class RedissonConfig {
    private String host;
    private String port;
    private String auth;

    @Bean
    public RedissonClient redissonClient() {
    Config config = new Config();
    System.out.println(host+":"+port);
    //1.创建配置
    String redisAddress = String.format("redis://%s:%s", host, port);
    config.useSingleServer().setAddress(redisAddress).setDatabase(3);
    //创建实例
    // Sync and Async API
    RedissonClient redisson = Redisson.create(config);

    return redisson;

    }

    }

    注意:

    ​ 1.waitTime设置为0,只能抢一次,抢不到就放弃

    ​ 2.注意释放锁要写在finally中(因为写在try的话要是过程中报错了,锁就释放不掉了)

    Redisson 看门狗机制:

    这个是开启一个监听线程进行续期

    原理:

    ​ 1.监听当前线程,如果方法还没执行完,重置redis锁的过期时间

    ​ 2.如果线程挂掉(注意debug模式也会被当作服务器宕机),则不会续期。

    @Scheduled(cron = "0 0/1 * * * ? ")
    public void doCacheRecommendUser() {
    RLock lock = redissonClient.getLock("lsw: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> userList = userservice.page(new Page<>(1, 20), queryWrapper);
    String redisKey = String.format("partner:user:recommend:%s", userId);
    try {
    redisTemplate.opsForValue().set(redisKey, userList, 10000);
    } catch (Exception e) {
    log.error("redis存储查询数据出错", e);
    }

    }
    }
    } catch (InterruptedException e) {
    throw new RuntimeException(e);
    }finally {
    //只能释放自己的锁
    if (lock.isHeldByCurrentThread()) {
    System.out.println("unlock"+ Thread.currentThread().getId());
    lock.unlock();
    }
    }

    }

    5.匹配算法

    使用编辑距离算法:

    public 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];
    }

    匹配算法的思路是,将登录用户标签,和所有用户标签进行匹配,取匹配度最高的用户

    代码解释:

    1.用户输入要匹配的人数

    2.根据关键信息来获取所有用户

    3.获取当前用户的标签,并将其转成String类型的数组。

    4.遍历获得的用户数组,计算其相似度然后放到集合中

    5.最后来取值进行比较

    @Override
    public List<User> matchUsers(long num, User loginUser) {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.select("id","tags");//避免不必要的数据传输
    queryWrapper.isNotNull("tags");
    List<User> userList = this.list();
    String tags = loginUser.getTags();
    Gson gson = new Gson();
    List<String> tagList = gson.fromJson(tags,new TypeToken<List<String>>(){}.getType());

    //用户列表的下表=>相似度
    List<Pair<User,Long>> list = new ArrayList<>();

    for (int i = 0; i < userList.size(); i++) {
    User user = userList.get(i);
    String userTags = user.getTags();
    if (StringUtils.isEmpty(userTags) || user.getId() == loginUser.getId()){
    continue;
    }
    List<String> userTagList = gson.fromJson(userTags,new TypeToken<List<String>>(){}.getType());
    //计算分数
    long distance = AlgorithmUtils.minDistance(tagList,userTagList);
    list.add(new Pair<>(user,distance));
    }
    //按编辑距离由小到大排序
    List<Pair<User,Long>> topUserPairList = list.stream().sorted((a,b) -> (int)(a.getB() b.getB())).limit(num).collect(Collectors.toList());
    //这里的比较是设置了比较规则,结果为负的话,距离是小的,说明更加相似,排在前面
    List<Long> userIdList = topUserPairList.stream().map(pair -> pair.getA().getId()).collect(Collectors.toList());
    QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
    userQueryWrapper.in("id",userIdList);
    Map<Long,List<User>> userIdUserListMap = this.list(userQueryWrapper).stream().map(user -> this.getSafetyUser(user)).collect(Collectors.groupingBy(User::getId));
    List<User> finalUserList = new ArrayList<>();
    for (Long userId : userIdList) {
    finalUserList.add(userIdUserListMap.get(userId).get(0));
    }
    return finalUserList;
    }

    在这里插入图片描述

    最后:

    Map<Long,List<User>> userIdUserListMap = this.list(userQueryWrapper).stream().map(user -> this.getSafetyUser(user)).collect(Collectors.groupingBy(User::getId));
    List<User> finalUserList = new ArrayList<>();
    for (Long userId : userIdList) {
    finalUserList.add(userIdUserListMap.get(userId).get(0));
    }

    这里是解决userQueryWrapper.in(“id”,userIdList);有可能会打乱我们原本的id用户顺序,因为我们原本的id是按相似度排好的,所以我们现在可以用一个map来对其进行分组,获取的是id:对象的格式,然后再遍历我们最初的id集合,根据id来从map的对应关系中取出对象,这样就可以返回与原来顺序一样的集合了。

    赞(0)
    未经允许不得转载:171主机测评 » 伙伴匹配系统复习
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址