每日签到功能

假设你正在开发一个在线学习平台,平台需要支持用户的每日签到功能。为了鼓励用户连续签到,平台会根据用户的连续签到天数给予不同的奖励积分。签到记录存储在Redis中,使用位图(Bitmap)来表示每一天的签到情况。

请实现以下两个功能:

  1. 添加签到记录:用户签到后,记录签到信息并计算连续签到天数及相应的奖励积分。
  2. 查询签到记录:查询用户在当前月份的签到记录。
代码实现

请完成以下类 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;
    }
}
问题
  1. 请解释 addSignRecords 方法的实现逻辑。
  2. 请解释 querySignRecords 方法的实现逻辑。
  3. countSignDays 方法是如何计算连续签到天数的?
  4. 为什么在 addSignRecords 方法中使用了 mqHelper.send 发送消息?
  5. 如何确保用户不能重复签到?
参考答案
  1. addSignRecords 方法的实现逻辑:

    • 获取当前登录用户的ID和当前日期。
    • 生成Redis键,格式为 sign:uid:用户ID:年月
    • 计算当前日期的偏移量(即 当前天数 - 1)。
    • 使用 redisTemplate.opsForValue().setBit 方法在Redis中设置签到记录。如果已经签到过,则抛出异常。
    • 调用 countSignDays 方法计算连续签到天数。
    • 根据连续签到天数计算奖励积分。
    • 使用 mqHelper.send 方法发送消息到RabbitMQ,记录签到和奖励积分。
    • 创建 SignResultVO 对象,设置连续签到天数和奖励积分,返回给调用者。
  2. querySignRecords 方法的实现逻辑:

    • 获取当前登录用户的ID和当前日期。
    • 生成Redis键,格式为 sign:uid:用户ID:年月
    • 从Redis中读取签到记录。
    • 将读取到的整数转换为字节数组,每个字节表示一天的签到情况。
    • 返回字节数组,表示用户在当前月份的签到记录。
  3. countSignDays 方法是如何计算连续签到天数的:

    • 从Redis中获取指定键的签到记录。
    • 初始化一个计数器,用于记录连续签到天数。
    • 通过位运算逐位检查签到记录,如果某位为1表示当天签到,计数器加1;如果某位为0表示当天未签到,循环终止。
    • 返回连续签到的天数。
  4. 为什么在 addSignRecords 方法中使用了 mqHelper.send 发送消息:

    • 使用消息队列(如RabbitMQ)可以异步处理签到记录和奖励积分的保存,提高系统的响应速度和可靠性。这样可以避免在主线程中进行耗时的操作,确保用户体验。
  5. 如何确保用户不能重复签到:

    • addSignRecords 方法中,使用 redisTemplate.opsForValue().setBit 方法设置签到记录时,如果返回值为 true,表示该位置已经为1,即用户已经签到过。此时抛出异常,提示用户“不允许重复签到”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值