Memory 伙伴匹配系统-开发文档

本文最后更新于:8 个月前

前端项目初始化

1
2
3
4
5
6
7
Vue 3 开发框架(提高页面开发的效率)

Vant UI(基于 Vue 的移动端组件库)(React 版 Zent)

Vite 2(打包工具,快!)

Nginx 来单机部署
  • 使用vite快速搭建一个项目(跟着文档操作就好了)

  • 在指定目录下执行该命令, 初始化项目
1
yarn create vite
  • package.json下的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"name": "partnermatch",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"vant": "^4.1.1",
"vue": "^3.2.47"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^4.9.3",
"unplugin-vue-components": "^0.24.1",
"vite": "^4.2.0",
"vue-tsc": "^1.2.0"
}
}

  • index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
  • main.js
1
2
3
4
5
6
7
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";

const app = createApp(App);
app.mount("#app");

  • App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>

<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />

<van-button type="primary" />
</template>

<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
  • 安装依赖
1
npm install
  • 启动项目
1
npm run dev
  • 项目正常启动, 接下来就是引入组件, 开发我们自己的页面了(详见官方文档)
  • 安装最新版vant
1
npm i vant
  • 引入vant组件(这里我们使用按需引入的方法)
  • 安装插件
1
2
# 通过 yarn 安装
yarn add unplugin-vue-components -D
  • 配置插件
  • 在vite.config.js下配置插件
1
2
3
4
5
6
7
8
9
10
11
12
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';

export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
};
  • 使用组件
  • 在App.vue中引入组件
1
2
3
4
5
6
7
8
<template>
<van-button type="primary">主要按钮</van-button>
<van-button type="success">成功按钮</van-button>
<van-button type="default">默认按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>
</template>

  • 在main.js中导入相应的库(因为我们使用按需引入的方法来引入组件, 所以这些库得自己导)
1
2
3
4
5
6
7
8
9
import { createApp } from "vue";
import { Button } from "vant";
import "./style.css";
import App from "./App.vue";

const app = createApp(App);
app.mount("#app");
app.use(Button);

  • 完成! 页面已经成功被我们自定义了
  • 接下来就是快速搭建我们自己的页面了
  • /layouts/BasicLayout.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<!-- 导航栏 -->
<van-nav-bar
title="标题"
fixed
left-arrow
@click-left="onClickLeft"
@click-right="onClickRight"
>
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>

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

<slot> 这里是内容 </slot>
<!-- 标签页 -->
<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>
</template>

<!-- 脚本 -->
<script setup lang="ts">
import { ref } from "vue";

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

const onClickLeft = () => alert("左");
const onClickRight = () => alert("右");

const active = ref("index");
// const onChange = (index) => alert(`${index}`);
</script>

<!-- 样式 -->
<style></style>

  • 注意vant组件的导入和vue的引入
  • 开发主页和队伍页 (/pages/Index.vue /pages/Team.vue)
1
2
3
4
5
<template>

<slot> 主页 </slot>

</template>
1
2
3
4
5
<template>

<slot> 队伍 </slot>

</template>

数据库表设计

  • 设计标签表
1
2
3
4
5
6
7
8
9
10
11
create table tag
(
id bigint auto_increment comment 'id' primary key,
tag_name varchar(256) null comment '标签名称',
userId bigint null comment '用户id',
is_Parent tinyint null comment '是否为父标签 0 - 不是 1 - 是',
parentId bigint null comment '父标签id',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null comment '更新时间',
is_delete tinyint default 0 null comment '是否删除 0 - 正常'
) comment '标签表';
  • 用户表添加tags字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create table user
(
id bigint not null comment 'id' primary key,
user_account varchar(256) null comment '账号',
username varchar(256) null comment '昵称',
user_password varchar(128) not null comment '密码',
avatar_url varchar(512) null comment '头像',
gender varchar(128) default '0' null comment '邮箱',
phone varchar(128) null comment '电话',
email varchar(128) null comment '邮箱',
user_status int default 0 not null comment '状态 0 - 正常',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null comment '更新时间',
is_delete tinyint default 0 null comment '是否删除 0 - 正常',
user_role int(1) default 0 not null comment '用户权限 0 - 管理员 1 - 普通用户',
planet_code varchar(512) not null comment '星球编号'
tags varchar(1024) null comment '标签'
)
comment '用户';

后端接口开发

根据标签查询

  • 初始化后端环境
  • 开发根据标签查询符合条件用户的方法 (service层)
  • 在内存中判断用户是个否拥有选中的标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 根据标签查询用户(在内存中判断用户是个否拥有选中的标签)
*
* @param tagNameList 打上的标签列表
* @return 查询到的用户
*/
@Override
public List<User> searchUserByTags(List<String> tagNameList) {
// 1.默认查询全部用户
QueryWrapper<User> uqw = new QueryWrapper<>();
List<User> userList = userMapper.selectList(uqw);

Gson gson = new Gson();
// 2.从查询到的用户中, 根据标签筛选出符合的用户, 组合成列表并返回
return userList.stream().filter(user -> {
// 2.1.获取用户标签
String tagsStr = user.getTags();
// 2.1.校验是否有标签
if (StringUtils.isBlank(tagsStr))
return false;
// 2.2.将标签从json字符串转换为List集合
List<String> tempTagsNameSet = gson.fromJson(tagsStr, new TypeToken<List<String>>() {
}.getType());
// 2.3.筛出标签不符合的用户
for (String tagName : tagNameList) {
if (!tempTagsNameSet.contains(tagName))
return false;
}
// 2.4.返回符合用户
return true;
}).map(this::getSafetyUser).collect(Collectors.toList());
}
  • SQL查询数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据标签查询用户(SQL查询数据库)
*
* @param tagNameList 打上的标签列表
* @return 查询到的用户
*/
@Deprecated
// @Override
private List<User> searchUserByTags2(List<String> tagNameList) {
// 1.设置查询条件
QueryWrapper<User> uqw = new QueryWrapper<>();
// where tags like "..." and like "..." and ......
for (String tagName : tagNameList) {
uqw.like("tags", tagName);
}
// 2.查询到符合标签的用户
List<User> userList = userMapper.selectList(uqw);
// 3.用户信息脱敏
return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}
  • 数据库表中添加几条测试数据

image-20230326091433304

  • 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest
public class UserServiceImplTest {
@Resource
private UserService userService;

@Test
public void searchUserByTags() {
List<String> tagNameList = Arrays.asList("java", "python");
List<User> userList = userService.searchUserByTags(tagNameList);
Assertions.assertNotNull(userList);
}

}
  • 该功能开发完成

前端整合路由

  • 记得之前开发好的前端页面吧, 当时实现跳转的方法是这样的
1
2
3
4
5
6
7
8
9
 <!-- 内容 -->
<div id="content">
<template v-if="active === 'index'">
<Index />
</template>
<template v-if="active === 'team'">
<Team />
</template>
</div>
1
2
3
4
5
6
7
8
<!-- 标签页 -->

<van-tabbar v-model="active">
<van-tabbar-item to="/" icon="home-o" name="index">主页</van-tabbar-item>
<van-tabbar-item to="/team" icon="search" name="team">队伍</van-tabbar-item>
<van-tabbar-item to="/user" icon="friends-o" name="user"
\>个人</van-tabbar-item>
</van-tabbar>\
1
2
// 默认选中页
const active = ref("index");
  • 真他妈原始, 我们要整点高级的 ==> 使用路由跳转的方式实现页面跳转
  • 引入Vue-Router组件
  • 看官方文档, 就好了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 定义路由组件.
// 也可以从其他文件导入
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }

// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]

// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes, // `routes: routes` 的缩写
})

// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router)

app.mount('#app')

// 现在,应用已经启动了!

  • 我们稍微修改一下, 实现我们自定义页面的路由跳转
  • 把路由的定义封装到src/config/route.ts下, 在main文件中引入就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 定义路由组件.
import IndexPage from "../pages/IndexPage.vue";
import TeamPage from "../pages/TeamPage.vue";
import UserPage from "../pages/UserPage.vue";

// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: "/", component: IndexPage },
{ path: "/team", component: TeamPage },
{ path: "/user", component: UserPage },
];

export default routes;

  • 这是我们的main.ts文件了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { createApp } from "vue";
import { Button, Icon, NavBar, Tabbar, TabbarItem } from "vant";
import App from "./App.vue";
import * as VueRouter from "vue-router";
import routes from "./config/route";

const app = createApp(App);
app.use(Button);
app.use(Icon);
app.use(NavBar);
app.use(Tabbar);
app.use(TabbarItem);

// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = VueRouter.createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: VueRouter.createWebHashHistory(),
routes: routes, // `routes: routes` 的缩写
});
// 5. 创建并挂载根实例
//确保 _use_ 路由实例使
//整个应用支持路由。
app.use(router);
app.mount("#app");
  • 这里我们的路由就配置好了, 总体来说还是很简单的
  • 浅浅尝试一下用法吧 (尝完就可以删了)
1
2
3
4
5
6
<div id="content">
<router-view />
</div>

<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
  • 如上, 这样就能够使用路由实现页面跳转了, 非常方便!
  • 幸运的是, 我们的van-tabbar组件也内置了路由跳转, 改写为以下形式:
1
2
3
4
5
6
7
8
<!-- 标签页 -->
<van-tabbar route>
<van-tabbar-item to="/" icon="home-o" name="index">主页</van-tabbar-item>
<van-tabbar-item to="/team" icon="search" name="team">队伍</van-tabbar-item>
<van-tabbar-item to="/user" icon="friends-o" name="user"
>个人</van-tabbar-item
>
</van-tabbar>
  • 路由整合完成了呢
  • 差点忘记了, 这里有个非常恶心的BUG, 我配完vue-router配置后, 启动服务显示页面为空白, 怎么搞都没反应, 结果把自定义路由那儿的 “/“ 删了改成 “/index”后, 就他妈有页面了, 我他妈给改回去后, 既然能正常显示了?!真奶奶的无语, 还好老子聪明机智哈哈哈, 差点栽这儿出不来了

前端页面开发

  • 路由整合完毕之后,接下来就要开发我们的搜索页面了:

搜索页面

  • 开发页面之前,我们先把搜索页面的路由配置好吧 (src/config/route.ts)
1
2
3
4
5
6
7
8
9
10
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routes = [
{ path: "/", component: IndexPage },
{ path: "/team", component: TeamPage },
{ path: "/user", component: UserPage },
{ path: "/search", component: SearchPage },
{ path: "/user/edit", component: UserEditPage },
];
  • 这里顺带把后面的用户信息页和用户信息修改页路由页配置好了,后面就不涉及了

  • 点击导航栏的搜索符号跳转到搜索页面
1
2
3
const onClickRight = () => {
router.push("/search");
};
  • 去找到合适的组件,完成页面开发
1
2
3
4
5
6
7
8
9
10
<!-- 搜索栏 -->
<form action="/">
<van-search
v-model="searchText"
show-action
placeholder="请输入搜索关键词"
@search="onSearch"
@cancel="onCancel"
/>
</form>
1
2
3
<!-- 分割线 -->
<van-divider content-position="left">已选标签</van-divider>
<van-divider content-position="right">可选标签</van-divider>
1
2
3
4
5
6
7
8
<!-- 选中的标签  layout布局 -->
<van-row gutter="20">
<van-col v-for="tag in activeIds">
<van-tag closeable size="medium" type="primary" @close="close">
{{ tag }}
</van-tag>
</van-col>
</van-row>
1
2
3
4
5
6
<!-- 标签列表 -->
<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="tagList"
/>
  • 在脚本里编写一些逻辑,最终达成了:
  1. 搜索栏可以筛选标签列表里的标签
  2. 选中标签列表后可以把标签整合成json字符串,将来可以发送json字符串实现根据标签搜索用户
  3. 注:这块逻辑比较难,可以多加理解消化,这里不做过多介绍了(因为我自己也看懵了,照着人家的代码写下来的)
    展示一下搜索页最终代码和实现效果吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<template>
<!-- 搜索栏 -->
<form action="/">
<van-search
v-model="searchText"
show-action
placeholder="请输入搜索关键词"
@search="onSearch"
@cancel="onCancel"
/>
</form>
<!-- 分割线 -->
<van-divider content-position="left">已选标签</van-divider>
<div v-if="activeIds.length === 0">请选择标签</div>
<!-- 选中的标签 layout布局 -->
<van-row gutter="20">
<van-col v-for="tag in activeIds">
<van-tag closeable size="medium" type="primary" @close="close">
{{ tag }}
</van-tag>
</van-col>
</van-row>

<van-divider content-position="right">可选标签</van-divider>
<!-- 标签列表 -->
<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="tagList"
/>
</template>

<script setup lang="ts">
import { ref } from "vue";

const searchText = ref("");

// 已选中的标签
const activeIds = ref([]);
const activeIndex = ref(0);

// 原始标签列表
const originTagList = [
{
text: "年级",
children: [
{ text: "大一", id: "大一" },
{ text: "大二", id: "大二" },
{ text: "大三", id: "大三" },
{ text: "大四", id: "大四" },
{ text: "大五", id: "大五", disabled: true },
],
},
{
text: "性别",
children: [
{ text: "男", id: "男" },
{ text: "女", id: "女" },
],
},
];

// 实际标签列表
let tagList = ref(originTagList);

/**
* 搜索过滤
* @param val
*/
const onSearch = (val: any) => {
tagList.value = originTagList.map((parentTag) => {
const tempChildren = [...parentTag.children];
const tempParentTag = { ...parentTag };
tempParentTag.children = tempChildren.filter((item) =>
item.text.includes(searchText.value)
);
return tempParentTag;
});
};

// 取消
const onCancel = () => {
searchText.value = "";
tagList.value = originTagList;
};

// 关闭标签
const close = (tag: any) => {
activeIds.value = activeIds.value.filter((item) => {
return item !== tag;
});
};
</script>

image-20230402160806287

用户信息页

  • 这里就比较简单了,开发!
  • 引入相关表单组件 Cell单元格
1
2
3
4
5
6
7
8
9
10
11
<template>
<!-- <slot> 个人 </slot> -->
<van-cell title="账号" is-link to="/user/edit" :value="user.username" />
<van-cell title="昵称" is-link to="/user/edit" :value="user.userAccount" />
<van-cell title="头像" is-link to="/user/edit" :value="user.avatarUrl" />
<van-cell title="性别" is-link to="/user/edit" :value="user.gender" />
<van-cell title="电话" is-link to="/user/edit" :value="user.phone" />
<van-cell title="邮箱" is-link to="/user/edit" :value="user.email" />
<van-cell title="星球编号" is-link to="/user/edit" :value="user.planetCode" />
<van-cell title="注册时间" is-link to="/user/edit" :value="user.createTime.toISOString()"/>
</template>
  • 自己在src/models下写一个user.d.ts,自定义userType类型,将来填充表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 用户信息
export type userType = {
id?: number;
userAccount?: string;
username?: string;
avatarUrl?: string;
gender?: string;
phone?: string;
email?: string;
createTime?: Date;
userStatus?: string;
userRole?: number;
planetCode?: string;
};
  • 自定义一个用户对象,由于目前没有相关接口从后端拿取数据,给定一些假数据测试效果
1
2
3
4
5
6
7
8
9
10
11
const user = {
id: 1,
userAccount: "memory",
username: "邓哈哈",
avatarUrl: "",
gender: "男",
phone: "18887786754",
email: "3348407547@qq.com",
planetCode: "17625",
createTime: new Date(),
};
  • 对表单稍作修改,给表单列绑定数据模型,同时点击相关列后可以修改该项,并跳转至用户修改页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<template>
<!-- <slot> 个人 </slot> -->
<van-cell
title="账号"
is-link
to="/user/edit"
:value="user.userAccount"
@click="toEdit('userAccount', '账号', user.userAccount)"
/>
<van-cell
title="昵称"
is-link
to="/user/edit"
:value="user.username"
@click="toEdit('username', '昵称', user.username)"
/>
<van-cell
title="头像"
is-link
to="/user/edit"
:value="user.avatarUrl"
@click="toEdit('avatarUrl', '头像', user.avatarUrl)"
/>
<van-cell
title="性别"
is-link
to="/user/edit"
:value="user.gender"
@click="toEdit('gender', '性别', user.gender)"
/>
<van-cell
title="电话"
is-link
to="/user/edit"
:value="user.phone"
@click="toEdit('phone', '电话', user.phone)"
/>
<van-cell
title="邮箱"
is-link
to="/user/edit"
:value="user.email"
@click="toEdit('email', '邮箱', user.email)"
/>
<van-cell
title="星球编号"
is-link
to="/user/edit"
:value="user.planetCode"
@click="toEdit('planetCode', '星球编号', user.planetCode)"
/>
<van-cell
title="注册时间"
is-link
to="/user/edit"
:value="user.createTime.toISOString()"
@click="toEdit('createTime', '注册时间', user.createTime.toISOString())"
/>
</template>
  • script下实现toEdit方法,携带相关参数并跳转至用户修改页下(路由我们前面写过了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useRouter } from "vue-router";

const router = useRouter();

const toEdit = (editKey: string, editName: string, currentValue: string) => {
router.push({
path: "/user/edit",
query: {
editKey,
editName,
currentValue,
},
});
};
  • 这个跳转并携带参数涉及到vue-Router的两个组件,非常重要,马上讲到👇

用户信息修改页

  • 跟前面同样的操作,引入相关组件 From表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="username"
name="用户名"
label="用户名"
placeholder="用户名"
/>
</van-cell-group>

<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
  • 自定义一个修改后的用户信息对象
1
2
3
4
5
const editUser: editUserType = ref({
editKey: route.query.editKey,
editName: route.query.editName,
currentValue: route.query.currentValue,
});
  • 修改一下表单,能不能实现用户编辑信息页的某项信息时,用户信息修改页正确显示
  • 改成这样👇:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- <slot>用户编辑页</slot> -->
<van-form @submit="onSubmit">
<van-field
v-model="editUser.currentValue"
:name="editUser.editKey"
:label="editUser.editName"
:placeholder="`${editUser.editKey}`"
/>

<div style="margin: 16px">
<van-button
round
block
type="primary"
native-type="submit"
>
提交
</van-button>
</div>
</van-form>
  • 测试一下,完成功能!

image-20230402163407935

  • 什么原理呢?用户信息页的信息是怎样传到用户信息修改页并正确显示的呢?我们捋一捋:
  • 用户信息页我们写过这个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useRouter } from "vue-router";

const router = useRouter();

const toEdit = (editKey: string, editName: string, currentValue: string) => {
router.push({
path: "/user/edit",
query: {
editKey,
editName,
currentValue,
},
});
};
  • 干了什么?一句话:就是携带了参数 query 并跳转到了对应路由组件 “/user/edit” 里
  • 当然,组件 router 实现了这个功能
  • 那用户信心修改页呢,又干了什么?
1
2
3
4
5
6
7
8
9
import { useRoute } from "vue-router";

const route = useRoute();

const editUser: editUserType = ref({
editKey: route.query.editKey,
editName: route.query.editName,
currentValue: route.query.currentValue,
});
1
2
3
4
5
6
<van-field
v-model="editUser.currentValue"
:name="editUser.editKey"
:label="editUser.editName"
:placeholder="`${editUser.editKey}`"
/>
  • 也是一句话:拿到了 当前路由组件下接收的参数 query , 并在表单上 绑定了数据模型,同步显示了而已
  • 当然,组件 route 实现了这个功能

Swagger + knif4j 自动生成接口文档

  • 这里我们用knif4j,来到官方文档跟着快速操作即可
1
2
3
4
5
6
7
<!--引入Knife4j的官方start包,该指南选择Spring Boot版本<3.0,开发者需要注意-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>

  • 第二步:创建Swagger配置依赖,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.memory.usercenter.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
* @author 邓哈哈
* 2023/4/2 23:39
* Function:
* Version 1.0
*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
@Bean(value = "dockerBean")
public Docket dockerBean() {
//指定使用Swagger2规范
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
//描述字段支持Markdown语法
.title("Memory用户中心")
.description("# Memory用户中心接口文档")
.termsOfServiceUrl("https://gitee.com/deng-2022/client-center")
.contact("3348407547@qq.com")
.version("1.0")
.build())
//分组名称
.groupName("用户服务")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.memory.usercenter.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
}

  • 然后启动项目即可
  • 访问 http://localhost:8081/api/doc.html 成功自动生成接口文档!
  • 如果springBoot版本高于2.6,可能会有报错,这是因为 knif4j 不兼容现今高版本的springBoot,这里有两种解决办法:
  1. 降低springBoot的版本使其兼容knif4j(很不现实的解决方法,不推荐)
  2. application.yaml文件里添加如下配置:
1
2
3
4
spring:
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
  • 问题即可解决!
  • 这里还要注意Swagger文档不能随意暴露在外!可能会有不法分子利用该文档调用接口、泄露数据
  • 我们在application.yaml下定义开发环境:
1
2
3
spring:
profiles:
active: dev
  • 在knif4j配置前还可以配置@Profile,在指定开发环境下才会生成接口文档,否则访问失败
1
2
3
4
5
6
@Configuration
@EnableSwagger2WebMvc
@Profile({"dev", "test"})
public class Knife4jConfiguration {
...........................
}

抓取网页信息

  • 从Excel表格中导入用户数据 -> EasyExcel
  • 看着官网,跟着操作即可 (在新增目录/once下):
  • 编写映射对象
1
2
3
4
5
6
7
8
9
10

@Data
public class UserInfo {
@ExcelProperty("成员编号")
private String planetCode;


@ExcelProperty("成员昵称")
private String username;
}
  • 编写监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 有个很重要的点 DemoDataListener 不能被spring管理,
// 要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class TableListener implements ReadListener<UserInfo> {

/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(UserInfo data, AnalysisContext context) {
log.info("解析到一条数据:{}", data);
}

/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}

}
  • 在/resources下导入一张Excel表格testExcel.xlsx
  • 读取表中数据,这里有两种方式:
1
2
3
4
5
6
7
8
9
10
@Slf4j
public class ImportExcel {
/**
* 指定列的下标或者列名
*/
public static void main(String[] args) {
readByListener();
synchronousRead();
}
}
  • 方法一:使用监听器
1
2
3
4
5
6
7
8
/**
* 监听器
*/
public static void readByListener() {
String fileName = "D:\\Project\\星球项目\\ClientCenter\\user-center\\src\\main\\resources\\testExcel.xlsx";
// 这里默认读取第一个sheet
EasyExcel.read(fileName, UserInfo.class, new TableListener()).sheet().doRead();
}
  • 方法二:同步返回
1
2
3
4
5
6
7
8
9
10
/**
* 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面
*/
public static void synchronousRead() {
String fileName = "D:\\Project\\星球项目\\ClientCenter\\user-center\\src\\main\\resources\\testExcel.xlsx";
List<UserInfo> list = EasyExcel.read(fileName).head(UserInfo.class).sheet().doReadSync();
for (UserInfo data : list) {
log.info("读取到数据:{}", data);
}
}
  • 要了解这两者的区别和优缺点,请移步至鱼皮的开发文档
  • 执行main方法,成功地读取到了Excel表的数据,如下
1
2
3
4
5
22:42:23.479 [main] INFO com.memory.usercenter.once.ImportExcel - 读取到数据:UserInfo(planetCode=1, username=邓哈哈)
22:42:23.491 [main] INFO com.memory.usercenter.once.ImportExcel - 读取到数据:UserInfo(planetCode=2, username=邓呵呵)
22:42:23.491 [main] INFO com.memory.usercenter.once.ImportExcel - 读取到数据:UserInfo(planetCode=3, username=邓嘻嘻)
22:42:23.492 [main] INFO com.memory.usercenter.once.ImportExcel - 读取到数据:UserInfo(planetCode=4, username=邓哇哇)
22:42:23.492 [main] INFO com.memory.usercenter.once.ImportExcel - 读取到数据:UserInfo(planetCode=5, username=邓几把)

根据标签搜索用户

  • 搜索到的用户总得展示吧,那么我们先做一个用户列表页
  • 写一个搜索按钮,用来携带已选中标签信息,获取符合要求用户,并跳转到用户列表页展示,不要纠结样式
1
2
3
4
5
6
7
<!-- 搜索 -->
<van-button
type="primary"
style="margin: 8px; padding: 20px"
@click="doSearch()"
>搜索</van-button
>
  • 先不谈发送请求,这里我们先实现跳转吧,很简单
1
2
3
4
5
6
7
8
9
10
11
12
// 根据标签搜索, 向后台发送请求
import { useRouter } from "vue-router";
const router = useRouter();

const doSearch = () => {
router.push({
path: "/user/list",
query: {
tags: activeIds.value,
},
});
};
  • 开发用户列表页 (Card 商品卡片)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<van-card
num="2"
price="2.00"
desc="描述信息"
title="商品标题"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #tags>
<van-tag plain type="primary">标签</van-tag>
<van-tag plain type="primary">标签</van-tag>
</template>
<template #footer>
<van-button size="mini">按钮</van-button>
<van-button size="mini">按钮</van-button>
</template>
</van-card>

  • 这里再次console.log()打印一下,是可以拿到SearchPage页携带的选中的标签的
1
2
3
4
import { useRoute } from "vue-router";
const route = useRoute();

console.log(route.query);

image-20230404211104286

  • 筛选逻辑这里先不考虑,用假数据测试吧
  • 写个测试数据,也就是一个用户列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const users = [
{
id: 1,
userAccount: "memory",
username: "邓哈哈",
avatarUrl: "",
gender: "男",
phone: "18887786754",
email: "3348407547@qq.com",
planetCode: "17625",
createTime: new Date(),
profile: "这个用户很懒,什么也没写~",
tags: ["java", "emo", "努力中"],
},
{
id: 1,
userAccount: "memory",
username: "邓哈哈",
avatarUrl: "",
gender: "男",
phone: "18887786754",
email: "3348407547@qq.com",
planetCode: "17625",
createTime: new Date(),
profile: "这个用户很懒,什么也没写~",
tags: ["java", "emo", "努力中"],
},
];
  • 稍微修改一下,遍历users拿到每一个user,再正确显示user的属性,自己看着设计样式吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<van-card
v-for="user in users"
:tag="user.gender"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #tags>
<van-tag
plain
type="primary"
v-for="tag in user.tags"
style="margin-right: 3px; margin-top: 3px"
>
{{ tag }}
</van-tag>
</template>
<template #footer>
<van-button size="mini">联系我</van-button>
</template>
</van-card>
  • 测试代码,开发完成

image-20230404211229967

  • 接下来就是打通前后端,实现根据标签搜索用户

根据标签查询(补充)

  • 我们先在表user中添加个新字段 profile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- auto-generated definition
create table user
(
id bigint not null comment 'id'
primary key,
user_account varchar(256) null comment '账号',
username varchar(256) null comment '昵称',
user_password varchar(128) not null comment '密码',
avatar_url varchar(512) null comment '头像',
gender varchar(128) default '0' null comment '邮箱',
phone varchar(128) null comment '电话',
email varchar(128) null comment '邮箱',
user_status int default 0 not null comment '状态 0 - 正常',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null comment '更新时间',
is_delete tinyint default 0 null comment '是否删除 0 - 正常',
user_role int(1) default 0 not null comment '用户权限 0 - 管理员 1 - 普通用户',
planet_code varchar(512) not null comment '星球编号',
profile varchar(512) default '这个用户很懒,什么也没写~' null comment '用户描述',
tags varchar(1024) null comment '标签'
)
comment '用户';
  • 记得修改几处地方,这里就不一一演示了:/model/User,userMapper.xml,service下的 getSafetyUser()方法
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 标签列表
*
* @param tagNameList
* @return
*/
@GetMapping("/search/tags")
public BaseResponse<List<User>> searchByTags(@RequestParam List<String> tagNameList) {
if (CollectionUtils.isEmpty(tagNameList))
throw new BusinessException(PARMS_ERROR);

List<User> userList = userService.searchUserByTags(tagNameList);
return ResultUtils.success(userList);
}
  • 接下来我们要在前端页面发送请求了,跟着官网来,安装axios
1
yarn add axios
  • 在/plugins/myAxios.ts下配置 myAxios 和请求拦截器、响应拦截器,
1
2
3
4
5
import axios from "axios";
// Set config defaults when creating the instance
const myAxios = axios.create({
baseURL: "http://localhost:8081/api",
});
1
2
3
4
5
6
7
8
9
10
11
12
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
console.log(`我他妈发请求了${config}`);
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
console.log(`我他妈响应了 ${response}`);
// 对响应数据做点什么
return response;
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
);

export default myAxios;
  • 在userListPage.vue下写个钩子函数,发送请求到后端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import myAxios from "../plugins/myAxios";
import qs from "qs";

onMounted(() => {
const route = useRoute();
const { tags } = route.query;
// 上面的请求也可以这样做
myAxios
.get("/user/search/tags", {
params: {
tagNameList: tags,
},
paramsSerializer: {
serialize: (params) => qs.stringify(params, { indices: false }),
},
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
});
  • 简单介绍一下运作原理
  • 钩子函数不用多废话了
  • 引入我们的myAxios,发送请求,语法要熟悉
  • 引入 userRoute,拿到searchPage页携带的标签列表,语法要熟悉
  • 然后就是引入 qs , 可以正确地在axios请求中携带数组参数发送到后端,详情还需去百度了解
  • 这里我还踩了两个坑,补充说明一下吧:
  1. axios配置baseURL时,我给配成了 “https://localhost:8081/api",把http写成了https,导致响应状态码一直是500,唉
  2. 然后就是 qs 了,坑死我了,先介绍一下 qs 引入流程:
1
yarn add qs
1
2
3
4
5
6
7
8
9
10
11
import qs from "qs";

// 旧版
paramsSerializer: (params) => {
return qs.stringify(params, {arrayFormat: 'repeat'})
}

// 新版
paramsSerializer: {
serialize: (params) => qs.stringify(params, { indices: false }),
},
  • 我他妈就用了旧版,半天都不能正确发送请求,那用户列表页还他么卡死
  • 好了,请求成功发送了

打通前后端查询用户

  • 发送请求成功后,后端接口成功返回包含对应标签的用户(具体逻辑实现看 => 后端接口开发)
  • 发送请求逻辑我们已经写好了,接下来就是响应后端发回数据的逻辑了
  • 我们在钩子函数前后写上:
1
2
3
4
5
import {ref} from "vue";

const route = useRoute();
const { tags } = route.query;
const userList = ref([]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
onMounted(async () => {
// 响应数据
const userListData = await myAxios
.get("/user/search/tags", {
params: {
tagNameList: tags,
},
paramsSerializer: {
serialize: (params) => qs.stringify(params, { indices: false }),
},
})
.then(function (response) {
console.log(response);
// 返回响应数据(用户列表)
return response.data?.data;
})
.catch(function (error) {
console.log(error);
});

if (userListData) {
userListData.forEach((user: any) => {
if (user.gender == 1) user.gender = "男";
if (user.gender == 0) user.gender = "女";

if (user.tags) user.tags = JSON.parse(user.tags);
});
}
userList.value = userListData;
});
  • 这块儿实现了什么逻辑呢,简单讲讲:
  1. 返回了响应数据,返回的用户列表是Json字符串,我们把它序列化为列表 userList
  2. 将用户信息中的性别显示为 ‘男’ ‘女’
  • 再修改一下表数据,之前我们用的是假数据
1
2
3
4
5
6
7
<van-card
v-for="user in userList"
:tag="`${user.gender}`"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
  • 好极了,我们根据 ‘男’ 标签,搜索用户,得到了正确结果

image-20230405150346771

Session共享实现

  • 用来实现在多台服务器之间共享登录态
  • 安装redis Redis 5.0.14 下载:
1
2
链接:https://pan.baidu.com/s/1XcsAIrdeesQAyQU2lE3cOg
提取码:vkoi
  • 安装quick-redis
1
quick redis:https://quick123.net/
  • 引入redis,能够操作 redis:
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
  • 引入 spring-session 和 redis 的整合 使得自动将 session 存储到 redis 中:
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
  • 修改 spring-session 存储配置 spring.session.store-type
  • 默认是 none,表示存储在单台服务器
  • store-type: redis,表示从 redis 读写 session
1
2
3
4
5
6
7
8
9
10
spring:
# session 失效时间(分钟)
session:
timeout: 86640
store-type: redis
# redis 配置
redis:
port: 6379
host: localhost
database: 0
  • 完成!
  • 现在登录态已经可以写入redis了,还是共享的

image-20230405204848156

  • 接下来就要实现用户登录、用户信息展示、修改用户信息,但涉及到了跨域问题
  • 这块login后,后端种下了session,但axios发送请求时,老是携带不到cookie,getCurrentUser()获取不到当前用户登录态
  • 先做后边的功能吧,这一块儿我再研究研究,这个跨域携带cookie好像挺难搞定
  • axios不能成功携带cookie的话,上面的需求都做不了
  • 三天后老子回来了,爷爷解决问题了!那么接下来,就让我捋一捋这个周末都学到了些什么吧!

后端接口开发

修改用户信息

  • 后端 service层
  • 实现思路:
  • 管理员修改用户接口
1
2
3
4
5
6
7
8
9
10
11
/**
* 管理员修改用户信息
*
* @param user
* @return
*/
@Override
public String userUpdateByAdmin(User user) {
userMapper.updateById(user);
return "修改信息成功";
}
  • 用户普通用户修改用户接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 修改用户信息
*
* @param user 要修改的用户
* @param loginUser 当前登录用户
* @return 修改接过信息
*/
@Override
public String userUpdate(User user, User loginUser) {
// 1.1.校验管理员权限
if (isAdmin(loginUser)) {
// 1.2.如果是管理员, 就跳转到管理员修改用户接口, 执行修改并返回结果
return userUpdateByAdmin(user);
}

// 1.3.非管理员, 就执行普通用户修改用户方法
// 根据传回来的id, 判断当前用户是否为要修改的用户
if (!loginUser.getId().equals(user.getId()))
throw new BusinessException(NO_AUTH);

userMapper.updateById(user);
return "修改信息成功";
}
  • 这里我们在service层封装了两个 isAdmin() 方法
1
2
3
4
5
6
7
8
9
/**
* @param loginUser 校验的用户
* @return 校验成功与否(t / f)
*/
@Override
public Boolean isAdmin(User loginUser) {
//校验是否为管理员
return loginUser != null && loginUser.getUserRole() == ADMIN_ROLE;
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 校验是否为管理员
*
* @param request request
* @return 校验成功与否(t / f)
*/
public Boolean isAdmin(HttpServletRequest request) {
//校验是否为管理员
User user = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
return user != null && user.getUserRole() == ADMIN_ROLE;
}

  • 顺带的,我们整理了所有的 controller层 和 service层的代码,最终呈现出统一格式:
  • controller 层负责:简单校验参数 —> 调用service层的接口代码 —>通用返回结果
  • service 层负责:实现全部的业务逻辑代码
  • 整个项目结构就变得很清晰了,那我们简单展示一下现在的 项目结构 和 代码规范 吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
项目结构
|_ main
|_ java
|_ com.memory.usercenter
|_ common
|_ BaseResponse.java
|_ ErrorCode.java
|_JacksonObjectMapper.java
|_ ResultUtils.java
|_ config
|_ Knif4jConfiguration.java
|_ MyIntercepter.java
|_ WebConfig.java
|_ constant
|_ UserConstant.java
|_ controller
|_UserCtroller.java
|_ exception
|_ BusinessException.java
|_ GlobalExceptionHandler.java
|_ mapper
|_ UserMapper.java
|_ model
|_ request
|_ UserLoginRequest.java
|_ UserRegisterRequest.java
|_ User
|_ once
|_ ImportExcel.java
|_ InsertUser.java
|_ TableListener.java
|_UserInfo.java
|_ service
|_ impl
|_ UserServiceImpl.java
|_ UserService.java
|_ util
|_ UserCenterApplication.java
|_ resources
|_ mapper
|_ UserMapper.xml
|_ application.yaml
|_ testExcel.xlsx
|_ test
|_ java
|_ com.memory.usercenter
...................
|_ target
|_ pom.xml
  • 前端
  • 修改用户信息,我们提到了要校验在线用户权限对吧,这个在线用户登录态我们在用户中心里做过,但伙伴匹配还未实现,所以在实现接下来,我们先实现登录和记录用户登录态功能
  • 添加登录页路由
1
2
3
4
5
6
7
8
9
10
11
import UserLoginPage from "../pages/UserLoginPage.vue";

const routes = [
{ path: "/", component: IndexPage }, // 主页
{ path: "/team", component: TeamPage }, // 队伍页
{ path: "/user", component: UserPage }, // 个人页
{ path: "/search", component: SearchPage }, // 搜索页
{ path: "/user/edit", component: UserEditPage }, // 用户信息修改页
{ path: "/user/list", component: UserListPage }, // 用户列表页
{ path: "/user/login", component: UserLoginPage }, // 用户登录页
];
  • 借助组件开发用户登录页 Form表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="userAccount"
name="用户名"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-model="userPassword"
type="password"
name="密码"
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>
</template>
  • 发送登录请求
  • 还记得之前封装好的MyAxios吧,与表单双向绑定后,携带 userAccount userPassword json字符串 发送至后端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup lang="ts">
import { ref } from "vue";
import myAxios from "../plugins/myAxios";
import { useRouter } from "vue-router";
import { showSuccessToast, showFailToast } from "vant";
import { requestData } from "../models/user";

const router = useRouter();
const userAccount = ref("");
const userPassword = ref("");

const onSubmit = async () => {
const res: requestData = await myAxios.post("/user/login", {
userAccount: userAccount.value,
userPassword: userPassword.value,
});

if (res.code === 0 && res.data) {
showSuccessToast("登录成功");
// 登录成功后跳转至主页
router.push("/");
} else {
showFailToast("登录失败");
}
};
</script>
  • 用户可以登录了,且登录后记录了用户登录态,将来可以获取到登录用户信息
  • 我们在主页的钩子函数上实现一个功能,如果获取不到用户登录态,就跳转至登录页进行登录
1
2
3
4
5
6
7
8
9
// 校验是否登录, 未登录则跳转至登录页;
onMounted(async () => {
const res = await getCurrentUser();
console.log(res);
if (!res.data) {
console.log("未登录!");
router.replace("/user/login");
}
});
  • 类似的,我们可以在之后的展示当前用户信息、修改用户信息之前,获取当前用户登录态,以便对该用户进行权限校验

展示当前用户信息

  • 我们要在多个页面上 获取当前用户登录态 来校验权限,所以我们先把这个方法封装起来
  • 在 service/user.ts 下实现
1
2
3
4
5
6
import myAxios from "../plugins/myAxios";

// 获取当前登录用户
export const getCurrentUser = async () => {
return await myAxios.get("/user/currentUser");
};
  • 那么这个方法就是封装好的 getCurrentUser() 方法了
  • 接下来,在UserPage下写一个钩子函数,获取当前用户并成功展示
1
2
3
4
5
import { useRouter } from "vue-router";
import { onMounted } from "vue";
import { showSuccessToast, showFailToast } from "vant";
import { getCurrentUser } from "../service/user";
import { ref } from "vue";
1
2
3
4
5
6
7
8
9
10
11
12
13
const user = ref();
// 钩子函数
onMounted(async () => {
// 发送获取当前登录用户请求
const res = await getCurrentUser();

if (res.data) {
showSuccessToast("获取用户信息成功");
user.value = res.data;
} else {
showFailToast("获取用户信息失败");
}
});
  • 顺便优化了表单显示效果:当获取到 user 数据就显示表单信息,否则显示空白页面 Empty
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<div v-if="user">
<van-cell
title="账号"
is-link
to="/user/edit"
:value="user.userAccount"
@click="toEdit('userAccount', '账号', user.userAccount)"
/>vu
<van-cell
title="昵称"
is-link
to="/user/edit"
:value="user.username"
@click="toEdit('username', '昵称', user.username)"
/>
<van-cell
title="头像"
is-link
to="/user/edit"
:value="user.avatarUrl"
@click="toEdit('avatarUrl', '头像', user.avatarUrl)"
/>
<van-cell
title="性别"
is-link
to="/user/edit"
:value="user.gender"
@click="toEdit('gender', '性别', user.gender)"
/>
<van-cell
title="电话"
is-link
to="/user/edit"
:value="user.phone"
@click="toEdit('phone', '电话', user.phone)"
/>
<van-cell
title="邮箱"
is-link
to="/user/edit"
:value="user.email"
@click="toEdit('email', '邮箱', user.email)"
/>
<van-cell
title="星球编号"
is-link
to="/user/edit"
:value="user.planetCode"
@click="toEdit('planetCode', '星球编号', user.planetCode)"
/>
<van-cell
title="注册时间"
is-link
to="/user/edit"
:value="user.createTime"
@click="toEdit('createTime', '注册时间', user.createTime)"
/>
<van-empty v-else description="获取用户信息失败" />
</div>

发送修改信息请求

  • 之前我们实现过 UserPage页 的数据传输到 UserEditPage页
  • 这次实现成功修改用户信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const onSubmit = async (values: string) => {
const currentUser = await getCurrentUser();
//提交之前做校验
if (currentUser.data) {
// 发送用户修改请求
const res = myAxios.post("/user/update", {
id: currentUser.data.id,
[editUser.value.editKey as string]: editUser.value.currentValue,
});

if (res.code === 0 && res.data) {
showSuccessToast("修改成功");
router.replace("/user");
}
}
};
  • 同样的,执行修改之前我们又获取了一把用户登录态,并将该用户 id 连同 修改字段 一并发送至后端,这样后端就可以鉴权了

打通前后端?

  • ok,到目前为止,前后端基本打通了——是吗?
  • axios 发送请求时,默认不会携带cookie,这就导致了:你登录成功了,你的登录态也在后端写进cookie了,但是之后axios发送请求获取当前登录用户信息,由于没有cookie,是拿不到的
  • 拿不到当前登录用户信息,剩下的用户信息展示、信息修改就都是扯淡了
  • 这是为什么呢?因为我们前后端分离,虽然前后端服务器都在本地,但前端端口号:7070 后端端口号:8081
  • 两个相同域名 但不同端口的服务器 在请求响应,这就是浏览器上的跨域问题:解决跨域问题引起的axios无法正确携带cookie
  • 如何解决?这里有最简单的解决方法
  • 前端 MyAxios.ts 加一行代码 —— withCredentials: true
1
2
3
4
const myAxios = axios.create({
baseURL: "http://localhost:8081/api",
withCredentials: true,
});
  • 后端 controller 加一行代码 —— @CrossOrigin
1
2
3
4
@CrossOrigin
public class UserController {
......................
}
  • 到这里正常来讲问题就应该解决了,但如果这时你发现你前端所有的请求都报错的话,那说明:前端axios是允许携带cookie了,但后端服务器不接受
  • @CrossOrigin没有起作用
  • 那就试试第二种解决方案吧:
  • config/WebConfig 下编写拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.memory.usercenter.config;

import com.memory.usercenter.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;

/**
* Web配置类
*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 添加Web项目的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对所有访问路径,都通过MyInterceptor类型的拦截器进行拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
//放行登录页,登陆操作,静态资源
}

/**
* 允许跨域请求
* @param registry registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:7070", "http://localhost:8000")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}

  • 这个拦截器的重点是:
1
2
3
4
// 允许访问的请求地址 - 这里是 伙伴匹配 和 用户中心
.allowedOrigins("http://localhost:7070", "http://localhost:8000")
// 允许携带cookie的请求
.allowCredentials(true)
1
2
import dns from "dns";
dns.setDefaultResultOrder("verbatim");
1
2
3
4
server: {
host: "localhost",
port: 7070,
},
  • 好了,以上就是跨域axios携带cookie的解决办法
  • 最后还有个小BUG,就是后端接收到前端发送的id时,由于id是long型,在传输过程中可能会有精度丢失,导致前后端的id不一致,这里的解决办法在之前的raggie_take_out里也用过,那便是:
  • 提供对象转换器JacksonObjectMapper common 基于Jackson进行Java到json数据的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.memory.usercenter.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
  • 在WebMvcConfig配置类中扩展Spring mvc的消息处理器
  • 在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 扩展消息转换器
*
* @param converters converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置具体的对象映射器
messageConverter.setObjectMapper(new JacksonObjectMapper());
//通过索引设置,让自己的转换器放在最前面,否则默认的jackson转换器会在最前面,用不上我们设置的转换器
converters.add(0, messageConverter);
}
  • 这样就解决了long型数据精度丢失的问题了

导入用户数据

  • 第一种:Export data to file导出数据(CSV格式),再Export data from file导入数据 适用于快速导入少量的数据
  • 第二种就是用编程的方式批量插入数据了
  • 先简单地写下代码实现方法吧:
  • 在once/InsertUser下编写:
1
2
3
4
@Component
public class InsertUser {
....................
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Resource
private UserService userService;

// 项目启动后, 每隔5秒就执行一次该方法
@Scheduled(fixedDelay = 5000)
// @Scheduled(initialDelay = 5000, fixedDelay = Long.MAX_VALUE)
public void doInsertUsers() {
StopWatch stopWatch = new StopWatch();
System.out.println("go go go go");
// 计时开始
stopWatch.start();
// 插入数据条数
final int INSERT_NUM = 1000;
for (int i = 0; i < INSERT_NUM; i++) {
User user = new User();
user.setUserAccount("memory");
user.setUsername("邓哈哈");
user.setUserPassword("12345678");
user.setAvatarUrl("");
user.setGender("");
user.setPhone("18889889898");
user.setEmail("3348407547@qq.com");
user.setUserStatus(0);
user.setUserRole(1);
user.setPlanetCode("17625");
user.setTags("");
// 插入数据
userService.save(user);
}
// 计时结束
stopWatch.stop();
// 计算整个插入过程耗费的时间
System.out.println(stopWatch.getTotalTimeMillis());
}
  • 最后在项目启动类上写下:
1
@EnableScheduling
  • 启动项目!每隔5秒就会插入1000条用户数据~
  • 然后我们写个测试类,在测试类中测试批量插入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@SpringBootTest
public class InsertUserTest {
@Resource
private UserService userService;

/**
* 插入数据
*/
@Test
public void doInsertUsers1() {
StopWatch stopWatch = new StopWatch();
System.out.println("go go go go");
stopWatch.start();

final int INSERT_NUM = 1000;
for (int i = 0; i < INSERT_NUM; i++) {
User user = new User();
user.setUserAccount("memory");
user.setUsername("邓哈哈");
user.setUserPassword("12345678");
user.setAvatarUrl("");
user.setGender("");
user.setPhone("18889889898");
user.setEmail("3348407547@qq.com");
user.setUserStatus(0);
user.setUserRole(1);
user.setPlanetCode("17625");
user.setTags("");

userService.save(user);
}

stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
}
  • 我们采取批量插入的方式,一次性插入多条记录,减少建立和释放数据库连接的次数,提高插入效率
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 批量插入数据
*/
@Test
public void doInsertUsers2() {
StopWatch stopWatch = new StopWatch();
System.out.println("go go go go");
stopWatch.start();

ArrayList<User> userList = new ArrayList<>();

final int INSERT_NUM = 1000;
for (int i = 0; i < INSERT_NUM; i++) {
User user = new User();
user.setUserAccount("memory");
user.setUsername("邓哈哈");
user.setUserPassword("12345678");
user.setAvatarUrl("");
user.setGender("");
user.setPhone("18889889898");
user.setEmail("3348407547@qq.com");
user.setUserStatus(0);
user.setUserRole(1);
user.setPlanetCode("17625");
user.setTags("");
userList.add(user);
}
userService.saveBatch(userList, 100);

stopWatch.stop();
System.out.println(stopWatch.getTotalTimeMillis());
}
  • 我们也可以采用并发的方式批量插入数据,效率更高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 并发批量插入数据
*/
@Test
public void doConcurrencyInsertUsers() {
// new一个StopWatch对象
StopWatch stopWatch = new StopWatch();
// 计时开始
stopWatch.start();
// 每条线程插入1000条
int batchSize = 1000;
int j = 0;
// 创建一个异步任务集合
ArrayList<CompletableFuture<Void>> futureList = new ArrayList<>();
// 开10条线程
for (int i = 0; i < 10; i++) {
// 每条线程下new一个userList
ArrayList<User> userList = new ArrayList<>();
while (true) {
j++;
User user = new User();
user.setUserAccount("memory");
user.setUsername("邓哈哈");
user.setUserPassword("12345678");
user.setAvatarUrl("");
user.setGender("");
user.setPhone("18889889898");
user.setEmail("3348407547@qq.com");
user.setUserStatus(0);
user.setUserRole(1);
user.setPlanetCode("17625");
user.setTags("");

userList.add(user);
// 当该线程插满1000条数据,便退出该线程循环
if (j % batchSize == 0) {
break;
}
}
// 异步条件下, 执行批量插入
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("threadName: " + Thread.currentThread().getName());
userService.saveBatch(userList, batchSize);
});
// 将该任务存储到异步任务集合当中
futureList.add(future);
}
// 结束所有异步任务
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[]{})).join();
// 计时结束
stopWatch.stop();
// 计算插入所用总时间
System.out.println(stopWatch.getTotalTimeMillis());
}
  • 到此为止,批量导入用户数据我们就实现了
  • 接下来我们优化了批量插入的数据:1.账户非空且唯一 2.密码 加密 3.标签字符串 4.星球编号非空且唯一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    User user = new User();
user.setUserAccount("memory" + "_" + (UUID.randomUUID() + "").substring(0, 8));
user.setUsername("邓哈哈");
// user.setUsername("邓哇哇");
String password = DigestUtils.md5DigestAsHex((SALT + 12345678).getBytes());
user.setUserPassword(password);
user.setAvatarUrl("https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg");
user.setGender("1");
// user.setGender("0");
user.setPhone("18535854763");
user.setEmail("3348407547@qq.com");
user.setUserStatus(0);
user.setUserRole(0);
user.setPlanetCode("17625");
user.setTags("[\"男\",\"Java\",\"Python\",\"在校本科\",\"开朗\",\"努力中\"]");
// user.setTags("[\"女\",\"Vue\",\"Python\",\"在校本科\",\"发呆\",\"emo中\"]");

  • 接下来我们要开发主页了
  • 现阶段思路很简单,主页能够展示所有用户信息就行

后端接口开发

查询全部用户

  • service 层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 展示所有用户信息
* 分页查询
*
* @param currentPage 当前页
* @param pageSize 每页显示数
* @return 用户列表
*/
@Override
public Page<User> selectPage(long currentPage, long pageSize) {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
Page<User> userPage = new Page<>(currentPage, pageSize);

return userMapper.selectPage(userPage, lqw);
}
  • controller 层
1
2
3
4
5
6
7
8
9
10
/**
* 根据标签查询用户
*
* @return 用户列表
*/
@GetMapping("/recommend")
public BaseResponse<Page<User>> recommend(@RequestParam long currentPage, long pageSize) {
Page<User> userList = userService.selectPage(currentPage, pageSize);
return ResultUtils.success(userList);
}
  • config WebConfig MP分页插件
1
2
3
4
5
6
7
8
9
/**
* 分页插件(官网最新)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
  • 接下来就是开发主页了

前端页面开发

主页页面开发

  • 使用组件快速开发页面 (跟UserListPage页大同小异)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<div id="content" style="padding-bottom: 30px">
<!-- 用户信息展示 -->
<van-card
v-for="user in userList"
:tag="`${user.gender}`"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
:thumb="`${user.avatarUrl}`"
>
<!-- 标签展示 -->
<template #tags>
<van-tag
plain
type="primary"
v-for="tag in user.tags"
style="margin-right: 3px; margin-top: 3px"
>
{{ tag }}
</van-tag>
</template>

<template #footer>
<van-button size="mini">联系俺</van-button>
</template>
</van-card>
<!-- 无用户信息展示 -->
<van-empty v-if="!userList" description="获取用户信息失败" />
<!-- 分页插件 -->
<van-pagination
v-model="currentPage"
:total-items="total"
:items-per-page="pageSize"
force-ellipses
@change="change"
/>
</div>
  • 页面发送请求到后端接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
import { userType } from "../models/user";
import myAxios from "../plugins/myAxios";

const userList = ref([]);

// 当前页码
const currentPage = ref(1);
// 每页显示数
let pageSize = 10;
// 总记录数
let total: number = 0;

const getPage = async (currentPage: number) => {
// 发送请求, 获取用户数据列表
const userListData = await myAxios
.get("/user/recommend", {
params: {
currentPage: currentPage,
pageSize: pageSize,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
total = response.data?.total;
pageSize = response.data?.size;
return response.data?.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功拿到用户数据列表(不为空)
if (userListData) {
// 遍历用户数据列表
userListData.forEach((user: userType) => {
// 将gender的 "1"/"0" 渲染成 "男"/"女"
if (user.gender === "1") user.gender = "男";
if (user.gender === "0") user.gender = "女";
// JSON字符串序列化为列表
if (user.tags) user.tags = JSON.parse(user.tags);
});
// 处理过后的用户列表
userList.value = userListData;
}
};

// 用户列表, 钩子函数
onMounted(() => {
getPage(currentPage.value);
});

// 改变页码
const change = () => {
getPage(currentPage.value);
};
</script>

  • 这块儿逻辑注释已经写得很清楚了,不过还是简单介绍一下吧:
  • axios请求,携带currentPage和pageSize发送至后端,响应到对应用户列表
  • 之前MyAxios封装过返回值:response.data
  • 需注意到,我们使用了vant的分页组件
1
2
3
4
5
6
7
8
<!-- 分页插件 -->
<van-pagination
v-model="currentPage" -- 当前页码
:total-items="total" -- 记录总数
:items-per-page="pageSize" -- 每页记录数
force-ellipses
@change="change" -- 改变页码
/>
  • 有了这个组件,通过change方法,可以很方便地改变当前页码currentPage,同时执行查询,可以查到每页数据
  • 组件其他属性也都标明了,可以控制总记录数、每页记录数
  • 这里我们使用response.data?.records、response.data?.size、response.data?.total可分别获取:每页数据记录、每页数据容量、总记录数
  • 当然,组件上的总记录数total就可以拿到了,pageSize这边是固定死的,每页10条,暂不支持灵活改变每页显示数
  • 以上是对这段逻辑实现的粗略介绍,如有疑问还请研读代码,实现过程是十分清晰的

Redis缓存

  • 之前我们给数据库批量插入了大量数据,并且在主页根据展示出所有用户信息
  • 为了提高查询效率,我们还采用了分页查询的方式
  • 但这种解决办法并不彻底,频繁查询数据库还是很低效,所以我们使用Redis技术来解决查询效率低下的问题

Redis的引入和测试

  • 之前在解决Session共享的时候已经做过了:
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.4</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
spring:
# session 失效时间(分钟)
session:
timeout: 86640
store-type: redis
# redis 配置
redis:
port: 6379
host: localhost
database: 0
  • 引入了Redis,二话不说先测增删改查,接下来小小地测试一把(test/redis/RedisTest):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@SpringBootTest
public class RedisTest {
@Resource
private RedisTemplate redisTemplate;

@Resource
private StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();

@Test
public void test1() {
stringRedisTemplate.opsForValue().set("memory", "邓哈哈");
// stringRedisTemplate.opsForValue().set("memory", 18);
redisTemplate.opsForValue().set("memory2", 9);
String memory = stringRedisTemplate.opsForValue().get("memory");
}

@Test
public void test2() {
// 增
redisTemplate.opsForValue().set("memoryString", "dog");
redisTemplate.opsForValue().set("memoryInt", 1);
redisTemplate.opsForValue().set("memoryDouble", 3.0);
User user = new User();
user.setId(9999L);
user.setUserAccount("memoryc7b93cb1b3");
redisTemplate.opsForValue().set("memoryUser", user);
// 查
Object dog = redisTemplate.opsForValue().get("memoryString");
Assertions.assertTrue("dog".equals(dog));
Object anInt = redisTemplate.opsForValue().get("memoryInt");
Assertions.assertTrue(1 == (Integer) anInt);
Object anDouble = redisTemplate.opsForValue().get("memoryDouble");
Assertions.assertTrue(3.0 == (Double) anDouble);
Object memoryUser = redisTemplate.opsForValue().get("memoryUser");
log.info(memoryUser + "");
// 删
redisTemplate.delete("memoryInt");
}
}
  • 增删改查没有问题
  • 这里存在这样的问题:RedisTemplate 底层的序列化方式,会导致存入的序列化后的value值成为乱码
  • StringRedisTemplate 继承了 RedisTemplate 有效解决了这个问题,但只能存放<String,String>
  • 综上,我们在使用Redis缓存技术时,可以自己自定义(封装一个)RedisTemplate
  • 自定义 RedisTemplate<String, Object> (config/RedisTemplateConfig)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 1.创建RedisTemplate对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 2.设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 3.设置Key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
// 4.创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 5.设置value的序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
return redisTemplate;
}
}
  • 接下来,就要修改原分页查询用户信息的逻辑代码了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 展示所有用户信息
* Redis缓存
* 分页查询
*
* @param currentPage 当前页
* @param pageSize 每页显示数
* @return 用户列表
*/
@Override
public Page<User> selectPage(long currentPage, long pageSize, HttpServletRequest request) {
// 获取当前登录用户
User loginUser = getLoginUser(request);
// 拿到当前登录用户的key(每个用户都有各自对应的key)
String redisKey = String.format("memory:user:recommend:%s", loginUser.getId());
// 查缓存
Page<User> userPage = (Page<User>) redisTemplate.opsForValue().get(redisKey);
// 缓存未中, 则返回用户信息
if (userPage != null) {
return userPage;
}
// 缓存未命中, 查询数据库
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
userPage = userMapper.selectPage(new Page<>(currentPage, pageSize), lqw);
// 将查询到的用户信息写到缓存中
try {
redisTemplate.opsForValue().set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set key error", e);
}
// 返回用户数据
return userPage;
}
  • 那首次查询是不是不走缓存,只能走数据库了?那我们就要实现缓存预热,定时缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @author 邓哈哈
* 2023/4/15 15:22
* Function: 缓存预热
* Version 1.0
*/

@Component
@Slf4j
public class PreCacheJob {
@Resource
private UserService userService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
// 重点用户id(提供推送的用户id)
private final List<Long> mainUserList = List.of(1L);
// 每天执行,预热推荐用户
@Scheduled(cron = "0 * * * * *") //自己设置时间测试
public void doCacheRecommendUser2() {
// 遍历每个重点用户
for (Long userId : mainUserList) {
QueryWrapper<User> qw = new QueryWrapper<>();
// 分页查询用户信息
Page<User> userPage = userService.page(new Page<>(1, 20), qw);
// 为每个重点用户设置预查询锁
String redisKey = String.format("memory:user:recommend:%s", userId);
//写缓存,30s过期
try {
redisTemplate.opsForValue().set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set key error", e);
}
}
}

分布式锁

  • 问题来了,在实际工作中,我们面对的往往是集群服务器,难道在某一刻每台服务器都要执行预热缓存用户吗?
  • 显然不需要。我们只需要一台服务器成功缓存到用户就行了。
  • 要控制定时任务在同一时间只有 1 个服务器能执行。(怎么做?)
  • 分离定时任务程序和主程序,只在 1 个服务器运行定时任务。成本太大

  • 写死配置,每个服务器都执行定时任务,但是只有 ip 符合配置的服务器才真实执行业务逻辑,其他的直接返回。成本最低;但是我们的 IP 可能是不固定的,把 IP 写的太死了

  • 动态配置,配置是可以轻松的、很方便地更新的(代码无需重启),但是只有 ip 符合配置的服务器才真实执行业务逻辑。问题:服务器多了、IP 不可控还是很麻烦,还是要人工修改

    • 数据库
    • Redis
    • 配置中心(Nacos、Apollo、Spring Cloud Config)
  • 分布式锁,只有抢到锁的服务器才能执行业务逻辑。坏处:增加成本;好处:不用手动配置,多少个服务器都一样。

  • 单机就会存在单点故障。
  • 多的概念在这里不再赘述,可以去看鱼皮的项目笔记,分布式锁实现原理以及注意事项讲的很清楚

Redisson 实现分布式锁

  • Java 客户端,数据网格,实现了很多 Java 里支持的接口和数据结构
  • Redisson 是一个 java 操作 Redis 的客户端
  • 提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在。
  • 那我们就使用redission来快速实现分布式锁控制吧
  • 导入相关依赖
1
2
3
4
5
6
<!--redission-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.1</version>
</dependency>
  • 完成相关配置 config/RedissionConfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data

public class RedissionConfig {
private String host;

private String port;

@Bean
public RedissonClient redissonClient() {
// 1. 创建配置
Config config = new Config();
String redisAddress = String.format("redis://%s:%s", host, port);
// 使用单个Redis,没有开集群 useClusterServers() 设置地址和使用库
config.useSingleServer().setAddress(redisAddress).setDatabase(3);
// 2. 创建实例
RedissonClient redisson = Redisson.create(config);
return redisson;
}

}
  • 快速测试 test/RedisTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SpringBootTest
public class RedissonTest {

@Resource
private RedissonClient redissonClient;

@Test
void test() {
// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));

list.remove(0);

// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
// rList.remove(0);

// map
Map<String, Integer> map = new HashMap<>();
map.put("yupi", 10);
map.get("yupi");

RMap<Object, Object> map1 = redissonClient.getMap("test-map");
map1.put("memory", 12);
map1.put("memory2", 12);
map1.put("memory3", 12);
}
}
  • 改造定时执行任务,用分布式锁实现,以下是实现逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* @author 邓哈哈
* 2023/4/15 15:22
* Function: 缓存预热
* Version 1.0
*/

@Component
@Slf4j
public class PreCacheJob {
@Resource
private UserService userService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedissonClient redissonClient;
// 重点用户id(提供推送的用户id)
private final List<Long> mainUserList = List.of(1L);
// 每天执行,预热推荐用户
@Scheduled(cron = "0 * * * * *") //自己设置时间测试
public void doCacheRecommendUser() {
// 设置分布式锁
RLock lock = redissonClient.getLock("memory:preCacheJob:doCache:lock");

try {
// 如果抢锁成功
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
// 遍历每个重点用户
for (Long userId : mainUserList) {
QueryWrapper<User> qw = new QueryWrapper<>();
// 分页查询用户信息
Page<User> userPage = userService.page(new Page<>(1, 20), qw);
// 为每个重点用户设置预查询锁
String redisKey = String.format("memory:user:recommend:%s", userId);
//写缓存,30s过期
try {
redisTemplate.opsForValue().set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("redis set key error", e);
}
}
}
} catch (InterruptedException e) {
log.info("error = " + e);
throw new RuntimeException(e);
} finally {
// 如果该分布式锁是自己持有的
if (lock.isHeldByCurrentThread()) {
log.info(String.format("unlock: %s", Thread.currentThread().getId()));
// 释放锁
lock.unlock();
}
}
}
}

  • 这个逻辑就很清晰了,我们多开几个服务器测试一把
  • 下次再更新这块儿吧,还有很多坑呢

组队功能

后端开发

  • 队伍表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create table team
(
id bigint auto_increment comment 'id' primary key,
user_id bigint null comment '队长id',
name varchar(256) not null comment '队伍名称',
description varchar(1024) null comment '描述',
max_num int default 1 not null comment '最大人数',
join_num int default 0 not null comment '已加人数',
status int default 0 not null comment '0 - 公开,1 - 私有,2 - 加密',
password varchar(512) null comment '密码',
expire_time datetime null comment '过期时间',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
is_delete tinyint default 0 not null comment '是否删除'
)
comment '队伍表';
  • 队伍-用户表
1
2
3
4
5
6
7
8
9
10
11
create table user_team
(
id bigint auto_increment comment 'id' primary key,
user_id bigint null comment '用户id',
team_id bigint null comment '队伍id',
join_time datetime null comment '加入时间',
create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null comment '修改时间',
is_delete tinyint default 0 not null comment '是否删除'
)
comment '用户队伍关系';
  • 首先快速开发相关接口,之后再根据业务逻辑优化
    • 添加队伍
    • 删除队伍
    • 修改队伍
    • 按队伍id查找队伍
    • 查找全部队伍
  • 咱这建表速度还是很快滴~
  • 再然后借助MybatisX-Generator快速生成domain、mapper、service和相关注意事项等就不说了

新增队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 新增队伍
*
* @param team 队伍
* @return 队伍id
*/
@Override
@Transactional
public String teamAdd(TeamAdd team, HttpServletRequest request) {
// 1.是否登录,未登录不允许创建
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.队伍人数 > 1 且 <= 20
Integer maxNum = team.getMaxNum();
if (maxNum < 1 || maxNum > 20)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍人数不符合要求");

// 3.队伍标题 <= 20
String name = team.getName();
if (StringUtils.isBlank(name) || name.length() > 20)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍标题不符合要求");

// 4.描述 <= 512
String description = team.getDescription();
if (StringUtils.isBlank(description) || description.length() > 512)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍描述不符合要求");

// 5.status 是否公开(int)不传默认为 0(公开)
int status = Optional.ofNullable(team.getStatus()).orElse(0);
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");

// 6.校验队伍密码
String password = team.getPassword();
// 6.1.如果队伍非加密, 则不允许设置密码
if (statusEnum.getValue() != 2) {
if (StringUtils.isNotBlank(password))
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍密码不符合要求");
}

// 6.2.如果队伍加密, 一定要有密码, 且密码 <= 32
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍密码不符合要求");
}

// 7.当前时间 > 超时时间
Date expireTime = team.getExpireTime();
if (new Date().after(expireTime))
throw new BusinessException(ErrorCode.PARMS_ERROR, "超时时间不符合要求");

// 8.校验用户已创建队伍数量(最多创建 5 个队伍)
Long userId = loginUser.getId();
QueryWrapper<Team> lqw = new QueryWrapper<>();
lqw.eq("user_id", userId);
long count = this.count(lqw);
if (count >= 5)
throw new BusinessException(ErrorCode.PARMS_ERROR, "该用户创建队伍数量超出限制");

Team addTeam = new Team();
BeanUtils.copyProperties(team, addTeam);

// 9.插入队伍信息到team表
addTeam.setId(null);
addTeam.setUserId(userId);

boolean teamSave = this.save(addTeam);
if (!teamSave) throw new BusinessException(ErrorCode.UPDATE_ERROR);

// 10.插入用户-队伍关系到user_team表
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(addTeam.getId());

boolean userTeamSave = userTeamService.save(userTeam);
if (!userTeamSave) throw new BusinessException(ErrorCode.UPDATE_ERROR);

return "新增队伍成功";
}
  • 这里status校验要留意,封装一个 constant/TeamStatuaEnum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

/**
* @author 邓哈哈
* 2023/4/20 10:36
* Function: 队伍状态枚举
* Version 1.0
*/
public enum TeamStatusEnum {
/**
* 0 - 公开, 在队伍大厅中可以直接加入
*/
PUBLIC(0, "公开"),

/**
* 1 - 私有, 在队伍大厅中不可以直接加入
*/
PRIVATE(1, "私有"),

/**
* 2 - 公开且加密, 加入队伍需要密码
*/
SECRET(2, "加密");

/**
* 状态码
*/
private int value;

/**
* 状态描述
*/
private String text;

/**
* 判断用户状态
*
* @param value 用户状态
* @return 存在与否
*/
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;
}
}
  • 提醒一下,Enum枚举类不支持@Data注解哦
  • 再封装一个 /model/request/TeamAdd,前端人员无需自行筛选填写哪些字段,这里还涉及到了 BeanUtils 的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author 邓哈哈
* 2023/4/20 14:22
* Function: 新增队伍参数
* Version 1.0
*/
@Data
public class TeamAdd {
/**
* 队伍名称
*/
private String name;

/**
* 描述
*/
private String description;

/**
* 最大人数
*/
private Integer maxNum;

/**
* 过期时间
*/
private Date expireTime;

/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;

/**
* 密码
*/
private String password;
}
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 新增队伍
*
* @param team 新增队伍参数
* @param request request
* @return 新增成功与否
*/
@PostMapping("/add")
public BaseResponse<String> teamAdd(@RequestBody TeamAdd team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

String teamAdd = teamService.teamAdd(team, request);
return ResultUtils.success(teamAdd);
}
  • 完成了,新增队伍功能暂时告一段落

阶段性问题

  1. 使用 Swagger 接口文档快速测试,这里访问文档还报错了,在启动类上加了 @EnableSwagger2WebMvc 就解决了
  2. 奶奶的想启动个前端登陆一下还他妈报错启动失败,看了一下是我的node没了(鬼删的?)导致npm环境变量没配好
  3. 看了眼环境变量,真几把乱,我就纳闷了我记得很整洁来着,很快就搞好了
  4. 重新捋了一遍通用返回对象,新增了新的异常对象(ErrorCode errorCode,String description)
    实现思路可以看下通用返回对象.Xmind,具体代码还请跳转至用户中心-开发文档,内容已同步更新
  5. 改了下npm环境变量还手贱删除了nodejs,俺的个人博客都被搞垮啦,之后会总结个人博客部署流程的
  6. team表添加了join_num字段,存储当前队伍已加人数,记得同步修改更新xml、mapper、domain

查询队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 查询队伍(分页查询)
*
* @param team 查询参数
* @return 符合条件的队伍
*/
@Override
public Page<Team> teamList(TeamQuery team, HttpServletRequest request) {
// 登录校验
User loginUser = getLoginUser(request);
if (loginUser != null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

QueryWrapper<Team> tqw = new QueryWrapper<>();

// 1.根据队伍名查询
String name = team.getName();
if (StringUtils.isBlank(name) || name.length() > 20)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍名不符合要求");
tqw.like("name", name);

// 2.根据队伍描述查询
String description = team.getDescription();
if (StringUtils.isBlank(description) || description.length() > 512)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍描述不符合要求");
tqw.like("description", description);

// 3.根据队长id查询
Long userId = team.getUserId();
if (userId == null || userId < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队长id不符合要求");
tqw.eq("user_id", userId);

// 4.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum == null || maxNum < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍最大人数不符合要求");
tqw.eq("max_num", maxNum);

// 5.根据队伍状态查询
Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");
tqw.like("status", status);

// 6.分页查询
Page<Team> teamPage = this.page(new Page<>(1, 5), tqw);
if (teamPage == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

return teamPage;
}
  • 逻辑还是很简单的,前端传param参数,后端接口封装一个队伍查询信息类,再依次对每个字段校验,最后分页查询实现
  • 这里还遇到一个问题,我写成这样:QueryWrapper tqw = new QueryWrapper<>(team) 它的SQL查询语句就默认自带了 where name = ? 导致 twq.like(“name”, name) 失效了,搞了半天才发现
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 查询队伍
* 分页查询
*
* @param team 查询队伍参数
* @return 队伍列表
*/
@GetMapping("/list/page")
public BaseResponse<Page<Team>> teamList(TeamQuery team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Page<Team> teamPage = teamService.teamList(team, request);
return ResultUtils.success(teamPage);
}

修改队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 修改队伍
*
* @param team 队伍修改信息
* @return 修改成功与否
*/
@Override
public String teamUpdate(TeamUpdate team, HttpServletRequest request) {
// 校验是否登录
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 1.校验修改权限(只有管理员或队长可以修改)
if (!loginUser.getId().equals(team.getUserId()) && !isAdmin(loginUser))
throw new BusinessException(ErrorCode.NO_AUTH, "非队长且非管理员");

// 2.判断队伍是否存在
Long id = team.getId();
if (id == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(id);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

// 3.校验队伍名
String name = team.getName();
if (StringUtils.isBlank(name) || name.length() > 20)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍名不符合要求");

// 4.校验队伍描述
String description = team.getDescription();
if (StringUtils.isBlank(description) || description.length() > 512)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍描述不符合要求");

// 5.校验队伍状态
Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");

// 6.校验队伍密码
String password = team.getPassword();
// 6.1.如果队伍非加密, 则不允许设置密码
if (!TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isNotBlank(password))
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍密码不符合要求");
}

// 6.2.如果队伍加密, 一定要有密码, 且密码 <= 32
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍密码不符合要求");
}

// 7.更新队伍信息
Team updateTeam = new Team();
BeanUtils.copyProperties(team, updateTeam);

boolean update = this.updateById(updateTeam);
if (!update)
throw new BusinessException(ErrorCode.PARMS_ERROR, "修改队伍失败");

return "修改信息成功";
}
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 修改队伍
*
* @param team 修改队伍参数
* @param request request
* @return 修改成功游戏
*/
@PostMapping("/update")
public BaseResponse<String> teamUpdate(@RequestBody TeamUpdate team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

String teamUpdate = teamService.teamUpdate(team, request);
return ResultUtils.success(teamUpdate);
}
  • 封装 查询队伍参数 teamQuery
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @author 邓哈哈
* 2023/4/20 10:10
* Function: 查询队伍参数
* Version 1.0
*/

@Data
public class TeamQuery {
/**
* 队伍名称
*/
private String name;

/**
* 队伍描述
*/
private String description;

/**
* 最大人数
*/
private Integer maxNum;

/**
* 队长id
*/
private Long userId;

/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;

}
  • 封装 修改队伍参数 teamUpdate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author 邓哈哈
* 2023/4/26 14:00
* Function: 修改队伍参数
* Version 1.0
*/

@Data
public class TeamUpdate {
/***
* 队伍id
*/
private Long id;

/***
* 队长id
*/
private Long userId;

/**
* 队伍名称
*/
private String name;

/**
* 描述
*/
private String description;

/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;

/**
* 密码
*/
private String password;
}

阶段性问题

  • 注意到我们查询队伍接收param参数,新增队伍和修改队伍接收JSON参数,记得封装的参数类要实现每个成员属性的get、set方法
  • 体会到封装参数接受类request的好处,还是那句话,前后端联调更加方便 –> BeanUtils的用法
  • 优化了相关业务的代码,更加简洁易懂了
  • 这里应该捋一捋我对相关业务流程的理解的,放到明天啦~

业务流程梳理

  • 新增队伍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
用户传参:队伍名、描述、最大人数、状态、密码、过期时间
(队长id指定、队伍id随机生成、初始化成员数量为0、创建时间、修改时间、逻辑删除)
1.校验是否登录,未登录不允许新增
2.队伍名 <= 20
3.描述 <= 512
4.队伍人数 > 1 且 <= 20
5.队伍状态,公开?私有?加密?
6.校验队伍密码
6.1.如果队伍非加密, 则不允许设置密码
6.2.如果队伍加密, 一定要有密码, 且密码 <= 32
7.校验过期时间:当前时间 > 过期时间
8.校验用户已创建队伍数量(最多创建 5 个队伍)
9.更新team表
10.更新user_team表
  • 查询队伍
1
2
3
4
5
6
7
8
9
10
11
12
用户传参:队伍名/描述/最大人数/状态/队长id
用户可以根据这些参数来筛选队伍:队伍名、描述、最大人数、状态、队长id
1.校验是否登录,未登录不允许查询
2.根据队伍名查询(<= 20)
3.根据描述查询(<= 512)
4.根据最大人数查询(<= 20)
5.根据状态查询,公开?私有?加密?
6.根据队长id查询
注:
0 - 公开, 在队伍大厅中可以直接加入
1 - 私有, 在队伍大厅中不可以直接加入
2 - 公开且加密, 加入队伍需要密码
  • 修改队伍
1
2
3
4
5
6
7
8
9
10
11
12
用户传参:队伍id、队长id、队伍名/描述/状态/密码
用户可以修改这些队伍信息:队伍名、描述、状态、密码
修改队伍参数中还会封装队伍id和队长id
1.校验是否登录,未登录不允许修改
2.校验队伍是否存在
3.校验用户是否为队长或管理员,否则不允许修改
4.修改队伍名(<= 20)
5.修改描述(<= 512)
6.修改状态
7.1.如果修改公开或私有,则不允许设置密码
7.2.如果修改为加密,则密码不允许为空(<= 32)
8.执行更新

用户加入队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* 加入队伍
*
* @param team 加入队伍参数
* @param request request
* @return 加入队伍成功
*/
@Transactional
@Override
public String joinTeam(TeamJoin team, HttpServletRequest request) {
// 登录校验
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 1.校验队伍是否存在
Long id = team.getId();
if (id == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(id);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");

// 2.用户不能加入私有队伍
if (TeamStatusEnum.PRIVATE.equals(statusEnum))
throw new BusinessException(ErrorCode.PARMS_ERROR, "不能主动加入私有队伍");

String password = team.getPassword();

// 校验密码
// 3.1.用户加入加密队伍必须输入密码
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入加密队伍要提供正确的密码");
}

// 3.2.用户加入公开队伍不需要密码
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isNotBlank(password))
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入公开队伍无需密码");
}

// 4.同一用户最多加入5个队伍
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
Long userId = loginUser.getId();
utqw.eq("user_id", userId);
long count = userTeamService.count(utqw);
if (count >= 5)
throw new BusinessException(ErrorCode.PARMS_ERROR, "该用户加入队伍已达上限");

// 5.不能重复加入已加入的队伍
utqw.eq("team_id", team.getId());
count = userTeamService.count(utqw);
if (count > 0)
throw new BusinessException(ErrorCode.PARMS_ERROR, "您已在该队伍中");

// 6.不能加入满员的队伍
Integer joinNum = team.getJoinNum();
if (joinNum >= team.getMaxNum())
throw new BusinessException(ErrorCode.PARMS_ERROR, "该队伍已满员");

// 7.更新team表队伍成员数量
UpdateWrapper<Team> tuw = new UpdateWrapper<>();
Long teamId = team.getId();
tuw.eq("id", teamId).set("join_user", ++joinNum);
boolean updateTeam = this.update(tuw);

// 8.插入用户-队伍关系到user_team表
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);

boolean saveUserTeam = userTeamService.save(userTeam);

if (!updateTeam || !saveUserTeam)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "用户加入队伍失败");

return "加入队伍成功";
}
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 加入队伍
*
* @param team 加入队伍参数
* @param request request
* @return 加入队伍成功
*/
@PostMapping("/join")
public BaseResponse<String> joinTeam(@RequestBody TeamJoin team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

String joinTeam = teamService.joinTeam(team, request);
return ResultUtils.success(joinTeam);
}
  • 封装 加入队伍参数 teamJoin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* @author 邓哈哈
* 2023/4/27 21:04
* Function: 加入队伍参数
* Version 1.0
*/

@Data
public class TeamJoin {
/**
* 队伍id
*/
private Long id;

/**
* 队长id
*/
private Long userId;

/**
* 最大人数
*/
private Integer maxNum;

/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;

/**
* 密码
*/
private String password;

/**
* 成员数量
*/
private Integer joinNum;
}
  • 还没测试接口呢,写累了,有啥问题明天再看吧~
  • 测试结果完美!写下业务流程吧:
1
2
3
4
5
6
7
8
9
10
11
12
用户传参:队伍id、队伍状态、密码、最大人数、已加入人数
1.登录校验
2.校验队伍是否存在
3.校验状态
4.1.加入加密队伍必须输入密码
4.2.加入公开队伍不需要密码
4.3不能加入私有队伍
5.最多加入5个队伍
6.不能重复加入已加入的队伍
7.不能加入满员的队伍
8.更新team表队伍成员数量
9.插入用户-队伍关系到user_team表

用户退出队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* 退出队伍
*
* @param team 退出队伍参数
* @param request request
* @return 退出队伍成功
*/
@Transactional
@Override
public String quitTeam(TeamQuit team, HttpServletRequest request) {
// 1.校验登录
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.校验队伍是否存在
Long teamId = team.getId();
if (teamId == null || teamId < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(teamId);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

// 3.校验用户状态
Integer userStatus = loginUser.getUserStatus();
if (userStatus == 1)
throw new BusinessException(ErrorCode.NO_AUTH, "该账号已被封");

// 4.校验队伍剩余人数
Integer joinNum = team.getJoinNum();
// 4.1.系统错误
if (joinNum - 1 < 0)
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "队伍人数为空");

// 4.2.队伍人数为空
if (joinNum - 1 == 0) {
boolean remove = this.removeById(teamId);
if (!remove)
throw new BusinessException(ErrorCode.UPDATE_ERROR);
}

// 4.3.队伍人数未空
if (joinNum - 1 > 0) {
// 5.队伍人数-1
UpdateWrapper<Team> tuw = new UpdateWrapper<>();
tuw.eq("id", teamId).set("join_num", --joinNum);
boolean updateNum = this.update(tuw);
if (!updateNum)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

// 6.校验用户为队长(传位)
if (team.getUserId().equals(loginUser.getId())) {
// 6.1.userTeam表查询: 按加入队伍时间升序排序
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
utqw.eq("team_id", teamId).orderByAsc("create_time");
List<UserTeam> userTeamList = userTeamService.list(utqw);

// 6.2.将加入时间第二早的队员指定为队长
UserTeam userTeam = userTeamList.get(1);
Long userId = userTeam.getUserId();
UpdateWrapper<Team> utuw = new UpdateWrapper<>();
utuw.eq("id", teamId).set("user_id", userId);
boolean updateUser = this.update(utuw);
if (!updateUser)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

// 6.3.删除原队长
utqw = new QueryWrapper<>();
utqw.eq("user_id", team.getUserId());
boolean remove = userTeamService.remove(utqw);
if (!remove)
throw new BusinessException(ErrorCode.UPDATE_ERROR);
}
}

// 6.删除队伍
return "退出队伍成功";
}
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 退出队伍
*
* @param team 退出队伍参数
* @param request request
* @return 退出队伍成功
*/
@PostMapping("/quit")
public BaseResponse<String> quitTeam(@RequestBody TeamQuit team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

String joinTeam = teamService.quitTeam(team, request);
return ResultUtils.success(joinTeam);
}

  • 封装teamQuit对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author 邓哈哈
* 2023/4/29 21:36
* Function: 退出队伍参数
* Version 1.0
*/
@Data
public class TeamQuit {
/**
* 队伍id
*/
private Long id;

/**
* 队长id
*/
private Long userId;

/**
* 剩余人数
*/
private Integer joinNum;
}
  • 测试了一个小时,总算没问题了,这块儿逻辑还挺缜密的,之后再来梳理业务逻辑吧
  • 业务流程:
1

思考

  • 测试接口永远是最费时间的,我可算理解这句话了

队长解散队伍

  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 解散队伍
*
* @param team 解散队伍参数
* @param request request
* @return 解散成功与否
*/
@Transactional
@Override
public String deleteTeam(TeamDelete team, HttpServletRequest request) {
// 1.校验登录
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.检验队伍是否存在
Long teamId = team.getId();
if (teamId == null || teamId < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(teamId);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

// 3.校验用户状态
Integer userStatus = loginUser.getUserStatus();
if (userStatus == 1)
throw new BusinessException(ErrorCode.NO_AUTH, "该账号已被封");

// 4.校验权限(是否为队长或管理员)
if (!team.getUserId().equals(loginUser.getId()) && !isAdmin(loginUser))
throw new BusinessException(ErrorCode.NO_AUTH, "非队长且非管理员");

// 5.校验队伍状态
Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");

// 6.校验密码
String password = team.getPassword();
// TODO
// 4.1.解散加密队伍必须输入密码
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入加密队伍要提供正确的密码");
}

// 4.2.解散公开或私有队伍不需要密码
if (TeamStatusEnum.PUBLIC.equals(statusEnum)) {
if (StringUtils.isNotBlank(password))
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入公开队伍无需密码");
}

return "解散队伍成功";
}
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 解散队伍
*
* @param team 解散队伍参数
* @param request request
* @return 解散成功与否
*/
@PostMapping("/delete")
public BaseResponse<String> deleteTeam(@RequestBody TeamDelete team, HttpServletRequest request) {
// controller对参数的校验
if (team == null) {
throw new BusinessException(ErrorCode.PARMS_ERROR);
}

String deleteTeam = teamService.deleteTeam(team, request);
return ResultUtils.success(deleteTeam);
}
  • 封装teamDelete对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @author 邓哈哈
* 2023/4/30 21:54
* Function: 解散队伍参数
* Version 1.0
*/
@Data
public class TeamDelete {
/**
* 队伍id
*/
private Long userId;

/**
* 队长id
*/
private Long id;

/**
* 0 - 公开,1 - 私有,2 - 加密
*/
private Integer status;

/**
* 密码
*/
private String password;
}

  • 启动项目的时候还出现了一条报错:
1
2
3
4
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'teamController' method 
com.memory.usercenter.controller.TeamController#joinTeam(TeamJoin, HttpServletRequest)
to {POST [/team/join]}: There is already 'teamController' bean method
com.memory.usercenter.controller.TeamController#quitTeam(TeamQuit, HttpServletRequest) mapped.
  • 什么问题呢,其实就是SpringMVC访问路径冲突,写了俩POST(“/join”)

获取当前队伍信息

service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 获取当前队伍信息
*
* @param teamId 队伍id
* @return 队伍信息
*/
@Override
public Team getTeam(Long teamId, HttpServletRequest request) {
// 1.校验登录
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.检验队伍是否存在
if (teamId == null || teamId < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(teamId);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

// 3.校验用户状态
Integer userStatus = loginUser.getUserStatus();
if (userStatus == 1)
throw new BusinessException(ErrorCode.NO_AUTH, "该账号已被封");

// 4.查询队伍信息
Team team = this.getById(teamId);
if (team == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

return getSafetyTeam(team);
}

@Override
public List<Team> getJoinedTeam(Long userId, HttpServletRequest request) {
// 1.校验登录
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.获取已加入的队伍
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
utqw.eq("user_id", userId);
List<UserTeam> userTeamList = userTeamService.list(utqw);

List<Team> teamList = new ArrayList<>();

for (UserTeam userTeam : userTeamList) {
Long teamId = userTeam.getTeamId();
Team team = this.getById(teamId);
Team safetyTeam = getSafetyTeam(team);
teamList.add(safetyTeam);
}

return teamList;
}


controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 获取当前队伍信息
*
* @param teamId 队伍id
* @return 队伍信息
*/
@GetMapping("/one")
public BaseResponse<Team> getTeam(Long teamId, HttpServletRequest request) {
// controller对参数的校验
if (teamId == null) {
throw new BusinessException(ErrorCode.PARMS_ERROR);
}

Team team = teamService.getTeam(teamId, request);
return ResultUtils.success(team);
}

前端开发

展示用户已创建队伍

展示用户已加入队伍

  • pages/teamPage.vue
  • 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div id="content" style="padding-bottom: 30px">
<!-- <slot> 队伍 </slot> -->
<van-divider content-position="left">我创建的队伍</van-divider>
<van-card
v-for="team in teamJoined"
:tag="team.status"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="primary">修改</van-button>
<van-button size="mini" type="danger">解散</van-button>
</template>
</van-card>

<van-divider content-position="right">我加入的队伍</van-divider>
<van-card
v-for="team in teamCreated"
:tag="team.status"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="success">进群聊天</van-button>
</template>
</van-card>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
import { requestData, teamType, userType } from "../models/user";
import myAxios from "../plugins/myAxios";
import { getCurrentUser } from "../service/user";

const loginUser = ref();
const teamJoined = ref([]);
const teamCreated = ref([]);

// 钩子函数 => 查询用户已加入的队伍 用户创建的队伍
onMounted(async () => {
// 校验用户登录
const user: requestData = await getCurrentUser();
if (user.data) {
console.log("获取用户信息成功");
loginUser.value = user.data;
} else {
console.log("获取用户信息失败");
}
// 发送请求 - 用户已创建的队伍
const teamList1 = await myAxios
.get("/team/created", {
params: {
userId: loginUser.value.id,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

// 发送请求 - 用户已加入的队伍
const teamList2 = await myAxios
.get("/team/joined", {
params: {
userId: loginUser.value.id,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

// 处理队伍状态
if (teamList1 && teamList2) {
teamList1.forEach(
(team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
},
teamList2.forEach((team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
})
);
}
// 拿到数据
teamJoined.value = teamList1;
teamCreated.value = teamList2;
});
</script>
  • models/team.ts
1
2
3
4
5
6
7
8
9
10
11
12
// 队伍信息
export type teamType = {
id?: number;
userId?: number;
name?: string;
description?: string;
maxNum?: number;
joinNum?: number;
status?: string;
expireTime?: Date;
createTime?: Date;
};
  • 当然别忘记在config/route.ts下添加路由,这一点在之后的开发中也要注意

阶段性问题

  • 首先这一段逻辑很简单:登录用户的id做参数,拿到其已加入的队伍
  • 一如既往地使用 Card 商品组件
  • 我写这一段还是挺轻松的,虽然不熟练,写的慢,但是很轻松快乐哈
  • 需要注意的点有这些:
  • van-card 的属性使用方法:<template #buttom> 等
  • 对了,像咱们这种表单页,还得微调内边距啥的,自己看着用最简单的HTML+CSS做就可以凑合实现
  • 奶奶的,打算趁热打铁开发修改队伍页面的,发现这个表单写起来还挺生疏,就先开发新增队伍页面吧

新增队伍

  • pages/teamAddPage.vue
  • 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<!-- <slot>新增队伍</slot> -->
<div id="content">
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="teamAdd.name"
name="name"
label="队伍名"
placeholder="请输入队伍名"
:rules="[{ message: '请输入队伍名' }]"
/>

<van-field
v-model="teamAdd.description"
rows="4"
name="description"
label="队伍描述"
type="textarea"
placeholder="请输入队伍描述"
/>

<van-field name="maxNum" label="最大人数">
<template #input>
<van-stepper v-model="teamAdd.maxNum" max="20" min="2" />
</template>
</van-field>

<van-field
v-model="teamAdd.expireTime"
is-link
readonly
name="expireTime"
label="过期时间"
placeholder="点击选择时间"
@click="showPicker = true"
/>

<van-popup v-model:show="showPicker" position="bottom">
<van-date-picker @confirm="onConfirm" @cancel="showPicker = false" />
</van-popup>

<van-field name="radio" label="队伍状态">
<template #input>
<van-radio-group v-model="teamAdd.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(teamAdd.status) === 2"
v-model="teamAdd.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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<script setup lang="ts">
import { ref } from "vue";
import myAxios from "../plugins/myAxios";
import { showSuccessToast, showFailToast } from "vant";
import { useRouter } from "vue-router";

// 日期展示
const showPicker = ref(false);
const onConfirm = ({ selectedValues }) => {
teamAdd.value.expireTime = selectedValues.join("/");
showPicker.value = false;
};

const initTeam = {
name: "",
description: "",
maxNum: 2,
expireTime: "",
status: 0,
password: "",
};

const teamAdd = ref({ ...initTeam });

// 提交表单
const onSubmit = async () => {
// 发送请求 - 新增队伍
const res = await myAxios
.post("/team/add", teamAdd.value)
// 响应s
.then(function (response) {
// 返回响应数据(用户列表)
console.log("response.data = " + response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (res) {
showSuccessToast(res.data.data);
} else {
showFailToast("新增队伍失败");
}

const router = useRouter();
router.replace("/team");

console.log("res = " + res);
};
</script>
  • 他奶奶的,这个页面还真不好搞定呢,踩了无数坑,前后拖了3天可算完成了,我简单总结一番:
  • 首先就是把那form表单合适的、好用的拿过来,先别管那些数据交互
  • 然后创建一个对象,成员属性跟表单作数据绑定
  • 对这些表单的属性都必须有所了解,每个属性是干嘛的都得心知肚明
  • 简单的比如label、placeholder、type,计步器的max、min,日期的展示逻辑:showPicker = true
  • 日历的@confirm、@cancel实现逻辑,这些自个儿多写多练就能记住了
  • 别的不多说了,再说几个注意点

阶段性问题

  • 这里的对象要声明为响应式的,即:const teamAdd = ref({ …initTeam });,不然作数据绑定的时候会有问题
  • 提交按钮不用写@click=”onSubmit”了,表单上写过了,否则会连续执行两次提交
  • POST请求这里参数我写错了,teamAdd.value写成teadAdd了,请求里的json数据一直请求不到接口
  • 前端传回的empireTime是yyyy/MM/dd格式的,传到后端还接收不了了,奶奶的测试的时候好端端的
  • 解决方法:修改后端empireTime字段的Date格式就可以:(暂时先这样解决)
1
2
3
4
5
/**
* 过期时间
*/
@JsonFormat(pattern = "yyyy/MM/dd")
private Date expireTime;
  • 这用户加入和创建队伍还显示不正常了,浏览器好多毛病

修改队伍

  • 快速开发
  • 新增teamEditPage, 新增页面路由”team/edit”
  • teamPage下跳转修改页面:
1
2
3
4
5
6
7
8
9
// 跳转到修改队伍页
const toEditTeam = (id: number) => {
router.push({
path: "/team/edit",
query: {
id,
},
});
};
  • 绑定到修改按钮
1
2
3
4
5
6
<van-button 
size="mini"
type="primary"
@click="toEditTeam(team.id)">
修改
</van-button>
  • 这里思路很好,前端跳转页面时传递所需参数(队伍id)
  • 修改队伍页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<template>
<!-- <slot>新增队伍</slot> -->
<div id="content">
<!-- <slot>修改队伍</slot> -->
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="teamUpdate.name"
name="name"
label="队伍名"
placeholder="请输入队伍名"
:rules="[{ message: '请输入队伍名' }]"
/>

<van-field
v-model="teamUpdate.description"
rows="4"
name="description"
label="队伍描述"
type="textarea"
placeholder="请输入队伍描述"
/>

<van-field name="radio" label="队伍状态">
<template #input>
<van-radio-group v-model="checked" 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(checked) === 2"
v-model="teamUpdate.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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script setup lang="ts">
import { onMounted, ref } from "vue";
import myAxios from "../../plugins/myAxios";
import { useRoute } from "vue-router";
import { useRouter } from "vue-router";
import { showSuccessToast, showFailToast } from "vant";

// 队伍状态
const checked = ref("");
// 修改队伍信息
const teamUpdate = ref({});

const route = useRoute();
const teamId = route.query.id;

// 钩子函数, 根据队伍id查询队伍信息
onMounted(async () => {
// console.log("teamId = " + teamId);
const teamData = await myAxios
.get("/team/one", { params: { teamId } })
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log("response.data = " + response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (teamData) {
teamUpdate.value = teamData;
console.log("stasus = " + typeof teamData.status);
checked.value = `${teamData.status}`;

if (teamData.status == 2) {
console.log("psd = " + teamData.password);
}
}
});
</script>
  • 修改页面钩子函数根据teamId查询发送请求get(“/team/one”)查询到队伍信息,将队伍信息绑定表单
  • 提交表单,发送修改请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 提交表单
const onSubmit = async () => {
// 发送请求 - 新增队伍
const update = await myAxios
.post("/team/update", teamUpdate.value)
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log("response.data = " + response.data);
return response;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (update.code == 0) {
showSuccessToast(update.data);
} else {
showFailToast("修改队伍失败");
}

const router = useRouter();
router.push("/team");
};
  • 功能大概实现了,比如队伍状态的实现比较有趣,可以稍微留意一下

阶段性问题

  • 这里解决了之前提到的问题:用户加入和创建队伍还显示不正常,这不是浏览器问题
  • 这是因为我删除了队伍team,却没有删除user_team对应记录,查询teamList时查到的队伍出现null值,前端渲染就出现了错误
  • 还有teamUpdatePage.vue显示不正常,修改了下文件名为teamEditPage.vue就可以显示了,这浏览器真几把无语
  • 整理了下数据库,删除了一些脏数据;整理了下队伍页面,都放进pages/team下了,修改包注意同步修改路由和对其他页面引用

思考

  • 这里又明确了下前端axios请求接收到的的响应值response:首先axios封装了response,我们从后台传回的数据都封装在response.data下,这一点毋庸置疑。全局响应拦截器帮我们做了这一点,所以我们使用myAxios发送请求,响应值就是被封装好的response.data了,即后端封装的BaseResponse对象(code、data、message、description),理清这一点后,之后处理响应值的思路就很清晰了

退出队伍

1
<van-button size="mini" type="danger" @click="quitTeam(team)">退出</van-button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 退出队伍
const quitTeam = async (team: any) => {
showConfirmDialog({
title: "提示",
message: "你确定要退出队伍吗?",
})
.then(async () => {
// on confirm
const quit = await myAxios
.post("/team/quit", {
id: team.id,
userId: team.userId,
joinNum: team.joinNum,
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (quit.data) {
console.log(`${quit.data}`);
} else {
console.log("退出队伍失败");
}
// 刷新页面
location.reload;
})
.catch(() => {
// on cancel
});
};
  • 注意到这里还使用了Dialog弹出框组件,使用逻辑简单了解一下就行

阶段性问题

  • 解散队伍逻辑还有问题
  • 引发新问题:当已创建队伍/已加入队伍为空时,后端不应该抛出业务异常,需优化前后端逻辑

优化后端逻辑

  • getCreatedTeam返回为空,将返回的team置为null
1
2
3
4
// TODO 创建队伍数为空
if (CollectionUtils.isEmpty(teamList)) {
teamList = null;
}
  • getJoinedTeam返回为空,将返回的team置为null
1
2
3
4
// TODO 加入队伍数为空
if (CollectionUtils.isEmpty(userTeamList)) {
return null;
}

优化前端逻辑

  • 前端处理队伍状态逻辑不应该将createdTeam和joinedTeam耦合,分开校验(这里也为解散队伍功能埋下了祸根,之后我们会解决这个问题)
1
2
3
4
5
6
7
8
// 处理已创建队伍状态
if (teamList1) {
teamList1.forEach((team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
});
}
1
2
3
4
5
6
7
8
// 处理已加入队伍状态
if (teamList2) {
teamList2.forEach((team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
});
}
  • 优化队伍展示逻辑,如果createdTeam/joinedTeam为空则展示为空组件(这一点我们在之前也做过)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<van-divider content-position="left">我创建的队伍</van-divider> 
<van-card
v-if="teamCreated"
v-for="team in teamCreated"
:tag="team.status"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="primary" @click="toEditTeam(team.id)">
修改
</van-button>
<van-button size="mini" type="danger" @click="deleteTeam(team)"
>解散</van-button
>
</template>
</van-card>
<!-- 已创建队伍信息展示 -->
<van-empty v-if="!teamCreated" description="创建队伍为空" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<van-divider content-position="right">我加入的队伍</van-divider>
<van-card
v-if="teamJoined"
v-for="team in teamJoined"
:tag="team.status"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="success">进群聊天</van-button>
<van-button size="mini" type="danger" @click="quitTeam(team)"
>退出</van-button
>
</template>
</van-card>

<!-- 无用户信息展示 -->
<van-empty v-if="!teamJoined" description="加入队伍为空" />
  • 这里我才发现,之前把getCreatedTeam和getJoinedTeam的响应值赋值错了,导致俩表单展示相反了,我说怎么看起来那么奇怪
  • 正确的赋值:
1
2
3
// 拿到数据
teamCreated.value = teamList1;
teamJoined.value = teamList2;

阶段性问题

  • 解散队伍逻辑实现不了
  • 就是前面提到的问题,队伍页面展示时team.status被硬修改了(0-“公开”,1-“私密”,2-“加密”),而在解散队伍的逻辑当中,需要获取队伍的status,导致请求参数错误。解决办法就是不要直接修改team的status,另找个变量。
  • 现在这个问题解决了,改动挺多,听我娓娓道来~
  • 这里另找个变量status不好实现,得遍历到所有team,得到的每个team对象中新增变量保存status修改后的状态。(设想这样是挺复杂的,可以实现,但感觉完全没必要)
  • 另一个思路很直接,不变team.status,表单在作展示时可以调用一个函数,按(0-“公开”,1-“私密”,2-“加密”)转换显示
  • 那我们就在service/function\getStatus.ts下实现一个函数,负责转换status
1
2
3
4
5
6
// 转换stasus
export const getStatus = (status: number) => {
if (status === 0) return "公开";
else if (status === 1) return "私密";
else return "加密";
};
  • tag标签下绑定这个函数就行
1
2
3
4
5
6
7
8
<van-card
v-if="teamCreated"
v-for="team in teamCreated"
:tag="getStatus(team.status)"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
  • 那我们之前的处理status就不需要了,直接删除↓
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 处理已创建队伍状态
if (teamList1) {
teamList1.forEach((team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
});
}
// 处理已加入队伍状态
if (teamList2) {
teamList2.forEach((team: teamType) => {
if (team.status == "0") team.status = "公开";
else if (team.status == "1") team.status = "私有";
else team.status = "加密";
});
}
  • 那解散队伍就好操作了:

解散队伍

1
<van-button size="mini" type="danger" @click="deleteTeam(team)">解散</van-button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 解散队伍
const deleteTeam = async (team: any) => {
showConfirmDialog({
title: "提示",
message: "你确定要解散队伍吗?",
})
.then(async () => {
// on confirm
const del = await myAxios
.post("/team/delete", {
id: team.id,
userId: team.userId,
status: team.status,
password: team.password,
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (del.data) {
console.log(`${del.data}`);
} else {
console.log("解散队伍失败");
}
// 刷新页面
location.reload;
})
.catch(() => {
// on cancel
});
};

阶段性问题

  • 这里要留心,由于后端使用@requestBody接收参数,所以退出/解散队伍发送的json字符串一定要和后端对应
1
2
3
4
5
6
7
// 退出队伍
const quit = await myAxios
.post("/team/quit", {
id: team.id,
userId: team.userId,
joinNum: team.joinNum,
})
1
2
3
4
5
6
7
8
// 解散队伍
const del = await myAxios
.post("/team/delete", {
id: team.id,
userId: team.userId,
status: team.status,
password: team.password,
})
  • 还要注意这两个操作的后端逻辑校验,那就是:
  • 退出队伍时,如果只剩最后一人,要解散队伍和队长直接解散队伍,一定要同时清楚team和user_team中的记录,这样不仅影响业务逻辑,还会在脏数据多的时候,前端队伍显示不正常,这问题你找都找不到
  • 哎我真几把服了,还以为teamList1和teamList2没用了,把赋值语句删了。搞得队伍表单连数据都没有,怪不得突然就不显示队伍了
1
2
3
4
// 拿到数据
teamCreated.value = teamList1;
teamJoined.value = teamList2;
console.log("teamJoin => " + teamJoined.value);

思考

  • 疯狂整理了下该文档的目录格式(2023/05/13晚)
  • 每天完成一个小功能真的很快乐
1
2
3
4
5
6
7
8
9
10
11
// 1.删除team记录
boolean teamRemove = this.removeById(teamId);
if (!teamRemove)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

// 2.删除user_team记录
QueryWrapper<UserTeam> uqw = new QueryWrapper<>();
uqw.eq("team_id", teamId);
boolean userTeamRemove = userTeamService.remove(uqw);
if (!userTeamRemove)
throw new BusinessException(ErrorCode.UPDATE_ERROR);
  • 哦我操他奶奶的,做个数据库实验不小心把数据库删了,他妈的。。。(2023/05/12晚)
  • 补充一点: 做测试的时候, 记得加上这样一条:
1
@SpringBootTest(classes = UserCenterApplication.class)
  • 数据基本恢复完成!之前的同步工作做的不够好,建个表不是少tags就是缺profile,小问题
  • 顺便优化了join_num字段,默认为1,新增队伍时已加人数就默认置为1了
  • 当然了,有了数据才解决了上面的解散队伍的问题
  • 浅浅记录一下效果吧:

image-20230520133756831

image-20230520133811546

浅浅记录

  • 想起来了,上次推送blog老是更新不成功,不知道这机子吃错药了还是,现在就没问题了
  • 好几天没有更新内容了,这几天都去做毛概PPT和性交视频剪辑了,基本没精力完善这个(2023/05/18早)
  • 今天就完成搜索队伍功能吧

搜索队伍

  • 今天升到win11系统了,新界面还挺不适应的
  • 先完成一个小功能,给index.vue页面加一个返回顶部的功能,就是用了一个小组件
1
<van-back-top />
  • PPT制作完成,视频剪辑完成,可以安心搞Java了 (2023/05/20黄昏)
  • 快速记录一下开发情况吧:
  • 页面开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<van-form @submit="onSearchTeam">
<van-cell-group inset>
<van-field
v-model="searchItem.name"
name="name"
label="队伍名"
placeholder="请输入队伍名"
:rules="[{ message: '请输入队伍名' }]"
/>

<van-field
v-model="searchItem.description"
rows="4"
name="description"
label="队伍描述"
type="textarea"
placeholder="请输入队伍描述"
/>

<van-field
v-model="searchItem.userId"
name="userId"
label="队长id"
placeholder="请输入队长id"
:rules="[{ message: '请输入队长id' }]"
/>

<van-field name="maxNum" label="最少人数">
<template #input>
<van-stepper v-model="searchItem.maxNum" max="20" min="2" />
</template>
</van-field>

<van-field name="radio" label="队伍状态">
<template #input>
<van-radio-group v-model="searchItem.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-cell-group>

<div style="margin: 16px">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>

<van-divider content-position="left">符合条件的队伍</van-divider>
<van-card
v-if="teamList"
v-for="team in teamList"
:tag="getStatus(team.status)"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="primary" @click=""> 详细信息 </van-button>
</template>
</van-card>
<!-- 无用户信息展示 -->
<van-empty v-if="!teamList" description="加入队伍为空" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<script lang="ts" setup>
import { ref } from "vue";
import myAxios from "../../plugins/myAxios";
import { getStatus } from "../../service/function/getStatus";

const initItem = {
name: "",
description: "",
maxNum: "",
userId: "",
status: "",
};
// 搜索条件
const searchItem = ref({ ...initItem });
// 队伍列表
const teamList = ref([]);
// 表单项
const onSearchTeam = async () => {
const res = await myAxios
.get("/team/list/page", {
params: {
name: searchItem.value.name,
description: searchItem.value.description,
maxNum: searchItem.value.maxNum,
userId: searchItem.value.userId,
status: searchItem.value.status,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 拿取队伍列表
teamList.value = res;
console.log(teamList.value);
};
</script>

  • 后端接口优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 查询队伍(分页查询)
*
* @param team 查询参数
* @return 符合条件的队伍
*/
@Transactional
@Override
public Page<Team> teamList(TeamQuery team, HttpServletRequest request) {
// 登录校验
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

QueryWrapper<Team> tqw = new QueryWrapper<>();

// 1.根据队伍名查询
String name = team.getName();
if (StringUtils.isNotEmpty(name) && name.length() < 20)
tqw.like("name", name);

// 2.根据队伍描述查询
String description = team.getDescription();
if (StringUtils.isNotEmpty(description) && description.length() > 512)
tqw.like("description", description);

// 3.根据队长id查询
Long userId = team.getUserId();
if (userId != null)
tqw.eq("user_id", userId);

// 4.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum >= 3 && maxNum <= 20)
tqw.gt("max_num", maxNum).eq("max_num", maxNum);

// 5.根据队伍状态查询
Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum != null)
tqw.like("status", status);

// 6.分页查询
Page<Team> teamPage = this.page(new Page<>(1, 5), tqw);
if (teamPage == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR);

return teamPage;
}
  • 这里我们记着要默认初始化查询条件均为空吧
1
2
3
4
5
6
7
const initItem = {
name: "",
description: "",
maxNum: "",
userId: "",
status: "",
};
  • 最后注意一下maxNum的查询条件是 队伍的最少人数
  • ok了, 可以根据所选条件来筛选队伍了, 就是样式比较丑

阶段性问题

  • 开发搜索表单页面,就做好两点:搜索条件和搜索结果,把对应数据跟表单绑定好就成
  • 注意到后端处理请求的业务逻辑需要变动,之前写的不好,要求为:有值且值正确,就去查询数据库
  • 拿到响应数据后,注意分页查询返回的队伍列表是封装在data下的records中的
  • 还有一个蛋疼的问题,就是浏览器不自动刷新了,只能重启前端服务才有效果,最后发现是TeamListPage.vue的路由里把Team小写了,服务启动没问题,但修改就有问题了,无语了

思考

  • 好的,队伍功能基本开发完毕,那么接下来的开发内容就是优化代码,开发自动推送,界面开发,追加一些小功能

随机匹配

用户匹配

  • 那我们二话不说,直接拿取人家的算法做成工具类了
  • utils/AlgorithmUtils下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.memory.usercenter.utils;

import java.util.List;
import java.util.Objects;

/**
* @author 邓哈哈
* 2023/5/20 21:31
* Function:
* Version 1.0
*/

public class AlgorithmUtils {

/**
* 编辑距离算法(用于计算最相似的两组标签)
* 原理:<a href="https://blog.csdn.net/DBC_121/article/details/104198838">...</a>
*
* @param tagList1 标签1
* @param tagList2 标签2
* @return 操作数
*/
public static int minDistance(List<String> tagList1, List<String> tagList2) {
int n = tagList1.size();
int m = tagList2.size();

if (n * m == 0) {
return n + m;
}

int[][] d = new int[n + 1][m + 1];
for (int i = 0; i < n + 1; i++) {
d[i][0] = i;
}

for (int j = 0; j < m + 1; j++) {
d[0][j] = j;
}

for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = d[i - 1][j] + 1;
int down = d[i][j - 1] + 1;
int left_down = d[i - 1][j - 1];
if (!Objects.equals(tagList1.get(i - 1), tagList2.get(j - 1))) {
left_down += 1;
}
d[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return d[n][m];
}

/**
* 编辑距离算法(用于计算最相似的两个字符串)
* 原理:<a href="https://blog.csdn.net/DBC_121/article/details/104198838">...</a>
*
* @param word1 字符串1
* @param word2 字符串2
* @return 操作数
*/
public static int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();

if (n * m == 0) {
return n + m;
}

int[][] d = new int[n + 1][m + 1];
for (int i = 0; i < n + 1; i++) {
d[i][0] = i;
}

for (int j = 0; j < m + 1; j++) {
d[0][j] = j;
}

for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = d[i - 1][j] + 1;
int down = d[i][j - 1] + 1;
int left_down = d[i - 1][j - 1];
if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
left_down += 1;
}
d[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return d[n][m];
}
}
  • 就实现了俩方法:匹配俩标签 匹配俩字符串
  • 简单做个测试吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@SpringBootTest(classes = UserCenterApplication.class)
public class AlgorithmUtilsTest {
@Test
void test() {
String str1 = "鱼皮是狗";
String str2 = "鱼皮不是狗";
String str3 = "鱼皮是鱼不是狗";
// String str4 = "鱼皮是猫";
// 1
int score1 = AlgorithmUtils.minDistance(str1, str2);
// 3
int score2 = AlgorithmUtils.minDistance(str1, str3);
System.out.println(score1);
System.out.println(score2);
}

@Test
void testCompareTags() {
List<String> tagList1 = Arrays.asList("Java", "大一", "男");
List<String> tagList2 = Arrays.asList("Java", "大一", "女");
List<String> tagList3 = Arrays.asList("Python", "大二", "女");
// 1
int score1 = AlgorithmUtils.minDistance(tagList1, tagList2);
// 3
int score2 = AlgorithmUtils.minDistance(tagList1, tagList3);
System.out.println(score1);
System.out.println(score2);
}
}
  • 没错,直接照搬鱼皮的哈哈
  • 测试完成! 了解到编辑距离算法的功能就行

匹配用户接口

  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 用户匹配
*
* @param num 推荐/匹配数目
* @param request request 获取登陆用户
* @return 匹配到的用户
*/
@GetMapping("/match")
public BaseResponse<List<User>> matchUsers(Integer num, HttpServletRequest request) {
// controller对参数的校验
if (num == null)
throw new BusinessException(PARMS_ERROR);

List<User> userList = userService.matchUsers(num, request);
return ResultUtils.success(userList);
}
  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 用户匹配
*
* @param num 推荐/匹配数目
* @param request request 获取登陆用户
* @return 匹配到的用户
*/
@Override
public List<User> matchUsers(long num, HttpServletRequest request) {
// 1.获取登录用户标签(json字符串 -> List列表)
User loginUser = getLoginUser(request);
String tags = loginUser.getTags();
Gson gson = new Gson();
List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
}.getType());
System.out.println(tagList);

// 2.遍历所有查询到的用户, 依次进行标签比较, 并存储到容器中
// 2.1.查询到所有用户
List<User> userList = this.list();
// 2.2.使用SortedMap容器
SortedMap<Long, Integer> indexDistanceMap = new TreeMap<>();
for (int i = 0; i < userList.size(); i++) {
// 2.2.1.拿到用户
User user = userList.get(i);
// 2.2.2.拿到其标签
String userTags = user.getTags();
// 2.2.3.无标签用户, 直接过滤
if (StringUtils.isBlank(userTags)) {
continue;
}
// 2.2.4.转换标签(json字符串 -> List列表)
List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
}.getType());
// 2.2.5.进行标签比较(编辑距离算法)
long distance = AlgorithmUtils.minDistance(tagList, userTagList);
// 2.2.6.将比较结果存入SortedMap容器中(存储了用户下标和匹配度, 并按distance升序排列)
indexDistanceMap.put(distance, i);
}

// 3.拿取匹配度前num条数据, 并存储到容器中
// 3.1.使用List容器存储
List<User> userListVo = new ArrayList<>();
int i = 0;
for (Map.Entry<Long, Integer> entry : indexDistanceMap.entrySet()) {
// 3.1.1.存储数量到位, 直接退出
if (i > num) {
break;
}
// 3.1.2.拿取用户, 将用户信息存放进List容器中
User user = userList.get(entry.getValue());
userListVo.add(user);
i++;
}

// 4.返回结果
return userListVo;
}
  • 拿接口文档测试一下

阶段性问题

  • 这里的SortedMap是用来存储用户和匹配度的,且默认按key值升序排序,不允许重复key值,实现起来有点问题,改天看完鱼皮再试一试,先记着 (2023/05/20晚)
  • 实现随机匹配的逻辑, 我们要优化两个点: 执行时长为多少? 是否能正确匹配?

  • 首先优化执行时长, 有以下几点可以优化:

1
2
3
4
5
6
7
8
9
10
-- 必做
1.数据量过大时, 不要循环输出日志
2.细节: 匹配结果剔除自己
3.标签为空, 不查
4.只查需要的字段(即投影, 如只查id和tags)

-- 选做
1.维护一个固定长度的有序集合, 以时间换空间
2.根据部分标签筛选(前提是能区分出那些标签比较重要)
3.提前查, 搞缓存
  • 然后就是优化能否正确匹配了, 很显然, 昨晚的那个SortedMap并不能完成这个功能, 我就说嘛

  • SortedMap默认按key值升序排列, 把distance做value是不行的, 把distance做key值也不行, 因为SortedMap不允许重复key, 那样的话相同distance就被顶替了.

  • 比较可行的方法是把用户和相应相似度存放进List中, 再按distance升序排序, 最后查出前num条

  • 代码实现的话就明天写吧 (2023/05/21晚)

  • 他妈的连抄带粘写完了, 先不详细解释了, 必做的优化点做到位了, 剩下的逻辑看着真恶心, 放下边慢慢体会吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 用户匹配
*
* @param num 推荐/匹配数目
* @param request request 获取登陆用户
* @return 匹配到的用户
*/
@Override
public List<User> matchUsers(long num, HttpServletRequest request) {
// 1.获取登录用户标签(json字符串 -> List列表)
User loginUser = getLoginUser(request);
String tags = loginUser.getTags();
Gson gson = new Gson();
List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
}.getType());

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("tags");
queryWrapper.select("id", "tags");
// 2.遍历所有查询到的用户, 依次进行标签比较, 并存储到容器中
// 2.1.查询到所有用户
List<User> userList = this.list();
// 2.2.使用SortedMap容器
List<Pair<User, Long>> userDistanceList = new ArrayList<>();
for (User user : userList) {
// 2.2.1.拿到用户
// 2.2.2.拿到其标签
String userTags = user.getTags();
// 2.2.3.无标签用户, 直接过滤, 匹配结果剔除自己
if (StringUtils.isBlank(userTags) || user.getId().equals(loginUser.getId())) {
continue;
}
// 2.2.4.转换标签(json字符串 -> List列表)
List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
}.getType());
// 2.2.5.进行标签比较(编辑距离算法)
long distance = AlgorithmUtils.minDistance(tagList, userTagList);
// 2.2.6.将比较结果存入SortedMap容器中(存储了用户下标和匹配度, 并按distance升序排列)
userDistanceList.add(new Pair<>(user, distance));
}

// 3.按编辑距离由小到大排序
List<Pair<User, Long>> sortedUserDistanceList = userDistanceList.stream()
.sorted((a, b) -> (int) (a.getB() - b.getB()))
.limit(num)
.collect(Collectors.toList());

// 4.有顺序的userID列表
List<Long> userIdList = sortedUserDistanceList.stream().map(pair -> pair.getA().getId()).collect(Collectors.toList());

// 5.根据id查询user完整信息
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.in("id", userIdList);
Map<Long, List<User>> userIdUserListMap = this.list(userQueryWrapper).stream()
.map(this::getSafetyUser)
.collect(Collectors.groupingBy(User::getId));

// 6.因为上面查询打乱了顺序,这里根据上面有序的userId列表赋值
List<User> finalUserList = new ArrayList<>();
for (Long userId : userIdList) {
finalUserList.add(userIdUserListMap.get(userId).get(0));
}

// 7.返回匹配用户列表
return finalUserList;
}

匹配用户页面

  • 之前写过的分页查询所有用户就先保留了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
<template>
<!-- 寄语 -->
<van-notice-bar
left-icon="volume-o"
text="盛年不重来,一日难再晨。及时当勉励,岁月不待人。"
/>
<!-- 首页轮播图 -->
<van-swipe :autoplay="3000" lazy-render :width="480" :height="300">
<van-swipe-item v-for="image in images" :key="image">
<img :src="image" />
</van-swipe-item>
</van-swipe>

<!-- 匹配用户表单 -->
<div id="recommend" style="padding-bottom: 53px">
<van-divider
:style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"
>
每周推荐用户
</van-divider>

<!-- 匹配用户信息页 -->
<van-card
v-for="user in matchUserList"
:tag="`${user.gender}`"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
:thumb="`${user.avatarUrl}`"
>
<!-- 标签展示 -->
<template #tags>
<van-tag
plain
type="primary"
v-for="tag in user.tags"
style="margin-right: 3px; margin-top: 3px"
>
{{ tag }}
</van-tag>
</template>
<template #footer>
<van-button size="mini">联系俺</van-button>
</template>
</van-card>
<!-- 无用户信息展示 -->
<van-empty v-if="!matchUserList" description="获取用户信息失败" />
</div>

<!-- 用户表单 -->
<div id="index" style="padding-bottom: 53px">
<van-divider
:style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"
>
主题
</van-divider>

<!-- 用户信息页 -->
<van-card
v-for="user in userList"
:tag="`${user.gender}`"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
:thumb="`${user.avatarUrl}`"
>
<!-- 标签展示 -->
<template #tags>
<van-tag
plain
type="primary"
v-for="tag in user.tags"
style="margin-right: 3px; margin-top: 3px"
>
{{ tag }}
</van-tag>
</template>
<template #footer>
<van-button size="mini">联系俺</van-button>
</template>
</van-card>
<!-- 无用户信息展示 -->
<van-empty v-if="!userList" description="获取用户信息失败" />
<!-- 分页插件 -->
<van-pagination
v-if="userList"
v-model="currentPage"
:total-items="total"
:items-per-page="pageSize"
force-ellipses
@change="change"
/>
</div>
<!-- 返回顶部 -->
<van-back-top />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
import { userType } from "../models/user";
import myAxios from "../plugins/myAxios";

// 轮播图片
const images = [
"https://gitee.com/deng-2022/pictures/raw/master/images/sunset.jpg",
"https://gitee.com/deng-2022/pictures/raw/master/images/typora-icon.png",
];
// 匹配用户列表
const matchUserList = ref([]);
// 用户列表
const userList = ref([]);
// 当前页码
const currentPage = ref(1);
// 每页显示数
let pageSize = 10;
// 总记录数
let total: number = 0;
//
const matchUsers = async () => {
// 发送请求, 获取用户数据列表
const userListData = await myAxios
.get("/user/match", {
params: {
num: 3,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
total = response.data.total;
pageSize = response.data.size;
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功拿到用户数据列表(不为空)
if (userListData) {
// 遍历用户数据列表
userListData.forEach((user: userType) => {
// 将gender的 "1"/"0" 渲染成 "男"/"女"
if (user.gender === "1") user.gender = "男";
if (user.gender === "0") user.gender = "女";
// JSON字符串序列化为列表
if (user.tags) user.tags = JSON.parse(user.tags);
});
// 处理过后的用户列表
matchUserList.value = userListData;
}
};

const getPage = async (currentPage: number) => {
// 发送请求, 获取用户数据列表
const userListData = await myAxios
.get("/user/recommend", {
params: {
currentPage: currentPage,
pageSize: pageSize,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
total = response.data.total;
pageSize = response.data.size;
return response.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功拿到用户数据列表(不为空)
if (userListData) {
// 遍历用户数据列表
userListData.forEach((user: userType) => {
// 将gender的 "1"/"0" 渲染成 "男"/"女"
if (user.gender === "1") user.gender = "男";
if (user.gender === "0") user.gender = "女";
// JSON字符串序列化为列表
if (user.tags) user.tags = JSON.parse(user.tags);
});
// 处理过后的用户列表
userList.value = userListData;
}
};

// 用户列表, 钩子函数
onMounted(() => {
// 匹配用户
matchUsers();
// 主页
getPage(currentPage.value);
});

// 改变页码
const change = () => {
console.log(currentPage.value);
console.log(userList.value.length);
getPage(currentPage.value);
};
</script>

  • 加了俩组件 寄语和轮播图
  • 然后我就想用自己的图片来做这个轮播图
  • 那我搭建一个自己的图床呗 拿个图片老方便了 学一学怎么搭建图床 说干就干

搭建图床

  • 搭建过程就不多说了 收藏了好几个CSDN博客教程
  • 最重要的是在PicGo里下一个插件 搭建一个Gitee图床 (不用GitHub图床是因为这玩意儿BUG太多了 尤其是网络原因)
  • 两个图床的配置都放下面了 我用了Gitee图床

image-20230523223215190

image-20230523223310839

  • 上传到图床的图片可以随意使用了
  • 在Typora -> 偏好设置 -> 图像 里配置好上传服务和PicGo路径后 Typora里使用到的图片都会自动上传到图床上去
  • 最后展示一下我刚上传到图床里的图片吧

  • 操他妈的这张图片怎么就显示不出来呢

  • 操他妈的上个厕所就能显示了
  • 总之 图床可以正常使用了 现在把浏览器收藏夹整理一下先 (2023/05/23晚)
  • 今天距伙伴匹配系统从零开发 整整俩个月了 简单纪念一下 但是今天不想编码了 干点儿别的吧 (2023/05/24晚)

持续优化

  • 之后的优化就统一写这里吧(新功能实现除外)

随机匹配优化1.0

  • 增加了个可以开关的按钮
1
2
3
4
5
<van-field name="switch" label="开启每周推荐">
<template #input>
<van-switch v-model="checked" @click="matchUsers" />
</template>
</van-field>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 开启/关闭每周推荐
const checked = ref(false);
// 匹配用户列表
const matchUserList = ref([]);
// 每周推荐功能
const matchUsers = async () => {
console.log("checked = " + checked.value);
// 每周推荐是否开启
if (checked.value) {
// 发送请求, 获取用户数据列表
const userListData = await myAxios
.get("/user/match", {
params: {
num: 3,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
total = response.data.total;
pageSize = response.data.size;
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功拿到用户数据列表(不为空)
if (userListData) {
// 遍历用户数据列表
userListData.forEach((user: userType) => {
// 将gender的 "1"/"0" 渲染成 "男"/"女"
if (user.gender === "1") user.gender = "男";
if (user.gender === "0") user.gender = "女";
// JSON字符串序列化为列表
if (user.tags) user.tags = JSON.parse(user.tags);
});
// 处理过后的用户列表
matchUserList.value = userListData;
}
} else {
matchUserList.value = [];
}
};
  • 这里有个BUG,想实现关闭按钮后显示无用户页面无果(待解决)

页面标题优化

  • 还记得咱的页面标题写哪吗?嘿嘿哈哈
  • layouts/BasicLayout.vue 下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<!-- 导航栏 -->
<van-nav-bar
title="标题"
fixed
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>
  • 就在这页增加些逻辑吧,留心下routes,routerAPI的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 默认页面标题
const DEFAULT_TITLE = "伙伴匹配";
// 页面标题
const title = ref(DEFAULT_TITLE);

// 跳转页面前校验将要跳转的路由(目标路由)
router.beforeEach((to) => {
// 拿取目标路由的path
const toPath = to.path;
// 逐个比对自定义路由中的path
const route = routes.find((route) => {
// 匹配到该路由的path
return toPath == route.path;
});
// 设置title为匹配路由的title
title.value = route?.title ?? DEFAULT_TITLE;
});

// 左”/“按钮, 返回上个页面
const onClickLeft = () => {
router.back();
};
// 跳转至搜索页
const onClickRight = () => {
router.push("/search");
};

image-20230526233615488

image-20230526233636359

  • 效果很明显嘛,完成!

用户标签优化

  • 记得队伍卡片上左上角的状态显示吧,我们优化一下用户卡片上的性别显示
  • service/function/getUserGender.ts下:
1
2
3
4
5
6
// 转换stasus
export const getUserGender = (gender: string) => {
if (gender === "0") return "女";
else if (gender === "1") return "男";
else return "人妖";
};
  • 然后在用户列表页和匹配用户页调用一下就行
1
2
3
4
5
6
7
<van-card
v-for="user in userList"
:tag="getUserGender(user.gender)"
:title="`${user.userAccount} ${user.username} ${user.planetCode}`"
:desc="user.profile"
:thumb="`${user.avatarUrl}`"
>

阶段性问题

  • 这里把getTeamStatus.ts也优化了下,结果给网页整空白了,卡了半天发现TeamListPage和TeamPage都调用了这个方法而前者我忘记改过来了,要注意一下
  • 第一遍做的时候取了user的status,才反应过来user的是gender,又全改了一遍
  • 然后发现数据库中的gender是varchar型,而status是int型,真几把服,谁设计的表,害我前端数据类型不对应,逻辑判断都有问题
  • 今天就干到这吧(2023/05/26晚)

加入队伍

  • 他爷爷的,我把这功能忘了
  • 之前后端接口已经写好了:
  • controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 加入队伍
*
* @param team 加入队伍参数
* @param request request
* @return 加入队伍成功
*/
@PostMapping("/join")
public BaseResponse<String> joinTeam(@RequestBody TeamJoin team, HttpServletRequest request) {
// controller对参数的校验
if (team == null)
throw new BusinessException(ErrorCode.PARMS_ERROR);

String joinTeam = teamService.joinTeam(team, request);
return ResultUtils.success(joinTeam);
}
  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 加入队伍
*
* @param team 加入队伍参数
* @param request request
* @return 加入队伍成功
*/
@Transactional
@Override
public String joinTeam(TeamJoin team, HttpServletRequest request) {
// 1.登录校验
User loginUser = getLoginUser(request);
if (loginUser == null)
throw new BusinessException(ErrorCode.NOT_LOGIN);

// 2.校验队伍是否存在
Long teamId = team.getId();
if (teamId == null || teamId < 0)
throw new BusinessException(ErrorCode.PARMS_ERROR);

Team currentTeam = this.getById(teamId);
if (currentTeam == null)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "该队伍不存在");

// 3.校验状态
Integer status = team.getStatus();
TeamStatusEnum statusEnum = TeamStatusEnum.getEnumByValue(status);
if (statusEnum == null)
throw new BusinessException(ErrorCode.PARMS_ERROR, "队伍状态不符合要求");

String password = team.getPassword();
// 4.1.加入加密队伍必须输入密码
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入加密队伍要提供正确的密码");
}

// 4.2.加入公开队伍不需要密码用户加入公开队伍不需要密码
if (TeamStatusEnum.PUBLIC.equals(statusEnum)) {
if (StringUtils.isNotBlank(password))
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入公开队伍无需密码");
}

// 4.3.不能加入私有队伍
if (TeamStatusEnum.PRIVATE.equals(statusEnum))
throw new BusinessException(ErrorCode.PARMS_ERROR, "不能主动加入私有队伍");

// 5.最多加入5个队伍
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
Long userId = loginUser.getId();
utqw.eq("user_id", userId);
long count = userTeamService.count(utqw);
if (count >= 5)
throw new BusinessException(ErrorCode.PARMS_ERROR, "该用户加入队伍已达上限");

// 6.不能重复加入已加入的队伍
utqw.eq("team_id", team.getId());
count = userTeamService.count(utqw);
if (count > 0)
throw new BusinessException(ErrorCode.PARMS_ERROR, "您已在该队伍中");

// 7.不能加入满员的队伍
Integer joinNum = team.getJoinNum();
if (joinNum >= team.getMaxNum())
throw new BusinessException(ErrorCode.PARMS_ERROR, "该队伍已满员");

// 8.更新team表队伍成员数量
UpdateWrapper<Team> tuw = new UpdateWrapper<>();
tuw.eq("id", teamId).set("join_num", ++joinNum);
boolean updateTeam = this.update(tuw);

// 9.插入用户-队伍关系到user_team表
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());

boolean saveUserTeam = userTeamService.save(userTeam);

if (!updateTeam || !saveUserTeam)
throw new BusinessException(ErrorCode.UPDATE_ERROR, "用户加入队伍失败");

return "加入队伍成功";
}
  • 快速开发前端页面:
  • 挂个按钮,再携带参数发送个toJoinTeam请求
1
2
3
<van-button size="mini" type="success" @click="toJoinTeam(team)">
申请加入
</van-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 申请加入队伍
const toJoinTeam = async (team: any) => {
// 发送请求, 获取用户数据列表
const joinTeam = await myAxios
.post("/team/join", {
id: team.id,
userId: team.userId,
maxNum: team.maxNum,
status: team.status,
password: team.password,
joinNum: team.joinNum,
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log("加入队伍: = " + response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功加入队伍
if (joinTeam) {
showSuccessToast(`joinTeam`);
console.log(joinTeam);
}
};
  • 那我们在搜索页面里找个队伍,申请加入吧

搜索队伍优化

  • 这边发现按照队伍最大人数来筛选队伍时,后端逻辑校验有问题:本意是查询>=最小人数的队伍,但写成了:
1
2
3
4
// 4.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum >= 3 && maxNum <= 20)
tqw.gt("max_num", maxNum).eq("max_num", maxNum);
  • 显然有问题的,查询逻辑加个or就行了:
1
2
3
4
// 4.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum >= 3 && maxNum <= 20)
tqw.gt("max_num", maxNum).or().eq("max_num", maxNum);

退出队伍优化

  • 点击申请加入,可以正常加入公开队伍了,但是这边发现退出队伍时,没有正常删除user_team记录,这里捋一下退出队伍的逻辑吧:
1
2
退出队伍 -> 校验队伍剩余人数 -> 退出后为空?-> 删除user_team记录,删除team记录
-> 仅删除user_team记录 -> 是否为队长?-> 是,传位队长给队员
  • 在判断退出后不为空后没有删除user_team记录,导致调试时出现问题了
  • 优化完成

搜索队伍优化2.0

  • 我们要在搜索页面根据公开/加密分页展示两类队伍,再从中筛选所需队伍
  • 首先我们使用 Tab标签页 组件,把队伍列表表单分为公开和加密两页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<van-divider content-position="left">符合条件的队伍</van-divider>

<!-- 队伍列表分页展示 -->
<van-tabs v-model:active="active">
<!-- 公开队伍列表 -->
<van-tab title="公开" style="padding-bottom: 57px">
<van-card
v-if="pubTeamList"
v-for="team in pubTeamList"
:tag="getTeamStatus(team.status)"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="success" @click="toJoinTeam(team)">
申请加入
</van-button>
<van-button size="mini" type="primary" @click="">
详细信息
</van-button>
</template>
</van-card>
<!-- 无队伍信息展示 -->
<van-empty v-if="!pubTeamList" description="公开队伍为空" />
</van-tab>

<!-- 加密队伍列表 -->
<van-tab title="加密" style="padding-bottom: 57px">
<van-card
v-if="safeTeamList"
v-for="team in safeTeamList"
:tag="getTeamStatus(team.status)"
:title="team.name"
:desc="team.description"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
>
<template #bottom>
<div>队伍人数: {{ team.joinNum }}/ {{ team.maxNum }}</div>
<div>创建时间: {{ team.createTime }}</div>
<div>解散时间: {{ team.expireTime }}</div>
</template>

<template #footer>
<van-button size="mini" type="success" @click="toJoinTeam(team)">
申请加入
</van-button>
<van-button size="mini" type="primary" @click="">
详细信息
</van-button>
</template>
</van-card>
<!-- 无队伍信息展示 -->
<van-empty v-if="!safeTeamList" description="加密队伍为空" />
</van-tab>
</van-tabs>
1
2
// 标签页-激活
const active = ref(0);
  • 进入搜索页就按队伍状态 status:0 和 status:2 发两次请求,并将响应绑定到对应表单上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 公开队伍列表
const pubTeamList = ref([]);
// 加密队伍列表
const safeTeamList = ref([]);
// 钩子函数 - 加载所有队伍信息
onMounted(async () => {
console.log("active = " + active.value);
// 公开队伍列表
const teamListData1 = await myAxios
.get("/team/list/page", {
params: {
name: searchItem.value.name,
description: searchItem.value.description,
maxNum: searchItem.value.maxNum,
userId: searchItem.value.userId,
status: 0,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

// 加密队伍列表
const teamListData2 = await myAxios
.get("/team/list/page", {
params: {
name: searchItem.value.name,
description: searchItem.value.description,
maxNum: searchItem.value.maxNum,
userId: searchItem.value.userId,
status: 2,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

// 拿取公开队伍列表
pubTeamList.value = teamListData1;
console.log(pubTeamList.value);
// 拿取加密队伍列表
safeTeamList.value = teamListData2;
console.log(safeTeamList.value);
});
  • 当然,我们可以再次使用onSearchTeam来根据搜索表单筛选队伍,分两种情况:
  • 公开页激活(active.value === 0)和加密页激活(active.value === 1)
  • 根据页面激活情况发送相应请求即可,并将结果绑定到对应表单上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 搜索队伍
const onSearchTeam = async () => {
let teamListData;
if (active.value === 0) {
teamListData = await myAxios
.get("/team/list/page", {
params: {
name: searchItem.value.name,
description: searchItem.value.description,
maxNum: searchItem.value.maxNum,
userId: searchItem.value.userId,
status: 0,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 拿取公开队伍列表
pubTeamList.value = teamListData;
console.log(pubTeamList.value);
} else {
teamListData = await myAxios
.get("/team/list/page", {
params: {
name: searchItem.value.name,
description: searchItem.value.description,
maxNum: searchItem.value.maxNum,
userId: searchItem.value.userId,
status: 2,
},
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data.records;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 拿取队伍列表
safeTeamList.value = teamListData;
console.log(safeTeamList.value);
}

// 拿取队伍列表
safeTeamList.value = teamListData;
console.log(safeTeamList.value);
};
  • 编写完成!效果如下:

image-20230529233029591s

image-20230529233050906

  • 这里我们发现搜索队伍后端逻辑校验有问题:
1
2
3
4
// 5.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum >= 2 && maxNum <= 20)
tqw.gt("max_num", maxNum).or().eq("max_num", maxNum);
  • 我们之前是这样实现的,本意是想筛选出>=maxNum的队伍,但MP的每个QueryWrapper后紧跟条件时,会自动使用and来拼接(懂吧,就是SQL中的where语句下的and拼接条件)。当最大人数筛选条件正确填写时,执行情况为:把.or()前的所有条件和.or()后的.eq(“max_num”, maxNum)条件用or连接去查询,结果显然不对,查到了多于预期的数据。
  • 那为了实现查询>=maxNum的队伍,我们舍弃了.eq,把maxNum + 1就行了,逻辑上没有问题了
1
2
3
4
// 5.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum >= 2 && maxNum <= 20)
tqw.gt("max_num", maxNum - 1);
  • 今天就做到这里,明天继续!(2023/05/29晚)

搜索队伍优化3.0

  • 搜索到的队伍不包含用户已加入的队伍(当然如果重复加入队伍,我们也是有逻辑校验的)
  • 添加后端逻辑即可,代码如下:
1
2
3
4
5
6
7
8
// 6.排除当前用户已加入的队伍
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
// 6.1.查询当前用户已加入的队伍信息(为提高性能, 仅拿取需要的team_id字段即可)
utqw.select("team_id").eq("user_id", loginUser.getId());
List<UserTeam> userTeamList = userTeamService.list(utqw);
// 6.2.队伍列表排除掉用户已加入的队伍
List<Long> teamIdList = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toList());
tqw.notIn("id", teamIdList);
  • 现在大厅中就bu显示用户已加入的队伍了,美滋滋~(2023/05/31下午)

加入队伍优化

  • 他妈的好多东西,明天再做吧嘿嘿嘿
  • 实现申请加入加密队伍时,需要填写密码
  • 这个功能基本实现了,但发现目前后端仅校验password的格式,并没有校验其正确与否,待明日优化完毕这个逻辑后再作记录吧(2023/05/31晚)
1
2
3
4
5
6
7
8
9
const joinTeam = await myAxios
.post("/team/join", {
id: team.id,
userId: team.userId,
maxNum: team.maxNum,
password: team.password,
joinNum: team.joinNum,
status: team.status,
})
  • 我们边分析逻辑,边作相应的代码展示:
  • 在加密队伍列表下,点击申请加入后,首先弹出对话框,并要求填写队伍密码
1
2
3
4
5
6
7
8
<template #footer>
<van-button size="mini" type="success" @click="preJoinTeam(team)">
申请加入
</van-button>
<van-button size="mini" type="primary" @click="">
详细信息
</van-button>
</template>
1
2
3
4
5
6
7
8
9
10
<!-- 密码对话框 -->
<van-dialog
v-model:show="show"
title="提示"
show-cancel-button
@confirm="toJoinTeam(joinTeamParam, password)"
@cancel="cancelJoin"
>
<van-field v-model="password" placeholder="请输入密码"> </van-field>
</van-dialog>
1
2
3
4
5
6
// 加入队伍参数
const joinTeamParam = ref({});
// 队伍密码
const password = ref("");
// 密码对话框
const show = ref(false);
1
2
3
4
5
// 弹出对话框 封装队伍信息
const preJoinTeam = async (team: any) => {
show.value = true;
joinTeamParam.value = team;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 确定 申请加入队伍
const toJoinTeam = async (team: any, password: any) => {
// on confirm
// 发送请求, 获取用户数据列表
const joinTeam = await myAxios
.post("/team/join", {
id: team.id,
userId: team.userId,
maxNum: team.maxNum,
joinNum: team.joinNum,
status: team.status,
password: password,
})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log("加入队伍: = " + response.data);
return response.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});
// 成功加入队伍
if (joinTeam) {
showSuccessToast(`joinTeam`);
console.log(joinTeam);
}
};
1
2
3
4
5
// 取消 清空密码槽 关闭对话框
const cancelJoin = async () => {
password.value = "";
show.value = false;
};
  • 前端就是这样,这边优化一下后端的密码校验
1
2
3
4
5
6
7
8
9
10
11
12
13
// 4.加入加密队伍必须输入密码且必须是正确的密码
if (TeamStatusEnum.SECRET.equals(statusEnum)) {
// 4.1.查询所加队伍的password, 并判断输入的密码是否正确
QueryWrapper<Team> tqw = new QueryWrapper<>();
tqw.select("password");
String password = team.getPassword();

if (StringUtils.isBlank(password) || password.length() > 32)
throw new BusinessException(ErrorCode.PARMS_ERROR, "加入加密队伍要提供正确的密码");

if (!password.equals(this.getById(teamId).getPassword()))
throw new BusinessException(ErrorCode.UPDATE_ERROR, "输入的密码不正确");
}
  • ok,优化完毕,加密队伍必须正确输入密码才能加入了。看一下效果:(2023/06/02)

image-20230602162748112

分页查询队伍优化

  • 拿到加入队伍的所有队员的信息(2023/05/28)

主页优化

  • 之前写匹配用户时,请求/user/match是粘贴请求/user/recommend的,导致把这些代码也粘进来了:
1
2
3
4
5
6
7
8
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
console.log(response.data);
total = response.data.total;
pageSize = response.data.size;
return response.data.records;
})
  • 怪不得每次开启匹配用户后,底边的分页条出问题了
  • 完美解决!

阶段性问题

  • 前两天给GiHub仓库上推送用户中心和伙伴匹配时,由于近期没有按时推送项目到Gitee上,操作的时候不小心把Gitee上的旧代码拉到本地了。最要命的是,还他妈把本地项目给覆盖了,还好一查发现GitHub仓库里的代码是最新的了,差点以为这几天白干了
  • 以后更新项目要及时推送到Gitee和GitHub上(2023/06/08)

定时查询任务优化

  • 给重点用户推荐用户:每天00:00定时缓存预热,并保存24小时(好像没啥卵用,给匹配相似用户加这个功能还不错)
  • 这个定时查询使用了redisson实现分布式锁,完成了多台服务器中只能有一台抢锁成功并缓存预热的功能,目前这个定时任务只是预热了重点用户的查询用户,用处不大,仅学习定时任务和分布式锁,将来优化(2023/06/08)

随机匹配优化2.0

  • 使用redis缓存(保存6 hour),减少查询数据库的次数,减小数据库压力
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 用户匹配
*
* @param num 推荐/匹配数目
* @param request request 获取登陆用户
* @return 匹配到的用户
*/
@Override
public List<User> matchUsers(long num, HttpServletRequest request) {
// 1.获取登录用户标签(json字符串 -> List列表)
User loginUser = getLoginUser(request);

// 拿到当前登录用户的key(每个用户都有各自对应的key)
String redisKey = String.format("memory:user:match:%s", loginUser.getId());
// 查缓存
List<User> userList = (List<User>) redisTemplate.opsForValue().get(redisKey);
// 缓存命中, 则返回用户信息
if (userList != null) {
return userList;
}

// 缓存未命中, 查询数据库
String tags = loginUser.getTags();
Gson gson = new Gson();
List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
}.getType());

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("tags");
queryWrapper.select("id", "tags");
// 2.遍历所有查询到的用户, 依次进行标签比较, 并存储到容器中
// 2.1.查询到所有用户
userList = this.list(queryWrapper);
// 2.2.使用SortedMap容器
List<Pair<User, Long>> userDistanceList = new ArrayList<>();
for (User user : userList) {
// 2.2.1.拿到用户
// 2.2.2.拿到其标签
String userTags = user.getTags();
// 2.2.3.无标签用户, 直接过滤, 匹配结果剔除自己
if (StringUtils.isBlank(userTags) || user.getId().equals(loginUser.getId())) {
continue;
}
// 2.2.4.转换标签(json字符串 -> List列表)
List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
}.getType());
// 2.2.5.进行标签比较(编辑距离算法)
long distance = AlgorithmUtils.minDistance(tagList, userTagList);
// 2.2.6.将比较结果存入SortedMap容器中(存储了用户下标和匹配度, 并按distance升序排列)
userDistanceList.add(new Pair<>(user, distance));
}

// 3.按编辑距离由小到大排序
List<Pair<User, Long>> sortedUserDistanceList = userDistanceList.stream()
.sorted((a, b) -> (int) (a.getB() - b.getB()))
.limit(num)
.collect(Collectors.toList());

// 4.有顺序的userID列表
List<Long> userIdList = sortedUserDistanceList.stream().map(pair -> pair.getA().getId()).collect(Collectors.toList());

// 5.根据id查询user完整信息
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.in("id", userIdList);
Map<Long, List<User>> userIdUserListMap = this.list(userQueryWrapper).stream()
.map(this::getSafetyUser)
.collect(Collectors.groupingBy(User::getId));

// 6.因为上面查询打乱了顺序,这里根据上面有序的userId列表赋值
List<User> finalUserList = new ArrayList<>();
for (Long userId : userIdList) {
finalUserList.add(userIdUserListMap.get(userId).get(0));
}

// 将匹配到的用户信息写到缓存中
try {
redisTemplate.opsForValue().set(redisKey, finalUserList, 6, TimeUnit.HOURS);
} catch (Exception e) {
log.error("redis set key error", e);
}

// 7.返回匹配用户列表
return finalUserList;
}

TODO优化

  • 后端日期传至前端变成一串数字的解决办法
  • 实体属性前添加注解即可:
1
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")

搜索队伍优化4.0

  • 这次优化使用搜索条件搜索的体验
  • 之前那搜索条件直接挂屏幕上边,很是格格不入,我们把它改成弹窗形式的
  • 引入组件 Popup 弹出层
1
2
3
4
5
6
7
8
9
<!-- 搜索条件圆角弹窗(底部) -->
<van-popup
v-model:show="showBottom"
round
position="bottom"
:style="{ height: '50%' }"
>

</van-popup>
  • 最顶部加个控制弹出的按钮
1
2
3
4
5
6
7
8
<!-- 筛选按钮 -->
<van-button
plain
type="success"
style="margin-left: 110px"
@click="showSearchMenu"
>点此筛选所需队伍</van-button
>
1
2
3
4
5
6
// 展示底部搜索条件
const showBottom = ref(false);
// 弹出底部搜索条件弹窗
const showSearchMenu = () => {
showBottom.value = true;
};
  • 再把之前写好的搜索条件表单塞进底部弹出层中(样式、逻辑啥的都不需要改的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- 搜索表单 -->
<van-form @submit="onSearchTeam">
<van-cell-group inset>
<van-field
v-model="searchItem.name"
name="name"
label="队伍名"
placeholder="请输入队伍名"
:rules="[{ message: '请输入队伍名' }]"
/>

<van-field
v-model="searchItem.description"
rows="4"
name="description"
label="队伍描述"
type="textarea"
placeholder="请输入队伍描述"
/>

<van-field
v-model="searchItem.userId"
name="userId"
label="队长id"
placeholder="请输入队长id"
:rules="[{ message: '请输入队长id' }]"
/>

<van-field name="maxNum" label="最少人数">
<template #input>
<van-stepper v-model="searchItem.maxNum" max="20" min="2" />
</template>
</van-field>
</van-cell-group>

<div style="margin: 16px">
<van-button round block type="primary" native-type="submit">
查询队伍
</van-button>
</div>
</van-form>
  • 现在使用条件搜索队伍就方便、轻快多了:

image-20230608203846407

image-20230608203904842

  • 其他小的改动记录:
  • 点击查询队伍,关闭底部弹窗
1
2
// 关闭底部搜索条件弹窗
showBottom.value = false;
  • 后端对 maxNum 的校验不合格:参数值maxNum为空时报系统错误,浅浅优化一下:
1
2
3
4
// 5.根据最大人数查询
Integer maxNum = team.getMaxNum();
if (maxNum != null && maxNum >= 2 && maxNum <= 20)
tqw.gt("max_num", maxNum - 1);
  • 标签页上添加 swipeable 属性,可以实现tab标签页之间的平滑切换
1
2
<!-- 队伍列表分页展示 -->
<van-tabs v-model:active="active" swipeable>
  • 注意底部弹窗组件的 :style=”{ height: ‘50%’ }” 属性,调整其高度至合适位置
  • 分割线样式改成居中且自带好看的样式的了(2023/06/08)

个人信息页优化

  • 用户头像的显示
  • 表单里加个img标签,设置一下宽高,图片路径url即为用户头像路径
1
2
3
4
5
6
7
8
9
<van-cell
title="头像"
is-link
to="/user/edit"
:value="user.avatarUrl"
@click="toEdit('avatarUrl', '头像', user.avatarUrl)"
>
<img :src="url" alt="头像" style="height: 45px" />
</van-cell>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用户头像url
const url = ref("");
// 用户信息
const user = ref();
// 钩子函数
onMounted(async () => {
// 发送获取当前登录用户请求
const res: requestData = await getCurrentUser();

if (res.data) {
showSuccessToast("获取用户信息成功");
// 获取用户信息
user.value = res.data;
// 获取用户头像url
url.value = res.data.avatarUrl;
} else {
showSuccessToast("获取用户信息失败");
}
});

  • Tabbar标签栏优化
1
2
3
4
<!-- 右侧按钮 -->
<template #right>
<van-icon name="setting-o" size="18" />
</template>
1
2
3
4
5
6
7
8
9
<!-- 标签页 -->
<van-tabbar route>
<van-tabbar-item to="/" icon="home-o" name="index">主页</van-tabbar-item>
<van-tabbar-item to="/team" icon="paid" name="team">队伍 </van-tabbar-item>
<van-tabbar-item to="/friend" icon="friends-o" name="friend"
>联系人</van-tabbar-item
>
<van-tabbar-item to="/user" icon="user-o" name="user">个人</van-tabbar-item>
</van-tabbar>
  • icon 属性里就是图标,在组件 icon 下可以选择合适的

退出登录

  • 补充之前一直未实现的功能:退出登录
  • 在UserPage.vue下:
1
2
3
4
5
6
7
8
9
<!-- 退出登录 -->
<van-button
plain
type="danger"
size="large"
style="margin-top: 50px"
@click="logout"
>退出登录</van-button
>
  • 发送退出登录请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 退出登录
const logout = async () => {
const logout = await myAxios
.post("/user/logout", {})
// 响应
.then(function (response) {
// 返回响应数据(用户列表)
return response?.data;
})
// 抛异常
.catch(function (error) {
console.log(error);
});

if (logout) {
showSuccessToast(logout);
console.log(logout);
router.replace("/user/login");
}
};
  • 后端接口开发(这个用户中心写好的,直接用就行了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   /**
* 用户注销
*
* @param request request
* @return 注销成功与否(t / f)
*/
@Override
public String userLogout(HttpServletRequest request) {
User user = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
// 判断对象是否为空
// if (Optional.ofNullable(user).isPresent())
if (user == null)
throw new BusinessException(NULL_ERROR);

// 移除session
return "退出登录成功";
}
  • 退出登录功能完成!效果如下:

image-20230611233522701

验证码登录

  • 基本实现验证码登录功能了,后端校验业务逻辑不够严谨,前端页面也有待改善(2023/06/11晚)

前端开发

  • pages/index下新增CodeLogin.vue页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<van-cell-group inset>
<van-field
v-model="phoneNumber"
name="手机号"
label="手机号"
placeholder="请填写手机号"
:rules="[{ required: true, message: '请填写手机号' }]"
/>

<van-field
v-model="code"
name="验证码"
placeholder="请填写验证码"
:rules="[{ required: true, message: '请填写验证码' }]"
/>

<van-button plain type="primary" @click="getCode">发送验证码</van-button>
</van-cell-group>

<div style="margin: 16px">
<van-button
round
block
type="primary"
native-type="submit"
@click="codeLogin"
>
登录
</van-button>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<script setup lang="ts">
import { ref } from "vue";
import myAxios from "../../plugins/myAxios";
import { useRouter } from "vue-router";
import { showSuccessToast, showFailToast } from "vant";
import { requestData } from "../../models/user";

const router = useRouter();
// 电话号码
const phoneNumber = ref("");
// 填写的验证码
const code = ref("");
// 收到的验证码
const rightCode = ref("");

// 直接获取验证码
const getCode = async () => {
const res: requestData = await myAxios.get("/user/getCode", {
params: {
phoneNumber: phoneNumber.value,
},
});

if (res.code === 0 && res.data) {
//获取验证码
rightCode.value = res.data;
} else {
showFailToast("获取验证码失败");
}
};

// 发送短信验证码

// 验证码登录
const codeLogin = async () => {
const res: requestData = await myAxios.get("/user/codeLogin", {
params: {
phoneNumber: phoneNumber.value,
code: code.value,
rightCode: rightCode.value,
},
});

// 登录成功,跳转至主页
if (res.code === 0 && res.data) {
showSuccessToast("登录成功");
router.replace("/");
} else {
showFailToast("登录失败");
}
};
</script>
  • 添加路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 1. 定义路由组件.
import IndexPage from "../pages/index/IndexPage.vue";
import UserPage from "../pages/user/UserPage.vue";
import SearchPage from "../pages/user/SearchPage.vue";
import UserEditPage from "../pages/user/UserEditPage.vue";
import UserListPage from "../pages/user/UserListPage.vue";
import UserLoginPage from "../pages/user/UserLoginPage.vue";
import TeamPage from "../pages/team/TeamPage.vue";
import TeamEditPage from "../pages/team/TeamEditPage.vue";
import TeamAddPage from "../pages/team/TeamAddPage.vue";
import TeamListPage from "../pages/team/TeamListPage.vue";
import FriendPage from "../pages/friend/FriendPage.vue";
import codeLoginPage from "../pages/index/CodeLoginPage.vue";

// 2. 定义一些路由
// 每个路由都需要映射到一个组件
// 我们后面再讨论嵌套路由
const routes = [
{ path: "/", title: "主页", component: IndexPage }, // 主页
{ path: "/code/login", title: "验证码登录", component: codeLoginPage }, // 验证码登录页

{ path: "/user", title: "个人信息页", component: UserPage }, // 个人页
{ path: "/search", title: "用户搜索页", component: SearchPage }, // 搜索页
{ path: "/user/edit", title: "用户编辑页", component: UserEditPage }, // 用户信息修改页
{ path: "/user/list", title: "用户列表页", component: UserListPage }, // 用户列表页
{ path: "/user/login", title: "登录页", component: UserLoginPage }, // 用户登录页

{ path: "/team", title: "队伍信息页", component: TeamPage }, // 队伍页
{ path: "/team/edit", title: "队伍编辑", component: TeamEditPage }, // 队伍修改页
{ path: "/team/add", title: "队伍新增页", component: TeamAddPage }, // 队伍修改页
{ path: "/team/list", title: "队伍列表页", component: TeamListPage }, // 队伍列表页

{ path: "/friend", title: "用户列表页", component: FriendPage }, // 队伍列表页
];

export default routes;

阶段性问题

  • 妈的,用post请求发送以params请求行发送请求,能打到后端,但接收参数为null,再复习下post/get传参方式

后端开发

  • 实现验证码登录分两步:1、获取验证码 2、验证码登录
  • 先增加两个工具类:1、生成随机验证码 2、发送短信验证码
  • 生成随机验证码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return 验证码
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}

/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
  • 发送短信验证码
1
2
3
4
5
6
7
8
9
10
11
12

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>

<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 短信发送工具类
*/
public class SMSUtils {

/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}
  • 直接获取验证码 controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 直接获取验证码
*
* @param phoneNumber 电话号码
* @return 验证码
*/
@GetMapping("/getCode")
public BaseResponse<String> getCode(String phoneNumber) {
//controller对参数的校验
if (StringUtils.isAnyBlank(phoneNumber))
throw new BusinessException(PARMS_ERROR);

String code = userService.getCode(phoneNumber);
return ResultUtils.success(code);
}
  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 获取验证码
*
* @param phoneNumber 电话号码
* @return 验证码
*/
@Override
public String getCode(String phoneNumber) {
// 1.校验电话号码
String pattern = "1\\d{10}";
if (StringUtils.isBlank(phoneNumber) || !Pattern.matches(pattern, phoneNumber))
throw new BusinessException(PARMS_ERROR, "电话号码有误");
// 2.判断该用户是否已注册
QueryWrapper<User> uqw = new QueryWrapper<>();
uqw.eq("phone", phoneNumber);
User user = getOne(uqw);
// 3.未注册
if (user == null) throw new BusinessException(NOT_REGISTER);
// 4.已注册 随机生成4位验证码并返回
return ValidateCodeUtils.generateValidateCode(4).toString();
}
  • 这里还提供了阿里云短信服务,通过发送短信来接收验证码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 发送短信验证码
*
* @param phoneNumber 手机号
* @param request request
* @return 短信验证码发送成功
*/
@PostMapping("/sendMsg")
public BaseResponse<String> sendMsg(String phoneNumber, HttpServletRequest request) {
// 1.校验手机号
if (StringUtils.isEmpty(phoneNumber))
throw new BusinessException(PARMS_ERROR);
// 2.生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
// 3.调用阿里云提供的短信服务API完成发送短信
SMSUtils.sendMessage("伙伴匹配", "", phoneNumber, code);
// 4.需要将生成的验证码保存到Session
request.getSession().setAttribute(phoneNumber, code);
// 5.短信发送成功
return ResultUtils.success("短信验证码发送成功!");
}
  • 验证码登录 controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 验证码登录
*
* @param phoneNumber 电话号码
* @param code 验证码
* @return 脱敏用户信息
*/
@GetMapping("/codeLogin")
public BaseResponse<User> codeLogin(String phoneNumber, String code, String rightCode, HttpServletRequest request) {
//controller对参数的校验
if (StringUtils.isAnyBlank(phoneNumber, code, rightCode))
throw new BusinessException(PARMS_ERROR);

User user = userService.codeLogin(phoneNumber, code, rightCode, request);
return ResultUtils.success(user);
}
  • service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 验证码登录
*
* @param phoneNumber 电话号码
* @param code 验证码
* @return 脱敏用户信息
*/
@Override
public User codeLogin(String phoneNumber, String code, String rightCode, HttpServletRequest request) {
// 1.校验验证码
if (!code.equals(rightCode)) throw new BusinessException(CODE_ERROR);

// 2.校验电话号码
String pattern = "1\\d{10}";
if (StringUtils.isBlank(phoneNumber) || !Pattern.matches(pattern, phoneNumber))
throw new BusinessException(PARMS_ERROR, "电话号码有误");

// 3.获取用户信息
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("phone", phoneNumber);
User one = this.getOne(qw);
if (one == null) throw new BusinessException(UPDATE_ERROR);

// 4.脱敏用户信息
User safetyUser = getSafetyUser(one);

// 5.记录用户登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);

// 6.返回脱敏用户信息
return safetyUser;
}
  • 验证码登录功能基本实现!效果如下:(2023/06/12午)

image-20230612114804354

注册功能

前端开发

  • pages/UserRegisterPage.vue下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="userAccount"
name="用户名"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>

<van-field
v-model="userPassword"
type="password"
name="密码"
label="密码"
placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>

<van-field
v-model="checkPassword"
type="password"
name="确认密码"
label="确认密码"
placeholder="确认密码"
:rules="[{ required: true, message: '请再次填写密码' }]"
/>

<van-field
v-model="planetCode"
name="星球编号"
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>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script setup lang="ts">
import { ref } from "vue";
import myAxios from "../../plugins/myAxios";
import { useRouter } from "vue-router";
import { showSuccessToast, showFailToast } from "vant";
import { requestData } from "../../models/user";

const router = useRouter();
// 用户名
const userAccount = ref("");
// 密码
const userPassword = ref("");
// 确认密码
const checkPassword = ref("");
// 星球编号
const planetCode = ref("");

const onSubmit = async () => {
const res: requestData = await myAxios.post("/user/register", {
userAccount: userAccount.value,
userPassword: userPassword.value,
checkPassword: checkPassword.value,
planetCode: planetCode.value,
});

if (res.code === 0 && res.data) {
showSuccessToast("注册成功");
router.replace("/user/login");
} else {
showFailToast("注册失败");
}
};

// 跳转至验证码登录页
const toCodeLogin = () => {
router.push("/code/login");
};
</script>
  • config/route.ts 下添加路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 1. 定义路由组件.
import IndexPage from "../pages/index/IndexPage.vue";
import UserPage from "../pages/user/UserPage.vue";
import SearchPage from "../pages/user/SearchPage.vue";
import UserEditPage from "../pages/user/UserEditPage.vue";
import UserListPage from "../pages/user/UserListPage.vue";
import UserLoginPage from "../pages/user/UserLoginPage.vue";
import TeamPage from "../pages/team/TeamPage.vue";
import TeamEditPage from "../pages/team/TeamEditPage.vue";
import TeamAddPage from "../pages/team/TeamAddPage.vue";
import TeamListPage from "../pages/team/TeamListPage.vue";
import FriendPage from "../pages/friend/FriendPage.vue";
import CodeLoginPage from "../pages/index/CodeLoginPage.vue";
import UserRegisterPage from "../pages/index/UserRegisterPage.vue";

// 2. 定义一些路由
// 每个路由都需要映射到一个组件
// 我们后面再讨论嵌套路由
const routes = [
{ path: "/", title: "主页", component: IndexPage }, // 主页
{ path: "/code/login", title: "验证码登录", component: CodeLoginPage }, // 验证码登录页
{ path: "/user/register", title: "新用户注册", component: UserRegisterPage }, // 验证码登录页

{ path: "/user", title: "个人信息页", component: UserPage }, // 个人页
{ path: "/search", title: "用户搜索页", component: SearchPage }, // 搜索页
{ path: "/user/edit", title: "用户编辑页", component: UserEditPage }, // 用户信息修改页
{ path: "/user/list", title: "用户列表页", component: UserListPage }, // 用户列表页
{ path: "/user/login", title: "登录页", component: UserLoginPage }, // 用户登录页

{ path: "/team", title: "队伍信息页", component: TeamPage }, // 队伍页
{ path: "/team/edit", title: "队伍编辑", component: TeamEditPage }, // 队伍修改页
{ path: "/team/add", title: "队伍新增页", component: TeamAddPage }, // 队伍修改页
{ path: "/team/list", title: "队伍列表页", component: TeamListPage }, // 队伍列表页

{ path: "/friend", title: "用户列表页", component: FriendPage }, // 队伍列表页
];

export default routes;
  • 注意:注册后的跳转页面
  • 其他修改:登录后的显示信息
  • So easy!

后端开发

  • 用户中心就做过了:controller层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 用户注册
*
* @param userRegisterRequest 注册用户信息
* @return id
*/
@PostMapping("/register")
public BaseResponse<Long> userRegister(@RequestBody UserRegister userRegisterRequest) {
//controller对参数的校验
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
String planetCode = userRegisterRequest.getPlanetCode();
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, planetCode))
throw new BusinessException(PARMS_ERROR);

long userRegister = userService.userRegister(userAccount, userPassword, checkPassword, planetCode);
return ResultUtils.success(userRegister);
}
  • service层:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 用户注册
*
* @param userAccount 账户
* @param userPassword 密码
* @param checkPassword 二次密码
* @return 用户id
*/
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword, String planetCode) {
// 1.校验
// 1.1.账户, 密码, 二次密码不能为空
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) throw new BusinessException(PARMS_ERROR);

// 1.2.账户不小于4位
if (userAccount.length() < 4) throw new BusinessException("账户不符合要求", 50000, "账户小于4位");

// 1.3.账户不包含特殊字符
String pattern = ".*[\\s`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?\\\\]+.*";
if (Pattern.matches(pattern, userAccount)) throw new BusinessException("账户不符合要求", 50001, "账户包含特殊字符");

// 1.4.用户密码不小于8位
if (userPassword.length() < 8) throw new BusinessException("密码不符合要求", 60000, "用户密码小于8位");

// 1.5.二次密码与密码相同
if (!userPassword.equals(checkPassword)) throw new BusinessException("二次密码不符合要求", 60001, "二次密码与密码不相同");

// 1.6.星球编号不能超过5位
if (planetCode.length() > 5) throw new BusinessException("星球编号不符合要求", 60002, "星球编号超过5位");

// 1.7.账户不能重复
QueryWrapper<User> ua_lqw = new QueryWrapper<>(); // LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
ua_lqw.eq("user_account", userAccount); // userLambdaQueryWrapper.eq(User::getUserAccount, userAccount);
Long ua_count = userMapper.selectCount(ua_lqw); // long count = this.count(lqw);
if (ua_count > 0) throw new BusinessException("账户不符合要求", 50002, "账户重复");

// 1.8.星球编号不能重复
QueryWrapper<User> pc_lqw = new QueryWrapper<>();
pc_lqw.eq("planet_code", planetCode);
Long pc_count = userMapper.selectCount(pc_lqw);
if (pc_count > 0) throw new BusinessException("星球编号不符合要求", 60003, "星球编号重复");

// 2.对密码进行加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

// 3.向数据库中插入用户数据
User user = new User();
//
user.setUserAccount(userAccount);
user.setUserPassword(encryptPassword);
user.setPlanetCode(planetCode);
boolean save = this.save(user);
//插入失败
if (!save) throw new BusinessException(ErrorCode.UPDATE_ERROR);

return user.getId();
}
  • 测试一下,完成!(2023/06/14晚)

image-20230614225016567

阶段性问题

  • 这里发现由于新用户没有已加入的队伍,在筛选队伍的后端逻辑校验时,排除自己已加入的队伍
1
2
3
4
5
6
7
8
9
10
// 6.排除当前用户已加入的队伍
QueryWrapper<UserTeam> utqw = new QueryWrapper<>();
// 6.1.查询当前用户已加入的队伍信息(为提高性能, 仅拿取需要的team_id字段即可)
utqw.select("team_id").eq("user_id", loginUser.getId());
List<UserTeam> userTeamList = userTeamService.list(utqw);
// 6.2.队伍列表排除掉用户已加入的队伍
if (!CollectionUtils.isEmpty(userTeamList)) {
List<Long> teamIdList = userTeamList.stream().map(UserTeam::getTeamId).collect(Collectors.toList());
tqw.notIn("id", teamIdList);
}
  • 要判断userTeamList不为空才能进行stream流操作,否则会报错,已修正(2023/06/14晚)

思考

  • 浅浅记录一下,由于英语四六级和临近期末,伙伴匹配系统暂时开发。
  • 但基础功能已经完备,这几天也正在了解云服务器领域的知识,为将来的项目的顺利部署打点基础,也要准备接下来项目的学习:API接口开放平台、微服务搜索、BI智能平台(2023/06/26午)
  • 今天是个大喜的日子,Memory-伙伴匹配成功部署上线:http://120.55.62.195:7071/#/(2023/07/29晚)

轮播图

  • 这功能简直不要太简单,加个组件,放两张图片不就搞定了
  • 一个多月前,就已经有这个想法了,不过我想放几张我喜欢的图片,但存储在Gitee图床的图片好像有跨域处理,外界访问不到
  • 于是这个想法就被搁置了
  • 不过我开通了七牛云对象存储服务,当我的免费图床hhh
  • 以下是轮播图的具体代码:(2023/07/31早)
1
2
3
4
5
// 轮播图片
const images = [
"http://ry2s7czdf.hd-bkt.clouddn.com/imgs/wallpaper/girl4.jpg",
"http://ry2s7czdf.hd-bkt.clouddn.com/imgs/wallpaper/girl3.jpg",
];
1
2
3
4
5
6
<!-- 首页轮播图 -->
<van-swipe :autoplay="3000" lazy-render :width="400" :height="300">
<van-swipe-item v-for="image in images" :key="image">
<img :src="image" width="400" />
</van-swipe-item>
</van-swipe>
  • 效果如下:

image-20230731094458270

image-20230731094503317

搜索用户优化

TODO

  • 新老值一样,无需修改(减少查询数据库)
  • 根据队长id查询队伍——>根据队长姓名?(这个也可以不改的,可以把用户星球编号设置为队员id编号)
  • 校验老三样:校验登录、校验用户(存在?权限?封号?)、校验队伍(存在?状态?)(√)
  • 退出队伍,校验该用户是否为该队伍成员
  • 解散加密队伍必须输入密码
  • 获取当前用户创建的队伍(√)
  • 获取当前用户加入的队伍(√)
  • 分页查询用户信息的脱敏
  • 分页查询队伍信息的脱敏
  • 代码封装性不好,好多重复代码
  • 代码思路很清晰,但有些许不整洁
  • user_team表join_time冗余字段 (收回这个问题,create_time与业务无关,不能替代join_time)
  • createTime、expireTime、updateTime传到前端显示不正常(√)
  • 前端的pages、models等目录杂乱了,需要分包管理
  • 前端关于页面跳转的问题,router.push()、router.replace()的使用
  • axios请求的返回值:response、response.data(√)
  • 修改队伍还要扩展支持修改最大人数
  • 获取队伍信息team/one我直接返回team了,没有脱敏,其实密码加密就行
  • 根据标签搜索用户、根据队伍名、队伍描述等搜索队伍需要优化(√)
  • 前端页面的标题 对应每个页面都应该显示对应的标题(√)
  • 登录后页面跳转到原先页
  • 搜索队伍分为公开和加密两栏,私有队伍不对外显示(√)
  • 加入队伍功能,加入加密队伍需要输入密码(√)
  • 分布式锁防止单用户加入两次队伍
  • 项目打包上线
  • 拿到加入队伍的所有队员的信息
  • 前台的提示信息不够完善,比如登录时的用户名、密码校验提示,电话号码、验证码校验提示,新增、删除、加入队伍提示等等
  • 支持用户上传头像

Memory 伙伴匹配系统-开发文档
http://example.com/2023/03/24/Memory 伙伴匹配系统-开发文档/
作者
Memory
发布于
2023年3月24日
更新于
2023年9月20日
许可协议