前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CSDN是怎么实现用户签到,统计签到次数,连续签到天数等功能微服务的

CSDN是怎么实现用户签到,统计签到次数,连续签到天数等功能微服务的

作者头像
共饮一杯无
发布2022-12-25 10:16:24
2K0
发布2022-12-25 10:16:24
举报

文章目录

需求分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上图:CSDN每日签到,和每日练习打卡。 在很多互联网应用中,我们会存在签到送积分、签到领取奖励等这样的需求,比如:

  • 签到1天送10积分,连续签到2天送20积分,3天送30积分,4天以上均送50积分等。
  • 如果连续签到中断,则重置计数,每月初重置计数。
  • 显示用户某个月的签到次数。
  • 在日历控件上展示用户每月签到情况,可以切换年月显示。

设计思路

最简单的设计思路就是利用关系型数据库保存签到数据(t_user_sign),如下:

字段名

描述

id

数据表主键(AUTO_INCREMENT)

fk_user_id

用户ID

sign_date

签到日期(如2022-12-19)

amount

连续签到天数(如19)

  • 用户签到:往此表插入一条数据,并更新连续签到天数;
  • 查询根据签到日期查询
  • 统计根据 amount 统计

如果这样存数据的话,对于用户量比较大的应用,数据库可能就扛不住,比如1000W用户,一天一条,那么一个月就是3亿数据,这是非常庞大的,因此使用 Redis 的 Bitmaps 优化。 考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。key 的格式为 user:sign:userid:yyyyMM,value 则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。从高位插入,也就是说左边位算是开始日期。 与传统数据库存储空间对比:

在这里插入图片描述
在这里插入图片描述

例如 user:sign:98:202212 表示用户 id=98 的用户在2022年12月的签到记录。那么

代码语言:javascript
复制
# 2022年12月1号签到 
127.0.0.1:0>SETBIT user:sign:98:202212 0 1 
"1"      
# 2022年12月2号签到 
127.0.0.1:0>SETBIT user:sign:98:202212 1 1 
"1" 
# 2022年12月3号签到 
127.0.0.1:0>SETBIT user:sign:98:202212 2 1 
"0" 
# 2022年12月4号签到 
127.0.0.1:0>SETBIT user:sign:98:202212 3 1 
"1" 
# 获取2022年12月4号签到情况 
127.0.0.1:0>GETBIT user:sign:98:202212 3 
"1" 
# 统计2022年12月签到次数 
127.0.0.1:0>BITCOUNT user:sign:98:202212  
"4" 
# 获取2022年12月首次签到 
127.0.0.1:0>BITPOS user:sign:98:202212 1 
"0" 
# 获取2022年12月前3签到情况,返回7,二进制111,意味着前三天都签到了 
127.0.0.1:0>BITFIELD user:sign:98:202212 get u3 0 
"7" 
在这里插入图片描述
在这里插入图片描述

Bitmaps叫位图,它不是Redis的基本数据类型 (比如Strings、Lists、Sets、Hashes这类实际的数据类型),而是基于string数据类型的按位操作,高阶数据类型的一种。Bitmaps支持的最大位数是232位。使用512M内存就可以存储多达42.9亿的字节信息(2^32 = 4,294,967,296) 它是由一组bit位组成的,每个bit位对应0和1两个状态,虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作位图,可以把它看作是一个bit数组,数组的下标就是偏移量。它的优点是内存开销小、效率高目操作简单,很适合用于签到这类场景。比如按月进行存储,一个月最多31天,那么我们将该月用户的签到缓存二进制就是00000000000000000000000000000000,当某天签到将0改成1即可,而目Redis提供对bitmap的很多操作比如存储、获取、统计等指令,使用起来非常方便。

用户签到和统计连续签到的次数

用户签到,默认是当天,但可以通过传入日期补签,返回用户连续签到次数(后续如果有积分规则,就会返回用户此次签到积分)

签到控制层 SignController

代码语言:javascript
复制
    /**
     * 签到,可以补签
     *
     * @param access_token
     * @param date
     * @return
     */
    @PostMapping
    public ResultInfo sign(String access_token,
                           @RequestParam(required = false) String date) {
        int count = signService.doSign(access_token, date);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), count);
    }

签到业务逻辑层 SignService

逻辑如下:

  • 获取登录用户信息
  • 根据日期获取当前是多少号(使用BITSET指令关注时,offset从0开始计算,0就代表1号)
  • 构建用户按月存储key(user:sign:用户id:月份)
  • 判断用户是否签到(GETBIT指令)
  • 用户签到(SETBIT)
  • 返回用户连续签到次数(BITFIELD key GET [u/i] type offset value, 获取从用户从当前日期开始到1号的所有签到状态,然后进行位移操作,获取连续签到天数)
代码语言:javascript
复制
    /**
     * 用户签到
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public int doSign(String accessToken, String dateStr) {
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);
        // 获取日期对应的天数,多少号( 从 0 开始,0就代表1号)
        int offset = DateUtil.dayOfMonth(date) - 1;
        // 构建 Key user:sign:5:yyyyMM
        String signKey = buildSignKey(userInfo.getId(), date);
        // 查看是否已签到
        boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);
        AssertUtil.isTrue(isSigned, "当前日期已完成签到,无需再签");
        // 签到
        redisTemplate.opsForValue().setBit(signKey, offset, true);
        // 统计连续签到的次数
        int count = getContinuousSignCount(userInfo.getId(), date);
        return count;
    }

    /**
     * 统计连续签到的次数
     *
     * @param userId
     * @param date
     * @return
     */
    private int getContinuousSignCount(Integer userId, Date date) {
        // 获取日期对应的天数,多少号,假设是 30
        int dayOfMonth = DateUtil.dayOfMonth(date);
        // 构建 Key
        String signKey = buildSignKey(userId, date);
        // bitfield user:sign:5:202212 u30 0
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return 0;
        }
        int signCount = 0;
        long v = list.get(0) == null ? 0 : list.get(0);
        // i 表示位移操作次数
        for (int i = dayOfMonth; i > 0; i--) {
            // 右移再左移,如果等于自己说明最低位是 0,表示未签到
            if (v >> 1 << 1 == v) {
                // 低位 0 且非当天说明连续签到中断了
                if (i != dayOfMonth) {
                    break;
                }
            } else {
                signCount++;
            }
            // 右移一位并重新赋值,相当于把最低位丢弃一位
            v >>= 1;
        }
        return signCount;
    }

    /**
     * 构建 Key -- user:sign:5:yyyyMM
     *
     * @param userId
     * @param date
     * @return
     */
    private String buildSignKey(Integer userId, Date date) {
        return String.format("user:sign:%d:%s", userId,
                DateUtil.format(date, "yyyyMM"));
    }

    /**
     * 获取日期
     *
     * @param dateStr
     * @return
     */
    private Date getDate(String dateStr) {
        if (StrUtil.isBlank(dateStr)) {
            return new Date();
        }
        try {
            return DateUtil.parseDate(dateStr);
        } catch (Exception e) {
            throw new ParameterException("请传入yyyy-MM-dd的日期格式");
        }
    }

    /**
     * 获取登录用户信息
     *
     * @param accessToken
     * @return
     */
    private SignInUserInfo loadSignInUserInfo(String accessToken) {
        // 必须登录
        AssertUtil.mustLogin(accessToken);
        String url = oauthServerName + "user/me?access_token={accessToken}";
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        SignInUserInfo userInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                new SignInUserInfo(), false);
        if (userInfo == null) {
            throw new ParameterException(ApiConstant.NO_LOGIN_CODE, ApiConstant.NO_LOGIN_MESSAGE);
        }
        return userInfo;
    }

如何统计连续签到的次数:

在这里插入图片描述
在这里插入图片描述

测试

id为6的用户发起签到:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

id为6的用户发起重复签到:

在这里插入图片描述
在这里插入图片描述

补签19号:

在这里插入图片描述
在这里插入图片描述

21号再次签到,可以发现连续签到日期为3天。

在这里插入图片描述
在这里插入图片描述

按月统计用户签到的次数

用户需求:统计某月签到次数,默认是当月

签到控制层 SignController

代码语言:javascript
复制
    /**
     * 获取签到次数 默认当月
     *
     * @param access_token
     * @param date
     * @return
     */
    @GetMapping("count")
    public ResultInfo getSignCount(String access_token, String date) {
        Long count = signService.getSignCount(access_token, date);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), count);
    }

签到业务逻辑层 SignService

代码语言:javascript
复制
    /**
     * 获取用户签到次数
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public long getSignCount(String accessToken, String dateStr) {
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);
        // 构建 Key
        String signKey = buildSignKey(userInfo.getId(), date);
        // e.g. BITCOUNT user:sign:6:202212
        return (Long) redisTemplate.execute(
                (RedisCallback<Long>) con -> con.bitCount(signKey.getBytes())
        );
    }

测试

上述签到逻辑补签17号和22号:

在这里插入图片描述
在这里插入图片描述

发现连续签到日期为4天。 查询当月签到总天数为5天:

在这里插入图片描述
在这里插入图片描述

获取用户签到明细情况

获取用户某月签到情况,默认当前月,返回当前月的所有日期以及该日期的签到情况

签到控制层 SignController

代码语言:javascript
复制
    /**
     * 获取用户签到情况 默认当月
     *
     * @param access_token
     * @param dateStr
     * @return
     */
    @GetMapping
    public ResultInfo getSignInfo(String access_token, String dateStr) {
        Map<String, Boolean> map = signService.getSignInfo(access_token, dateStr);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), map);
    }

签到业务逻辑层 SignService

获取某月签到情况,默认当月

  • 获取登录用户信息
  • 构建Redis保存的Key
  • 获取月份的总天数(考虑2月闰、平年)
  • 通过BITFIELD指令获取当前月的所有签到数据
  • 遍历进行判断是否签到,并存入TreeMap方便排序
代码语言:javascript
复制
    /**
     * 获取当月签到情况
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public Map<String, Boolean> getSignInfo(String accessToken, String dateStr) {
        // 获取登录用户信息
        SignInUserInfo userInfo = loadSignInUserInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);
        // 构建 Key
        String signKey = buildSignKey(userInfo.getId(), date);
        // 构建一个自动排序的 Map
        Map<String, Boolean> signInfo = new TreeMap<>();
        // 获取某月的总天数(考虑闰年)
        int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1,
                DateUtil.isLeapYear(DateUtil.dayOfYear(date)));
        // bitfield user:sign:5:202011 u30 0
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);
        List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return signInfo;
        }
        long v = list.get(0) == null ? 0 : list.get(0);
        // 从低位到高位进行遍历,为 0 表示未签到,为 1 表示已签到
        for (int i = dayOfMonth; i > 0; i--) {
            /*
                签到:  yyyy-MM-01 true
                未签到:yyyy-MM-01 false
             */
            LocalDateTime localDateTime = LocalDateTimeUtil.of(date).withDayOfMonth(i);
            boolean flag = v >> 1 << 1 != v;
            signInfo.put(DateUtil.format(localDateTime, "yyyy-MM-dd"), flag);
            v >>= 1;
        }
        return signInfo;
    }

测试验证

上述测试验证时,我们已经在17,19,20,21,22号五天都签到了,继续补签2,4,6,8号后,查看当月签到明细:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到当前登陆用户在2,4,6,8,17,19,20,21,22号9天都进行了签到,当月总签到天数为9天,连续签到4天。

本文内容到此结束了, 如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。 如有错误❌疑问💬欢迎各位指出。 主页共饮一杯无的博客汇总👨‍💻 保持热爱,奔赴下一场山海。🏃🏃🏃

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 需求分析
  • 设计思路
  • 用户签到和统计连续签到的次数
    • 签到控制层 SignController
      • 签到业务逻辑层 SignService
        • 测试
        • 按月统计用户签到的次数
          • 签到控制层 SignController
            • 签到业务逻辑层 SignService
              • 测试
              • 获取用户签到明细情况
                • 签到控制层 SignController
                  • 签到业务逻辑层 SignService
                    • 测试验证
                    相关产品与服务
                    云数据库 Redis
                    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档