北尘 發表於 2025-8-20 10:55:00

伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05

<h1 id="伙伴匹配系统移动端-h5-网站app-风格基于spring-boot-后端--vue3---05">伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05</h1>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202508/3084824-20250820105453695-1894799762.gif"></p>
<p><strong>项目地址:</strong></p>
<ul>
<li>Github:https://github.com/China-Rainbow-sea/yupao</li>
<li>Gitee:https://gitee.com/Rainbow--Sea/yupao</li>
</ul>
<p>@</p><div class="toc"><div class="toc-container-header">目录</div><ul><li>伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05</li><li>系统(接口)设计<ul><li>创建队伍</li><li>查询队伍列表</li><li>修改队伍信息</li><li>用户可以加入队伍</li><li>用户可以退出队伍</li><li>队长可以解散队伍</li><li>获取当前用户创建的队伍:包括:私有的,公开的,加密的,只要是自己创建的</li><li>获取当前用户加入的队伍,包括:私有的,公开的,加密的,只要是自己加入的</li></ul></li><li>推荐算法:随机匹配</li><li>前端简单的功能调整</li><li>最后:</li></ul></div><p></p>
<h1 id="系统接口设计">系统(接口)设计</h1>
<h2 id="创建队伍">创建队伍</h2>
<p>用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 PO</p>
<blockquote>
<p>队长、剩余的人数</p>
<p>聊天?</p>
<p>公开 或 private 或加密</p>
<p>信息流中不展示已过期的队伍</p>
</blockquote>
<ol>
<li>请求参数是否为空?</li>
<li>是否登录,未登录不允许创建</li>
<li>校验信息
<ol>
<li>队伍人数&gt;1且&lt;=20</li>
<li>队伍标题&lt;= 20</li>
<li>描述 &lt;= 512</li>
<li>status 是否公开(int)不传默认为(公开)</li>
<li>如果 status 是加密状态,一定要有密码,且密码&lt;= 32</li>
<li>超时时间》当前时间</li>
<li>校验用户最多创建5个队伍</li>
</ol>
</li>
<li>插入队伍信息到队伍表</li>
<li>插入用户=&gt;队伍关系到关系表</li>
</ol>
<p>加入不同的队伍,抢的不是同一个资源,就不需要,抢锁了。可以将锁的范围缩小一些。</p>
<p>用户和队伍 Id,都要锁一下。</p>
<ul>
<li>同一个用户,同时刻只允许你加入一个队伍,一个用户不可以一次性加入 10 个队伍。</li>
<li>sysnchronized 是(加在对象上的),String.valueOf(id).intern 表示根据 Id 是,每次生成的是同一个对象的地址,不会新 new 一个 String 对象,新 new 就是不同的对象,不同的锁了。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202508/3084824-20250820105451545-859929634.png"></p>
<pre><code class="language-java">

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.DeleteRequest;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.request.TeamAddRequest;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/team")
@Api("接口文档的一个别名处理定义 TeamController ")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"})// 配置前端访问路径的放行,可以配置多个
@Slf4j
public class TeamController {

    @Resource
    private UserService userService;

    @Resource
    private TeamService teamService;

    @Resource
    private UserTeamService userTeamService;


    /**
   * 插入 team 队伍,添加队伍
   *
   * @param teamAddRequest
   * @return teamId
   */
    @PostMapping("/add")
    public BaseResponse&lt;Long&gt; addTeam(@RequestBody TeamAddRequest teamAddRequest, HttpServletRequest request) {
      if (teamAddRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }

      User loginUser = userService.getLoginUser(request);
      Team team = new Team();
      BeanUtils.copyProperties(teamAddRequest, team);
      long teamId = teamService.addTeam(team, loginUser);

      return ResultUtils.success(teamId);
    }

}
</code></pre>
<pre><code class="language-java">
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.mapper.TeamMapper;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.model.vo.UserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
*
*/
@Service
public class TeamServiceImpl extends ServiceImpl&lt;TeamMapper, Team&gt;
      implements TeamService {

    @Resource
    private UserTeamService userTeamService;

    @Resource
    private UserService userService;

    @Resource
    private RedissonClient redissonClient;


    /***
   * 添加队伍
   * @param team 队伍
   * @param loginUser User 用户
   * @return
   */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public long addTeam(Team team, User loginUser) {
      // 1. 请求参数是否为空?
      if (team == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      // 2. 是否登录,未登录不允许创建
      if (loginUser == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN);
      }
      final long userId = loginUser.getId();
      // 3. 校验信息
      //   1. 队伍人数 &gt; 1 且 &lt;= 20
      int maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
      if (maxNum &lt; 1 || maxNum &gt; 20) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍人数不满足要求");
      }
      //   2. 队伍标题 &lt;= 20
      String name = team.getName();
      if (StringUtils.isBlank(name) || name.length() &gt; 20) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍标题不满足要求");
      }
      //   3. 描述 &lt;= 512
      String description = team.getDescription();
      if (StringUtils.isNotBlank(description) &amp;&amp; description.length() &gt; 512) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍描述过长");
      }
      //   4. status 是否公开(int)不传默认为 0(公开)
      int status = Optional.ofNullable(team.getStatus()).orElse(0);
      TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
      if (statusEnum == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍状态不满足要求");
      }
      //   5. 如果 status 是加密状态,一定要有密码,且密码 &lt;= 32
      String password = team.getPassword();
      if (TeamStatusEnum.SECRET.equals(statusEnum)) {
            if (StringUtils.isBlank(password) || password.length() &gt; 32) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码设置不正确");
            }
      }
      // 6. 超时时间 &gt; 当前时间
      Date expireTime = team.getExpireTime();
      if (new Date().after(expireTime)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "超时时间 &gt; 当前时间");
      }
      // 7. 校验用户最多创建 5 个队伍
      // todo 有 bug,可能同时创建 100 个队伍
      QueryWrapper&lt;Team&gt; queryWrapper = new QueryWrapper&lt;&gt;();
      queryWrapper.eq("userId", userId);
      long hasTeamNum = this.count(queryWrapper);
      if (hasTeamNum &gt;= 5) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建 5 个队伍");
      }
      // 8. 插入队伍信息到队伍表
      team.setId(null);
      team.setUserId(userId);
      boolean result = this.save(team);
      Long teamId = team.getId();
      if (!result || teamId == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
      }
      // 9. 插入用户=&gt; 队伍关系到关系表
      UserTeam userTeam = new UserTeam();
      userTeam.setUserId(userId);
      userTeam.setTeamId(teamId);
      userTeam.setJoinTime(new Date());
      result = userTeamService.save(userTeam);
      if (!result) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
      }
      return teamId;
    }


}
</code></pre>
<p><strong>补充:事务注解</strong></p>
<pre><code class="language-java">@Transaction(rollbackFor = Exception.class)// 在方法上添加上注解,启动事务控制
void public main() {
   
}
</code></pre>
<h2 id="查询队伍列表">查询队伍列表</h2>
<p>分页展示队伍列表,根据名称、最大人数等搜索队伍PO,信息流中不展示已过期的队伍。</p>
<ol>
<li>从请求参数中取出队伍名称等查询条件,如果存在则作为查询条件</li>
<li>需要登录,才能查询</li>
<li>不展示已过期的队伍 (根据过期时间筛选)</li>
<li>可以通过某个关键词同时对名称和描述查询</li>
<li>只有管理员才能查看加密还有非公开的房间</li>
<li>关联查询已加入队伍的用户信息</li>
<li>关联查询已加入队伍的用户信息(可能会很耗费性能,建议大家用自己写SQL的方式实现)</li>
</ol>
<p>自己写 SQL</p>
<pre><code class="language-java">// 1. 自己写 SQL
// 查询队伍和创建人的信息
// select * from team t left join user u on t.userId = u.id
// 查询队伍和已加入队伍成员的信息
// select *
// from team t
//         left join user_team ut on t.id = ut.teamId
//         left join user u on ut.userId = u.id;

</code></pre>
<pre><code class="language-java">

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.DeleteRequest;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.request.TeamAddRequest;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/team")
@Api("接口文档的一个别名处理定义 TeamController ")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"})// 配置前端访问路径的放行,可以配置多个
@Slf4j
public class TeamController {

    @Resource
    private UserService userService;

    @Resource
    private TeamService teamService;

    @Resource
    private UserTeamService userTeamService;

/**
   * 显示队伍列表,私有的不显示
   **/
    @GetMapping("/list")
    public BaseResponse&lt;List&lt;TeamUserVO&gt;&gt; listTeams(TeamQuery teamQuery, HttpServletRequest request) {
      if (teamQuery == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      boolean isAdmin = userService.isAdmin(request);
      // 1、查询队伍列表
      List&lt;TeamUserVO&gt; teamList = teamService.listTeams(teamQuery, isAdmin);
      final List&lt;Long&gt; teamIdList = teamList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
      // 2、判断当前用户是否已加入队伍
      QueryWrapper&lt;UserTeam&gt; userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
      try {
            User loginUser = userService.getLoginUser(request);
            userTeamQueryWrapper.eq("userId", loginUser.getId());
            userTeamQueryWrapper.in("teamId", teamIdList);
            List&lt;UserTeam&gt; userTeamList = userTeamService.list(userTeamQueryWrapper);
            // 已加入的队伍 id 集合
            Set&lt;Long&gt; hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
            teamList.forEach(team -&gt; {
                boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
                team.setHasJoin(hasJoin);
            });
      } catch (Exception e) {
      }
      // 3、查询已加入队伍的人数
      QueryWrapper&lt;UserTeam&gt; userTeamJoinQueryWrapper = new QueryWrapper&lt;&gt;();
      userTeamJoinQueryWrapper.in("teamId", teamIdList);
      List&lt;UserTeam&gt; userTeamList = userTeamService.list(userTeamJoinQueryWrapper);
      // 队伍 id =&gt; 加入这个队伍的用户列表
      Map&lt;Long, List&lt;UserTeam&gt;&gt; teamIdUserTeamList = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
      teamList.forEach(team -&gt; team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList&lt;&gt;()).size()));
      return ResultUtils.success(teamList);
    }
}
</code></pre>
<pre><code class="language-java">

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.mapper.TeamMapper;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.model.vo.UserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
*
*/
@Service
public class TeamServiceImpl extends ServiceImpl&lt;TeamMapper, Team&gt;
      implements TeamService {

    @Resource
    private UserTeamService userTeamService;

    @Resource
    private UserService userService;

    @Resource
    private RedissonClient redissonClient;



    /**
   * 搜索队伍
   * @param teamQuery
   * @param isAdmin
   * @return
   */
    @Override
    public List&lt;TeamUserVO&gt; listTeams(TeamQuery teamQuery, boolean isAdmin) {
      QueryWrapper&lt;Team&gt; queryWrapper = new QueryWrapper&lt;&gt;();
      // 组合查询条件
      if (teamQuery != null) {
            Long id = teamQuery.getId();
            if (id != null &amp;&amp; id &gt; 0) {
                queryWrapper.eq("id", id);
            }
            List&lt;Long&gt; idList = teamQuery.getIdList();
            if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(idList)) {
                queryWrapper.in("id", idList);
            }
            String searchText = teamQuery.getSearchText();
            if (StringUtils.isNotBlank(searchText)) {
                queryWrapper.and(qw -&gt; qw.like("name", searchText).or().like("description", searchText));
            }
            String name = teamQuery.getName();
            if (StringUtils.isNotBlank(name)) {
                queryWrapper.like("name", name);
            }
            String description = teamQuery.getDescription();
            if (StringUtils.isNotBlank(description)) {
                queryWrapper.like("description", description);
            }
            Integer maxNum = teamQuery.getMaxNum();
            // 查询最大人数相等的
            if (maxNum != null &amp;&amp; maxNum &gt; 0) {
                queryWrapper.eq("maxNum", maxNum);
            }
            Long userId = teamQuery.getUserId();
            // 根据创建人来查询
            if (userId != null &amp;&amp; userId &gt; 0) {
                queryWrapper.eq("userId", userId);
            }
            // 根据状态来查询
            Integer status = teamQuery.getStatus();
            TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
            if (statusEnum == null) {
                statusEnum = TeamStatusEnum.PUBLIC;
            }
            if (!isAdmin &amp;&amp; statusEnum.equals(TeamStatusEnum.PRIVATE)) {
                throw new BusinessException(ErrorCode.NO_AUTH);
            }
            queryWrapper.eq("status", statusEnum.getValue());
      }
      // 不展示已过期的队伍
      // expireTime is null or expireTime &gt; now()
      queryWrapper.and(qw -&gt; qw.gt("expireTime", new Date()).or().isNull("expireTime"));
      List&lt;Team&gt; teamList = this.list(queryWrapper);
      if (org.apache.commons.collections4.CollectionUtils.isEmpty(teamList)) {
            return new ArrayList&lt;&gt;();
      }
      List&lt;TeamUserVO&gt; teamUserVOList = new ArrayList&lt;&gt;();
      // 关联查询创建人的用户信息
      for (Team team : teamList) {
            Long userId = team.getUserId();
            if (userId == null) {
                continue;
            }
            User user = userService.getById(userId);
            TeamUserVO teamUserVO = new TeamUserVO();
            BeanUtils.copyProperties(team, teamUserVO);
            // 脱敏用户信息
            if (user != null) {
                UserVO userVO = new UserVO();
                BeanUtils.copyProperties(user, userVO);
                teamUserVO.setCreateUser(userVO);
            }
            teamUserVOList.add(teamUserVO);
      }
      return teamUserVOList;
    }
      }
}
</code></pre>
<h2 id="修改队伍信息">修改队伍信息</h2>
<ol>
<li>判断请求参数是否为空</li>
<li>查询队伍是否存在</li>
<li>只有管理员或者队伍的创建者可以修改</li>
<li>如果用户传入的新值和老值一致,就不用update了 (可自行实现,降低数据库使用次数)</li>
<li>如果队伍状态改为加密,必须要有密码</li>
<li>更新成功</li>
</ol>
<pre><code class="language-java">
    /**
   * 更新队伍内容
   *
   * @param teamUpdateRequest teamUpdateRequest 对象
   * @return Boolean 更新成功 true,否则 false
   */
    @PostMapping("/update")
    public BaseResponse&lt;Boolean&gt; updateTeam(@RequestBody TeamUpdateRequest teamUpdateRequest, HttpServletRequest request) {
      if (teamUpdateRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }

      User loginUser = userService.getLoginUser(request);
      boolean result = teamService.updateTeam(teamUpdateRequest, loginUser);


      if (!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新失败");
      }

      return ResultUtils.success(true);
    }
</code></pre>
<pre><code class="language-java">
    /**
   * 更新队伍
   * @param teamUpdateRequest
   * @param loginUser
   * @return
   */
    @Override
    public boolean updateTeam(TeamUpdateRequest teamUpdateRequest, User loginUser) {
      if (teamUpdateRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      Long id = teamUpdateRequest.getId();
      if (id == null || id &lt;= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      Team oldTeam = this.getById(id);
      if (oldTeam == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
      }
      // 只有管理员或者队伍的创建者可以修改
      if (!oldTeam.getUserId().equals(loginUser.getId()) &amp;&amp; !userService.isAdmin(loginUser)) {
            throw new BusinessException(ErrorCode.NO_AUTH);
      }
      TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(teamUpdateRequest.getStatus());
      if (statusEnum.equals(TeamStatusEnum.SECRET)) {
            if (StringUtils.isBlank(teamUpdateRequest.getPassword())) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "加密房间必须要设置密码");
            }
      }
      Team updateTeam = new Team();
      BeanUtils.copyProperties(teamUpdateRequest, updateTeam);
      return this.updateById(updateTeam);
    }
</code></pre>
<h2 id="用户可以加入队伍">用户可以加入队伍</h2>
<p>其他人、未满、未过期,允许加入多个队伍,但是要有个上限PO</p>
<ol>
<li>用户最多加入 5个队伍</li>
<li>队伍必须存在,只能加入未满、未过期的队伍</li>
<li>不能加入自己的队伍,不能重复加入已加入的队伍 (幂等性)</li>
<li>禁止加入私有的队伍</li>
<li>如果加入的队伍是加密的,必须密码匹配才可以</li>
<li>新增队伍-用户关联信息</li>
</ol>
<p>注意,一定要加上事务注解!!!!</p>
<pre><code class="language-java">
    /**
   * 加入队伍
   * @param teamJoinRequest
   * @param request
   * @return BaseResponse&lt;Boolean&gt;
   */
    @PostMapping("/join")
    public BaseResponse&lt;Boolean&gt; joinTeam(@RequestBody TeamJoinRequest teamJoinRequest, HttpServletRequest request) {
      if (teamJoinRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      User loginUser = userService.getLoginUser(request);
      boolean result = teamService.joinTeam(teamJoinRequest, loginUser);
      return ResultUtils.success(result);
    }
</code></pre>
<pre><code class="language-java">
    /**
   * 加入队伍
   *
   * @param teamJoinRequest
   * @param loginUser
   * @return boolean
   */
    @Override
    public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
      if (teamJoinRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      Long teamId = teamJoinRequest.getTeamId();
      Team team = getTeamById(teamId);
      Date expireTime = team.getExpireTime();
      if (expireTime != null &amp;&amp; expireTime.before(new Date())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
      }
      Integer status = team.getStatus();
      TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
      if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
      }
      String password = teamJoinRequest.getPassword();
      if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
            if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
            }
      }
      // 该用户已加入的队伍数量
      long userId = loginUser.getId();
      // 只有一个线程能获取到锁
      RLock lock = redissonClient.getLock("yupao:join_team");
      try {
            // 抢到锁并执行
            while (true) {
                if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
                  System.out.println("getLock: " + Thread.currentThread().getId());
                  QueryWrapper&lt;UserTeam&gt; userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
                  userTeamQueryWrapper.eq("userId", userId);
                  long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
                  if (hasJoinNum &gt; 5) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
                  }
                  // 不能重复加入已加入的队伍
                  userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
                  userTeamQueryWrapper.eq("userId", userId);
                  userTeamQueryWrapper.eq("teamId", teamId);
                  long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);
                  if (hasUserJoinTeam &gt; 0) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
                  }
                  // 已加入队伍的人数
                  long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
                  if (teamHasJoinNum &gt;= team.getMaxNum()) {
                        throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
                  }
                  // 修改队伍信息
                  UserTeam userTeam = new UserTeam();
                  userTeam.setUserId(userId);
                  userTeam.setTeamId(teamId);
                  userTeam.setJoinTime(new Date());
                  return userTeamService.save(userTeam);
                }
            }
      } catch (InterruptedException e) {
            log.error("doCacheRecommendUser error", e);
            return false;
      } finally {
            // 只能释放自己的锁
            if (lock.isHeldByCurrentThread()) {
                System.out.println("unLock: " + Thread.currentThread().getId());
                lock.unlock();
            }
      }
    }
</code></pre>
<h2 id="用户可以退出队伍">用户可以退出队伍</h2>
<p>请求参数:用户 ID</p>
<ol>
<li>校验队伍是否存在</li>
<li>校验我是否已加入队伍</li>
<li>如果队伍
<ol>
<li>只剩一人,队伍解散,(只剩一人,说明本身自己就是队长)</li>
<li>还有其他人</li>
<li>如果是队长退出队伍,权限转移给第二早加入的用户一一先来后到(只用取id最小的2 条数据)</li>
<li>非队长,自己退出队伍</li>
</ol>
</li>
</ol>
<pre><code class="language-java">
    /**
   * 退出队伍,
   * @param teamQuitRequest
   * @param request
   * @returnBaseResponse&lt;Boolean&gt;
   */
    @PostMapping("/quit")
    public BaseResponse&lt;Boolean&gt; quitTeam(@RequestBody TeamQuitRequest teamQuitRequest, HttpServletRequest request) {
      if (teamQuitRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      User loginUser = userService.getLoginUser(request);
      boolean result = teamService.quitTeam(teamQuitRequest, loginUser);

      return ResultUtils.success(result);
    }
</code></pre>
<pre><code class="language-java"> /**
   * 退出队伍
   * @param teamQuitRequest
   * @param loginUser
   * @return
   */
    @Override
    @Transactional(rollbackFor = Exception.class)// 添加上事务
    public boolean quitTeam(TeamQuitRequest teamQuitRequest, User loginUser) {
      if (teamQuitRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      Long teamId = teamQuitRequest.getTeamId();
      Team team = this.getById(teamId);
      //Team team = getTeamById(teamId);
      long userId = loginUser.getId();
      UserTeam queryUserTeam = new UserTeam();
      queryUserTeam.setTeamId(teamId);
      queryUserTeam.setUserId(userId);
      QueryWrapper&lt;UserTeam&gt; queryWrapper = new QueryWrapper&lt;&gt;(queryUserTeam);
      long count = userTeamService.count(queryWrapper);
      if (count == 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "未加入队伍");
      }
      long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
      // 队伍只剩一人,解散
      if (teamHasJoinNum == 1) {
            // 删除队伍
            this.removeById(teamId);
      } else {
            // 队伍还剩至少两人
            // 是队长
            if (team.getUserId() == userId) {
                // 把队伍转移给最早加入的用户
                // 1. 查询已加入队伍的所有用户和加入时间
                QueryWrapper&lt;UserTeam&gt; userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
                userTeamQueryWrapper.eq("teamId", teamId);
                userTeamQueryWrapper.last("order by id asc limit 2");
                List&lt;UserTeam&gt; userTeamList = userTeamService.list(userTeamQueryWrapper);
                if (CollectionUtils.isEmpty(userTeamList) || userTeamList.size() &lt;= 1) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR);
                }
                UserTeam nextUserTeam = userTeamList.get(1);
                Long nextTeamLeaderId = nextUserTeam.getUserId();
                // 更新当前队伍的队长
                Team updateTeam = new Team();
                updateTeam.setId(teamId);
                updateTeam.setUserId(nextTeamLeaderId);
                boolean result = this.updateById(updateTeam);
                if (!result) {
                  throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新队伍队长失败");
                }
            }
      }
      // 移除关系
      return userTeamService.remove(queryWrapper);
    }

</code></pre>
<h2 id="队长可以解散队伍">队长可以解散队伍</h2>
<p>请求参数:队伍 id</p>
<p>业务流程:</p>
<ol>
<li>校验请求参数</li>
<li>校验队伍是否存在</li>
<li>校验你是不是队伍的队长</li>
<li>移除所有加入队伍的关联信息</li>
<li>删除队伍</li>
</ol>
<pre><code class="language-java">
    /**
   * 删除队伍,移除队伍
   *
   * @param deleteRequest 队伍的 id
   * @return Boolean 移除成功 true,否则 false
   */
    @PostMapping("/delete")
    public BaseResponse&lt;Boolean&gt; deleteTeam(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
      if (deleteRequest == null || deleteRequest.getId() &lt;= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      long id = deleteRequest.getId();
      User loginUser = userService.getLoginUser(request);
      boolean result = teamService.deleteTeam(id, loginUser);
      if (!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除失败");
      }
      return ResultUtils.success(true);
    }
</code></pre>
<pre><code class="language-java">
    /**
   * 删除队伍
   * @param id 队伍中队长的 ID
   * @param loginUser
   * @return
   */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTeam(long id, User loginUser) {
      // 校验队伍是否存在
      Team team = this.getTeamById(id);
      Long teamId = team.getId();
      // 校验你是不是队伍的队长
      if (!team.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH, "不是队长,无权限删除");
      }

      // 移除所有加入队伍的关联信息
      QueryWrapper&lt;UserTeam&gt; userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
      userTeamQueryWrapper.eq("teamId", teamId);
      boolean result = userTeamService.remove(userTeamQueryWrapper);
      if(!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR,"删除队伍关联信息失败");
      }
      // 删除队伍
      return this.removeById(teamId);

    }

</code></pre>
<h2 id="获取当前用户创建的队伍包括私有的公开的加密的只要是自己创建的">获取当前用户创建的队伍:包括:私有的,公开的,加密的,只要是自己创建的</h2>
<pre><code class="language-java">
    /**
   * 获取当前用户创建的队伍:包括:私有的,公开的,加密的,只要是自己创建的
   * @param teamQuery
   * @param request
   * @return
   * @Author: RainbowSea
   */
    @GetMapping("/list/my/create")
    public BaseResponse&lt;List&lt;TeamUserVO&gt;&gt; listMyCreateTeams(TeamQuery teamQuery, HttpServletRequest request) {
      if (teamQuery == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }

      // 获取当前登录用户
      User loginUser = userService.getLoginUser(request);

      // 设置查询条件,查询当前用户创建的团队
      teamQuery.setUserId(loginUser.getId());

      // 构建查询条件
      QueryWrapper&lt;Team&gt; queryWrapper = new QueryWrapper&lt;&gt;();
      queryWrapper.eq("userId", teamQuery.getUserId()); // 根据 `userId` 字段进行查询

      // 查询所有队伍
      List&lt;Team&gt; teamList = teamService.list(queryWrapper);

      // 转换为 TeamUserVO 并加入必要字段
      List&lt;TeamUserVO&gt; teamUserVOList = teamList.stream()
                .map(team -&gt; {
                  TeamUserVO teamUserVO = new TeamUserVO();
                  teamUserVO.setId(team.getId());
                  teamUserVO.setName(team.getName());
                  teamUserVO.setDescription(team.getDescription());
                  teamUserVO.setUserId(team.getUserId());
                  teamUserVO.setCreateTime(team.getCreateTime());
                  teamUserVO.setUpdateTime(team.getUpdateTime());
                  teamUserVO.setMaxNum(team.getMaxNum());
                  teamUserVO.setExpireTime(team.getExpireTime());
                  teamUserVO.setDescription(team.getDescription());
                  teamUserVO.setStatus(team.getStatus());
                  // 其他字段的映射
                  return teamUserVO;
                }).collect(Collectors.toList());

      // 获取当前用户已加入的队伍
      List&lt;Long&gt; teamIdList = teamUserVOList.stream().map(TeamUserVO::getId).collect(Collectors.toList());

      QueryWrapper&lt;UserTeam&gt; userTeamQueryWrapper = new QueryWrapper&lt;&gt;();
      try {
            userTeamQueryWrapper.eq("userId", loginUser.getId());
            userTeamQueryWrapper.in("teamId", teamIdList);
            List&lt;UserTeam&gt; userTeamList = userTeamService.list(userTeamQueryWrapper);
            // 已加入的队伍 id 集合
            Set&lt;Long&gt; hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
            teamUserVOList.forEach(team -&gt; {
                boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
                team.setHasJoin(hasJoin);
            });
      } catch (Exception e) {
            // 处理异常情况,日志记录等
      }

      // 查询已加入队伍的人数
      QueryWrapper&lt;UserTeam&gt; userTeamJoinQueryWrapper = new QueryWrapper&lt;&gt;();
      userTeamJoinQueryWrapper.in("teamId", teamIdList);
      List&lt;UserTeam&gt; userTeamList = userTeamService.list(userTeamJoinQueryWrapper);

      // 队伍 id =&gt; 加入这个队伍的用户列表
      Map&lt;Long, List&lt;UserTeam&gt;&gt; teamIdUserTeamList = userTeamList.stream()
                .collect(Collectors.groupingBy(UserTeam::getTeamId));

      teamUserVOList.forEach(team -&gt;
                team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList&lt;&gt;()).size())
      );

      // 返回所有队伍信息
      return ResultUtils.success(teamUserVOList);
    }
</code></pre>
<h2 id="获取当前用户加入的队伍包括私有的公开的加密的只要是自己加入的">获取当前用户加入的队伍,包括:私有的,公开的,加密的,只要是自己加入的</h2>
<pre><code class="language-java">
    /**
   * 获取当前用户加入的队伍
   * 包括:私有的,公开的,加密的,只要是自己加入的
   * @Author: RainbowSea
   */
    @GetMapping("/list/my/join")
    public BaseResponse&lt;List&lt;TeamUserVO&gt;&gt; listMyJoinTeams(TeamQuery teamQuery, HttpServletRequest request) {
      if (teamQuery == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }

      // 获取当前登录用户
      User loginUser = userService.getLoginUser(request);

      // 先查询用户已加入的队伍
      QueryWrapper&lt;UserTeam&gt; queryWrapper = new QueryWrapper&lt;&gt;();
      queryWrapper.eq("userId", loginUser.getId());
      List&lt;UserTeam&gt; userTeamList = userTeamService.list(queryWrapper);

      // 提取不重复的队伍 ID
      Map&lt;Long, List&lt;UserTeam&gt;&gt; listMap = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
      ArrayList&lt;Long&gt; idList = new ArrayList&lt;&gt;(listMap.keySet());

      // 设置查询条件,只查询当前用户已加入的队伍
      teamQuery.setIdList(idList);

      // 构建查询条件
      QueryWrapper&lt;Team&gt; teamQueryWrapper = new QueryWrapper&lt;&gt;();
      if (teamQuery.getIdList() != null &amp;&amp; !teamQuery.getIdList().isEmpty()) {
            teamQueryWrapper.in("id", teamQuery.getIdList());// 根据队伍ID查询
      }

      // 如果有搜索条件,加入搜索条件
      if (teamQuery.getSearchText() != null &amp;&amp; !teamQuery.getSearchText().isEmpty()) {
            teamQueryWrapper.like("name", teamQuery.getSearchText())// 根据队伍名称进行模糊搜索
                  .or().like("description", teamQuery.getSearchText());// 或者根据队伍描述进行模糊搜索
      }

      // 分页处理
      teamQueryWrapper.last("LIMIT " + ((teamQuery.getPageNum() - 1) * teamQuery.getPageSize()) + ", " + teamQuery.getPageSize());

      // 查询队伍列表
      List&lt;Team&gt; teamList = teamService.list(teamQueryWrapper);

      // 转换为 TeamUserVO 并加入必要字段
      List&lt;TeamUserVO&gt; teamUserVOList = teamList.stream()
                .map(team -&gt; {
                  TeamUserVO teamUserVO = new TeamUserVO();
                  teamUserVO.setId(team.getId());
                  teamUserVO.setName(team.getName());
                  teamUserVO.setDescription(team.getDescription());
                  teamUserVO.setUserId(team.getUserId());
                  teamUserVO.setCreateTime(team.getCreateTime());
                  teamUserVO.setUpdateTime(team.getUpdateTime());
                  teamUserVO.setMaxNum(team.getMaxNum());
                  teamUserVO.setExpireTime(team.getExpireTime());
                  teamUserVO.setStatus(team.getStatus());
                  // 其他字段的映射
                  return teamUserVO;
                }).collect(Collectors.toList());

      // 获取当前用户已加入的队伍
      Set&lt;Long&gt; hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
      teamUserVOList.forEach(team -&gt; {
            boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
            team.setHasJoin(hasJoin);// 是否已经加入
      });

      // 查询已加入队伍的人数
      QueryWrapper&lt;UserTeam&gt; userTeamJoinQueryWrapper = new QueryWrapper&lt;&gt;();
      userTeamJoinQueryWrapper.in("teamId", idList);
      List&lt;UserTeam&gt; userTeamListForCount = userTeamService.list(userTeamJoinQueryWrapper);

      // 队伍 ID =&gt; 加入该队伍的用户列表
      Map&lt;Long, List&lt;UserTeam&gt;&gt; teamIdUserTeamList = userTeamListForCount.stream()
                .collect(Collectors.groupingBy(UserTeam::getTeamId));

      teamUserVOList.forEach(team -&gt;
                team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList&lt;&gt;()).size())// 设置已加入人数
      );

      // 返回所有队伍信息
      return ResultUtils.success(teamUserVOList);
    }

</code></pre>
<h1 id="推荐算法随机匹配">推荐算法:随机匹配</h1>
<p>需求背景:为了帮助大家更快的发现和自己新区相同的朋友。</p>
<p>思考:匹配 1 个还是匹配多个。</p>
<blockquote>
<p>答:匹配多个,并且按照匹配的相似度从高到低排序</p>
</blockquote>
<p>思考:怎么匹配?(根据什么匹配)</p>
<blockquote>
<p>答:这里我们根据用户 user 表当中设置的 tags 属性进行匹配。</p>
</blockquote>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202508/3084824-20250820105451503-1803853743.png"></p>
<p>还可以根据 user_team 匹配加入相同队伍的用户。</p>
<p><strong>问题本质:找到有相似标签的用户</strong>。</p>
<p>举例:</p>
<ul>
<li>用户 A: 【Java,大一,男】</li>
<li>用户 B: 【Java,大二,男】</li>
<li>用户 C: 【Python,大二,女】</li>
<li>用户 D: 【Java,大一,女】</li>
</ul>
<p><strong>怎么匹配?</strong></p>
<ol>
<li>找到有共同标签最多的用户(ToPN)</li>
<li>共同标签越多,分数越高,排在越前面</li>
<li>如果没有匹配的用户,随机推荐几个(降级方案)</li>
</ol>
<p><strong>两种算法:</strong></p>
<ol>
<li><strong>编辑距离算法:</strong>https://blog.csdn.net/DBC_121/article/details/104198838</li>
</ol>
<blockquote>
<p>最小编辑距离:就是一个字符串 1 通过对其字符串最少多少次增删改<strong>字符</strong>的操作可以变成 字符串 2(一样的)</p>
</blockquote>
<ol start="2">
<li><strong>余弦相似度算法:(如果需要为对应标签带权重进行计算,比如:学什么方向最重要,性别相对次要)</strong></li>
</ol>
<ul>
<li>https://www.cnblogs.com/kakarotto-chen/p/17822394.html#_label2_2</li>
<li>https://blog.51cto.com/u_16175448/12195054</li>
</ul>
<p>这里我们采用的是编辑距离算法:</p>
<pre><code class="language-java">package com.rainbowsea.yupao.utils;

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

/**
* 算法工具类
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
*/
public class AlgorithmUtils {

    /**
   * 编辑距离算法(用于计算最相似的两组标签)
   * 原理:https://blog.csdn.net/DBC_121/article/details/104198838
   *
   * @param tagList1
   * @param tagList2
   * @return
   */
    public static int minDistance(List&lt;String&gt; tagList1, List&lt;String&gt; tagList2) {
      // 在开始计算的时候,进行一个排序,让其中的tag 标签的内容,是相同等内容排序比较对比
      Collections.sort(tagList1);
      Collections.sort(tagList2);
      int n = tagList1.size();
      int m = tagList2.size();

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

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

      for (int j = 0; j &lt; m + 1; j++) {
            d = j;
      }

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


    /**
   * 编辑距离算法(用于计算最相似的两个字符串)
   * 原理: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;
      for (int i = 0; i &lt; n + 1; i++) {
            d = i;
      }

      for (int j = 0; j &lt; m + 1; j++) {
            d = j;
      }

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

</code></pre>
<p><strong>怎么对所有用户匹配,取 TOP ?</strong></p>
<p>直接取出所有用户,依次和当前用计算分数,取 TOPN (50W 花费 54 秒 )</p>
<p><strong>优化方法:</strong></p>
<ol>
<li>切忌不要在数据量大的时候循环输出日志(取消掉日志后 20 秒),这里指的是 MyBatisPlus 的输出日志在 yaml 当中的配置。</li>
<li>Map 存了所有的分数信息,占用内存。</li>
</ol>
<p><strong>解决:</strong>维护一个固定长度的有序集合(sortedSet),只需要保留分数最高的几个用户集合(利用时间换空间)</p>
<p><strong>eg:</strong> 【5,3,4,6,7】 取 TOP 5 即可,id 为 1 的用户就不用放进去了。</p>
<p>细节:</p>
<ol>
<li>剔除自己,自己不需要匹配查询</li>
<li>尽量只查需要的数据:
<ol>
<li>过滤掉标签为空的用户</li>
<li>根据部分标签用户(前提是能区分出来那个标签比较重要)</li>
<li>只查需要的数据(比如只查 Id 和 tags ),不要用 * 。</li>
</ol>
</li>
<li>提前查?(使用定时任务)
<ol>
<li>提前把所有用户给缓存(不适用于经常更新的数据)</li>
<li>提前运算出来结果,缓存(针对一些重点用户,提前缓存)</li>
</ol>
</li>
</ol>
<p><strong>类比大数据推荐机制:</strong></p>
<p>大数据推荐场景:比如说有几十亿个商品,难道要查出来所有的商品?难道要对所有的数据计算一遍相似度?</p>
<p>大数据推荐流程:</p>
<ul>
<li>检索—&gt; 召回—&gt; 粗排—&gt; 精排—&gt; 重排序等等等。</li>
<li>检索:尽可能多的查符合要求的数据(比如:按记录查)</li>
<li>召回:查询可能要用到的数据(不做运算)</li>
<li>粗排:粗略排序,简单地运算(运算相对轻量)</li>
<li>精排:精细排序,确定固定排位</li>
</ul>
<p>这里我们使用的方式:</p>
<pre><code class="language-java">
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.request.UserLoginRequest;
import com.rainbowsea.yupao.model.request.UserRegisterRequest;
import com.rainbowsea.yupao.service.UserService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static com.rainbowsea.yupao.contant.UserConstant.USER_LOGIN_STATE;

@RestController
@RequestMapping("/user")
@Api("接口文档的一个别名处理定义 UserController")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"})// 配置前端访问路径的放行,可以配置多个
@Slf4j
public class UserController {


    @Resource
    private UserService userService;

    @Resource
    private RedisTemplate&lt;String, Object&gt; redisTemplate;
    /**
   * 获取最匹配的用户
   *
   * @param num
   * @param request
   * @return
   */
    @GetMapping("/match")
    public BaseResponse&lt;List&lt;User&gt;&gt; matchUsers(long num, HttpServletRequest request) {
      if (num &lt;= 0 || num &gt; 20) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
      }
      User user = userService.getLoginUser(request);
      return ResultUtils.success(userService.matchUsers(num, user));
    }

}

</code></pre>
<pre><code class="language-java">
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.contant.UserConstant;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.mapper.UserMapper;
import com.rainbowsea.yupao.utils.AlgorithmUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.Pair;
import org.apache.poi.ss.formula.functions.Now;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static com.rainbowsea.yupao.contant.UserConstant.USER_LOGIN_STATE;

/**
* @author RainbowSea
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2025-04-14 16:03:21
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl&lt;UserMapper, User&gt;
      implements UserService {

    /**
   * 加盐 混淆,让密码更加没有规律,更加安全一些
   */
    private static final String SALT = "rainbowsea";


    @Resource
    private UserMapper userMapper;



    /**
   * 推荐(前端心动模式)的推荐算法,推荐用户
   * @param num
   * @param loginUser
   * @return
   */
    @Override
    public List&lt;User&gt; matchUsers(long num, User loginUser) {
      QueryWrapper&lt;User&gt; queryWrapper = new QueryWrapper&lt;&gt;();
      queryWrapper.select("id", "tags");
      queryWrapper.isNotNull("tags");
      List&lt;User&gt; userList = this.list(queryWrapper);
      String tags = loginUser.getTags();
      Gson gson = new Gson();
      List&lt;String&gt; tagList = gson.fromJson(tags, new TypeToken&lt;List&lt;String&gt;&gt;() {
      }.getType());
      // 用户列表的下标 =&gt; 相似度
      List&lt;Pair&lt;User, Long&gt;&gt; list = new ArrayList&lt;&gt;();
      // 依次计算所有用户和当前用户的相似度
      for (int i = 0; i &lt; userList.size(); i++) {
            User user = userList.get(i);
            String userTags = user.getTags();
            // 无标签或者为当前用户自己
            if (StringUtils.isBlank(userTags) || user.getId().equals(loginUser.getId())) {
                continue;
            }
            List&lt;String&gt; userTagList = gson.fromJson(userTags, new TypeToken&lt;List&lt;String&gt;&gt;() {
            }.getType());
            // 计算分数
            long distance = AlgorithmUtils.minDistance(tagList, userTagList);
            list.add(new Pair&lt;&gt;(user, distance));
      }
      // 按编辑距离由小到大排序
      List&lt;Pair&lt;User, Long&gt;&gt; topUserPairList = list.stream()
                .sorted((a, b) -&gt; (int) (a.getValue() - b.getValue()))
                .limit(num)
                .collect(Collectors.toList());
      // 原本顺序的 userId 列表
      List&lt;Long&gt; userIdList = topUserPairList.stream().map(pair -&gt; pair.getKey().getId()).collect(Collectors.toList());
      QueryWrapper&lt;User&gt; userQueryWrapper = new QueryWrapper&lt;&gt;();
      userQueryWrapper.in("id", userIdList);
      // 1, 3, 2
      // User1、User2、User3
      // 1 =&gt; User1, 2 =&gt; User2, 3 =&gt; User3
      Map&lt;Long, List&lt;User&gt;&gt; userIdUserListMap = this.list(userQueryWrapper)
                .stream()
                .map(user -&gt; getSafetyUser(user))
                .collect(Collectors.groupingBy(User::getId));
      List&lt;User&gt; finalUserList = new ArrayList&lt;&gt;();
      for (Long userId : userIdList) {
            finalUserList.add(userIdUserListMap.get(userId).get(0));
      }
      return finalUserList;
    }
}
</code></pre>
<pre><code class="language-java">package com.rainbowsea.yupao.utils;

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

/**
* 算法工具类
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
*/
public class AlgorithmUtils {

    /**
   * 编辑距离算法(用于计算最相似的两组标签)
   * 原理:https://blog.csdn.net/DBC_121/article/details/104198838
   *
   * @param tagList1
   * @param tagList2
   * @return
   */
    public static int minDistance(List&lt;String&gt; tagList1, List&lt;String&gt; tagList2) {
      // 在开始计算的时候,进行一个排序,让其中的tag 标签的内容,是相同等内容排序比较对比
      Collections.sort(tagList1);
      Collections.sort(tagList2);
      int n = tagList1.size();
      int m = tagList2.size();

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

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

      for (int j = 0; j &lt; m + 1; j++) {
            d = j;
      }

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


    /**
   * 编辑距离算法(用于计算最相似的两个字符串)
   * 原理: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;
      for (int i = 0; i &lt; n + 1; i++) {
            d = i;
      }

      for (int j = 0; j &lt; m + 1; j++) {
            d = j;
      }

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

</code></pre>
<p><strong>分表学习建议:</strong></p>
<p>mycat,sharding sphere 框架</p>
<p>一致性 hash 算法</p>
<h1 id="前端简单的功能调整">前端简单的功能调整</h1>
<p>权限整理</p>
<ul>
<li>加入队伍按钮:仅非队伍创建人、且未加入队伍的人可见</li>
<li>更新队伍按钮:仅创建人可见</li>
<li>解散队伍按钮:仅创建人可见</li>
<li>退出队伍按钮:创建人不可见,仅已加入队伍的人可见</li>
</ul>
<ol>
<li>仅加入队伍和创建队伍的人能看到队伍操作按钮(listTeam接口要能获取我加入的队伍状态)</li>
</ol>
<p>方案1:前端查询我加入了哪些队伍列表,然后判断每个队伍id 是否在列表中(前端要多发一次请</p>
<p>求)</p>
<p>方案 2:在后端去做上述事情 (推荐)</p>
<p>解决:使用router.beforeEach,根据要跳转页面的url 路径匹配 config/routes 配置的 title 字段。</p>
<ol start="2">
<li>前端导航栏死【标题】问题,实时动态显示前端标题信息</li>
<li>没有登录无法查询信息,强制登录,自动跳转到登录页,</li>
</ol>
<p>解决:axios 全局配置响应拦截、并且添加重定向</p>
<ol start="4">
<li>区分公开和加密房间;加入有密码的房间,要指定密码</li>
<li>展示已加入队伍人数</li>
</ol>
<hr>
<h1 id="最后">最后:</h1>
<blockquote>
<p>“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”</p>
<p><img src="https://img2024.cnblogs.com/blog/3084824/202508/3084824-20250820105451965-2126976518.gif"></p>
</blockquote><br><br>
来源:https://www.cnblogs.com/TheMagicalRainbowSea/p/19048125
頁: [1]
查看完整版本: 伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 05