伙伴匹配系统(十一)


笔记

接上期的接口设计

5. 用户可以退出队伍

请求参数:队伍 id

  1. 校验请求参数
  2. 校验队伍是否存在
  3. 校验我是否已加入队伍
  4. 如果队伍
    1. 只剩一人,队伍解散
    2. 还有其他人
      1. 如果是队长退出队伍,权限转移给第二早加入的用户 —— 先来后到只用取 id 最小的 2 条数据
      2. 非队长,自己退出队伍
6. 队长可以解散队伍

请求参数:队伍 id 业务流程:

  1. 校验请求参数
  2. 校验队伍是否存在
  3. 校验你是不是队伍的队长
  4. 移除所有加入队伍的关联信息
  5. 删除队伍
7. 获取当前用户已加入的队伍
8. 获取当前用户创建的队伍

复用 listTeam 方法,只新增查询条件,不做修改(开闭原则)


事务注解

@Transactional(rollbackFor = Exception.class) 要么数据操作都成功,要么都失败

一、接口设计

1.用户可以退出队伍

(1). 新建退出请求体

@Data
    public class TeamQuitRequest implements Serializable {

        private static final long serialVersionUID = -2038884913144640407L;

        /**
* id
*/
        private Long teamId;

    }

(2).新建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);
}

(3).在TeamService是写入quitTeam方法

/**
* 退出队伍
* @param teamQuitRequest
* @param loginUser
* @return
*/
boolean quitTeam(TeamQuitRequest teamQuitRequest, User loginUser);

(4).在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);
}

同时在joinTeam方法里修改代码 image.png

(5).测试(详细)

我这里是队伍18有了两个用户,其中4是创建者,55007是队员(为了方便可复制得到) image.png 现在4退出队伍 image.png 的确,4已经不存在队伍18之中了 image.png 房主的确顺位给了55007 image.png 现在为了方便测试,我们直接在数据库里修改用户队伍关系表,把55007改为退出,4依旧为房主,队伍表里把房主设置为4,然后再次用4退出队伍 image.png 队伍18被成功删除 image.png 到此为止退出功能基本实现

2.在TeamService里编写删除队伍方法并在TeamServiceImpl里实现

(1).修改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);
}

(2).在TeamService里面写入deleteTeam方法

/**
* 删除队伍
* @param id
* @param loginUser
* @return
*/
boolean deleteTeam(long id, User loginUser);

(3).在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里添加一个禁止操作 image.png

@Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTeam(long id, User loginUser) {
    // 校验队伍是否存在
    Team team = getTeamById(id);
    long teamId = team.getId();
    // 校验你是不是队伍的队长
    //        //test
    //        System.out.println("team.getUserId():"+team.getUserId().getClass().getName());
    //        System.out.println("loginUser.getId():"+loginUser.getId().getClass().getName());
    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);
}

注意在操作多个数据库时,在方法上要加上@Transactional(rollbackFor = Exception.class)注解,表示要么数据操作都成功,要么都失败。 踩坑处:这里踩了大坑在校验是不是队长时,按照鱼皮的写了,运行发现报错无权限,debug发现两者的id也是一样的,最后在球友的帮助下,发现是类型的问题,打印出两者的类型是Long封装类,判断两者需要使用equals,而不是==(好像是不支持==),我这里修改为equals成功实现,建议大家先按鱼皮的写,如果报错,就可以替换下!

(4).测试

我这里重新创建了20队伍用来测试删除队伍,然后在knife4j里直接删除队伍20 image.png 查看数据库 image.png image.png 成功删除,删除队伍功能基本完成

二、前端设计

1.新建一个TeamAddPage,并在路由里添加这个页面

image.png 在TeamPage里写一个按钮跳转到TeamAddPage image.png

2.现在正式设计TeamAddPage页面,主要是在vant组件库里选择合适的组件粘贴

(1).队伍名和描述名

我们可以发现队伍名和描述名类似于用户登录页面的表单组件,所以拿来即用(修改下参数) 这个主要是运用了表单,单元格,输入框这三个组件,其中描述使用了高度自适应 image.png 参数我们可以从后台获得(knife4j接口文档) image.png

(2).过期时间

我们选择vant里的DatetimePicker选择完整时间 image.png 这里的min-date 我们不能直接new Date(),因为这会导致页面一直渲染,从而页面加载不出来,我能得在建一个常量min-date,同时这个日期默认不显示,我们要在JS里展示日期选择器 image.png

(3).最大人数

这里我们选择Stepper不进器里的限制输入范围 image.png

(4).队伍状态(当只有选择加密队伍时,才会跳出密码框)

这里我们选择表单类型里的单选框,和field输入框。 注意一定要在判断状态时,把类型转为Number,因为通过打印可得,状态是字符串类型的。 image.png

(5).提交按钮

image.png native-type=”submit”属性, 点击自动获取van-field name中的值组成的对象。 关键是提交所传的的状态也要转换成Number,同时创建成功后跳转到队伍页面 image.png AddTeamPage页面完整代码如下:

<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="datetimePicker"
            label="过期时间"
            :placeholder="addTeamData.expireTime ?? '点击选择过期时间'"
            @click="showPicker = true"
        />
        <van-popup v-model:show="showPicker" position="bottom">
          <van-datetime-picker
              v-model="addTeamData.expireTime"
              @confirm="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 {Toast} from "vant";

const router = useRouter();
// 展示日期选择器
const showPicker = ref(false);

const minDate = new Date();

const initFormData = {
  "name": "",
  "description": "",
  "expireTime": null,
  "maxNum": 3,
  "password": "",
  "status": 0,
}

// 需要用户填写的表单数据
const addTeamData = ref({...initFormData})

// 提交
const onSubmit = async () => {
  const postData = {
    ...addTeamData.value,
    status: Number(addTeamData.value.status)
  }
  // todo 前端参数校验
  const res = await myAxios.post("/team/add", postData);
  if (res?.code === 0 && res.data){
    Toast.success('添加成功');
    router.push({
      path: '/team',
      replace: true,
    });
  } else {
    Toast.success('添加失败');
  }
}
</script>

<style scoped>
#teamPage {

}
</style>

(6).启动前后端项目,测试

前端页面显示如下 image.png 输入数据,提交。我这边已经添加队伍了(忘记截图了),如果正确的话,成功跳转 数据库成功加入数据 image.png

3.设计队伍列表

1.我们首先要定义队伍类型(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;
}

2.创建一个队伍卡片列表组件(类似于用户卡片列表)

(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>

(2).然后我们将此组件挂载在TeamPage页面

image.png image.png 注意:引入team-card-list时,编译器可能不会帮你把引入的类型自动带上,需自己添加 image.png 将team-card-list里的原来的用户参数换成队伍的,测试一下 image.png 刷新页面,成功加载出组件(就是很丑,展示不齐全) image.png

(3).现在我们要完善teamcardlist组件

添加队伍状态,最大人数等以及实现加入队伍功能 我们下方要涉及到队伍的状态,我们先创建队伍状态常量 team.ts image.png 完整代码如下:

<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>

注意:1.这里thumb我们引入了图片(显示更美观),可以把自己心仪的图片放入assets里并引入

2. 加入队伍里面失败,写的形式是模板字符串,可自行了解
   式,原来的图片过于宽,要指定高度关闭自适应,这里我们使用样式穿透,不然不起作用,**如果我们给部分组件引入的样式不起作用,都可以使用样式穿透!**

(4).测试

刷新页面,页面显示如下,点击加入队伍,成功显示插入失败,已加入队伍(这里我是房主,所以加入失败,也可以登入其他用户测试) image.png

over !!!!!!!!!!!!!!


Author: qwq小小舒
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source qwq小小舒 !
  TOC