使用 Redisson + BitMap 实现签到
1、引入 Redisson 依赖
pom.xml
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置 Redisson
java
@Configuration
public class RedissonConfig {
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
3、签到功能
数据库
sql
CREATE TABLE `user_integral` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`integral` int(11) DEFAULT NULL COMMENT '用户积分',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `user_integral_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`integral_type` int(1) DEFAULT NULL COMMENT '积分类型:1.正常签到 2.连续签到获赠积分',
`integral` int(11) DEFAULT NULL COMMENT '积分',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
常量
java
public interface RedisKeyConstant {
/**
* 用户签到 Redis key 前缀
*/
String USER_SIGN_IN = "userSign";
/**
* 连续签到积分奖励配置,后续改为从数据库中获取
*/
Map<Integer, Integer> SIGN_CONFIGURATION = new HashMap<>() {{
put(5, 10);
put(7, 30);
put(15, 80);
put(32, 150);
}};
/**
* 用户普通签到所获取积分
*/
Integer SIGN_TYPE_NORMAL_INTEGRAL = 5;
}
服务类
下边实现了签到功能,并且可以获得连续签到天数
补签功能目前没有实现,不过也不难,在补签的接口中,只需要拿到 userId 和 day(当前日期) 即可,这样在 BitMap 中,把对应日期的 bit 设置为 1,再扣除补签所对应的代价即可。
java
@Service
@Slf4j
public class UserIntegralServiceImpl extends ServiceImpl<UserIntegralMapper, UserIntegral> implements UserIntegralService {
@Autowired
Redisson redisson;
@Autowired
private UserIntegralLogService userIntegralLogService;
@Override
public ResultBody sign(Integer userId) {
LocalDateTime now = LocalDateTime.now();
int year = now.getYear();
int month = now.getMonthValue();
int day = now.getDayOfMonth();
// usersign key 为 usersign:[userId][year][month]
RBitSet userSign = redisson.getBitSet(RedisKeyConstant.USER_SIGN_IN + userId + year + month);
// 进行签到
userSign.set(day);
log.info("{} 签到成功,key 为:{}", userId, RedisKeyConstant.USER_SIGN_IN + userId + year + month);
BitSet bs = userSign.asBitSet();
// 用户所得积分
int count = 0;
// 每次签到获得基础分数
count += RedisKeyConstant.SIGN_TYPE_NORMAL_INTEGRAL;
List<UserIntegralLog> userIntegralLogs = new ArrayList<>();
// 记录正常签到日志
UserIntegralLog userIntegralLog = new UserIntegralLog();
userIntegralLog.setIntegral(count);
userIntegralLog.setUserId(userId);
userIntegralLog.setIntegralType(UserSignTypeEnums.SIGN_NORMAL_INTEGRAL.getType());
userIntegralLog.setCreateTime(LocalDateTime.now());
userIntegralLogs.add(userIntegralLog);
int continousSignDays = getContinousSignDays(bs);
// 如果连续签到的天数等于当月的总天数的话,表明整月全部签到,用连续签到 32 天来标记
if (continousSignDays == LocalDate.now().lengthOfMonth()) {
continousSignDays = 32;
}
// 获取连续签到奖励配置
Map<Integer, Integer> signConfiguration = RedisKeyConstant.SIGN_CONFIGURATION;
/**
* 获取当前连续签到天数所对应的奖励,只有连续签到指定天数才会获得奖励
* 如果对应的连续签到天数没有奖励,那么获取的 continousIntegral 为 null
*/
Integer continousIntegral = signConfiguration.get(continousSignDays);
if (continousIntegral != null) {
// 如果是整月都签到,这里将连续签到天数设置为准确的该月天数
if (continousSignDays == 32) {
continousSignDays = LocalDate.now().lengthOfMonth();
}
// 记录连续签到获得奖励日志
UserIntegralLog continousIntegralLog = new UserIntegralLog();
continousIntegralLog.setIntegral(continousIntegral);
continousIntegralLog.setUserId(userId);
continousIntegralLog.setIntegralType(UserSignTypeEnums.SIGN_CONTINOUS_INTEGRAL.getType());
continousIntegralLog.setCreateTime(LocalDateTime.now());
userIntegralLogs.add(continousIntegralLog);
// count 为用户本次签到所得积分
count += continousIntegral;
}
boolean flag = updateUserIntegral(userId, count) && userIntegralLogService.saveBatch(userIntegralLogs);
return flag ? ResultBody.success("签到成功") : ResultBody.error("签到失败");
}
private boolean updateUserIntegral(Integer userId, int count) {
QueryWrapper<UserIntegral> qw = new QueryWrapper<>();
qw.eq("user_id", userId);
UserIntegral userIntegral = this.getBaseMapper().selectOne(qw);
// 如果之前没有记录用户积分,这里记录一下
if (userIntegral == null) {
userIntegral = new UserIntegral();
userIntegral.setUserId(userId);
userIntegral.setIntegral(0);
userIntegral.setCreateTime(LocalDateTime.now());
this.getBaseMapper().insert(userIntegral);
}
userIntegral.setIntegral(userIntegral.getIntegral() + count);
int i = this.getBaseMapper().updateById(userIntegral);
return i > 0;
}
@Override
public Map<Integer, Boolean> getSignMap(Integer userId) {
LocalDateTime now = LocalDateTime.now();
int year = now.getYear();
int month = now.getMonthValue();
int day = now.getDayOfMonth();
RBitSet userSign = redisson.getBitSet(String.format(RedisKeyConstant.USER_SIGN_IN, month, day));
BitSet bs = userSign.asBitSet();
Map<Integer, Boolean> map = new HashMap<>();
for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
// operate on index i here
if (i == Integer.MAX_VALUE) {
break; // or (i+1) would overflow
}
map.put(i, true);
}
/**
* 先通过 Map 存储连续签到对应的积分
* 1. 通过连续签到天数拿到对应需要得到的积分
* 2. 日志记录积分
*/
return map;
}
public int getContinousSignDays(BitSet bs) {
List<Integer> list = new ArrayList<>();
for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
// operate on index i here
if (i == Integer.MAX_VALUE) {
break; // or (i+1) would overflow
}
list.add(i);
}
int day = LocalDateTime.now().getDayOfMonth();
int lastDay = day;
int continousSignDay = 0;
if (list.get(list.size() - 1) == day) {
for (int i = list.size() - 1; i >= 0; i --) {
if (list.get(i) == lastDay) {
continousSignDay ++;
lastDay --;
} else {
break;
}
}
}
return continousSignDay;
}
}
Redis 原生实现签到
- 签到
我们先来设计 key:userid:yyyyMM
,那么假如 usera 在2023年10月3日和2023年10月4日签到的话,使用以下命令:
setbit key offset value
:
在3日签到的话,偏移量应该设置为2,则签到之后为 001
在4日签到的话,偏移量应该设置为3,则3日、4日签到后为0011
(第3位和第4位都是1,表示这两天签到了)
在31日签到的话,偏移量应该设置为30,表示向右偏移30位
bash
127.0.0.1:6379> setbit userx:202310 2 1
(integer) 0
127.0.0.1:6379> setbit userx:202310 3 1
(integer) 0
127.0.0.1:6379> setbit userx:202310 30 1
(integer) 0
- 查看2023年10月哪些天签到了,10月有31天,所以使用
u31
(31位无符号整数),后边偏移量为0
bash
127.0.0.1:6379> bitfield userx:202310 get u31 0
1) (integer) 402653185
通过 bitfield [key] get u31 0
获取了31位的无符号整数,将该整数402653185
转为二进制如下:(可以发现从左向右第4位和第5位位1,表示这两天签到了,也就是3号签到的时候,在setbit key value offset
中,偏移量设置了为3,所以向右偏移3位,在第4位上)
402653185 十进制
0011000000000000000000000000001 二进制
通过取出来这个无符号整数,我们在代码中就可以通过位运算来判断某一天是否签到,可以看出,第3、4、31位都是1,表明在这三天都进行了签到
那么如果今天是10月26日,我们想知道10月1日-10月26日有哪些天进行签到,使用如下命令:
bash
127.0.0.1:6379> bitfield userx:202310 get u26 0
1) (integer) 12582912
将 12582912
转为二进制为:
26位无符号整数:
00110000000000000000000000
那么我们在计算连续签到的次数就可以使用以下方法:
java
public Integer getContinuousSignCount() {
/*
假如今天是 10月26日
假如通过 redis 的 bitfield userx:202310 get u26 0 命令得到了从1日-26日的签到数据为:12582912
*/
Long v = 12582912;
// 这里 26 为今天的日期
Integer signCount = 0;
for (int i = 0; i < 26; i ++) {
if (v & 1 == 0) return signCount;
signCount ++;
v >>= 1;
}
}