心链 — 伙伴匹配系统
组队功能开发
需求分析
理想的应用场景
我要跟别人一起参加竞赛或者做项目,可以发起队伍或者加入别人的队伍
用户可以 创建 一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 P0
- 队长、剩余的人数
- 聊天?
- 公开 或 private 或加密
- 用户创建队伍最多 5 个
展示队伍列表,根据名称搜索队伍 P0,信息流中不展示已过期的队伍
修改队伍信息 P0 ~ P1
用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是要有个上限 P0
是否需要队长同意?筛选审批?
用户可以退出队伍(如果队长 退出,权限转移给第二早加入的用户 —— 先来后到) P1
队长可以解散队伍 P0分享队伍 =》 邀请其他用户加入队伍 P1
业务流程:
- 生成分享链接(分享二维码)
- 用户访问链接,可以点击加入
队伍人满后发送消息通知 P1
数据库表设计
队伍表 team
字段:
- id 主键 bigint(最简单、连续,放 url 上比较简短,但缺点是爬虫)
- name 队伍名称
- description 描述
- maxNum 最大人数
- expireTime 过期时间
- userId 创建人 id
- status 0 - 公开,1 - 私有,2 - 加密
- password 密码
- createTime 创建时间
- updateTime 更新时间
- isDelete 是否删除
create table team
(
id bigint auto_increment comment 'id'
primary key,
name varchar(256) not null comment '队伍名称',
description varchar(1024) null comment '描述',
maxNum int default 1 not null comment '最大人数',
expireTime datetime null comment '过期时间',
userId bigint 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,
isDelete tinyint default 0 not null comment '是否删除'
)
comment '队伍';
用户 - 队伍表 user_team
字段:
- id 主键
- userId 用户 id
- teamId 队伍 id
- joinTime 加入时间
- createTime 创建时间
- updateTime 更新时间
- isDelete 是否删除
create table user_team
(
id bigint auto_increment comment 'id'
primary key,
userId bigint comment '用户id',
teamId bigint comment '队伍id',
joinTime datetime null comment '加入时间',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
isDelete tinyint default 0 not null comment '是否删除'
)
comment '用户队伍关系';
两个关系:
- 用户加了哪些队伍?
- 队伍有哪些用户?
方式:
- 建立用户 - 队伍关系表 teamId userId(便于修改,查询性能高一点,可以选择这个,不用全表遍历)
- 用户表补充已加入的队伍字段,队伍表补充已加入的用户字段(便于查询,不用写多对多的代码,可以直接根据队伍查用户、根据用户查队伍)
后端代码
实体生成
实体生成team和user-team
使用MybatisX-Generator生成domain,service和mapper文件,然后把生成的文件都移到对应的目录里面,别忘了把mapper.xml里的路径改成自己对应的。
如果直接将生成的文件拉到对应的文件,就会自动修改mapper.xml的路径
PS:别忘了在team和user_team类中的is_delete字段添加@TableLogic注解,实现逻辑删除
队伍controller接口
①增删改查
②PageRequest(序列化)---- TeamQuery继承
③自己测试 http://localhost:8080/api/doc.html#/home
@RestController
@RequestMapping("/user")
@CrossOrigin(origins = {"http://localhost:5173/"})
@Slf4j
public class TeamController {
@Resource
private UserService userService;
@Resource
private RedisTemplate redisTemplate;
@Resource
private TeamService teamService;
@PostMapping("/add")
public BaseResponse<Long> addTeam(@RequestBody Team team){
if (team == null){
throw new BusinessException(ErrorCode.NULL_ERROR);
}
boolean save = teamService.save(team);
if (!save){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"插入失败");
}
return ResultUtils.success(team.getId());
}
@PostMapping("/delete")
public BaseResponse<Boolean> deleteTeam(@RequestBody long id){
if (id <= 0){
throw new BusinessException(ErrorCode.NULL_ERROR);
}
boolean result = teamService.removeById(id);
if (!result){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"删除失败");
}
return ResultUtils.success(true);
}
@PostMapping("/update")
public BaseResponse<Boolean> updateTeam(@RequestBody Team team){
if (team == null){
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean result = teamService.updateById(team);
if (!result){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"更新失败");
}
return ResultUtils.success(true);
}
@GetMapping("/get")
public BaseResponse<Team> getTeamById(long id){
if (id <= 0){
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Team team = teamService.getById(id);
if (team == null){
throw new BusinessException(ErrorCode.NULL_ERROR);
}
return ResultUtils.success(team);
}
@GetMapping("/list")
public BaseResponse<List<Team>> listTeams(TeamQuery teamQuery) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Team team = new Team();
BeanUtils.copyProperties(team, teamQuery);
QueryWrapper<Team> queryWrapper = new QueryWrapper<>(team);
List<Team> teamList = teamService.list(queryWrapper);
return ResultUtils.success(teamList);
}
@GetMapping("/list/page")
public BaseResponse<Page<Team>> listPageTeams(TeamQuery teamQuery) {
if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Team team = new Team();
BeanUtils.copyProperties(teamQuery, team);
Page<Team> page = new Page<>(teamQuery.getPageNum(),teamQuery.getPageSize());
QueryWrapper<Team> queryWrapper = new QueryWrapper<>(team);
Page<Team> resultPage = teamService.page(page,queryWrapper);
return ResultUtils.success(resultPage);
}
}
:::success
这边我们需要新建请求参数包装类和包装类,原因如下:
为什么需要请求参数包装类?
- 请求参数名称 / 类型和实体类不一样
- 有一些参数用不到,如果要自动生成接口文档,会增加理解成本
- 对个实体类映射到同一个对象
为什么需要包装类?
可能有些字段需要隐藏,不能返回给前端
或者有些字段某些方法是不关心的
在model包里新建一个dto包,写一个包装类TeamQuery
:::
/**
* 队伍查询封装类
* @TableName team
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class TeamQuery extends PageRequest {
/**
* id
*/
private Long id;
/**
* id 列表
*/
private List<Long> idList;
/**
* 搜索关键词(同时对队伍名称和描述搜索)
*/
private String searchText;
/**
* 队伍名称
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 最大人数
*/
private Integer maxNum;
/**
* 用户id
*/
private Long userId;
/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;
}
在common包下新建分页请求参数包装类
@Data
public class PageRequest implements Serializable {
private static final long serialVersionUID = -4162304142710323660L;
/**
* 页面大小
*/
protected int pageSize;
/**
* 当前是第几页
*/
protected int pageNum;
}
接口系统设计
1、创建队伍
用户可以 创建 一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 P0
队长、剩余的人数
聊天?
公开 或 private 或加密
信息流中不展示已过期的队伍
- 请求参数是否为空?
- 是否登录,未登录不允许创建
- 校验信息
- 队伍人数 > 1 且 <= 20
- 队伍标题 <= 20
- 描述 <= 512
- status 是否公开(int)不传默认为 0(公开)
- 如果 status 是加密状态,一定要有密码,且密码 <= 32
- 超时时间 > 当前时间
- 校验用户最多创建 5 个队伍
- 插入队伍信息到队伍表
- 插入用户 => 队伍关系到关系表
@PostMapping("/add")
public BaseResponse<Long> addTeam(@RequestBody TeamAddRequest teamAddRequest, HttpServletRequest request){
if (teamAddRequest == null){
throw new BusinessException(ErrorCode.NULL_ERROR);
}
User logininUser = userService.getLogininUser(request);
Team team = new Team();
BeanUtils.copyProperties(teamAddRequest,team);
long teamId = teamService.addTeam(team,logininUser);
return ResultUtils.success(teamId);
}
public interface TeamService extends IService<Team> {
/**
* 添加队伍
* @param team
* @param loginUser
* @return
*/
long addTeam(Team team, User loginUser);
}
@Service
public class TeamServiceImpl extends ServiceImpl<TeamMapper, Team>
implements TeamService{
@Resource
UserTeamService userTeamService;
@Transactional(rollbackFor = Exception.class)
@Override
public long addTeam(Team team, User loginUser) {
//1. 请求参数是否为空?
if (team == null){
throw new BusinessException(ErrorCode.NULL_ERROR);
}
//2. 是否登录,未登录不允许创建
if (loginUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN);
}
final long userId = loginUser.getId();
//3. 校验信息
// a. 队伍人数 > 1 且 <= 20
Integer maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
if (maxNum < 1 || maxNum >20){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"队伍人数不符合要求");
}
// b. 队伍标题 <= 20
String name = team.getName();
if (StringUtils.isBlank(name) || name.length() > 20) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍标题不满足要求");
}
// c. 描述 <= 512
String description = team.getDescription();
if (StringUtils.isNotBlank(description) && description.length() > 512) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍描述过长");
}
// d. 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, "队伍状态不满足要求");
}
// e. 如果 status 是加密状态,一定要有密码,且密码 <= 32
String password = team.getPassword();
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码设置不正确");
}
}
// f. 超时时间 > 当前时间
Date expireTime = team.getExpireTime();
if (new Date().after(expireTime)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "超时时间 > 当前时间");
}
// g. 校验用户最多创建 5 个队伍
// todo 有 bug,可能同时创建 100 个队伍
QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userId", userId);
long hasTeamNum = this.count(queryWrapper);
if (hasTeamNum >= 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建 5 个队伍");
}
//4. 插入队伍信息到队伍表
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, "创建队伍失败");
}
//5. 插入用户 => 队伍关系到关系表
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;
}
}
/**
* 队伍状态枚举
*/
public enum TeamStatusEnum {
PUBLIC(0, "公开"),
PRIVATE(1, "私有"),
SECRET(2, "加密");
private int value;
private String text;
public static TeamStatusEnum getEnumByValue(Integer value) {
if (value == null) {
return null;
}
TeamStatusEnum[] values = TeamStatusEnum.values();
for (TeamStatusEnum teamStatusEnum : values) {
if (teamStatusEnum.getValue() == value) {
return teamStatusEnum;
}
}
return null;
}
TeamStatusEnum(int value, String text) {
this.value = value;
this.text = text;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
/**
* 用户添加队伍请求体
*
* @author yupi
*/
@Data
public class TeamAddRequest implements Serializable {
private static final long serialVersionUID = 3191241716373120793L;
/**
* 队伍名称
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 最大人数
*/
private Integer maxNum;
/**
* 过期时间
*/
private Date expireTime;
/**
* 用户id
*/
private Long userId;
/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;
/**
* 密码
*/
private String password;
}
ps:这里过期时间的获取可从控制台输入一下代码来实现,单单的输入年月日会导致数据库里的时间增加8小时(应该是时区的问题)
多次发送添加请求,当插入5次之后,再插入会报错