心链 — 伙伴匹配系统
用户可以退出队伍
请求参数:队伍 id
- 校验请求参数
- 校验队伍是否存在
- 校验我是否已加入队伍
- 如果队伍
- 只剩一人,队伍解散
- 还有其他人
1. 如果是队长退出队伍,权限转移给第二早加入的用户 —— 先来后到 只用取 id 最小的 2 条数据
2. 非队长,自己退出队伍
新建退出请求体
@Data
public class TeamQuitRequest implements Serializable {
private static final long serialVersionUID = -2038884913144640407L;
/**
* id
*/
private Long teamId;
}
新建quit请求接口
@PostMapping("/quit")
public BaseResponse<Boolean> 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);
}
在TeamService是写入quitTeam方法
/**
* 退出队伍
* @param teamQuitRequest
* @param loginUser
* @return
*/
boolean quitTeam(TeamQuitRequest teamQuitRequest, User loginUser);
在TeamServiceImpl里实现quitTeam方法
@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();
if (teamId == null || teamId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Team team = this.getById(teamId);
if (team == null) {
throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
}
long userId = loginUser.getId();
UserTeam queryUserTeam = new UserTeam();
queryUserTeam.setTeamId(teamId);
queryUserTeam.setUserId(userId);
QueryWrapper<UserTeam> 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<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("teamId", teamId);
userTeamQueryWrapper.last("order by id asc limit 2");
List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
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, "更新队伍队长失败");
}
}
}
//移除关系
return userTeamService.remove(queryWrapper);
}
这里我们由于多次需要获得队伍当前人数,所以封装了countTeamUserByTeamId方法
/**
* 获取某队伍当前人数
*
* @param teamId
* @return
*/
private long countTeamUserByTeamId(long teamId) {
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("teamId", teamId);
return userTeamService.count(userTeamQueryWrapper);
}
测试
队伍1中有5和32两个user
让1退出
队伍成功顺位给第二位32
让32再退出
队伍直接解散
队长可以解散队伍
请求参数:队伍 id
业务流程:
- 校验请求参数
- 校验队伍是否存在
- 校验你是不是队伍的队长
- 移除所有加入队伍的关联信息
- 删除队伍
在TeamService里编写删除队伍方法并在TeamServiceImpl里实现
修改delete接口
@PostMapping("/delete")
public BaseResponse<Boolean> deleteTeam(@RequestBody long id,HttpServletRequest request) {
if (id <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
boolean result = teamService.deleteTeam(id,loginUser);
if (!result) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "删除失败");
}
return ResultUtils.success(true);
}
在TeamService里面写入deleteTeam方法
/**
* 删除队伍
* @param id
* @param loginUser
* @return
*/
boolean deleteTeam(long id, User loginUser);
在TeamServiceImpl里实现deleteTeam方法
跟上面一样,我们需要根据id获取队伍信息,这个代码我们重复的写,所以提取出来
/**
* 根据 id 获取队伍信息
*
* @param teamId
* @return
*/
private Team getTeamById(Long teamId) {
if (teamId == null || teamId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
Team team = this.getById(teamId);
if (team == null) {
throw new BusinessException(ErrorCode.NULL_ERROR, "队伍不存在");
}
return team;
}
在Error_Code里添加一个禁止操作
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTeam(long id, User loginUser) {
// 校验队伍是否存在
Team team = getTeamById(id);
long teamId = team.getId();
// 校验你是不是队伍的队长
if (!team.getUserId().equals(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,"删除队伍关联信息失败");
}
// 删除队伍
return this.removeById(teamId);
}
成功解散
:::danger
事务注解
@Transactional(rollbackFor = Exception.class)
要么数据操作都成功,要么都失败
:::
TODO
获取当前用户已加入的队伍
获取当前用户创建的队伍
复用 listTeam 方法,只新增查询条件,不做修改(开闭原则)
前端
加入队伍页面
新建一个TeamAddPage,并在路由里添加这个页面
在TeamPage里写一个按钮跳转到TeamAddPage
设计TeamAddPage页面,主要是在vant组件库里选择合适的组件
(1).队伍名和描述名
我们可以发现队伍名和描述名类似于用户登录页面的表单组件,所以拿来即用(修改下参数)
这个主要是运用了表单,单元格,输入框这三个组件,其中描述使用了高度自适应
参数我们可以从后台获得(knife4j接口文档)
(2).过期时间
我们选择vant里的DatePicker 日期选择和from种的时间选择器
这里的min-date 我们不能直接new Date(),因为这会导致页面一直渲染,从而页面加载不出来,我能得在建一个常量min-date,同时这个日期默认不显示,我们要在JS里展示日期选择器,确当按钮函数
踩坑: 由于这里官方文档修改了,在这里我们前后端都要加上一个时间格式化
前端时间格式化:
下载一个moment格式化工具 npm i moent
后端时间格式化
在expireTime属性加上一个格式化注解,并给定格式
(3).最大人数
这里我们选择Stepper不进器里的限制输入范围
(4).队伍状态(当只有选择加密队伍时,才会跳出密码框)
这里我们选择表单类型里的单选框,和field输入框。
注意一定要在判断状态时,把类型转为Number,因为通过打印可得,状态是字符串类型的。
(5).提交按钮
native-type="submit"属性, 点击自动获取van-field name中的值组成的对象。
关键是提交所传的的状态也要转换成Number,同时创建成功后跳转到队伍页面
AddTeamPage页面完整代码如下:
<!--
User:Shier
CreateTime:19:54
-->
<template>
<div id="teamAddPage">
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="addTeamData.name"
name="name"
label="队伍名称"
placeholder="请输入队伍名称"
:rules="[{ required: true, message: '请输入队伍名称' }]"
/>
<van-field
v-model="addTeamData.description"
rows="4"
autosize
label="队伍描述"
type="textarea"
placeholder="请输入队伍描述"
/>
<!--过期时间-->
<van-field
is-link
readonly
name="datePicker"
label="时间选择"
:placeholder="addTeamData.expireTime ?? '点击选择关闭队伍加入的时间'"
@click="showPicker = true"
/>
<van-popup v-model:show="showPicker" position="bottom">
<van-date-picker
@confirm="onConfirm"
@cancel="showPicker = false"
type="datetime"
title="请选择关闭队伍加入的时间"
:min-date="minDate"/>
</van-popup>
<van-field name="stepper" label="最大人数">
<template #input>
<van-stepper v-model="addTeamData.maxNum" max="10" min="3"/>
</template>
</van-field>
<van-field name="radio" label="队伍状态">
<template #input>
<van-radio-group v-model="addTeamData.status" direction="horizontal">
<van-radio name="0">公开</van-radio>
<van-radio name="1">私有</van-radio>
<van-radio name="2">加密</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field
v-if="Number(addTeamData.status) === 2"
v-model="addTeamData.password"
type="password"
name="password"
label="密码"
placeholder="请输入队伍密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import {useRouter} from "vue-router";
import {ref} from "vue";
import myAxios from "../plugins/myAxios";
import moment from 'moment';
import {showFailToast, showSuccessToast} from "vant/lib/vant.es";
const router = useRouter();
// 日期展示器
const showPicker = ref(false);
// 当前时间
const minDate = new Date();
const onConfirm = ({selectedValues}) => {
addTeamData.value.expireTime = selectedValues.join('-');
showPicker.value = false;
};
const initFormData = {
"name": "",
"description": "",
"expireTime": null,
"maxNum": 5,
"password": "",
"status": 0,
}
// 需要用户填写的表单数据 对象扩展
const addTeamData = ref({...initFormData})
// 提交
const onSubmit = async () => {
const postData = {
...addTeamData.value,
status: Number(addTeamData.value.status),
expireTime: moment(addTeamData.value.expireTime).format("YYYY-MM-DD HH:mm:ss")
}
const res = await myAxios.post("/team/add", postData);
if (res?.code === 0 && res.data) {
showSuccessToast('添加成功');
router.push({
path: '/team',
replace: true,
});
} else {
showFailToast('添加失败');
}
}
</script>
<style scoped>
#teamPage {
}
</style>
队伍表单
我们首先要定义队伍类型(team.d.ts)
import {UserType} from "./user";
/**
* 队伍类别
*/
export type TeamType = {
id: number;
name: string;
description: string;
expireTime?: Date;//表示可有可无
maxNum: number;
password?: string,
// todo 定义枚举值类型,更规范
status: number;
createTime: Date;
updateTime: Date;
createUser?: UserType;
hasJoinNum?: number;
};
创建一个队伍卡片列表组件(类似于用户卡片列表)
(1).复制用户卡片列表,将userlist改为teamlist,UserCardList改为TeamCardList,UserType改为TeamType
<template>
<van-card
v-for="user in props.teamList"
:desc="user.profile"
:title="`${user.username} (${user.planetCode})`"
:thumb="user.avatarUrl"
>
<template #tags>
<van-tag plain type="danger" v-for="tag in user.tags" style="margin-right: 8px; margin-top: 8px" >
{{ tag }}
</van-tag>
</template>
<template #footer>
<van-button size="mini">联系我</van-button>
</template>
</van-card>
</template>
<script setup lang="ts">
import {TeamType} from "../models/team";
interface TeamCardListProps{
teamList: TeamType[];
}
const props= withDefaults(defineProps<TeamCardListProps>(),{
//@ts-ignore
teamList: [] as TeamType[]
});
</script>
<style scoped>
</style>
然后我们将此组件挂载在TeamPage页面
注意:引入team-card-list时,编译器可能不会帮你把引入的类型自动带上,需自己添加
将team-card-list里的原来的用户参数换成队伍的,测试一下
刷新页面,成功加载出组件(就是很丑,展示不齐全)
现在我们要完善teamcardlist组件
添加队伍状态,最大人数等以及实现加入队伍功能
我们下方要涉及到队伍的状态,我们先创建队伍状态常量 team.ts
<template>
<div>
<van-card
v-for="team in props.teamList"
:thumb="mouse"
:desc="team.description"
:title="`${team.name}`"
>
<template #tags>
<van-tag plain type="danger" style="margin-right: 8px; margin-top: 8px">
{{
teamStatusEnum[team.status]
}}
</van-tag>
</template>
<template #bottom>
<div>
{{ '最大人数: ' + team.maxNum }}
</div>
<div v-if="team.expireTime">
{{ '过期时间: ' + team.expireTime }}
</div>
<div>
{{ '创建时间: ' + team.createTime }}
</div>
</template>
<template #footer>
<van-button size="small" type="primary" plain @click="doJoinTeam(team.id)">加入队伍</van-button>
</template>
</van-card>
</div>
</template>
<script setup lang="ts">
import {TeamType} from "../models/team";
import {teamStatusEnum} from "../constants/team";
import mouse from '../assets/mouse.jpg';
import myAxios from "../plugins/myAxios";
import {Toast} from "vant";
import {useRouter} from "vue-router";
interface TeamCardListProps {
teamList: TeamType[];
}
const props = withDefaults(defineProps<TeamCardListProps>(), {
// @ts-ignore
teamList: [] as TeamType[],
});
const router = useRouter();
/**
* 加入队伍
*/
const doJoinTeam = async (id:number) => {
const res = await myAxios.post('/team/join', {
teamId: id,
});
if (res?.code === 0) {
Toast.success('加入成功');
} else {
Toast.fail('加入失败' + (res.description ? `,${res.description}` : ''));
}
}
</script>
<style scoped>
#teamCardList :deep(.van-image__img) {
height: 128px;
object-fit: unset;
}
</style>