# 伙伴匹配项目

移动端的H5网页(尽量支持PC端哈哈)

# 需求分析

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

# 技术栈

# 前端

  1. Vue 3 开发框架(提高页面开发的效率)
  2. Vant UI(基于 Vue 的移动端组件库)(React 版 Zent)
  3. Vite 2(打包工具,快!)
  4. Nginx 来单机部署

# 后端

  1. Java 编程语言 + SpringBoot 框架
  2. SpringMVC + MyBatis + MyBatis Plus(提高开发效率)
  3. MySQL 数据库
  4. Redis 缓存
  5. Swagger + Knife4j 接口文档

# 前端项目初始化

搭建前端我们使用的是Vant组件库

https://vant.pro/vant/#/zh-CN/quickstart

(1)从0开始的话我建议是使用脚手架

目前我们只学习了两种

1.vue cli:https://cli.vuejs.org/zh/

2.vite:https://vitejs.cn/guide/

(2)整合组建库Vant:

安装Vant

配置按需引入:npm i vite-plugin-style-import

(3)创建项目

npm init vite@latest
1

# 整合组件库

1.安装插件

# 通过 npm 安装 npm i unplugin-vue-components -D

2.配置插件

如果是基于 vite 的项目,在 vite.config.ts 文件中配置插件(注意部分粘贴)

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()],
    }),
  ],
};

1
2
3
4
5
6
7
8
9
10
11
12
13

3.使用组件

完成以上两步,就可以直接在模板中使用 Vant 组件了,unplugin-vue-components 会解析模板并自动注册对应的组件。(也就是说啥也不用做)

main.ts

image-20250520204551089

# 通过 npm 安装Vant

在现有项目中使用 Vant 时,可以通过 npm 进行安装:(项目终端上)

# Vue 3 项目,安装最新版 Vant

 npm i vant
1

# Button按钮

<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>
1
2
3
4
5

# 前端开发页面经验

1.多参考

2.从整体到局部

3.先想清楚页面要做成什么样子,再写代码

# 前端主页+组件概览

# 设计

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

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

内容

tab 栏:

  • 主页(推荐页 + 广告

    • 搜索框
    • banner
    • 推荐信息流
  • 队伍页

  • 用户页(消息 - 暂时考虑发邮件)

# 开发

很多页面要复用组件 / 样式,重复写很麻烦、不利于维护,所以抽象一个通用的布局(Layout)

组件化

# 添加导航组件

清空App.vue,删除HelloWord.vue,新建layout文件夹。(注意layout文件的层级)

img

在layout文件新建BasicLayout.vue。在vant3里面找到NavBar 导航栏组件,按照官网的来。

img

NavBar 导航栏组件,在main.ts中引入。

img

然后在BasicLayout.vue(NavBar.vue)中添加导航栏。(跟着文档来,有不一样的地方自己修改一下。)

img

最后在APP.Vue中引入导航栏。

img

运行的效果

img

踩坑

如果你的项目运行成这样了,是因为你main.ts里的样式没有删掉。

image-20250520210530056

添加搜索插槽,icon图标样式引用

img

img

# 主页内容添加

新建pages文件夹,新建Index.vue和Team.vue

img

然后添加主页内容

<template>
  <van-nav-bar
      title="标题"
      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>

  <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>
import {ref} from "vue";
import {Toast} from "vant";
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) => Toast(`标签 ${index}`);

</script>

<style scoped>

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

(这个标签有点小瑕疵,他会自己消失,就是没有样式,无伤大雅。)

img

后面我调了一下啊,需要加一下标签的样式和控件(注意手动引用一个是在main.ts里,一个是在BasicLayout.vue里。)

img

# 数据库表设计

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

# 标签表(分类表)

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

性别:男女

学习方向:java,C++

目标:考研还是求职(社招还是校招)、考公、竞赛(蓝桥杯,ACM)

段位:初级、中级、高级

身份:大一、大二、大三、大四、学生、程序员

状态:丧

【用户自己定义标签】

字段:

id,int 主键

标签名 varchar 非空(加索引)

上传标签的用户 userId int(加索引)

父标签 id,parentid,int

是否为父标签 isParent,tinyint

创建时间

更新时间

是否删除

怎么查询所有标签,并且把标签分好组

根据父标签查询子标签?根据id去查询就好了

CREATE TABLE `tag` (
                       `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
                       `tagName` varchar(255) NOT NULL COMMENT '标签名称',
                       `userId` int NOT NULL COMMENT '上传标签的用户ID',
                       `parentid` int DEFAULT NULL COMMENT '父标签ID',
                       `isParent` tinyint(1) DEFAULT '0' COMMENT '是否为父标签(0-否,1-是)',
                       `createTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
                       `updateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
                       `是否删除` tinyint(1) DEFAULT '0' COMMENT '是否删除(0-未删除,1-已删除)',
                       PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表';

-- 为 tagName 添加普通索引
ALTER TABLE `tag` ADD INDEX `idx_tagName` (`tagName`);

-- 为 userId 添加普通索引
ALTER TABLE `tag` ADD INDEX `idx_userId` (`userId`);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 用户表

用户有哪些标签

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

​ 好处:可以不用新建一个表,比如查询用户列表,要查询100个数据,那关联表要拿100个id去查关联表,减少开发成本,但是数据量比较少的时候可以用这种,如果后续用户比较多了,我们后面也可以使用缓存,查一次存一下。

​ 坏处:查询起来查看不太方便,但是可以使用模糊查询

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

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

​ 坏处:要多建一个表,多维护一个表,企业开发中尽量避免关联查询,大家可以去看阿里巴巴的开发手册,关联表尽量不超过3张表

CREATE TABLE `user` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    `username` varchar(256) DEFAULT NULL COMMENT '用户昵称',
    `userAccount` varchar(256) DEFAULT NULL COMMENT '登录账号',
    `avatarUrl` varchar(1024) DEFAULT NULL COMMENT '用户头像URL',
    `gender` tinyint DEFAULT NULL COMMENT '性别(0-未知 1-2-)',
    `userPassword` varchar(512) NOT NULL COMMENT '加密后的密码',
    `phone` varchar(128) DEFAULT NULL COMMENT '手机号',
    `email` varchar(512) DEFAULT NULL COMMENT '邮箱',
    `userStatus` int DEFAULT 0 NOT NULL COMMENT '账号状态(0-正常 1-禁用)',
    `userRole` int DEFAULT 0 NOT NULL COMMENT '用户角色(0-普通用户 1-管理员)',
    `tags` varchar(1024) DEFAULT NULL COMMENT '标签列表(JSON格式存储)',
    `createTime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `isDelete` tinyint DEFAULT 0 NOT NULL COMMENT '是否删除(0-未删除 1-已删除)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信息表';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 后端接口开发

# 搜索标签功能

根据标签搜索相对应的用户(匹配队友)

使用sql语句

1.允许用户传入多个标签,多个标签都存在才能搜索出来 and

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

两种方式:

1.SQL查询

2.内存查询

SQL查询

searchUsersByTags

    /**
     *   根据标签搜索用户。
     * @param tagNameList  用户要搜索的标签
     * @return
     */
    @Override
    public List<User> searchUsersByTags(List<String> tagNameList){
        if (CollectionUtils.isEmpty(tagNameList)){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //拼接tag
        // like '%Java%' and like '%Python%'
        for (String tagList : tagNameList) {
            queryWrapper = queryWrapper.like("tags", tagList);
        }
        List<User> userList = userMapper.selectList(queryWrapper);
        // 将用户列表转换为安全用户列表:
        // 1. 使用stream()将List<User>转换为流(Stream<User>),便于链式操作
        // 2. 通过map()对每个用户对象应用getSafetyUser方法,去除敏感信息
        // 3. 使用collect(Collectors.toList())将处理后的流重新收集为List<User>
        // 最终返回一个不包含敏感信息的用户列表
        return  userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

添加测试类 img

内存查询

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.9</version>
</dependency>

1
2
3
4
5
6
7

searchUsersByTags

内存查询,先查询所有用户再判断内存中是否包含要求的标签

  /**
     *   根据标签搜索用户。
     * @param tagNameList  用户要搜索的标签
     * @return
     */
    @Override
    public List<User> searchUsersByTags(List<String> tagNameList){
        if (CollectionUtils.isEmpty(tagNameList)){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
//        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//        //拼接tag
//        // like '%Java%' and like '%Python%'
//        for (String tagList : tagNameList) {
//            queryWrapper = queryWrapper.like("tags", tagList);
//        }
//        List<User> userList = userMapper.selectList(queryWrapper);
//        return  userList.stream().map(this::getSafetyUser).collect(Collectors.toList());

        //1.先查询所有用户
        QueryWrapper queryWrapper = new QueryWrapper<>();
        List<User> userList = userMapper.selectList(queryWrapper);
        Gson gson = new Gson();
        //2.判断内存中是否包含要求的标签
        //使用Java 8的Stream API对用户列表进行过滤。
        return userList.stream().filter(user -> {
            //获取用户标签:获取用户的标签字符串,如果为空则直接过滤掉该用户。
            String tagstr = user.getTags();
            if (StringUtils.isBlank(tagstr)){
                return false;
            }
            //JSON转Set:将用户标签的JSON字符串转换为Set集合。
            Set<String> tempTagNameSet =  gson.fromJson(tagstr,new TypeToken<Set<String>>(){}.getType());
            //java8  Optional 来判断空
            //处理可能的空Set:使用Optional处理可能的null值,确保得到一个非空的Set。
            tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
            //检查是否包含所有要求的标签:检查用户的标签Set是否包含搜索条件中的所有标签,只有包含所有标签的用户才会被保留。
            for (String tagName : tagNameList){
                if (!tempTagNameSet.contains(tagName)){
                    return false;
                }
            }
            return true;
            //转换和安全处理:对过滤后的用户进行安全处理(可能隐藏敏感信息),然后收集为List返回。
        }).map(this::getSafetyUser).collect(Collectors.toList());
    }
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

比较运行速度

 /**
     *   根据标签搜索用户。
     * @param tagNameList  用户要搜索的标签
     * @return
     */
    @Override
    public List<User> searchUsersByTags(List<String> tagNameList){
        if (CollectionUtils.isEmpty(tagNameList)){
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        return sqlSearch(tagNameList);   //先 sql query time = 5982 后 memory query time = 5606
//        return memorySearch(tagNameList);    // 先 memory query time = 5938 后 sql query time = 5956 (清过缓存)
    }

    /**
     *     sql运行查询
     * @param tagNameList
     * @return
     */
    public List<User> sqlSearch(List<String> tagNameList){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        long starTime = System.currentTimeMillis();
        //拼接tag
        // like '%Java%' and like '%Python%'
        for (String tagList : tagNameList) {
            queryWrapper = queryWrapper.like("tags", tagList);
        }
        List<User> userList = userMapper.selectList(queryWrapper);
        log.info("sql query time = " + (System.currentTimeMillis() - starTime));
        return  userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
    }

    /**
     *     查询,内存运行筛选
     * @param tagNameList
     * @return
     */
    public List<User> memorySearch(List<String> tagNameList){

        //1.先查询所有用户
        QueryWrapper queryWrapper = new QueryWrapper<>();
        long starTime = System.currentTimeMillis();
        List<User> userList = userMapper.selectList(queryWrapper);
        Gson gson = new Gson();
        //2.判断内存中是否包含要求的标签
        userList.stream().filter(user -> {
            String tagstr = user.getTags();
            if (StringUtils.isBlank(tagstr)){
                return false;
            }
            Set<String> tempTagNameSet =  gson.fromJson(tagstr,new TypeToken<Set<String>>(){}.getType());
            for (String tagName : tagNameList){
                if (!tempTagNameSet.contains(tagName)){
                    return false;
                }
            }
            return true;
        }).map(this::getSafetyUser).collect(Collectors.toList());
        log.info("memory query time = " + (System.currentTimeMillis() - starTime));
        return  userList;
    }

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

# 前端开发

# 前端整合路由

Vue-Router:直接看官方文档引入

https://router.vuejs.org/zh/guide/#html

Vue-Router 其实就是帮助你根据不同的 url 来展示不同的页面(组件),不用自己写 if / else

路由配置影响整个项目,所以建议单独用 config 目录、单独的配置文件去集中定义和管理。

有些组件库可能自带了和 Vue-Router 的整合,所以尽量先看组件文档、省去自己写的时间。

npm install vue-router@4
1

详情看之前的路由笔记

router.ts

import { createRouter, createWebHistory } from 'vue-router'
import Index from "../pages/Index.vue";
import Team from "../pages/Team.vue";
import User from "../pages/User.vue";

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

const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

main.ts

import { createApp } from 'vue'
import { Button } from 'vant';
import { NavBar } from 'vant';
import { Tabbar, TabbarItem } from 'vant';
import App from './App.vue'
import 'vant/lib/index.css';
import router from "./config/router.ts";
//引入组件样式

const app = createApp(App);

// 注册所有插件
app.use(Button);
app.use(NavBar);
app.use(Tabbar);
app.use(TabbarItem);
app.use(router);

// 最后挂载应用
app.mount('#app')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

单独提出路由,新建config文件夹,新建router.ts文件。 (上面调好了的引入了router.ts)

image.png

新建个人页面,复制粘贴一下就行了。

image.png

然后就是主页点击跳转要使用路由的操作了。

在Tabbar 标签栏 - Vant 3Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)中有提到路由的使用。

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

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

      <van-tabbar route @change="onChange">
      <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>

      </template>

      <script setup>
      import {ref} from "vue";
      import {Toast} from "vant";

      import 'vant/es/toast/style';


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

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

    </script>

<style scoped>

</style>



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

在启动测试,就可以得到带路由的跳转。

# 搜索页面

Vant3中找到搜索样式。Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)(事件监听)并添加到新建的搜索页面searchPage.vue里

image.png

主页搜索图标跳转路由编程式导航 | Vue Router (opens new window)

img

路由挂载

img

Tag标签Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)可关闭标签

img

改一下名,引入所有组件(这个是官方不推荐的,体量大。现在官网这个描述看不到了。2022年11月13日)

img

添加分类选择Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)多选模式

img

选择与标签连接

img

关闭标签

img

美化一下标签的间隔样式Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)

img

img

# 过滤筛选标签

img

<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>
  <van-row gutter="16" style="padding: 0 16px">
    <van-col v-for="tag in activeIds">
      <van-tag  closeable size="small" type="primary" @close="doclose(tag)">
      {{ tag }}
    </van-tag>
  </van-col>
</van-row>

  <van-divider content-position="left">已选标签</van-divider>
    <van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="tagList"
  />
  </template>

    <script setup>
      import { ref } from 'vue';

      const searchText = ref('');

      const originTagList = [{
        text: '性别',
      children: [
      { text: '男', id: '男' },
      { text: '女', id: '女' },
      { text: '嬲', id: '嬲' },
      ],
      }, {
        text: '年级',
      children: [
      { text: '大一', id: '大一' },
      { text: '大二', id: '大二' },
      { text: '大三', id: '大三' },
      { text: '大四', id: '大四' },
      { text: '大五', id: '大五' },
      { text: '大六', id: '大六' },
      ],
      },
      ];
      //标签列表
      let tagList = ref(originTagList);
      /**
      *  搜索过滤
      * @param val
      */
      const onSearch = (val) => {
        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 activeIds = ref([]);
      const activeIndex = ref(0);


      //关闭标签
      const  doclose = (tag) =>{
        activeIds.value = activeIds.value.filter(item =>{
          return item !== tag;
        })
                                                
      }
                                           
    </script>
                                          
    <style scoped>
                                         
    </style>
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

# 用户信息页

新建前端接受用户信息实体。user.d.ts

image.png

添加展示组件Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)展示箭头,添加数据展示。

image.png

image-20250525214545362

# 编辑页面

新建UserEditPage.vue,并添加路由。

img

在个人页添加修改的跳转方法。

img

UserEditPage页面添加form表单,并完成获取数值和修改。Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window)

img

image-20250525214615521

页面返回

img

# 后端整合 Swagger + Knife4j 接口文档

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

  • 请求参数

  • 响应参数

  • 错误码

  • 接口地址

  • 接口名称

  • 请求类型

  • 请求格式

  • 备注

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

为什么需要接口文档?

  • 有个书面内容(背书或者归档),便于大家参考和查阅,便于 沉淀和维护 ,拒绝口口相传
  • 接口文档便于前端和后端开发对接,前后端联调的 介质 。后端 => 接口文档 <= 前端
  • 好的接口文档支持在线调试、在线测试,可以作为工具提高我们的开发测试效率

怎么做接口文档?

  • 手写(比如腾讯文档、Markdown 笔记)
  • 自动化接口文档生成:自动根据项目代码生成完整的文档或在线调试的网页。Swagger,Postman(侧重接口管理)(国外);apifox、apipost、eolink(国产)

接口文档有哪些技巧?

Swagger 原理:

  1. 引入依赖(Swagger 或 Knife4j:https://doc.xiaominfo.com/knife4j/documentation/get_start.html)
  2. 自定义 Swagger 配置类
  3. 定义需要生成接口文档的代码位置(Controller)
  4. 千万注意:线上环境不要把接口暴露出去!!!可以通过在 SwaggerConfig 配置文件开头加上 @Profile({"dev", "test"}) 限定配置仅在部分环境开启
  5. 启动即可
  6. 可以通过在 controller 方法上添加 @Api、@ApiImplicitParam(name = "name",value = "姓名",required = true) @ApiOperation(value = "向客人问好") 等注解来自定义生成的接口描述信息

如果 springboot version >= 2.6,需要添加如下配置:

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
1
2
3
4

# swagger

<!-- swagger 接口文档 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11

# 存量用户信息导入及同步

1.把班级中的所有学生信息导入

# 上了网页信息,怎么抓到?

  1. 分析原网站是怎么获取这些数据的?哪个接口?

按 F 12 打开控制台,查看网络请求,复制 curl 代码便于查看和执行:

  1. 用程序去调用接口 (java okhttp httpclient / python 都可以)
  2. 处理(清洗)一下数据,之后就可以写到数据库里

# 流程(数据导入)

  1. 从 excel 中导入全量用户数据,判重

easy excel

  1. 抓取写了自我介绍的同学信息,提取出用户昵称、用户唯一 id、自我介绍信息
  2. 从自我介绍中提取信息,然后写入到数据库中

两种读对象的方式:

  1. 确定表头:建立对象,和表头形成映射关系
  2. 不确定表头:每一行数据映射为 Map<String, Object>

两种读取模式:

  1. 监听器:先创建监听器、在读取文件时绑定监听器。单独抽离处理逻辑,代码清晰易于维护;一条一条处理,适用于数据量大的场景。
  2. 同步读:无需创建监听器,一次性获取完整数据。方便简单,但是数据量大时会有等待时常,也可能内存溢出。

# easy excel

pom.xml

 <!-- easy Excel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.1.0</version>
        </dependency>
1
2
3
4
5
6

新建文件夹和用户信息文件。

@Data
public class XingQiuTableUserInfo {
    /**
     * id
     */
    @ExcelProperty("成员编号")
    private String planetCode;

    /**
     * 用户昵称
     */
    @ExcelProperty("成员昵称")
    private String username;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

新建监听器。

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;


// 有个很重要的点 TableListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class TableListener implements ReadListener<XingQiuTableUserInfo> {

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

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

}
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

新建读取数据文件。

import com.alibaba.excel.EasyExcel;

import java.util.List;

/**
 * @author: shayu
 * @date: 2022/11/20
 * @ClassName: yupao-backend01
 * @Description:    导入Excel,读取数据
 */
public class ImportExcel {
    /**
     * 读取数据
     */
    public static void main(String[] args) {
        // 写法1:JDK8+ ,不用额外写一个DemoDataListener
        // since: 3.0.0-beta1
        //Excel数据文件放在自己电脑上,能够找到的路径
        String fileName = "C:\\Users\\25073\\Desktop\\testExcel.xlsx";
//          readByListener(fileName);
        synchronousRead(fileName);

    }
    /**
     * 监听器读取
     * @param fileName
     */
    public static void readByListener(String fileName) {
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        // 这里每次会读取100条数据 然后返回过来 直接调用使用数据就行
        EasyExcel.read(fileName, XingQiuTableUserInfo.class, new TableListener()).sheet().doRead();
    }

    /**
     * 同步读
     * 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面
     */
    public static void synchronousRead(String fileName) {
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish
        List<XingQiuTableUserInfo> list = EasyExcel.read(fileName).head(XingQiuTableUserInfo.class).sheet().doReadSync();
        for (XingQiuTableUserInfo xingQiuTableUserInfo : list) {
            System.out.println(xingQiuTableUserInfo);
        }

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

两种读取模式:

  1. 监听器:先创建监听器、在读取文件时绑定监听器。

单独抽离处理逻辑,代码清晰易于维护;一条一条处理,适用于数据量大的场景。

  1. 同步读:无需创建监听器,一次性获取完整数据。

方便简单,但是数据量大时会有等待时常,也可能内存溢出。

# 前端开发

# 搜索详情页面

之前前端的代码写到了搜索页面可以挑选搜索标签,并没有去根据具体标签搜索用户。这里就开始实现

新建SearchResultPage.vue,并添加相关路由。

image-20250529174404701

image-20250529174414656

然后是在搜索页添加搜索按钮,和触发点击。搜索页选择标签,点击搜索。

image-20250529174427603

img

然后就开始修改SearchResultPage.vue页面相关信息。

Vant3 商品卡片组件

<template>
  <van-card
      v-for="user in userList"
      :desc="user.profile"
      :title="`${user.username} (${user.planetCode})`"
      :thumb="user.avatarUrl"
  >
    <template #tags>
      <van-tag plain type="danger" v-for="tag in tags" style="margin-right: 8px; margin-top: 8px" >
        {{tag}}
      </van-tag>
    </template>
    <template #footer>
      <van-button size="mini">联系我</van-button>
    </template>
  </van-card>
</template>

<script setup >
import {ref} from "vue";
import {useRoute} from "vue-router";

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

const mockUser = ref({
  id: 931,
  username: '沙鱼',
  userAccount: 'shayu',
  profile: '一条咸鱼',
  gender: 0,
  phone: '123456789101',
  email: 'shayu-yusha@qq.com',
  planetCode: '931',
  avatarUrl: 'https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/shayu931/shayu.png',
  tags: ['java', 'emo', '打工中', 'emo', '打工中'],
  createTime: new Date(),
})

const userList = ref({mockUser});

</script>

<style scoped>

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

# 前端对接

http://www.axios-js.com/

 npm install axios
1

新建plugins文件夹,新建myAxios.js文件。

image-20250529201957942

myAxios.js

// Set config defaults when creating the instance
//自定义实例默认值
import axios from "axios";

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

//拦截器
// 添加请求拦截器
myAxios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    console.log("请求发送了",config)
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
myAxios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    console.log("请求收到了了",response)
    return response;
}, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
});

export default myAxios;

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

然后是用户根据标签搜素,要去调用后端的数据。就需要axios接受和发送请求。

在searchResultPage.vue页面添加axios。并测试访问,会报错,因为跨域了

image-20250529202052913

后端允许一下,前端端口访问。修改usercontroller。在运行访问一下。(前后端都要启动)

image-20250529202121974

WebMvcConfig

package com.yupi.usercenter.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
                /* 放自己的前端域名*/
                .allowedOrigins("http://localhost:5173", "http://127.0.0.1:5173", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
                //是否允许证书 不再默认开启
                .allowCredentials(true)
                //设置允许的方法
                .allowedMethods("*")
                //跨域允许时间
                .maxAge(3600);
    }
}

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

然后就是从后端拿取数据,传到前端啦。先去数据添加几条数据,前端也要进行修改一下。

SearchResultPage.vue

<template>
  <van-card
    v-for="user in userList"
    :desc="user.profile"
    :title="`${user.username} (${user.planetCode})`"
    :thumb="user.avatarUrl"
    >
    <template #tags>
      <van-tag plain type="danger" v-for="tag in tags" style="margin-right: 8px; margin-top: 8px" >
        {{tag}}
      </van-tag>
    </template>
<template #footer>
  <van-button size="mini">联系我</van-button>
</template>
             </van-card>
  <van-empty v-if="!userList || userList.length < 1" description="搜索结果为空" />
</template>

  <script setup >
  import {onMounted, ref} from "vue";
  import {useRoute} from "vue-router";
  import {Toast} from "vant";

  import myAxios from "../plugins/myAxios.js";

  import qs from 'qs'

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

  const mockUser = ref({
  id: 931,
  username: '沙鱼',
  userAccount: 'shayu',
  profile: '一条咸鱼',
  gender: 0,
  phone: '123456789101',
  email: 'shayu-yusha@qq.com',
  planetCode: '931',
  avatarUrl: 'https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/shayu931/shayu.png',
  tags: ['java', 'emo', '打工中', 'emo', '打工中'],
  createTime: new Date(),
  })

  const userList = ref([]);


  onMounted( async () =>{
  // 为给定 ID 的 user 创建请求
  const userListData = await  myAxios.get('/user/search/tags',{
  withCredentials: false,
  params: {
  tagNameList: tags
  },

  //序列化
  paramsSerializer: {
  serialize: params => qs.stringify(params, { indices: false}),
  }
  })
  .then(function (response) {
  console.log('/user/search/tags succeed',response);
  Toast.success('请求成功');
  return response.data?.data;
  })
  .catch(function (error) {
  console.log('/user/search/tags error',error);
  Toast.fail('请求失败');
  });
  if (userListData){
  userListData.forEach(user =>{
  if (user.tags){
  user.tags = JSON.parse(user.tags);
  }
  })
  userList.value = userListData;
  }
  })



</script>

<style scoped>

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

# 后端开发

banner.txt

https://www.bootschool.net/ascii-art

# 根据标签搜索用户接口

后端

@RequestMapping("/userList")
    public Result userList(@RequestParam(value = "tagsId", required = false) List<String> tagsId){
        if (tagsId.isEmpty()){
            return Result.error("传递标签为空");
        }
        //根据前端传递参数查询对应的用户信息
        // 先查询所有用户
        List<User> lists = userService.list();
        if (lists.isEmpty()){
            return Result.error("查询的标签用户为空");
        }
        // 使用 Java 8 Stream API 进行过滤(OR 条件)
        List<User> filteredUsers = lists.stream()
                .filter(user -> {
                    String tags = user.getTags();
                    if (StringUtils.isBlank(tags)) {
                        return false;
                    }

                    // 将 JSON 字符串转换为 Set
                    Set<String> tempTagNameSet = gson.fromJson(tags, new TypeToken<Set<String>>() {}.getType());

                    // OR 逻辑:只要包含任意一个标签就返回 true
                    for (String tagName : tagsId) {
                        if (tempTagNameSet.contains(tagName)) {
                            return true; // 包含任意一个就保留
                        }
                    }
                    return false; // 一个都不包含则过滤掉
                })
                .collect(Collectors.toList());

        // System.out.println(filteredUsers);
        return Result.success(filteredUsers);
    }
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

前端

<script setup lang="ts">
import { useRoute } from "vue-router";
import { onMounted, ref } from "vue";
import myAxios from '../config/myAxios';
import { showFailToast } from 'vant';

const route = useRoute();
const userList = ref<Array<{
  username: string;
  userAccount: string;
  profile: string;
  avatarUrl: string;
  gender: string;
  phone: string;
  email: string;
  tags: string[];
}>>([]);

onMounted(async () => {
  try {
    const activeIds = Array.isArray(route.query.activeIds)
        ? route.query.activeIds
        : route.query.activeIds?.split(',') || [];

    const response = await myAxios.get('/userList', {
      params: {
        tagsId: activeIds
      },
      paramsSerializer: params => {
        return Object.entries(params)
            .map(([key, value]) => {
              if (Array.isArray(value)) {
                return value.map(v => `${key}=${encodeURIComponent(v)}`).join('&');
              }
              return `${key}=${encodeURIComponent(value)}`;
            })
            .join('&');
      }
    });

    // console.log('完整响应数据:', response.data);

    // 修正这里:补全右括号
    if (Array.isArray(response.data.data)) {
      // 处理整个用户列表
      userList.value = response.data.data.map(user => {
        // 处理tags格式转换
        let tagsArray: string[] = [];
        try {
          if (typeof user.tags === 'string') {
            tagsArray = JSON.parse(user.tags.replace(/'/g, '"'));
          } else if (Array.isArray(user.tags)) {
            tagsArray = user.tags;
          }
        } catch (e) {
          console.error('解析tags失败:', e);
        }

        return {
          username: user.username || '',
          userAccount: user.userAccount || '',
          profile: user.profile || '暂无简介',
          avatarUrl: user.avatarUrl || '',
          gender: user.gender?.toString() || '',
          phone: user.phone || '',
          email: user.email || '',
          tags: tagsArray
        };
      });
    } else {
      showFailToast('返回数据格式不正确');
    }

  } catch (error) {
    console.error('获取用户数据失败:', error);
    showFailToast('加载用户信息失败');
  }
});
</script>

<template>
  <div class="user-list-container">
    <!-- 循环渲染每个用户卡片 -->
    <van-card
        v-for="user in userList"
        :key="user.userAccount"
        :desc="user.profile"
        :title="user.username"
        :thumb="user.avatarUrl"
    >
      <template #tags>
        <van-tag
            v-for="tag in user.tags"
            :key="tag"
            plain
            type="primary"
            style="margin-right: 10px"
        >
          {{ tag }}
        </van-tag>
      </template>
      <template #footer>
        <van-button type="primary" size="small">联系我</van-button>
        <van-button plain size="small" style="margin-left: 8px">查看详情</van-button>
      </template>
    </van-card>
  </div>
</template>

<style scoped>
.user-list-container {
  padding: 12px;
}

.van-card {
  margin-bottom: 16px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>

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

# 用户信息接口

# session单点登录

使用redis实现sessin数据共享

可以使用springsession redis实现将session存入到redis中

# 后端用户个人信息修改接口

1.校验参数是否为空

2.校验权限,是否更新的是自己人(仅管理员和自己可以修改)

3.修改实现

前端修改用户信息,点击提交;现在无法对接到后端,需要在后端新写一个接口。

控制层新增用户信息更新接口。

img

service层提供用户信息修改方法,并提取了获取当前用户信息和是否为管理员的方法。

image.png

serviceImpl层进行实现。

img

UserController

// 修改接口
    @RequestMapping("/user/edit")
    public Result userEdit(@RequestBody UserDto userDto, HttpSession session){
        if (userDto == null){
            return Result.error("传递的数据为空");
        }
        //如果要修改的话你得验证你修改的是你自己的信息,但是管理员就可以随意修改
        // System.out.println(userDto);
        //将登录之后存储到session中的用户信息取出来,跟前端传递过来的用户信息进行比较
        User userInfo = (User) session.getAttribute("userInfo");
        // System.out.println("这是我session中拿到的数据:"+userInfo);
        if (userInfo.getId().equals(userDto.getId())){
            User users = BeanUtil.copyProperties(userDto, User.class);
            //证明修改的是同一个用户
            if (userService.updateById(users)){
                return Result.success();
            }
        }
        return Result.error("修改失败");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

后端由于要获取session,所以创建session必须是同一个对象,所以需要开启session跨域!!!!

MyAxios.js

//关键,跨域请求
axios.defaults.withCredentials = true
1
2

WebMvcConfig

package cn.lanqiao.lanqiaofriendadmin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @ Author: 李某人
 * @ Date: 2025/05/30/10:42
 * @ Description:
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private FilterConfig filterConfig;
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**")
                //设置允许跨域请求的域名
                //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
                /* 放自己的前端域名*/
                .allowedOrigins("http://localhost:5173", "http://127.0.0.1:5173", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
                //是否允许证书 不再默认开启
                .allowCredentials(true)
                //设置允许的方法
                .allowedMethods("*")
                //跨域允许时间
                .maxAge(3600);
    }
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(filterConfig).addPathPatterns("/**");
    }
}

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

FilterConfig

package cn.lanqiao.lanqiaofriendadmin.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class FilterConfig implements HandlerInterceptor{

    public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
            throws Exception {
    }

    public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2)
            throws Exception {
    }

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {

        response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));//支持跨域请求
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");//是否支持cookie跨域
        response.setHeader("Access-Control-Allow-Headers", "Authorization,Origin, X-Requested-With, Content-Type, Accept,Access-Token");//Origin, X-Requested-With, Content-Type, Accept,Access-Token
        return 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

EditUserPage

<script setup lang="ts">
import { ref } from "vue";
import { useRoute } from "vue-router";
import myAxios from "../config/myAxios";
import { showFailToast, showSuccessToast } from "vant";
import router from "../config/router.ts";
// 获取路由参数
const route = useRoute();

// 从 query 参数中获取值(适用于 /edit/user?field=username&value=lmx 这种URL)
const initialValue = ref((route.query.value as string) || "");
const labelValue = ref((route.query.label as string) || "");
const fieldValue = ref((route.query.field as string) || "");
const userId = ref((route.query.userId as string) || "");
console.log(userId);
// 表单数据
const username = ref(initialValue.value); // 初始化表单值为路由传递的值

const onSubmit = async (values: any) => {
  console.log("提交数据:", values); //{username: 'lmx'}
  // 获取第一个键名(字段名)和对应的值
  const field = Object.keys(values)[0];
  console.log(field)
  const value = values[field];
  const res = await myAxios.post("/user/edit", {
    [field.valueOf()]: value,
    id: userId.value,
  });
  if (res.data.code == 0 && res.data) {
    showSuccessToast("修改成功");
    router.back();
  } else {
    showFailToast("修改失败");
  }
};
</script>

<template>
  <van-form @submit="onSubmit">
    <van-cell-group inset>
      <van-field
        v-model="username"
        :name="fieldValue"
        :label="labelValue"
        :placeholder="labelValue"
        :rules="[{ required: true, message: '请填写' + labelValue }]"
      />
    </van-cell-group>
    <div style="margin: 16px">
      <van-button round block type="primary" native-type="submit">
        提交
      </van-button>
    </div>
  </van-form>
</template>

<style scoped>
</style>

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

修改一下细节,性别

// 表单数据
let displayValue = "";
if (fieldValue.value === "gender") {
  displayValue =
    initialValue.value === "1"
      ? "男"
      : initialValue.value === "2"
      ? "女"
      : initialValue.value;
} else {
  displayValue = initialValue.value;
}
const username = ref(displayValue);
1
2
3
4
5
6
7
8
9
10
11
12
13

修改一下发送请求携带的数据

const onSubmit = async (values: any) => {
  console.log("提交数据:", values); //{username: 'lmx'}
  // 获取第一个键名(字段名)和对应的值
  const field = Object.keys(values)[0];
  console.log(field);
  let value = values[field];
  // 如果是性别字段,需要转换回数字
  if (fieldValue.value === "gender") {
    value = value === "男" ? "1" : "2";
  }
  const res = await myAxios.post("/user/edit", {
    [field.valueOf()]: value,
    id: userId.value,
  });
  if (res.data.code == 0 && res.data) {
    showSuccessToast(res.data.msg);
    router.back();
  } else {
    showFailToast(res.data.msg);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

后端bug处理一下:如果用户登录session不存在了记得提示一下用户

 // 修改接口
    @RequestMapping("/user/edit")
    public Result userEdit(@RequestBody UserDto userDto, HttpSession session){
        if (userDto == null){
            return Result.error("传递的数据为空");
        }
        //如果要修改的话你得验证你修改的是你自己的信息,但是管理员就可以随意修改
        // System.out.println(userDto);
        //将登录之后存储到session中的用户信息取出来,跟前端传递过来的用户信息进行比较
        User userInfo = (User) session.getAttribute("userInfo");
        if (userInfo == null){
            return Result.error("用户登录失效,请重新登录");
        }
        // System.out.println("这是我session中拿到的数据:"+userInfo);
        if (userInfo.getId().equals(userDto.getId())){
            User users = BeanUtil.copyProperties(userDto, User.class);
            //证明修改的是同一个用户
            if (userService.updateById(users)){
                return Result.success();
            }
        }
        return Result.error("修改失败");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 登录

Vant 3 - Lightweight Mobile UI Components built on Vue (opens new window) (vant3-form表单)

新建登录页面

img

<template>
  <van-form @submit="onSubmit">
    <van-cell-group inset>
      <van-field
          v-model="userAccout"
          name="userAccout"
          label="账户"
          placeholder="账户"
          :rules="[{ required: true, message: '请填写账户' }]"
      />
      <van-field
          v-model="userPassword"
          type="password"
          name="userPassword"
          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>
<script setup lang="ts">

import {useRouter} from "vue-router";
import {ref} from "vue";
import myAxios from "../plugins/myAxios";
import {Toast} from "vant";

const router = useRouter();

const userAccout = ref('');
const userPassword = ref('');

const onSubmit = async () => {
  const res = await myAxios.post('/user/login',{
    userAccount: userAccout.value,
    userPassword: userPassword.value,
  })
  console.log(res,'用户登录');
  if (res.code == 0 && res.data){
    Toast.success('登录成功');
    router.replace('/')
  } else {
    Toast.fail('登录失败')
  }
};

</script>

<style scoped>

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

添加路由

img

数据相应时获取date。

img

# 主页开发

用户只要登录成功,首页就会展现出来,首页会展示我们所有的用户或者跟用户标签差不多的用户

如果我想要将数据一次性全部展示在页面上,会有什么样的问题?

10w条数据 2s 3s这个样子 20w 30w 这个数据怎么办?

一般这种数据量大并且访问次数比较频繁的情况我们基本都会使用缓存来解决这个问题

什么是缓存?先把一些数据存起来,到时候要用的时候我们直接取(Redis)

但是第一个用户进来还是会花2s 3s这个样子才会显示出来数据,为什么?因为缓存是你需要从数据库中查

询出来再存储到缓存里面去,而且缓存是有时间的,不可能一直存在redis中,所以说隔一段时间就会有一

个用户卡在首页(体验感会非常差),怎么解决?

其实我们可以使用定时任务,什么是定时任务:每隔一段时间就会执行一段程序或者说一段接口

我们可以选择在某一个时间段,比如 凌晨的 3点 4点这个样子,用户量访问是最少的,那我们可以把定时

任务定在凌晨3点,去把数据库中的数据提前缓存到redis中,那这样我们这个项目早上第一个访问的用户

他就不需要再去数据库查询了,这样就提高了用户的体验感

我们等会模拟30w条数据进去,首页基本会卡死的

分布式缓存

# 分页查询

之前我们一开始直接查询10w条数据太过于庞大,所以我们考虑分页查询

MybatisPlus的分页查询我们也不需要过多讲解了,比较简单,这里我们开始直接上手

先需要做一个MybatisPlus分页插件的配置

MybatisPlusConfig

package cn.lanqiao.lanqiaofriendadmin.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @ Author: 李某人
 * @ Date: 2025/06/05/21:13
 * @ Description:
 */
@EnableTransactionManagement
@Configuration
@MapperScan("cn.lanqiao.lanqiaofriendadmin.mapper")
public class MybatisPlusConfig {
    /**
     * 新版本插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //分页查询插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

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

前端需要传递两个参数:1.当前页,2.一页显示多少条

 const res = await myAxios.get("/user/index", {
    //携带两个分页参数
    params: {
      //当前页
      currentPage: 1,
      //一页显示多少条
      pageSize: 10,
    }
  });
1
2
3
4
5
6
7
8
9

后端分页接口

//主页接口
@RequestMapping("/user/index")
public Result userIndex(@RequestParam(value = "currentPage", required = false) Long currentPage,
                        @RequestParam(value = "pageSize", required = false) Long pageSize) throws JsonProcessingException {
    //创建page对象:当前页,每页展示的数据数量
    //注意细节不要导错包了,要导入mybatisplus的包 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    Page<User> page = new Page<>(currentPage,pageSize);
    Page<User> userPage = userService.page(page);
    return Result.success(userPage);
}
1
2
3
4
5
6
7
8
9
10

这个时候由于前端传递的参数有了改变看以下截图

image-20250605212937206

所以前端我们需要修改渲染的数据

image-20250605213013331

这样就可以渲染数据了

这个时候我们可以判断redis中是否存在查询到的数据,而且每个用户缓存的数据应该是不一致的,所以我们要根据用的id来进行缓存

获取到当前登录用户的id

无缓存,查询数据库

//这个时候我们可以判断redis中是否存在查询到的数据,而且每个用户缓存的数据应该是不一致的,所以我们要根据用的id来进行缓存
        //获取到当前登录用户的id
        User userInfo = (User) session.getAttribute("userInfo");
        // System.out.println("当前登录的用户:"+userInfo);
        Long userId = userInfo.getId();
        //然后利用用户的id创建redis的key,先看一下redis中有没有该用户创建的key
        Page<User> userRedis = (Page<User>) redisTemplate.opsForValue().get("user:index:" + userId);
        if (userRedis!=null){
            //说明有数据直接取就好了
            return Result.success(userRedis);
        }
        //无缓存,查询数据库
        //创建page对象:当前页,每页展示的数据数量
        //注意细节不要导错包了,要导入mybatisplus的包 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
        Page<User> page = new Page<>(currentPage,pageSize);
        Page<User> userPage = userService.page(page);
        //写入缓存
        try {
            redisTemplate.opsForValue().set("user:index:" + userId,userPage);
        } catch (Exception e) {
            log.error("传入redis中有误");
        }
        return Result.success(userPage);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

最后记得设置redis过期时间因为redis的内存会自动淘汰满的数据

redisTemplate.opsForValue().set("user:index:" + userId,userPage,30, TimeUnit.MINUTES);
1

如何让第一个用户进来的体验感也是非常好的呢?我们可以使用缓存预热

# 缓存预热

缓存预热的优点:

1.解决上面的问题,可以让用户始终访问很快

缺点: 1.增加开发成本(你要额外的开发、设计)

2.预热的时机和时间如果错了,有可能你缓存的数据不对或者太老

3.需要占用额外空间

方法:

最简单的方法:

1.定时任务

2.模拟触发(手动触发)

# 定时任务

1.Spring Scheduler(spring boot默认整合)

2.Quartz(独立于Spring存在的定时任务框架)

3.XXL-Job之类的分布式任务调度平台(界面+sdk)

https://gitee.com/xuxueli0323/xxl-job

PreCacheJob

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yupi.usercenter.common.ResultUtils;
import com.yupi.usercenter.model.domain.User;
import com.yupi.usercenter.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;


@Component
@Slf4j
public class PreCacheJob {

    @Resource
    private UserService userService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    // 重点用户
    private List<Long> mainUserList = Arrays.asList(1L);

    // 每天执行,预热推荐用户
    @Scheduled(cron = "0 12 1 * * *")   //自己设置时间测试
    public void doCacheRecommendUser() {
        //查数据库
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        Page<User> userPage = userService.page(new Page<>(1,20),queryWrapper);
        String redisKey = String.format("shayu:user:recommend:%s",mainUserList);
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //写缓存,30s过期
        try {
            valueOperations.set(redisKey,userPage,30000, TimeUnit.MILLISECONDS);
        } catch (Exception e){
            log.error("redis set key error",e);
        }
    }

}
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

不要去背 cron 表达式!!!!!

  • https://cron.qqe2.com/
  • https://www.matools.com/crontab/

记得在启动类加一个注解

@EnableScheduling
1

# 分布式锁

如果我们的后端部署在多个服务器上,那我的服务器会一次性执行多次,那么就很浪费资源

主要是为了控制在同一时间只能有一个服务器能执行

第一个方法:

1.分离定时任务和主程序,只在一个服务器去执行定时任务

2.写死配置,只能某一个ip地址的执行(ip地址有可能会改变)

3.动态配置

4.分布式锁,只有抢到锁的服务器才能执行业务逻辑,好处就是无论多少个服务器都是一样的配置

我们按道理在大二上的时候应该会学习一个Java锁:Synchronzied,但是他只对自己的类生效

为什么需要使用分布式锁?

  1. 有限资源的情况下,控制同一时间(段)只有某些线程(用户 / 服务器)能访问到资源。
  2. 单个锁只对单个 JVM 有效

# 分布式锁实现的关键

# 抢锁机制

怎么保证同一时间只有 1 个服务器能抢到锁?

核心思想 就是:先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待。

等先来的人执行方法结束,把标识清空,其他的人继续抢锁。

MySQL 数据库:select for update 行级锁(最简单)

(乐观锁)

Redis 实现:内存数据库,读写速度快 。支持 setnx、lua 脚本,比较方便我们实现分布式锁。

setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false。

# 注意事项

  1. 用完锁要释放(腾地方)√

  2. 锁一定要加过期时间 √

  3. 如果方法执行时间过长,锁提前过期了?问题:

    1. 连锁效应:释放掉别人的锁
    2. 这样还是会存在多个方法同时执行的情况

​ 解决方案:续期

boolean end = false;

//线程
new Thread(() -> {
    //判断状态
    if (!end)}{
        续期
    })

    end = true;
1
2
3
4
5
6
7
8
9
10
  1. 释放锁的时候,有可能先判断出是自己的锁,但这时锁过期了,最后还是释放了别人的锁
// 原子操作
if(get lock == A) {
    // set lock B
    del lock
}
1
2
3
4
5
  1. Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办?

Redis + lua 脚本实现

https://blog.csdn.net/feiying0canglang/article/details/113258494

# Redisson 实现分布式锁

我们肯定使用人家写好的来帮助我们开发哈

Redisson是一个Java操作 redis的客户端,提供了大量的分布式数据集来简化对redis的操作和使用,可以让开发者像使用本地集合一样使用redis,完全感受不到redis的存在

https://gitee.com/dyl_guang/redisson#https://gitee.com/link?target=https%3A%2F%2Fredisson.org%2Fdocs%2Fgetting-started%2F

https://github.com/redisson/redisson#quick-start

https://redisson.pro/docs/getting-started/

添加依赖,编写RedissonConfig文件。然后是对Redisson的测试,对Redis数据进行增删改。

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.19.1</version>
</dependency>  
1
2
3
4
5

RedissonConfig


/**
 * Redisson 配置
 */
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

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

RedissonTest


@SpringBootTest
public class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

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

        list.remove(0);

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

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

        RMap<Object, Object> map1 = redissonClient.getMap("test-map");

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

# 分布式锁的实现

# 定时任务 + 锁

  1. waitTime 设置为 0,只抢一次,抢不到就放弃
  2. 注意释放锁要写在 finally 中

PreCacheJob

/**
 * @author: shayu
 * @date: 2022/12/11
 * @ClassName: yupao-backend01
 * @Description: 数据预热
 */

@Component
@Slf4j
public class PreCacheJob {

    @Resource
    private UserService userService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private RedissonClient redissonClient;
    // 重点用户
    private List<Long> mainUserList = Arrays.asList(1L);

    // 每天执行,预热推荐用户
    @Scheduled(cron = "0 12 1 * * *")   //自己设置时间测试
    public void doCacheRecommendUser() {
        RLock lock = redissonClient.getLock("shayu:precachejob:docache:lock");

        try {
            // 只有一个线程能获取到锁
            if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
                System.out.println("getLock: " + Thread.currentThread().getId());
                for (Long userId : mainUserList) {
                    //查数据库
                    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                    Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
                    String redisKey = String.format("shayu:user:recommend:%s", mainUserList);
                    ValueOperations valueOperations = redisTemplate.opsForValue();
                    //写缓存,30s过期
                    try {
                        valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
                    } catch (Exception e) {
                        log.error("redis set key error", e);
                    }
                }
            }
        } catch (InterruptedException e) {
            log.error("doCacheRecommendUser error", e);
        } finally {
            // 只能释放自己的锁
            if (lock.isHeldByCurrentThread()) {
                System.out.println("unLock: " + Thread.currentThread().getId());
                lock.unlock();
            }
        }

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

测试一下分布式,三个服务程序争抢锁(这里指缓存数据查询程序)。

(提醒:我们这里是用定时任务触发抢锁,所以定时任务时间先设定好,大概延后几分钟就行,下面的操作包括 设定定时任务时间---打包---启动三个服务---到时间抢锁其中一个服务抢到锁控制台回馈)

maven-package打包,生成target文件。cd进去相应文件夹。

在使用Java命令设置端口启动项目。java -jar .\yupao-backend-0.0.1-SNAPSHOT.jar --server.port=8081(8081端口);开启三个服务。

# 组队功能

# 退出

最近更新: 6/9/2025, 8:55:27 AM
失败才是人生的主旋律   |