基于Redis位图实现用户签到功能

场景需求

适用场景如签到送积分、签到领取奖励等,大致需求如下:
签到1天送1积分,连续签到7天,15天送,30天以上送不同积分等。
如果连续签到中断,则重置计数,每月初重置计数。
显示用户某个月的签到次数和首次签到时间。
在日历控件上展示用户每月签到情况,可以切换年月显示……等等

@Service
public class DailyCheckInServiceImpl implements DailyCheckInService {

    @Autowired
    private RedisService redisTemplate;


    /**
     * 用户签到
     *
     * @param aid  用户ID
     * @param date 日期
     * @return 之前的签到状态
     */
    @Override
    public boolean doSign(Long aid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;

        return redisTemplate.opsForValue().setBit(buildSignKey(aid, date), offset, true);
    }


    /**
     * 检查用户是否签到
     *
     * @param aid  用户ID
     * @param date 日期
     * @return 当前的签到状态
     */
    @Override
    public boolean checkSign(Long aid, LocalDate date) {
        int offset = date.getDayOfMonth() - 1;
        return redisTemplate.opsForValue().getBit(buildSignKey(aid, date), offset);
    }


    /**
     * 获取用户当月签到次数
     *
     * @param aid  用户ID
     * @param date 日期
     * @return 当月的签到次数
     */
    @Override
    public long getSignCount(Long aid, LocalDate date) {
        return redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(buildSignKey(aid, date).getBytes(StandardCharsets.UTF_8)));
    }

    @Override
    public long getContinuousSignCount(Long aid, LocalDate date) {
        int signCount = 0;
        List<Long> list = redisTemplate.opsForValue().bitField(buildSignKey(aid, date), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType
                .unsigned(date.getDayOfMonth())).valueAt(0));
        if (list != null && list.size() > 0) {
            // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 0; i < date.getDayOfMonth(); i++) {
                if (v >> 1 << 1 == v) {
                    // 低位为0且非当天说明连续签到中断了
                    if (i > 0) {
                        break;
                    }
                } else {
                    signCount += 1;
                }
                v >>= 1;
            }
        }
        return signCount;
    }

    /**
     * 获取当月首次签到日期
     *
     * @param aid  用户ID
     * @param date 日期
     * @return 首次签到日期
     */
    /**
     * 获取当月首次签到日期
     */
    @Override
    public LocalDate getFirstSignDate(Long aid, LocalDate date) {
        long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(buildSignKey(aid, date).getBytes(), true));
        return bitPosition < 0 ? null : date.withDayOfMonth((int) bitPosition + 1);
    }


    /**
     * 获取当月签到情况
     *
     * @param aid  用户ID
     * @param date 日期
     * @return Key为签到日期,Value为签到状态的Map
     */

    @Override
    public Map<String, Boolean> getSignInfo(Long aid, LocalDate date) {
        Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth());
        List<Long> list = redisTemplate.opsForValue().bitField(buildSignKey(aid, date), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType
                .unsigned(date.lengthOfMonth())).valueAt(0));

        if (!CollectionUtils.isEmpty(list)) {
            // 由低位到高位,为0表示未签,为1表示已签
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = date.lengthOfMonth(); i > 0; i--) {
                LocalDate d = date.withDayOfMonth(i);
                signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
                v >>= 1;
            }
        }
        return new TreeMap<>(signMap);

    }

    /**
     * 构建指定类型的Redis的key:u:sign:10000:202001
     */
    private static String buildSignKey(Long aid, LocalDate date) {
        return String.format("u:sign:%d:%s", aid, formatDate(date));
    }


    /**
     * 固定202205格式
     */
    private static String formatDate(LocalDate date) {
        return formatDate(date, "yyyyMM");
    }


    /**
     * LocalDate按照指定格式进行转换字符串
     */
    private static String formatDate(LocalDate date, String pattern) {
        return date.format(DateTimeFormatter.ofPattern(pattern));
    }
}

你可能感兴趣的:(redis,java,缓存)