如何设计一个用户签到系统,支持连续签到统计?

如何设计一个用户签到系统,支持连续签到统计?

引言:

在互联网业务中,用户活跃度 是衡量平台粘性的重要指标之一。许多产品通过"签到送积分/奖励"的机制来激励用户每日登录,提升日活。

你是否也曾在业务中遇到这样的需求:设计一个支持连续签到、断签重置、补签、统计连续天数的签到系统?

作为一名有 8年开发经验的 Java 工程师 ,我曾在多个项目中设计过类似系统,今天就和大家聊聊如何从 0 到 1 设计这样一个 既高性能又易扩展 的签到系统。


一、业务需求分析

我们先从产品角度解构这个需求:

1.1 核心功能点

  • 用户每日可签到一次
  • 支持计算连续签到天数
  • 允许断签后重新开始统计
  • 签到数据需可查询和展示(如:某月签到日历)
  • 支持补签(如:VIP 用户可补签)

1.2 非功能性要求

  • 高并发下保证用户签到数据不重复、不丢失
  • 数据结构设计需支持快速查询与统计
  • 系统应支持横向扩展(高并发、大用户量)

二、技术选型与设计思路

2.1 数据存储选型

我们可选用两种存储方式:

  • MySQL:适合持久存储、查询统计
  • Redis:适合快速读写、缓存热点数据

2.2 签到数据结构设计

✅ 推荐方案:Redis 位图(bitmap)+ MySQL
  • Redis 用 bitmap 存储每日签到情况,快速读写
  • MySQL 存储每月签到记录、奖励记录,做持久化
示例:bitmap 存储
  • key: sign:{userId}:{yyyyMM}

  • value: 二进制位,1 表示已签到

  • 例如:sign:1001:202506

    • 第 1 位是 1 → 表示用户 1001 在 6 月 1 日签到
优势:
  • 存储空间小(一个月只需几个字节)
  • 查询/统计性能高 (BITCOUNT, GETBIT)
  • 可通过 Lua 脚本实现原子操作

三、系统设计图(简述)

markdown 复制代码
用户请求 → Controller → Service  
       ↓            ↓
     Redis ← 签到状态查询 / 写入  
       ↓
    定时任务 ← MySQL 异步持久化(每日或每周)

四、Java 实现代码示例

4.1 Redis 签到核心逻辑

ini 复制代码
@Service
public class SignInService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String SIGN_KEY_PREFIX = "sign:";

    /**
     * 用户签到
     */
    public boolean sign(Long userId) {
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth() - 1; // bitmap 从 0 开始
        String redisKey = buildSignKey(userId, now);

        // 设置今天为已签到
        Boolean signed = redisTemplate.opsForValue().getBit(redisKey, dayOfMonth);
        if (Boolean.TRUE.equals(signed)) {
            // 已签到
            return false;
        }

        redisTemplate.opsForValue().setBit(redisKey, dayOfMonth, true);
        return true;
    }

    /**
     * 获取本月签到记录(如:用于日历展示)
     */
    public List<Integer> getSignInDays(Long userId, YearMonth month) {
        int daysInMonth = month.lengthOfMonth();
        String redisKey = buildSignKey(userId, month.atDay(1));

        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < daysInMonth; i++) {
            Boolean signed = redisTemplate.opsForValue().getBit(redisKey, i);
            if (Boolean.TRUE.equals(signed)) {
                result.add(i + 1);
            }
        }
        return result;
    }

    /**
     * 计算连续签到天数
     */
    public int getConsecutiveDays(Long userId) {
        LocalDate today = LocalDate.now();
        String redisKey = buildSignKey(userId, today);

        int dayOfMonth = today.getDayOfMonth();
        int count = 0;

        for (int i = dayOfMonth - 1; i >= 0; i--) {
            Boolean signed = redisTemplate.opsForValue().getBit(redisKey, i);
            if (Boolean.TRUE.equals(signed)) {
                count++;
            } else {
                break;
            }
        }
        return count;
    }

    /**
     * 构建 Redis Key:sign:用户ID:年月
     */
    private String buildSignKey(Long userId, LocalDate date) {
        return SIGN_KEY_PREFIX + userId + ":" + date.format(DateTimeFormatter.ofPattern("yyyyMM"));
    }
}

4.2 Controller 示例

less 复制代码
@RestController
@RequestMapping("/sign")
public class SignInController {

    @Autowired
    private SignInService signInService;

    @PostMapping
    public ResponseEntity<String> signIn(@RequestParam Long userId) {
        boolean signed = signInService.sign(userId);
        return signed ? ResponseEntity.ok("签到成功")
                      : ResponseEntity.ok("今日已签到");
    }

    @GetMapping("/calendar")
    public ResponseEntity<List<Integer>> getSignCalendar(@RequestParam Long userId) {
        YearMonth month = YearMonth.now();
        return ResponseEntity.ok(signInService.getSignInDays(userId, month));
    }

    @GetMapping("/consecutive")
    public ResponseEntity<Integer> getConsecutive(@RequestParam Long userId) {
        return ResponseEntity.ok(signInService.getConsecutiveDays(userId));
    }
}

五、可能的扩展点

  1. 补签功能:限制次数,补签后标记 bitmap
  2. 签到奖励:连续签到奖励、等级奖励(结合积分系统)
  3. 签到排行榜:每月签到次数排序(结合 Redis SortedSet)
  4. 数据落库:每日异步任务将 Redis 数据持久化到 MySQL
  5. 签到提醒:结合消息推送系统做未签到提醒

六、总结

  • Redis bitmap 是构建签到系统的高效利器,节省空间又性能出众。
  • 业务逻辑中应充分考虑幂等性、防刷机制(如接口限流、Token)
  • 签到系统虽然简单,但能衍生出很多与用户互动的玩法(抽奖、积分、等级)

最后的话:

一个"简单"的签到系统,其实背后能练习到 高性能读写、数据结构优化、缓存设计、接口幂等、系统扩展性 等多个高级工程能力。

如果你也是一个追求代码优雅与系统设计的 Java 程序员,欢迎点赞收藏,也欢迎留言探讨更多业务系统设计!

相关推荐
Cyanto1 小时前
Spring注解IoC与JUnit整合实战
java·开发语言·spring·mybatis
qq_433888931 小时前
Junit多线程的坑
java·spring·junit
gadiaola1 小时前
【SSM面试篇】Spring、SpringMVC、SpringBoot、Mybatis高频八股汇总
java·spring boot·spring·面试·mybatis
写不出来就跑路1 小时前
WebClient与HTTPInterface远程调用对比
java·开发语言·后端·spring·springboot
Cyanto2 小时前
深入MyBatis:CRUD操作与高级查询实战
java·数据库·mybatis
麦兜*2 小时前
Spring Boot 集成Reactive Web 性能优化全栈技术方案,包含底层原理、压测方法论、参数调优
java·前端·spring boot·spring·spring cloud·性能优化·maven
天上掉下来个程小白2 小时前
MybatisPlus-06.核心功能-自定义SQL
java·spring boot·后端·sql·微服务·mybatisplus
知了一笑2 小时前
独立开发第二周:构建、执行、规划
java·前端·后端
寻月隐君2 小时前
想用 Rust 开发游戏?这份超详细的入门教程请收好!
后端·rust·github
晴空月明3 小时前
分布式系统高可用性设计 - 缓存策略与数据同步机制
后端