孜稞 发表于 2025-8-16 09:50:46

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

@
目录

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

[*]怎么缓存预热,预热操作
[*]控制定时任务的执行
[*]锁
[*]分布式锁实现的关键
[*]Redisson-实现分布式锁

[*]组队

[*]数据库表设计

[*]前端不同页面传递数据
[*]最后:

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


项目地址:

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

我这边的解决方法是:

myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie整体的:
import axios from "axios";


// axios.defaults.withCredentials = true; // 允许携带凭证
// const isDev = process.env.NODE_ENV === 'development';

// 创建实例时配置默认值
const myAxios = axios.create({
    LookupAddress: undefined, LookupAddressEntry: undefined,
    baseURL: 'http://localhost:8080/api'
});

// const myAxios: AxiosInstance = axios.create({
//   baseURL: isDev ? 'http://localhost:8080/api' : '线上地址',
// });

myAxios.defaults.withCredentials = true; // 配置为true,表示前端向后端发送请求的时候,需要携带上凭证cookie
// 创建实例后修改默认值


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

// 添加响应拦截器
myAxios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    console.log('我收到你的响应了',response)
    return response.data;
}, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
});

// Add a request interceptor
// myAxios.interceptors.request.use(function (config) {
//   console.log('我要发请求啦', config)
//   // Do something before request is sent
//   return config;
// }, function (error) {
//   // Do something with request error
//   return Promise.reject(error);
// });
//
//
// // Add a response interceptor
// myAxios.interceptors.response.use(function (response) {
//   console.log('我收到你的响应啦', response)
//   // 未登录则跳转到登录页
//   if (response?.data?.code === 40100) {
//         const redirectUrl = window.location.href;
//         window.location.href = `/user/login?redirect=${redirectUrl}`;
//   }
//   // Do something with response data
//   return response.data;
// }, function (error) {
//   // Do something with response error
//   return Promise.reject(error);
// });

export default myAxios;后端配置:

在 Spring Boot 中,可以通过在配置类中添加 @CrossOrigin 注解或实现 WebMvcConfigurer 接口并重写 addCorsMappings 方法来允许特定来源的跨域请求:
package com.rainbowsea.yupao.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:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http" +
                        "://127.0.0.1:8083","http://127.0.0.1:8080","http://127.0.0.1:5173")
                //是否允许证书 不再默认开启
                .allowCredentials(true)
                //设置允许的方法
                .allowedMethods("*")
                //跨域允许时间
                .maxAge(3600);
    }
}相关博客链接:

[*]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/

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// 导出配置对象,使用ES模块语法
export default defineConfig({
plugins: , // 启用Vue插件
server: { // 注意:在Vite的新版本中,配置项`devServer`已更名为`server`
    proxy: {
      '/api': {
      target: 'http://localhost:8080/api', // 目标服务器地址
      changeOrigin: true, // 是否改变源
      // 如果需要路径重写,可以取消以下行的注释
      // pathRewrite: { 1'^/api': '' }
      }
    }
}
});
server:
port: 8080
servlet:
    context-path: /api
    session:
      cookie:
      domain: localhost
      secure: true
      same-site: none # 上述方法不行,就配置上
spring:
    # session 失效时间
session:
    timeout: 86400
    store-type: redis
# Redis 配置
redis:
    port: 6379
    host: localhost
    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。
分布式锁的注意事项:

[*]用完锁一定要释放锁
[*]一定要设置锁的过期时间,防止对应占用锁的服务器宕机了,无法释放锁。导致死锁。
[*]如果方法执行过长的话,锁被提前过期,释放了,怎么办。——续期
boolean end = false;// 方法没有结束的标志

new Thread(() -> {
    if (!end)}{// 表示执行还没结束,续期
    续期
})

end = true;// 到这里,方法执行结束了问题:

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

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

[*]在项目启动类上,添加上 @EnableScheduling注解。表示启动定时任务。




[*]编写具体要执行的定时任务,程序,这里我们名为 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
<dependency>
    <groupId>org.redisson</groupId>
    redisson</artifactId>
    <version>3.17.5</version>
</dependency>使用 Ression

[*]配置 Ression 配置类:

package com.rainbowsea.yupao.config;


import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Redisson 配置
*
*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis")// 同时这个获取到 application.yaml 当中前缀的配置属性
@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);
      config.useSingleServer().setAddress(redisAddress).setDatabase(3);
      // 2. 创建实例
      RedissonClient redisson = Redisson.create(config);
      return redisson;
    }
}
[*]操作 Redis : 测试是否,能操作 Redis ,通过 Ression
// list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));

list.remove(0);

// 数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list"); // redis操作的 key 的定义
rList.add("yupi");
System.out.println("rlist:" + rList.get(0));
rList.remove(0);
package com.rainbowsea.yupao.service;


import org.junit.jupiter.api.Test;
import org.redisson.api.RList;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class RedissonTest {

    @Resource
    private RedissonClient redissonClient;


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


      // 数据存入 redis 的内存中
      RList<Object> rList = redissonClient.getList("test-list");// 表示 redis 当中的 key
      rList.add("yupi");
      System.out.println("rlist:" + rList.get(0));
    }
}
其中 Redisson 操作Redis 当中的 set,map 都是一样的道理,就不多赘述了。
分布式锁保证定时任务不重复执行:
实现代码如下:
void testWatchDog() {
    RLock lock = redissonClient.getLock("yupao:precachejob:docache:lock");
    try {
      // 只有一个线程能获取到锁
      if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
            // todo 实际要执行的方法
            doSomeThings();
            System.out.println("getLock: " + Thread.currentThread().getId());
      }
    } catch (InterruptedException e) {
      System.out.println(e.getMessage());
    } finally {
      // 只能释放自己的锁
      if (lock.isHeldByCurrentThread()) { // 判断该锁是不是当前线程创建的锁
            System.out.println("unLock: " + Thread.currentThread().getId());
            lock.unlock();
      }
    }
}注意:

[*]waitTime 设置为 0,其他线程不会去等待这个锁的释放,就是抢到了就用,没抢到就不用了,只抢一次,抢不到就放弃。
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
[*]注意释放锁要放到 finally 中,不然,发生了异常就被中断,无法释放锁了。
缓存预热,定时执行具体的任务的具体代码:

@Scheduled(cron = "0 5 21 25 6 ?")
package com.rainbowsea.yupao.job;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.rainbowsea.yupao.model.User;
import com.rainbowsea.yupao.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.ArrayList;
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;

    @Resource
    private RedissonClient redissonClient;

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

    // 每天执行,预热推荐用户,每个月的 31号,0:00执行
    @Scheduled(cron = "0 31 0 * * *")
    public void doCacheRecommendUser() {
      RLock lock = redissonClient.getLock("yupao: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("yupao:user:recommend:%s", userId);
                  ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
                  // 写缓存
                  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();
            }
      }
    }

}运行测试:


\yupao\yupao-backend\target>java -jar .\yupao-backend-0.0.1-SNAPSHOT.jar --server.port=9090
Redisson 看门狗机制:
Redisson 中提供的续期机制。
开一个监听线程,如果方法还没执行完,就帮你重置 Redis 锁的过期时间。
原理:

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


(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是否删除
create table team
(
    id         bigint auto_increment comment 'id'
      primary key,
    name   varchar(256)                   not null comment '队伍名称',
    description varchar(1024)                      null comment '描述',
    maxNum    int      default 1               not null comment '最大人数',
    expireTime    datetimenull comment '过期时间',
    userId            bigint comment '用户id',
    status    int      default 0               not null comment '0 - 公开,1 - 私有,2 - 加密',
    password varchar(512)                     null comment '密码',
   
      createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    isDelete   tinyintdefault 0               not null comment '是否删除'
)
    comment '队伍';用户-队伍表 user_team
两个关系:

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

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

[*]id 主键
[*]userld 用户 id
[*]teamld 队伍 id
[*]joinTime 加入时间
[*]createTime 创建时间
[*]updateTime 更新时间
[*]isDelete是否删除
create table user_team
(
    id         bigint auto_increment comment 'id'
      primary key,
    userId            bigint comment '用户id',
    teamId            bigint comment '队伍id',
    joinTime datetimenull comment '加入时间',
    createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',
    updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,
    isDelete   tinyintdefault 0               not null comment '是否删除'
)
    comment '用户队伍关系';为什么需要请求参数包装类?

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

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


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

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


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04