1.流程分析

1.1发送短信验证码

        提交手机号的时候要进行校验手机号,校验成功才会去生成验证码,将验证码保存到session,发生他把这部分那。

1.2短信验证码登录/注册

        如果提交手机号和验证码之后,校验一致才进行根据手机号查询用户,进行创建新用户/登录成功,将信息保存到session进行返回。

1.3校验登录状态

        前端传递过来cookie,携带其中的sessionID,从session中获取到用户信息,校验是否存在,存在就将用户保存到TheadLocal,不存在就拦截。

2.实现发送短信验证码

        利用一些封装的工具生成验证码和校验手机号。

        利用session进行存储验证码,方便校验,比较不错。

@Override
public Result sendCode(String phone, HttpSession session) {if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 3. 生成验证码String code = RandomUtil.randomNumbers(6);// 2. 保存验证码到sessionsession.setAttribute(LOGIN_CODE_KEY + phone, code);// 发送验证码// todo 实现发送验证码log.debug("发送短信验证码成功: {}", code);// 返回OKreturn Result.ok();
}

3.短信验证码登录

        最重要的可能是封装的思想吧,封装一定的常量和借助mybatis-plus的高级的功能。

        重点:抽取逻辑,mybatis-plus高级功能。

/*** 实现登录功能** @param loginForm* @param session* @return*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1. 校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确");}// 2. 校验验证码// 从session中获取到验证码String phoneForCode = (String) session.getAttribute(LOGIN_CODE_KEY + phone);// 校验验证码String code = loginForm.getCode();if (!StrUtil.equals(code, phoneForCode)) {return Result.fail("验证码错误!");}// 3. 查询用户是否存在// QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();// userQueryWrapper.eq("phone", phone);// User user = userMapper.selectOne(userQueryWrapper);User user = query().eq("phone", phone).one();// 不存在则注册if (user == null) {user = createUserWithPhone(phone);}// 4. 保存信息到session中session.setAttribute("user", user);return Result.ok();
}/*** 封装一个创建用户的逻辑** @param phone* @return*/
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;
}

4.登录验证功能

        需要封装一个登录校验的功能供前端进行调取使用。

        重点:通过SpringMVC进行拦截请求,封装数据到ThreadLocal中。

        使用SpringMVC统一拦截请求可以方便将数据存储到ThreadLocal中,这样就无需在每个接口中进行配置了十分方便。

        ThreadLocal是每个tomcat创建的请求线程中独有的,不会被其它线程访问到的。

        封装拦截器,从session中获取到数据即可,最后一定要在请求后拦截器中将ThreadLocal中的数据删除。

package com.hmdp.utils;import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.beans.BeanUtils;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取sessonHttpSession session = request.getSession();// 2. 获取用户信息User user = (User) session.getAttribute("user");// 3. 处理用户不存在if (user == null) {response.setStatus(401);return false;}// 4. 存储数据到ThreadLocalUserDTO userDTO = new UserDTO();BeanUtils.copyProperties(user, userDTO);UserHolder.saveUser(userDTO);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

        将拦截器配置到SpringBoot中,可以进行配置什么路径需要排除,什么无需排除。

/*** MVC拦截器配置*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/shop-type/**","/upload/**","/voucher/**","/blog/hot","/user/code","/user/login").order(1);}
}

        通过拦截器可以完成登录校验功能。

5.集群session共享问题

        多台tomcat是并不进行共享session存储空间的,虽然tomact提供了将session共享到多台tomcat,但是这样性能太差了,还会有很多问题,所以为了进行实现分布式服务器tomcat共享session,建议使用redis进行替代session,这样就可以做到分布式session了。

6.基于redis实现

        其实就是使用redis去利用键值对的形式进行存储用户的信息.

        key的设计:项目名:业务名:类型:id。

        这里只使用项目名,类型和id,使用这种结构式key可以大大的帮助到我们。

        其实就是把使用session的部分换为使用redis了,存储key-value,取出key-value都使用redis即可。

        重要点:使用token令牌替代了cookie。

        使用redis的时候,一定要使用token吗?未必的,只是说将token作为一个帮助客户端和服务端之间进行身份认证的手段,完全也可以进行使用分布式session,使用redis存储session,前端依然携带cookie而来,所以这只是一种手段而已,各有所长。

6.1获取验证码

        session => redis。

        没有太多亮点,就简单分析一下key的构造吧。

        login:code:phone,典型的业务+类型+分辨标识key,这样就很好的能架构出合理的key了。

public Result sendCode(String phone, HttpSession session) {if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式错误!");}// 1. 生成验证码String code = RandomUtil.randomNumbers(6);// 2. 保存验证码到session// session.setAttribute(LOGIN_CODE_KEY, code);// 2. 保存验证码到redis// 构造keyString key = LOGIN_CODE_KEY + phone;stringRedisTemplate.opsForValue().set(key, code, LOGIN_CODE_TTL, TimeUnit.SECONDS);// 3. 发送验证码// todo 实现发送验证码log.debug("发送短信验证码成功: {}", code);// 返回OKreturn Result.ok();
}

6.2注册/登录

public Result login(LoginFormDTO loginForm, HttpSession session) {// 1. 校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确");}// 2. 校验验证码// 从session中获取到验证码// String phoneForCode = (String) session.getAttribute(LOGIN_CODE_KEY);// 从redis中获取到验证码String codeKey = LOGIN_CODE_KEY + phone;String phoneForCode = stringRedisTemplate.opsForValue().get(codeKey);// 校验验证码String code = loginForm.getCode();if (!StrUtil.equals(code, phoneForCode)) {return Result.fail("验证码错误!");}// 3. 查询用户是否存在// QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();// userQueryWrapper.eq("phone", phone);// User user = userMapper.selectOne(userQueryWrapper);User user = query().eq("phone", phone).one();// 不存在则注册if (user == null) {user = createUserWithPhone(phone);}// 4. 保存信息到session中// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));// 4. 保存信息到redis中// 4.1 随机生成token, 作为登录令牌String token = UUID.randomUUID().toString();// 4.2 将User对象转换为Hash存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 4.3 存储数据到redisString userKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(userKey, userMap);// 4.4 设置stringRedisTemplate.expire(userKey, LOGIN_USER_TTL, TimeUnit.MINUTES);return Result.ok(token);
}

6.2.1key的设计

        其中key的设计是login:token:token,也是遵循的业务+类型+标识的设计思想。

6.2.2token的设计

        这里的token很值得分析一下,尤其是可以对比苍穹外卖的进行分析。

        这里的key仅仅是用来作为一个获取redis中数据使用的,并不是加密携带payload负载数据的,

        其实就是使用了token替代了sessionID,但是其实还是有一个更为方便的解决方法的。

6.2.3存储hash数据到redis中的注意事项

1.使用什么API?

        使用的是StringRedisTemplate中的opsForHash.putAll(),这个方法接收两个参数,key => 字符串,value => Map,它可以将Map中的key-value全部存入redis的hash中,十分方便。

2.Map中数据规范是什么样的?

        由于我们进行使用的Redis客户端是stringRedisTemplate,这就限制了我们存储hash数据的时候,map中的key-value都必须是string类型的,如果出现了其它类型:比如Long类型,就会抛出错误,所以我们在将DTO转换为Map的时候,必须对value进行处理。

        借助hutool工具类中的BeanUtil.beanToMap就可以完成这个操作。

UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setFieldValueEditor((filedName, filedValue) -> filedValue.toString()));

6.2.4Redis中Key设置时间的问题

        使用stringRedisTemplate中的expire进行设置key的存活时间,传入key,time,TimeUnit即可。

6.3登录认证拦截器

        我们需要将以前使用session进行将数据取出存入ThreadLocal的逻辑变更为使用前端传递来的token进行获取数据,从redis中获取用户数据DTO进行使用。

6.3.1思考:没有被SpringIOC托管的对象如何注入Bean

        鉴于我们的登录拦截器配置类是我们自定义的,并且没有托管到SpringIOC容器,所以我们不能使用Resouce/Autowired。

        那应该怎么办呢?我们发现MVC拦截器配置类是使用@Configuration进行注解的,这个类会被托管到SpringIOC容器,而且我们的自定义拦截器也被该类实例化了一个对象,所以完全可以通过该类将Bean在自定义拦截器实例化的时候,传递进来。

        在构造函数被回调的时候,接收StringRedisTemplate对象,进行赋值给自身字段。

/*** 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}}

        将StringRedisTemplate进行注入到MVC配置类中,在调用拦截器构造函数的时候进行注入StringRedisTemplate进去即可。

/*** MVC拦截器配置*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/shop/**","/shop-type/**","/upload/**","/voucher/**","/blog/hot","/user/code","/user/login").order(1);}
}

6.3.2整体登录校验流程

/*** 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取sesson// HttpSession session = request.getSession();// 1. 获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {response.setStatus(401);return false;}// 2. 获取用户信息// UserDTO user = (UserDTO) session.getAttribute("user");String userKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(userKey);// 3. 处理用户不存在if (userMap.isEmpty()) {response.setStatus(401);return false;}// 4. 将用户数据map -> userDTOUserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 5. 存储数据到ThreadLocalUserHolder.saveUser(user);// 6. 刷新token有效期stringRedisTemplate.expire(userKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 7. 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

        1.整体流程:先从request请求头中获取到authorization中的token数据 => 然后封装一个userkey => 然后封装出来一个KEY,在redis客户端中获取到userDTO数据 => 然后处理用户不存在(redis获取不到数据的情况)=> 将用户数据map转换为userDTO => 将用户DTO存储到ThreadLocal中即可 => 最后进行刷新token有效期。

6.3.3刷新token时间

        token在一定时间后会过期,但是如果在用户持续使用的过程中过期,那真是一个糟糕的事件,所以要采用拦截器刷新token时间的方式,这样就是在用户持续使用的时候,可以帮用户进行刷新token,延期,不会导致用户持续使用的时候过期。

        在拦截器中进行token续期,是一个非常聪明的决策,这里采用的续期策略是,只要发送了请求,在token有效期内还在使用,就将时间续期到原始状态。

// 6. 刷新token有效期
stringRedisTemplate.expire(userKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

7.登录拦截器和刷新缓存拦截器如何设置?

7.1拆分的思路

        主要是因为有一些请求是打不到登录拦截器的,一些不需要登录的请求根本打不到登录校验拦截器,所以我们不能将刷新token时间放在登录拦截器中去做,因为这样就会有可能用户看的都是不需要登录的接口数据,这样就会导致token无法进行续期,有可能出现用户看着看着就token过期了,所以为了避免这种情况的发生,可以进行设置两个拦截器:1.刷新token拦截器,所有接口都可以进行刷新token请求,当用户进行发送请求之后,从redis中获取到用户的token数据(因为前端会将token传递过来),当查询到数据的时候,就会去更新token时间,如果有数据将数据存储到ThreadLocal中。2.登录状态拦截器,仅仅进行拦截需要登录状态才能进行访问的接口,在拦截器中进行看一下ThreadLocal是否有数据,有就放行,无则滚蛋。

7.2实现token刷新拦截器

/*** 更新拦截器*/
public class RefreshInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取sesson// HttpSession session = request.getSession();// 1. 获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2. 获取用户信息// UserDTO user = (UserDTO) session.getAttribute("user");String userKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(userKey);// 3. 处理用户不存在if (userMap.isEmpty()) {return true;}// 4. 将用户数据map -> userDTOUserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 5. 存储数据到ThreadLocalUserHolder.saveUser(user);// 6. 刷新token有效期stringRedisTemplate.expire(userKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 7. 放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

7.3实现登录拦截器

/*** 登录拦截器*/
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (UserHolder.getUser() == null) {response.setStatus(401);return false;}return true;}}

7.4SpringMVC配置拦截器

        拦截器的优先级:可以在registry注册的时候进行指定order,里面的排序数越小的越先执行,如果不进行指定优先级,就按代码的顺序进行注册,越靠上进行注册的拦截器,越先执行。

        这里将刷新token的拦截器放在最前面,指定的顺序数是最小的。

/*** MVC拦截器配置*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/shop-type/**","/upload/**","/voucher/**","/blog/hot","/user/code","/user/login").order(1);registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

疯狂星期四文案网第47天运营日记

网站运营第47天&#xff0c;点击观站&#xff1a; 疯狂星期四 crazy-thursday.com 全网最全的疯狂星期四文案网站 运营报告 今日访问量 今日搜索引擎收录情况 必应现在是边收录边k页面 百度快倒闭 网站优化点 完善工作流&#xff0c;全面实现文案自动化采集&#xff0c;se…

Vue生命周期以及自定义钩子和路由

Vue生命周期常用的onMounted挂载后执行和onUnmounted卸载前以及onupdated更新后实际上用react对比就是useEffect&#xff0c;而且挂载顺序也是子组件先于父组件然后往外的栈结构&#xff0c;先进后出。1.Vue的生命周期<template><h2>当前求和为{{ sum }}</h2>…

探索Thompson Shell:Unix初代Shell的智慧

引言 在计算机科学的漫漫长河中&#xff0c;Thompson Shell 无疑占据着举足轻重的开创性地位&#xff0c;它是 Unix 系统的第一个 shell&#xff0c;诞生于 1971 年&#xff0c;由计算机领域的传奇人物 Ken Thompson 开发。在那个计算机技术刚刚起步、硬件资源极度匮乏的年代&a…

MySQL B+ 树索引详解:从原理到实战优化

引言在现代数据库应用中&#xff0c;查询效率是影响系统性能的关键因素之一。而索引&#xff0c;尤其是 B 树索引&#xff0c;是 MySQL 中最常用、最重要的性能优化手段。正确使用索引可以将查询时间从毫秒级降低到微秒级&#xff0c;极大地提升应用响应速度。1. B 树索引的重要…

计算机内存中的整型存储奥秘、大小端字节序及其判断方法

目录 一、回顾与引入&#xff1a;整数在内存中的存储方式 为什么要采用补码存储&#xff1f; 二、大小端字节序及其判断方法 1、什么是大小端&#xff1f; 2、为什么存在大小端&#xff1f; 3、练习 练习1&#xff1a;简述大小端概念并设计判断程序&#xff08;百度面试…

Redis 最常用的 5 种数据类型

Redis 支持多种灵活的数据类型&#xff0c;每种类型针对特定场景优化。以下是 **Redis 最常用的 5 种数据类型**及其核心特点和应用场景&#xff1a;1. 字符串&#xff08;String&#xff09;描述&#xff1a;最基本的数据类型&#xff0c;可存储文本、数字&#xff08;整数/浮…

【嵌入式】RK3588 对比 NVIDIA Jetson,Radxa Rock 5B vs Orange Pi 5 Max

RK3588这个芯片,适合AI应用么,为什么这么贵呢 AI 边缘盒子里的旗舰芯 深度分析一下 RK3588(瑞芯微 Rockchip RK3588) 为什么被很多人关注在 AI 应用,以及它价格偏高的原因。 🧩 1. RK3588 的基本情况 制程:8nm(Samsung 8nm LP) CPU:8 核 big.LITTLE 架构(4 Cortex-…

暴雨让高性能计算更“冷静”

当AI大模型的参数突破万亿&#xff0c;当深地探测的精度迈向微米&#xff0c;当数字经济的脉搏与千行百业深度共振&#xff0c;算力已成为驱动时代向前的核心引擎。然而&#xff0c;传统风冷技术在高密度算力需求面前渐显乏力——机柜内的热浪如同无形的枷锁&#xff0c;既制约…

SpringAI集成MCP

文章目录1_调用公用MCP2_Stdio方式3_Stdio实现原理4_SSE方式5_自定义MCP客户端6_MCP Server权限控制SpringAI 通过 SpringBoot 集成扩展了 MCP Java SDK &#xff0c;提供了客户端和服务端 starter&#xff0c;让 AI 应用程序快速支持 MCP。接下来直接演示。 1_调用公用MCP 在…

Spring Start Here 读书笔记:第10章 Implementing REST services

REST 服务可用于实现两个应用之间的通讯&#xff0c;包括 Web 应用中的客户端和服务器之间&#xff0c;移动应用与后端服务之间&#xff0c;或两个后端服务之间。 10.1 使用 REST 服务在应用之间交换数据 REST端点是应用程序通过 Web 协议公开服务的方式&#xff0c;因此也称…

SYBASE ASE、Oracle、MySQL/MariaDB、SQL Server及PostgreSQL在邮件/短信发送功能上的全面横向对比报告

以下是对SYBASE ASE、Oracle、MySQL/MariaDB、SQL Server及PostgreSQL在邮件/短信发送功能上的全面横向对比报告&#xff08;截至2025年8月最新版本&#xff09;&#xff0c;涵盖技术实现、配置复杂度、适用场景及权威评测&#xff1a;​​一、邮件发送能力对比​​​​1. Orac…

服务器与客户端

目录 一、服务器&#xff08;Server&#xff09; 核心特点 常见类型 二、客户端&#xff08;Client&#xff09; 核心特点 常见类型 客户端与服务器的交互流程 补充&#xff1a;与 “对等网络&#xff08;P2P&#xff09;” 的区别 C/S模式 一、C/S 模式的核心原理 …

GaussDB 并发自治事务数达到最大值处理案例

1 业务背景自治事务&#xff08;Autonomous Transactions&#xff09;是一种高级特性&#xff0c;允许你在一个事务中执行另一个独立的事务。这种机制特别有用&#xff0c;尤其是在需要在一个事务中执行多个操作但又不想因为其中一个操作失败而影响整个事务的场景。2 业务影响在…

【传奇开心果系列】Flet分页自定义组件CustomPaginationComponent封装版自定义模板

Flet分页自定义组件CustomPaginationComponent封装版自定义模板一、效果展示GIF动图二、应用场景三、特色说明四、源码下载地址一、效果展示GIF动图 二、应用场景 图片浏览应用&#xff1a; 用户可以通过分页组件浏览多张图片&#xff0c;每点击一次“上一页”或“下一页”按钮…

数据安全——39页解读数字化转型大数据安全基础培训方案【附全文阅读】

适应人群为企业数据安全管理人员、IT 运维人员、数字化转型决策者、网络安全工程师及关注大数据安全的从业人员。主要内容围绕数字化转型中大数据安全展开,核心包括基础概念(信息、数据与大数据的定义及区别,大数据 4V 特点与来源);安全风险(企业面临的数据资产管理缺失、…

week3-[二维数组]小方块

week3-[二维数组]小方块 题目描述 如果四个数 a,b,c,da,b,c,da,b,c,d 可以分成两组&#xff0c;每组两个数&#xff0c;满足每组里面的两个数一样&#xff0c;那么称这四个数是好的。 比如&#xff0c;2,5,2,52,5,2,52,5,2,5 是好的&#xff0c;因它满足两组&#xff1a;222 与…

Swift 项目结构详解:构建可维护的大型应用

Swift 项目结构详解&#xff1a;构建可维护的大型应用一、基础结构&#xff08;推荐新手使用&#xff09;二、组件化结构&#xff08;企业级应用推荐&#xff09;层级架构&#xff1a;MVVM Coordinator路由实现&#xff08;Coordinator模式&#xff09;三、通用组件实现DI&…

【实时Linux实战系列】基于实时Linux的数字转换器设计

在现代电子系统中&#xff0c;数字转换器&#xff08;如模数转换器ADC和数模转换器DAC&#xff09;扮演着至关重要的角色。它们负责将模拟信号转换为数字信号&#xff0c;或将数字信号转换为模拟信号&#xff0c;从而实现信号的数字化处理和传输。在实时系统中&#xff0c;如工…

FastTracker:实时准确的视觉跟踪

摘要 https://arxiv.org/pdf/2508.14370 传统的多目标跟踪(MOT)系统主要设计用于行人跟踪&#xff0c;通常对其他物体类别的泛化能力有限。本文提出了一种能够处理多种物体类型的通用跟踪框架&#xff0c;特别强调在复杂交通场景中的车辆跟踪。所提出的1方法包含两个关键组件&a…

国产轻量级桌面GIS软件Snaplayers从入门到精通(20)

国产轻量级桌面GIS软件Snaplayers实操&#xff1a;打开图层并显示属性信息1、根据数据格式选择图层文件2、加载图层到地图中&#xff0c;并在左侧显示图层的属性表格3、属性表格分页显示Snaplayers研发团队承诺&#xff1a;国产轻量级桌面GIS软件Snaplayers永久免费并持续更新