找回密码
 立即注册
首页 业界区 安全 伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Bo ...

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

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

1.gif

项目地址:

  • Github:https://github.com/China-Rainbow-sea/yupao
  • Gitee:https://gitee.com/Rainbow--Sea/yupao
@
目录

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

    • 创建队伍
    • 查询队伍列表
    • 修改队伍信息
    • 用户可以加入队伍
    • 用户可以退出队伍
    • 队长可以解散队伍
    • 获取当前用户创建的队伍:包括:私有的,公开的,加密的,只要是自己创建的
    • 获取当前用户加入的队伍,包括:私有的,公开的,加密的,只要是自己加入的

  • 推荐算法:随机匹配
  • 前端简单的功能调整
  • 最后:

系统(接口)设计

创建队伍

用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 PO
队长、剩余的人数
聊天?
公开 或 private 或加密
信息流中不展示已过期的队伍
<ol>请求参数是否为空?
是否登录,未登录不允许创建
校验信息<ol>
队伍人数>1且= 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. 插入用户  => 队伍关系到关系表        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]补充:事务注解
  1. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  2. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  3. import com.rainbowsea.yupao.common.BaseResponse;
  4. import com.rainbowsea.yupao.common.DeleteRequest;
  5. import com.rainbowsea.yupao.common.ErrorCode;
  6. import com.rainbowsea.yupao.utils.ResultUtils;
  7. import com.rainbowsea.yupao.exception.BusinessException;
  8. import com.rainbowsea.yupao.model.Team;
  9. import com.rainbowsea.yupao.model.User;
  10. import com.rainbowsea.yupao.model.UserTeam;
  11. import com.rainbowsea.yupao.model.dto.TeamQuery;
  12. import com.rainbowsea.yupao.model.request.TeamAddRequest;
  13. import com.rainbowsea.yupao.model.request.TeamJoinRequest;
  14. import com.rainbowsea.yupao.model.request.TeamQuitRequest;
  15. import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
  16. import com.rainbowsea.yupao.model.vo.TeamUserVO;
  17. import com.rainbowsea.yupao.service.TeamService;
  18. import com.rainbowsea.yupao.service.UserService;
  19. import com.rainbowsea.yupao.service.UserTeamService;
  20. import io.swagger.annotations.Api;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.beans.BeanUtils;
  23. import org.springframework.web.bind.annotation.CrossOrigin;
  24. import org.springframework.web.bind.annotation.GetMapping;
  25. import org.springframework.web.bind.annotation.PostMapping;
  26. import org.springframework.web.bind.annotation.RequestBody;
  27. import org.springframework.web.bind.annotation.RequestMapping;
  28. import org.springframework.web.bind.annotation.RestController;
  29. import javax.annotation.Resource;
  30. import javax.servlet.http.HttpServletRequest;
  31. import java.util.ArrayList;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.stream.Collectors;
  36. @RestController
  37. @RequestMapping("/team")
  38. @Api("接口文档的一个别名处理定义 TeamController ")
  39. @CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"})  // 配置前端访问路径的放行,可以配置多个
  40. @Slf4j
  41. public class TeamController {
  42.     @Resource
  43.     private UserService userService;
  44.     @Resource
  45.     private TeamService teamService;
  46.     @Resource
  47.     private UserTeamService userTeamService;
  48.     /**
  49.      * 插入 team 队伍,添加队伍
  50.      *
  51.      * @param teamAddRequest
  52.      * @return teamId
  53.      */
  54.     @PostMapping("/add")
  55.     public BaseResponse<Long> addTeam(@RequestBody TeamAddRequest teamAddRequest, HttpServletRequest request) {
  56.         if (teamAddRequest == null) {
  57.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  58.         }
  59.         User loginUser = userService.getLoginUser(request);
  60.         Team team = new Team();
  61.         BeanUtils.copyProperties(teamAddRequest, team);
  62.         long teamId = teamService.addTeam(team, loginUser);
  63.         return ResultUtils.success(teamId);
  64.     }
  65. }
复制代码
查询队伍列表

分页展示队伍列表,根据名称、最大人数等搜索队伍PO,信息流中不展示已过期的队伍。

  • 从请求参数中取出队伍名称等查询条件,如果存在则作为查询条件
  • 需要登录,才能查询
  • 不展示已过期的队伍 (根据过期时间筛选)
  • 可以通过某个关键词同时对名称和描述查询
  • 只有管理员才能查看加密还有非公开的房间
  • 关联查询已加入队伍的用户信息
  • 关联查询已加入队伍的用户信息(可能会很耗费性能,建议大家用自己写SQL的方式实现)
自己写 SQL
  1. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  3. import com.rainbowsea.yupao.common.ErrorCode;
  4. import com.rainbowsea.yupao.exception.BusinessException;
  5. import com.rainbowsea.yupao.mapper.TeamMapper;
  6. import com.rainbowsea.yupao.model.Team;
  7. import com.rainbowsea.yupao.model.User;
  8. import com.rainbowsea.yupao.model.UserTeam;
  9. import com.rainbowsea.yupao.model.dto.TeamQuery;
  10. import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
  11. import com.rainbowsea.yupao.model.request.TeamJoinRequest;
  12. import com.rainbowsea.yupao.model.request.TeamQuitRequest;
  13. import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
  14. import com.rainbowsea.yupao.model.vo.TeamUserVO;
  15. import com.rainbowsea.yupao.model.vo.UserVO;
  16. import com.rainbowsea.yupao.service.TeamService;
  17. import com.rainbowsea.yupao.service.UserService;
  18. import com.rainbowsea.yupao.service.UserTeamService;
  19. import org.apache.commons.lang3.StringUtils;
  20. import org.redisson.api.RLock;
  21. import org.redisson.api.RedissonClient;
  22. import org.springframework.beans.BeanUtils;
  23. import org.springframework.stereotype.Service;
  24. import org.springframework.transaction.annotation.Transactional;
  25. import org.springframework.util.CollectionUtils;
  26. import javax.annotation.Resource;
  27. import java.util.ArrayList;
  28. import java.util.Date;
  29. import java.util.List;
  30. import java.util.Optional;
  31. import java.util.concurrent.TimeUnit;
  32. /**
  33. *
  34. */
  35. @Service
  36. public class TeamServiceImpl extends ServiceImpl<TeamMapper, Team>
  37.         implements TeamService {
  38.     @Resource
  39.     private UserTeamService userTeamService;
  40.     @Resource
  41.     private UserService userService;
  42.     @Resource
  43.     private RedissonClient redissonClient;
  44.     /***
  45.      * 添加队伍
  46.      * @param team 队伍
  47.      * @param loginUser User 用户
  48.      * @return
  49.      */
  50.     @Override
  51.     @Transactional(rollbackFor = Exception.class)
  52.     public long addTeam(Team team, User loginUser) {
  53.         // 1. 请求参数是否为空?
  54.         if (team == null) {
  55.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  56.         }
  57.         // 2. 是否登录,未登录不允许创建
  58.         if (loginUser == null) {
  59.             throw new BusinessException(ErrorCode.NOT_LOGIN);
  60.         }
  61.         final long userId = loginUser.getId();
  62.         // 3. 校验信息
  63.         //   1. 队伍人数 > 1 且 <= 20
  64.         int maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
  65.         if (maxNum < 1 || maxNum > 20) {
  66.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍人数不满足要求");
  67.         }
  68.         //   2. 队伍标题 <= 20
  69.         String name = team.getName();
  70.         if (StringUtils.isBlank(name) || name.length() > 20) {
  71.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍标题不满足要求");
  72.         }
  73.         //   3. 描述 <= 512
  74.         String description = team.getDescription();
  75.         if (StringUtils.isNotBlank(description) && description.length() > 512) {
  76.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍描述过长");
  77.         }
  78.         //   4. status 是否公开(int)不传默认为 0(公开)
  79.         int status = Optional.ofNullable(team.getStatus()).orElse(0);
  80.         TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
  81.         if (statusEnum == null) {
  82.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍状态不满足要求");
  83.         }
  84.         //   5. 如果 status 是加密状态,一定要有密码,且密码 <= 32
  85.         String password = team.getPassword();
  86.         if (TeamStatusEnum.SECRET.equals(statusEnum)) {
  87.             if (StringUtils.isBlank(password) || password.length() > 32) {
  88.                 throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码设置不正确");
  89.             }
  90.         }
  91.         // 6. 超时时间 > 当前时间
  92.         Date expireTime = team.getExpireTime();
  93.         if (new Date().after(expireTime)) {
  94.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "超时时间 > 当前时间");
  95.         }
  96.         // 7. 校验用户最多创建 5 个队伍
  97.         // todo 有 bug,可能同时创建 100 个队伍
  98.         QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
  99.         queryWrapper.eq("userId", userId);
  100.         long hasTeamNum = this.count(queryWrapper);
  101.         if (hasTeamNum >= 5) {
  102.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建 5 个队伍");
  103.         }
  104.         // 8. 插入队伍信息到队伍表
  105.         team.setId(null);
  106.         team.setUserId(userId);
  107.         boolean result = this.save(team);
  108.         Long teamId = team.getId();
  109.         if (!result || teamId == null) {
  110.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
  111.         }
  112.         // 9. 插入用户  => 队伍关系到关系表
  113.         UserTeam userTeam = new UserTeam();
  114.         userTeam.setUserId(userId);
  115.         userTeam.setTeamId(teamId);
  116.         userTeam.setJoinTime(new Date());
  117.         result = userTeamService.save(userTeam);
  118.         if (!result) {
  119.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
  120.         }
  121.         return teamId;
  122.     }
  123. }
复制代码
  1. @Transaction(rollbackFor = Exception.class)  // 在方法上添加上注解,启动事务控制
  2. void public main() {
  3.    
  4. }
复制代码
  1. // 1. 自己写 SQL
  2. // 查询队伍和创建人的信息
  3. // select * from team t left join user u on t.userId = u.id
  4. // 查询队伍和已加入队伍成员的信息
  5. // select *
  6. // from team t
  7. //         left join user_team ut on t.id = ut.teamId
  8. //         left join user u on ut.userId = u.id;
复制代码
修改队伍信息


  • 判断请求参数是否为空
  • 查询队伍是否存在
  • 只有管理员或者队伍的创建者可以修改
  • 如果用户传入的新值和老值一致,就不用update了 (可自行实现,降低数据库使用次数)
  • 如果队伍状态改为加密,必须要有密码
  • 更新成功
  1. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  2. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  3. import com.rainbowsea.yupao.common.BaseResponse;
  4. import com.rainbowsea.yupao.common.DeleteRequest;
  5. import com.rainbowsea.yupao.common.ErrorCode;
  6. import com.rainbowsea.yupao.utils.ResultUtils;
  7. import com.rainbowsea.yupao.exception.BusinessException;
  8. import com.rainbowsea.yupao.model.Team;
  9. import com.rainbowsea.yupao.model.User;
  10. import com.rainbowsea.yupao.model.UserTeam;
  11. import com.rainbowsea.yupao.model.dto.TeamQuery;
  12. import com.rainbowsea.yupao.model.request.TeamAddRequest;
  13. import com.rainbowsea.yupao.model.request.TeamJoinRequest;
  14. import com.rainbowsea.yupao.model.request.TeamQuitRequest;
  15. import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
  16. import com.rainbowsea.yupao.model.vo.TeamUserVO;
  17. import com.rainbowsea.yupao.service.TeamService;
  18. import com.rainbowsea.yupao.service.UserService;
  19. import com.rainbowsea.yupao.service.UserTeamService;
  20. import io.swagger.annotations.Api;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.beans.BeanUtils;
  23. import org.springframework.web.bind.annotation.CrossOrigin;
  24. import org.springframework.web.bind.annotation.GetMapping;
  25. import org.springframework.web.bind.annotation.PostMapping;
  26. import org.springframework.web.bind.annotation.RequestBody;
  27. import org.springframework.web.bind.annotation.RequestMapping;
  28. import org.springframework.web.bind.annotation.RestController;
  29. import javax.annotation.Resource;
  30. import javax.servlet.http.HttpServletRequest;
  31. import java.util.ArrayList;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.stream.Collectors;
  36. @RestController
  37. @RequestMapping("/team")
  38. @Api("接口文档的一个别名处理定义 TeamController ")
  39. @CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"})  // 配置前端访问路径的放行,可以配置多个
  40. @Slf4j
  41. public class TeamController {
  42.     @Resource
  43.     private UserService userService;
  44.     @Resource
  45.     private TeamService teamService;
  46.     @Resource
  47.     private UserTeamService userTeamService;
  48. /**
  49.      * 显示队伍列表,私有的不显示
  50.      **/
  51.     @GetMapping("/list")
  52.     public BaseResponse<List<TeamUserVO>> listTeams(TeamQuery teamQuery, HttpServletRequest request) {
  53.         if (teamQuery == null) {
  54.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  55.         }
  56.         boolean isAdmin = userService.isAdmin(request);
  57.         // 1、查询队伍列表
  58.         List<TeamUserVO> teamList = teamService.listTeams(teamQuery, isAdmin);
  59.         final List<Long> teamIdList = teamList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
  60.         // 2、判断当前用户是否已加入队伍
  61.         QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
  62.         try {
  63.             User loginUser = userService.getLoginUser(request);
  64.             userTeamQueryWrapper.eq("userId", loginUser.getId());
  65.             userTeamQueryWrapper.in("teamId", teamIdList);
  66.             List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
  67.             // 已加入的队伍 id 集合
  68.             Set<Long> hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
  69.             teamList.forEach(team -> {
  70.                 boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
  71.                 team.setHasJoin(hasJoin);
  72.             });
  73.         } catch (Exception e) {
  74.         }
  75.         // 3、查询已加入队伍的人数
  76.         QueryWrapper<UserTeam> userTeamJoinQueryWrapper = new QueryWrapper<>();
  77.         userTeamJoinQueryWrapper.in("teamId", teamIdList);
  78.         List<UserTeam> userTeamList = userTeamService.list(userTeamJoinQueryWrapper);
  79.         // 队伍 id => 加入这个队伍的用户列表
  80.         Map<Long, List<UserTeam>> teamIdUserTeamList = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
  81.         teamList.forEach(team -> team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList<>()).size()));
  82.         return ResultUtils.success(teamList);
  83.     }
  84. }
复制代码
  1. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  3. import com.rainbowsea.yupao.common.ErrorCode;
  4. import com.rainbowsea.yupao.exception.BusinessException;
  5. import com.rainbowsea.yupao.mapper.TeamMapper;
  6. import com.rainbowsea.yupao.model.Team;
  7. import com.rainbowsea.yupao.model.User;
  8. import com.rainbowsea.yupao.model.UserTeam;
  9. import com.rainbowsea.yupao.model.dto.TeamQuery;
  10. import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
  11. import com.rainbowsea.yupao.model.request.TeamJoinRequest;
  12. import com.rainbowsea.yupao.model.request.TeamQuitRequest;
  13. import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
  14. import com.rainbowsea.yupao.model.vo.TeamUserVO;
  15. import com.rainbowsea.yupao.model.vo.UserVO;
  16. import com.rainbowsea.yupao.service.TeamService;
  17. import com.rainbowsea.yupao.service.UserService;
  18. import com.rainbowsea.yupao.service.UserTeamService;
  19. import org.apache.commons.lang3.StringUtils;
  20. import org.redisson.api.RLock;
  21. import org.redisson.api.RedissonClient;
  22. import org.springframework.beans.BeanUtils;
  23. import org.springframework.stereotype.Service;
  24. import org.springframework.transaction.annotation.Transactional;
  25. import org.springframework.util.CollectionUtils;
  26. import javax.annotation.Resource;
  27. import java.util.ArrayList;
  28. import java.util.Date;
  29. import java.util.List;
  30. import java.util.Optional;
  31. import java.util.concurrent.TimeUnit;
  32. /**
  33. *
  34. */
  35. @Service
  36. public class TeamServiceImpl extends ServiceImpl<TeamMapper, Team>
  37.         implements TeamService {
  38.     @Resource
  39.     private UserTeamService userTeamService;
  40.     @Resource
  41.     private UserService userService;
  42.     @Resource
  43.     private RedissonClient redissonClient;
  44.     /**
  45.      * 搜索队伍
  46.      * @param teamQuery
  47.      * @param isAdmin
  48.      * @return
  49.      */
  50.     @Override
  51.     public List<TeamUserVO> listTeams(TeamQuery teamQuery, boolean isAdmin) {
  52.         QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
  53.         // 组合查询条件
  54.         if (teamQuery != null) {
  55.             Long id = teamQuery.getId();
  56.             if (id != null && id > 0) {
  57.                 queryWrapper.eq("id", id);
  58.             }
  59.             List<Long> idList = teamQuery.getIdList();
  60.             if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(idList)) {
  61.                 queryWrapper.in("id", idList);
  62.             }
  63.             String searchText = teamQuery.getSearchText();
  64.             if (StringUtils.isNotBlank(searchText)) {
  65.                 queryWrapper.and(qw -> qw.like("name", searchText).or().like("description", searchText));
  66.             }
  67.             String name = teamQuery.getName();
  68.             if (StringUtils.isNotBlank(name)) {
  69.                 queryWrapper.like("name", name);
  70.             }
  71.             String description = teamQuery.getDescription();
  72.             if (StringUtils.isNotBlank(description)) {
  73.                 queryWrapper.like("description", description);
  74.             }
  75.             Integer maxNum = teamQuery.getMaxNum();
  76.             // 查询最大人数相等的
  77.             if (maxNum != null && maxNum > 0) {
  78.                 queryWrapper.eq("maxNum", maxNum);
  79.             }
  80.             Long userId = teamQuery.getUserId();
  81.             // 根据创建人来查询
  82.             if (userId != null && userId > 0) {
  83.                 queryWrapper.eq("userId", userId);
  84.             }
  85.             // 根据状态来查询
  86.             Integer status = teamQuery.getStatus();
  87.             TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
  88.             if (statusEnum == null) {
  89.                 statusEnum = TeamStatusEnum.PUBLIC;
  90.             }
  91.             if (!isAdmin && statusEnum.equals(TeamStatusEnum.PRIVATE)) {
  92.                 throw new BusinessException(ErrorCode.NO_AUTH);
  93.             }
  94.             queryWrapper.eq("status", statusEnum.getValue());
  95.         }
  96.         // 不展示已过期的队伍
  97.         // expireTime is null or expireTime > now()
  98.         queryWrapper.and(qw -> qw.gt("expireTime", new Date()).or().isNull("expireTime"));
  99.         List<Team> teamList = this.list(queryWrapper);
  100.         if (org.apache.commons.collections4.CollectionUtils.isEmpty(teamList)) {
  101.             return new ArrayList<>();
  102.         }
  103.         List<TeamUserVO> teamUserVOList = new ArrayList<>();
  104.         // 关联查询创建人的用户信息
  105.         for (Team team : teamList) {
  106.             Long userId = team.getUserId();
  107.             if (userId == null) {
  108.                 continue;
  109.             }
  110.             User user = userService.getById(userId);
  111.             TeamUserVO teamUserVO = new TeamUserVO();
  112.             BeanUtils.copyProperties(team, teamUserVO);
  113.             // 脱敏用户信息
  114.             if (user != null) {
  115.                 UserVO userVO = new UserVO();
  116.                 BeanUtils.copyProperties(user, userVO);
  117.                 teamUserVO.setCreateUser(userVO);
  118.             }
  119.             teamUserVOList.add(teamUserVO);
  120.         }
  121.         return teamUserVOList;
  122.     }
  123.         }
  124. }
复制代码
用户可以退出队伍

请求参数:用户 ID

  • 校验队伍是否存在
  • 校验我是否已加入队伍
  • 如果队伍

    • 只剩一人,队伍解散,(只剩一人,说明本身自己就是队长)
    • 还有其他人
    • 如果是队长退出队伍,权限转移给第二早加入的用户一一先来后到(只用取id最小的2 条数据)
    • 非队长,自己退出队伍

  1.     /**
  2.      * 更新队伍内容
  3.      *
  4.      * @param teamUpdateRequest teamUpdateRequest 对象
  5.      * @return Boolean 更新成功 true,否则 false
  6.      */
  7.     @PostMapping("/update")
  8.     public BaseResponse<Boolean> updateTeam(@RequestBody TeamUpdateRequest teamUpdateRequest, HttpServletRequest request) {
  9.         if (teamUpdateRequest == null) {
  10.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  11.         }
  12.         User loginUser = userService.getLoginUser(request);
  13.         boolean result = teamService.updateTeam(teamUpdateRequest, loginUser);
  14.         if (!result) {
  15.             throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新失败");
  16.         }
  17.         return ResultUtils.success(true);
  18.     }
复制代码
  1.     /**
  2.      * 更新队伍
  3.      * @param teamUpdateRequest
  4.      * @param loginUser
  5.      * @return
  6.      */
  7.     @Override
  8.     public boolean updateTeam(TeamUpdateRequest teamUpdateRequest, User loginUser) {
  9.         if (teamUpdateRequest == null) {
  10.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  11.         }
  12.         Long id = teamUpdateRequest.getId();
  13.         if (id == null || id <= 0) {
  14.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  15.         }
  16.         Team oldTeam = this.getById(id);
  17.         if (oldTeam == null) {
  18.             throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
  19.         }
  20.         // 只有管理员或者队伍的创建者可以修改
  21.         if (!oldTeam.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {
  22.             throw new BusinessException(ErrorCode.NO_AUTH);
  23.         }
  24.         TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(teamUpdateRequest.getStatus());
  25.         if (statusEnum.equals(TeamStatusEnum.SECRET)) {
  26.             if (StringUtils.isBlank(teamUpdateRequest.getPassword())) {
  27.                 throw new BusinessException(ErrorCode.PARAMS_ERROR, "加密房间必须要设置密码");
  28.             }
  29.         }
  30.         Team updateTeam = new Team();
  31.         BeanUtils.copyProperties(teamUpdateRequest, updateTeam);
  32.         return this.updateById(updateTeam);
  33.     }
复制代码
获取当前用户加入的队伍,包括:私有的,公开的,加密的,只要是自己加入的
  1.     /**
  2.      * 加入队伍
  3.      * @param teamJoinRequest
  4.      * @param request
  5.      * @return BaseResponse<Boolean>
  6.      */
  7.     @PostMapping("/join")
  8.     public BaseResponse<Boolean> joinTeam(@RequestBody TeamJoinRequest teamJoinRequest, HttpServletRequest request) {
  9.         if (teamJoinRequest == null) {
  10.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  11.         }
  12.         User loginUser = userService.getLoginUser(request);
  13.         boolean result = teamService.joinTeam(teamJoinRequest, loginUser);
  14.         return ResultUtils.success(result);
  15.     }
复制代码
推荐算法:随机匹配

需求背景:为了帮助大家更快的发现和自己新区相同的朋友。
思考:匹配 1 个还是匹配多个。
答:匹配多个,并且按照匹配的相似度从高到低排序
思考:怎么匹配?(根据什么匹配)
答:这里我们根据用户 user 表当中设置的 tags 属性进行匹配。
2.png

还可以根据 user_team 匹配加入相同队伍的用户。
问题本质:找到有相似标签的用户
举例:

  • 用户 A: 【Java,大一,男】
  • 用户 B: 【Java,大二,男】
  • 用户 C: 【Python,大二,女】
  • 用户 D: 【Java,大一,女】
怎么匹配?

  • 找到有共同标签最多的用户(ToPN)
  • 共同标签越多,分数越高,排在越前面
  • 如果没有匹配的用户,随机推荐几个(降级方案)
两种算法:

  • 编辑距离算法:https://blog.csdn.net/DBC_121/article/details/104198838
最小编辑距离:就是一个字符串 1 通过对其字符串最少多少次增删改字符的操作可以变成 字符串 2(一样的)

  • 余弦相似度算法:(如果需要为对应标签带权重进行计算,比如:学什么方向最重要,性别相对次要)


  • https://www.cnblogs.com/kakarotto-chen/p/17822394.html#_label2_2
  • https://blog.51cto.com/u_16175448/12195054
这里我们采用的是编辑距离算法:
  1.     /**
  2.      * 加入队伍
  3.      *
  4.      * @param teamJoinRequest
  5.      * @param loginUser
  6.      * @return boolean
  7.      */
  8.     @Override
  9.     public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
  10.         if (teamJoinRequest == null) {
  11.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  12.         }
  13.         Long teamId = teamJoinRequest.getTeamId();
  14.         Team team = getTeamById(teamId);
  15.         Date expireTime = team.getExpireTime();
  16.         if (expireTime != null && expireTime.before(new Date())) {
  17.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
  18.         }
  19.         Integer status = team.getStatus();
  20.         TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
  21.         if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
  22.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
  23.         }
  24.         String password = teamJoinRequest.getPassword();
  25.         if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
  26.             if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
  27.                 throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
  28.             }
  29.         }
  30.         // 该用户已加入的队伍数量
  31.         long userId = loginUser.getId();
  32.         // 只有一个线程能获取到锁
  33.         RLock lock = redissonClient.getLock("yupao:join_team");
  34.         try {
  35.             // 抢到锁并执行
  36.             while (true) {
  37.                 if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  38.                     System.out.println("getLock: " + Thread.currentThread().getId());
  39.                     QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
  40.                     userTeamQueryWrapper.eq("userId", userId);
  41.                     long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
  42.                     if (hasJoinNum > 5) {
  43.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
  44.                     }
  45.                     // 不能重复加入已加入的队伍
  46.                     userTeamQueryWrapper = new QueryWrapper<>();
  47.                     userTeamQueryWrapper.eq("userId", userId);
  48.                     userTeamQueryWrapper.eq("teamId", teamId);
  49.                     long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);
  50.                     if (hasUserJoinTeam > 0) {
  51.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
  52.                     }
  53.                     // 已加入队伍的人数
  54.                     long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
  55.                     if (teamHasJoinNum >= team.getMaxNum()) {
  56.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
  57.                     }
  58.                     // 修改队伍信息
  59.                     UserTeam userTeam = new UserTeam();
  60.                     userTeam.setUserId(userId);
  61.                     userTeam.setTeamId(teamId);
  62.                     userTeam.setJoinTime(new Date());
  63.                     return userTeamService.save(userTeam);
  64.                 }
  65.             }
  66.         } catch (InterruptedException e) {
  67.             log.error("doCacheRecommendUser error", e);
  68.             return false;
  69.         } finally {
  70.             // 只能释放自己的锁
  71.             if (lock.isHeldByCurrentThread()) {
  72.                 System.out.println("unLock: " + Thread.currentThread().getId());
  73.                 lock.unlock();
  74.             }
  75.         }
  76.     }
复制代码
怎么对所有用户匹配,取 TOP ?
直接取出所有用户,依次和当前用计算分数,取 TOPN (50W 花费 54 秒 )
优化方法:

  • 切忌不要在数据量大的时候循环输出日志(取消掉日志后 20 秒),这里指的是 MyBatisPlus 的输出日志在 yaml 当中的配置。
  • Map 存了所有的分数信息,占用内存。
解决:维护一个固定长度的有序集合(sortedSet),只需要保留分数最高的几个用户集合(利用时间换空间)
eg: 【5,3,4,6,7】 取 TOP 5 即可,id 为 1 的用户就不用放进去了。
细节:

  • 剔除自己,自己不需要匹配查询
  • 尽量只查需要的数据:

    • 过滤掉标签为空的用户
    • 根据部分标签用户(前提是能区分出来那个标签比较重要)
    • 只查需要的数据(比如只查 Id 和 tags ),不要用 * 。

  • 提前查?(使用定时任务)

    • 提前把所有用户给缓存(不适用于经常更新的数据)
    • 提前运算出来结果,缓存(针对一些重点用户,提前缓存)

类比大数据推荐机制:
大数据推荐场景:比如说有几十亿个商品,难道要查出来所有的商品?难道要对所有的数据计算一遍相似度?
大数据推荐流程:

  • 检索—> 召回—> 粗排—> 精排—> 重排序等等等。
  • 检索:尽可能多的查符合要求的数据(比如:按记录查)
  • 召回:查询可能要用到的数据(不做运算)
  • 粗排:粗略排序,简单地运算(运算相对轻量)
  • 精排:精细排序,确定固定排位
这里我们使用的方式:
  1.     /**
  2.      * 退出队伍,
  3.      * @param teamQuitRequest
  4.      * @param request
  5.      * @return  BaseResponse<Boolean>
  6.      */
  7.     @PostMapping("/quit")
  8.     public BaseResponse<Boolean> quitTeam(@RequestBody TeamQuitRequest teamQuitRequest, HttpServletRequest request) {
  9.         if (teamQuitRequest == null) {
  10.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  11.         }
  12.         User loginUser = userService.getLoginUser(request);
  13.         boolean result = teamService.quitTeam(teamQuitRequest, loginUser);
  14.         return ResultUtils.success(result);
  15.     }
复制代码
  1. /**
  2.      * 退出队伍
  3.      * @param teamQuitRequest
  4.      * @param loginUser
  5.      * @return
  6.      */
  7.     @Override
  8.     @Transactional(rollbackFor = Exception.class)  // 添加上事务
  9.     public boolean quitTeam(TeamQuitRequest teamQuitRequest, User loginUser) {
  10.         if (teamQuitRequest == null) {
  11.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  12.         }
  13.         Long teamId = teamQuitRequest.getTeamId();
  14.         Team team = this.getById(teamId);
  15.         //Team team = getTeamById(teamId);
  16.         long userId = loginUser.getId();
  17.         UserTeam queryUserTeam = new UserTeam();
  18.         queryUserTeam.setTeamId(teamId);
  19.         queryUserTeam.setUserId(userId);
  20.         QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>(queryUserTeam);
  21.         long count = userTeamService.count(queryWrapper);
  22.         if (count == 0) {
  23.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "未加入队伍");
  24.         }
  25.         long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
  26.         // 队伍只剩一人,解散
  27.         if (teamHasJoinNum == 1) {
  28.             // 删除队伍
  29.             this.removeById(teamId);
  30.         } else {
  31.             // 队伍还剩至少两人
  32.             // 是队长
  33.             if (team.getUserId() == userId) {
  34.                 // 把队伍转移给最早加入的用户
  35.                 // 1. 查询已加入队伍的所有用户和加入时间
  36.                 QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
  37.                 userTeamQueryWrapper.eq("teamId", teamId);
  38.                 userTeamQueryWrapper.last("order by id asc limit 2");
  39.                 List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
  40.                 if (CollectionUtils.isEmpty(userTeamList) || userTeamList.size() <= 1) {
  41.                     throw new BusinessException(ErrorCode.SYSTEM_ERROR);
  42.                 }
  43.                 UserTeam nextUserTeam = userTeamList.get(1);
  44.                 Long nextTeamLeaderId = nextUserTeam.getUserId();
  45.                 // 更新当前队伍的队长
  46.                 Team updateTeam = new Team();
  47.                 updateTeam.setId(teamId);
  48.                 updateTeam.setUserId(nextTeamLeaderId);
  49.                 boolean result = this.updateById(updateTeam);
  50.                 if (!result) {
  51.                     throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新队伍队长失败");
  52.                 }
  53.             }
  54.         }
  55.         // 移除关系
  56.         return userTeamService.remove(queryWrapper);
  57.     }
复制代码
  1.     /**
  2.      * 加入队伍
  3.      *
  4.      * @param teamJoinRequest
  5.      * @param loginUser
  6.      * @return boolean
  7.      */
  8.     @Override
  9.     public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
  10.         if (teamJoinRequest == null) {
  11.             throw new BusinessException(ErrorCode.PARAMS_ERROR);
  12.         }
  13.         Long teamId = teamJoinRequest.getTeamId();
  14.         Team team = getTeamById(teamId);
  15.         Date expireTime = team.getExpireTime();
  16.         if (expireTime != null && expireTime.before(new Date())) {
  17.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
  18.         }
  19.         Integer status = team.getStatus();
  20.         TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
  21.         if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
  22.             throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
  23.         }
  24.         String password = teamJoinRequest.getPassword();
  25.         if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
  26.             if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
  27.                 throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
  28.             }
  29.         }
  30.         // 该用户已加入的队伍数量
  31.         long userId = loginUser.getId();
  32.         // 只有一个线程能获取到锁
  33.         RLock lock = redissonClient.getLock("yupao:join_team");
  34.         try {
  35.             // 抢到锁并执行
  36.             while (true) {
  37.                 if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  38.                     System.out.println("getLock: " + Thread.currentThread().getId());
  39.                     QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
  40.                     userTeamQueryWrapper.eq("userId", userId);
  41.                     long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
  42.                     if (hasJoinNum > 5) {
  43.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
  44.                     }
  45.                     // 不能重复加入已加入的队伍
  46.                     userTeamQueryWrapper = new QueryWrapper<>();
  47.                     userTeamQueryWrapper.eq("userId", userId);
  48.                     userTeamQueryWrapper.eq("teamId", teamId);
  49.                     long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);
  50.                     if (hasUserJoinTeam > 0) {
  51.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
  52.                     }
  53.                     // 已加入队伍的人数
  54.                     long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
  55.                     if (teamHasJoinNum >= team.getMaxNum()) {
  56.                         throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
  57.                     }
  58.                     // 修改队伍信息
  59.                     UserTeam userTeam = new UserTeam();
  60.                     userTeam.setUserId(userId);
  61.                     userTeam.setTeamId(teamId);
  62.                     userTeam.setJoinTime(new Date());
  63.                     return userTeamService.save(userTeam);
  64.                 }
  65.             }
  66.         } catch (InterruptedException e) {
  67.             log.error("doCacheRecommendUser error", e);
  68.             return false;
  69.         } finally {
  70.             // 只能释放自己的锁
  71.             if (lock.isHeldByCurrentThread()) {
  72.                 System.out.println("unLock: " + Thread.currentThread().getId());
  73.                 lock.unlock();
  74.             }
  75.         }
  76.     }
复制代码
分表学习建议:
mycat,sharding sphere 框架
一致性 hash 算法
前端简单的功能调整

权限整理

  • 加入队伍按钮:仅非队伍创建人、且未加入队伍的人可见
  • 更新队伍按钮:仅创建人可见
  • 解散队伍按钮:仅创建人可见
  • 退出队伍按钮:创建人不可见,仅已加入队伍的人可见

  • 仅加入队伍和创建队伍的人能看到队伍操作按钮(listTeam接口要能获取我加入的队伍状态)
方案1:前端查询我加入了哪些队伍列表,然后判断每个队伍id 是否在列表中(前端要多发一次请
求)
方案 2:在后端去做上述事情 (推荐)
解决:使用router.beforeEach,根据要跳转页面的url 路径匹配 config/routes 配置的 title 字段。

  • 前端导航栏死【标题】问题,实时动态显示前端标题信息
  • 没有登录无法查询信息,强制登录,自动跳转到登录页,
解决:axios 全局配置响应拦截、并且添加重定向

  • 区分公开和加密房间;加入有密码的房间,要指定密码
  • 展示已加入队伍人数
最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
3.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册