签到系统技术方案设计
Prompt 相关(AI 辅助开发)
生成UI设计图
你是一位资深全栈软件开发工程师,同时精通产品规划和UI设计。
现在要开发一个每日签到得积分的APP,主要页面和功能如下。
项目有两个页面,首页和积分详情页。
- 首页页面最上方展示总积分和查看详情链接,点击积分详情跳转到积分详情页查看积分明细记录。。
- 首页中间展示签到月历,展示当月连续签到天数和剩余可补签次数,以及当前月份每天的签到详情。
- 积分详情页以列表形式展示积分明细记录。
参照下面的内容,结合用户需求,以产品经理的视角去规划 APP 的功能、页面和交互;
- 作为设计师思考这些原型界面的设计,并以设计师的视角去输出完整的 UI/UX;
- 直接生成可交互的 HTML 原型,确保所有元素都符合移动端的交互习惯,文件命名为:
prototype.html。 - 使用 FontAwesome 等开源图标库,让原型显得更精美和接近真实
- 引入 tailwindcss 来完成页面样式,而不使用 style 样式。
- 图片使用 unsplash
输出的 prototype.html 需要可以直接进行前端开发。
生成前端代码
你是一个资深全栈开发工程师,使用 Vue3 + typescript + vite 设计开发一个每天签到领积分的 H5 应用。
- 以 prototype.html 文件为原型进行开发,去掉项目中无用的初始vue组件和样式。
- 尽量使用社区成熟的UI框架或插件实现,使用 tailwindcss 和 FontAwesome。
- 使用 pinia 管理状态
- 使用 axios 进行网络请求
后端提供以下几个接口。
查询积分接口,返回总积分。
签到记录接口,返回当月签到记录、当月连续签到天数、当月可补签次数。
当天签到接口。
补签接口,参数带日期。
积分记录列表接口。
功能点:
- 签到领积分,每天签到领1积分。
- 登录后展示总积分,点击积分详情展示积分明细。
- 每月连续签到3天、7天、15天有格外积分奖励,分别奖励5积分、10积分、20积分,每个月满签奖励100积分。
- 当月支持补签,每次补签消耗100积分,且每月最多补签三次。
- 展示用户的本月连续签到次数和当月剩余补签次数。
生成数据表设计和API设计
你是一个资深软件架构师,熟悉各种互联网应用程序的架构设计。现在需要你设计一个每日签到领取积分的应用程序。
主要功能点:
- 用户每天签到可得1积分。
- 每个月连续签到3、7、15天和当月满签还会额外获得5积分、10积分、20积分和100积分。
- 需要统计用户的累计签到次数和当月连续签到次数。
- 支持用户补签当月的记录,补签1次需要消耗100积分,每个月最多补签3次。
- 首页展示用户总积分和签到日历(标识出补签记录),支持查看之前的签到日历。
- 支持用户查看积分详情。
参考互联网大厂的设计规范和经验,请输出 MySQL 数据库表结构设计和遵循 RESTful 风格的 API 设计。
生成了 prototype.html 和 API 文档之后,你就找一个 code agent (Cursor/trae)让他写前端代码了。
后端技术方案设计
用户模块
主要实现 用户注册、用户登录和用户鉴权三部分。
鉴权使用 JWT ,同时提供 刷新 Token 接口,支持使用 refresh token 获取新 token。
签到模块
签到功能分为 『每日签到』和『补签』两部分,同时需要返回用户签到月历和当月连续签到天数。
每日签到只允许签当天的日期。
补签只允许补签当月的日期,并且每个月最多补签3次。
签到还需要额外考虑,连续签到奖励。
- 每日签到触发连续签到奖励。
- 补签也可以触发连续签到奖励。
积分模块
积分模块主要分为用户当前积分和积分明细两个功能,同时还要支持用户签到功能的联动。
- 每日签到
+1积分 - 每月连续签到额外奖励
+x积分 - 用户补签消耗
-100积分
RESTful API
用户模块相关API
用户注册
POST /api/v1/users获取用户信息
GET /api/v1/users/me登录
POST /api/v1/auth/login刷新 token
POST /api/v1/auth/refresh
签到模块相关API
今日签到
POST /api/v1/checkins补签
POST /api/v1/checkins/retroactive获取签到详情接口
GET /api/v1/checkins/calendar
积分模块相关API
获取总积分
GET /api/v1/points/summary获取积分明细
GET /api/v1/points/records
数据表设计
-- 用户基本信息表
DROP TABLE IF EXISTS `userinfo`;
CREATE TABLE `userinfo` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`password` VARCHAR(60) NOT NULL COMMENT '用户密码(bcrypt 加密)',
`email` VARCHAR(100) COMMENT '用户邮箱',
`avatar` VARCHAR(256) COMMENT '用户头像',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户基本信息表';
-- 用户积分汇总表
DROP TABLE IF EXISTS `user_points`;
CREATE TABLE `user_points` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`points` BIGINT DEFAULT 0 COMMENT '当前可用积分',
`points_total` BIGINT DEFAULT 0 COMMENT '累计获得积分',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分汇总表';
-- 用户积分交易明细表
DROP TABLE IF EXISTS `user_points_transactions`;
CREATE TABLE `user_points_transactions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`points_change` BIGINT NOT NULL COMMENT '积分变动值 (正数为增加,负数为扣除)',
`current_balance` BIGINT NOT NULL COMMENT '当前余额',
`transaction_type` TINYINT NOT NULL COMMENT '交易类型(1:签到 2:连续签到 3:补签 4:每日任务 5:福利任务)',
`description` VARCHAR(100) COMMENT '积分变动说明',
`ext_json` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '扩展字段',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分明细表';
-- 月度奖励记录表
DROP TABLE IF EXISTS `user_monthly_bonus_log`;
CREATE TABLE `user_monthly_bonus_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`year_month` CHAR(6) NOT NULL COMMENT '年月(YYYYMM)',
`bonus_type` TINYINT NOT NULL COMMENT '奖励类型(1:连续签到3天 2:连续签到7天 3:连续签到15天 4:月满签)',
`description` VARCHAR(100) COMMENT '积分变动说明',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_yearmonth_type` (`user_id`, `year_month`, `bonus_type`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户月度连续签到奖励记录表';
-- 签到记录表
DROP TABLE IF EXISTS `user_checkin_records`;
CREATE TABLE `user_checkin_records` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录ID',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`checkin_date` DATE NOT NULL COMMENT '签到日期',
`checkin_type` TINYINT NOT NULL DEFAULT 1 COMMENT '签到类型:1=正常签到,2=补签',
`points_awarded_base` INT NOT NULL DEFAULT 1 COMMENT '获得积分',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_date` (`user_id`, `checkin_date`),
KEY `idx_user_id` (`user_id`),
KEY `idx_checkin_date` (`checkin_date`),
KEY `idx_checkin_type` (`checkin_type`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '签到记录表';采用 Redis Bitmap 存储用户签到数据,采用 MySQL 数据库存储用户数据和积分数据。
Redis Bitmap 介绍
由于签到数据只有签到和没签到两种状态,可以用0和1来表示签到状态。
- 0:表示没签到
- 1:表示已签到
而签到日期又是一些数据量固定的连续数据,所以非常适合使用 Redis 的 Bitmap 来存储。
在介绍项目的 Redis Key 设置之前,先来了解下 Redis 中的 Bitmap。
一、核心概念
Redis Bitmap 是 Redis 中一种基于二进制位(bit)的存储结构,本质上是一个字节数组(byte array),每个元素(bit)只能是 0 或 1。它并非独立的数据类型,而是基于字符串(String)类型实现的位操作抽象,可以看作是一个可变长度的二进制向量,适用于大规模数据的状态统计、快速查询和内存优化场景。
二、技术特性
- 每个位(bit)仅占 1 位空间,1MB 内存可存储 8388608 个状态位(约 800 万),相比传统的布尔值数组(每个元素占 1 字节)节省 8 倍内存,最大支持
2^32-1位(约42.9亿)。 - 按需分配:Bitmap 的内存分配并非预先确定,而是根据用户设置的最大偏移量(offset)动态调整。比如,当你设置了
SETBIT key 1000 1,Redis 会确保该 key 至少能容纳 1001 位(偏移量从 0 开始)。 - 字节对齐:内存是以字节(8 位)为单位进行分配的。若你设置的最大偏移量是 1000,Redis 会分配
ceil(1001/8) = 126字节(也就是 1008 位)。 - 自动扩展机制:设置超出当前范围的offset时会自动扩容
偏移量: 0 8 16 24 ... 100
↓ ↓ ↓ ↓ ↓
字节: [00000001][00000000][00000000] ... [00000100]
↑ ↑
第 0 字节 第 13 字节 (100/8=12)- 时间复杂度:基本操作O(1),位运算O(N)
- 空间效率:1亿用户每日签到仅需约12MB存储
- 原子性操作:适合高并发场景
常用命令
命令 作用描述 示例 SETBIT key offset value设置指定索引(offset)的位值(0 或 1),返回旧值。 SETBIT user:1:days 10 1(设置用户 1 第 10 天的状态为 1)GETBIT key offset获取指定索引的位值。 GETBIT user:1:days 10(获取用户 1 第 10 天的状态)BITCOUNT key [start end]统计指定范围内(以字节为单位)值为 1 的位的数量。 BITCOUNT user:1:days 0 -1(统计用户 1 所有天数的活跃次数)BITOP operation destkey key [key ...]对一个或多个 Bitmap 执行按位运算(AND/OR/XOR/NOT),结果存入目标键。 BITOP OR result key1 key2(对 key1 和 key2 执行按位或,结果存 result)BITPOS key bit [start end]查找指定位(bit)在指定范围内第一次出现的位置。 `BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP SAT FAIL]` BITFIELD_RO key [GET encoding offset [GET encoding offset ...]]5.0+版本引入, BITFIELD_RO仅支持 GET 操作,不允许修改数据。它专为只读场景设计,提供了更高的安全性和性能优化,适合需要频繁读取但不修改 Bitmap 数据的场景。BITFIELD_RO user:score:1001 GET u16 0读取用户1001 的积分典型应用场景
- 用户在线状态统计
场景:记录用户是否在线(1 表示在线,0 表示离线)。
实现:用
user:online作为键,用户 ID 作为 offset,通过SETBIT更新状态,GETBIT查询状态。优势:百万用户仅需约 125KB 内存(100 万 ÷ 8 ÷ 1024 ≈ 122KB)。
- 签到打卡系统
场景:记录用户一个月内的签到情况(1 表示签到,0 表示未签到)。
实现:用
user:uid:month作为键,天数(1-31)作为 offset,每日签到时调用SETBIT。统计:通过
BITCOUNT计算当月签到天数,或用BITOP AND统计多个用户的共同签到天数。
- 布隆过滤器(Bloom Filter)
场景:快速判断元素是否存在于集合中(存在误判可能,适合允许容错的场景)。
实现:利用多个哈希函数将元素映射到 Bitmap 的不同位置,设置为 1;查询时检查所有映射位是否为 1。
优势:相比传统哈希表,内存占用极低(例如存储 100 万元素仅需约 1MB 内存)。
- 活跃用户统计(UV)
场景:统计一段时间内的活跃用户数(如日活、周活)。
实现:每天用一个独立的 Bitmap(如
day:20231001:uv),用户 ID 作为 offset,登录时置为 1;通过BITOP OR合并多天的 Bitmap 后,用BITCOUNT统计总活跃用户数。
BITFIELD 命令详解
一、概述
BITFIELD是 Redis 3.2 版本引入的高级位操作命令,用于对 Bitmap(位图) 执行复杂的位级操作。它允许在一个命令中对多个位范围进行读写,支持不同的整数类型(如 8 位、16 位、32 位整数),并提供了溢出控制和原子性操作,适合处理需要精确控制的位级数据。二、核心特性
- 多字段操作 在单个命令中对同一个键的多个位范围进行读写,减少网络开销。
- 类型支持
支持有符号整数(
i8、i16、i32、i64)和无符号整数(u8、u16、u32、u64),满足不同精度需求。 - 溢出控制
处理数值溢出时,可选择:
WRAP:环绕(如255+1=0)。SAT:饱和(如255+1=255)。FAIL:失败(返回NULL)。
- 原子性 所有操作在单个命令中执行,保证原子性,无需使用事务。
三、语法与常用子命令
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]常用子命令:
GET type offset获取指定位偏移量开始的整数。type:类型(如u8表示 8 位无符号整数)。offset:位偏移量(从 0 开始)。
SET type offset value设置指定位偏移量开始的整数。INCRBY type offset increment对指定位偏移量开始的整数进行自增 / 自减。OVERFLOW policy设置溢出策略(默认WRAP),影响后续INCRBY操作。
四、示例
1. 存储用户积分(16 位无符号整数)
# 初始化用户 1001 的积分为 100(从位偏移 0 开始,使用 16 位无符号整数) BITFIELD user:score:1001 SET u16 0 100 # 增加 50 积分 BITFIELD user:score:1001 INCRBY u16 0 50 # 获取当前积分 BITFIELD user:score:1001 GET u16 02. 多字段操作(同时处理多个用户)
# 为用户 1001 设置积分为 100,用户 1002 设置为 200 # 假设每个用户使用 16 位,用户 1002 的偏移量为 16 BITFIELD users:scores SET u16 0 100 SET u16 16 200
3. 溢出控制
# 从 counter 键的第 0 位开始,读取一个 u8 整数 # 将该值增加 250 # 应用饱和溢出处理,确保结果不超过类型范围 BITFIELD counter OVERFLOW SAT INCRBY u8 0 250 # 结果:255(饱和策略,不会溢出到 256)五、应用场景
- 精确计数器
- 统计点赞数、访问量,避免计数器溢出。
- 示例:用
INCRBY i32 0 1实现点赞计数。
- 状态压缩存储
- 将多个布尔状态(如用户权限)压缩到一个 Bitmap 中。
- 示例:用
u8存储 8 种权限状态。
- 时间序列数据
- 存储每日活跃用户数、订单量等,按天递增偏移量。
- 示例:每天使用新的 32 位整数存储数据。
- 用户画像标签
- 用不同位表示用户属性(如性别、年龄段、兴趣)。
- 示例:
BITFIELD user:1001 SET u1 0 1 SET u1 1 0表示男性、非会员。
六、性能与注意事项
- 时间复杂度
- 单个操作:O (1)。
- 多个操作:O (N),N 为操作数量。
- 内存占用
- 自动扩容,但需注意大偏移量(如
offset=1000000)可能导致内存突然增长。
- 自动扩容,但需注意大偏移量(如
- 与 SETBIT 的对比
SETBIT:仅支持单个位的操作,适合简单场景。BITFIELD:支持多字段、多类型、溢出控制,适合复杂场景。
- 二进制兼容性
- 操作结果以 网络字节序(大端序) 返回,需注意跨语言处理时的字节序问题。
Bitmap和bitfield 代码示例
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// bitmap 示例代码
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
defer rdb.Close()
ctx := context.Background()
// bitmapDemo(ctx, rdb)
bitFieldDemo(ctx, rdb)
}
func bitmapDemo(ctx context.Context, rdb *redis.Client) {
key := "test:bitmap:hahaha"
// setbit
val1 := rdb.SetBit(ctx, key, 0, 1).Val() // 返回的是原来的值(setbit之前的值)
fmt.Printf("setbit ret:%v\n", val1)
// getbit
val2 := rdb.GetBit(ctx, key, 0).Val()
fmt.Printf("getbit ret:%v\n", val2)
// bitcount
val3 := rdb.BitCount(ctx, key, &redis.BitCount{Start: 0, End: -1}).Val()
fmt.Printf("bitcount ret:%v\n", val3)
// bitop
key2 := "test:bitmap:hahaha2"
rdb.Del(ctx, key2)
rdb.SetBit(ctx, key2, 2, 1)
// 1 0 0
// 0 0 1
key3 := "test:bitmap:hahaha3"
val4 := rdb.BitOpAnd(ctx, key3, key, key2).Val() // 返回的是目标key3的字节数
fmt.Printf("bitop ret:%v\n", val4)
val5 := rdb.BitCount(ctx, key3, &redis.BitCount{Start: 0, End: -1}).Val()
fmt.Printf("bitcount ret:%v\n", val5)
// get
// rdb.SetBit(ctx, key2, 0, 1)
// rdb.SetBit(ctx, key2, 1, 1)
// val6 := rdb.Get(ctx, key2).Val() // bitmap 取出的是字符串
val6 := rdb.BitCount(ctx, key2, &redis.BitCount{Start: 0, End: -1}).Val()
fmt.Printf("get ret:%v\n", val6)
// bitpos
val7 := rdb.BitPos(ctx, key2, 1).Val()
fmt.Printf("bitpos ret:%v\n", val7)
}
func bitFieldDemo(ctx context.Context, rdb *redis.Client) {
key := "test:checkin:2026" // 存储2026年整年的打卡情况
// 假设现在是 2026-05
// 5.1 打卡
// 1.1 0
// 1.2 1
// 1.3 2
// ...
// 12.31 365
// 计算出5.1号是今年的第几天,索引位就是天数-1
t, _ := time.Parse("2006-01-02", "2026-05-01")
offset := t.YearDay() - 1 // 5.1
offset52 := offset + 1 // 5.2
offset53 := offset + 2 // 5.3
rdb.SetBit(ctx, key, int64(offset52), 1) // 5.2打卡
rdb.SetBit(ctx, key, int64(offset53), 1) // 5.3打卡
// 查看5月的打卡情况
// 从5.1 到 5.31的数据读取出来
// 5.1 到 5.31 共多少天?
// 用多大的整数能存下31天
ret := rdb.BitField(ctx, key, "GET", "u31", offset, "GET", "u4", offset).Val()
fmt.Printf("ret:%v\n", ret)
fmt.Printf("bet:%031b\n", ret[0])
fmt.Printf("bet:%04b\n", ret[1])
}Redis Key 设计
1、用户签到记录(Bitmap)
记录一个用户年度签到记录,key 格式:
user:checkins:daily:userID:年份
一个 key 存储用户一年的签到记录。
- 方便使用 bitcount 统计年度累计签到天数。
- 每年之后将 Redis 中的签到数据持久化入 DB。
2、用户补签记录(Bitmap)
因为产品设计用户只能补签当月的日期,所以这里使用月份维度的 Redis Key。一个用户每月补签记录,key格式:
user:checkins:retro:userID:月份
一个 key 存储用户当月的补签记录。
- 既能记录当月补签日期,又能方便的计算当月补签次数。
- 每个月后将 Redis 中的补签数据持久化入 DB。
3、如何计算当月连续签到天数?
当月每日签到 bitmap |(逻辑或) 当月补签 bitmap = 当月签到 bitmap

逐位遍历当月签到 bitmap 后判断可得出当月连续签到天数。