用Int32的32位表示每月最多31天签到状态,第0位对应1号,第30位对应31号;签到置1,未签到为0;仅需4字节,位与操作即可快速判断。

用 Int32 存一个月签到,位运算判断是否签到
每月最多31天,Int32 有32位,完全够用。第0位代表当月1号,第1位代表2号……第30位代表31号。签到就置1,未签到保持0。
这样存比存31个布尔值或日期数组省得多:一个字段固定4字节,查询也快——不需要遍历数组,直接位与操作就能判断。
(bitmask & (1 判断第<code>day号是否签到(注意:day是1~31)- 签到操作用
bitmask | (1 - 取消签到用
bitmask & ~(1 - MongoDB里存成
NumberInt类型(不是字符串或浮点),否则聚合时位运算会出错
按月分文档设计:用 yearMonth 作复合索引主键
别把一年365天全塞进一个文档——单文档超16MB、更新冲突高、读取低效。按月拆,每条文档只管某年某月,结构干净,增删改查都可控。
典型文档结构:
{"_id": "2024-04", "userId": "u123", "bitmask": 12345},其中 _id 是 "YYYY-MM" 字符串,便于范围查询和索引。
- 建索引必须包含
userId和_id,比如{ userId: 1, _id: 1 },否则按用户查多个月份时容易全表扫 - 避免用
Date类型字段做月份标识——排序、范围查询、sharding 都更麻烦 - 如果需跨月统计(如“近30天连续签到”),得在应用层拼多个文档,MongoDB原生不支持跨文档位运算
聚合阶段用 $bit 操作符提取某日状态(4.4+才支持)
MongoDB 4.4 引入了 $bit 聚合操作符,能直接对数值字段做位运算。想批量查用户4月1~7号的签到情况?不用应用层解码,聚合里就能算。
例如查4月5号(即第4位)是否签到:
{$project: {day5: {$ne: [{$bit: {and: ["$bitmask", {"$pow": [2, 4]}]}}, 0]}}}
-
$bit只支持and/or/xor/not,不支持左移右移,所以得手动算掩码(比如第n天就是2^(n-1)) - 低于4.4版本只能靠应用层处理,或者用
$mod+$divide硬解——但精度易丢,Int32转Double后位信息可能被截断 - 聚合中多次调用
$bit不影响性能,但别在$match阶段用它过滤——无法走索引
时区与月末天数不一致带来的坑
用户在北京,服务在UTC,2024-04 对他可能是4月1日00:00~4月30日23:59,但服务端生成的 yearMonth 如果按UTC时间截取,4月30日23:00的签到会被算进 2024-05。
- 所有时间计算必须统一用用户所在时区的
startOfDay和endOfDay推算yearMonth,不能依赖服务器本地时间 - 2月只有28或29天,但
Int32还是存满31位——多出来的位永远为0,读取时要限制判断范围(比如2月只查0~27位) - 跨年场景(如12月31日签到)容易漏掉
yearMonth切换逻辑,建议封装成工具函数:getYearMonth(date, timezone),别散落在各处
位图本身不难,难的是时间边界、时区对齐、版本兼容这三块。尤其是聚合里想靠 $bit 偷懒时,得先确认 MongoDB 小版本——差一个小号,整个 pipeline 就得推倒重写。










