Go语言实现用户签到功能

Go语言实现用户签到功能

1
2
3
4
5
1、签到检查
2、签到记录添加、计算连续签到天数
3、签到统计记录添加 (汇总数据)
4、签到积分变更明细记录添加
5、更新用户最新积分总数

相关技术栈

1
2
3
1、goZero 框架
2、gorm
3、暂时不用 redis 的 BitMap 实现....

数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
-- 用户签到记录表
CREATE TABLE `t_user_checkin` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '签到id',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户Id',
`checkin_date` date NOT NULL COMMENT '签到日期',
`checkin_time` datetime NOT NULL COMMENT '签到时间',
`continuous_days` int unsigned NOT NULL DEFAULT '1' COMMENT '本次签到时的连续天数(签到时更新)',
`reward_points` int unsigned NOT NULL DEFAULT '0' COMMENT '本次签到获得的积分奖励',
`reward_type` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '奖励类型(1:积分,2:优惠券)',
`remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注信息',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id_checkin_date` (`user_id`,`checkin_date`),
KEY `idx_user_id_reward_type` (`user_id`,`reward_type`) USING BTREE,
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户签到记录表';

-- 用户签到统计表
CREATE TABLE `t_user_checkin_stats` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '签到统计id',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`total_checkin_days` int unsigned NOT NULL DEFAULT '0' COMMENT '累计签到总天数',
`max_continuous_days` int unsigned NOT NULL DEFAULT '0' COMMENT '历史最长连续签到天数(里程碑)',
`current_continuous_days` int unsigned NOT NULL DEFAULT '0' COMMENT '当前连续签到天数(未中断)',
`last_checkin_date` date DEFAULT NULL COMMENT '最近一次签到日期',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户签到统计表';

-- 用户积分变更日志表
CREATE TABLE `t_user_point_change_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`point` int NOT NULL DEFAULT '0' COMMENT '积分变更值:正数为增加,负数为减少',
`point_balance` int NOT NULL DEFAULT '0' COMMENT '变动后积分余额',
`type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '类型:(1:签到 2:抽奖 )',
`business_id` bigint NOT NULL DEFAULT '0' COMMENT '关联业务id,如签到id、抽奖id等',
`change_date` date NOT NULL COMMENT '积分变更日期',
`change_time` datetime NOT NULL COMMENT '变更时间',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id_type` (`user_id`,`type`),
KEY `idx_business` (`user_id`,`business_id`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户积分变更日志表';

-- 用户信息
CREATE TABLE `t_user_info` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL COMMENT '用户id',
`egold` decimal(8,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '币',
`point` int unsigned NOT NULL DEFAULT '0' COMMENT '积分',
`avatar` varchar(5000) DEFAULT '',
`home_bg` varchar(255) DEFAULT '' COMMENT '个人主页背景',
`nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '昵称',
`gender` tinyint(1) DEFAULT NULL COMMENT '性别 1-男 0-女 -1保密',
`birthday` date DEFAULT NULL COMMENT '生日',
`sign` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '个性签名',
`ip` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'ip地址',
`last_open_time` bigint DEFAULT '0' COMMENT '最近一次打开APP的时间',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
PRIMARY KEY (`id`),
UNIQUE KEY `userid` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户信息';

涉及到的几个主要 model 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// @package    : checkin
// @file : user_checkin.go
// @author : hqd8080
// @contact : hanquanding@163.com
// @time : 2025/8/6
// @description: user_checkin

package checkin

import "time"

type UserCheckin struct {
ID int64 `gorm:"column:id;autoIncrement" json:"id"` // 签到id
UserID int64 `gorm:"column:user_id" json:"user_id"` // 用户Id
CheckinDate time.Time `gorm:"column:checkin_date" json:"checkin_date"` // 签到日期
CheckinTime time.Time `gorm:"column:checkin_time" json:"checkin_time"` // 签到时间
ContinuousDays int `gorm:"column:continuous_days" json:"continuous_days"` // 本次签到时的连续天数(签到时更新)
RewardPoints int `gorm:"column:reward_points" json:"reward_points"` // 本次签到获得的积分奖励
RewardType int8 `gorm:"column:reward_type" json:"reward_type"` // 奖励类型
Remark string `gorm:"column:remark" json:"remark"` // 备注信息
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` // 更新时间
}

func (u *UserCheckin) TableName() string {
return "t_user_checkin"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// @package    : checkin
// @file : user_checkin_stats.go
// @author : hqd8080
// @contact : hanquanding@163.com
// @time : 2025/8/6
// @description: user_checkin_stats

package checkin

import (
"time"
)

type UserCheckinStats struct {
ID int64 `gorm:"column:id;autoIncrement" json:"id"` // 签到统计id
UserID int64 `gorm:"column:user_id;uniqueIndex" json:"user_id"` // 用户id
TotalCheckinDays int `gorm:"column:total_checkin_days" json:"total_checkin_days"` // 累计签到总天数
MaxContinuousDays int `gorm:"column:max_continuous_days" json:"max_continuous_days"` // 历史最长连续签到天数(里程碑)
CurrentContinuousDays int `gorm:"column:current_continuous_days" json:"current_continuous_days"` // 当前连续签到天数(未中断)
LastCheckinDate time.Time `gorm:"column:last_checkin_date" json:"last_checkin_date"` // 最近一次签到日期
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` // 更新时间
}

func (u *UserCheckinStats) TableName() string {
return "t_user_checkin_stats"
}

type UserCheckinLog struct {
ID int64 `json:"id"` // 签到id
UserID int64 `json:"user_id"` // 用户Id
CheckinDate time.Time `json:"checkin_date"` // 签到日期
CheckinTime time.Time `json:"checkin_time"` // 签到时间
ContinuousDays int `json:"continuous_days"` // 本次签到时的连续天数(签到时更新)
RewardType int8 `json:"reward_type"` // 奖励类型
Remark string `json:"remark"` // 备注信息
TotalCheckinDays int `json:"total_checkin_days"` // 累计签到总天数
MaxContinuousDays int `json:"max_continuous_days"` // 历史最长连续签到天数(里程碑)
CurrentContinuousDays int `json:"current_continuous_days"` // 当前连续签到天数(未中断)
Point int64 `json:"point"` // 用户总积分
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
}

type UserCheckinStats struct {
UserID int64 `json:"user_id"`
Point int64 `json:"point"`
CurrentContinuousDays int64 `json:"current_continuous_days"`
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// @package    : checkin
// @file : user_point_change_log.go
// @author : hqd8080
// @contact : hanquanding@163.com
// @time : 2025/8/6
// @description: user_point_change_log

package checkin

import "time"

type UserPointChangeLog struct {
ID int64 `gorm:"column:id;autoIncrement" json:"id"` // id
UserID int64 `gorm:"column:user_id" json:"user_id"` // 用户id
Point int `gorm:"column:point" json:"point"` // 积分变更值:正数为增加,负数为减少
PointBalance int `gorm:"column:point_balance" json:"point_balance"` // 变动后积分余额
Type int8 `gorm:"column:type" json:"type"` // 类型
BusinessId int64 `gorm:"column:business_id" json:"business_id"` // 关联业务id,如签到id、抽奖id等
ChangeDate time.Time `gorm:"column:change_date" json:"change_date"` // 积分变更日期
ChangeTime time.Time `gorm:"column:change_time" json:"change_time"` // 变更时间
Remark string `gorm:"column:remark" json:"remark"` // 备注信息
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` // 更新时间
}

func (u *UserPointChangeLog) TableName() string {
return "t_user_point_change_log"
}

Api定义

1
/apis/checkin.api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
syntax = "v1"

@server(
group: v1/checkin
middleware: UserAgentMiddleware
)

service user-api {
@doc "用户签到"
@handler checkinAdd
post /v1/user/checkin_add (CheckinAddReq) returns (CheckinAddResp)

@doc "用户签到数据"
@handler checkinInfo
get /v1/user/checkin_info (CheckinInfoReq) returns (CheckinInfoResp)
}

type(
CheckinAddReq {
Date string `form:"date"` // 签到日期
}
CheckinAddResp {
UserID int64 `json:"user_id"`
Point int64 `json:"point"` // 积分总数
}
CheckinInfoReq {
StartDate string `form:"start_date"`
endDate string `form:"end_date"`
}
CheckinInfoResp {
CheckinInfo []CheckinInfo `json:"checkin_info"`
ContinuousDays int64 `json:"continuous_days"` // 连续签到天数
Point int64 `json:"point"` // 积分总数
}
CheckinInfo {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
CheckinDate string `json:"checkin_date"`
CheckinTime string `json:"checkin_time"`
Status int `json:"status"` // 是否已经签到(0:未签,1:已签)
}
)

签到业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
type CheckinAddLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}

// 用户签到
func NewCheckinAddLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckinAddLogic {
return &CheckinAddLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}

func (l *CheckinAddLogic) CheckinAdd(req *types.CheckinAddReq) (resp *types.CheckinAddResp, err error) {
resp = &types.CheckinAddResp{}

sUserId := l.ctx.Value("userid").(string)
iUserId, _ := strconv.Atoi(sUserId)

checkinDate, err := time.ParseInLocation(time.DateOnly, req.Date, time.Local)
if err != nil {
return nil, errors.New("日期格式不正确")
}
logx.Infof("checkinDate:%s", checkinDate.String())

db := l.svcCtx.DB
var userCheckin checkin.UserCheckin
db = db.Model(&checkin.UserCheckin{}).
Where("user_id = ? AND checkin_date = ?", iUserId, checkinDate.Format(time.DateOnly)).First(&userCheckin)
if db.Error != nil && !errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, db.Error
}
if userCheckin.ID > 0 {
return resp, errors.New("已签到")
}

t := time.Now()
startTime := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).In(time.Local)
endTime := time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location()).In(time.Local)

if checkinDate.Before(startTime) || checkinDate.After(endTime) {
return resp, errors.New("只能当天签到") // 暂时不支持补签
}

// 签到业务
tx := l.svcCtx.DB.Begin()

var lastCheckin checkin.UserCheckin
result := tx.Model(&checkin.UserCheckin{}).Where("user_id = ?", iUserId).Order("checkin_date DESC").
First(&lastCheckin)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
tx.Rollback()
return nil, result.Error
}

continuousDays := 1 // 默认连续天数为1

if result.Error == nil {
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
if lastCheckin.CheckinDate.Format("2006-01-02") == yesterday {
continuousDays = lastCheckin.ContinuousDays + 1
}
// 如果不是昨天,则保持默认的1(断签后重新开始计数)
}

userCheckin := checkin.UserCheckin{
UserID: int64(iUserId),
CheckinDate: checkinDate,
CheckinTime: time.Now(),
ContinuousDays: continuousDays, // 本次签到时的连续天数
RewardPoints: constant.CheckinRewardPoints,
RewardType: 1, // 奖励类型(1 - 签到,2 - 优惠券)
Remark: fmt.Sprintf("签到:奖励积分%d", constant.CheckinRewardPoints),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
result = tx.Model(&checkin.UserCheckin{}).Create(&userCheckin)
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
return nil, result.Error
}
// 签到统计
var userCheckinStat checkin.UserCheckinStats
result = tx.Model(&checkin.UserCheckinStats{}).Where("user_id = ?", iUserId).First(&userCheckinStat)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
tx.Rollback()
return nil, result.Error
}

if userCheckinStat.ID == 0 {
// 用户首次签到,初始化统计数据
result = tx.Model(&checkin.UserCheckinStats{}).Create(&checkin.UserCheckinStats{
UserID: int64(iUserId),
TotalCheckinDays: 1, // 累计签到总天数
MaxContinuousDays: 1, // 首次签到最大连续天数为1
CurrentContinuousDays: continuousDays,
LastCheckinDate: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
return nil, result.Error
}
} else {
// 更新统计信息
result = tx.Model(&checkin.UserCheckinStats{}).Where("user_id = ?", iUserId).Updates(map[string]interface{}{
"total_checkin_days": gorm.Expr("total_checkin_days + ?", 1),
"max_continuous_days": gorm.Expr("GREATEST(max_continuous_days, ?)", continuousDays),
"current_continuous_days": continuousDays,
"last_checkin_date": time.Now(),
"updated_at": time.Now(),
})
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
return nil, result.Error
}
}
// 用户当前积分
var userInfo model.UserInfo
result = tx.Model(&model.UserInfo{}).Where("user_id = ?", iUserId).First(&userInfo)
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
tx.Rollback()
return nil, result.Error
}

pointBalance := userInfo.Point + constant.CheckinRewardPoints

// 积分变更明细
result = tx.Model(&checkin.UserPointChangeLog{}).Create(&checkin.UserPointChangeLog{
UserID: int64(iUserId),
Point: constant.CheckinRewardPoints,
PointBalance: int(pointBalance),
Type: 1, // 类型:1-签到 2-抽奖
BusinessID: userCheckin.ID, // 关联签到表ID
ChangeDate: time.Now(),
ChangeTime: time.Now(),
Remark: fmt.Sprintf("签到:奖励积分%d", constant.CheckinRewardPoints),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
return nil, result.Error
}
// 更新用户积分
result = db.Model(&model.UserInfo{}).Where("user_id = ?", iUserId).Updates(map[string]interface{}{
"point": gorm.Expr("point + ?", constant.CheckinRewardPoints),
"updated_at": time.Now(),
})
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
return nil, result.Error
}

tx.Commit()

resp.UserID = int64(userInfo.UserID)
resp.Point = int64(pointBalance)

return resp, nil
}

用户签到数据接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
type CheckinInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}

// 用户签到数据
func NewCheckinInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckinInfoLogic {
return &CheckinInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}

func (l *CheckinInfoLogic) CheckinInfo(req *types.CheckinInfoReq) (resp *types.CheckinInfoResp, err error) {
resp = &types.CheckinInfoResp{}

startDate, err := time.ParseInLocation(time.DateOnly, req.StartDate, time.Local)
if err != nil {
return nil, errors.New("解析开始时间失败")
}
endDate, err := time.ParseInLocation(time.DateOnly, req.EndDate, time.Local)
if err != nil {
return nil, errors.New("解析结束时间失败")
}
// 检查开始时间是否晚于结束时间
if startDate.After(endDate) {
return nil, errors.New("开始时间不能晚于结束时间")
}

dates, err := pkg.GetAllDates(startDate, endDate)
if err != nil {
return nil, errors.New("计算并返回开始日期到结束日期之间的所有日期错误")
}
logx.Info(dates)

db := l.svcCtx.DB
sUserId := l.ctx.Value("userid").(string)
iUserId, _ := strconv.Atoi(sUserId)

var userCheckinLog []checkin.UserCheckinLog
db = db.Select("*").
Table("t_user_checkin").
Where("user_id = ?", iUserId).
Order("created_at ASC").
Find(&userCheckinLog)
if db.Error != nil && !errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("查询用户签到记录出错")
}

userCheckinMap := make(map[string]checkin.UserCheckinLog)
for _, log := range userCheckinLog {
dateStr := log.CheckinDate.Format(time.DateOnly)
userCheckinMap[dateStr] = log
}

for _, date := range dates {
dateStr := date.Format(time.DateOnly)
// 初始化当前日期的签到信息(默认未签到)
checkinInfo := types.CheckinInfo{
ID: 0,
UserID: int64(iUserId),
CheckinDate: dateStr,
CheckinTime: "",
Status: 0,
}

if log, exists := userCheckinMap[dateStr]; exists {
// 有签到记录
checkinInfo.ID = log.ID
checkinInfo.UserId = log.UserId
checkinInfo.CheckinDate = dateStr
checkinInfo.CheckinTime = log.CheckinTime.Format(time.DateTime)
checkinInfo.Status = 1 // 已签到
}
resp.CheckinInfo = append(resp.CheckinInfo, checkinInfo)
}

db = l.svcCtx.DB
var userCheckinStats checkin.UserCheckinStats
db = db.Select("a.user_id,point,current_continuous_days")
db = db.Table("t_user_info AS a")
db = db.Joins("INNER JOIN t_user_checkin_stats AS b ON a.user_id = b.user_id ")
db = db.Where("a.user_id = ?", iUserId)
db = db.First(&userCheckinStats)
if db.Error != nil && !errors.Is(db.Error, gorm.ErrRecordNotFound) {
return nil, errors.New("查询用户签到信息出错")
}

resp.Point = userCheckinStats.Point
resp.ContinuousDays = userCheckinStats.CurrentContinuousDays
return
}