打卡功能的设计

一、引言

打卡功能对于一个系统来说还算是挺重要的,我们可以通过用户打卡来给用户一些激励,也可以提高我们系统的用户活跃度。

因此这篇文章就介绍一下我对打卡功能的一个设计流程。

二、功能

一般打卡功能需要包括三个部分:每日签到、补签、历史签到记录。

除了上述三个功能之外,我们还需要在此基础上得到最大签到和连续签到两个指标。

三、设计

对于上述功能,我们应该采用什么方式来进行打卡记录的存储呢?

  1. 直接用户打卡一次,就存储一条打卡记录
  2. 因为打卡就两种状态,使用long来进行存储

一般来说就这两种,我们来分析一下这两种存储方式的优缺点,假如我们不引入其他的中间键例如redis:

        直接存储的优点:在数据库中查找插入签到记录很方便。

        直接存储的缺点:数据量大,打卡记录可以更加简便的存储。

        long存储的优点:数据量小,可以很方便的根据月份来进行划分。

        long存储的缺点:mysql对long的支持不太够,每次查询和插入需要对long值进行转换后才能得到数据(使用redis后可以大幅度减小这种情况)。

综上所述,我们使用long来进行存储,配合Redis来提示效率。

1、打卡

已经确定了存储方案,我们可以来进行一个功能的设计了,首先我们知道Redis中有一个数据类型叫Bitmap,它可以储存bit,并且它可以很快的给我们返回指定索引位置的情况,而且插入也很方便。我们可以根据月份来进行存储,打卡bit就设置成1,否则为0。

具体代码如下:

// 前端传递的数据类似于 "2024-08-16",所以这个需要进行转化
Date date = getDate(dateStr);

int day = DateUtil.dayOfMonth(date) - 1; // bitmap从0开始

String key = StrUtil.format("sign:{}:{}:{}",openId
                ,DateUtil.format(date,"yyyy")
                ,DateUtil.format(date,"MM"));

// 操作Bitmap
RBitSet bitSet = redisService.getBitSet(key);

// 获取某月的总天数
int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1
                ,DateUtil.isLeapYear(DateUtil.year(date)));


// 判断该日期是否已经签到
boolean isSign = bitSet.get(day);

if (isSign) {
            return SignTypeVO.REPEAT_SIGN;
}
boolean sign = bitSet.set(day);


// 这个方法返回一个long,需要持久化到数据库
long unsigned = bitSet.getUnsigned(dayOfMonth, 0);

// 签到记录保存,将redis中的数据转换成long存储到数据库
saveSignRecord(openId,date,unsigned);

// 这个方法就是进行String -> Date的方法,
private Date getDate(String dateStr) {
        return StrUtil.isBlank(dateStr) 
            ? Date.valueOf(new Date(System.currentTimeMillis()).toString()) 
            : new Date(DateUtil.parseDate(dateStr).getTime());
}

注意:

我这里使用Date是java.sql.Date,他的java.util.Date的一个子类,它默认是没有时间的,只有日期,如果业务中需要精确到秒,那可能需要考虑使用java.util.Date。

我这里使用的Redission来操作Redis,如果使用其他库可以方法不太一致。

2、补签

补签和今日签到其实流程差不多,这里就不过多介绍。

3、获取签到记录

public List<String> getSignRecords(String openId, String dateStr) {
        Date date = getDate(dateStr);

        // 因为是按月份进行储存的,所以要转换成 "2024-08"这种格式
        String key = StrUtil.format("sign:{}:{}:{}",openId
                ,DateUtil.format(date,"yyyy")
                ,DateUtil.format(date,"MM"));

        RBitSet bitSet = redisService.getBitSet(key);

        List<String> records = new ArrayList<>();

        // 获取某月的总天数
        int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1
                ,DateUtil.isLeapYear(DateUtil.year(date)));

        long unsigned = bitSet.getUnsigned(dayOfMonth, 0);

    //这里还可以做判断,假如redis失败了,可以去mysql中获取,看选择,我这里就不写了

        if (unsigned == 0) {
            // 为0则代表无任何数据
            return records;
        }

    // 这里开始将bit转换成对应的日期
        for (int i = dayOfMonth; i > 0 ; i--) {
            if (unsigned >> 1 << 1 != unsigned) {
                LocalDateTime localDateTime = LocalDateTimeUtil.of(date.getTime()).withDayOfMonth(i);
                records.add(DateUtil.format(localDateTime,"yyyy-MM-dd"));
            }
            unsigned >>= 1;
        }
        return records;
    }

这里转换的原理:

将long右移动1位,在像左移动一位,假如当前移动的位是1,那边左移回来变成0,那么就不会相等,进行添加,如此往复,就能得到我这个月的一个情况。

4、获取总签到数和连续签到数

获取总签到数很简单,我们不可能每次都去进行统计,我们可以在我们的用户表中加一个总签到数的字段,每次签到或者补签就上1就像,后续直接去获取这个字段即可。

获取连续签到数就复杂一些,我们不可能每次获取连续签到记录,就去通过位运算统计,这样太恐怖了,所以这里我通过设计一张表来进行处理,这张表包括四个字段id、start_time、end_time、sign_length,这张表主要就是来记录我们每一段连续签到的信息。

当我们今日签到,我们根据我们end_time = 今天的日期-1去连续签到表中进行查询,是否存在连续签到记录,存在我们就将这个记录的end_time 修改成我们今天的日期,长度+1,不存在我就新插入一条记录,开始时间和结束时间都为今天的日期,长度为1。

我我们补签的时候,判断的情况就会多一些,我们要查询两个数据,一个就是当start_time = 今天的日期+1(记录1) ,另一个就是end_time=今天的日期-1(记录2)。

  • 记录1==null && 记录2==null:这时候我们需要插入一条新记录,开始时间和结束时间都为补签的日期,长度为1。
  • 记录1!=null && 记录2==null:这时候我们需要将记录1的start_time 设置成补签的日期,长度+1
  • 记录1==null && 记录2!=null:这时候我们需要将记录1的end_time设置成补签的日期,长度+1
  • 记录1!=null && 记录2!=null:这时候我们需要将这两个记录给连接起来,我们可以创建一个新记录,开始时间为记录2的start_time ,结束时间为记录1的end_time,长度为两个记录长度总和+1,让后将这两个记录删除。

有了这个表了,我们获取我们的连续签到记录就很容易了,这里也有两种情况。

  1. 根据redis判断今天是否签到,签到就根据end_time=今天的日期去连续签到记录表中进行查询,得到长度返回即可
  2. 假如今天没签到就需要根据end_time=今天的日期-1去连续签到记录表中进行查询长度。
  3. 假如上面流程都为null,这时候就代表我们的签到断开了,我们直接返回0即可。

这样是不是比去根据一个个的long来判断更加快,虽然在连续签到短的时候到是通过long来判断不影响,但是当连续签到很长,我们需要先获取这个月的连续签到,假如1号还没短,我们还需要继续去上一个月重复同样操作,这样花儿都谢了。通过我们这样,只需要查询一次数据库即可。

四、总结

上述基本上就是打卡的一个总设计了,只是在细节上的优化可能不太够,比如,

1、我们的redis不可用了,我们应该去到数据库获取long来进行操作。

2、我们在进行签到补签的时候假如插入redis或者插入数据库有一个失败了怎么办,这里可以取捕获redis抛出来的异常,或者数据库更新条数来判断给用户返回什么。

3、崩溃后Redis数据怎么恢复,看什么情况了,假如是个人项目,你可以重启弄一个缓存预热,在将数据加载进Redis中,假如是企业里面,那应该做一些集群之类的部署,开一个定时任务不定期去将mysql中的数据加载进Redis。

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值