SpringBoot防抖方案(防止表单重复提交)

SpringBoot防抖方案(防止表单重复提交)

1.应用场景(什么是防抖)

图片

所谓防抖,一是防用户手抖,二是防网络抖动。在Web系统中,表单提交是一个非常常见的功能,如果不加控制,容易因为用户的误操作或网络延迟导致同一请求被发送多次,进而生成重复的数据记录。要针对用户的误操作,前端通常会实现按钮的loading状态,阻止用户进行多次点击。而对于网络波动造成的请求重发问题,仅靠前端是不行的。为此,后端也应实施相应的防抖逻辑,确保在网络波动的情况下不会接收并处理同一请求多次。

2.思路分析

哪一类接口需要防抖?

通常用于表单的提交,相同的参数用户连续的点击,由于网络的波动或者前端没有阻止用户进行多次的提交点击,都有可能会请求重发问题进而产生重复的数据记录。

如何确定接口是重复的?

防抖也即防重复提交,那么如何确定两次接口就是重复的呢?首先,我们需要给这两次接口的调用加一个时间间隔,大于这个时间间隔的一定不是重复提交;其次,两次请求提交的参数比对,不一定要全部参数,选择标识性强的参数即可;最后,如果想做的更好一点,还可以加一个请求地址的对比。

3.分布式部署下如何做接口防抖?

使用共享缓存

image-20240628163203915

使用分布式锁

image-20240628163232879

4.具体实现

4.0 准备工作

  • 引入依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- spring2.X 集成 redis 所需 common-pool-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <!--不要带版本号,防止冲突-->
        </dependency>

        <!--redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.10.6</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>
  • yaml配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    # password: llp123
    #Redis 数据库索引(默认为 0)
    database: 0
    #连接超时时间(毫秒)
    timeout: 1800000
    lettuce:
      pool:
        #连接池最大连接数(使用负值表示没有限制)
        max-active: 20
        #最大阻塞等待时间(负数表示没限制)
        max-wait: -1
        #连接池中的最大空闲连接
        min-idle: 0
  • 启动类
@SpringBootApplication
public class RepeatSubmitApplication {
    public static void main(String[] args) {
        SpringApplication.run(RepeatSubmitApplication.class, args);
    }
}

4.1定义生成key的注解

首先我们需要定义一个用于生成key的注解,timeUnit是时间的单位,expire用于指定锁过期的时间,prefix用于指定业务前缀可以使用枚举或者常量来定义,delimiter参数之前的分隔符

示例:

repeat-submit&AddRequestVO(userName=zhangsan, userPhone=157, roleIdList=[1, 2, 4])

repeat-submit&zhangsan&157

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLock {

    TimeUnit timeUnit() default TimeUnit.SECONDS; // 时间单位,默认为秒

    long expire() default 3; // 锁过期时间,默认为3s

    String prefix() default "";

    String delimiter() default "&";

}

@RequestKeyParam注解用于指定生成key的参数

  • 修饰方法的参数则方法参数所有的值会拼接作为key,repeat-submit&AddRequestVO(userName=zhangsan, userPhone=157, roleIdList=[1, 2, 4])
  • 修饰字段则字段参数的值作为key, repeat-submit&zhangsan&157
/**
 * @description 加上这个注解可以将参数设置为key
 */
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {

}

4.2 核心代码

  • 枚举
@AllArgsConstructor
@Getter
public enum ResponseCodeEnum {
    BIZ_CHECK_FAIL(500),
    ;

    private Integer code;
}
  • 异常处理
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class BizException extends RuntimeException {

    /**
     * 业务错误码
     *
     */
    private Integer code;
    /**
     * 错误提示
     */
    private String message;

    public BizException(ResponseCodeEnum responseCodeEnum, String message){
        this.code = responseCodeEnum.getCode();
        this.message = message;
    }

}
@RestControllerAdvice
@AllArgsConstructor
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 业务异常
     */
    @ExceptionHandler(value = BizException.class)
    public Result BizException(BizException ex) {
        return Result.error(String.valueOf(BAD_REQUEST.value()), ex.getMessage());
    }

}
  • 统一返回
@Data
public class Result<T> {
    private String code;
    private String msg;
    private T data;

    public Result() {
    }

    public Result(T data) {
        this.data = data;
    }

    /**
     * 表示成功的Result,不携带返回数据
     *
     * @return
     */
    public static Result success() {
        Result result = new Result();
        result.setCode("200");
        result.setMsg("success");
        return result;
    }

    /**
     * 便是成功的Result,携带返回数据
     * 如果需要在static方法使用泛型,需要在static后指定泛型表示 static<T>
     *
     * @param data
     * @return
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>(data);
        result.setCode("200");
        result.setMsg("success");
        return result;
    }

    /**
     * 失败不携带数据
     * 将错误的code、msg作为形参,灵活传入
     *
     * @param code
     * @param msg
     * @return
     */
    public static Result error(String code, String msg) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

    /**
     * 失败携带数据
     * 将错误的code、msg、data作为形参,灵活传入
     * @param code
     * @param msg
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> error(String code, String msg, T data) {
        Result<T> result = new Result<>(data);
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

}
  • redis配置
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template =
                new RedisTemplate<>();
        System.out.println("template=>" + template);//这里可以验证..
        RedisSerializer<String> stringRedisSerializer =
                new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
                new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> stringRedisSerializer =
                new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}
  • redisson配置
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 这里假设你使用单节点的Redis服务器
        config.useSingleServer()
            // 使用与Spring Data Redis相同的地址
            .setAddress("redis://127.0.0.1:6379")
        // 如果有密码
        //.setPassword("xxxx");
        // 其他配置参数
        .setDatabase(0)
        .setConnectionPoolSize(10)
        .setConnectionMinimumIdleSize(2);
        // 创建RedissonClient实例
        return Redisson.create(config);
    }
}
  • 从缓存中获取key值
public class RequestKeyGenerator {
    /**
     * 获取LockKey
     *
     * @param joinPoint 切入点
     * @return
     */
    public static String getLockKey(ProceedingJoinPoint joinPoint) {
        // 获取连接点的方法签名对象
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // Method对象
        Method method = methodSignature.getMethod();
        // 获取Method对象上的注解对象
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        // 获取方法参数
        final Object[] args = joinPoint.getArgs();
        // 获取Method对象上所有的注解
        final Parameter[] parameters = method.getParameters();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < parameters.length; i++) {
            final RequestKeyParam keyParam = parameters[i].getAnnotation(RequestKeyParam.class);
            // 如果属性不是RequestKeyParam注解,则不处理
            if (keyParam == null) {
                continue;
            }
            // 如果属性是RequestKeyParam注解,则拼接 连接符 "& + RequestKeyParam"
            sb.append(requestLock.delimiter()).append(args[i]);
        }
        // 如果方法上没有加RequestKeyParam注解
        if (StringUtils.isEmpty(sb.toString())) {
            // 获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)
            final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            // 循环注解
            for (int i = 0; i < parameterAnnotations.length; i++) {
                final Object object = args[i];
                // 获取注解类中所有的属性字段
                final Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    // 判断字段上是否有RequestKeyParam注解
                    final RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);
                    // 如果没有,跳过
                    if (annotation == null) {
                        continue;
                    }
                    // 如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)
                    field.setAccessible(true);
                    // 如果属性是RequestKeyParam注解,则拼接 连接符" & + RequestKeyParam"
                    sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }
        // 返回指定前缀的key
        return requestLock.prefix() + sb;
    }
}
  • redis方式实现
import com.llp.repeatsubmit.annotaion.RequestLock;
import com.llp.repeatsubmit.constant.ResponseCodeEnum;
import com.llp.repeatsubmit.exception.BizException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;

/**
 * @description 缓存实现
 */
@Aspect
@Configuration
@Order(2)
public class RedisRequestLockAspect {

    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public RedisRequestLockAspect(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Around("execution(public * * (..)) && @annotation(com.llp.repeatsubmit.annotaion.RequestLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (StringUtils.isEmpty(requestLock.prefix())) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
        }
        // 获取自定义key
        final String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        // 使用RedisCallback接口执行set命令,设置锁键;设置额外选项:过期时间和SET_IF_ABSENT选项
        // SET_IF_ABSENT是 RedisStringCommands.SetOption 枚举类中的一个选项,
        // 用于在执行 SET 命令时设置键值对的时候,如果键不存在则进行设置,如果键已经存在,则不进行设置。
        final Boolean success = stringRedisTemplate.execute(
            (RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0],
                Expiration.from(requestLock.expire(), requestLock.timeUnit()),
                RedisStringCommands.SetOption.SET_IF_ABSENT));
        if (!success) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
        }
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
        }
    }
}
  • redisson方式实现

    • 校验key值是否存在,如果存在且尚未过期则为重复提交
    • 如果key值不存在或者已过期,则放行
import com.llp.repeatsubmit.annotaion.RequestLock;
import com.llp.repeatsubmit.constant.ResponseCodeEnum;
import com.llp.repeatsubmit.exception.BizException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

/**
 * @description 分布式锁实现
 */
@Aspect
@Configuration
@Order(2)
public class RedissonRequestLockAspect {
    private RedissonClient redissonClient;

    @Autowired
    public RedissonRequestLockAspect(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Around("execution(public * * (..)) && @annotation(com.llp.repeatsubmit.annotaion.RequestLock)")
    public Object interceptor(ProceedingJoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        RequestLock requestLock = method.getAnnotation(RequestLock.class);
        if (StringUtils.isEmpty(requestLock.prefix())) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重复提交前缀不能为空");
        }
        // 获取自定义key
        final String lockKey = RequestKeyGenerator.getLockKey(joinPoint);
        // 使用Redisson分布式锁的方式判断是否重复提交
        RLock lock = redissonClient.getLock(lockKey);
        boolean isLocked = false;
        try {
            // 尝试抢占锁
            isLocked = lock.tryLock();
            // 没有拿到锁说明已经有了请求了
            if (!isLocked) {
                throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
            }
            // 拿到锁后设置过期时间
            lock.lock(requestLock.expire(), requestLock.timeUnit());
            try {
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系统异常");
            }
        } catch (Exception e) {
            throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,请稍后重试");
        } finally {
            // 释放锁
            if (isLocked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

    }
}

4.3测试

@RestController
@RequiredArgsConstructor
public class RepeatSubmitController {

    private final UserService userService;

    @PostMapping("/add")
    @RequestLock(prefix = "repeat-submit")
    /**
     *@RequestKeyParam修饰方法时,表示方法的所有参数拼接作为key repeat-submit&AddRequestVO(userName=zhangsan, userPhone=157, roleIdList=[1, 2, 4])
     * @RequestKeyParam修饰字段时,表示该字段的值作为key,  repeat-submit&zhangsan&157
     * 如果key存在且为超过过期时间则会拒绝重复提交
     */
    public Result add(@RequestBody @RequestKeyParam AddRequestVO add) {
        userService.add(add);
        return Result.success();
    }

}
public interface UserService {
    void add(AddRequestVO add);
}
@Service
public class UserServiceImpl implements UserService {

    @Override
    public void add(AddRequestVO add) {

    }
}
  • 正常请求

请求参数

{
    "userName": "zhangsan",
    "userPhone": "157",
    "roleIdList":[1,2,4]
}

image-20240628165725233

  • 触发防抖

image-20240628165837462

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/753886.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

最新AIGC系统源码-ChatGPT商业版系统源码,自定义ChatGPT指令Promp提示词,AI绘画系统,AI换脸、多模态识图理解文档分析

目录 一、前言 系统文档 二、系统演示 核心AI能力 系统快速体验 三、系统功能模块 3.1 AI全模型支持/插件系统 AI模型提问 文档分析 ​识图理解能力 3.2 GPts应用 3.2.1 GPTs应用 3.2.2 GPTs工作台 3.2.3 自定义创建Promp指令预设应用 3.3 AI专业绘画 3.3.1 文…

Linux——echo命令,管道符,vi/vim 文本编辑器

1.echo 命令 作用 向终端设备上输出字符串或变量的存储数据 格式 echo " 字符串 " echo $ 变 量名 [rootserver ~] # echo $SHELL # 输出变量的值必须加 $ /bin/bash [rootserver ~] # str1" 我爱中国 " # 自定义变量 echo 重定向输出到文件 ec…

【自然语言处理系列】手动安装和测试Spacy中en_core_web_sm模型的详细教程

摘要&#xff1a;本教程旨在为自然语言处理&#xff08;NLP&#xff09;初学者提供一个详细的指南&#xff0c;用于手动安装流行的NLP库Spacy及其英语模型en_core_web_sm。文章将逐步指导您如何安装Spacy库、查看其版本&#xff0c;确定并下载适合的en_core_web_sm模型版本&…

RedHat9 | podman容器

1、容器技术介绍 传统问题 应用程序和依赖需要一起安装在物理主机或虚拟机上的操作系统应用程序版本比当前操作系统安装的版本更低或更新两个应用程序可能需要某一软件的不同版本&#xff0c;彼此版本之间不兼容 解决方式 将应用程序打包并部署为容器容器是与系统的其他部分…

MySQL实训项目——学生成绩录入与分析系统

项目简述&#xff1a;在校园中&#xff0c;除了上课之外&#xff0c;我们会有许多大大小小的考试&#xff0c;本项目将实现对学生数据的增添&#xff0c;删除&#xff0c;查询与修改&#xff0c;能让教育者更好的了解学生情况&#xff0c;进而优化教学方法和管理策略。 1.建表…

揭秘系统架构:从零开始,探索技术世界的无限可能

文章目录 引言一、系统架构的基本概念二、系统架构的设计原则模块化可扩展性高可用性安全性 三、常见的系统架构模式1. **分层架构&#xff08;Layered Architecture&#xff09;**&#xff1a;2. **微服务架构&#xff08;Microservices Architecture&#xff09;**&#xff1…

【嵌入式DIY实例】-LCD ST7735显示LM35传感器数据

LCD ST7735显示LM35传感器数据 文章目录 LCD ST7735显示LM35传感器数据1、硬件准备与接线2、代码实现本文将介绍如何使用 LM35 模拟温度传感器构建一个简单的温度计,其中温度值打印在 ST7735 TFT 显示屏上(以摄氏度、开尔文度和华氏度为单位)。 ST7735 TFT是一款分辨率为128…

从一万英尺外看libevent(源码刨析)

从一万英尺外看libevent 温馨提示&#xff1a;阅读时间大概二十分钟 前言 Libevent是用于编写高速可移植非阻塞IO应用的库&#xff0c;其设计目标是&#xff1a; 可移植性&#xff1a;使用libevent编写的程序应该可以在libevent支持的所有平台上工作。即使没有好的方式进行非…

使用minio搭建oss

文章目录 1.minio安装1.拉取镜像2.启动容器3.开启端口1.9090端口2.9000端口 4.访问1.网址http://:9090/ 5.创建一个桶 2.minio文件服务基本环境搭建1.创建一个文件模块2.目录结构3.配置依赖3.application.yml 配置4.编写配置类MinioConfig.java&#xff0c;构建minioClient5.Fi…

用Python将PowerPoint演示文稿转换到图片和SVG

PowerPoint演示文稿作为展示创意、分享知识和表达观点的重要工具&#xff0c;被广泛应用于教育、商务汇报及个人项目展示等领域。然而&#xff0c;面对不同的分享场景与接收者需求&#xff0c;有时需要我们将PPT内容以图片形式保存与传播。这样能够避免软件兼容性的限制&#x…

【网络架构】keepalive

目录 一、keepalive基础 1.1 作用 1.2 原理 1.3 功能 二、keepalive安装 2.1 yum安装 2.2 编译安装 三、配置文件 3.1 keepalived相关文件 3.2 主配置的组成 3.2.1 全局配置 3.2.2 配置虚拟路由器 四、实际操作 4.1 lvskeepalived高可用群集 4.2 keepalivedngi…

iOS政策解读之三丨商务、设计和法律 “三重奏“

上一篇的iOS政策解读文章&#xff0c;我们从安全和性能两方面进行了学习和解读&#xff0c;这两个方面是最为重要&#xff0c;也是优先级最高的方面。 如果您还没来得及阅读&#xff0c;欢迎移步我们前两篇的解读文章&#xff1a; iOS政策解读之一丨App提交审核前注意事项必知…

建投数据人力资源管理系统APP完成迭代升级

近日&#xff0c;建投数据人力资源管理系统APP完成迭代升级。 此次升级思路&#xff0c;遵循提升移动应用的功能和用户体验&#xff1b;直观的界面、快速的响应速度和安全的数据存储&#xff1b;个性化的功能&#xff0c;以满足不同员工的需求和使用偏好。 人力资源管理系统A…

ozon定价计算器下载,ozon定价计算器

各位电商卖家们&#xff0c;大家好&#xff01;在这个竞争激烈的电商时代&#xff0c;你是否还在为产品定价而头疼不已&#xff1f;特别是在俄罗斯ozon电商平台&#xff0c;本土与跨境的定价策略更是需要精细把控。今天&#xff0c;就为大家带来一款强大的定价工具——萌啦ozon…

QT QThread 线程类的使用及示例

QThread 是 Qt 框架提供的一个用于处理多线程的类&#xff0c;它允许开发者编写具有并发功能的应用程序&#xff0c;提高程序的响应速度、执行效率和用户体验。 在操作系统中&#xff0c;线程是进程内的执行单元&#xff0c;拥有独立的执行路径。每个线程有自己独立的栈空间&a…

数据库同步最简单的方法

数据库同步到底有咩有简单的方法&#xff0c;有肯定是有的&#xff0c;就看你有咩有缘&#xff0c;看到这篇文章&#xff0c;你就是有缘人。众所周知&#xff0c;数据库同步向来都不是一件简单的事情&#xff0c;它很繁琐&#xff0c;很费精力&#xff0c;很考验经验&#xff0…

unity 导入的模型设置讲解

咱们先讲Model这一栏 Model Scene&#xff1a;场景级属性&#xff0c;例如是否导入灯光和照相机&#xff0c;以及使用什么比例因子。 Scale Factor&#xff1a;缩放因子&#xff08;也就是模型导入后大小如果小了或者大了在这里直接改是相当于该模型的大小的&#xff0c;而且在…

Windows系统开启python虚拟环境

.\env4socre\Scripts\activate : 无法加载文件 E:\SocreMan\env4socre\Scripts\Activate.ps1&#xff0c;因为在此系统上禁止运行脚本。 环境&#xff1a;windows 11、vscode 1、用管理员权限打开powershell 输入set-executionpolicy remotesigned&#xff0c;选择Y 2、返回v…

网工内推 | 网络工程师,IE认证优先,最高18k*14薪,周末双休

01 上海吾索信息科技有限公司 &#x1f537;招聘岗位&#xff1a;网络工程师 &#x1f537;岗位职责&#xff1a; 1&#xff09;具备网络系统运维服务经验以及数据库实施经验&#xff0c;具备网络系统认证相关资质或证书&#xff1b; 2&#xff09;掌握常用各设备的运维巡检…

Logback-打印方法名及代码行号

背景 公司产品使用了logback作为日志输出框架&#xff0c;日志输出的pattern里配置了打印调用方法名及代码行号的配置&#xff0c;但是实际输出的日志方法名总是显示? 在强迫症的驱使下&#xff0c;开启了探秘之旅 Logback版本 1.2.3 项目中Logging.pattern配置如下&#xff1…