模拟实现短信登录功能 (session 和 Redis 两种代码实例) 带前端演示

news2024/11/18 3:18:50

                                               

目录

整体流程

发送验证码

短信验证码登录、注册

校验登录状态

基于 session 实现登录

实现发送短信验证码功能

1. 前端发送请求

2. 后端处理请求

3. 演示

实现登录功能

1. 前端发送请求

2. 后端处理请求

校验登录状态

1. 登录拦截器

2. 注册拦截器

3. 登录完整演示

session共享问题

问题描述

基于 Redis 实现登录

设计 key 的结构

整体访问流程

基于 session 登录-流程图

基于 Redis 实现共享 session 登录-流程图

代码实现

发短信

登录功能

拦截器功能修改

登录演示

解决状态登录刷新问题

优化方案

代码修改

第一个拦截器

第二个拦截器

涉及到 order 的源码​​​​​​

整体流程

基础模版如下,可以根据具体需求进行修改和拓展!!!!

发送验证码

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

短信验证码登录、注册

用户将验证码和手机号进行输入,后台从 session 中拿到当前验证码,然后和用户输入的验证码进行校验:

如果不一致,则无法通过校验

如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存 session 中,方便后续获得当前登录信息

校验登录状态

用户在请求时候,会从 cookie 中携带者 sessionId 到后台,后台通过 sessionId 从 session 中拿到用户信息,如果没有 session 信息,则进行拦截,如果有 session 信息,则将用户信息保存到 ThreadLocal 中,并且放行。

基于 session 实现登录

实现发送短信验证码功能

1. 前端发送请求

前端输入电话,点击发送验证码,发送请求

2. 后端处理请求

Controller

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

Service

Result sendCode(String phone, HttpSession session);

ServiceImpl

 /**
     * 发送验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 如果不符合 返回错误信息
            return  Result.fail("手机号格式错误!");
        }
        // 3. 符合生成验证码
        // hutool 工具类 随机生成一个6位数的号码
        String code = RandomUtil.randomNumbers(6);
        // 4. 保存验证码到session
        session.setAttribute("code",code);
        // 5. 发送验证码
        log.debug("模拟发送短信验证码成功,验证码:{}",code);

        // 结束
        return Result.ok();
    }

3. 演示

后端生成验证码

实现登录功能

1. 前端发送请求

从后端获取到验证码

2. 后端处理请求

操作数据库是用的技术是 Mybatis-Plus

Controller

    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

Service

Result login(LoginFormDTO loginForm, HttpSession session);

ServiceImpl

/**
     * 登录
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.检验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 如果不符合 返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 校验验证码
        // 从session中获取刚刚生成的验证码
        Object cacheCode = session.getAttribute("code");
        // 获取从前端发送过来的验证码
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.toString().equals(code)) {
            // 验证码和手机不一致
            return Result.fail("验证码错误!");
        }
        // 4. 验证码和手机一致,根据手机号查询用户 
        // 使用了 Mybatis-Plus 
        User user = query().eq("phone", phone).one();

        if (user == null) {
            // 5. 不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }
        // 7. 保存用户信息到 session 中
        UserDTO userDTO = new UserDTO();
        // 8. 用户信息脱敏
        BeanUtils.copyProperties(user, userDTO);
        session.setAttribute("user", userDTO);
        log.debug("保存用户信息到session user = {}", user);
        return Result.ok();
    }

不熟悉 MybasitPlus 的小伙伴可以看看这个文章,快速入个小门

快速熟悉MybatisPlus Mybatis的使用与区别_mybatis和mybatis-plus-CSDN博客

UserDTO

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

createUserWithPhone 方法 -- 新增用户

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);
    log.debug("创建新用户成功,用户信息:{}",user);
    return user;
}

注:

根据前端需要渲染的页面,建立相关的 DTO 类做用户信息脱敏,返回一些主要的信息给前端就行。

  1. 减少 session 存储用户所占用的内存大小,从而减小服务器的压力(session 是存在服务器端的嘛)。
  2. 数据量小得话,在网络传输信息的时候花的时间也会更少,加快服务端的响应,降低网络对服务的影响,使用户体验更加。

校验登录状态

温馨小贴士:tomcat的运行原理

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat 也不例外,当监听线程知道用户想要和 tomcat 连接连接时,那会由监听线程创建 socket 连接socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao 中,并且访问对应的 DB,在用户执行完请求后,再统一返回,再找到 tomcat 端的 socket ,再将数据写回到用户端的 socket ,完成请求和响应

通过上述,我们可以得知,每个用户其实对应都是去找 tomcat 线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用。ThreadLocal 来做到线程隔离每个线程操作自己的一份数据。

因为涉及到线程池,给大家推荐一篇池化技术的文章,可以简单了解了解池化技术,拓宽自己的知识哈!!!

概述池化技术-CSDN博客

1. 登录拦截器


import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.ws.handler.Handler;

/**
 * @author lhd
 * date 2024/7/26
 * @apiNote
 */
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 中获取用户的状态信息
        // 用户信息脱敏
        UserDTO user = (UserDTO) session.getAttribute("user");
        // 3. 判断用户是否存在
        if (user == null){
            // 不存在就拦截
            response.setStatus(401);
            return false;
        }
        // 5. 存在 保存用户信息到 ThredLocal
        UserHolder.saveUser(user);
        // 6. 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

UserHolder

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

2. 注册拦截器

/**
 * @author lhd
 * date 2024/7/26
 * @apiNote
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                // 不拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
        WebMvcConfigurer.super.addInterceptors(registry);
    }
}

3. 登录完整演示

登录跳转到个人中心页面

因为这是一个新用户,后台数据库也会插入这个用户的信息

session共享问题

问题描述

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

我们能如何解决这个问题呢?

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

但是这种方案具有两个大问题:

1、每台服务器中都有完整的一份session数据,服务器压力过大。

2、session拷贝数据时,可能会出现延迟

模拟项目架构图

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

基于 Redis 实现登录

设计 key 的结构

首先我们要思考一下利用 redis 来存储数据,那么到底使用哪种结构呢?

由于存入的数据比较简单,我们可以考虑使用 String ,或者是使用哈希,如下图,如果使用 String,注意他的value,用多占用一点空间,如果使用哈希,则他的 value 中只会存储他数据本身,如果不是特别在意内存,其实使用 String 就可以啦。

所以我们可以使用 String 结构,就是一个简单的 key,value 键值对的方式,但是关于 key 的处理,session 他是每个用户都有自己的 session,但是 redis 的 key 是共享的,咱们就不能使用 code(验证码) 了。

在设计这个 key 的时候,我们之前讲过需要满足两点

        1、key要具有唯一性

        2、key要方便携带

如果我们采用 phone:手机号 作为key,用这个 key 来存储当然是可以的,但是如果把这样的敏感数据存储到 redis 中并且从页面中带过来毕竟不太合适。

所以我们在后台生成一个随机串token,然后让前端带来这个 token 就能完成我们的整体逻辑了。

整体访问流程

基于 session 登录-流程图

基于 Redis 实现共享 session 登录-流程图

总的来说总体功能没变,只是功能实现发生了改变。

主要变化:

  1. 使用 redis 完成用户登录后,需要返回一个随机 token 给前端,以便于用户后续访问。
  2. 发送请求从携带 cookie 转变为携带后端生成的随机 token。
  3. 获取用户信息从 session 中获取转变为通过 token从 redis 中获取。
  4. 获取验证码从 session 中获取转变为以手机号为 key 从 redis 中获取。

代码实现

发短信

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 发送验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1、校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 如果不符合 返回错误信息
            return  Result.fail("手机号格式错误!");
        }
        // 3. 符合生成验证码
        // hutool 工具类 随机生成一个6位数的号码
        String code = RandomUtil.randomNumbers(6);
        // 4. 保存验证码到session
        // session.setAttribute("code",code);

        // 4.以 String 数据结构 保存验证码到 redis中
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 5. 发送验证码
        log.debug("模拟发送短信验证码成功,验证码:{}",code);

        // 结束
        return Result.ok();
    }
    

登录功能

/**
     * 登录
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.检验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2. 如果不符合 返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 验证码不一致
            return Result.fail("验证码错误!");
        }
        // 4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        if (user == null) {
            // 5. 不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis 中
        // 7.1 随机生成 token 作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        // BeanUtil 是 hutool 工具类中的 复制 user 的信息 到 脱敏 UserDTO 中
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        // 忽略空值错误
                        .setIgnoreNullValue(true)
                        // 自定义转换器 因为 userDTO 中的 id 为 long 类型
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3 存储
        String tokenKey = LOGIN_USER_KEY + token;
        // 以 Hash 数据结构存储用户信息
        // login:token: + token 生成存储字符串作为 key 保存当前用户的信息到 redis中
        stringRedisTemplate.opsForHash().putAll(tokenKey + token, userMap);

        // 7.4 设置 token 有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        log.debug("保存用户信息到 redis user = {}", userDTO);
        // 8. 返回 token
        return Reslt.ok(token);
    }

拦截器功能修改

LoginInterceptor

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    
    private  StringRedisTemplate stringRedisTemplate;
    // 因为这个类不属于 spring 管理的类所以需要通过构造器去获取 StringRedisTemplate类
    // 构造器注入的方式 挺不错的思想
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的 token
        log.debug("拦截器被调用");
        String token = request.getHeader("authorization");
        System.out.println("从头获取到的token === " + token);
        if (StrUtil.isBlank(token)){
            // token为空说明没有登录过
            return false;
        }
        // 2.基于TOKEN获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3. 判断用户是否存在
        if (userMap.isEmpty()){
            // 不存在就拦截
            response.setStatus(401);
            return false;
        }
        // 5. 将查询到的 Hash 数据转为 UserDTO对象
        // 不忽略转换的错误
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6. 保存用户信息到 ThreadLocal
        UserHolder.saveUser(user);
        // 7. 刷新 token 有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        log.debug("ThreadLocal 中的User为:" + user);
        // 8. 放行
        return true;
    }

MvcConfig

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    // 本类加了 @Configuration 项目启动时候会进行自动装配 可以获取到 StringRedisTemplate
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                // 不拦截的路径
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/upload/**",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**"
                );
    }
}

抛了个类型转换异常

原因:

因为 StringRedisTemplate key 和 value 都需要 String 类型

解决方案:

一开始 UserDTO 直接转 map,出了一个类型转换异常,需要自定义一下转换规则,login 实现类中将 UserDTO 转换为 map 存入 redis 之前做个调整,将所有 value 都转为 String 类型,在存入map;

        // Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        // 忽略空值错误
                        .setIgnoreNullValue(true)
                        // 自定义转换器 因为 userDTO中的 id 为 long 类型
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

登录演示

redis中的数据

验证码

token

数据库数据:

前端页面 --- 登录成功 进入个人主页

解决状态登录刷新问题

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的。

优化方案

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,

在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了 ThreadLocal 的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

总的来说:

  • 第一个拦截器负责刷新 token 。
  • 第二个拦截器负责拦截需要登录的页面,校验用户当前是否登录。

代码修改

第一个拦截器 刷新token


/**
 * @author lhd
 * date 2024/7/26
 * @apiNote
 */
@Slf4j
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
        log.debug("拦截器被调用");
        String token = request.getHeader("authorization");
        System.out.println("从头获取到的token === " + token);
        if (StrUtil.isBlank(token)){
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3. 判断用户是否存在
        if (userMap.isEmpty()){
            return true;
        }
        // 5. 将查询到的 Hash 数据转为 UserDTO对象
        // 不忽略转换的错误
        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6. 保存用户信息到 ThreadLocal
        UserHolder.saveUser(user);
        // 7. 刷新 token 有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        log.debug("ThreadLocal 中的User为:" + user);
        // 8. 放行
        return true;
    }



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

第二个拦截器 拦截需要登录的

/**
 * @author lhd
 * date 2024/7/26
 * @apiNote
 */
@Slf4j
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;
    }

}

MVCconfig

设置 order 是为了让第一个拦截器先执行(0的优先级最大)。

/**
 * @author lhd
 * date 2024/7/26
 * @apiNote
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

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

注:

拦截器都不设置 order 的参数,就是默认为0, 默认就按照添加顺序实现拦截。

如果要实现第一个拦截器先执行,就把他放在前面。

代码如下:


        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**");
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
      

如果更加严谨的话就设置 order(0)

涉及到 order 的源码

进入

进入

找到order

 后期会更新使用阿里云发送短信!!!!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Boost_Searcher测试用例编写

功能描述&#xff1a; 用户在客户端页面&#xff0c;在搜索框输入关键词&#xff0c;页面将显示Boost库中所有包含该关键词的内容。 界面功能兼容性易用性安全性性能弱网安装/卸载 编写测试用例&#xff1a; 功能&#xff1a; 在浏览器搜索框中输入ip地址与端口号&#xff0…

MySQL的库操作和表操作

文章目录 MYSQLSQL语句分类服务器&#xff0c;数据库和表的关系 库操作表操作 MYSQL SQL语句分类 DDL【data definition language】 数据定义语言&#xff0c;用来维护存储数据的结构代表指令: create, drop, alterDML【data manipulation language】 数据操纵语言&#xff0…

Playwright 的使用

Playwright 的特点 支持当前所有主流浏览器&#xff0c;包括 Chrome 和 Edge &#xff08;基于 Chromiuns&#xff09;, Firefox , Safari 支持移动端页面测试&#xff0c;使用设备模拟技术&#xff0c;可以让我们在移动Web 浏览器中测试响应式的 Web 应用程序 支持所有浏览…

做一个能和你互动玩耍的智能机器人之四--固件

在openbot的firmware目录下我们能够找到arduino的固件源码和相关的文档。 openbot的controller目录下&#xff0c;是控制器的代码目录&#xff0c;用来控制机器人做一些动作。未来的目标是加入大模型&#xff0c;使其能够理解人的语言和动作来控制。 固件代码&#xff0c;支持…

数据结构 -- 算法的时间复杂度和空间复杂度

数据结构 -- 算法的时间复杂度和空间复杂度 1.算法效率1.1 如何衡量一个算法的好坏1.2 算法的复杂度 2.时间复杂度2.1 时间复杂度的概念2.2 大O的渐进表示法2.3常见时间复杂度计算举例 3.空间复杂度4. 常见复杂度对比 1.算法效率 1.1 如何衡量一个算法的好坏 如何衡量一个算法…

数据库实验:SQL Server基本表单表查询

一、实验目的&#xff1a; 1、掌握使用SQL语法实现单表查询 二、实验内容&#xff1a; 1. 查询订购日期为2001年5月22日的订单情况。&#xff08;Orders&#xff09;&#xff08;时间日期的表达方式为 dOrderDate ‘2001-5-22’&#xff0c;类似字符串&#xff0c;使用单引号…

Linux---git工具

目录 初步了解 基本原理 基本用法 安装git 拉取远端仓库 提交三板斧 1、添加到缓存区 2、提交到本地仓库 3、提交到远端 其他指令补充 多人协作管理 windows用户提交文件 Linux用户提交文件 初步了解 在Linux中&#xff0c;git是一个指令&#xff0c;可以帮助我们做…

Python爬虫-中国汽车市场月销量数据

前言 本文是该专栏的第34篇,后面会持续分享python爬虫干货知识,记得关注。 在本文中,笔者将通过某汽车平台,来采集“中国汽车市场”的月销量数据。 具体实现思路和详细逻辑,笔者将在正文结合完整代码进行详细介绍。废话不多说,下面跟着笔者直接往下看正文详细内容。(附…

【原创】使用keepalived虚拟IP(VIP)实现MySQL的高可用故障转移

1. 背景 A、B服务器均部署有MySQL数据库&#xff0c;且互为主主。此处为A、B服务器部署MySQL数据库实现高可用的部署&#xff0c;当其中一台MySQL宕机后&#xff0c;VIP可自动切换至另一台MySQL提供服务&#xff0c;实现故障的自动迁移&#xff0c;实现高可用的目的。具体流程…

微服务-MybatisPlus下

微服务-MybatisPlus下 文章目录 微服务-MybatisPlus下1 MybatisPlus扩展功能1.1 代码生成1.2 静态工具1.3 逻辑删除1.4 枚举处理器1.5 JSON处理器**1.5.1.定义实体****1.5.2.使用类型处理器** **1.6 配置加密&#xff08;选学&#xff09;**1.6.1.生成秘钥**1.6.2.修改配置****…

哪里可以查找短视频素材?6个素材查找下载渠道分享!

在短视频的风靡浪潮中&#xff0c;不少创作者纷纷投身于这一领域&#xff0c;无论是分享生活点滴还是进行商业宣传&#xff0c;高质量的短视频内容总能吸引众多观众的目光。然而&#xff0c;精良的短视频制作离不开优质的素材支持。本文将为大家介绍6个优秀的高质量短视频素材下…

ProxmoxPVE虚拟化平台--U盘挂载、硬盘直通

界面说明 ### 网络设置 ISO镜像文件 虚拟机中使用到的磁盘 挂载USB设备 这个操作比较简单&#xff0c;不涉及命令 选中需要到的虚拟机&#xff0c;然后选择&#xff1a; 添加->USB设置选择使用USB端口&#xff1a;选择对应的U盘即可 硬盘直通 通常情况下我们需要将原有…

前端Long类型精度丢失:后端处理策略

文章目录 精度丢失的具体原因解决方法1. 使用 JsonSerialize 和 ToStringSerializer2. 使用 JsonFormat 注解3. 全局配置解决方案 结论 开发商城管理系统的品牌管理界面时&#xff0c;发现一个问题&#xff0c;接口返回品牌Id和页面展示的品牌Id不一致&#xff0c;如接口返回的…

C/C++大雪纷飞代码

目录 写在前面 C语言简介 EasyX简介 大雪纷飞 运行结果 写在后面 写在前面 本期博主给大家带来了C/C实现的大雪纷飞代码&#xff0c;一起来看看吧&#xff01; 系列推荐 序号目录直达链接1爱心代码https://want595.blog.csdn.net/article/details/1363606842李峋同款跳…

Prime Land(牛客)

计算出n-1 对n-1进行质因数分解 // Problem: Prime Land // Contest: NowCoder // URL: https://ac.nowcoder.com/acm/contest/21094/D // Memory Limit: 524288 MB // Time Limit: 2000 ms // // Powered by CP Editor (https://cpeditor.org)#include<iostream> #in…

PAT1060它们是否相等

感谢松鼠爱葡萄 大佬代码太简洁了ilil #include <iostream> #include <cstring>using namespace std;string change(string a, int n) { // 找到小数点的位置&#xff0c;从0开始计数int k a.find("."); // 如果字符串中没有 "."&…

Linux 理解文件系统

查看文件信息 ls -l 每行包含7列&#xff1a; 模式硬链接数文件所有者组大小最后修改时间文件名 stat查看更多信息 硬盘抽象理解 注意&#xff1a; 一个block的大小是由格式化的时候确定的&#xff0c;并且不可以更改mke2fs的-b选项可以设定block大小为1024、2048或4096字节…

光盘文件系统 (iso9660) 格式解析

越简单的系统, 越可靠, 越不容易出问题. 光盘文件系统 (iso9660) 十分简单, 只需不到 200 行代码, 即可实现定位读取其中的文件. 参考资料: https://wiki.osdev.org/ISO_9660 相关文章: 《光盘防水嘛 ? DVDR 刻录光盘泡水实验》 https://blog.csdn.net/secext2022/article/d…

GD 32 UNIX时间戳

前言 ... UINX时间戳定义 UNIX时间戳是一种表示时间的方法&#xff0c;广泛用于计算机系统和网络协议中。它定义的时间起点是1970年1月1日午夜&#xff08;协调世界时UTC&#xff09;&#xff0c;也就是所谓的“UNIX纪元”开始的时刻。 Unix 时间戳(Unix Timestamp)定义为从U…