面试目的:学习Java8特性、接口文档、网页内容抓取、分布式登录、大数据量导入、并发编程、Redis、缓存及预热、定时任务、分布式锁、幂等性、算法、免备案上线项目等重要知识
伙伴匹配系统项目学习01
项目介绍:帮助大家找到志同道合的伙伴。移动端H5,大概兼容一下PC。
需求分析
- 用户去添加标签,标签分类(有哪些白鸥去、怎么把标签分类) 学习方向 Java/C++,工作/大学
- 主动搜索:允许用户根据标签去搜索其他用户
- Redis 缓存
- 组队
- 创建队伍
- 加入队伍
- 根据标签查询队伍
- 邀请其他人
- 允许用户去修改标签
- 推荐
- 相似度计算算法+本地分布式计算
技术栈
前端
- Vue 3开发框架(提高页面开发的效率)
- Vant UI(基于Vue的移动端组件库)组件库(React版本叫Zent)
- Vite(打包工具,速度快)
- Nginx单机部署
后端
- Java编程语言+SpringBoot框架
- SpringMVC + MyBatis + MyBatis Plus
- MySQL数据库
- Redis缓存
- Swagger + Knife4j接口文档
计划
- 前端项目初始化 ✅ 2026-03-06
- 前端主页 + 组件概览
- 数据库表设计
- 标签表
- 用户表
- 后端 - 根据标签搜索用户
- 前端 - 根据标签搜索用户
前端项目初始化(Vue3)
使用Vite脚手架初始化项目
输入 pnpm create vite 选择 vue TypeScript等内容创建初始项目
基本项目结构:
public 是公共资源
components 用来放组件
App.vue是项目入口
package.json 同react 管理项目的依赖和启动脚本
添加vant组件
pnpm add vant添加按需引入插件:
pnpm add unplugin-vue-components -D根据官方文档配置:
import vue from '@vitejs/plugin-vue';import AutoImport from 'unplugin-auto-import/vite';import Components from 'unplugin-vue-components/vite';import { VantResolver } from '@vant/auto-import-resolver';
export default { plugins: [ vue(), AutoImport({ resolvers: [VantResolver()], }), Components({ resolvers: [VantResolver()], }), ],};接下来就可以直接使用组件了:
<van-button type="primary">主要按钮</van-button><van-button type="success">成功按钮</van-button><van-button type="default">默认按钮</van-button><van-button type="danger">危险按钮</van-button><van-button type="warning">警告按钮</van-button>
前端主页 + 组件概览
写前端页面之前最重要的一件事情就是应该先想清楚你要做的页面长啥样。 确认好之后,再开始写代码。
主页设计:
导航条:展示当前页面名称 主页搜索框 => 搜索页(标签筛选页) 内容 tab栏:
- 主页(推荐页+广告)
- 搜索框
- banner
- 推荐信息流
- 队伍页
- 用户页(消息 - 暂时考虑发邮件)
开发:
很多页面要复用组件 / 样式,重复写很麻烦、不利于维护,因此抽象一个通用的布局(Layout)
创建Layout文件夹 创建BasicLayout.vue 基本布局文件 书写基本布局代码
创建两个页面 根据选中内容切换页面
代码:
<script setup lang="ts">import { ref } from 'vue';import Index from '../pages/index.vue';import Team from '../pages/team.vue';
const onClickLeft = () => history.back();//@ts-ignoreconst onClickRight = () => showToast('提示内容');
const active = ref('index');//@ts-ignoreconst onChange = (index: number) => showToast({ message: `标签 ${index}`, position: 'bottom',});
</script>
<template> <van-nav-bar fixed 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-else-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>
<style scoped></style>数据库表设计
标签的分类
新增标签表(分类表)
使用标签比较灵活
思考有哪些分类:
性别:男、女 方向:Java、C++、Go、前端 目标:考研、春招、秋招、社招、考公、竞赛(蓝桥杯)、转行、跳槽 段位:初级、中级、高级、王者 身份:大一、大二、大三、大四、学生、待业、已就业、研一、研二、研三 状态:乐观、有点丧、一般、单身、已婚、有对象 【为了可以用户自定义标签 所以才需要表 否则可以直接写死】
字段: id bigint 主键 标签名 varchar 非空 (加唯一索引) 上传标签的用户 userId int (普通索引) 父标签id parentId int(标签分类) 是否为父标签 isParent tinyint (0、1) 创建时间 createTime datatime 关系时间 updateTime datatime 是否删除 isDelete tinyint (0、1)
思考需求: 怎么查询所有标签,并分好组? 一次全量查询,然后根据父id分类 根据父标签查询子标签?根据id查询
确认需求可完成 再确定这个表可行
SOL 语言分类: DDL define 建表、操作表 DML manage更新删除数据,影响实际表里的内容 DCL control控制,权限 DQL query 查询
修改用户表
1.直接在用户表补充tags字段,[Java',男]存json字符串
优点:查询方便、不用新建关联表,标签是用户的固有属性(除了该系统、其他系统可能要用到,标签是用户的固有属性) 节省开发成本
查询用户列表,查关系表拿到这100个用户有的所有标签id,,再根据标签id去查标签表。
性能低可以用缓存缓解。
缺点:用户表多一列,可能会导致原先的一些内容需要修。
2.加一个关联表,记录用户和标签的关系 关联表的应用场景:查询灵活,可以正查反查缺点:要多建一个表、多维护一个表 重点:企业大项目开发中尽量减少关联查询,会影响扩展性和性能。
开发后端接口
搜索标签
- 允许用户传入多个标签,多个标签都存在才搜索出来 and。 like ‘%Java%’ and like ‘%C++%’
- 允许用户传入多个标签,有任何一个标签存在就能搜索出来or。 like ‘%java%’ or like ‘%C++%’
两种方式:
- SQL查询
- 内存查询 方案选择:
- 如果参数可以分析,根据用户的参数去选择查询方式,比如标签数
- 如果参数不可分析,并且数据库连接足够、内存空间足够,可以并发同时查询,谁先返回用谁。
- 还可以 SQL 查询与内存计算相结合,比如先用 SQL 过滤掉部分 tag
建议通过实际测试来分析哪种查询比较快,数据量大的时候验证效果更明显!
解析 JSON 字符串
序列化:java 对象转成 json
反序列化:把 json 转为 java 对象
java json 序列化库有很多:
- gson(google的,推荐)
- fastjson alibaba(阿里出品,快,但是漏洞太多)
- jackson
- kryo(性能极高的序列化库)
开发步骤:
- 拿之前的项目复制一份并ctrl+shift+H 替换项目名称
- 创建tag表,并且修改用户表添加字段tags
- 生成tag数据库的代码(使用MyBatis-plus插件)
- 修改原有的用户对象加入tags,修改UserService给SafytyUser加入tags字段
- 新增方法getUserByTags:
public List<User> getUserByTags(List<String> tagNameList) { if (CollectionUtils.isEmpty(tagNameList)) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } // 方式1:直接使用SQL查询 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); for (String tagName : tagNameList) { queryWrapper = queryWrapper.like("tags", tagName); } List<User> users = userMapper.selectList(queryWrapper); return users.stream().map(this::getSafetyUser).toList();
// 方式2:全量查询或先传入一个常用标签来查询 然后在内存中判断 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); List<User> users = userMapper.selectList(queryWrapper);
return users.stream().filter(user->{ String tags = user.getTags(); if(StringUtils.isBlank(tags)) return false; // 使用Set可以在O(1)时间判断是否存在 Set<String> tempTags = gson.fromJson(tags, new TypeToken<Set<String>>() { }.getType()); for (String tagName : tagNameList) { if(!tempTags.contains(tagName)){ return false; } } return true; }).map(this::getSafetyUser).toList();}这里写了两种方式,可以根据情况,哪种方式快用哪种。
拓展: 并行流:stream可以换成parallelStream来变成并行操作 使用默认线程池forkjoin 要注意 如果有其他地方把线程池里的线程用完了,可能导致这边卡住 因为是用的公共线程池
return users.parallelStream().filter(user->{ String tags = user.getTags(); if(StringUtils.isBlank(tags)) return false; // 使用Set可以在O(1)时间判断是否存在 Set<String> tempTags = gson.fromJson(tags, new TypeToken<Set<String>>() { }.getType()); for (String tagName : tagNameList) { if(!tempTags.contains(tagName)){ return false; } } return true;}).map(this::getSafetyUser).toList();另外要注意对set集合判空 前面的代码没判空可能出现异常: 下面使用Optional.orNullable判空
return users.stream().filter(user->{ String tags = user.getTags(); if(StringUtils.isBlank(tags)) return false; // 使用Set可以在O(1)时间判断是否存在 Set<String> tempTags = gson.fromJson(tags, new TypeToken<Set<String>>() { }.getType()); // 使用Optional判空 tempTags = Optional.ofNullable(tempTags).orElse(new HashSet<>());
for (String tagName : tagNameList) { if(!tempTags.contains(tagName)){ return false; } } return true; }).map(this::getSafetyUser).toList();}Java 8 新特性:
- stream / parallelStream 流式处理
- Optional 可选类
伙伴匹配系统项目学习02
主要内容:
- 标签搜索接口调试
- 前端整合路由,简单介绍原理
- 前端页面开发,搜索页、用户页、用户修改页。(这里做的慢了些,大家可以倍速观看)
前端整合路由
Vue-Router:https://router.vuejs.org/zh/guide/#html,直接看官方文档引入。
Vue-Router 其实就是帮助你根据不同的 url 来展示不同的页面(组件),不用自己写 if / else。
路由配置影响整个项目,所以建议单独用 config 目录、单独的配置文件去集中定义和管理。
有些组件库可能自带了和 Vue-Router 的整合,所以尽量先看组件文档、省去自己写的时间。
主要内容:
- Swagger + Knife4j 接口文档整合
- 分析星球接口以及后端 Excel 处理(大家可以倍速观看)
后端整合 Swagger + Knife4j 接口文档
什么是接口文档?写接口信息的文档。
每个接口的信息包括:
- 请求参数
- 响应参数
- 错误码
- 接口地址
- 接口名称
- 请求类型
- 请求格式
- 备注
谁用接口文档?
答:一般是后端或者负责人来提供,后端和前端都要使用。
为什么需要接口文档?
- 有个书面内容(背书或者归档),便于大家参考和查阅,便于 沉淀和维护 ,拒绝口口相传
- 接口文档便于前端和后端开发对接,前后端联调的 介质 。后端 => 接口文档 <= 前端
- 好的接口文档支持在线调试、在线测试,可以作为工具提高我们的开发测试效率
怎么做接口文档?
- 手写:比如腾讯文档、Markdown 笔记
- 自动化接口文档生成:自动根据项目代码生成完整的文档或在线调试的网页。Swagger、Postman(侧重接口管理)(国外);apifox、apipost、eolink(国产)
使用 Swagger
- 引入依赖(Swagger 或 Knife4j:https://doc.xiaominfo.com/knife4j/documentation/get_start.html)
- 自定义 Swagger 配置类
- 定义需要生成接口文档的代码位置(Controller)
- 千万注意:线上环境不要把接口暴露出去!!!可以通过在 SwaggerConfig 配置文件开头加上
@Profile({"dev", "test"})限定配置仅在部分环境开启 - 启动即可
- 可以通过在 controller 方法上添加 [@Api、@ApiImplicitParam(name ](/Api、@ApiImplicitParam(name ) = “name”,value = “姓名”,required = true) [@ApiOperation(value ](/ApiOperation(value ) = “向客人问好”) 等注解来自定义生成的接口描述信息
如果 springboot version >= 2.6,需要添加如下配置:
spring: mvc: pathmatch: matching-strategy: ANT_PATH_MATCHER用户信息导入及同步
计划:
- 把所有星球用户的信息导入
- 把写了自我介绍的同学的标签信息导入
怎么抓取网页内容?
推荐安装 FeHelper 前端辅助插件。
1)分析原网站是怎么获取这些数据的?哪个接口?
按 F12 打开控制台,查看网络请求,复制 curl 代码便于查看和执行,比如:
curl "https://api.zsxq.com/v2/hashtags/48844541281228/topics?count=20" ^ -H "authority: api.zsxq.com" ^ -H "accept: application/json, text/plain, */*" ^ -H "accept-language: zh-CN,zh;q=0.9" ^ -H "cache-control: no-cache" ^ -H "origin: https://wx.zsxq.com" ^ -H "pragma: no-cache" ^ -H "referer: https://wx.zsxq.com/" ^ --compressed2) 用程序去调用接口 (java okhttp httpclient / python request 库等都可以)
3)处理(清洗)一下数据,之后就可以写到数据库里
导入流程
- 从 excel 中导入全量用户数据并去重(使用 Easy Excel 库来实现)
- 抓取写了自我介绍的同学信息,提取出用户昵称、用户唯一 id、自我介绍信息
- 从自我介绍中提取信息,然后写入到数据库中
EasyExcel
地址:https://alibaba-easyexcel.github.io/index.html
两种读对象的方式:
- 确定表头:建立对象,和表头形成映射关系
- 不确定表头:每一行数据映射为 Map<String, Object>
两种读取模式:
- 监听器:先创建监听器、在读取文件时绑定监听器。单独抽离处理逻辑,代码清晰易于维护;一条一条处理,适用于数据量大的场景。
- 同步读:无需创建监听器,一次性获取完整数据。方便简单,但是数据量大时会有等待时常,也可能内存溢出。
伙伴匹配系统项目学习快速梳理
47. 组队功能 | 基础接口开发及测试(三)
修改查询队伍列表功能
- 做需求分析
- 根据需求创建VO对象
- 书写listTeams方法(service)构建查询条件,然后查询返回