找回密码
 立即注册
首页 业界区 安全 伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Bo ...

伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04

孜稞 2025-8-16 09:50:46
@
目录

  • 伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04
  • 补充:问题:CORS ,跨域问题
  • 缓存预热

    • 怎么缓存预热,预热操作
    • 控制定时任务的执行

    • 分布式锁实现的关键
    • Redisson-实现分布式锁

  • 组队

    • 数据库表设计

  • 前端不同页面传递数据
  • 最后:

伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04

1.gif

项目地址:

  • Github:https://github.com/China-Rainbow-sea/yupao
  • Gitee:https://gitee.com/Rainbow--Sea/yupao
补充:问题:CORS ,跨域问题

我这边的解决方法是:
2.png
  1. myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie
复制代码
整体的:
  1. import axios from "axios";
  2. // axios.defaults.withCredentials = true; // 允许携带凭证
  3. // const isDev = process.env.NODE_ENV === 'development';
  4. // 创建实例时配置默认值
  5. const myAxios = axios.create({
  6.     LookupAddress: undefined, LookupAddressEntry: undefined,
  7.     baseURL: 'http://localhost:8080/api'
  8. });
  9. // const myAxios: AxiosInstance = axios.create({
  10. //     baseURL: isDev ? 'http://localhost:8080/api' : '线上地址',
  11. // });
  12. myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie
  13. // 创建实例后修改默认值
  14. // 添加请求拦截器
  15. myAxios.interceptors.request.use(function (config) {
  16.     // 在发送请求之前做些什么
  17.     console.log('我要发请求了')
  18.     return config;
  19. }, function (error) {
  20.     // 对请求错误做些什么
  21.     return Promise.reject(error);
  22. });
  23. // 添加响应拦截器
  24. myAxios.interceptors.response.use(function (response) {
  25.     // 2xx 范围内的状态码都会触发该函数。
  26.     // 对响应数据做点什么
  27.     console.log('我收到你的响应了',response)
  28.     return response.data;
  29. }, function (error) {
  30.     // 超出 2xx 范围的状态码都会触发该函数。
  31.     // 对响应错误做点什么
  32.     return Promise.reject(error);
  33. });
  34. // Add a request interceptor
  35. // myAxios.interceptors.request.use(function (config) {
  36. //     console.log('我要发请求啦', config)
  37. //     // Do something before request is sent
  38. //     return config;
  39. // }, function (error) {
  40. //     // Do something with request error
  41. //     return Promise.reject(error);
  42. // });
  43. //
  44. //
  45. // // Add a response interceptor
  46. // myAxios.interceptors.response.use(function (response) {
  47. //     console.log('我收到你的响应啦', response)
  48. //     // 未登录则跳转到登录页
  49. //     if (response?.data?.code === 40100) {
  50. //         const redirectUrl = window.location.href;
  51. //         window.location.href = `/user/login?redirect=${redirectUrl}`;
  52. //     }
  53. //     // Do something with response data
  54. //     return response.data;
  55. // }, function (error) {
  56. //     // Do something with response error
  57. //     return Promise.reject(error);
  58. // });
  59. export default myAxios;
复制代码
后端配置:
3.png

在 Spring Boot 中,可以通过在配置类中添加 @CrossOrigin 注解或实现 WebMvcConfigurer 接口并重写 addCorsMappings 方法来允许特定来源的跨域请求:
  1. package com.rainbowsea.yupao.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.servlet.config.annotation.CorsRegistry;
  4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  5. /**
  6. * 跨域配置
  7. *
  8. */
  9. @Configuration
  10. public class WebMvcConfg implements WebMvcConfigurer {
  11.     @Override
  12.     public void addCorsMappings(CorsRegistry registry) {
  13.         //设置允许跨域的路径
  14.         registry.addMapping("/**")
  15.                 //设置允许跨域请求的域名
  16.                 //当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
  17.                 .allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http" +
  18.                         "://127.0.0.1:8083","http://127.0.0.1:8080","http://127.0.0.1:5173")
  19.                 //是否允许证书 不再默认开启
  20.                 .allowCredentials(true)
  21.                 //设置允许的方法
  22.                 .allowedMethods("*")
  23.                 //跨域允许时间
  24.                 .maxAge(3600);
  25.     }
  26. }
复制代码
相关博客链接:

  • https://blog.csdn.net/yuanlong12178/article/details/147143201 参考该 blog 解决的
  • https://blog.csdn.net/xhmico/article/details/122338365 这篇也不错。
缓存预热

缓存预热:问题:第一个用户访问还是很慢(加入第一个老板),比如:双十一,第一次就是很多用户呢,也能一定程度上保护数据库。
缓存预热的优点:

  • 解决上面的问题,可以让用户始终访问很快。
缺点:

  • 增加了开发成本,访问人数不多。(你要额外的开发,设计)
  • 预热的时机和时间如果错了,有可能你缓存的数据不对或者数据太旧了
  • 需要占用空间。拿空间换时间。
分析优缺点的时候,要打开思路,从整个项目从 0 到 1 的链路上分析。
怎么缓存预热,预热操作

两种方式:

  • 定时任务预热。
  • 模拟触发(手动触发)
这里我们采用定时任务预热
定时任务实现:

  • Spring Scheduler (Spring Boot 默认整合了)
  • Quartz (独立于 Spring 存在的定时任务框架)
  • XXL-Job 之类的分布式任务调度平台(界面+sdk)
用定时任务,每天刷新所有用户的推荐列表
注意点:

  • 缓存预热的意义(新增少,总用户多)
  • 缓存的空间不能太大,要预留给其他缓存空间
  • 缓存数据的周期(此处每天一次)
采用第一种方式:步骤:

  • 主类开启:@EnableScheduling
  • 给要定时执行的方法添加上 @Scheduling注解,指定 cron 表达式或者执行频率。
不需要去背 cron 表达式,用现成的工具即可:

  • https://cron.qqe2.com/
  • https://www.matools.com/crontab/
4.png
  1. import { defineConfig } from 'vite'
  2. import vue from '@vitejs/plugin-vue'
  3. // 导出配置对象,使用ES模块语法
  4. export default defineConfig({
  5.   plugins: [vue()], // 启用Vue插件
  6.   server: { // 注意:在Vite的新版本中,配置项`devServer`已更名为`server`
  7.     proxy: {
  8.       '/api': {
  9.         target: 'http://localhost:8080/api', // 目标服务器地址
  10.         changeOrigin: true, // 是否改变源
  11.         // 如果需要路径重写,可以取消以下行的注释
  12.         // pathRewrite: { 1'^/api': '' }
  13.       }
  14.     }
  15.   }
  16. });
复制代码
5.png
  1. server:
  2.   port: 8080
  3.   servlet:
  4.     context-path: /api
  5.     session:
  6.       cookie:
  7.         domain: localhost
  8.         secure: true
  9.         same-site: none # 上述方法不行,就配置上
  10. spring:
  11.     # session 失效时间
  12.   session:
  13.     timeout: 86400
  14.     store-type: redis
  15.   # Redis 配置
  16.   redis:
  17.     port: 6379
  18.     host: localhost
  19.     database: 1   
复制代码
控制定时任务的执行

要控制定时任务在同一时间只有 1 个 服务器能执行。
为什么呢?

  • 浪费资源,想象 1W 台服务器能执行。
  • 脏数据,比如重复插入。
怎么做?几种方案:

  • 分离定时任务程序和主程序,只在 1 个服务器运行定时任务,成本太大。
  • 写死配置,每个服务器都执行定时任务,但是只有 IP 符合配置的服务器才真实执行业务逻辑,其他的直接返回。成本最低;但是我们的 IP 可能不是固定的,把 IP 写死的方式太死了。
  • 动态配置:配置是可以轻松的,很方便地更新的(代码无需重启),但是只有 IP 符合配置的服务器才真实执行业务逻辑。可以使用

    • 数据库
    • Redis
    • 配置中心(Nacos,Apollo,Spring Cloud Config)

问题:服务器多了,IP 不可控还是很麻烦,还是要人工修改。

  • 分布式锁,只有抢到锁的服务器才能执行业务逻辑,坏处:增加成本;好处:就是不用手动配置,多少个服务器都一样。
注意:只要是单机,就会存在单点故障。


有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。
Java 实现锁:sychronized 关键字,并发包的类
但存在问题:只对单个 JVM 有效。
分布式锁实现的关键

枪锁机制:
怎么保证同一时间只有 1 个服务器能抢到锁?
核心思想:就是:先来的人先把数据改成自己的标识(服务器 IP),后来的人发现标识已存在,就抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人继续抢锁。
MySQL 数据库:select for update 行级锁(最简单),或者乐观锁
Redis 实现:内存数据库,读写速度快,支持 setnx,lua 脚本,比较方便我们实现分布式锁。
setnx:set if not exists 如果不存在,则设置;只有设置成功才会返回 true,否则返回 false。
分布式锁的注意事项:

  • 用完锁一定要释放锁
  • 一定要设置锁的过期时间,防止对应占用锁的服务器宕机了,无法释放锁。导致死锁。
  • 如果方法执行过长的话,锁被提前过期,释放了,怎么办。——续期
  1. boolean end = false;  // 方法没有结束的标志
  2. new Thread(() -> {
  3.     if (!end)}{  // 表示执行还没结束,续期
  4.     续期
  5. })
  6. end = true;  // 到这里,方法执行结束了
复制代码
问题:

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

  • 释放锁的时候(判断出是自己的锁了,准备执行释放的锁的过程中时,很巧的时,锁过期了,然后,这个时候就有一个新的东东插入进来了,这样锁就删除了别人的锁了)。
解决方案:Redis + lua 脚本保证操作原子性
  1. // 原子操作
  2. if(get lock == A) {
  3.     // set lock B
  4.     del lock
  5. }
复制代码
步骤:缓存预热数据:

  • 在项目启动类上,添加上 @EnableScheduling  注解。表示启动定时任务。
6.png

7.png

8.png


  • 编写具体要执行的定时任务,程序,这里我们名为 PreCacheJob
注意:这里我们使用上 注解,使用 cron 表达式表示一个定时时间上的设置
这里往下看,我们使用 Redisson 进行一个实现分布式锁的操作。
相关 Redissson 配置使用如下:
Redisson-实现分布式锁

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

  • Spring boot starter 引入(不推荐,因为版本迭代太快了,容易发生版本冲突):https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
  • 直接引入(推荐):https://github.com/redisson/redisson#quick-start
  1. <dependency>
  2.     <groupId>org.redisson</groupId>
  3.     redisson</artifactId>
  4.     <version>3.17.5</version>
  5. </dependency>
复制代码
使用 Ression

  • 配置 Ression 配置类:
9.png
  1. package com.rainbowsea.yupao.config;
  2. import lombok.Data;
  3. import org.redisson.Redisson;
  4. import org.redisson.api.RedissonClient;
  5. import org.redisson.config.Config;
  6. import org.springframework.boot.context.properties.ConfigurationProperties;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. /**
  10. * Redisson 配置
  11. *
  12. */
  13. @Configuration
  14. @ConfigurationProperties(prefix = "spring.redis")  // 同时这个获取到 application.yaml 当中前缀的配置属性
  15. @Data
  16. public class RedissonConfig {
  17.     private String host;
  18.     private String port;
  19.     @Bean
  20.     public RedissonClient redissonClient() {
  21.         // 1. 创建配置
  22.         Config config = new Config();
  23.         String redisAddress = String.format("redis://%s:%s", host, port);
  24.         config.useSingleServer().setAddress(redisAddress).setDatabase(3);
  25.         // 2. 创建实例
  26.         RedissonClient redisson = Redisson.create(config);
  27.         return redisson;
  28.     }
  29. }
复制代码

  • 操作 Redis : 测试是否,能操作 Redis ,通过 Ression
  1. // list,数据存在本地 JVM 内存中
  2. List<String> list = new ArrayList<>();
  3. list.add("yupi");
  4. System.out.println("list:" + list.get(0));
  5. list.remove(0);
  6. // 数据存在 redis 的内存中
  7. RList<String> rList = redissonClient.getList("test-list"); // redis操作的 key 的定义
  8. rList.add("yupi");
  9. System.out.println("rlist:" + rList.get(0));
  10. rList.remove(0);
复制代码
10.png
  1. package com.rainbowsea.yupao.service;
  2. import org.junit.jupiter.api.Test;
  3. import org.redisson.api.RList;
  4. import org.redisson.api.RedissonClient;
  5. import org.springframework.boot.test.context.SpringBootTest;
  6. import javax.annotation.Resource;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. @SpringBootTest
  10. public class RedissonTest {
  11.     @Resource
  12.     private RedissonClient redissonClient;
  13.     @Test
  14.     void test() {
  15.         // list 数据存在代 本地 JVM 内存中
  16.         List<String> list = new ArrayList<>();
  17.         list.add("yupi");
  18.         list.get(0);
  19.         System.out.println("list: " + list.get(0));
  20.         list.remove(0);
  21.         // 数据存入 redis 的内存中
  22.         RList<Object> rList = redissonClient.getList("test-list");  // 表示 redis 当中的 key
  23.         rList.add("yupi");
  24.         System.out.println("rlist:" + rList.get(0));
  25.     }
  26. }
复制代码
11.png

其中 Redisson 操作  Redis 当中的 set,map 都是一样的道理,就不多赘述了。
分布式锁保证定时任务不重复执行:
实现代码如下:
  1. void testWatchDog() {
  2.     RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
  3.     try {
  4.         // 只有一个线程能获取到锁
  5.         if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  6.             // todo 实际要执行的方法
  7.             doSomeThings();
  8.             System.out.println("getLock: " + Thread.currentThread().getId());
  9.         }
  10.     } catch (InterruptedException e) {
  11.         System.out.println(e.getMessage());
  12.     } finally {
  13.         // 只能释放自己的锁
  14.         if (lock.isHeldByCurrentThread()) { // 判断该锁是不是当前线程创建的锁
  15.             System.out.println("unLock: " + Thread.currentThread().getId());
  16.             lock.unlock();
  17.         }
  18.     }
  19. }
复制代码
注意:

  • waitTime 设置为 0,其他线程不会去等待这个锁的释放,就是抢到了就用,没抢到就不用了,只抢一次,抢不到就放弃。
  1. if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
复制代码

  • 注意释放锁要放到 finally 中,不然,发生了异常就被中断,无法释放锁了。
缓存预热,定时执行具体的任务的具体代码:
12.png
  1. @Scheduled(cron = "0 5 21 25 6 ?")
复制代码
13.png
  1. package com.rainbowsea.yupao.job;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  4. import com.rainbowsea.yupao.model.User;
  5. import com.rainbowsea.yupao.service.UserService;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.redisson.api.RLock;
  8. import org.redisson.api.RedissonClient;
  9. import org.springframework.data.redis.core.RedisTemplate;
  10. import org.springframework.data.redis.core.ValueOperations;
  11. import org.springframework.scheduling.annotation.Scheduled;
  12. import org.springframework.stereotype.Component;
  13. import javax.annotation.Resource;
  14. import java.util.ArrayList;
  15. import java.util.Arrays;
  16. import java.util.List;
  17. import java.util.concurrent.TimeUnit;
  18. /**
  19. * 缓存预热任务
  20. *
  21. */
  22. @Component
  23. @Slf4j
  24. public class PreCacheJob {
  25.     @Resource
  26.     private UserService userService;
  27.     @Resource
  28.     private RedisTemplate<String, Object> redisTemplate;
  29.     @Resource
  30.     private RedissonClient redissonClient;
  31.     // 重点用户
  32.     private List<Long> mainUserList = Arrays.asList(1L);
  33.     // 每天执行,预热推荐用户,每个月的 31号,0:00执行
  34.     @Scheduled(cron = "0 31 0 * * *")
  35.     public void doCacheRecommendUser() {
  36.         RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
  37.         try {
  38.             // 只有一个线程能获取到锁
  39.             if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  40.                 System.out.println("getLock: " + Thread.currentThread().getId());
  41.                 for (Long userId : mainUserList) {
  42.                     QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  43.                     Page<User> userPage = userService.page(new Page<>(1, 20), queryWrapper);
  44.                     String redisKey = String.format("yupao:user:recommend:%s", userId);
  45.                     ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
  46.                     // 写缓存
  47.                     try {
  48.                         valueOperations.set(redisKey, userPage, 30000, TimeUnit.MILLISECONDS);
  49.                     } catch (Exception e) {
  50.                         log.error("redis set key error", e);
  51.                     }
  52.                 }
  53.             }
  54.         } catch (InterruptedException e) {
  55.             log.error("doCacheRecommendUser error", e);
  56.         } finally {
  57.             // 只能释放自己的锁
  58.             if (lock.isHeldByCurrentThread()) {
  59.                 System.out.println("unLock: " + Thread.currentThread().getId());
  60.                 lock.unlock();
  61.             }
  62.         }
  63.     }
  64. }
复制代码
运行测试:
14.png

15.png
  1. \yupao\yupao-backend\target>java -jar .\yupao-backend-0.0.1-SNAPSHOT.jar --server.port=9090
复制代码
16.png

Redisson 看门狗机制:
Redisson 中提供的续期机制。
开一个监听线程,如果方法还没执行完,就帮你重置 Redis 锁的过期时间。
原理:

  • 监听当前线程, 默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
  • 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期。
为什么 Redisson 续期时间是 30 秒
因为方式 Redis 宕机了,就成了,占着茅坑不拉屎。
17.png

18.png

(Redis 如果是集群(而不是只有一个 Redis),如果分布式锁的数据不同步怎么办 )如果 Reids 分布式锁导致数据不一致的问题——> Redis 红锁。
组队

用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间PO。
队长、剩余的人数
聊天?
公开或private或加密
用户创建队伍最多5个
展示队伍列表,根据名称搜索队伍PO,信息流中不展示已过期的队伍
修改队伍信息PO~P1
用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是要有个上限PO
是否需要队长同意?筛选审批?
用户可以退出队伍(如果队长退出,权限转移给第二早加入的用户——先来后到)P1
队长可以解散队伍PO
分享队伍=>邀请其他用户加入队伍P1
业务流程:

  • 生成分享链接 (分享二维码)
  • 用户访问链接,可以点击加入
数据库表设计

队伍表 team
字段:

  • id 主键bigint (最简单、连续,放 url 上比较简短,但缺点是爬虫)
  • name 队伍名称
  • description 描述
  • maxNum最大人数
  • expireTime 过期时间)userld 创建人 id
  • status 0-公开,1-私有,2-加密
  • password 密码)
  • createTime 创建时间
  • updateTime 更新时间
  • isDelete是否删除
  1. create table team
  2. (
  3.     id           bigint auto_increment comment 'id'
  4.         primary key,
  5.     name   varchar(256)                   not null comment '队伍名称',
  6.     description varchar(1024)                      null comment '描述',
  7.     maxNum    int      default 1                 not null comment '最大人数',
  8.     expireTime    datetime  null comment '过期时间',
  9.     userId            bigint comment '用户id',
  10.     status    int      default 0                 not null comment '0 - 公开,1 - 私有,2 - 加密',
  11.     password varchar(512)                       null comment '密码',
  12.    
  13.         createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
  14.     updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
  15.     isDelete     tinyint  default 0                 not null comment '是否删除'
  16. )
  17.     comment '队伍';
复制代码
用户-队伍表 user_team
两个关系:

  • 用户加入了哪些队伍?
  • 队伍有哪些用户?
两种实现方式:

  • 建立用户-队伍关系表 teamid userid(便于修改,查询性能高一点,可以选择这个,不用全表遍历)
  • 用户表补充已加入的队伍字段,队伍表补充已加入的用户字段(便于查询,不用写多对多的代码,可以直接根据队伍查用户,根据用户查队伍)。
字段:

  • id 主键
  • userld 用户 id
  • teamld 队伍 id
  • joinTime 加入时间
  • createTime 创建时间
  • updateTime 更新时间
  • isDelete是否删除
  1. create table user_team
  2. (
  3.     id           bigint auto_increment comment 'id'
  4.         primary key,
  5.     userId            bigint comment '用户id',
  6.     teamId            bigint comment '队伍id',
  7.     joinTime datetime  null comment '加入时间',
  8.     createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
  9.     updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
  10.     isDelete     tinyint  default 0                 not null comment '是否删除'
  11. )
  12.     comment '用户队伍关系';
复制代码
为什么需要请求参数包装类?

  • 请求参数名称 / 类型和实体类不一样。
  • 有些参数用不到,如果要自动生成接口文档,会增加理解成本。
  • 对个实体类映射到同一个对象。
为什么需要包装类?

  • 可能有些字段需要隐藏,不能返回给前端。
  • 或者有些字段某些方法是不关心的。
前端不同页面传递数据


  • url querystring(xxx?id=1)比较适用于页面跳转
  • url (/team/:id, xxx/1)
  • hash (/team#1)
  • localStorage
  • context(全局变量,同页面或整个项目要访问公共变量)
最后:

“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
19.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册