Redis实战—验证码登录功能实现

  本博客为个人学习笔记,学习网站:黑马程序员Redis入门到实战 实战篇之验证码登录

目录

基于Session实现登录流程

发送短信验证码

短信验证码登录、注册

校验登录状态

session共享问题

Redis代替session的业务流程

设计Key的结构

设置Key的细节

整体访问流程

代码

解决状态登录刷新问题


基于Session实现登录流程

发送短信验证码
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号;如果手机号合法,后台此时生成对应的验证码,同时将验证码保存到session中,然后再通过短信的方式将验证码发送给用户。

短信验证码登录、注册
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息。

校验登录状态
用户在请求时候,会从cookie中携带JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行 。


发送短信验证码

Controller层代码

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

Service层代码

//Service接口类代码
public interface IUserService extends IService<User> {
    Result sendCode(String phone, HttpSession session);
}

//Service接口实现类代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号码格式错误!");
        }
        // 3.如果符合,生成随机6位验证码
        String code = RandomUtil.randomNumbers(6);
        // 4.保存验证码到session
        session.setAttribute("code", code);
        // 5.模拟发送验证码到手机(控制台)
        log.debug("你收到的黑马点评短信验证码为:" + code);
        return Result.ok();
    }
}

短信验证码登录、注册

如果我们在信息传输时使用User类,通过浏览器可以观察到此时用户的全部信息都被暴露,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了。

UserDTO类代码

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

Controller层代码

/**
 * 登录功能
 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
 */
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
    // 实现登录功能
    return userService.login(loginForm, session);
}

@GetMapping("/me")
public Result me() {
    // 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

Service层代码

//Service接口类方法代码
Result login(LoginFormDTO loginForm, HttpSession session);

//Service接口实现类代码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号码格式错误!");
    }
    // 2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.toString().equals(code)) {
        // 3.验证码不一致则报错
        return Result.fail("验证码错误");
    }

    // 4.一致 -> 根据手机号查询用户
    User user = query().eq("phone", phone).one();
    // 5.判断用户是否存在
    if (user == null) {
        // 6.若用户不存在 -> 创建新用户
        user = createUserWithPhone(phone);
    }
    // 7.将user转换为UserDTO类,保存用户信息到session中
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    // 1.创建用户
    User user = new User();
    user.setPhone(phone);
    user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2.保存用户
    save(user);
    return user;
}

校验登录状态

通过拦截器实现登录校验功能


拦截器类代码

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户
        Object user = session.getAttribute("user");
        // 3.判断用户是否存在
        if (user == null) {
            // 4.若用户不存在则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.若用户存在则保存用户信息到 ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        // 6.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

 拦截器配置类代码

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                    //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

session共享问题

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?

早期的方案是session拷贝,虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session被修改时,都会同步给其他Tomcat服务器的session,这样的话,就可以实现session的共享了。

但是这种方案具有两个大问题
1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟

所以后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了


Redis代替session的业务流程

设计Key的结构


首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。


设置Key的细节

我们可以使用String结构,就是一个简单的key,value键值对的方式。但是关于key的处理,session的特性是每个用户都拥有自己的session,各个用户之间的session相互独立,互不干扰。但redis的key是共享的,为避免干扰,我们不能使用code(验证码)来作为key了,我们需要找到一个能唯一标识各个用户的key。

在设计这个key的时候,我们之前讲过需要满足两点
1、key要具有唯一性
2、key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了。


整体访问流程

当注册完成后,用户登录时会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。


代码

接口参数修改,去掉session参数

public interface IUserService extends IService<User> {

    Result sendCode(String phone);

    Result login(LoginFormDTO loginForm);
}

修改Service接口实现类代码,将验证码保存到redis并设置有效时间为2min

@Override
public Result sendCode(String phone) {
    // 1.校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号码格式错误!");
    }
    // 3.如果符合,生成随机6位验证码
    String code = RandomUtil.randomNumbers(6);
    // 4.保存验证码到redis并设置有效时间   // set key value ex 120
    stringRedisTemplate.opsForValue().set("login:code" + phone, code, 2, TimeUnit.MINUTES);
    // 5.模拟发送验证码到手机(控制台)
    log.debug("你收到的黑马点评短信验证码为:" + code);
    return Result.ok();
}

修改Service接口实现类登录代码,

    @Override
    public Result login(LoginFormDTO loginForm) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号码格式错误!");
        }
        // 2.从redis中获取验证码并进行校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.toString().equals(code)) {
            // 3.验证码不一致则报错
            return Result.fail("验证码错误");
        }

        // 4.一致 -> 根据手机号查询用户
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在
        if (user == null) {
            // 6.若用户不存在 -> 创建新用户
            user = createUserWithPhone(phone);
        }
        // 7.保存用户信息到redis中
        // 7.1 随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2 将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        // 7.3 存储
        stringRedisTemplate.opsForHash().putAll("login:token:" + token, userMap);
        // 7.4 设置有效期
        stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
        // 8 返回token
        return Result.ok(token);
    }

修改拦截器代码

注意
由于该LoginInterceptor的对象是我们手动创建,手动new出来的,并不是通过spring创建的,因此StringRedisTemplate对象的注入不能采用@Autowire,@Resource等方式,只有spring/springboot帮我们创建的对象才可以使用@Autowire,@resource

public class LoginInterceptor implements HandlerInterceptor {

    /*
     * 注意!!!
     * 由于该LoginInterceptor的对象是我们手动创建,手动new出来的,并不是通过spring创建的
     * 因此StringRedisTemplate对象的注入不能采用@Autowire,@Resource等方式
     * 只有spring/springboot帮我们创建的对象才可以使用@Autowire,@resource
     * */
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("Authorization");
        if (StrUtil.isBlank(token)) {
            // 用户不存在则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3.判断用户是否存在(entries如果获取到一个为null的Map时,会返回一个空Map,因此下面的if判断只需要判断该Map是否为空就行)
        if (userMap.isEmpty()) {
            // 4.若用户不存在则拦截,返回401状态码
            response.setStatus(401);
            System.err.println("不放行!");
            return false;
        }
        // 5.将查询到的Hash数据转换为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.若用户存在则保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.SECONDS);
        // 8.放行
        System.err.println("放行!");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

拦截器配置类代码

注意

在LoginInterceptor类中的注释中,我们说到:由于LoginInterceptor类并不是通过spring创建的,因此不能通过注解的形式注入StringRedisTemplate类,只能通过构造函数添加。
而配置类MvcConfig是通过注解@Configuration由spring创建的,因此可以用注解的形式注入StringRedisTemplate类。

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /*
     * 在LoginInterceptor类中的注释中,我们说到
     * 由于LoginInterceptor类并不是通过spring创建的,因此不能通过注解的形式注入StringRedisTemplate类,只能通过构造函数添加
     * 而配置类MvcConfig是通过注解@Configuration由spring创建的,因此可以用注解的形式注入StringRedisTemplate类
     * */

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

再次运行,输入手机号获取验证码,填写验证码点击登录后出现报错,内容如下图
报错:显示Long类型无法转换为String类型

 修改接口实现类中,login方法中的代码


解决状态登录刷新问题

由于我们设置的拦截器只能拦截有关登录请求,使得其他无关登录的请求无法被拦截,将导致部分请求无法及时更新token的有效期,因此我们再设置一个拦截器,用来专门拦截所有请求,更新token的有效期。

新增拦截器代码如下:

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("Authorization");
        if (StrUtil.isBlank(token)) {
            // 该拦截器只负责更新数据有效期,因此如果token值为空,则直接放行,由后面的拦截器处理
            return true;
        }
        // 2.基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3.判断用户是否存在(entries如果获取到一个为null的Map时,会返回一个空Map,因此下面的if判断只需要判断该Map是否为空就行)
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的Hash数据转换为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.若用户存在则保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.SECONDS);
        // 8.放行
        System.err.println("放行!");
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

修改原来的拦截器代码
由于新增的拦截器在获取请求头中的token,并基于token获取到redis中的用户时,会将该用户转换为UserDTO对象,并存储到UserHolder中,因此原来的拦截器不必再做多余的解析token操作,只需要根据UserHolder中的User是否存在来判断用户的登录状态并决定是否放行即可。

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截 (ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            //不存在用户,需要拦截
            response.setStatus(401);
            return false;
        }
        // 有用户则放行
        return true;
    }
}

修改拦截器配置类:添加新拦截器,令其拦截所有请求,同时为两个拦截器设置order(order越小则越先执行)

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        //排除不需要拦截的路径
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

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

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

相关文章

FreeRTOS操作系统学习——任务管理

任务概念 在FreeRTOS中&#xff0c;一个任务相当于一个线程&#xff0c;可以有很多的任务&#xff0c;每个人任务可以设置不同的优先级。相同优先级的任务轮流使用CPU&#xff0c;高优先级的任务可以一直使用CPU&#xff0c;直到主动放弃&#xff0c;低级的任务才有被执行的机…

Stable Diffusion 模型分享:DucHaiten-AIart-SDXL(动漫、3D、逼真)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 这是一个丰富多彩的 SDXL 模型&#xff0c;可以绘制动漫、3D、科幻、真实等类型的图片。 …

响应式编程五股票订阅系统实现

响应式编程五 使用StepVerifier测试响应式流StepVerifier要点 使用StepVerifier进行高级测试股票订阅系统数据库表 使用StepVerifier测试响应式流 出于测试目的&#xff0c;Reactor 提供了额外的 reactor-test 模块&#xff0c;该模块提供了 StepVerifier。StepVerifier 提供了…

Go的安装

一. 下载地址 Go官方下载地址&#xff1a;https://golang.org/dl/ Go中文网&#xff1a;https://go.p2hp.com/go.dev/dl/ 根据不同系统下载不同的包。 二. 配置GOPATH GOPATH是一个环境变量&#xff0c;用来表明你写的go项目的存放路径。 GOPATH路径最好只设置一个&#xff0…

【JavaEE初阶】 JVM 运行时数据区简介

文章目录 &#x1f343;前言&#x1f332;堆&#xff08;线程共享&#xff09;&#x1f384;Java虚拟机栈&#xff08;线程私有&#xff09;&#x1f38b;本地方法栈&#xff08;线程私有&#xff09;&#x1f333;程序计数器&#xff08;线程私有&#xff09;&#x1f334;方法…

CentOS上安装MySQL 5.7和MySQL 8.0教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

Hive的性能优化

1.调优概述 Hive 作为大数据领域常用的数据仓库组件&#xff0c;在设计和查询时要特别注意效率。影响 Hive 效率的几乎从不是数据量过大&#xff0c;而是数据倾斜、数据冗余、Job或I/O过多、MapReduce分配不合理等等。对 Hive 的调优既包含 Hive 的建表设计方面&#xff0c;对H…

如何学习I2C协议

文章目录 学习I2C协议0 懒人直达1 了解协议开发者2 从恩智浦半导体公司下载官方技术文档3 翻译成中文4 资源下载 学习I2C协议 0 懒人直达 点击直达 1 了解协议开发者 I2C&#xff08;Inter-Integrated Circuit&#xff09;协议是由荷兰皇家飞利浦电子公司&#xff08;现恩智…

python 基础知识点(蓝桥杯python科目个人复习计划59)

今日复习内容&#xff1a;做题 例题1&#xff1a;建造房屋 问题描述&#xff1a; 小蓝和小桥是两位年轻的建筑师&#xff0c;他们正在设计一座新的城市。 在这个城市中&#xff0c;有N条街道&#xff0c;每条街道上有M个位置可以建造房屋&#xff08;一个位置只能建造一个房…

#14vue3生成表单并跳转到外部地址的方式

1、背景 后端返回的json数据中包含一个json数组&#xff0c;此数组中是目标跳转地址所需要的form表单的数据。 2、跳转前的页面 const goto () > {finish.value true;request.post(/xxx/yyy,{zzz: zzz.value}).then(res > {const url res.data.submitUrlconst params…

洞察未来5-10年数字化转型的革命性趋势

“十四五”规划和2035年远景目标纲要提出了加快数字化发展&#xff0c;建设数字中国的目标&#xff0c; 这意味着数字化转型将成为推动生产方式、生活方式和治理方式变革的关键力量。 加快数字化发展 建设数字中国 ⭐ 打造数字经济新优势 ⭐ 加快数字社会建设步伐 ⭐ 提高数字…

【力扣白嫖日记】1070.产品销售分析III

前言 练习sql语句&#xff0c;所有题目来自于力扣&#xff08;https://leetcode.cn/problemset/database/&#xff09;的免费数据库练习题。 今日题目&#xff1a; 1070.产品销售分析III 表&#xff1a;Sales 列名类型sale_idintproduct_idintyearintquantityintpriceint …

Selenium自动化测试-3.元素定位(1)

这次我们要分享的是对元素的定位&#xff0c;在一个页面中有很多不同的策略来定位一个元素&#xff0c;我们选择最合适的方法即可。 一个页面最基本组成单元是元素&#xff0c;想要定位一个元素&#xff0c;我们需要特定的信息来说明这个元素的唯一特征。 selenium 主要提供了…

Javaweb之Web后端开发总结的详细解析

4. Web后端开发总结 到此基于SpringBoot进行web后端开发的相关知识我们已经学习完毕了。下面我们一起针对这段web课程做一个总结。 我们来回顾一下关于web后端开发&#xff0c;我们都学习了哪些内容&#xff0c;以及每一块知识&#xff0c;具体是属于哪个框架的。 web后端开…

BUUCTF-Misc2

wireshark1 1.打开附件 发现是流量包&#xff0c;放到Wireshark中分析 2.过滤 根据题目的提示寻找管理员登录的网站&#xff0c;从中获取密码 用http.request.methodPOST&#xff0c;过滤当前的 HTTP 请求为 POST 方法 3.查找 双击过滤后的流量包&#xff0c;查找管理员密码…

HarmonyOS(二)Ability应用模型概述

目录 1 Ability概念 2 Ability形态 3 Stage优势 4 Stage模型结构 5 总结 注&#xff1a;本章内容提前声明。 基于HarmonyOS开发者3.1/4.0版本配套的开发者文档&#xff0c;对应API能力级别为API 9 Release。 详情可参考官网API入门第一章应用模型文档中心 1 Ability概念…

Sleuth(Micrometer)+ZipKin分布式链路追踪

Sleuth(Micrometer)ZipKin分布式链路追踪 Micrometer springboot3之前还可以用sleuth&#xff0c;springboot3之后就被Micrometer所替代 官网https://github.com/spring-cloud/spring-cloud-sleuth 为什么会出现这个技术&#xff1f; 在微服务框架中&#xff0c;一个由客户…

SAP MM学习笔记44 - 特殊调达流程 - Blanket购买发注(汇总采购)

上一章学习了 支付计划&#xff0c;本章继续学习 Blanket购买发注&#xff08;汇总采购&#xff09;。 SAP MM学习笔记43 - 特殊调达流程 - 支付计划-CSDN博客 1&#xff0c;Blanket购买发注 概要 其实就是订好一个大致数额&#xff0c;然后让随便买&#xff0c;只要不超这个…

O2O:Offline Meta-Reinforcement Learning with Online Self-Supervision

ICML 2022 paper Introduction 元强化学习(Meta RL)结合O2O。元RL需要学习一个探索策略收集数据&#xff0c;同时还需学习一个策略快速适应新任务。由于策略是在固定的离线数据集上进行元训练的&#xff0c;因此在适应探索策略收集的数据时&#xff0c;它可能表现得不可预测&…

汽车小车车灯无痕修复用的胶是什么胶?

汽车小车车灯无痕修复用的胶是什么胶&#xff1f; 可以使用在小车车灯无痕修复中的胶水&#xff0c;通常使用的车灯无痕修复专用UV胶。 车灯无痕修复专用胶主要成份是改性丙烯酸UV树脂&#xff0c;主要应用在车灯的专业无痕修复领域。它可以用于修复车灯壳的裂缝或破损&#xf…