跳到主要内容

项目初始化

需求分析

  1. 用户去添加标签,标签的分类(要有哪些标签,怎么把标签进行分类) 学习方向 java/c++,工作/大学
  2. 主动搜索,允许用户根据标签去搜索其他用户
    1. Redis缓存
  3. 组队
    1. 创建队伍
    2. 加入队伍
    3. 根据标签查询队伍
    4. 邀请其他人
  4. 允许用户去修改标签
  5. 推荐
    1. 相似度计算算法+本地式分布式计算

技术栈

前端

  1. Vue3 开发框架
  2. Vant UI (基于Vue的移动组件库) (React版Zent)
  3. Vite
  4. nginx来单机部署

后端

  1. Java+Springboot框架
  2. SpringMVC+Mybatis+MybatisPlus
  3. MySQL数据库
  4. Redis缓存
  5. Swagger+Knife4j接口文档

前端项目初始化

Vue:https://cn.vuejs.org/

Vite:https://cn.vitejs.dev/

Vant:https://vant-contrib.gitee.io/vant/#/zh-CN/home

用脚手架初始项目

yarn create vite

整合组件库 Vant

yarn add vant

按需引入组件:

yarn add @vant/auto-import-resolver unplugin-vue-components -D

配置插件:

import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from '@vant/auto-import-resolver';

export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
};

注意!

有些样式样式还是需要自己引入

image-20231020103149569

前端主页+组件

导航条:展示当前页面名称:

主页搜索框=>搜索页=>搜索结果页(标签筛选页面)

内容

tab栏:

  • 主页(推荐页)
    • 搜索框
    • banner
    • 推荐信息流
  • 队伍页
  • 用户页(消息->考虑邮件发送方式)

添加navbar导航栏

    <van-nav-bar
title="标题"
left-text="返回"
right-text="按钮"
left-arrow
@click-left="onClickLeft"
@click-right="onClickRight"
>
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>


const onClickLeft = () => history.back();
const onClickRight = () => showToast('按钮');

image-20231020102414409

页面效果如下:

image-20231020102432835

添加tabbar标签栏

vue页面

    <van-tabbar v-model="active" @change="onChange">
<van-tabbar-item icon="home-o" name="index">主页</van-tabbar-item>
<van-tabbar-item icon="search" name="team">队伍</van-tabbar-item>
<van-tabbar-item icon="friends-o" name="user">个人</van-tabbar-item>
</van-tabbar>

ts

const active = ref("index");
const onChange = (index) => showToast(`标签 ${index}`);

效果如下

image-20231020103349095

页面的切换,组件化思想

    <div id="content">
<template v-if="active==='index'">
<Index/>
</template>
<template v-else-if="active==='team'">
<Team/>
</template>

</div>

数据库设计

标签的分类(要有哪些标签,怎么把标签进行分类)

标签表(分类表)

建议用标签,不要用分类,更灵活

性别:男,女

方向:Java,c++,go,前端

目标:考研,春招,秋招,社招,考公,竞赛,转行,跳槽

段位:初级,中级,高级,王者

身份:大一,大二,大三,大四,学生,待业,以就业,研一,研二,研三

状态:乐观,消极,一般,单身,已婚,有对象

标签表

字段类型备注
idint主键
tagNamevarchar标签名,唯一,索引
userIdint上传标签的用户,普通索引
parentIdint父标签id
isParenttinyint是否为父标签
createTimedatetime创建时间
updateTimedatetime修改时间
isDeletetinyint是否删除

怎么查询所有标签,并且把标签分好组? 根据父标签id查询

根据父标签查询子标签?根据id查询

create table tag
(
id bigint auto_increment comment 'id' primary key,
tagName varchar(256) null comment '标签名称',
userId bigint null comment '用户id',
parentId bigint null comment '父标签id',
isParent tinyint default 0 not null comment '0-否 1-是',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
)
comment '标签';


修改用户表

用户有哪些标签?

  1. 直接在用户表补充tags字段,['java','男'] 存json字符串

    优点:查询方便,不用新建关联表,标签是用户的固有属性(除了该系统,其他系统可能要用到,标签是用户的固有属性)

    查询用户列表,查关系表拿到这100个用户的所有标签id,再根据标签id去查标签表

    哪怕性能低,可以用缓存

    缺点:用户表多一列,会有点

  2. 加一个关联表,记录用户和标签的关系

    关联表的应用场景:查询灵活,可以正查反查

    缺点:要多建一个表,多维护一个表

    重点:企业大项目开发中尽量减少关联查询,很影响扩展性,而且会影响查询性能

选择第一种

alter table user
add tags varchar(1024) null comment '标签列表';

添加索引

create unique index tagName_idx
on tag (tagName);

create index userId_idx
on tag (userId);

image-20231020111358100

后端接口开发

搜索标签

  1. 允许用户传入多个标签,多个标签都存在才搜索出来 and
  2. 允许用户传入多个标签,有任何一个标签存在就能搜索出来 or

两种方式:

  1. SQL查询 (实现简单)
  2. 内存查询 (灵活 ,可以通过并发进一步优化)

如果参数可以分析,根据用户的参数去选择查询方式,比如标签数

如果不可以分析,并且数据库足够,内存足够,可以并发查询,谁先返回用谁

解析JSON字符串:

序列化:把Java对象转为json

反序列化:把json转为Java对象

json序列化库:

  1. Fastjson alibaba (快,漏洞太多)
  2. gson (google )
  3. jackson
  4. kryo
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>

代码:

    /**
* 根据标签搜索用户
*
* @param tagNameList 标签列表
* @return 用户列表
*/
@Override
public List<User> searchUsersByTags(List<String> tagNameList) {
if (CollectionUtils.isEmpty(tagNameList)) {
throw new BussinessException(Code.PARAMS_ERROR);
}
//return searchUsersByTagsBySQL(tagNameList);
return searchUsersByTagsByMemory(tagNameList);
}

private List<User> searchUsersByTagsByMemory(List<String> tagNameList) {
//先查询所有用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
List<User> userList = userMapper.selectList(queryWrapper);
//在内存中判断:
Gson gson = new Gson();
List<User> users = userList.stream().filter(user -> {
String tagsStr = user.getTags();
if (StringUtils.isBlank(tagsStr)) {
return false;
}
Set<String> set = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {
}.getType());
set = Optional.ofNullable(set).orElse(new HashSet<>());

for (String tagName : tagNameList) {
if (!set.contains(tagName)) {
return false;
}
}
return true;
}).map(this::getSafetyUser).collect(Collectors.toList());
return users;
}

@Deprecated //废弃
private List<User> searchUsersByTagsBySQL(List<String> tagNameList) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
for (String tagName : tagNameList) {
queryWrapper = queryWrapper.like("tags", tagName);
}
List<User> userList = userMapper.selectList(queryWrapper);
List<User> users = userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
return users;
}

前端整合路由

https://router.vuejs.org/zh/installation.html

安装

vue-router

yarn add vue-router@4

配置类route.ts

import * as VueRouter from 'vue-router'

import Index from "../pages/Index/index.vue";
import Team from "../pages/Team/index.vue";
import User from "../pages/User/index.vue";

const routes = [
{path: '/', component: Index},
{path: '/team', component: Team},
{path: '/user', component: User},
]

const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})

export default router

搜索页面

前端vue

    <van-nav-bar
title="标题"
left-text="返回"
right-text="按钮"
left-arrow
@click-left="onClickLeft"
@click-right="onClickRight"
>
<template #right>
<van-icon name="search" size="18"/>
</template>
</van-nav-bar>


<div id="content">
<router-view/>
</div>

<van-tabbar v-model="active" @change="onChange" route>
<van-tabbar-item icon="home-o" name="index" to="/">主页</van-tabbar-item>
<van-tabbar-item icon="search" name="team" to="/team">队伍</van-tabbar-item>
<van-tabbar-item icon="friends-o" name="user" to="/user">个人</van-tabbar-item>
</van-tabbar>

添加选择标签组件:

<van-divider content-position="left">已选标签</van-divider>

<template v-if="activeIds.length===0">请选择标签</template>

<van-row gutter="16">
<van-col v-for="tag in activeIds">
<van-tag closeable size="small" type="primary" @close="doClose(tag)">
{{ tag }}
</van-tag>
</van-col>
</van-row>

<van-divider content-position="left">选择标签</van-divider>

<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="filterTagList"
/>

ts代码

//搜索框文字
const searchText = ref('');
//已选中标签
const activeIds = ref([]);
//当前激活的标签
const activeIndex = ref(0);
const tagList = [
{
text: '性别',
children: [
{text: '男', id: '男'},
{text: '女', id: '女'},
],
},
{
text: '年级',
children: [
{text: '大一', id: '大一'},
{text: '大二', id: '大二'},
],
},
]
const filterTagList = ref(tagList);

//搜索 过滤
const onSearch = () => {
filterTagList.value = tagList.map(parentTag => {
const tempChildren = [...parentTag.children];
const tempParentTag = {...parentTag};
tempParentTag.children = tempChildren.filter(childTag => {
return childTag.text.includes(searchText.value);
});
return tempParentTag;
});

}

//清空搜索框
const onCancel = () => {
showToast('取消');
searchText.value = '';
filterTagList.value = tagList;
}

//关闭标签
const doClose = (tag) => {
activeIds.value = activeIds.value.filter(item => {
return item !== tag;
});
}

效果如下:

image-20231020142621629

新建一个用户模型

user.d.ts

/**
* 用户类别
*/
export type UserType = {
id: number;
username: string;
userAccount: string;
avatarUrl?: string;
profile?: string;
gender:number;
phone: string;
email: string;
userStatus: number;
userRole: number;
planetCode: string;
tags: string;
createTime: Date;
};

image-20231020142853717

个人页面

vue页面:

<template>
<van-cell title="用户名" is-link :value="user.username"
@click="toEdit('username','用户名',user.username)"/>
<van-cell title="账号" is-link :value="user.userAccount"/>
<van-cell title="头像" is-link :value="user.avatarUrl">
<VanImage :src="user.avatarUrl" height="48px" alt="cxk"/>
</van-cell>
<van-cell title="性别" is-link :value="user.gender"
@click="toEdit('gender','性别',user.gender)"/>
<van-cell title="电话" is-link :value="user.phone"
@click="toEdit('phone','电话',user.phone)"/>
<van-cell title="邮箱" is-link :value="user.email"
@click="toEdit('email','邮箱',user.email)"/>
<van-cell title="ikun编号" is-link :value="user.ikunCode"/>
<van-cell title="注册时间" is-link :value="user.createTime.toDateString()"/>
</template>

ts路由跳转传参:

<script setup lang="ts">
import {useRouter} from "vue-router";

const user = {
id: 1,
username: 'ikun',
userAccount: 'ikun',
avatarUrl: "https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp",
gender: '男',
phone: "114514",
email: "1@qq.com",
ikunCode: 1,
createTime: new Date()
}

const router = useRouter();

const toEdit = (editKey: string, editName: string, currentValue: string) => {
router.push({
path: '/user/edit',
query: {
editKey,
editName,
currentValue,
}
})
}

</script>

效果如下:

image-20231020152357669

编辑页面

vue

<template>
<van-form @submit="onSubmit">
<van-field
v-model="editUser.currentValue"
:name="editUser.editKey"
:label="editUser.editName"
:placeholder="`请输入${editUser.editName}`"
/>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>

</template>

ts逻辑:

<script setup lang="ts">

import {useRoute} from "vue-router";
import {ref} from "vue";

const route = useRoute();
const editUser = ref({
editKey: route.query.editKey,
editName: route.query.editName,
currentValue: route.query.currentValue
})
console.log(route.query)

const onSubmit = (values) => {
//todo 提交到后台
console.log('onSubmit', values)
}

</script>

效果如下:

image-20231020152503955

整合接口文档

什么是接口文档?写接口信息的文档,每条接口包括:

  • 请求参数
  • 响应参数
    • 错误码
  • 接口地址
  • 接口名称
  • 接口类型
  • 请求格式
  • 备注

一般是后端或者负责人来提供,后端和前端都要用

  • 便于沉淀和维护
  • 便于前端和后端对接,前后端联调
  • 在线测试,作为工具

怎么做:

  • 手写
  • 自动化接口文档生成 postman ,swagger,apifox,apipost

导入包:

        <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.7</version>
</dependency>

自定义配置类:

@Configuration
@EnableSwagger2WebMvc
@Profile({"dev","test"})
public class Knife4jConfiguration {

@Bean(value = "defaultApi2")
public Docket defaultApi2() {
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
//.title("swagger-bootstrap-ui-demo RESTful APIs")
.description("# swagger-bootstrap-ui-demo RESTful APIs")
.termsOfServiceUrl("http://www.xx.com/")
.contact("xx@qq.com")
.version("1.0")
.build())
//分组名称
.groupName("2.X版本")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.yunfei.ikunfriend.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
}

注意:springboot2.6和swagger不兼容,需要增加配置application.yml

spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER

访问链接:http://localhost:8080/api/doc.html

成功:

image-20231021100926834

前端开发

前端页面跳转传值

  1. Query =>url 附加参数,传递的值长度有限

搜索页传递参数:

const doSearchResult = () => {
router.push({
path: '/user/list',
query: {
tags: activeIds.value
}
})
}

搜索结果页面获取参数:

const route = useRoute();
const {tags} = route.query;

数据库新增一列个人简介:

image-20231021120837707

增加搜索接口:

    @GetMapping("/search/tags")
public Result<List<User>> searchUsersByTags(@RequestParam(required = false) List<String> tagNameList) {
if (CollectionUtils.isEmpty(tagNameList)) {
throw new BussinessException(Code.PARAMS_ERROR);
}
List<User> users = userService.searchUsersByTags(tagNameList);
return ResultUtils.success(users);
}

前端整合axios

https://axios.nodejs.cn/docs/intro

yarn add axios

配置请求:

import axios from "axios";

const request = axios.create({
baseURL: "http://localhost:8080/api"
});

request.interceptors.request.use(function (config) {
console.log("发送请求")
return config;
}, function (error) {
return Promise.reject(error);
});

request.interceptors.response.use(function (response) {
console.log("响应请求")
return response;
}, function (error) {
return Promise.reject(error);
});
export default request;

发送请求:

onMounted(async () => {
const userListData = await request.get('/user/search/tags', {
params: {tagNameList: tags},
paramsSerializer: params => {
return qs.stringify(params, {indices: false})
}
}).then(function (response) {
return response.data?.data;
}).catch(function (error) {
})
if (userListData) {
userListData.forEach(user => {
if (user.tags) {
user.tags = JSON.parse(user.tags);
}
})
userList.value = userListData;
}
})

结果:

image-20231021144315604

组队功能

需求分析

理想场景:

和别人一起参加竞赛,做项目,可以发起队伍或者加入别人的队伍

用户可以创建一个队伍,设置队伍的人数,队伍名称(标题),描述,超时时间

队长,剩余人数

聊天?

公开 or 加密

不展示过期的队伍

修改队伍信息

用户可以加入队伍(其他人,未满,未过期)

是否需要队长同意

用户可以退出队伍(如果是队长,权限转给第二个进入的用户)

队长可以解散队伍

邀请其他用户加入队伍,分享队伍

实现

数据库设计

队伍表team

字段类型说明
idbigint主键
name队伍名称
description描述
maxNum最大人数
expireTime过期时间
userId用户id
status0-公开,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 not null comment '过期时间',
userId bigint comment '队长id',
status int default 0 not null comment '0-公开,1-私有,2-加密 ',
password varchar(256) null comment '密码',
createTime datetime default CURRENT_TIMESTAMP null comment '创建时间',
updateTime datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
)
comment '队伍';

两个关系:

  1. 用户加入了哪些队伍
  2. 队伍有哪些用户?

建立 用户-队伍表 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 comment '更新时间',
isDelete tinyint default 0 not null comment '是否删除'
)
comment '用户-队伍表';

增删改查

创建队伍:

  1. 请求参数是否为空
  2. 是否登录,未登录不允许创建
  3. 校验信息
    1. 队伍>1 且<=20
    2. 队伍标题
    3. 描述<=512
    4. status是否公开
    5. 如果是加密,必须要有密码
    6. 超时时间>当前时间
    7. 校验用户最多创建五个队伍
  4. 插入队伍信息到队伍表
  5. 插入用户=>队伍关系到关系表
    @Override
@Transactional(rollbackFor = Exception.class)
public long addTeam(Team team, User loginUser) {
//1. 请求参数是否为空
if (team == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
//2. 是否登录,未登录不允许创建
if (loginUser == null) {
throw new BussinessException(Code.NOT_LOGIN);
}
//3. 校验信息
// 1. 队伍>1 且<=20
int maxNum = Optional.ofNullable(team.getMaxNum()).orElse(0);
if (maxNum < 1 || maxNum > 20) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍人数不满足要求");
}
// 2. 队伍标题
String name = team.getName();
if (StringUtils.isBlank(name) || name.length() > 20) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍标题不满足要求");
}
// 3. 描述<=512
String description = team.getDescription();
if (StringUtils.isNotBlank(description) && description.length() > 512) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍描述过长");
}
// 4. status是否公开
Integer status = Optional.ofNullable(team.getStatus()).orElse(0);
if (status < 0 || status > 3) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍状态不满足要求");
}
// 5. 如果是加密,必须要有密码
String password = team.getPassword();
if (status.equals(TeamStatusEnum.PASSWORD)) {
if (StringUtils.isBlank(password) || password.length() > 32) {
throw new BussinessException(Code.PARAMS_ERROR, "密码设置不正确");
}
}
// 6. 超时时间>当前时间
Date expireTime = team.getExpireTime();
if (new Date().after(expireTime)) {
throw new BussinessException(Code.PARAMS_ERROR, "超时时间不正确");
}
// 7. 校验用户最多创建五个队伍
QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("userId", loginUser.getId());
long hasTeamCount = this.count(queryWrapper);
if (hasTeamCount >= 5) {
throw new BussinessException(Code.PARAMS_ERROR, "最多创建五个队伍");
}
//4. 插入队伍信息到队伍表
team.setId(null);
team.setUserId(loginUser.getId());
boolean save = this.save(team);
if (!save) {
throw new BussinessException(Code.PARAMS_ERROR, "创建队伍失败");
}
//5. 插入用户=>队伍关系到关系表
Long teamId = team.getId();
UserTeam userTeam = new UserTeam();
userTeam.setUserId(loginUser.getId());
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
boolean save1 = userTeamService.save(userTeam);
if (!save1) {
throw new BussinessException(Code.PARAMS_ERROR, "创建队伍失败");
}

return teamId;
}

查询队伍列表

分页展示队伍列表,根据名称、最大人数等搜索队伍 P0,信息流中不展示已过期的队伍

  1. 从请求参数中取出队伍名称等查询条件,如果存在则作为查询条件
  2. 不展示已过期的队伍(根据过期时间筛选)
  3. 可以通过某个关键词同时对名称和描述查询
  4. 只有管理员才能查看加密还有非公开的房间
  5. 关联查询已加入队伍的用户信息
  6. 关联查询已加入队伍的用户信息(可能会很耗费性能,建议大家用自己写 SQL 的方式实现)
    @Override
public List<TeamUserVO> listTeams(TeamQueryDTO teamQueryDto, boolean isAdmin) {
QueryWrapper<Team> queryWrapper = new QueryWrapper<>();
if (teamQueryDto != null) {
Long teamId = teamQueryDto.getId();
if (teamId != null && teamId > 0) {
queryWrapper.eq("id", teamId);
}
String name = teamQueryDto.getName();
if (StringUtils.isNotBlank(name)) {
queryWrapper.like("name", name);
}

String description = teamQueryDto.getDescription();
if (StringUtils.isNotBlank(description)) {
queryWrapper.like("description", description);
}
Integer maxNum = teamQueryDto.getMaxNum();
if (maxNum != null && maxNum > 0) {
queryWrapper.eq("maxNum", maxNum);
}
Long userId = teamQueryDto.getUserId();
if (userId != null && userId > 0) {
queryWrapper.eq("userId", userId);
}
Integer status = teamQueryDto.getStatus();
if (status == null) {
status = 0;
}
if (status > -1) {
queryWrapper.eq("status", status);
}
if (!isAdmin && !status.equals(TeamStatusEnum.PUBLIC)) {
throw new BussinessException(Code.PARAMS_ERROR, "只能查看公开的队伍");
}
String searchText = teamQueryDto.getSearchText();
if (StringUtils.isNotBlank(searchText)) {
queryWrapper.and(wrapper -> wrapper.like("name", searchText)
.or().like("description", searchText));
}
}
//不展示已过期的队伍
queryWrapper.and(wrapper -> wrapper.gt("expireTime", new Date())
.or().isNull("expireTime"));


List<Team> teamList = this.list(queryWrapper);
if (CollectionUtils.isEmpty(teamList)) {
return new ArrayList<>();
}
//关联查询用户信息
//查询队伍和已加入队伍成员信息
log.info("teamList size:{}", teamList.size());
List<TeamUserVO> teamUserVOList = new ArrayList<>();
for (Team team : teamList) {
Long userId = team.getUserId();
if (userId == null) {
continue;
}
User user = userService.getById(userId);
if (user == null) {
continue;
}
TeamUserVO teamUserVO = new TeamUserVO();
BeanUtils.copyProperties(team, teamUserVO);
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
teamUserVO.setCreateUser(userVO);
teamUserVOList.add(teamUserVO);
}
return teamUserVOList;
}

修改队伍信息

  1. 查询队伍是否存在
  2. 只有管理员或者队伍的创建者可以修改
  3. 如果用户传入的值和老的一致,就不用update
    @Override
public boolean updateTeam(TeamUpdateDTO teamUpdateDTO, User loginUser) {
if (teamUpdateDTO == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
Long id = teamUpdateDTO.getId();
if (id == null || id <= 0) {
throw new BussinessException(Code.PARAMS_ERROR);
}
Team oldTeam = this.getById(id);
if (oldTeam == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
if (oldTeam.getUserId() != loginUser.getId() && !userService.isAdmin(loginUser)) {
throw new BussinessException(Code.NO_AUTH, "只能修改自己创建的队伍");
}
if (oldTeam.getStatus().equals(TeamStatusEnum.PASSWORD)) {
if (StringUtils.isBlank(teamUpdateDTO.getPassword())) {
throw new BussinessException(Code.PARAMS_ERROR, "加密房间必须要设置密码");
}
}

Team team = new Team();
BeanUtils.copyProperties(teamUpdateDTO, team);
return this.updateById(team);

}

用户加入队伍

其他人、未满、未过期,允许加入多个队伍,但是要有个上限 P0

  1. 用户最多加入 5 个队伍
  2. 队伍必须存在,只能加入未满、未过期的队伍
  3. 不能加入自己的队伍,不能重复加入已加入的队伍(幂等性)
  4. 禁止加入私有的队伍
  5. 如果加入的队伍是加密的,必须密码匹配才可以
  6. 新增队伍 - 用户关联信息

加入队伍,如果一个用户疯狂点击,可能会出现错误,需要加一把分布式锁

        public boolean joinTeam(TeamJoinDTO teamJoinDTO, User loginUser) {
if (teamJoinDTO == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
Long teamId = teamJoinDTO.getTeamId();
Team team = getTeamById(teamId);
if (team.getExpireTime() != null && team.getExpireTime().before(new Date())) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍已过期");
}
if (team.getStatus().equals(TeamStatusEnum.PRIVATE)) {
throw new BussinessException(Code.NULL_ERROR, "禁止加入私有队伍");
}
String password = teamJoinDTO.getPassword();
if (team.getStatus().equals(TeamStatusEnum.PASSWORD)) {
if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
throw new BussinessException(Code.PARAMS_ERROR, "密码错误");
}
}

Long userId = loginUser.getId();
//分布式锁
RLock lock = redissonClient.getLock("ikun:join_team");

try {
while (true) {
if (lock.tryLock(0, 30000, TimeUnit.MICROSECONDS)) {
System.out.println("getLock" + Thread.currentThread().getId());
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId", userId);
long count = userTeamService.count(userTeamQueryWrapper);
if (count > 5) {
throw new BussinessException(Code.PARAMS_ERROR, "最多创建和加入五个队伍");
}
//不能重复加入已加入的队伍
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId", userId);
userTeamQueryWrapper.eq("teamId", teamId);
long count2 = userTeamService.count(userTeamQueryWrapper);
if (count2 > 0) {
throw new BussinessException(Code.PARAMS_ERROR, "不能重复加入已加入的队伍");
}

//已加入队伍的人数
long count1 = countTeamUserByTeamId(teamId);
if (count1 >= team.getMaxNum()) {
throw new BussinessException(Code.PARAMS_ERROR, "队伍已满");
}

//插入用户=>队伍关系到关系表
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
} catch (Exception e) {
throw new BussinessException(Code.SYSTEM_ERROR);
} finally {
//只能释放自己的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

用户可以退出队伍

请求参数:队伍 id

  1. 校验请求参数

  2. 校验队伍是否存在

  3. 校验我是否已加入队伍

  4. 如果队伍

    1. 只剩一人,队伍解散

    2. 还有其他人

      1. 如果是队长退出队伍,权限转移给第二早加入的用户 —— 先来后到

        > 只用取 id 最小的 2 条数据

      2. 非队长,自己退出队伍

    @Transactional(rollbackFor = Exception.class)
public boolean quitTeam(TeamQuitDTO teamQuitDTO, User loginUser) {
if (teamQuitDTO == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
long teamId = teamQuitDTO.getTeamId();
Team team = getTeamById(teamId);
long userId = loginUser.getId();
UserTeam queryUserTeam = new UserTeam();
queryUserTeam.setTeamId(teamId);
queryUserTeam.setUserId(userId);
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>(queryUserTeam);
long count = userTeamService.count(userTeamQueryWrapper);
if (count == 0) {
throw new BussinessException(Code.PARAMS_ERROR, "未加入队伍");
}
long teamHasJoinNum = countTeamUserByTeamId(teamId);
if (teamHasJoinNum == 1) {
//如果队伍只有一个人,直接删除队伍
return this.removeById(teamId);
} else {
//如果队伍有多个人
if (team.getUserId() == userId) {
//如果是队长,把队伍给最早加入的用户
QueryWrapper<UserTeam> userTeamQueryWrapper1 = new QueryWrapper<>();
userTeamQueryWrapper1.eq("teamId", teamId);
userTeamQueryWrapper1.last("order by id asc limit 2");
List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper1);
if (CollectionUtils.isEmpty(userTeamList) || userTeamList.size() < 2) {
throw new BussinessException(Code.SYSTEM_ERROR, "队伍异常");
}
UserTeam nextUserTeam = userTeamList.get(1);
Long nextUserTeamUserId = nextUserTeam.getUserId();
//更新队伍的队长
Team updateTeam = new Team();
updateTeam.setId(teamId);
updateTeam.setUserId(nextUserTeamUserId);
boolean result = this.updateById(updateTeam);
if (!result) {
throw new BussinessException(Code.SYSTEM_ERROR, "更新队伍队长失败");
}
}
}
//删除用户=>队伍关系到关系表
return userTeamService.remove(userTeamQueryWrapper);
}

队长可以解散队伍

请求参数:队伍 id

业务流程:

  1. 校验请求参数
  2. 校验队伍是否存在
  3. 校验你是不是队伍的队长
  4. 移除所有加入队伍的关联信息
  5. 删除队伍

获取当前用户已加入的队伍

    public Result<List<TeamUserVO>> listMyJoinTeams(TeamQueryDTO teamQueryDto, HttpServletRequest request) {
if (teamQueryDto == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);

QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("userId", loginUser.getId());
List<UserTeam> userTeamList = userTeamService.list(userTeamQueryWrapper);
//取出不重复的队伍id teamId(单)=>userId(多)
Map<Long, List<UserTeam>> listMap = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
List<Long> idList = new ArrayList<>(listMap.keySet());
teamQueryDto.setIdList(idList);
List<TeamUserVO> teamList = teamService.listTeams(teamQueryDto, true);
return ResultUtils.success(teamList);
}

获取当前用户创建的队伍

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

    public Result<List<TeamUserVO>> listMyCreateTeams(TeamQueryDTO teamQueryDto, HttpServletRequest request) {
if (teamQueryDto == null) {
throw new BussinessException(Code.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
teamQueryDto.setUserId(loginUser.getId());
List<TeamUserVO> teamList = teamService.listTeams(teamQueryDto, true);
return ResultUtils.success(teamList);
}