功能"/>
Redis实现签到功能
使用 Redisson + BitMap 实现签到
1、引入 Redisson 依赖
pom.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
@Configuration
public class RedissonConfig {@Beanpublic Redisson redisson() {// 此为单机模式Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);return (Redisson) Redisson.create(config);}
}
3、签到功能
数据库
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;
常量
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,再扣除补签所对应的代价即可。
@Service
@Slf4j
public class UserIntegralServiceImpl extends ServiceImpl<UserIntegralMapper, UserIntegral> implements UserIntegralService {@AutowiredRedisson redisson;@Autowiredprivate UserIntegralLogService userIntegralLogService;@Overridepublic 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;}@Overridepublic 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 hereif (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 hereif (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位
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
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日有哪些天进行签到,使用如下命令:
127.0.0.1:6379> bitfield userx:202310 get u26 0
1) (integer) 12582912
将 12582912
转为二进制为:
26位无符号整数:
00110000000000000000000000
那么我们在计算连续签到的次数就可以使用以下方法:
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;}
}
更多推荐
Redis实现签到功能
发布评论