假设你正在开发一个在线学习平台,平台需要支持用户的每日签到功能。为了鼓励用户连续签到,平台会根据用户的连续签到天数给予不同的奖励积分。签到记录存储在Redis中,使用位图(Bitmap)来表示每一天的签到情况。
请实现以下两个功能:
- 添加签到记录:用户签到后,记录签到信息并计算连续签到天数及相应的奖励积分。
- 查询签到记录:查询用户在当前月份的签到记录。
代码实现
请完成以下类 SignRecordServiceImpl
的实现:
package com.tianji.learning.service.impl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BooleanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.DateUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.constants.RedisConstants;
import com.tianji.learning.domain.vo.SignResultVO;
import com.tianji.learning.mq.message.SignInMessage;
import com.tianji.learning.service.ISignRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SignRecordServiceImpl implements ISignRecordService {
private final StringRedisTemplate redisTemplate;
private final RabbitMqHelper mqHelper;
@Override
public SignResultVO addSignRecords() {
// 1.签到
// 1.1.获取登录用户
Long userId = UserContext.getUser();
// 1.2.获取日期
LocalDate now = LocalDate.now();
// 1.3.拼接key sign:uid:110:202301
String key = RedisConstants.SIGN_RECORD_KEY_PREFIX
+ userId
+ now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);
// 1.4.计算offset
int offset = now.getDayOfMonth() - 1;
// 1.5.保存签到信息
Boolean exists = redisTemplate.opsForValue().setBit(key, offset, true);
if (BooleanUtils.isTrue(exists)) {
throw new BizIllegalException("不允许重复签到!");
}
// 2.计算连续签到天数
int signDays = countSignDays(key, now.getDayOfMonth());
// 3.计算签到得分
//连续7天奖励10分 连续14天 奖励20 连续28天奖励40分, 每月签到进度当月第一天重置
int rewardPoints = 0;
switch (signDays) {
case 7:
rewardPoints = 10;
break;
case 14:
rewardPoints = 20;
break;
case 28:
rewardPoints = 40;
break;
}
// 4.保存积分明细记录
mqHelper.send(
MqConstants.Exchange.LEARNING_EXCHANGE,
MqConstants.Key.SIGN_IN,
SignInMessage.of(userId, rewardPoints + 1));
// 5.封装返回
SignResultVO vo = new SignResultVO();
vo.setSignDays(signDays);
vo.setRewardPoints(rewardPoints);
return vo;
}
@Override
public Byte[] querySignRecords() {
// 1.获取登录用户
Long userId = UserContext.getUser();
// 2.获取日期
LocalDate now = LocalDate.now();
int dayOfMonth = now.getDayOfMonth();
// 3.拼接key
String key = RedisConstants.SIGN_RECORD_KEY_PREFIX
+ userId
+ now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);
// 4.读取
List<Long> result = redisTemplate.opsForValue()
.bitField(key, BitFieldSubCommands.create().get(
BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if (CollUtils.isEmpty(result)) {
return new Byte[0];
}
int num = result.get(0).intValue();
Byte[] arr = new Byte[dayOfMonth];
int pos = dayOfMonth - 1;
while (pos >= 0) {
arr[pos--] = (byte) (num & 1);
// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位
num >>>= 1;
}
return arr;
}
private int countSignDays(String key, int len) {
// 1.获取本月从第一天开始,到今天为止的所有签到记录
List<Long> result = redisTemplate.opsForValue()
.bitField(key, BitFieldSubCommands.create().get(
BitFieldSubCommands.BitFieldType.unsigned(len)).valueAt(0));
if (CollUtils.isEmpty(result)) {
return 0;
}
int num = result.get(0).intValue();
// 2.定义一个计数器
int count = 0;
// 3.循环,与1做与运算,得到最后一个bit,判断是否为0,为0则终止,为1则继续
while ((num & 1) == 1) {
// 4.计数器+1
count++;
// 5.把数字右移一位,最后一位被舍弃,倒数第二位成了最后一位
num >>>= 1;
}
return count;
}
}
问题
- 请解释
addSignRecords
方法的实现逻辑。 - 请解释
querySignRecords
方法的实现逻辑。 countSignDays
方法是如何计算连续签到天数的?- 为什么在
addSignRecords
方法中使用了mqHelper.send
发送消息? - 如何确保用户不能重复签到?
参考答案
-
addSignRecords
方法的实现逻辑:- 获取当前登录用户的ID和当前日期。
- 生成Redis键,格式为
sign:uid:用户ID:年月
。 - 计算当前日期的偏移量(即
当前天数 - 1
)。 - 使用
redisTemplate.opsForValue().setBit
方法在Redis中设置签到记录。如果已经签到过,则抛出异常。 - 调用
countSignDays
方法计算连续签到天数。 - 根据连续签到天数计算奖励积分。
- 使用
mqHelper.send
方法发送消息到RabbitMQ,记录签到和奖励积分。 - 创建
SignResultVO
对象,设置连续签到天数和奖励积分,返回给调用者。
-
querySignRecords
方法的实现逻辑:- 获取当前登录用户的ID和当前日期。
- 生成Redis键,格式为
sign:uid:用户ID:年月
。 - 从Redis中读取签到记录。
- 将读取到的整数转换为字节数组,每个字节表示一天的签到情况。
- 返回字节数组,表示用户在当前月份的签到记录。
-
countSignDays
方法是如何计算连续签到天数的:- 从Redis中获取指定键的签到记录。
- 初始化一个计数器,用于记录连续签到天数。
- 通过位运算逐位检查签到记录,如果某位为1表示当天签到,计数器加1;如果某位为0表示当天未签到,循环终止。
- 返回连续签到的天数。
-
为什么在
addSignRecords
方法中使用了mqHelper.send
发送消息:- 使用消息队列(如RabbitMQ)可以异步处理签到记录和奖励积分的保存,提高系统的响应速度和可靠性。这样可以避免在主线程中进行耗时的操作,确保用户体验。
-
如何确保用户不能重复签到:
- 在
addSignRecords
方法中,使用redisTemplate.opsForValue().setBit
方法设置签到记录时,如果返回值为true
,表示该位置已经为1,即用户已经签到过。此时抛出异常,提示用户“不允许重复签到”。
- 在