实战篇 16. 用户签到 - 实现签到功能学习文档

5 阅读4分钟

太棒了!上一节我们彻底理解了 BitMap(位图)的底层原理和极致的空间压缩能力。现在,我们要把理论转化为真实的 Java 代码,为我们的“黑马点评” App 加上每日签到功能。

这个接口的实现其实非常清爽,但里面对于时间格式化偏移量计算的细节处理,是日常开发中经常要用到的基本功。


📚 实战篇 16. 用户签到 - 实现签到功能学习文档

一、 业务逻辑与参数分析

场景还原: 用户在 App 前端点击“每日签到”按钮。

  • 前端传参: 不需要传任何参数! * 为什么?因为“谁在签到”可以通过请求头里的 Token(后端通过 UserHolder 获取)知道;“哪天签到”可以通过后端的服务器系统时间自动获取。绝对不能让前端传时间戳,否则用户只要修改手机本地时间,就能实现“时空穿梭”去补签或提前签到。
  • 后端目标: 将当前用户的签到记录,利用 SETBIT 命令写入 Redis 中。

二、 核心规则设计:Key 与 Offset

在动手写代码前,我们再复习一下上一节定下的死规矩,这是写出正确代码的灵魂:

  1. Redis Key 拼接规则: 前缀:用户ID:年月

    • 比如今天是 2026 年 3 月 17 日,用户 ID 是 1001。
    • 那么他的专属 Key 就是:sign:1001:202603
    • 为什么要按月划分?因为如果不带年月,一个用户的签到数据会无限期累积在一个 Key 里,虽然 BitMap 很省空间,但按月划分更方便进行按月统计、清理历史数据,也符合真实的业务周期(比如“全勤奖”通常是按月计算的)。
  2. Offset 偏移量计算规则: 今天是本月的第几天 - 1

    • 因为日历是从 1 号开始的,而 BitMap(底层也是数组)的下标(Offset)是从 0 开始的。
    • 比如 3 月 17 日,对应的 offset 就是 17 - 1 = 16

三、 Java 代码落地 (Service 层)

UserServiceImpl 中实现签到逻辑。

注意:Spring Data Redis 并没有提供一个叫 opsForBitMap() 的方法,因为 BitMap 本质上就是 String。所以我们使用的是 opsForValue().setBit()

Java

@Override
public Result sign() {
    // 1. 获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    
    // 2. 获取当前日期 (基于后端服务器的系统时间)
    LocalDateTime now = LocalDateTime.now();
    
    // 3. 拼接 Redis 的 Key
    // 3.1 格式化年月 (例如:"202603")
    String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
    // 3.2 拼接最终的 Key (假设 USER_SIGN_KEY 常量的值为 "sign:")
    String key = USER_SIGN_KEY + userId + ":" + keySuffix;
    
    // 4. 获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    
    // 5. 写入 Redis BitMap
    // 对应底层命令:SETBIT sign:1001:202603 16 1
    // 参数解读:key, offset(天数减1), value(true代表1, false代表0)
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    
    // 6. 返回成功
    return Result.ok();
}

四、 防坑指南与进阶思考

这段代码虽然极其简单,但如果你的产品经理比较严苛,可能会提出一个问题:

🤔 拷问:如果用户今天已经签过到了,他又狂点了一次签到按钮,代码会报错吗?

  • 答案: 不会报错,它是天然幂等的。

  • 原理解析: setBit 命令本身的作用就是强行把某个 offset 位置的值修改为 0 或 1。如果你已经签到过了(该位已经是 1),再执行一次 setBit(key, offset, true),它只是把 1 重新覆盖成 1 而已,数据不会有任何破坏。

  • 业务优化(可选): setBit 方法在底层执行完毕后,会返回该位修改前的值(布尔类型)。

    你可以根据返回值做判断:如果返回 true,说明他今天本来就签过了,你可以给前端返回一个特殊的提示语 "您今日已签到,请勿重复操作"。但为了追求极致的接口响应速度,通常像示例代码这样直接覆盖并返回成功即可。


学习总结

至此,你已经成功将用户的签到行为记录到了 Redis 内存中。这比在 MySQL 中执行 INSERT INTO tb_sign ... 无论是执行速度还是存储空间,都强了成百上千倍。


⚠️ 最后的终极大考预警:

仅仅记录“今天签没签”是不够的。产品经理的终极杀招来了:

“请在 App 页面展示当前用户本月连续签到了多少天。如果中间断签了一天,连续天数必须重置为 0 重新计算。”

为了实现这个功能,你不能用一个简单的 getBit,你需要把这一个月走到今天为止的所有 01 全部拿出来,然后通过**极其硬核的位运算(按位与、右移操作)**去计算结尾连续出现的 1 的个数!