跳至内容
签到系统技术方案设计

签到系统技术方案设计

Prompt 相关(AI 辅助开发)

生成UI设计图

你是一位资深全栈软件开发工程师,同时精通产品规划和UI设计。

现在要开发一个每日签到得积分的APP,主要页面和功能如下。

项目有两个页面,首页和积分详情页。

  1. 首页页面最上方展示总积分和查看详情链接,点击积分详情跳转到积分详情页查看积分明细记录。。
  2. 首页中间展示签到月历,展示当月连续签到天数和剩余可补签次数,以及当前月份每天的签到详情。
  3. 积分详情页以列表形式展示积分明细记录。

参照下面的内容,结合用户需求,以产品经理的视角去规划 APP 的功能、页面和交互;

  1. 作为设计师思考这些原型界面的设计,并以设计师的视角去输出完整的 UI/UX;
  2. 直接生成可交互的 HTML 原型,确保所有元素都符合移动端的交互习惯,文件命名为: prototype.html
  3. 使用 FontAwesome 等开源图标库,让原型显得更精美和接近真实
  4. 引入 tailwindcss 来完成页面样式,而不使用 style 样式。
  5. 图片使用 unsplash

输出的 prototype.html 需要可以直接进行前端开发。

生成前端代码

你是一个资深全栈开发工程师,使用 Vue3 + typescript + vite 设计开发一个每天签到领积分的 H5 应用。

  1. 以 prototype.html 文件为原型进行开发,去掉项目中无用的初始vue组件和样式。
  2. 尽量使用社区成熟的UI框架或插件实现,使用 tailwindcss 和 FontAwesome。
  3. 使用 pinia 管理状态
  4. 使用 axios 进行网络请求

后端提供以下几个接口。

  1. 查询积分接口,返回总积分。

  2. 签到记录接口,返回当月签到记录、当月连续签到天数、当月可补签次数。

  3. 当天签到接口。

  4. 补签接口,参数带日期。

  5. 积分记录列表接口。

功能点:

  1. 签到领积分,每天签到领1积分。
  2. 登录后展示总积分,点击积分详情展示积分明细。
  3. 每月连续签到3天、7天、15天有格外积分奖励,分别奖励5积分、10积分、20积分,每个月满签奖励100积分。
  4. 当月支持补签,每次补签消耗100积分,且每月最多补签三次。
  5. 展示用户的本月连续签到次数和当月剩余补签次数。

生成数据表设计和API设计

你是一个资深软件架构师,熟悉各种互联网应用程序的架构设计。现在需要你设计一个每日签到领取积分的应用程序。

主要功能点:

  1. 用户每天签到可得1积分。
  2. 每个月连续签到3、7、15天和当月满签还会额外获得5积分、10积分、20积分和100积分。
  3. 需要统计用户的累计签到次数和当月连续签到次数。
  4. 支持用户补签当月的记录,补签1次需要消耗100积分,每个月最多补签3次。
  5. 首页展示用户总积分和签到日历(标识出补签记录),支持查看之前的签到日历。
  6. 支持用户查看积分详情。

参考互联网大厂的设计规范和经验,请输出 MySQL 数据库表结构设计和遵循 RESTful 风格的 API 设计。

生成了 prototype.html 和 API 文档之后,你就找一个 code agent (Cursor/trae)让他写前端代码了。

后端技术方案设计

用户模块

主要实现 用户注册、用户登录和用户鉴权三部分。

鉴权使用 JWT ,同时提供 刷新 Token 接口,支持使用 refresh token 获取新 token。

签到模块

签到功能分为 『每日签到』和『补签』两部分,同时需要返回用户签到月历和当月连续签到天数。

每日签到只允许签当天的日期。

补签只允许补签当月的日期,并且每个月最多补签3次。

签到还需要额外考虑,连续签到奖励。

  • 每日签到触发连续签到奖励。
  • 补签也可以触发连续签到奖励。

积分模块

积分模块主要分为用户当前积分和积分明细两个功能,同时还要支持用户签到功能的联动。

  • 每日签到 +1 积分
  • 每月连续签到额外奖励 +x 积分
  • 用户补签消耗 -100 积分

RESTful API

用户模块相关API

  1. 用户注册

    POST /api/v1/users
  2. 获取用户信息

    GET /api/v1/users/me
  3. 登录

    POST /api/v1/auth/login
  4. 刷新 token

    POST /api/v1/auth/refresh

签到模块相关API

  1. 今日签到

    POST /api/v1/checkins
  2. 补签

    POST /api/v1/checkins/retroactive
  3. 获取签到详情接口

    GET /api/v1/checkins/calendar

积分模块相关API

  1. 获取总积分

    GET /api/v1/points/summary
  2. 获取积分明细

    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)只能是 01。它并非独立的数据类型,而是基于字符串(String)类型实现的位操作抽象,可以看作是一个可变长度的二进制向量,适用于大规模数据的状态统计、快速查询和内存优化场景。

二、技术特性

  1. 存储原理
  • 每个位(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)
  1. 性能优势
  • 时间复杂度:基本操作O(1),位运算O(N)
  • 空间效率:1亿用户每日签到仅需约12MB存储
  • 原子性操作:适合高并发场景
  1. 常用命令
    命令作用描述示例
    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 WRAPSATFAIL]`
    BITFIELD_RO key [GET encoding offset [GET encoding offset ...]]5.0+版本引入,BITFIELD_RO 仅支持 GET 操作,不允许修改数据。它专为只读场景设计,提供了更高的安全性和性能优化,适合需要频繁读取但不修改 Bitmap 数据的场景。BITFIELD_RO user:score:1001 GET u16 0 读取用户1001 的积分
  2. 典型应用场景
    1. 用户在线状态统计
    • 场景:记录用户是否在线(1 表示在线,0 表示离线)。

    • 实现:用 user:online 作为键,用户 ID 作为 offset,通过 SETBIT 更新状态,GETBIT 查询状态。

    • 优势:百万用户仅需约 125KB 内存(100 万 ÷ 8 ÷ 1024 ≈ 122KB)。

    1. 签到打卡系统
    • 场景:记录用户一个月内的签到情况(1 表示签到,0 表示未签到)。

    • 实现:用 user:uid:month 作为键,天数(1-31)作为 offset,每日签到时调用 SETBIT

    • 统计:通过 BITCOUNT 计算当月签到天数,或用 BITOP AND 统计多个用户的共同签到天数。

    1. 布隆过滤器(Bloom Filter)
    • 场景:快速判断元素是否存在于集合中(存在误判可能,适合允许容错的场景)。

    • 实现:利用多个哈希函数将元素映射到 Bitmap 的不同位置,设置为 1;查询时检查所有映射位是否为 1。

    • 优势:相比传统哈希表,内存占用极低(例如存储 100 万元素仅需约 1MB 内存)。

    1. 活跃用户统计(UV)
    • 场景:统计一段时间内的活跃用户数(如日活、周活)。

    • 实现:每天用一个独立的 Bitmap(如 day:20231001:uv),用户 ID 作为 offset,登录时置为 1;通过 BITOP OR 合并多天的 Bitmap 后,用 BITCOUNT 统计总活跃用户数。

    BITFIELD 命令详解

    一、概述

    BITFIELD 是 Redis 3.2 版本引入的高级位操作命令,用于对 Bitmap(位图) 执行复杂的位级操作。它允许在一个命令中对多个位范围进行读写,支持不同的整数类型(如 8 位、16 位、32 位整数),并提供了溢出控制原子性操作,适合处理需要精确控制的位级数据。

    二、核心特性
    1. 多字段操作 在单个命令中对同一个键的多个位范围进行读写,减少网络开销。
    2. 类型支持 支持有符号整数(i8i16i32i64)和无符号整数(u8u16u32u64),满足不同精度需求。
    3. 溢出控制 处理数值溢出时,可选择:
      • WRAP:环绕(如 255+1=0)。
      • SAT:饱和(如 255+1=255)。
      • FAIL:失败(返回 NULL)。
    4. 原子性 所有操作在单个命令中执行,保证原子性,无需使用事务。
    三、语法与常用子命令
    BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
    常用子命令:
    1. GET type offset 获取指定位偏移量开始的整数。
      • type:类型(如 u8 表示 8 位无符号整数)。
      • offset:位偏移量(从 0 开始)。
    2. SET type offset value 设置指定位偏移量开始的整数。
    3. INCRBY type offset increment 对指定位偏移量开始的整数进行自增 / 自减。
    4. 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 0
    2. 多字段操作(同时处理多个用户)
    # 为用户 1001 设置积分为 100,用户 1002 设置为 200
    # 假设每个用户使用 16 位,用户 1002 的偏移量为 16
    BITFIELD users:scores SET u16 0 100 SET u16 16 200

    image-20250603135151454

    3. 溢出控制
    # 从 counter 键的第 0 位开始,读取一个 u8 整数
    # 将该值增加 250
    # 应用饱和溢出处理,确保结果不超过类型范围
    BITFIELD counter OVERFLOW SAT INCRBY u8 0 250
    # 结果:255(饱和策略,不会溢出到 256)
    五、应用场景
    1. 精确计数器
      • 统计点赞数、访问量,避免计数器溢出。
      • 示例:用 INCRBY i32 0 1 实现点赞计数。
    2. 状态压缩存储
      • 将多个布尔状态(如用户权限)压缩到一个 Bitmap 中。
      • 示例:用 u8 存储 8 种权限状态。
    3. 时间序列数据
      • 存储每日活跃用户数、订单量等,按天递增偏移量。
      • 示例:每天使用新的 32 位整数存储数据。
    4. 用户画像标签
      • 用不同位表示用户属性(如性别、年龄段、兴趣)。
      • 示例:BITFIELD user:1001 SET u1 0 1 SET u1 1 0 表示男性、非会员。
    六、性能与注意事项
    1. 时间复杂度
      • 单个操作:O (1)。
      • 多个操作:O (N),N 为操作数量。
    2. 内存占用
      • 自动扩容,但需注意大偏移量(如 offset=1000000)可能导致内存突然增长。
    3. 与 SETBIT 的对比
      • SETBIT:仅支持单个位的操作,适合简单场景。
      • BITFIELD:支持多字段、多类型、溢出控制,适合复杂场景。
    4. 二进制兼容性
      • 操作结果以 网络字节序(大端序) 返回,需注意跨语言处理时的字节序问题。

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 后判断可得出当月连续签到天数。

最后更新于 • Q1mi