CoCo 伙伴匹配系统

需求分析

  1. 用户去添加标签,标签的分类(要有哪些标签,怎么把标签进行分类)
  2. 主动搜索:允许用户根据标签去搜索其他用户
  3. Redis缓存
  4. 组队
  5. 创建队伍
  6. 加入队伍
  7. 根据标签查询队伍
  8. 邀请其他人
  9. 允许用户去修改标签
  10. 推荐
  11. 相似度计算算法+本地分布式计算

技术栈

前端

  1. Vue3开发框架(提高页面开发的效率)
  2. Vant UI(基于Vue的移动端组件库) (React版Zent)
  3. Nginx来单机部署

后端

  • Java编程语言+ SpringBoot框架
  • SpringMVC + Mybatis + Mybatis Plus
  • MySQL 数据库
  • Redis缓存
  • Swagger + Knife4j接口文档

前端初始化

官方文档:开始 {#getting-started} | Vite中文网
安装插件:npm i unplugin-vue-components -D
安装vant:npm i vant


数据库表设计

新增标签表(分类表)

建议用标签,不要用分类,更灵活
性别:男、女
方向:Java、C++、Go、前端
正在学:Spring
目标:考研、春招、秋招、社招、考公、竞赛(蓝桥杯)、转行、跳槽
段位:初级、中级、高级、王者
身份:小学、初中、高中、大一、大二、大三、大四、学生、待业、已就业、研一、研二、研三
状态:乐观、有点丧、一般、单身、已婚、有对象

SQL语言分类

DDL: define建表、操作表
DML:manage 更新删除数据,影响实际表里的内容
DCL:control 控制,权限
DQL:query 查询

用户表user字段

  • id int 主键
  • 标签名 varchar 非空(必须唯一,唯一索引)
  • 上传标签的用户 userId int (如果要根据userId查已上传标签的话,最好加上普通索引)
  • 父标签id parentId int
  • 是否为父标签 isParent, tinyint(0不是父标签,1是父标签)
  • 创建时间 createTime, dateTime
  • 更新时间 updateTime, dateTime
  • 是否删除 isDelete, tinyint(0,1)
-- 用户表
create table user
(
  username     varchar(256) null comment '用户昵称',
  id           bigint auto_increment comment 'id'
  primary key,
  userAccount  varchar(256) null comment '账号',
  avatarUrl    varchar(1024) null comment '用户头像',
  gender       tinyint null comment '性别',
  userPassword varchar(512)       not null comment '密码',
  phone        varchar(128) null comment '电话',
  email        varchar(512) null comment '邮箱',
  userStatus   int      default 0 not null comment '状态 0 - 正常',
  createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
  updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
  isDelete     tinyint  default 0 not null comment '是否删除',
  userRole     int      default 0 not null comment '用户角色 0 - 普通用户 1 - 管理员',
  planetCode   varchar(512) null comment '星球编号',
  tags         varchar(1024) null comment '标签 json 列表'
) comment '用户';

后端初始化

搜索标签

  1. 允许用户传入多个标签,多个标签都存在才搜索出来 and。like ‘%java%’ and ‘%C++’
  2. 允许用户传入多个标签,有任何一个标签存在就能搜索出来 or。like ‘%java%’ or ‘%C++’

两种方式

  1. SQL查询
/**
 * 根据标签搜索用户(SQL 查询版)
 *
 * @param tagNameList 用户要拥有的标签
 */
@Deprecated
private List<User> searchUsersByTagsBySQL(List<String> tagNameList) {
   
    if (CollectionUtils.isEmpty(tagNameList)) {
   
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    // 拼接 and 查询
    // like '%Java%' and like '%Python%'
    for (String tagName : tagNameList) {
   
        queryWrapper = queryWrapper.like("tags", tagName);
    }
    List<User> userList = userMapper.selectList(queryWrapper);
    return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
}
  1. 内存查询
/**
 * 根据标签搜索用户(内存过滤)
 *
 * @param tagNameList 用户要拥有的标签
 * @return
 */
@Override
public List<User> searchUsersByTags(List<String> tagNameList) {
   
    if (CollectionUtils.isEmpty(tagNameList)) {
   
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    // 1. 先查询所有用户
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    List<User> userList = userMapper.selectList(queryWrapper);
    Gson gson = new Gson();
    // 2. 在内存中判断是否包含要求的标签
    return userList.stream().filter(user -> {
   
        String tagsStr = user.getTags();
        // 如果用户没有标签,返回false
        if (StringUtils.isBlank(tagsStr)) {
   
            return false;
        }
        // json转为java对象
        Set<String> tempTagNameSet = gson.fromJson(tagsStr, new TypeToken<Set<String>>() {
   
        }.getType());
        tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
        for (String tagName : tagNameList) {
   
            if (!tempTagNameSet.contains(tagName)) {
   
                return false;
            }
        }
        return true;
    }).map(this::getSafetyUser).collect(Collectors.toList());
}

后端整合 Swagger + Knife4j 接口文档

什么是接口文档?

  • 写接口信息的文档,每条接口包括:
  • 请求参数
  • 响应参数
    • 错误码
  • 接口地址
  • 请求类型
  • 请求格式
  • 备注

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

为什么需要接口文档?

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

怎么做接口文档?

  • 手写(Markdown等)
  • 自动化接口文档生成:自动根据项目代码生成完整的文档或在线测试的网页。

接口文档有哪些技巧?
Swagger原理:

  1. 引入依赖
<!--添加swagger的依赖-->
<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-spring-boot-starter</artifactId>
  <version>2.0.7</version>
</dependency>
  1. 自定义Swagger配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
 * 自定义 Swagger 接口文档的配置
 *
 */
@Configuration
@EnableSwagger2WebMvc
@Profile({
   "dev", "test"})
public class SwaggerConfig {
   

    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
   
        return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        // 这里一定要标注你控制器的位置
        .apis(RequestHandlerSelectors.basePackage("com.itcam.cocomate.controller"))
        .paths(PathSelectors.any())
        .build();
    }


    /**
     * api 信息
     * @return
     */
    private ApiInfo apiInfo() {
   
        return new ApiInfoBuilder()
        .title("伙伴匹配")
        .description("CoCo匹配接口文档")
        .termsOfServiceUrl("https://github.com/liyupi")
        .contact(new Contact("cammy","https://github.com/cammy","xxx@qq.com"))
        .version("1.0")
        .build();
    }
}
spring:
  # 支持 swagger3
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  1. 定义需要生成接口文档的代码位置(在第2步配置文件中实现了)
  2. 线上环境不要把接口文档暴露出去,通过SwaggerConfig配置文件开头加上@profile({“dev”,“test”})限制配置仅在部分环境开启(在第2步配置文件中实现了)
  3. 可以通过在 controller 方法上添加 @Api、@ApiImplicitParam(name = “name”,value = “姓名”,required =true) @ApiOperation(value = “向客人问好”) 等注解来自定义生成的接口描述信息
@RestController
@RequestMapping("/user")
@Api(tags = "用户接口文档")
public class UserController {
   

    @Resource
    private UserService userService;

    /**
     * 用户注册
     *
     * @param userRegisterRequest
     * @return
     */
    @ApiOperation("用户注册")
    @PostMapping("/register")
    public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
   
        if (userRegisterRequest == null) {
   
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        String userAccount = userRegisterRequest.getUserAccount();
        String userPassword = userRegisterRequest.getUserPassword();
        String checkPassword = userRegisterRequest.getCheckPassword();
        String planetCode = userRegisterRequest.getPlanetCode();
        if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword, planetCode)) {
   
            return null;
        }
        long result = userService.userRegister(userAccount, userPassword, checkPassword, planetCode);
        return ResultUtils.success(result);
    }

    /**
     * 用户登录
     *
     * @param userLoginRequest
     * @param request
     * @return
     */
    @ApiOperation("用户登录")
    @PostMapping("/login")
    public BaseResponse<User> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
   
        if (userLoginRequest == null) {
   
            return ResultUtils.error(ErrorCode.PARAMS_ERROR);
        }
        String userAccount = userLoginRequest.getUserAccount();
        String userPassword = userLoginRequest.getUserPassword();
        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
   
            return ResultUtils.error(ErrorCode.PARAMS_ERROR);
        }
        User user = userService.userLogin(userAccount, userPassword, request);
        return ResultUtils.success(user);
    }

    /**
     * 用户注销
     *
     * @param request
     * @return
     */
    @ApiOperation("用户注销")
    @PostMapping("/logout")
    public BaseResponse<Integer> userLogout(HttpServletRequest request) {
   
        if (request == null) {
   
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        int result = userService.userLogout(request);
        return ResultUtils.success(result);
    }

    /**
     * 获取当前用户
     *
     * @param request
     * @return
     */
    @ApiOperation("获取当前用户")
    @GetMapping("/current")
    public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
   
        Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
        User currentUser = (User) userObj;
        if (currentUser == null) {
   
            throw new BusinessException(ErrorCode.NOT_LOGIN);
        }
        Long userId = currentUser.getId();
        // 校验用户是否合法
        User user = userService.getById(userId);
        User safetyUser = userService.getSafetyUser(user);
        return ResultUtils.success(safetyUser);
    }

    /**
     * 用户查询
     *
     * @param username
     * @return
     */
    @ApiOperation("用户查询")
    @GetMapping("/search")
    public BaseResponse<List<User>> searchUsers(String username, HttpServletRequest request) {
   
        if (!isAdmin(request)) {
   
            throw new BusinessException(ErrorCode.NO_AUTH, "缺少管理员权限");
        }
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if (StringUtils.isNotBlank(username)) {
   
            queryWrapper.like("username", username);
        }
        List<User> userList = userService.list(queryWrapper);
        List<User> list = userList.stream().map(userService::getSafetyUser).collect(Collectors.toList());
        return ResultUtils.success(list);
    }

    /**
     * 用户删除
     *
     * @param id
     * @param request
     * @return
     */
    @ApiOperation("用户删除")
    @PostMapping("/delete")
    public BaseResponse<Boolean> deleteUser(@RequestBody Long id, HttpServletRequest request) {
   
        if (!isAdmin(request)) {
   
            return null;
        }
        if (id <= 0) {
   
            return null;
        }
        boolean b = userService.removeById(id);
        return ResultUtils.success(b);
    }


    /**
     * 判断用户是否为管理员
     *
     * @param request
     * @return
     */
    private boolean isAdmin(HttpServletRequest request) {
   
        // 仅管理员可查询
        Object userAttribute = request.getSession().getAttribute(USER_LOGIN_STATE);
        User user = (User) userAttribute;
        return user != null && user.getUserRole() == ADMIN_ROLE;
    }
}

image.png

存量用户信息导入及同步(爬虫)

看上了网页信息,怎么抓取?

  1. 分析原网站是怎么获取这些数据的?哪个接口?
curl "https://api.zsxq.com/v2/groups/51122858222824/topics?scope=questions&count=20" ^
  -H "accept: application/json, text/plain, */*" ^
  -H "accept-language: zh-CN,zh;q=0.9" ^
  -H ^"cookie: ***" ^
  -H "origin: https://wx.zsxq.com" ^
  -H "priority: u=1, i" ^
  -H "referer: https://wx.zsxq.com/" ^
  -H ^"sec-ch-ua: ^\^"Not)A;Brand^\^";v=^\^"99^\^", ^\^"Google Chrome^\^";v=^\^"127^\^", ^\^"Chromium^\^";v=^\^"127^\^"^" ^
  -H "sec-ch-ua-mobile: ?0" ^
  -H ^"sec-ch-ua-platform: ^\^"Windows^\^"^" ^
  -H "sec-fetch-dest: empty" ^
  -H "sec-fetch-mode: cors" ^
  -H "sec-fetch-site: same-site" ^
  -H "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" ^
  1. 用程序取调用接口
  2. 处理(清洗)一下数据,之后就可以写到数据库里

流程

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

EsayExcel

官方文档:EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网

引入依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcel</artifactId>
  <version>3.1.0</version>
</dependency>

Session共享

中session的时候注意范围

server:
  servlet:
    session:
      cookie:
        domain:
/**
 * 根据标签查询用户
 *
 * @param tagNameList
 * @return
 */
@ApiOperation("根据标签查询用户")
@GetMapping("/search/tags")
public BaseResponse<List<User>> searchUsersByTags(@RequestParam(required = false) List<String> tagNameList) {
   
    if (CollectionUtils.isEmpty(tagNameList)) {
   
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
    }
    List<User> userList = userService.searchUsersByTags(tagNameList);
    return ResultUtils.success(userList);
}

Session共享实现

  1. 安装Redis

官方文档:

  1. 引入Redis
<!-- 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>#{redis-version}</version>
</dependency>
  1. 引入Redis和spring-session的整合,是的session存储到redis中
<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
  <groupId>org.springframework.session</groupId>
  <artifactId>spring-session-data-redis</artifactId>
  <version>#{redis-version}</version>
</dependency>
  1. 修改spring-session存储配置spring.session.store-type
    1. 默认是none,标识存储在单台服务器
spring:
  # session 配置
  session:
    # 开启分布式 session
    store-type: redis

后端用户修改页面

/**
 * 更新用户信息
 *
 * @param user
 * @param request
 * @return
 */
@ApiOperation("更新用户信息")
@PostMapping("/update")
public BaseResponse<Integer> updateUser(@RequestBody User user, HttpServletRequest request) {
   
// 校验参数是否为空
if (user == null) {
   
    throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getCurrentUser(request);
int result = userService.updateUser(user, loginUser);
return ResultUtils.success(result);
}
/**
 * 获取当前登录用户信息
 * @return
 */
User getCurrentUser(HttpServletRequest request);

/**
 * 更新用户信息
 * @param user
 * @return
 */
int updateUser(User user, User loginUser);


/**
 * 是否为管理员
 *
 * @param request
 * @return
 */
boolean isAdmin(HttpServletRequest request);

/**
 * 是否为管理员
 *
 * @param loginUser
 * @return
 */
boolean isAdmin(User loginUser);
/**
 * 获取当前登录用户信息
 *
 * @param request
 * @return
 */
@Override
public User getCurrentUser(HttpServletRequest request) {
   
if (request == null) {
   
    throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = (User) request.getSession().getAttribute(USER_LOGIN_STATE);
if (user == null) {
   
    throw new BusinessException(ErrorCode.NOT_LOGIN);
}
return user;
}

/**
 * 更新用户信息
 *
 * @param user
 * @param loginUser
 * @return
 */
@Override
public int updateUser(User user, User loginUser) {
   
    // 1.获取用户id
    long userId 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值