心链 — 伙伴匹配系统
组队功能开发
需求分析
理想的应用场景
我要跟别人一起参加竞赛或者做项目,可以发起队伍或者加入别人的队伍
用户可以 创建 一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间 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 {@Resourceprivate UserService userService;@Resourceprivate RedisTemplate redisTemplate;@Resourceprivate 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{@ResourceUserTeamService userTeamService;@Transactional(rollbackFor = Exception.class)@Overridepublic 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 且 <= 20Integer maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);if (maxNum < 1 || maxNum >20){throw new BusinessException(ErrorCode.PARAMS_ERROR,"队伍人数不符合要求");}// b. 队伍标题 <= 20String name = team.getName();if (StringUtils.isBlank(name) || name.length() > 20) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍标题不满足要求");}// c. 描述 <= 512String 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 是加密状态,一定要有密码,且密码 <= 32String 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次之后,再插入会报错