组队功能
那个球参应用场景
建立群聊队伍,可以发起队伍或者加入别人的队伍
需求分析
- 用户角色
- 用户分为两种角色:
- 队长
- 队员
- 队长的行为:
- 创建队伍:为队伍指定队伍名称、人数限制、描述、超时时间等属性。
- 加入队伍:行为等同于创建队伍。
- 退出队伍:退出队伍的行为分为解散队伍或转让队伍两种。
- 队员的行为:
- 加入队伍:选择加入一个队伍(可能涉及申请、队长审批,交互逻辑可后续完善)。
- 退出队伍:简单地退出队伍。
- 用户分为两种角色:
- 队伍管理
-
队伍权限级别
队伍有三种权限级别:
- 公开:任何人可以加入。
- 私有:需要申请加入,可能需要队长审批。
- 加密:加入需要密码,队长提供密码给队员。
- 队伍的权限级别由队长在创建时指定。
-
队伍数量限制
用户可以创建多个队伍,但同时拥有的队伍数量不能超过五个,加入队伍的数量也有限制。
需要维护一个多对多关系(用户与队伍之间),确保队员与队伍之间的关系被正确管理。
-
队伍的基本属性
队伍应该具有以下基本属性:
- 标题:队伍的名称。
- 描述:队伍的简要描述,帮助用户了解队伍的基本情况。
- 人数限制:队伍允许的最大人数。
- 超时时间:队伍的过期时间,过期后队伍不能继续加入或活动。
- 这些属性需要在数据库中设计相应的数据表,并提供增删改查(CRUD)接口。
-
队伍的状态
队伍的状态应该有以下标识:
- 是否满员:队伍是否已经达到最大人数。
- 是否过期:队伍是否已经过期,过期队伍需要从用户列表中去除。
-
分享队伍
队长可以分享队伍给其他用户,生成一个邀请链接或二维码。
用户通过访问分享链接或扫描二维码,能够加入相应的队伍。
当队伍达到最大人数时,系统会发送通知告知用户队伍已经满员。
-
- 队伍列表与搜索
-
队伍列表展示
用户可以查看队伍的列表,列表展示内容包括队伍的名称、描述、当前人数等。
在队伍列表中,已过期的队伍不展示。
-
队伍搜索
用户可以根据队伍名称进行搜索,快速找到感兴趣的队伍。
-
表的设计
设计两个主要的数据表:队伍表(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. 优化设计
- 索引设计:对于高频查询的字段(如
teamId、userId),建议建立索引来提高查询性能。
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个队伍)。
业务流程:
- 校验请求参数是否有效。
- 校验用户是否登录,并检查已创建队伍数是否超过上限。
- 创建队伍并插入队伍表。
- 将用户与队伍建立关联,插入用户-队伍关系表。
- 返回创建成功信息。
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: 每页显示的队伍数,必填。
请求验证:
- 校验分页参数是否有效。
- 只有管理员才能查看加密或私有的队伍。
业务流程:
- 根据请求参数构造查询条件,分页查询队伍。
- 过滤已过期的队伍(根据
expire_time判断)。 - 根据权限过滤队伍,非管理员只能查看公开队伍。
- 返回队伍列表及其创建者和成员信息。
后端逻辑:
- 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是否有效。
- 是否登录
业务流程:
- 根据team_id查询队伍
- 返回队伍信息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: 如果队伍状态修改为加密状态,必填。
请求验证:
- 校验队伍是否存在。
- 只有队长或管理员才能修改队伍信息。
- 校验新值与旧值是否相同,如果相同则不做更新。
业务流程:
- 校验队伍是否存在。
- 校验用户是否是队长或管理员。
- 校验队伍状态是否修改为加密状态,如果是,检查密码是否填写。
- 更新队伍信息。
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个队伍。
- 校验加密队伍的密码是否正确。
业务流程:
- 校验队伍是否存在并且未满员、未过期。
- 校验用户是否已加入该队伍,防止重复加入。
- 如果是加密队伍,验证密码。
- 添加用户与队伍的关联信息。
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,必填。
请求验证:
- 校验队伍是否存在。
- 校验用户是否已加入该队伍。
- 如果是队长退出,判断是否需要转让队伍或解散队伍。
业务流程:
- 校验队伍是否存在。
- 校验用户是否加入该队伍。
- 如果队长退出,判断是否需要转让队伍。
- 如果只剩一个队员,解散队伍。
- 删除用户与队伍的关联。
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,必填。
请求验证:
- 校验队伍是否存在。
- 校验当前用户是否为队长。
业务流程:
- 校验队伍是否存在。
- 校验用户是否为队长。
- 删除队伍及所有相关的用户-队伍关系。
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(自动获取)。
业务流程:
- 查询当前用户加入的所有队伍。
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(自动获取)。
业务流程:
- 查询当前用户创建的所有队伍。
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);
}
💬 评论