--- title: "14-组队功能" created: 2025-12-02 tags: - 项目 aliases: - 组队功能 --- # 组队功能 ## 那个球参应用场景 建立群聊队伍,可以发起队伍或者加入别人的队伍 ## 需求分析 - **用户角色** - 用户分为两种角色: 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:** ```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:** ```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. **优化设计** - **索引设计**:对于高频查询的字段(如 `teamId`、`userId`),建议建立索引来提高查询性能。 ```java CREATE INDEX idx_team_id ON user_team (teamId); CREATE INDEX idx_user_id ON user_team (userId); ``` - **外键约束**:为了确保数据一致性,可以在 `user_team` 表中添加外键约束来连接 `team` 表和 `user` 表。 ```java 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 ```java 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: ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 ```java 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 { // 自定义查询,统计用户创建的队伍数 default long countTeamsByUserId(long userId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("userId", userId); return this.selectCount(queryWrapper); } } ``` #### controller ```java @PostMapping("/add") public BaseResponse 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 ```java /** * 创建队伍 * * @param teamAddRequest 创建队伍的请求参数 * @param loginUser 当前登录的用户 * @return 新队伍的 ID */ long addTeam(TeamAddRequest teamAddRequest, User loginUser); ``` #### serviceimpl 使用redisson分布式锁来解决 用户短时间点击多次可能创建多个队伍的问题 因为查询写入缓存 添加成功后 redis并没有新增内容 为了双库同步 需要手动更新 这里用最简单的方式实现 一旦添加队伍 就清除所有seach的缓存 后续可以细粒化优化 ```java @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(); } } } ``` ```java /** * 数据变动时清除缓存 */ private void clearTeamCache() { String searchKey = "FriendMarry:team:search:" + "*"; // todo 可以根据不同条件清理 细粒化 Set 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 ```java 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 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 ```java /** * 查询队伍列表 * * @param teamQueryRequest 查询队伍的请求参数 * @param request 请求上下文 * @return BaseResponse> */ @GetMapping("/list") public BaseResponse> 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 teamList = teamService.listTeams(teamQueryRequest, loginUser); return ResultUtils.success(teamList); } ``` #### service ```java /** * 搜索队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ List listTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser); ``` #### serviceimpl ```java /** * 搜索队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ @Override public List listTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) { // 判断是否为管理员 boolean isAdmin = userService.isAdmin(loginUser); String redisKey = generateRedisKey(teamQueryRequest, loginUser); // 根据查询参数和用户生成缓存Key ValueOperations valueOperations = redisTemplate.opsForValue(); // 尝试从 Redis 缓存中获取数据 List teamVOList = getFromRedis(redisKey, valueOperations); if (teamVOList != null) { return teamVOList; } // 查询队伍列表 QueryWrapper queryWrapper = new QueryWrapper<>(); // 构建查询条件 addQueryConditions(queryWrapper, teamQueryRequest, isAdmin); List teamList = this.list(queryWrapper); if (CollectionUtils.isEmpty(teamList)) { return new ArrayList<>(); } // 获取当前用户已加入的队伍ID集合 Set hasJoinTeamIdSet = getUserJoinedTeams(teamList.stream().map(Team::getId).collect(Collectors.toList()), loginUser); // 查询所有队伍的加入人数 Map teamJoinCountMap = getTeamJoinCounts(teamList.stream().map(Team::getId).collect(Collectors.toList())); // 关联查询队伍创建人的用户信息 List 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 getFromRedis(String redisKey, ValueOperations valueOperations) { try { Object cachedData = valueOperations.get(redisKey); if (cachedData != null) { log.info("Redis缓存成功命中,key: {}", redisKey); return (List) 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 teamVOList, ValueOperations 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 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 getUserJoinedTeams(List teamIdList, UserVO loginUser) { QueryWrapper userTeamQueryWrapper = new QueryWrapper<>(); userTeamQueryWrapper.eq("userId", loginUser.getId()).in("teamId", teamIdList); List userTeamList = userTeamService.list(userTeamQueryWrapper); return userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet()); } /** * 获取每个队伍的加入人数 * utils for listTeams */ private Map getTeamJoinCounts(List teamIdList) { QueryWrapper userTeamJoinQueryWrapper = new QueryWrapper<>(); userTeamJoinQueryWrapper.in("teamId", teamIdList); List allUserTeams = userTeamService.list(userTeamJoinQueryWrapper); return allUserTeams.stream() .collect(Collectors.groupingBy(UserTeam::getTeamId, Collectors.counting())); } ``` #### VO ```java 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: ```java /** * 根据id查找队伍 * * @param id 队伍id * @param request 当前请求 * @return BaseResponse */ @GetMapping("/get") public BaseResponse 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: ```java /** * 根据id查找队伍 * * @param teamId 队伍id * @param loginUser 当前登录用户 * @return 查询到的team封装VO */ TeamVO getTeamById(long teamId, UserVO loginUser); ``` #### serviceimpl: 这里修改比较频繁 与update配套使用 一般调用就要修改 没必要加上缓存 ```java /** * 根据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 ```java 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 ```java /** * 更新队伍信息 * * @param teamUpdateRequest 更新队伍的请求参数 * @param request 当前请求 * @return 修改后的队伍状态 */ @PutMapping("/{team_id}/update") public BaseResponse 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 ```java /** * 更新队伍 * * @param teamUpdateRequest 更新队伍的请求参数 * @param loginUser 当前登录用户 * @return 是否更新成功 */ boolean updateTeam(TeamUpdateRequest teamUpdateRequest, UserVO loginUser); ``` #### serviceimpl ```java /** * 更新队伍 * * @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 ```java 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: ```java /** * @param teamJoinRequest 加入队伍的请求参数 * @param request 当前请求 * @return BaseResponse */ @PostMapping("/join") public BaseResponse 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 ```java /** * 加入队伍 * * @param teamJoinRequest 加入队伍的请求参数 * @param loginUser 当前登录用户 * @return 是否加入成功 */ boolean joinTeam(TeamJoinRequest teamJoinRequest, UserVO loginUser); ``` #### serviceimpl ```java /** * 加入队伍 * * @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 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 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: ```java /** * 退出队伍 * * @param teamId 要退出的队伍id * @param request 当前请求 * @return BaseResponse */ @PostMapping("{team_id}/quit") public BaseResponse 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 ```java /** * 退出队伍 * * @param teamId 要退出的队伍id * @param loginUser 当前登录用户 * @return 是否退出成功 */ boolean quitTeam(Long teamId, UserVO loginUser); ``` #### serviceimpl ```java /** * 退出队伍 * * @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 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 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 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 getSortedUserTeamList(Long teamId) { QueryWrapper 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 queryWrapper = new QueryWrapper<>(queryUserTeam); return userTeamService.remove(queryWrapper); } ``` --- ### **解散队伍接口** **接口描述:** 队长可以解散队伍。 **请求路径:** `POST /team/{team_id}/delete` **请求参数:** - `team_id`: 队伍 ID,必填。 **请求验证:** - 校验队伍是否存在。 - 校验当前用户是否为队长。 **业务流程:** 1. 校验队伍是否存在。 2. 校验用户是否为队长。 3. 删除队伍及所有相关的用户-队伍关系。 #### controller: ```java /** * 解散队伍 * * @param teamId 要解散的队伍id * @param request 当前请求 * @return BaseResponse */ @PostMapping("{team_id}/delete") public BaseResponse 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: ```java /** * 解散队伍 * * @param teamId 要解散的队伍id * @param loginUser 当前登录用户 * @return 是否解散成功 */ boolean deleteTeam(Long teamId, UserVO loginUser); ``` #### serviceimpl: ```java /** * 解散队伍 * * @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 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: ```java /** * 获取我加入的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param request 请求上下文 * @return BaseResponse> */ @GetMapping("/list/my/join") public BaseResponse> listMyJoinTeams(@Valid TeamQueryRequest teamQueryRequest, HttpServletRequest request) { if (teamQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); } UserVO loginUser = userService.getLoginUser(request); List teamList = teamService.listMyJoinedTeams(teamQueryRequest, loginUser); return ResultUtils.success(teamList); } ``` #### service: ```java /** * 当前用户加入的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ List listMyJoinedTeams(@Valid TeamQueryRequest teamQueryRequest, UserVO loginUser); ``` #### serviceimpl ```java /** * 当前用户加入的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ @Override public List listMyJoinedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("userId", loginUser.getId()); List userTeamList = userTeamService.list(queryWrapper); Map> listMap = userTeamList.stream() .collect(Collectors.groupingBy(UserTeam::getTeamId)); List idList = new ArrayList<>(listMap.keySet()); teamQueryRequest.setIdList(idList); return listTeams(teamQueryRequest, loginUser); } ``` --- ### **获取当前用户创建的队伍接口** **接口描述:** 获取当前用户创建的所有队伍。 **请求路径:** `GET /api/teams/created` **请求参数:** - `user_id`: 当前用户 ID(自动获取)。 **业务流程:** 1. 查询当前用户创建的所有队伍。 #### controller: ```java /** * 获取我创建的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param request 请求上下文 * @return BaseResponse> */ @GetMapping("/list/my/create") public BaseResponse> listMyCreateTeams(@Valid TeamQueryRequest teamQueryRequest, HttpServletRequest request) { if (teamQueryRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数不能为空"); } UserVO loginUser = userService.getLoginUser(request); List teamList = teamService.listMyCreatedTeams(teamQueryRequest, loginUser); return ResultUtils.success(teamList); } ``` #### service: ```java /** * 当前用户创建的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ List listMyCreatedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser); ``` #### serviceimpl: ```java /** * 当前用户创建的队伍 * * @param teamQueryRequest 查询队伍的请求参数 * @param loginUser 当前登录用户 * @return 返回符合查询条件的队伍列表 */ @Override public List listMyCreatedTeams(TeamQueryRequest teamQueryRequest, UserVO loginUser) { teamQueryRequest.setUserId(loginUser.getId()); return listTeams(teamQueryRequest, loginUser); } ``` --- **项目分区导航**:⬅️ [[13-用户标签接口|13-用户标签接口]] | 14-组队功能 | ➡️ [[15-定时任务|15-定时任务]]