当前位置: 首页 > news >正文

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

@

目录
  • 伙伴匹配系统(移动端 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 中,可以通过在配置类中添加 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@CrossOrigin</font> 注解或实现 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">WebMvcConfigurer</font> 接口并重写 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">addCorsMappings</font> 方法来允许特定来源的跨域请求:

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 {@Overridepublic 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 这篇也不错。

缓存预热

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

缓存预热的优点:

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

缺点:

  1. 增加了开发成本,访问人数不多。(你要额外的开发,设计)
  2. 预热的时机和时间如果错了,有可能你缓存的数据不对或者数据太旧了
  3. 需要占用空间。拿空间换时间。

分析优缺点的时候,要打开思路,从整个项目从 0 到 1 的链路上分析。

怎么缓存预热,预热操作

两种方式:

  1. 定时任务预热。
  2. 模拟触发(手动触发)

这里我们采用定时任务预热

定时任务实现:

  1. Spring Scheduler (Spring Boot 默认整合了)
  2. Quartz (独立于 Spring 存在的定时任务框架)
  3. XXL-Job 之类的分布式任务调度平台(界面+sdk)

用定时任务,每天刷新所有用户的推荐列表

注意点:

  1. 缓存预热的意义(新增少,总用户多)
  2. 缓存的空间不能太大,要预留给其他缓存空间
  3. 缓存数据的周期(此处每天一次)

采用第一种方式:步骤:

  1. 主类开启:@EnableScheduling
  2. 给要定时执行的方法添加上 @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()], // 启用Vue插件server: { // 注意:在Vite的新版本中,配置项`devServer`已更名为`server`proxy: {'/api': {target: 'http://localhost:8080/api', // 目标服务器地址changeOrigin: true, // 是否改变源// 如果需要路径重写,可以取消以下行的注释// pathRewrite: { 1'^/api': '' }}}}
});

server:port: 8080servlet:context-path: /apisession:cookie:domain: localhostsecure: truesame-site: none # 上述方法不行,就配置上
spring:# session 失效时间session:timeout: 86400store-type: redis# Redis 配置redis:port: 6379host: localhostdatabase: 1    

控制定时任务的执行

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

为什么呢?

  1. 浪费资源,想象 1W 台服务器能执行。
  2. 脏数据,比如重复插入。

怎么做?几种方案:

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

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

  1. 分布式锁,只有抢到锁的服务器才能执行业务逻辑,坏处:增加成本;好处:就是不用手动配置,多少个服务器都一样。

注意:只要是单机,就会存在单点故障。

有限资源的情况下,控制同一时间(段)只有某些线程(用户/服务器)能访问到资源。

Java 实现锁:sychronized 关键字,并发包的类

但存在问题:只对单个 JVM 有效。

分布式锁实现的关键

枪锁机制:

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

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

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

MySQL 数据库:select for update 行级锁(最简单),或者乐观锁

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

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

分布式锁的注意事项:

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

问题:

  • 连锁效应:释放掉别人的锁
  • 这样还是会存在多个方法同时执行的情况。
  1. 释放锁的时候(判断出是自己的锁了,准备执行释放的锁的过程中时,很巧的时,锁过期了,然后,这个时候就有一个新的东东插入进来了,这样锁就删除了别人的锁了)。

解决方案:Redis + lua 脚本保证操作原子性

// 原子操作
if(get lock == A) {// set lock Bdel lock
}

步骤:缓存预热数据:

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

  1. 编写具体要执行的定时任务,程序,这里我们名为 PreCacheJob

注意:这里我们使用上 注解,使用 cron 表达式表示一个定时时间上的设置

这里往下看,我们使用 Redisson 进行一个实现分布式锁的操作。

相关 Redissson 配置使用如下:

Redisson-实现分布式锁

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

2 种引入方式:

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

使用 Ression

  1. 配置 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;@Beanpublic 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;}
}
  1. 操作 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 {@Resourceprivate RedissonClient redissonClient;@Testvoid 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 当中的 keyrList.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();}}
}

注意:

  1. waitTime 设置为 0,其他线程不会去等待这个锁的释放,就是抢到了就用,没抢到就不用了,只抢一次,抢不到就放弃。
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
  1. 注意释放锁要放到 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 {@Resourceprivate UserService userService;@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Resourceprivate 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 锁的过期时间。

原理:

  1. 监听当前线程, 默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
  2. 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期。

为什么 Redisson 续期时间是 30 秒

因为方式 Redis 宕机了,就成了,占着茅坑不拉屎。

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

组队

用户可以创建一个队伍,设置队伍的人数、队伍名称(标题)、描述、超时时间PO。

队长、剩余的人数

聊天?

公开或private或加密

用户创建队伍最多5个

展示队伍列表,根据名称搜索队伍PO,信息流中不展示已过期的队伍

修改队伍信息PO~P1

用户可以加入队伍(其他人、未满、未过期),允许加入多个队伍,但是要有个上限PO

是否需要队长同意?筛选审批?

用户可以退出队伍(如果队长退出,权限转移给第二早加入的用户——先来后到)P1

队长可以解散队伍PO

分享队伍=>邀请其他用户加入队伍P1

业务流程:

  1. 生成分享链接 (分享二维码)
  2. 用户访问链接,可以点击加入

数据库表设计

队伍表 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    datetime  null 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     tinyint  default 0                 not null comment '是否删除'
)comment '队伍';

用户-队伍表 user_team

两个关系:

  1. 用户加入了哪些队伍?
  2. 队伍有哪些用户?

两种实现方式:

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

字段:

  • 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 datetime  null comment '加入时间',createTime   datetime default CURRENT_TIMESTAMP null comment '创建时间',updateTime   datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP,isDelete     tinyint  default 0                 not null comment '是否删除'
)comment '用户队伍关系';

为什么需要请求参数包装类?

  1. 请求参数名称 / 类型和实体类不一样。
  2. 有些参数用不到,如果要自动生成接口文档,会增加理解成本。
  3. 对个实体类映射到同一个对象。

为什么需要包装类?

  1. 可能有些字段需要隐藏,不能返回给前端。
  2. 或者有些字段某些方法是不关心的。

前端不同页面传递数据

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

最后:

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

http://www.sczhlp.com/news/13024/

相关文章:

  • 不同主机间通信及网络基础
  • 视频讲解:多层感知机MLP与卷积神经网络CNN在服装图像识别中的应用
  • 单片机异常复位
  • 专题:2025母婴行业洞察报告|附60+份报告PDF汇总下载
  • cad.net 选择集搞坏CAD的问题
  • AGC011 题解
  • colab unsloth 安装出现错误【可行】
  • 视频讲解:ARIMA-LSTM注意力融合模型跨行业股价预测应用
  • 32_1 结构体的学习与练习
  • pytorch+torchvision+python版本对应关系 版本对照表
  • Siderophile:检测Rust代码库中的不安全代码
  • 增量可满足性问题解决方案验证方法
  • 云平台运维工具 —— 阿里云原生工具 - 详解
  • 麒麟系统中离线安装java环境
  • P2822 [NOIP 2016 提高组] 组合数问题
  • 行列式 / 矩阵树定理 笔记
  • Java 开发者必看!JBoltAI 框架让 AI 应用开发效率飙升
  • Java+AI 的黄金搭档!JBoltAI 框架让系统服务焕发新生
  • Java 技术团队的 AI 利器:JBoltAI 框架轻松搞定多模型适配
  • JBoltAI 框架来袭,重新定义 Java AI 开发的技术范式
  • 米哈游创始人 AI 游戏上线,与女主实时语音互动;昆仑万维上线 Mureka V7.5 模型,中文歌曲能力大幅提升丨日报
  • 研发成本砍半!JBoltAI:Java 团队的 AI 开发 “加速器”
  • 不懂大模型也能上手!JBoltAI 让 Java 开发者轻松搞定 AI 应用
  • 解锁 Java AI 开发新姿势,JBoltAI 框架助力系统智能化转型
  • JBoltAI:Java 企业级 AI 框架,从技术到服务的全维度支持
  • 求前驱后继 笔记
  • 2025-08-16 如何禁止vscode自动格式化代码==》在编译器左下角找到设置小图标,点进去找到设置(setting),点进去搜索‘格式化’,找到文本编译器下的格式化,把它们全部取消勾选即可
  • 颗粒vtp轨迹数据快速提取
  • 基于Java+Springboot+Vue开发的体育用品购物销售商城管理系统源码+运行
  • P1786 帮贡排序