跳至内容
签到系统项目开发

签到系统项目开发

项目开发

整个项目后端分为三个模块,

  • 用户模块
  • 签到模块
  • 积分模块

快速搭建项目框架。

此部分请直接看视频和项目源码此部分请直接看视频和项目源码此部分请直接看视频和项目源码

一、用户模块

如果公司已经存在独立的用户中心,则不需要开发这部分。直接通过 API/RPC 的方式接入即可。

本套课程为了项目演示方便,实现了一个简单的用户模块。

具体可参考 Go Web 进阶项目实战部分的用户模块设计

用户注册

用户登录

鉴权

个人信息

二、签到模块

每日签到

签到日历

补签

三、积分模块

总积分

积分明细

四、签到提醒

每日签到提醒

可以在页面设置一个签到提醒开发,用户打开后,支持每天发Push或短信提醒用户签到。需要考虑短信的成本。

运营策略提醒

可以根据特定的运营策略实现签到提醒功能。

比如在每天 18:00 点对那些前两天签到了,但是今天还没有签到的用户发送即将断签提醒,引导用户及时完成签到,获得连续签到奖励。

用户量级比较大的时候,需要编写 Lua 脚本,并使用 script load 功能,提高执行效率。

-- KEYS[1]: 用户签到key
-- ARGV[1]: 当前日偏移量(从年初开始的天数)
-- ARGV[2]: 提醒阈值

local offset = tonumber(ARGV[1])
local threshold = tonumber(ARGV[2])

-- 检查今天是否已签到(最新位)
local today = redis.call('GETBIT', KEYS[1], offset)
if today == 1 then return 0 end -- 已签到无需提醒

-- 检查最近threshold天的签到情况
local continuous = true
for i = 1, threshold do
    local bit = redis.call('GETBIT', KEYS[1], offset - i)
    if bit ~= 1 then
        continuous = false
        break
    end
end

-- 返回结果: 1需要提醒 0不需要
return continuous and 1 or 0

在 Go 语言中使用 go:embed 将 lua 脚本编译到可执行文件中。

Goframe 版本

//go:embed remind.lua
var remindScript string

// CheckAndNotify 检查签到并发送通知
func CheckAndNotify(ctx context.Context, remindThreshold int) error {
	// 1. 获取所有符合条件的用户
	// 可以通过扫 MySQL 用户表找到最近几天有登录过的(如果有现成的用户登录时间记录可以直接拿来用)
	// 或者可以在用户签到的时候记录一个 ZSet, userID:签到时间戳(如果用户量多需要拆分 Key)
	userIDs := []uint64{25016147980058993}
	rc := injection.MustInvoke[*redis.Client]()
	// 2. 加载 lua script
	sha, err := rc.ScriptLoad(ctx, remindScript).Result()
	if err != nil {
		fmt.Printf("ScriptLoad err: %v\n", err)
		return err
	}
	// 3. 遍历判断每个用户
	for _, userID := range userIDs {
		key := fmt.Sprintf(yearSignKeyFormat, userID, time.Now().Year())

		// 计算当前日偏移量(当年第几天)
		dayOfYearOffset := time.Now().YearDay() - 1
		fmt.Printf("key: %s, dayOfYearOffset: %d\n", key, dayOfYearOffset)
		// 执行LUA脚本
		result, err := rc.EvalSha(ctx, sha, []string{key}, dayOfYearOffset, 2).Int()
		fmt.Printf("result: %d, err: %v\n", result, err)
		if err != nil {
			return err
		}

		if result == 1 {
			fmt.Printf("用户%d需要发送断签提醒\n", userID)
			// 发送到消息队列,执行后续的推送逻辑(APP Push 或 短信等)
		}
	}
	return nil
}

使用 goframe 框架的 gcron 实现定时任务。

// 开启定时任务
_, err = gcron.Add(ctx, "# 0 18 * * *", func(ctx context.Context) {
	g.Log().Print(ctx, "每天18点跑定时任务")
	err := impl.CheckAndNotify(ctx, 2)
	fmt.Printf("CheckAndNotify err: %v\n", err)
})
if err != nil {
	panic(err)
}

Gin 版本

package task

import (
	"context"
	"fmt"
	"sunflower-gin/internal/dao"
	"time"

	_ "embed"
)

const (
	yearSignKeyFormat = "user:checkins:daily:%d:%d" // user:checkins:daily:12131321421312:2025
)

//go:embed remind.lua
var remindScript string

// CheckAndNotify 检查签到并发送通知
func CheckAndNotify(ctx context.Context, remindThreshold int) error {
	fmt.Println("start check and notify...")
	// 1. 获取所有符合条件的用户
	// 可以通过扫 MySQL 用户表找到最近几天有登录过的(如果有现成的用户登录时间记录可以直接拿来用)
	// 或者可以在用户签到的时候记录一个 ZSet, userID:签到时间戳(如果用户量多需要拆分 Key)
	userIDs := []uint64{25016147980058993}

	// 2. 加载 lua script
	sha, err := dao.RedisClient.ScriptLoad(ctx, remindScript).Result()
	if err != nil {
		fmt.Printf("ScriptLoad err: %v\n", err)
		return err
	}
	// 3. 遍历判断每个用户
	now := time.Now()
	for _, userID := range userIDs {
		key := fmt.Sprintf(yearSignKeyFormat, userID, time.Now().Year())

		// 计算当前日偏移量(当年第几天)
		dayOfYearOffset := now.YearDay() - 1
		fmt.Printf("key: %s, dayOfYearOffset: %d\n", key, dayOfYearOffset)
		// 执行LUA脚本
		result, err := dao.RedisClient.EvalSha(ctx, sha, []string{key}, dayOfYearOffset, remindThreshold).Int()
		fmt.Printf("result: %d, err: %v\n", result, err)
		if err != nil {
			return err
		}

		if result == 1 {
			fmt.Printf("用户%d需要发送断签提醒\n", userID)
			// 发送到消息队列,执行后续的推送逻辑(APP Push 或 短信等)
		}
	}
	return nil
}

使用 github.com/robfig/cron/v3 执行定时任务。

// task.go
package task

import (
	"context"
	"time"

	"github.com/robfig/cron/v3"
)

// 定时任务

/*
github.com/robfig/cron/v3 CRON 表达式格式

Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Minutes      | Yes        | 0-59            | * / , -
Hours        | Yes        | 0-23            | * / , -
Day of month | Yes        | 1-31            | * / , - ?
Month        | Yes        | 1-12 or JAN-DEC | * / , -
Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?
*/

func MustInit(ctx context.Context) *cron.Cron {
	tz, err := time.LoadLocation("Local")
	if err != nil {
		panic(err)
	}
	c := cron.New(cron.WithLocation(tz))
	c.AddFunc("25 20 * * *", func() { CheckAndNotify(ctx, 2) })
	c.Start()
	return c
}
最后更新于 • Q1mi