累计连续签到设计和实现
累計連續簽到設計和實現
-
最近公司業務上需要實現一個累計連續打卡的功能,現在把打卡設計問題和思路整理一下發給大家
-
目前搜集到一些基于 Redis 位圖 / 關系型數據庫的一些方案,可以參考一下,做出最優方案的選擇
- 玩轉Redis-京東簽到領京豆如何實現
- 基于Redis位圖實現用戶簽到功能
- 如何利用 Redis 快速實現簽到統計功能
由于需求的復雜,本文還是選擇使用關系型數據庫實現和存儲,因為關系型數據庫查詢無所不能,哈哈哈哈
功能要求
-
簽到
-
補簽
-
統計某用戶截至今天連續打卡天數
-
統計某用戶在某一天打卡排名
-
統計某用戶截至到某天連續打卡天數
-
最高連續簽到記錄
下面直接上一個需求圖
問題難點
-
怎么用比較好方式去統計連續打卡天數
-
怎么實現補卡功能以達到連續簽到的效果
-
怎么實現補簽后連續天數的統計功能
數據庫設計
以下是打卡記錄表的設計和實現,我已經去掉了一些業務字段,剩下都是表結構的核心字段
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_timeUNIQUE (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_timeFROM mark_recordWHERE user_id = #{userId}AND mark_day_time < #{markDayTime}ORDER BY mark_day_time DESCLIMIT 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_timeFROM mark_recordWHERE user_id = #{userId}AND mark_day_time < #{markDayTime}ORDER BY mark_day_time DESCLIMIT 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 1updateDayContinueById 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_continueWHEN 0THEN 1 + if(im.mark = 0, 0, 1)WHEN 1THEN to_days(#{yesterdayTime}) - to_days((SELECT mark_day_timeFROM mark_recordWHERE user_id = #{userId}AND mark_day_time < #{yesterdayTime}AND day_continue != 1ORDER BY mark_day_time DESCLIMIT 1)) + if(im.mark = 0, 0, 1) + 1ELSE1 + if(im.mark = 0, 0, 1)END)) AS continueMarkedDays,amc.markCount AS totalMarkedDays FROM (SELECT if(count(*) > 0, 1, 0) AS markFROM mark_recordWHERE user_id = #{userId}AND mark_day_time = #{markDayTime}) im,(SELECT if(count(*) > 0, 1, 0) AS mark, day_continueFROM mark_recordWHERE user_id = #{userId}AND mark_day_time = #{yesterdayTime}) yim,(SELECT count(*) AS markCountFROM mark_recordWHERE 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_continueFROM mark_record tmrWHERE tmr.mark_day_time = #{day}AND tmr.user_id = #{userId})AS tmrmdt,((SELECT bmr.mark_day_timeFROM mark_record bmrWHERE bmr.mark_day_time < #{day}AND bmr.day_continue != 1AND bmr.user_id = #{userId}ORDER BY bmr.mark_day_time DESCLIMIT 1)UNION ALL(SELECT #{day})LIMIT 1) tb,((SELECT amrt.mark_day_timeFROM mark_record amrt,((SELECT amr.mark_day_timeFROM mark_record amrWHERE amr.mark_day_time > #{day}AND amr.day_continue != 1AND amr.user_id = #{userId}ORDER BY amr.mark_day_timeLIMIT 1)UNION ALL(SELECT NULL)LIMIT 1) amrttWHERE 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 = 1AND amrt.user_id = #{userId}ORDER BY amrt.mark_day_time DESCLIMIT 1)UNION ALL(SELECT #{day})LIMIT 1) ta實現最高連續天數
用戶數據表加一個最高連續簽到記錄或者 redis 記錄用戶ID關聯的最高記錄,每次簽到后查詢連簽記錄,大于替換掉該值。
本文就不提供相關的代碼實現
總結
目前這個方案我總感覺還是不夠完美,希望大家看了可以提供一下相關的想法
我覺得比較好的方案是上面文章鏈接提到的 Redis 位圖實現方式與 目前方案 混合搭配使用,記錄時候分別記錄兩份數據
優點
使用關系型數據庫做了簽到記錄,關系型數據庫的強大易于統計相關的簽到數據
缺點
統計 SQL 復雜
當記錄數據量大,性能可能存在問題
總結
以上是生活随笔為你收集整理的累计连续签到设计和实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【数据仓库】Hive环境搭建和基础用法
- 下一篇: Vue中去掉表单对象上前后空格