前端优化
加载匹配功能与加载骨架特效
骨架屏 : vant-skeleton
index.vue中
/** * 加载数据 */
const loadData = async () => { let userListData; loading.value = true; //心动模式 if (isMatchMode.value){ const num = 10;//推荐人数 userListData = await myAxios.get('user/match',{ params: { num, }, }) .then(function (response) { console.log('/user/match succeed',response); return response?.data; }) .catch(function (error) { console.log('/user/match error',error); showFailToast('请求失败'); }); } else { //不开启推荐模式,默认全部查询 //普通用户使用分页查询todo 并没有实现 userListData = await myAxios.get('/user/recommend',{ params: { pageSize: 8, pageNum: 1, }, }) .then(function (response) { console.log('/user/recommend succeed', response); return response?.data?.records; }) .catch(function (error) { console.log('/user/recommends error',error); showFailToast('请求失败'); }); } if (userListData){ userListData.forEach((user: userType) =>{ if (user.tags){ user.tags = JSON.parse(user.tags); } }) userList.value = userListData; } loading.value = false;
} //`watchEffect`函数用于在响应式数据发生变化时执行副作用(side effect)操作。在这个例子中,当数据发生变化时,会调用`loadData()`函数来加载数据。watchEffect(() =>{ loadData();
})
前端导航标题
router :控制路由跳转
route: 获取路由信息
解决:使用 router.beforeEach,根据要跳转页面的 url 路径 匹配 config/routes 配置的 title 字段
//标题
const DEFAULT_TITLE='柚见'
const title=ref(DEFAULT_TITLE);
/** * 根据路由切换标题 */
router.beforeEach((to,from)=>{ const toPath=to.path; const route=routes.find((r)=>{ return toPath==r.path; }) title.value=route.title ?? DEFAULT_TITLE; })
重定向到登录页
根据后端返回的未登录的错误码,重定向
全局响应拦截器中修改
发现页面一直不能正常跳转
切换hash模式
不同的历史模式 | Vue Router (vuejs.org)
登录成功后自动跳转回之前页面
添加创建队伍按钮
添加样式
引入样式
换成vant库中的+
<!-- 创建队伍按钮--> <van-button type="primary" @click="doJoinTeam" class="add-button" icon="plus"> </van-button>
队伍操作按钮权限控制
更新队伍:仅创建人可见
v-if="team.userId === currentUser?.id"
解散队伍:仅创建人可见
v-if="team.userId === currentUser?.id"
加入队伍: 仅非队伍创建人、且未加入队伍的人可见
退出队伍:创建人不可见,仅已加入队伍的人可见
后端修改
仅加入队伍和创建队伍的人能看到队伍操作按钮(listTeam 接口要能获取我加入的队伍状态) ✔
方案 1:前端查询我加入了哪些队伍列表,然后判断每个队伍 id 是否在列表中(前端要多发一次请求)
方案 2:在后端去做上述事情(推荐)
加密队伍与公开队伍的展示
前端修改
Tab 标签页 - Vant 4 (gitee.io)
<van-tabs v-model:active="activeName"> <van-tab title="公开队伍" name="public"></van-tab> <van-tab title="加密队伍" name="secret"></van-tab>
</van-tabs>
const onTabChange=(name)=>{ if(name==='public') { //查询公开队伍 listTeam() }else if(name === 'secret'){ //查询加密队伍 listTeam(' ',2); } console.log(name) }
//只有管理员和本人才能查看非私有的房间
if (!isManager && !statusEnum.equals(TeamStatusEnum.PUBLIC) && !statusEnum.equals(TeamStatusEnum.SECRET)) {
throw new BusinessException(ErrorCode.NO_AUTH);
}
点击加入加密队伍后,弹窗密码跳出
<script setup lang="ts">
import {TeamType} from "../models/team";
import {teamStatusEnum} from "../constants/team.ts";
import myAxios from "../plungins/myAxios.js";
import {showFailToast, showSuccessToast} from "vant";
import {onMounted, ref} from "vue";
import {getCurrentUser} from "../services/user.ts";
import {useRouter} from "vue-router"; const router=useRouter();
const password=ref('');
//加入该队伍的id
const joinId=ref(0)
const showPwd=ref(false)
interface TeamCardListProps{ teamList: TeamType[];
} const props= withDefaults(defineProps<TeamCardListProps>(),{ //@ts-ignore teamList: [] as TeamType[]
}); //获得当前用户
const currentUser=ref()
onMounted(async ()=>{ const res=await getCurrentUser() currentUser.value=res
}) //加入队伍之前---------------------------------------------------------------------
const preJoinTeam=(team : TeamType)=>{ joinId.value=team.id; if(team.status === 2) { //打开加密弹窗 showPwd.value=true; } else if(team.status === 0) { doJoinTeam(); }
}
const doJoinCancel = () => { joinId.value = 0; password.value = '';
}
//点击公开队伍,加入队伍------------------------------------------------------------
const doJoinTeam=async()=>{ if(!joinId.value) { return ; } const res=await myAxios.post('/team/join',{ teamId:joinId.value, password:password.value }) if(res?.code===0){ showSuccessToast("加入成功") doJoinCancel(); }else{ showFailToast("加入失败\n"+(res.description ? `${res.description}`: '')) }
}
//跳转到更新页---------------------------------------------------------------------
const doUpdateTeam=({id}: { id: any })=>{ router.push({ path:'/team/update', query:{ id } })
}
//点击退出队伍-------------------------------------------------------------------
const doQuitTeam=async (id)=>{ const res=await myAxios.post('/team/quit',{ teamId:id }) if(res?.code===0){ showSuccessToast("操作成功") }else{ showFailToast("操作失败\n"+(res.description ? `${res.description}`: '')) } }
//点击解散队伍-------------------------------------------------------------------------
const doDeleteTeam=async ({id}: { id: any })=>{ const res=await myAxios.post('/team/delete',{ id }) if(res?.code===0){ showSuccessToast("操作成功") }else{ showFailToast("操作失败\n"+(res.description ? `${res.description}`: '')) }
} </script>
已加入队伍人数
<div> {{ `已加入人数 : ${team.hasJoinNum} / ` + team.maxNum }}
</div>
后端优化
队伍按钮权限控制
@GetMapping("/list")
public BaseResponse<List<TeamUserVO>> listTeams(TeamQuery teamQuery,HttpServletRequest request) { if (teamQuery == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean isManager = userService.isManager(request);
User loginUser = userService.getLoginUser(request); // 1、查询队伍列表
List<TeamUserVO> teamList = teamService.listTeams(teamQuery, isManager);
final List<Integer> teamIdList = teamList.stream().map(TeamUserVO::getId).collect(Collectors.toList());
// 2、判断当前用户是否已加入队伍
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
try {
userTeamQueryWrapper.eq("userId", loginUser.getId());
userTeamQueryWrapper.in("teamId", teamIdList);
List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
// 已加入的队伍 id 集合
Set<Integer> hasJoinTeamIdSet = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toSet());
teamList.forEach(team -> {
boolean hasJoin = hasJoinTeamIdSet.contains(team.getId());
team.setHasJoin(hasJoin);
});
} catch (Exception e) {} return ResultUtils.success(teamList);
}
查询已加入队伍的人数
//3.查询已加入队伍数
QueryWrapper<UserTeam> userTeamQueryWrapper2 = new QueryWrapper<>();
userTeamQueryWrapper.in("teamId", teamIdList);
List<UserTeam> userTeamList2 = userTeamService.list(userTeamQueryWrapper2); //队伍id => userId集合
Map<Integer, List<UserTeam>> teamIdUserTeamList = userTeamList2.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
teamList.forEach(team ->
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(), new ArrayList<>()).size())
);
多次快速点击加入队伍,重复加入队伍
只要我们点的足够快,就可以在同一时间内往数据库插入多条同样的数据,所以这里我们使用分布式锁(推荐)使用两把锁,一把锁锁队伍,一把锁锁用户(实现较难,不推荐)
之前的代码
//该用户已加入的队伍数量不能超过5个
int userId = loginUser.getId();
// 只有一个线程能获取到锁
RLock lock = redissonClient.getLock("youjian:join_team"); QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId",userId);
int hasJoinNum = (int) userTeamService.count(userTeamQueryWrapper);
if (hasJoinNum >= 5){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"最多创建和加入5个队伍");
} //不能重复加入已加入的队伍
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId",userId);
userTeamQueryWrapper.eq("teamId",teamId);
int hasUserJoinTeam = (int) userTeamService.count(userTeamQueryWrapper);
if (hasUserJoinTeam > 0){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户已加入该队伍");
}
//不能加入已满队伍
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("teamId",teamId);
int teamHasJoinNum = (int) userTeamService.count(userTeamQueryWrapper);
if (teamHasJoinNum >= team.getMaxNum()){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"队伍已满");
} //3.加入,修改队伍信息
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
修改后
@Resource
private RedissonClient redissonClient;