学习目标:
来源:黑马教程
使用Redis中BitMap数据结构使用签到功能和连续签到功能
学习产出:
解决方案:
1. 准备pom环境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.1</version>
</dependency>
2. 配置ThreadLocal和过滤器
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();
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redis;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**").order(2);
registry.addInterceptor(new RefreshTokenInterceptor(redis)).addPathPatterns("/**").order(1);
}
}
---------------------------------------------
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
//controller执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.判断是否需要拦截ThreadLocal
if (UserHolder.getUser()==null) {
response.setStatus(401);
return false;
}
//7.放行
return true;
}
//渲染后返回给前台数据前
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户,避免内存泄露
UserHolder.removeUser();
}
}
---------------------------------------------------
@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {
//这个对象不是由spring管理的所以不能用注解自动注入
private StringRedisTemplate redis;
public RefreshTokenInterceptor(StringRedisTemplate redis) {
this.redis = redis;
}
//controller执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
//2.基于token获取redis中的用户
//通过key取到hash中的map集合数据
Map<Object, Object> userMap = redis.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
//5.将查询到的hash数据转为userDto对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6.存在,保存用户信息到ThreadLocal中
UserHolder.saveUser(userDTO);
//7.刷新token有效期
redis.expire(LOGIN_USER_KEY + token, 30, TimeUnit.MINUTES);
log.info("我是第一个拦截器当前拦截所有请求的用户为,线程为{},{}",UserHolder.getUser(),Thread.currentThread());
//8.放行
return true;
}
3. Controller层:负责接收请求和向下分配
@RestController
@RequestMapping("/user")
public class UserController{
@Resource
private IUserService userService;
@PostMapping("/sign")
public Result sign(){
return userService.sign();
}
}
4. Service层:负责业务的处理逻辑签到功能
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
private static final ObjectMapper mapper = new ObjectMapper();
@Resource
private StringRedisTemplate redis;
@Override
public Result sign() {
//1. 获取当前登录用户Id
Long userId = UserHolder.getUser().getId();
//2.获取日期:Redis中的offset偏移量是从0开始算,偏移量n代表的这个月的第n+1天
int offset = LocalDateTime.now().getDayOfMonth() - 1;
//3.拼接key存入Redis
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = "sign:" + userId + keySuffix;
//true代表的是存入BitMap中的二进制是1
redis.opsForValue().setBit(key, offset, true);
return Result.ok();
}
}
`5. Service层实现连续签到功能`
```java
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
private static final ObjectMapper mapper = new ObjectMapper();
@Resource
private StringRedisTemplate redis;
@Override
public Result signCount() {
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime nowTime = LocalDateTime.now();
//3.拼接key
String keySuffix = nowTime.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = "sign:" + userId + keySuffix;
//4.获取今天是第几天
int day = nowTime.getDayOfMonth();
//5.获取本月截至今天为止所有的签到记录,返回的是一个十进制数字
List<Long> result = redis.opsForValue().bitField(
key,
BitFieldSubCommands.create().get(
BitFieldSubCommands.BitFieldType.unsigned(day)
).valueAt(0)
);
if (result.isEmpty() || result == null) {
return Result.ok(0);
}
Long signCount = result.get(0);
if (signCount == null || signCount == 0) {
return Result.ok(0);
}
//6.遍历
int count=0;
while (true) {
if ((signCount & 1) == 0) {
//不为0,跳出循环
break;
} else {
count++;
signCount = signCount >> 1;
}
}
//7.签到记录返回的十进制数字与1做 与运算
return Result.ok(count);
}
}