项目地址:
@
用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 PO
队长、剩余的人数
聊天?
公开 或 private 或加密
信息流中不展示已过期的队伍
加入不同的队伍,抢的不是同一个资源,就不需要,抢锁了。可以将锁的范围缩小一些。
用户和队伍 Id,都要锁一下。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.DeleteRequest;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.request.TeamAddRequest;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/team")
@Api("接口文档的一个别名处理定义 TeamController ")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"}) // 配置前端访问路径的放行,可以配置多个
@Slf4j
public class TeamController {
@Resource
private UserService userService;
@Resource
private TeamService teamService;
@Resource
private UserTeamService userTeamService;
/**
* 插入 team 队伍,添加队伍
*
* @param teamAddRequest
* @return teamId
*/
@PostMapping("/add")
public BaseResponse addTeam(@RequestBody TeamAddRequest teamAddRequest, HttpServletRequest request) {
if (teamAddRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
Team team = new Team();
BeanUtils.copyProperties(teamAddRequest, team);
long teamId = teamService.addTeam(team, loginUser);
return ResultUtils.success(teamId);
}
}
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.mapper.TeamMapper;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.model.vo.UserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
*
*/
@Service
public class TeamServiceImpl extends ServiceImpl
implements TeamService {
@Resource
private UserTeamService userTeamService;
@Resource
private UserService userService;
@Resource
private RedissonClient redissonClient;
/***
* 添加队伍
* @param team 队伍
* @param loginUser User 用户
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public long addTeam(Team team, User loginUser) {
// 1. 请求参数是否为空?
if (team == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 2. 是否登录,未登录不允许创建
if (loginUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
final long userId = loginUser.getId();
// 3. 校验信息
// 1. 队伍人数 > 1 且 20) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍人数不满足要求");
}
// 2. 队伍标题 20) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍标题不满足要求");
}
// 3. 描述 512) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍描述过长");
}
// 4. status 是否公开(int)不传默认为 0(公开)
int status = Optional.ofNullable(team.getStatus()).orElse(0);
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍状态不满足要求");
}
// 5. 如果 status 是加密状态,一定要有密码,且密码 32) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码设置不正确");
}
}
// 6. 超时时间 > 当前时间
Date expireTime = team.getExpireTime();
if (new Date().after(expireTime)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "超时时间 > 当前时间");
}
// 7. 校验用户最多创建 5 个队伍
// todo 有 bug,可能同时创建 100 个队伍
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("userId", userId);
long hasTeamNum = this.count(queryWrapper);
if (hasTeamNum >= 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建 5 个队伍");
}
// 8. 插入队伍信息到队伍表
team.setId(null);
team.setUserId(userId);
boolean result = this.save(team);
Long teamId = team.getId();
if (!result || teamId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
}
// 9. 插入用户 => 队伍关系到关系表
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
result = userTeamService.save(userTeam);
if (!result) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "创建队伍失败");
}
return teamId;
}
}
补充:事务注解
@Transaction(rollbackFor = Exception.class) // 在方法上添加上注解,启动事务控制
void public main() {
}
分页展示队伍列表,根据名称、最大人数等搜索队伍PO,信息流中不展示已过期的队伍。
自己写 SQL
// 1. 自己写 SQL
// 查询队伍和创建人的信息
// select * from team t left join user u on t.userId = u.id
// 查询队伍和已加入队伍成员的信息
// select *
// from team t
// left join user_team ut on t.id = ut.teamId
// left join user u on ut.userId = u.id;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.DeleteRequest;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.request.TeamAddRequest;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/team")
@Api("接口文档的一个别名处理定义 TeamController ")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"}) // 配置前端访问路径的放行,可以配置多个
@Slf4j
public class TeamController {
@Resource
private UserService userService;
@Resource
private TeamService teamService;
@Resource
private UserTeamService userTeamService;
/**
* 显示队伍列表,私有的不显示
**/
@GetMapping("/list")
public BaseResponse> listTeams(TeamQuery teamQuery, HttpServletRequest request) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean isAdmin = userService.isAdmin(request);
// 1、查询队伍列表
List teamList = teamService.listTeams(teamQuery, isAdmin);
final List teamIdList = teamList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
// 2、判断当前用户是否已加入队伍
QueryWrapper userTeamQueryWrapper = new QueryWrapper();
try {
User loginUser = userService.getLoginUser(request);
userTeamQueryWrapper.eq("userId", loginUser.getId());
userTeamQueryWrapper.in("teamId", teamIdList);
List userTeamList = userTeamService.list(userTeamQueryWrapper);
// 已加入的队伍 id 集合
Set hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin);
});
} catch (Exception e) {
}
// 3、查询已加入队伍的人数
QueryWrapper userTeamJoinQueryWrapper = new QueryWrapper();
userTeamJoinQueryWrapper.in("teamId", teamIdList);
List userTeamList = userTeamService.list(userTeamJoinQueryWrapper);
// 队伍 id => 加入这个队伍的用户列表
Map> teamIdUserTeamList = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
teamList.forEach(team -> team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList()).size()));
return ResultUtils.success(teamList);
}
}
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.mapper.TeamMapper;
import com.rainbowsea.yupao.model.Team;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.UserTeam;
import com.rainbowsea.yupao.model.dto.TeamQuery;
import com.rainbowsea.yupao.model.enums.TeamStatusEnum;
import com.rainbowsea.yupao.model.request.TeamJoinRequest;
import com.rainbowsea.yupao.model.request.TeamQuitRequest;
import com.rainbowsea.yupao.model.request.TeamUpdateRequest;
import com.rainbowsea.yupao.model.vo.TeamUserVO;
import com.rainbowsea.yupao.model.vo.UserVO;
import com.rainbowsea.yupao.service.TeamService;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.service.UserTeamService;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
*
*/
@Service
public class TeamServiceImpl extends ServiceImpl
implements TeamService {
@Resource
private UserTeamService userTeamService;
@Resource
private UserService userService;
@Resource
private RedissonClient redissonClient;
/**
* 搜索队伍
* @param teamQuery
* @param isAdmin
* @return
*/
@Override
public List listTeams(TeamQuery teamQuery, boolean isAdmin) {
QueryWrapper queryWrapper = new QueryWrapper();
// 组合查询条件
if (teamQuery != null) {
Long id = teamQuery.getId();
if (id != null && id > 0) {
queryWrapper.eq("id", id);
}
List idList = teamQuery.getIdList();
if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(idList)) {
queryWrapper.in("id", idList);
}
String searchText = teamQuery.getSearchText();
if (StringUtils.isNotBlank(searchText)) {
queryWrapper.and(qw -> qw.like("name", searchText).or().like("description", searchText));
}
String name = teamQuery.getName();
if (StringUtils.isNotBlank(name)) {
queryWrapper.like("name", name);
}
String description = teamQuery.getDescription();
if (StringUtils.isNotBlank(description)) {
queryWrapper.like("description", description);
}
Integer maxNum = teamQuery.getMaxNum();
// 查询最大人数相等的
if (maxNum != null && maxNum > 0) {
queryWrapper.eq("maxNum", maxNum);
}
Long userId = teamQuery.getUserId();
// 根据创建人来查询
if (userId != null && userId > 0) {
queryWrapper.eq("userId", userId);
}
// 根据状态来查询
Integer status = teamQuery.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null) {
statusEnum = TeamStatusEnum.PUBLIC;
}
if (!isAdmin && statusEnum.equals(TeamStatusEnum.PRIVATE)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
queryWrapper.eq("status", statusEnum.getValue());
}
// 不展示已过期的队伍
// expireTime is null or expireTime > now()
queryWrapper.and(qw -> qw.gt("expireTime", new Date()).or().isNull("expireTime"));
List teamList = this.list(queryWrapper);
if (org.apache.commons.collections4.CollectionUtils.isEmpty(teamList)) {
return new ArrayList();
}
List teamUserVOList = new ArrayList();
// 关联查询创建人的用户信息
for (Team team : teamList) {
Long userId = team.getUserId();
if (userId == null) {
continue;
}
User user = userService.getById(userId);
TeamUserVO teamUserVO = new TeamUserVO();
BeanUtils.copyProperties(team, teamUserVO);
// 脱敏用户信息
if (user != null) {
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
teamUserVO.setCreateUser(userVO);
}
teamUserVOList.add(teamUserVO);
}
return teamUserVOList;
}
}
}
/**
* 更新队伍内容
*
* @param teamUpdateRequest teamUpdateRequest 对象
* @return Boolean 更新成功 true,否则 false
*/
@PostMapping("/update")
public BaseResponse updateTeam(@RequestBody TeamUpdateRequest teamUpdateRequest, HttpServletRequest request) {
if (teamUpdateRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
boolean result = teamService.updateTeam(teamUpdateRequest, loginUser);
if (!result) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "更新失败");
}
return ResultUtils.success(true);
}
/**
* 更新队伍
* @param teamUpdateRequest
* @param loginUser
* @return
*/
@Override
public boolean updateTeam(TeamUpdateRequest teamUpdateRequest, User loginUser) {
if (teamUpdateRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Long id = teamUpdateRequest.getId();
if (id == null || id
其他人、未满、未过期,允许加入多个队伍,但是要有个上限PO
注意,一定要加上事务注解!!!!
/**
* 加入队伍
* @param teamJoinRequest
* @param request
* @return BaseResponse
*/
@PostMapping("/join")
public BaseResponse joinTeam(@RequestBody TeamJoinRequest teamJoinRequest, HttpServletRequest request) {
if (teamJoinRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
boolean result = teamService.joinTeam(teamJoinRequest, loginUser);
return ResultUtils.success(result);
}
/**
* 加入队伍
*
* @param teamJoinRequest
* @param loginUser
* @return boolean
*/
@Override
public boolean joinTeam(TeamJoinRequest teamJoinRequest, User loginUser) {
if (teamJoinRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Long teamId = teamJoinRequest.getTeamId();
Team team = getTeamById(teamId);
Date expireTime = team.getExpireTime();
if (expireTime != null && expireTime.before(new Date())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
}
Integer status = team.getStatus();
TeamStatusEnum teamStatusEnum = TeamStatusEnum.getEnumByValue(status);
if (TeamStatusEnum.PRIVATE.equals(teamStatusEnum)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有队伍");
}
String password = teamJoinRequest.getPassword();
if (TeamStatusEnum.SECRET.equals(teamStatusEnum)) {
if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
}
// 该用户已加入的队伍数量
long userId = loginUser.getId();
// 只有一个线程能获取到锁
RLock lock = redissonClient.getLock("yupao:join_team");
try {
// 抢到锁并执行
while (true) {
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
System.out.println("getLock: " + Thread.currentThread().getId());
QueryWrapper userTeamQueryWrapper = new QueryWrapper();
userTeamQueryWrapper.eq("userId", userId);
long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
if (hasJoinNum > 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "最多创建和加入 5 个队伍");
}
// 不能重复加入已加入的队伍
userTeamQueryWrapper = new QueryWrapper();
userTeamQueryWrapper.eq("userId", userId);
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());
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("doCacheRecommendUser error", e);
return false;
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}
请求参数:用户 ID
/**
* 退出队伍,
* @param teamQuitRequest
* @param request
* @return BaseResponse
*/
@PostMapping("/quit")
public BaseResponse quitTeam(@RequestBody TeamQuitRequest teamQuitRequest, HttpServletRequest request) {
if (teamQuitRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
boolean result = teamService.quitTeam(teamQuitRequest, loginUser);
return ResultUtils.success(result);
}
/**
* 退出队伍
* @param teamQuitRequest
* @param loginUser
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class) // 添加上事务
public boolean quitTeam(TeamQuitRequest teamQuitRequest, User loginUser) {
if (teamQuitRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Long teamId = teamQuitRequest.getTeamId();
Team team = this.getById(teamId);
//Team team = getTeamById(teamId);
long userId = loginUser.getId();
UserTeam queryUserTeam = new UserTeam();
queryUserTeam.setTeamId(teamId);
queryUserTeam.setUserId(userId);
QueryWrapper queryWrapper = new QueryWrapper(queryUserTeam);
long count = userTeamService.count(queryWrapper);
if (count == 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "未加入队伍");
}
long teamHasJoinNum = this.countTeamUserByTeamId(teamId);
// 队伍只剩一人,解散
if (teamHasJoinNum == 1) {
// 删除队伍
this.removeById(teamId);
} else {
// 队伍还剩至少两人
// 是队长
if (team.getUserId() == userId) {
// 把队伍转移给最早加入的用户
// 1. 查询已加入队伍的所有用户和加入时间
QueryWrapper userTeamQueryWrapper = new QueryWrapper();
userTeamQueryWrapper.eq("teamId", teamId);
userTeamQueryWrapper.last("order by id asc limit 2");
List userTeamList = userTeamService.list(userTeamQueryWrapper);
if (CollectionUtils.isEmpty(userTeamList) || userTeamList.size()
请求参数:队伍 id
业务流程:
/**
* 删除队伍,移除队伍
*
* @param deleteRequest 队伍的 id
* @return Boolean 移除成功 true,否则 false
*/
@PostMapping("/delete")
public BaseResponse deleteTeam(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId()
/**
* 删除队伍
* @param id 队伍中队长的 ID
* @param loginUser
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTeam(long id, User loginUser) {
// 校验队伍是否存在
Team team = this.getTeamById(id);
Long teamId = team.getId();
// 校验你是不是队伍的队长
if (!team.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH, "不是队长,无权限删除");
}
// 移除所有加入队伍的关联信息
QueryWrapper userTeamQueryWrapper = new QueryWrapper();
userTeamQueryWrapper.eq("teamId", teamId);
boolean result = userTeamService.remove(userTeamQueryWrapper);
if(!result) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"删除队伍关联信息失败");
}
// 删除队伍
return this.removeById(teamId);
}
/**
* 获取当前用户创建的队伍:包括:私有的,公开的,加密的,只要是自己创建的
* @param teamQuery
* @param request
* @return
* @Author: RainbowSea
*/
@GetMapping("/list/my/create")
public BaseResponse> listMyCreateTeams(TeamQuery teamQuery, HttpServletRequest request) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 获取当前登录用户
User loginUser = userService.getLoginUser(request);
// 设置查询条件,查询当前用户创建的团队
teamQuery.setUserId(loginUser.getId());
// 构建查询条件
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("userId", teamQuery.getUserId()); // 根据 `userId` 字段进行查询
// 查询所有队伍
List teamList = teamService.list(queryWrapper);
// 转换为 TeamUserVO 并加入必要字段
List teamUserVOList = teamList.stream()
.map(team -> {
TeamUserVO teamUserVO = new TeamUserVO();
teamUserVO.setId(team.getId());
teamUserVO.setName(team.getName());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setUserId(team.getUserId());
teamUserVO.setCreateTime(team.getCreateTime());
teamUserVO.setUpdateTime(team.getUpdateTime());
teamUserVO.setMaxNum(team.getMaxNum());
teamUserVO.setExpireTime(team.getExpireTime());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setStatus(team.getStatus());
// 其他字段的映射
return teamUserVO;
}).collect(Collectors.toList());
// 获取当前用户已加入的队伍
List teamIdList = teamUserVOList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
QueryWrapper userTeamQueryWrapper = new QueryWrapper();
try {
userTeamQueryWrapper.eq("userId", loginUser.getId());
userTeamQueryWrapper.in("teamId", teamIdList);
List userTeamList = userTeamService.list(userTeamQueryWrapper);
// 已加入的队伍 id 集合
Set hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamUserVOList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin);
});
} catch (Exception e) {
// 处理异常情况,日志记录等
}
// 查询已加入队伍的人数
QueryWrapper userTeamJoinQueryWrapper = new QueryWrapper();
userTeamJoinQueryWrapper.in("teamId", teamIdList);
List userTeamList = userTeamService.list(userTeamJoinQueryWrapper);
// 队伍 id => 加入这个队伍的用户列表
Map> teamIdUserTeamList = userTeamList.stream()
.collect(Collectors.groupingBy(UserTeam::getTeamId));
teamUserVOList.forEach(team ->
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList()).size())
);
// 返回所有队伍信息
return ResultUtils.success(teamUserVOList);
}
/**
* 获取当前用户加入的队伍
* 包括:私有的,公开的,加密的,只要是自己加入的
* @Author: RainbowSea
*/
@GetMapping("/list/my/join")
public BaseResponse> listMyJoinTeams(TeamQuery teamQuery, HttpServletRequest request) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 获取当前登录用户
User loginUser = userService.getLoginUser(request);
// 先查询用户已加入的队伍
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("userId", loginUser.getId());
List userTeamList = userTeamService.list(queryWrapper);
// 提取不重复的队伍 ID
Map> listMap = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
ArrayList idList = new ArrayList(listMap.keySet());
// 设置查询条件,只查询当前用户已加入的队伍
teamQuery.setIdList(idList);
// 构建查询条件
QueryWrapper teamQueryWrapper = new QueryWrapper();
if (teamQuery.getIdList() != null && !teamQuery.getIdList().isEmpty()) {
teamQueryWrapper.in("id", teamQuery.getIdList()); // 根据队伍ID查询
}
// 如果有搜索条件,加入搜索条件
if (teamQuery.getSearchText() != null && !teamQuery.getSearchText().isEmpty()) {
teamQueryWrapper.like("name", teamQuery.getSearchText()) // 根据队伍名称进行模糊搜索
.or().like("description", teamQuery.getSearchText()); // 或者根据队伍描述进行模糊搜索
}
// 分页处理
teamQueryWrapper.last("LIMIT " + ((teamQuery.getPageNum() - 1) * teamQuery.getPageSize()) + ", " + teamQuery.getPageSize());
// 查询队伍列表
List teamList = teamService.list(teamQueryWrapper);
// 转换为 TeamUserVO 并加入必要字段
List teamUserVOList = teamList.stream()
.map(team -> {
TeamUserVO teamUserVO = new TeamUserVO();
teamUserVO.setId(team.getId());
teamUserVO.setName(team.getName());
teamUserVO.setDescription(team.getDescription());
teamUserVO.setUserId(team.getUserId());
teamUserVO.setCreateTime(team.getCreateTime());
teamUserVO.setUpdateTime(team.getUpdateTime());
teamUserVO.setMaxNum(team.getMaxNum());
teamUserVO.setExpireTime(team.getExpireTime());
teamUserVO.setStatus(team.getStatus());
// 其他字段的映射
return teamUserVO;
}).collect(Collectors.toList());
// 获取当前用户已加入的队伍
Set hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamUserVOList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin); // 是否已经加入
});
// 查询已加入队伍的人数
QueryWrapper userTeamJoinQueryWrapper = new QueryWrapper();
userTeamJoinQueryWrapper.in("teamId", idList);
List userTeamListForCount = userTeamService.list(userTeamJoinQueryWrapper);
// 队伍 ID => 加入该队伍的用户列表
Map> teamIdUserTeamList = userTeamListForCount.stream()
.collect(Collectors.groupingBy(UserTeam::getTeamId));
teamUserVOList.forEach(team ->
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList()).size()) // 设置已加入人数
);
// 返回所有队伍信息
return ResultUtils.success(teamUserVOList);
}
需求背景:为了帮助大家更快的发现和自己新区相同的朋友。
思考:匹配 1 个还是匹配多个。
答:匹配多个,并且按照匹配的相似度从高到低排序
思考:怎么匹配?(根据什么匹配)
答:这里我们根据用户 user 表当中设置的 tags 属性进行匹配。
还可以根据 user_team 匹配加入相同队伍的用户。
问题本质:找到有相似标签的用户。
举例:
怎么匹配?
两种算法:
最小编辑距离:就是一个字符串 1 通过对其字符串最少多少次增删改字符的操作可以变成 字符串 2(一样的)
这里我们采用的是编辑距离算法:
package com.rainbowsea.yupao.utils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 算法工具类
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
*/
public class AlgorithmUtils {
/**
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
* @param tagList1
* @param tagList2
* @return
*/
public static int minDistance(List tagList1, List tagList2) {
// 在开始计算的时候,进行一个排序,让其中的tag 标签的内容,是相同等内容排序比较对比
Collections.sort(tagList1);
Collections.sort(tagList2);
int n = tagList1.size();
int m = tagList2.size();
if (n * m == 0) {
return n + m;
}
int[][] d = new int[n + 1][m + 1];
for (int i = 0; i
怎么对所有用户匹配,取 TOP ?
直接取出所有用户,依次和当前用计算分数,取 TOPN (50W 花费 54 秒 )
优化方法:
解决:维护一个固定长度的有序集合(sortedSet),只需要保留分数最高的几个用户集合(利用时间换空间)
eg: 【5,3,4,6,7】 取 TOP 5 即可,id 为 1 的用户就不用放进去了。
细节:
类比大数据推荐机制:
大数据推荐场景:比如说有几十亿个商品,难道要查出来所有的商品?难道要对所有的数据计算一遍相似度?
大数据推荐流程:
这里我们使用的方式:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rainbowsea.yupao.common.BaseResponse;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.utils.ResultUtils;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.model.request.UserLoginRequest;
import com.rainbowsea.yupao.model.request.UserRegisterRequest;
import com.rainbowsea.yupao.service.UserService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static com.rainbowsea.yupao.contant.UserConstant.USER_LOGIN_STATE;
@RestController
@RequestMapping("/user")
@Api("接口文档的一个别名处理定义 UserController")
@CrossOrigin(origins = {"http://localhost:5173","http://localhost:3000"}) // 配置前端访问路径的放行,可以配置多个
@Slf4j
public class UserController {
@Resource
private UserService userService;
@Resource
private RedisTemplate redisTemplate;
/**
* 获取最匹配的用户
*
* @param num
* @param request
* @return
*/
@GetMapping("/match")
public BaseResponse> matchUsers(long num, HttpServletRequest request) {
if (num 20) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getLoginUser(request);
return ResultUtils.success(userService.matchUsers(num, user));
}
}
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.rainbowsea.yupao.common.ErrorCode;
import com.rainbowsea.yupao.contant.UserConstant;
import com.rainbowsea.yupao.exception.BusinessException;
import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.service.UserService;
import com.rainbowsea.yupao.mapper.UserMapper;
import com.rainbowsea.yupao.utils.AlgorithmUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.Pair;
import org.apache.poi.ss.formula.functions.Now;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.rainbowsea.yupao.contant.UserConstant.USER_LOGIN_STATE;
/**
* @author RainbowSea
* @description 针对表【user(用户)】的数据库操作Service实现
* @createDate 2025-04-14 16:03:21
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl
implements UserService {
/**
* 加盐 混淆,让密码更加没有规律,更加安全一些
*/
private static final String SALT = "rainbowsea";
@Resource
private UserMapper userMapper;
/**
* 推荐(前端心动模式)的推荐算法,推荐用户
* @param num
* @param loginUser
* @return
*/
@Override
public List matchUsers(long num, User loginUser) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.select("id", "tags");
queryWrapper.isNotNull("tags");
List userList = this.list(queryWrapper);
String tags = loginUser.getTags();
Gson gson = new Gson();
List tagList = gson.fromJson(tags, new TypeToken>() {
}.getType());
// 用户列表的下标 => 相似度
List> list = new ArrayList();
// 依次计算所有用户和当前用户的相似度
for (int i = 0; i userTagList = gson.fromJson(userTags, new TypeToken>() {
}.getType());
// 计算分数
long distance = AlgorithmUtils.minDistance(tagList, userTagList);
list.add(new Pair(user, distance));
}
// 按编辑距离由小到大排序
List> topUserPairList = list.stream()
.sorted((a, b) -> (int) (a.getValue() - b.getValue()))
.limit(num)
.collect(Collectors.toList());
// 原本顺序的 userId 列表
List userIdList = topUserPairList.stream().map(pair -> pair.getKey().getId()).collect(Collectors.toList());
QueryWrapper userQueryWrapper = new QueryWrapper();
userQueryWrapper.in("id", userIdList);
// 1, 3, 2
// User1、User2、User3
// 1 => User1, 2 => User2, 3 => User3
Map> userIdUserListMap = this.list(userQueryWrapper)
.stream()
.map(user -> getSafetyUser(user))
.collect(Collectors.groupingBy(User::getId));
List finalUserList = new ArrayList();
for (Long userId : userIdList) {
finalUserList.add(userIdUserListMap.get(userId).get(0));
}
return finalUserList;
}
}
package com.rainbowsea.yupao.utils;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 算法工具类
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
*/
public class AlgorithmUtils {
/**
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:https://blog.csdn.net/DBC_121/article/details/104198838
*
* @param tagList1
* @param tagList2
* @return
*/
public static int minDistance(List tagList1, List tagList2) {
// 在开始计算的时候,进行一个排序,让其中的tag 标签的内容,是相同等内容排序比较对比
Collections.sort(tagList1);
Collections.sort(tagList2);
int n = tagList1.size();
int m = tagList2.size();
if (n * m == 0) {
return n + m;
}
int[][] d = new int[n + 1][m + 1];
for (int i = 0; i
分表学习建议:
mycat,sharding sphere 框架
一致性 hash 算法
权限整理
方案1:前端查询我加入了哪些队伍列表,然后判断每个队伍id 是否在列表中(前端要多发一次请
求)
方案 2:在后端去做上述事情 (推荐)
解决:使用router.beforeEach,根据要跳转页面的url 路径匹配 config/routes 配置的 title 字段。
解决:axios 全局配置响应拦截、并且添加重定向
“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
参与评论
手机查看
返回顶部