组队功能

那个球参应用场景

建立群聊队伍,可以发起队伍或者加入别人的队伍

需求分析

  • 用户角色
    • 用户分为两种角色:
      1. 队长
      2. 队员
    • 队长的行为:
      • 创建队伍:为队伍指定队伍名称、人数限制、描述、超时时间等属性。
      • 加入队伍:行为等同于创建队伍。
      • 退出队伍:退出队伍的行为分为解散队伍或转让队伍两种。
      • 队员的行为:
        • 加入队伍:选择加入一个队伍(可能涉及申请、队长审批,交互逻辑可后续完善)。
        • 退出队伍:简单地退出队伍。
  • 队伍管理
    • 队伍权限级别

      队伍有三种权限级别:

      1. 公开:任何人可以加入。
      2. 私有:需要申请加入,可能需要队长审批。
      3. 加密:加入需要密码,队长提供密码给队员。
      • 队伍的权限级别由队长在创建时指定。
    • 队伍数量限制

      用户可以创建多个队伍,但同时拥有的队伍数量不能超过五个,加入队伍的数量也有限制。

      需要维护一个多对多关系(用户与队伍之间),确保队员与队伍之间的关系被正确管理。

    • 队伍的基本属性

      队伍应该具有以下基本属性:

      1. 标题:队伍的名称。
      2. 描述:队伍的简要描述,帮助用户了解队伍的基本情况。
      3. 人数限制:队伍允许的最大人数。
      4. 超时时间:队伍的过期时间,过期后队伍不能继续加入或活动。
      • 这些属性需要在数据库中设计相应的数据表,并提供增删改查(CRUD)接口。
    • 队伍的状态

      队伍的状态应该有以下标识:

      1. 是否满员:队伍是否已经达到最大人数。
      2. 是否过期:队伍是否已经过期,过期队伍需要从用户列表中去除。
    • 分享队伍

      队长可以分享队伍给其他用户,生成一个邀请链接或二维码。

      用户通过访问分享链接或扫描二维码,能够加入相应的队伍。

      当队伍达到最大人数时,系统会发送通知告知用户队伍已经满员。

  • 队伍列表与搜索
    • 队伍列表展示

      用户可以查看队伍的列表,列表展示内容包括队伍的名称、描述、当前人数等。

      在队伍列表中,已过期的队伍不展示。

    • 队伍搜索

      用户可以根据队伍名称进行搜索,快速找到感兴趣的队伍。

表的设计

设计两个主要的数据表:队伍表team)和用户-队伍关系表user_team

这两个表用来存储队伍信息及其与用户之间的关系。

1. 队伍表(team)设计

存储队伍的相关信息,包括队伍的基本信息、创建者、状态、过期时间等。

字段设计:

  • id:队伍的唯一标识符,主键,BIGINT类型,自增长。
  • name:队伍名称,VARCHAR(256),不能为空。
  • description:队伍描述,VARCHAR(1024),可以为空。
  • maxNum:队伍最大人数,INT类型,默认为 1,不能为空。
  • expireTime:队伍的过期时间,DATETIME类型,不能为空。
  • userId:队伍的创建者用户ID,BIGINT类型,不能为空。
  • status:队伍的状态,INT类型,0 表示公开,1 表示私有,2 表示加密,默认为 0。
  • password:如果队伍是加密状态,这里存储加密的密码,VARCHAR(512)类型,最多 512 字符,允许为空。
  • createTime:队伍创建时间,DATETIME类型,默认为当前时间。
  • updateTime:队伍更新时间,DATETIME类型,默认为当前时间,且会在更新时自动更新。
  • isDelete:逻辑删除标志,TINYINT类型,0 表示未删除,1 表示已删除,默认为 0。

表结构SQL:

CREATE TABLE team (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '队伍ID',
    name         VARCHAR(256) NOT NULL COMMENT '队伍名称',
    description  VARCHAR(1024) NULL COMMENT '队伍描述',
    maxNum       INT DEFAULT 1 NOT NULL COMMENT '最大人数',
    expireTime   DATETIME NULL COMMENT '过期时间',
    userId       BIGINT NOT NULL COMMENT '创建人ID',
    status       INT DEFAULT 0 NOT NULL COMMENT '0 - 公开,1 - 私有,2 - 加密',
    password     VARCHAR(512) NULL COMMENT '密码',
    createTime   DATETIME DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间',
    updateTime   DATETIME DEFAULT CURRENT_TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    isDelete     TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除'
) COMMENT '队伍';

2. 用户-队伍关系表(user_team)设计

这个表用于存储用户与队伍之间的关系,表明哪些用户加入了哪些队伍,以及相关的时间信息。

字段设计:

  • id:关系表的唯一标识符,主键,BIGINT类型,自增长。
  • userId:用户ID,BIGINT类型,不能为空。
  • teamId:队伍ID,BIGINT类型,不能为空。
  • joinTime:用户加入队伍的时间,DATETIME类型,不能为空。
  • createTime:记录创建时间,DATETIME类型,默认为当前时间。
  • updateTime:记录更新时间,DATETIME类型,默认为当前时间,且会在更新时自动更新。
  • isDelete:逻辑删除标志,TINYINT类型,0 表示未删除,1 表示已删除,默认为 0。

表结构SQL:

CREATE TABLE user_team (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '关系ID',
    userId      BIGINT NOT NULL COMMENT '用户ID',
    teamId      BIGINT NOT NULL COMMENT '队伍ID',
    joinTime    DATETIME NULL COMMENT '加入时间',
    createTime  DATETIME DEFAULT CURRENT_TIMESTAMP NULL COMMENT '创建时间',
    updateTime  DATETIME DEFAULT CURRENT_TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    isDelete    TINYINT DEFAULT 0 NOT NULL COMMENT '是否删除'
) COMMENT '用户队伍关系';

3. 优化设计

  • 索引设计:对于高频查询的字段(如 teamIduserId),建议建立索引来提高查询性能。
  CREATE INDEX idx_team_id ON user_team (teamId);
  CREATE INDEX idx_user_id ON user_team (userId);
  • 外键约束:为了确保数据一致性,可以在 user_team 表中添加外键约束来连接 team 表和 user 表。
  ALTER TABLE user_team
  ADD CONSTRAINT fk_team_id FOREIGN KEY (teamId) REFERENCES team(id);

  ALTER TABLE user_team
  ADD CONSTRAINT fk_user_id FOREIGN KEY (userId) REFERENCES user(id);

pojo

Team

package com.zwnsyw.backend.pojo.domain;

import com.baomidou.mybatisplus.annotation.*;
import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.zwnsyw.backend.pojo.enums.TeamEnums.TeamStatus;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;

/**
 * 队伍
 *
 * @TableName  team
 * @author Zwww
 */
@TableName(value = "team")
@Data
public class Team implements Serializable {

    // 队伍ID
    @TableId(type = IdType.AUTO)
    private Long id;

    // 队伍名称
    @NotBlank(message = "队伍名称不能为空")
    @Size(max = 256, message = "队伍名称长度不能超过256")
    private String name;

    // 队伍描述
    @Size(max = 1024, message = "队伍描述长度不能超过1024")
    private String description;

    // 最大人数
    @Min(value = 1, message = "最大人数必须大于等于1")
    private Integer maxNum;

    // 过期时间
    @Future(message = "过期时间必须大于当前时间")
    private Date expireTime;

    // 创建人ID
    private Long userId;

    // 队伍状态
    @NotNull(message = "队伍状态不能为空")
    private TeamStatus status;

    // 密码
    private String password;

    // 创建时间
    private Date createTime;

    // 更新时间
    private Date updateTime;

    // 逻辑删除标志
    @TableLogic
    private Integer isDelete;

    // 序列化ID
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

enums

Gender:
package com.zwnsyw.backend.pojo.enums.UserEnums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

public enum Gender {
    MALE("MALE", "男"),
    FEMALE("FEMALE", "女"),
    UNKNOWN("UNKNOWN", "保密");

    @EnumValue
    private final String code;

    @JsonValue
    private final String description;

    Gender(String code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    public static Gender fromCode(String code) {
        for (Gender gender : values()) {
            if (gender.getCode().equals(code)) {
                return gender;
            }
        }
        throw new IllegalArgumentException("Unknown gender code: " + code);
    }
}
UserRole
package com.zwnsyw.backend.pojo.enums.UserEnums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

public enum UserRole {
    USER("USER", "普通用户"),
    ADMIN("ADMIN", "管理员");

    @EnumValue
    private final String code;

    @JsonValue
    private final String description;

    UserRole(String code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    public static UserRole fromCode(String code) {
        for (UserRole role : UserRole.values()) {
            if (role.getCode().equals(code)) {
                return role;
            }
        }
        throw new IllegalArgumentException("Unknown user role code: " + code);
    }
}
UserStatus
package com.zwnsyw.backend.pojo.enums.UserEnums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;

public enum UserStatus {
    ACTIVE("ACTIVE", "正常"),
    DISABLED("DISABLED", "禁用");

    @EnumValue
    private final String code;

    @JsonValue
    private final String description;

    UserStatus(String code, String description) {
        this.code = code;
        this.description = description;
    }

    public String getCode() {
        return code;
    }

    public String getDescription() {
        return description;
    }

    public static UserStatus fromCode(String code) {
        for (UserStatus status : UserStatus.values()) {
            if (status.getCode().equals(code)) {
                return status;
            }
        }
        throw new IllegalArgumentException("Unknown user status code: " + code);
    }
}

UserTeam

package com.zwnsyw.backend.pojo;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 用户队伍关系
 * @TableName user_team
 */
@TableName(value ="user_team")
@Data
public class  UserTeam implements Serializable {
    /**
     * 关系ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 队伍ID
     */
    private Long teamId;

    /**
     * 加入时间
     */
    private Date joinTime;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 更新时间
     */
    private Date updateTime;

    /**
     * 是否删除
     */
    @TableLogic
    private Integer isDelete;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

系统设计

创建队伍接口

接口描述:

用户可以创建一个新的队伍,并设置队伍的属性,如人数、标题、描述、超时时间等。

请求路径:

POST /team/add

请求参数:

  • title: 队伍标题,必填,最大长度 20。
  • description: 队伍描述,必填,最大长度 512。
  • max_members: 队伍最大人数,必填,限制范围:2 <= max_members <= 20
  • status: 队伍的状态(公开、私有、加密),默认为 0(公开)。
  • password: 如果队伍是加密状态,必填,最大长度 32。
  • expire_time: 队伍的过期时间,必须大于当前时间。
  • user_id: 当前创建队伍的用户 ID,自动获取(从登录状态中获取)。

请求验证:

  • 校验用户是否已登录(未登录不能创建队伍)。
  • 校验队伍人数是否符合条件。
  • 校验队伍标题和描述是否符合长度要求。
  • 校验超时时间是否大于当前时间。
  • 校验用户是否已创建超过5个队伍(用户不能同时拥有超过5个队伍)。

业务流程:

  1. 校验请求参数是否有效。
  2. 校验用户是否登录,并检查已创建队伍数是否超过上限。
  3. 创建队伍并插入队伍表。
  4. 将用户与队伍建立关联,插入用户-队伍关系表。
  5. 返回创建成功信息。

DTO

package com.zwnsyw.backend.pojo.dto.team;

import lombok.Data;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;

/**
 * 创建队伍请求体
 *
 * @author Zwww
 */
@Data
public class TeamAddRequest implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 队伍名称
     */
    @NotBlank(message = "队伍名称不能为空")
    @Size(max = 256, message = "队伍名称长度不能超过256")
    private String name;

    /**
     * 描述
     */
    @Size(max = 1024, message = "队伍描述长度不能超过1024")
    private String description;

    /**
     * 最大人数
     */
    @Min(value = 1, message = "最大人数必须大于等于1")
    @Max(value = 20, message = "最大人数不能大于20")
    private Integer maxNum;

    /**
     * 过期时间
     */
    @NotNull(message = "过期时间不能为空")
    @Future(message = "过期时间必须大于当前时间")
    private Date expireTime;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 0 - 公开,1 - 私有,2 - 加密
     */
    @NotNull(message = "队伍状态不能为空")
    @Range(min = 0, max = 2, message = "队伍状态值无效")
    private Integer status;

    /**
     * 密码
     */
    @Size(max = 32, message = "密码长度不能超过32")
    private String password;

    /**
     * 校验队伍名称是否合法
     */
    @AssertTrue(message = "队伍名称不合法")
    public boolean isValidTeamName() {
        return this.name.matches("[A-Za-z0-9\\u4e00-\\u9fa5]+"); // 只允许字母、数字、汉字
    }
}

mapper

package com.zwnsyw.backend.mapper;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zwnsyw.backend.pojo.Team;

/**
* @author Zwww
* @description 针对表【team(队伍)】的数据库操作Mapper
* @createDate 2024-12-07 18:03:46
* @Entity com.zwnsyw.bankend.pojo.Team
*/
public interface TeamMapper extends BaseMapper<Team> {
    // 自定义查询,统计用户创建的队伍数
    default long countTeamsByUserId(long userId) {
        QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userId", userId);
        return this.selectCount(queryWrapper);
    }

}

controller

  @PostMapping("/add")
    public BaseResponse<Long> addTeam(@RequestBody @Valid TeamAddRequest teamAddRequest, HttpServletRequest request) {
        if (teamAddRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }

        User loginUser = userService.getLoginUser(request);
        if (loginUser == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN, "用户未登录");
        }

        long teamId = teamService.addTeam(teamAddRequest, loginUser);

        return ResultUtils.success(teamId);
    }

service

/**
     * 创建队伍
     *
     * @param teamAddRequest 创建队伍的请求参数
     * @param loginUser 当前登录的用户
     * @return 新队伍的 ID
     */
    long addTeam(TeamAddRequest teamAddRequest, User loginUser);

serviceimpl

使用redisson分布式锁来解决 用户短时间点击多次可能创建多个队伍的问题

因为查询写入缓存 添加成功后 redis并没有新增内容 为了双库同步 需要手动更新 这里用最简单的方式实现 一旦添加队伍 就清除所有seach的缓存 后续可以细粒化优化

@Override
    @Transactional(rollbackFor = Exception.class)  //使用数据库事务管理,确保“队伍创建”与“用户与队伍关系创建”这两个操作要么都成功,要么都回滚,避免数据不一致问题。
    public long addTeam(TeamAddRequest teamAddRequest, UserVO loginUser) {
        // 分布式锁防止同一个用户重复创建队伍
        String lockKey = "lock:addTeam:" + loginUser.getId();
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 尝试获取分布式锁,最多等待5秒,锁定10秒
            boolean lockAcquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!lockAcquired) {
                log.warn("获取分布式锁失败,用户ID: {},锁Key: {}", loginUser.getId(), lockKey);
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统繁忙,请稍后再试");
            }

            // 校验队伍状态
            int status = Optional.ofNullable(teamAddRequest.getStatus()).orElse(0); // 默认为公开
            TeamStatus statusEnum = TeamStatus.fromCode(status);

            // 校验加密状态下的密码
            if (statusEnum == TeamStatus.ENCRYPTED &&
                    (StringUtils.isBlank(teamAddRequest.getPassword()) || teamAddRequest.getPassword().length() > 32)) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码设置不正确");
            }

            // 校验用户最多创建队伍数为 5
            long userId = loginUser.getId();
            long existingTeams = teamMapper.countTeamsByUserId(userId);
            if (existingTeams >= 5) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建 5 个队伍");
            }

            //创建队伍
            Team team = new Team();
            BeanUtils.copyProperties(teamAddRequest, team);
            team.setUserId(userId);
            team.setStatus(statusEnum);
            boolean teamCreated = teamMapper.insert(team) > 0;
            if (!teamCreated) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍创建失败");
            }
            long teamId = team.getId();

            //建立用户和队伍的关系
            UserTeam userTeam = new UserTeam();
            userTeam.setUserId(userId);
            userTeam.setTeamId(teamId);
            userTeam.setJoinTime(new Date());

            boolean userTeamCreated = userTeamService.save(userTeam);
            if (!userTeamCreated) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建用户和队伍关系失败");
            }

            // 清除缓存
            clearTeamCache();
            return teamId;
        } catch (BusinessException e) {
            throw e; // 将业务异常直接抛出,前端可以收到详细错误信息
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "系统内部异常,请稍后再试");
        } finally {
            // 确保在异常情况下也能释放分布式锁
            if (lock.isHeldByCurrentThread()) {
                log.info("释放分布式锁,用户ID: {}", loginUser.getId());
                lock.unlock();
            }
        }
    }
    /**
     * 数据变动时清除缓存
     */
    private void clearTeamCache() {
        String searchKey = "FriendMarry:team:search:" + "*"; // todo 可以根据不同条件清理 细粒化
        Set<String> searchKeys = redisTemplate.keys(searchKey);
        if (searchKeys != null && !searchKeys.isEmpty()) {
            redisTemplate.delete(searchKeys);
            log.info("缓存已清除,key: {}", searchKey);
        }
    }

查询队伍列表接口

接口描述:

分页展示队伍列表,用户可以根据名称、最大人数等条件查询队伍。

请求路径:

GET /team/list

请求参数:

  • title: 队伍标题,模糊匹配,非必填。
  • max_members: 最大人数,非必填。
  • status: 队伍状态(公开、私有、加密),非必填。
  • page: 页码,必填。
  • size: 每页显示的队伍数,必填。

请求验证:

  • 校验分页参数是否有效。
  • 只有管理员才能查看加密或私有的队伍。

业务流程:

  1. 根据请求参数构造查询条件,分页查询队伍。
  2. 过滤已过期的队伍(根据 expire_time 判断)。
  3. 根据权限过滤队伍,非管理员只能查看公开队伍。
  4. 返回队伍列表及其创建者和成员信息。

后端逻辑

  • Controller:接收请求并进行基本的参数校验,调用服务层方法进行查询。
  • Service:接收查询条件,处理分页,判断用户权限,查询符合条件的队伍列表。
  • ServiceImpl
    • 从 Redis 缓存中获取队伍列表,如果缓存未命中,则执行数据库查询。
    • 根据查询条件构造 QueryWrapper 进行数据库查询。
    • 过滤队伍(例如排除已过期的队伍,管理员可以访问所有队伍,非管理员只能访问公开队伍)。
    • 通过 TeamVO 将查询结果转化为适合前端展示的数据。
  • Redis 缓存:将查询结果缓存,避免频繁查询数据库。

数据格式化

  • 在返回的 TeamVO 中,需要对日期字段进行格式化,并且对队伍状态进行描述性转换。

DTO

 package com.zwnsyw.backend.pojo.dto.team;

import com.zwnsyw.backend.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.Size;
import java.util.List;

/**
 * 队伍查询封装类
 *
 * @author Zwww
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class TeamQueryRequest extends PageRequest {

    private static final long serialVersionUID = 1L;

    /**
     * 队伍ID
     */
    private Long id;

    /**
     * 队伍ID列表
     */
    private List<Long> idList;

    /**
     * 搜索关键词(可以同时对队伍名称和描述进行模糊搜索)
     */
    private String searchText;

    /**
     * 队伍名称
     */
    @Size(max = 100, message = "队伍名称长度不能超过100")
    private String name;

    /**
     * 描述
     */
    @Size(max = 500, message = "队伍描述长度不能超过500")
    private String description;

    /**
     * 最大人数
     */
    private Integer maxNum;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 队伍状态(0-公开,1-私有,2-加密)
     */
    private Integer status;
}

controller

 /**
     *  查询队伍列表
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param request 请求上下文
     * @return BaseResponse<List<TeamVO>>
     */
    @GetMapping("/list")
    public BaseResponse<List<TeamVO>> listTeams(@Valid TeamQueryRequest teamQueryRequest, HttpServletRequest request) {
        if (teamQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }

        // 获取当前登录用户信息
        UserVO loginUser = userService.getLoginUser(request);
		if (loginUser == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN, "用户未登录");
        }
        // 查询队伍列表并返回
        List<TeamVO> teamList = teamService.listTeams(teamQueryRequest, loginUser);

        return ResultUtils.success(teamList);
    }

service

  /**
     * 搜索队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param loginUser        当前登录用户
     * @return 返回符合查询条件的队伍列表
     */
    List<TeamVO> listTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser);

serviceimpl

 /**

     * 搜索队伍

     *

     * @param teamQueryRequest 查询队伍的请求参数

     * @param loginUser        当前登录用户

     * @return 返回符合查询条件的队伍列表

     */

    @Override

    public List<TeamVO> listTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) {

        // 判断是否为管理员

        boolean isAdmin = userService.isAdmin(loginUser);

        String redisKey = generateRedisKey(teamQueryRequest, loginUser);  // 根据查询参数和用户生成缓存Key

        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();

        // 尝试从 Redis 缓存中获取数据

        List<TeamVO> teamVOList = getFromRedis(redisKey, valueOperations);

        if (teamVOList != null) {

            return teamVOList;

        }

        // 查询队伍列表

        QueryWrapper<Team> queryWrapper = new QueryWrapper<>();

        // 构建查询条件

        addQueryConditions(queryWrapper, teamQueryRequest, isAdmin);

        List<Team> teamList = this.list(queryWrapper);

        if (CollectionUtils.isEmpty(teamList)) {

            return new ArrayList<>();

        }

        // 获取当前用户已加入的队伍ID集合

        Set<Long> hasJoinTeamIdSet = getUserJoinedTeams(teamList.stream().map(Team::getId).collect(Collectors.toList()), loginUser);

        // 查询所有队伍的加入人数

        Map<Long, Long> teamJoinCountMap = getTeamJoinCounts(teamList.stream().map(Team::getId).collect(Collectors.toList()));

        // 关联查询队伍创建人的用户信息

        List<TeamVO> teamVOListFromDb = new ArrayList<>();

        teamList.forEach(team -> {

            User user = userService.getById(team.getUserId());

            boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());

            Integer hasJoinNum = teamJoinCountMap.getOrDefault(team.getId(), 0L).intValue();

            TeamVO teamVO = TeamVO.fromTeam(team,

                    user != null ? UserVO.fromUser(user) : null,

                    hasJoinNum,

                    hasJoin);

            teamVOListFromDb.add(teamVO);

        });

        // 将查询结果写入缓存

        saveToRedis(redisKey, teamVOListFromDb, valueOperations);

        return teamVOListFromDb;

    }

    /**

     * 生成缓存Key,根据查询条件和当前用户

     * utils for listTeams

     */

    private String generateRedisKey(TeamQueryRequest teamQueryRequest, UserVO loginUser) {

        String baseKey = String.format("FriendMarry:team:search:%s", loginUser.getId());

        // todo 可以根据队伍查询条件生成更细粒度的Key

        if (teamQueryRequest.getId() != null) {

            baseKey += ":id:" + teamQueryRequest.getId();

        }

        if (teamQueryRequest.getSearchText() != null) {

            baseKey += ":search:" + teamQueryRequest.getSearchText();

        }

        if (teamQueryRequest.getStatus() != null) {

            baseKey += ":status:" + teamQueryRequest.getStatus();

        }

        if (teamQueryRequest.getMaxNum() != null) {

            baseKey += ":maxNum:" + teamQueryRequest.getMaxNum();

        }

        return baseKey;

    }

    /**

     * 从 Redis 获取缓存数据

     * utils for listTeams

     */

    private List<TeamVO> getFromRedis(String redisKey, ValueOperations<String, Object> valueOperations) {

        try {

            Object cachedData = valueOperations.get(redisKey);

            if (cachedData != null) {

                log.info("Redis缓存成功命中,key: {}", redisKey);

                return (List<TeamVO>) cachedData;

            }

        } catch (Exception e) {

            log.error("Redis缓存读取失败,key: {}", redisKey, e);

            throw new BusinessException(ErrorCode.DATABASE_ERROR, "缓存读写失败");

        }

        return null;

    }

    /**

     * 将查询结果写入 Redis 缓存

     * utils for listTeams

     */

    private void saveToRedis(String redisKey, List<TeamVO> teamVOList, ValueOperations<String, Object> valueOperations) {

        try {

            if (teamVOList != null) {

                valueOperations.set(redisKey, teamVOList, 30, TimeUnit.MINUTES); // 缓存过期时间30分钟,可配置化

                log.info("Redis缓存写入成功,key: {}", redisKey);

            } else {

                log.warn("查询到的队伍列表为空,未写入缓存,key: {}", redisKey);

            }

        } catch (Exception e) {

            log.error("Redis缓存写入失败,key: {}", redisKey, e);

            throw new BusinessException(ErrorCode.DATABASE_ERROR, "缓存写入失败");

        }

    }

    /**

     * 添加查询条件,用于根据请求参数过滤队伍

     * utils for listTeams

     */

    /* 最终sql语句类似:

            SELECT id,

                   name,

                   description,

                   maxNum,

                   expireTime,

                   userId,

                   status,

                   password,

                   createTime,

                   updateTime,

                   isDelete

            FROM team

            WHERE isDelete = 0

              AND (id = ? AND (name LIKE ? OR description LIKE ?) AND name LIKE ? AND description LIKE ? AND maxNum = ? AND

                   userId = ? AND status = ? AND (expireTime > ? OR expireTime IS NULL))

         */

    private void addQueryConditions(QueryWrapper<Team> queryWrapper, TeamQueryRequest teamQueryRequest, boolean isAdmin) {

        // 添加队伍ID条件

        if (teamQueryRequest.getId() != null && teamQueryRequest.getId() > 0) {

            queryWrapper.eq("id", teamQueryRequest.getId());

        }

        // 添加队伍ID集合条件

        if (teamQueryRequest.getIdList() != null && !teamQueryRequest.getIdList().isEmpty()) {

            queryWrapper.in("id", teamQueryRequest.getIdList());

        }

        // 添加搜索文本条件(包含队伍名称或描述)

        if (StringUtils.isNotBlank(teamQueryRequest.getSearchText())) {

            queryWrapper.like("name", teamQueryRequest.getSearchText())

                    .or()

                    .like("description", teamQueryRequest.getSearchText());

        }

        // 添加队伍名称条件

        if (StringUtils.isNotBlank(teamQueryRequest.getName())) {

            queryWrapper.like("name", teamQueryRequest.getName());

        }

        // 添加队伍描述条件

        if (StringUtils.isNotBlank(teamQueryRequest.getDescription())) {

            queryWrapper.like("description", teamQueryRequest.getDescription());

        }

        // 添加最大人数条件

        if (teamQueryRequest.getMaxNum() != null && teamQueryRequest.getMaxNum() > 0) {

            queryWrapper.eq("maxNum", teamQueryRequest.getMaxNum());

        }

        // 添加用户ID条件

        if (teamQueryRequest.getUserId() != null && teamQueryRequest.getUserId() > 0) {

            queryWrapper.eq("userId", teamQueryRequest.getUserId());

        }

        // 添加队伍状态条件(需要检查是否为管理员,避免访问私人队伍)

        if (teamQueryRequest.getStatus() != null) {

            TeamStatus statusEnum = TeamStatus.fromCode(teamQueryRequest.getStatus());

            if (!isAdmin && statusEnum == TeamStatus.PRIVATE) {

                throw new BusinessException(ErrorCode.NO_AUTH, "无权限");

            }

            queryWrapper.eq("status", statusEnum.getCode());

        }

        // 添加过期时间条件(查询未过期或无过期时间的队伍)

        queryWrapper.and(qw -> qw.gt("expireTime", new Date()).or().isNull("expireTime"));

    }

    /**

     * 获取当前用户已加入的队伍ID集合

     * utils for listTeams

     */

    private Set<Long> getUserJoinedTeams(List<Long> teamIdList, UserVO loginUser) {

        QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();

        userTeamQueryWrapper.eq("userId", loginUser.getId()).in("teamId", teamIdList);

        List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);

        return userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());

    }

    /**

     * 获取每个队伍的加入人数

     * utils for listTeams

     */

    private Map<Long, Long> getTeamJoinCounts(List<Long> teamIdList) {

        QueryWrapper<UserTeam> userTeamJoinQueryWrapper = new QueryWrapper<>();

        userTeamJoinQueryWrapper.in("teamId", teamIdList);

        List<UserTeam> allUserTeams = userTeamService.list(userTeamJoinQueryWrapper);

        return allUserTeams.stream()

                .collect(Collectors.groupingBy(UserTeam::getTeamId, Collectors.counting()));

    }

VO

 package com.zwnsyw.backend.pojo.vo;

import com.zwnsyw.backend.pojo.domain.Team;
import lombok.Data;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 队伍和用户信息封装类(脱敏)
 *
 * @author Zwww
 */
@Data
public class TeamVO implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    private Long id;

    /**
     * 队伍名称
     */
    private String name;

    /**
     * 描述
     */
    private String description;

    /**
     * 最大人数
     */
    private Integer maxNum;

    /**
     * 过期时间(格式化为字符串)
     */
    private String expireTime;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 队伍状态(描述性字符串)
     */
    private Integer status;

    /**
     * 创建时间
     */
    private String createTime;

    /**
     * 更新时间
     */
    private String updateTime;

    /**
     * 创建人用户信息
     */
    private UserVO createUser;

    /**
     * 已加入的用户数
     */
    private Integer hasJoinNum;

    /**
     * 当前用户是否已加入队伍
     */
    private boolean hasJoin;

    /**
     * 从 Team 实体类转换为 TeamVO
     */
    public static TeamVO fromTeam(Team team, UserVO createUser, Integer hasJoinNum, boolean hasJoin) {
        TeamVO vo = new TeamVO();
        vo.setId(team.getId());
        vo.setName(team.getName());
        vo.setDescription(team.getDescription());
        vo.setMaxNum(team.getMaxNum());

        // 格式化过期时间
        vo.setExpireTime(team.getExpireTime() != null ? formatDate(team.getExpireTime()) : null);

        vo.setUserId(team.getUserId());

        // 将队伍状态从枚举转换为描述字符串
        vo.setStatus(team.getStatus().getCode());

        // 格式化时间字段
        vo.setCreateTime(team.getCreateTime() != null ? formatDate(team.getCreateTime()) : null);
        vo.setUpdateTime(team.getUpdateTime() != null ? formatDate(team.getUpdateTime()) : null);

        // 设置创建人信息
        vo.setCreateUser(createUser);

        // 设置已加入的用户数和当前用户是否已加入
        vo.setHasJoinNum(hasJoinNum);
        vo.setHasJoin(hasJoin);

        return vo;
    }

    /**
     * 格式化日期为指定格式(yyyy-MM-dd HH:mm:ss)
     */
    private static String formatDate(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
}

根据id查询队伍接口

接口描述:

队伍详情页面 从队伍列表中点击进入详情页 为修改接口做铺垫

请求路径:

get /team/get

请求参数:

  • **id:**队伍id,必填

请求验证:

  • 校验id是否有效。
  • 是否登录

业务流程:

  1. 根据team_id查询队伍
  2. 返回队伍信息VO

controller:

/**
     * 根据id查找队伍
     *
     * @param id      队伍id
     * @param request 当前请求
     * @return BaseResponse<TeamVO>
     */
    @GetMapping("/get")
    public BaseResponse<TeamVO> getTeamById(@RequestParam("id") long id, HttpServletRequest request) {
        if (id <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍id不合法");
        }
        // 获取当前登录用户信息
        UserVO loginUser = userService.getLoginUser(request);
        if (loginUser == null) {
            throw new BusinessException(ErrorCode.NOT_LOGIN, "用户未登录");
        }

        TeamVO teamVO = teamService.getTeamById(id, loginUser);

        return ResultUtils.success(teamVO);
    }

service:

    /**
     * 根据id查找队伍
     *
     * @param teamId        队伍id
     * @param loginUser 当前登录用户
     * @return 查询到的team封装VO
     */
    TeamVO getTeamById(long teamId, UserVO loginUser);

serviceimpl:

这里修改比较频繁 与update配套使用 一般调用就要修改 没必要加上缓存

  /**
     * 根据id查找队伍
     *
     * @param teamId        队伍id
     * @param loginUser 当前登录用户
     * @return 查询到的team封装VO
     */
    @Override
    public TeamVO getTeamById(long teamId, UserVO loginUser) {

        // 查询队伍信息
        Team team = this.getById(teamId);
        if (team == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
        }

        // 获取队伍创建人信息
        User user = userService.getById(team.getUserId());
        boolean hasJoin = getUserJoinedTeams(Collections.singletonList(teamId), loginUser).contains(teamId);
        Integer hasJoinNum = getTeamJoinCounts(Collections.singletonList(teamId)).getOrDefault(teamId, 0L).intValue();

        return TeamVO.fromTeam(team, user != null ? UserVO.fromUser(user) : null, hasJoinNum, hasJoin);
    }

修改队伍信息接口

接口描述:

队长或管理员可以修改队伍的名称、描述等信息。

请求路径:

PUT /team/{team_id}/update

请求参数:

  • title: 队伍标题,非必填。
  • description: 队伍描述,非必填。
  • max_members: 队伍最大人数,非必填。
  • status: 队伍状态,非必填。
  • password: 如果队伍状态修改为加密状态,必填。

请求验证:

  • 校验队伍是否存在。
  • 只有队长或管理员才能修改队伍信息。
  • 校验新值与旧值是否相同,如果相同则不做更新。

业务流程:

  1. 校验队伍是否存在。
  2. 校验用户是否是队长或管理员。
  3. 校验队伍状态是否修改为加密状态,如果是,检查密码是否填写。
  4. 更新队伍信息。

DTO

 package com.zwnsyw.backend.pojo.dto.team;

import javax.validation.constraints.Future;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.zwnsyw.backend.pojo.enums.TeamEnums.TeamStatus;
import lombok.Data;

import java.util.Date;

/**
 * 队伍更新请求体
 *
 * @author Zwww
 */
@Data
public class TeamUpdateRequest {

    private Long id; // 队伍ID,用于查询和更新

    @Size(max = 50, message = "队伍标题最大长度为50字符")
    private String name; // 队伍标题,非必填

    @Size(max = 200, message = "队伍描述最大长度为200字符")
    private String description; // 队伍描述,非必填

    @Future(message = "过期时间必须大于当前时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date expireTime;//过期时间

    private Integer maxNum; // 队伍最大人数,非必填

    private TeamStatus status; // 队伍状态,非必填

    @Size(min = 6, max = 20, message = "密码长度必须在6到20个字符之间")
    private String password; // 密码字段,只有当队伍状态为加密时,才需要此字段,必填

}

controller

  /**
     * 更新队伍信息
     *
     * @param teamUpdateRequest 更新队伍的请求参数
     * @param request           当前请求
     * @return 修改后的队伍状态
     */
    @PutMapping("/{team_id}/update")
    public BaseResponse<Boolean> updateTeam(
            @RequestBody @Validated TeamUpdateRequest teamUpdateRequest,
            @PathVariable Long team_id,
            HttpServletRequest request) {

        if (teamUpdateRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }

        teamUpdateRequest.setId(team_id);

        UserVO loginUser = userService.getLoginUser(request);
        boolean result = teamService.updateTeam(teamUpdateRequest, loginUser);
        if (!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新失败");
        }
        return ResultUtils.success(true);
    }

service

 /**
     * 更新队伍
     *
     * @param teamUpdateRequest 更新队伍的请求参数
     * @param loginUser         当前登录用户
     * @return 是否更新成功
     */
    boolean updateTeam(TeamUpdateRequest teamUpdateRequest, UserVO loginUser);

serviceimpl

 /**
     * 更新队伍
     *
     * @param teamUpdateRequest 更新队伍的请求参数
     * @param loginUser         当前登录用户
     * @return 是否更新成功
     */
    @Override
    public boolean updateTeam(TeamUpdateRequest teamUpdateRequest, UserVO loginUser) {

        Long id = teamUpdateRequest.getId();
        if (id == null || id <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "无效的队伍ID");
        }

        Team oldTeam = this.getById(id);
        if (oldTeam == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
        }

        // 只有管理员或者队伍的创建者可以修改
        if (oldTeam.getUserId() != loginUser.getId() && !userService.isAdmin(loginUser)) {
            throw new BusinessException(ErrorCode.NO_AUTH,"无权限");
        }

        // 如果状态修改为加密,必须提供密码
        if (teamUpdateRequest.getStatus() == TeamStatus.ENCRYPTED &&
                (teamUpdateRequest.getPassword() == null || teamUpdateRequest.getPassword().isEmpty())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "加密队伍必须提供密码");
        }

        // 判断新值是否和旧值一样,如果一样则不做更新
        boolean isUpdated = false;

        if (teamUpdateRequest.getName() != null && !teamUpdateRequest.getName().equals(oldTeam.getName())) {
            oldTeam.setName(teamUpdateRequest.getName());
            isUpdated = true;
        }

        if (teamUpdateRequest.getDescription() != null && !teamUpdateRequest.getDescription().equals(oldTeam.getDescription())) {
            oldTeam.setDescription(teamUpdateRequest.getDescription());
            isUpdated = true;
        }

        if (teamUpdateRequest.getMaxNum() != null && !teamUpdateRequest.getMaxNum().equals(oldTeam.getMaxNum())) {
            oldTeam.setMaxNum(teamUpdateRequest.getMaxNum());
            isUpdated = true;
        }

        if (teamUpdateRequest.getStatus() != null && !teamUpdateRequest.getStatus().equals(oldTeam.getStatus())) {
            oldTeam.setStatus(teamUpdateRequest.getStatus());
            isUpdated = true;
        }

        if (teamUpdateRequest.getPassword() != null && !teamUpdateRequest.getPassword().equals(oldTeam.getPassword())) {
            oldTeam.setPassword(teamUpdateRequest.getPassword());
            isUpdated = true;
        }

        // 如果没有变化,不进行更新
        if (!isUpdated) {
            return false;
        }

        // 更新队伍信息
        boolean updateSuccessful = this.updateById(oldTeam);

        if (updateSuccessful) {
            // 清除缓存
            clearTeamCache();
        }

        return updateSuccessful;

    }

加入队伍接口

接口描述:

用户可以加入一个队伍,必须满足条件。

请求路径:

POST /team/{team_id}/join

请求参数:

  • password: 如果队伍是加密的,必填。
  • user_id: 当前用户的 ID,自动获取(从登录状态中获取)。

请求验证:

  • 校验队伍是否存在。
  • 校验队伍是否已满员。
  • 校验队伍是否已过期。
  • 校验用户是否已加入该队伍。
  • 校验用户是否已加入超过5个队伍。
  • 校验加密队伍的密码是否正确。

业务流程:

  1. 校验队伍是否存在并且未满员、未过期。
  2. 校验用户是否已加入该队伍,防止重复加入。
  3. 如果是加密队伍,验证密码。
  4. 添加用户与队伍的关联信息。

DTO

package com.zwnsyw.backend.pojo.dto.team;

import lombok.Data;

import javax.validation.constraints.Size;
import java.io.Serializable;

/**
 * 用户加入队伍请求体
 *
 * @author Zwww
 */
@Data
public class TeamJoinRequest implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    private Long teamId;

    /**
     * 密码
     */
    @Size(max = 32, message = "密码长度不能超过32")
    private String password;
}

controller:

   /**
     * @param teamJoinRequest 加入队伍的请求参数
     * @param request         当前请求
     * @return BaseResponse<Boolean>
     */
    @PostMapping("/join")
    public BaseResponse<Boolean> joinTeam(@RequestBody @Valid TeamJoinRequest teamJoinRequest, HttpServletRequest request) {
        if (teamJoinRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }
        UserVO loginUser = userService.getLoginUser(request);
        boolean result = teamService.joinTeam(teamJoinRequest, loginUser);
        return ResultUtils.success(result);
    }

service

 /**
     * 加入队伍
     *
     * @param teamJoinRequest 加入队伍的请求参数
     * @param loginUser 当前登录用户
     * @return 是否加入成功
     */
    boolean joinTeam(TeamJoinRequest teamJoinRequest, UserVO loginUser);

serviceimpl

/**
     * 加入队伍
     *
     * @param teamJoinRequest 加入队伍的请求参数
     * @param loginUser       当前登录用户
     * @return 是否加入成功
     */
    @Override
    public boolean joinTeam(TeamJoinRequest teamJoinRequest, UserVO loginUser) {

        Long teamId = teamJoinRequest.getTeamId();
        Team team = getTeamById(teamId);

        Date expireTime = team.getExpireTime();
        if (expireTime != null && expireTime.before(new Date())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
        }

        if (TeamStatus.PRIVATE.equals(team.getStatus())) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
        }

        String password = teamJoinRequest.getPassword();
        if (TeamStatus.ENCRYPTED.equals(team.getStatus())) {
            if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
                throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
            }
        }

        long userId = loginUser.getId();

        // 使用分布式锁来避免并发操作
        RLock lock = redissonClient.getLock("FriendMarry:join_team");
        try {
            if (lock.tryLock(0, 5, TimeUnit.SECONDS)) {  // 设置最大重试时间
                log.info("JoinTeam getLock: " + Thread.currentThread().getId());

                QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
                userTeamQueryWrapper.eq("userId", userId);
                long hasJoinNum = userTeamService.count(userTeamQueryWrapper);

                if (hasJoinNum >= 5) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
                }

                // 查询用户是否已加入该队伍
                userTeamQueryWrapper.eq("teamId", teamId);
                long hasUserJoinTeam = userTeamService.count(userTeamQueryWrapper);
                if (hasUserJoinTeam > 0) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
                }

                // 查询队伍当前人数
                long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
                if (teamHasJoinNum >= team.getMaxNum()) {
                    throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已满");
                }

                // 创建用户与队伍的关联记录
                UserTeam userTeam = new UserTeam();
                userTeam.setUserId(userId);
                userTeam.setTeamId(teamId);
                userTeam.setJoinTime(new Date());

                //加入成功删除缓存
                clearTeamCache();

                // 保存用户与队伍的关联信息
                return userTeamService.save(userTeam);
            } else {
                throw new BusinessException(ErrorCode.SYSTEM_ERROR, "加入队伍失败,锁竞争超时");
            }
        } catch (InterruptedException e) {
            log.error("加入队伍时发生错误", e);
            return false;
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                log.info("JoinTeam unLock: " + Thread.currentThread().getId());
                lock.unlock();
            }
        }
    }

    /**
     * 根据 id 获取队伍信息
     */
    private Team getTeamById(Long teamId) {
        if (teamId == null || teamId <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍id不合法");
        }
        Team team = this.getById(teamId);
        if (team == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
        }
        return team;
    }

    /**
     * 获取某队伍当前人数
     */
    private long countTeamUserByTeamId(long teamId) {
        QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
        userTeamQueryWrapper.eq("teamId", teamId);
        return userTeamService.count(userTeamQueryWrapper);
    }

退出队伍接口

接口描述:

用户可以退出自己加入的队伍。

请求路径:

POST /team/{team_id}/quit

请求参数:

  • team_id: 队伍 ID,必填。

请求验证:

  • 校验队伍是否存在。
  • 校验用户是否已加入该队伍。
  • 如果是队长退出,判断是否需要转让队伍或解散队伍。

业务流程:

  1. 校验队伍是否存在。
  2. 校验用户是否加入该队伍。
  3. 如果队长退出,判断是否需要转让队伍。
  4. 如果只剩一个队员,解散队伍。
  5. 删除用户与队伍的关联。

controller:

/**
     * 退出队伍
     *
     * @param teamId  要退出的队伍id
     * @param request 当前请求
     * @return BaseResponse<Boolean>
     */
    @PostMapping("{team_id}/quit")
    public BaseResponse<Boolean> quitTeam(@PathVariable("team_id") Long teamId, HttpServletRequest request) {
        if (teamId == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }
        UserVO loginUser = userService.getLoginUser(request);
        boolean result = teamService.quitTeam(teamId, loginUser);
        return ResultUtils.success(result);
    }

service

 /**
     * 退出队伍
     *
     * @param teamId    要退出的队伍id
     * @param loginUser 当前登录用户
     * @return 是否退出成功
     */
    boolean quitTeam(Long teamId, UserVO loginUser);

serviceimpl

 /**
     * 退出队伍
     *
     * @param teamId    要退出的队伍id
     * @param loginUser 当前登录用户
     * @return 是否退出成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean quitTeam(Long teamId, UserVO loginUser) {

        Team team = getTeamById(teamId);

        long userId = loginUser.getId();
        // 校验用户是否已加入队伍
        if (!isUserInTeam(teamId, userId)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户未加入该队伍");
        }

        // 获取当前队伍成员数
        long teamHasJoinNum = this.countTeamUserByTeamId(teamId);

        // 如果只剩一名成员,直接解散队伍
        if (teamHasJoinNum == 1) {
            return disbandTeam(teamId);
        }else if (team.getUserId() == userId) {// 如果队伍剩下至少两人,判断是否是队长退出
            return handleTeamLeaderQuit(teamId, userId);
        }

        //db变化 清除缓存以同步
        clearTeamCache();

        // 移除用户与队伍的关系
        return removeUserFromTeam(teamId, userId);
    }

    /**
     * 根据 id 获取队伍信息 (用于内部查询)
     * utils for joinTeam and quitTeam
     */
    private Team getTeamById(Long teamId) {
        if (teamId == null || teamId <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍id不合法");
        }
        Team team = this.getById(teamId);
        if (team == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
        }
        return team;
    }

    /**
     * 获取某队伍当前人数
     * utils for joinTeam and quitTeam
     */
    private long countTeamUserByTeamId(long teamId) {
        QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
        userTeamQueryWrapper.eq("teamId", teamId);
        return userTeamService.count(userTeamQueryWrapper);
    }

    /**
     * 校验用户是否已加入队伍
     * utils for quitTeam
     */
    private boolean isUserInTeam(Long teamId, long userId) {
        UserTeam queryUserTeam = new UserTeam();
        queryUserTeam.setTeamId(teamId);
        queryUserTeam.setUserId(userId);
        QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>(queryUserTeam);
        return userTeamService.count(queryWrapper) > 0;
    }

    /**
     * 解散队伍
     * utils for quitTeam
     */
    private boolean disbandTeam(Long teamId) {
        // 删除队伍
        boolean removed = this.removeById(teamId);
        if (!removed) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "解散队伍失败");
        }
        return true;
    }

    /**
     * 处理队长退出队伍的逻辑
     * utils for quitTeam
     */
    private boolean handleTeamLeaderQuit(Long teamId, long userId) {
        // 获取队伍成员,选择最早加入的成员作为新的队长
        List<UserTeam> userTeamList = getSortedUserTeamList(teamId);
        if (CollectionUtils.isEmpty(userTeamList) || userTeamList.size() <= 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, "更新队伍队长失败");
        }

        //db变化 清除缓存以同步
        clearTeamCache();

        // 移除队长与队伍的关系
        return removeUserFromTeam(teamId, userId);
    }

    /**
     * 获取队伍成员并按加入时间排序
     * utils for quitTeam
     */
    private List<UserTeam> getSortedUserTeamList(Long teamId) {
        QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("teamId", teamId);
        queryWrapper.orderByAsc("joinTime");
        return userTeamService.list(queryWrapper);
    }

    /**
     * 移除用户与队伍的关联
     * utils for quitTeam
     */
    private boolean removeUserFromTeam(Long teamId, long userId) {
        UserTeam queryUserTeam = new UserTeam();
        queryUserTeam.setTeamId(teamId);
        queryUserTeam.setUserId(userId);
        QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>(queryUserTeam);
        return userTeamService.remove(queryWrapper);
    }

解散队伍接口

接口描述:

队长可以解散队伍。

请求路径:

POST /team/{team_id}/delete

请求参数:

  • team_id: 队伍 ID,必填。

请求验证:

  • 校验队伍是否存在。
  • 校验当前用户是否为队长。

业务流程:

  1. 校验队伍是否存在。
  2. 校验用户是否为队长。
  3. 删除队伍及所有相关的用户-队伍关系。

controller:

/**
     * 解散队伍
     *
     * @param teamId  要解散的队伍id
     * @param request 当前请求
     * @return BaseResponse<Boolean>
     */
    @PostMapping("{team_id}/delete")
    public BaseResponse<Boolean> deleteTeam(@PathVariable("team_id") Long teamId, HttpServletRequest request) {
        if (teamId == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }
        UserVO loginUser = userService.getLoginUser(request);
        boolean result = teamService.deleteTeam(teamId, loginUser);
        if (!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除失败");
        }
        return ResultUtils.success(true);
    }

service:

/**
     * 解散队伍
     *
     * @param teamId    要解散的队伍id
     * @param loginUser 当前登录用户
     * @return 是否解散成功
     */
    boolean deleteTeam(Long teamId, UserVO loginUser);

serviceimpl:

 /**
     * 解散队伍
     *
     * @param teamId    要解散的队伍id
     * @param loginUser 当前登录用户
     * @return 是否解散成功
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTeam(Long teamId, UserVO loginUser) {
        Team team = getTeamById(teamId);
        // 校验是否为队伍的队长
        if (team.getUserId() != loginUser.getId()) {
            throw new BusinessException(ErrorCode.NO_AUTH, "无访问权限");
        }
        // 移除所有加入队伍的关联信息
        QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
        userTeamQueryWrapper.eq("teamId", teamId);
        boolean result = userTeamService.remove(userTeamQueryWrapper);
        if (!result) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除队伍关联信息失败");
        }
        // 删除队伍(队伍本身)
        boolean deleteResult = this.removeById(teamId);
        if (!deleteResult) {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "解散队伍失败");
        }

        // 记录解散队伍的操作日志(可以添加到审计系统中)
        log.info("队伍 {} 已被队长 {} 解散", teamId, loginUser.getId());

        //db变化 清除缓存以同步
        clearTeamCache();

        return true;
    }

获取当前用户已加入的队伍接口

接口描述:

获取当前用户已加入的所有队伍。

请求路径:

GET /team/list/my/join

请求参数:

  • user_id: 当前用户 ID(自动获取)。

业务流程:

  1. 查询当前用户加入的所有队伍。

controller:

/**
     * 获取我加入的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param request          请求上下文
     * @return BaseResponse<List < TeamVO>>
     */
    @GetMapping("/list/my/join")
    public BaseResponse<List<TeamVO>> listMyJoinTeams(@Valid TeamQueryRequest teamQueryRequest, HttpServletRequest request) {
        if (teamQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }
        UserVO loginUser = userService.getLoginUser(request);

        List<TeamVO> teamList = teamService.listMyJoinedTeams(teamQueryRequest, loginUser);
        return ResultUtils.success(teamList);
    }

service:

/**
     * 当前用户加入的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param loginUser        当前登录用户
     * @return 返回符合查询条件的队伍列表
     */
    List<TeamVO> listMyJoinedTeams(@Valid TeamQueryRequest teamQueryRequest, UserVO loginUser);

serviceimpl

 /**
     * 当前用户加入的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param loginUser        当前登录用户
     * @return 返回符合查询条件的队伍列表
     */
    @Override
    public List<TeamVO> listMyJoinedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) {
        QueryWrapper<UserTeam> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userId", loginUser.getId());
        List<UserTeam> userTeamList = userTeamService.list(queryWrapper);

        Map<Long, List<UserTeam>> listMap = userTeamList.stream()
                .collect(Collectors.groupingBy(UserTeam::getTeamId));
        List<Long> idList = new ArrayList<>(listMap.keySet());
        teamQueryRequest.setIdList(idList);
        return listTeams(teamQueryRequest, loginUser);
    }

获取当前用户创建的队伍接口

接口描述:

获取当前用户创建的所有队伍。

请求路径:

GET /api/teams/created

请求参数:

  • user_id: 当前用户 ID(自动获取)。

业务流程:

  1. 查询当前用户创建的所有队伍。

controller:

 /**
     * 获取我创建的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param request          请求上下文
     * @return BaseResponse<List < TeamVO>>
     */
    @GetMapping("/list/my/create")
    public BaseResponse<List<TeamVO>> listMyCreateTeams(@Valid TeamQueryRequest teamQueryRequest, HttpServletRequest request) {
        if (teamQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空");
        }
        UserVO loginUser = userService.getLoginUser(request);
        List<TeamVO> teamList = teamService.listMyCreatedTeams(teamQueryRequest, loginUser);
        return ResultUtils.success(teamList);
    }

service:

  /**
     * 当前用户创建的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param loginUser        当前登录用户
     * @return 返回符合查询条件的队伍列表
     */
    List<TeamVO> listMyCreatedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser);

serviceimpl:

  /**
     * 当前用户创建的队伍
     *
     * @param teamQueryRequest 查询队伍的请求参数
     * @param loginUser        当前登录用户
     * @return 返回符合查询条件的队伍列表
     */
    @Override
    public List<TeamVO> listMyCreatedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) {
        teamQueryRequest.setUserId(loginUser.getId());
        return listTeams(teamQueryRequest, loginUser);
    }

项目分区导航:⬅️ 13-用户标签接口 | 14-组队功能 | ➡️ 15-定时任务