累计连续签到设计和实现

累计连续签到 设计和实现

功能要求

  • 签到
  • 补签
  • 统计某用户截至今天连续打卡天数
  • 统计某用户在某一天打卡排名
  • 统计某用户截至到某天连续打卡天数
  • 最高连续签到记录

下面直接上一个需求图


问题难点

  • 怎么用比较好方式去统计连续打卡天数
  • 怎么实现补卡功能以达到连续签到的效果
  • 怎么实现补签后连续天数的统计功能

数据库设计

以下是打卡记录表的设计和实现,我已经去掉了一些业务字段,剩下都是表结构的核心字段

CREATE TABLE mark_record (
    id             BIGINT                  NOT NULL COMMENT 'ID'
        PRIMARY KEY,
    create_time    DATETIME                NOT NULL COMMENT '创建时间',
    update_time    DATETIME                NOT NULL COMMENT '更新时间',
    user_id        BIGINT                  NOT NULL COMMENT '用户ID',
    mark_day_time  INT                     NOT NULL COMMENT '打卡日期 yyyyMMdd',
    day_continue   BIGINT       DEFAULT 0  NOT NULL COMMENT '距离上次打卡相差天数',
    mark_type      TINYINT      DEFAULT 0  NOT NULL COMMENT '补签 0否 1是',
    CONSTRAINT uidx_user_id_mark_day_time
        UNIQUE (user_id, mark_day_time)
)
    COMMENT '打卡签到表';

id/create_time/update_time 表结构的常规字段,简单提醒一下,业务上这些字段也比较重要

  • id 表的唯一主键

  • create_time/update_time 比较重要数据信息字段一般都保留

列举一个比较实用业界数据分页案例:
数据分页翻页时候,防止新增数据导致分页加载出现重复数据,一般做法是当客户端打卡当前页面那瞬间时间戳传过来,上下翻页都是用同一个时间戳,后端查询数据时候只查询小于这个时间戳的数据,大于这个时间戳的数据就不会加载出来了
其他用途就不一一列举了

  • user_id & mark_day_time 组成一个唯一索引

一个用户一天只允许打卡一次,加唯一索引保证数据唯一防止脏数据

  • mark_type 记录打卡类型

区分正常打卡和补卡

  • day_continue 冗余字段 距离上次打卡记录相差天数

以方便统计相关打卡记录数据

代码实现

打卡功能实现

markDayTime 当前打卡签到日期,userId 当前打卡用户 ID

签到功能 SQL 实现

使用 INSERT INTO SELECT 查询小于当前签到日期(markDayTime)最近一条签到记录数据,如果不存在,day_continue 字段为 -1,如果存在打卡记录,则day_continue 字段为 markDayTime 与查询签到记录结果 mark_day_time 相差天数

INSERT INTO mark_record (id, create_time, update_time, user_id, mark_day_time, day_continue, mark_type)
SELECT #{id},
       #{createTime},
       #{updateTime},
       #{userId},
       #{markDayTime},
       IF(COUNT(t.id) = 0, -1, to_days(#{markDayTime}) - to_days(mark_day_time)), 
        #{markType}
FROM (SELECT id, mark_day_time
     FROM mark_record
     WHERE user_id = #{userId}
       AND mark_day_time < #{markDayTime}
    ORDER BY mark_day_time DESC
    LIMIT 1) t

补签功能实现

补签功能 SQL

其实和签到功能的sql一致,传入参数不一样:签到日期markDayTime为补签日期,markType类型为补签类型

INSERT INTO mark_record (id, create_time, update_time, user_id, mark_day_time, day_continue, mark_type)
SELECT #{id},
       #{createTime},
       #{updateTime},
       #{userId},
       #{markDayTime},
       IF(COUNT(t.id) = 0, -1, to_days(#{markDayTime}) - to_days(mark_day_time)),
       #{markType}
FROM (SELECT id, mark_day_time
      FROM mark_record
      WHERE user_id = #{userId}
        AND mark_day_time < #{markDayTime}
      ORDER BY mark_day_time DESC
      LIMIT 1) t

补签和普通打卡在代码上有不一致,因为需要更新大于补签日期最旧一条数据的day_continue字段

public MarkRecord completeMark(MarkRecord record) {
    DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
    
    Long userId = record.getUserId();
    Integer markDayTime = record.getMarkDayTime();
    int nowDayTime = Integer.parseInt(LocalDateTime.now().format(DATE_TIME_FORMATTER));
    
    if (nowDayTime <= markDayTime) {
        throw new ServiceFailException(FailCode.ERROR_PARAM, "补签日期异常");
    }
    
    // 构造打卡记录
    MarkRecord mark = fillMarkRecord(record, markDayTime, 1);
    int completeMarkResult = markRecordMapper.completeMark(mark);
    if (completeMarkResult != 1) {
        return null;
    }
    
    // 更新大于markDayTime的第一条记录dayContinue字段值
    MarkRecord nearestBeforeRecord = markRecordMapper.findNearestBeforeRecord(userId, markDayTime, clubId, markId);
    if (Objects.nonNull(nearestBeforeRecord)) {
        // 更新补签日期前一条数据间隔天数
        Integer time = nearestBeforeRecord.getMarkDayTime();
        long betweenDays = LocalDate.parse(String.valueOf(markDayTime), DATE_TIME_FORMATTER)
                .until(LocalDate.parse(String.valueOf(time), DATE_TIME_FORMATTER), ChronoUnit.DAYS);
        markRecordMapper.updateDayContinueById(betweenDays, nearestBeforeRecord.getId());
    }
    
    return mark;
}

findNearestBeforeRecord SQL:

SELECT *
FROM mark_record
WHERE user_id = #{userId}
  AND mark_day_time > #{markDayTime}
ORDER BY mark_day_time
LIMIT 1

updateDayContinueById SQL:

UPDATE mark_record
SET day_continue=#{updatedDayContinue}
WHERE id = #{id}

统计连续签到功能实现

计算今天是否打卡/连续打卡天数/总打卡数

今天是否打卡:查询今天是否存在打卡记录
连续打卡天数:当天没打卡,前一天打卡,也算连续打卡;如果前一天没有打卡,那就断签了,
总打卡数:统计用户所有打卡记录数量

SQL 参数说明:#{yesterdayTime}为昨天的日期,#{markDayTime}为今天的日期

SQL 连续签到统计逻辑:


SELECT im.mark AS marked,
       IF(yim.mark = 0,
          (IF(im.mark = 0, 0, 1)),
          (CASE yim.day_continue
                  WHEN 0
                          THEN 1 + if(im.mark = 0, 0, 1)
                  WHEN 1
                          THEN to_days(#{yesterdayTime}) - to_days((SELECT mark_day_time
                                                                    FROM mark_record
                                                                    WHERE user_id = #{userId}
                                                                      AND mark_day_time < #{yesterdayTime}
                                                                      AND day_continue != 1
                                                                    ORDER BY mark_day_time DESC
                                                                    LIMIT 1)) + if(im.mark = 0, 0, 1) + 1
                  ELSE
                          1 + if(im.mark = 0, 0, 1)
                  END)) AS continueMarkedDays,
       amc.markCount AS totalMarkedDays
FROM (SELECT if(count(*) > 0, 1, 0) AS mark
      FROM mark_record
      WHERE user_id = #{userId}
        AND mark_day_time = #{markDayTime}) im,
     (SELECT if(count(*) > 0, 1, 0) AS mark, day_continue
      FROM mark_record
      WHERE user_id = #{userId}
        AND mark_day_time = #{yesterdayTime}) yim,
     (SELECT count(*) AS markCount
      FROM mark_record
      WHERE user_id = #{userId}) amc

查询所在某天的连续签到天数

SELECT if(tmrmdt.day_continue != 1,
          to_days(ta.mark_day_time) - to_days(#{day}) + 1,
          to_days(ta.mark_day_time) - to_days(tb.mark_day_time) + 1)
FROM (SELECT tmr.day_continue
      FROM mark_record tmr
      WHERE tmr.mark_day_time = #{day}
        AND tmr.user_id = #{userId})
             AS tmrmdt,
     ((SELECT bmr.mark_day_time
       FROM mark_record bmr
       WHERE bmr.mark_day_time < #{day}
         AND bmr.day_continue != 1
         AND bmr.user_id = #{userId}
       ORDER BY bmr.mark_day_time DESC
       LIMIT 1)
      UNION ALL
      (SELECT #{day})
      LIMIT 1) tb,
     ((SELECT amrt.mark_day_time
       FROM mark_record amrt,
            ((SELECT amr.mark_day_time
              FROM mark_record amr
              WHERE amr.mark_day_time > #{day}
                AND amr.day_continue != 1
                AND amr.user_id = #{userId}
              ORDER BY amr.mark_day_time
              LIMIT 1)
             UNION ALL
             (SELECT NULL)
             LIMIT 1) amrtt
       WHERE if(amrtt.mark_day_time IS NOT NULL,
                amrt.mark_day_time < amrtt.mark_day_time,
                amrt.mark_day_time > #{day})
         AND amrt.day_continue = 1
         AND amrt.user_id = #{userId}
       ORDER BY amrt.mark_day_time DESC
       LIMIT 1)
      UNION ALL
      (SELECT #{day})
      LIMIT 1) ta

实现最高连续天数

用户数据表加一个最高连续签到记录或者 redis 记录用户ID关联的最高记录,每次签到后查询连签记录,大于替换掉该值。
本文就不提供相关的代码实现

总结

  • 目前这个方案我总感觉还是不够完美,希望大家看了可以提供一下相关的想法
  • 我觉得比较好的方案是上面文章链接提到的 Redis 位图实现方式与 目前方案 混合搭配使用,记录时候分别记录两份数据

优点

  • 使用关系型数据库做了签到记录,关系型数据库的强大易于统计相关的签到数据

缺点

  • 统计 SQL 复杂
  • 当记录数据量大,性能可能存在问题
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,312评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,578评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,337评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,134评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,161评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,303评论 1 280
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,761评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,421评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,609评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,450评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,504评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,194评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,760评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,836评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,066评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,612评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,178评论 2 341