Files
bl/common/data/share/task.go
2025-08-28 17:13:54 +00:00

347 lines
9.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package share
import (
"blazing/cool"
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/gconv"
)
// 周期类型定义
type PeriodType int8
const (
PeriodDaily PeriodType = 1 // 每日
PeriodWeekly PeriodType = 2 // 每周
PeriodMonthly PeriodType = 3 // 每月
PeriodQuarter PeriodType = 4 // 每季
)
// 周期函数类型定义
type (
// FormatFunc 生成周期格式化字符串的函数
FormatFunc func(t time.Time) string
// ExpireFunc 计算周期过期时间的函数
ExpireFunc func(t time.Time) time.Duration
)
// Period 周期结构体,聚合周期相关函数和元信息
type Period struct {
Type PeriodType // 周期类型
Format FormatFunc // 周期格式化函数
Expire ExpireFunc // 过期时间计算函数
Prefix string // 缓存键前缀
}
// 周期实例 - 每日
var DailyPeriod = Period{
Type: PeriodDaily,
Format: func(t time.Time) string {
return t.Format("20060102") // 格式: YYYYMMDD
},
Expire: func(t time.Time) time.Duration {
tomorrow := t.AddDate(0, 0, 1)
resetTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.Local)
return resetTime.Sub(t)
},
Prefix: "daily:",
}
// 周期实例 - 每周
var WeeklyPeriod = Period{
Type: PeriodWeekly,
Format: func(t time.Time) string {
// 计算当年第几周 (ISO周编号)
year, week := t.ISOWeek()
return fmt.Sprintf("%dW%02d", year, week) // 格式: YYYYWww
},
Expire: func(t time.Time) time.Duration {
// 计算下周一0点
weekday := t.Weekday()
daysToNextMonday := (8 - int(weekday)) % 7
nextMonday := t.AddDate(0, 0, daysToNextMonday)
resetTime := time.Date(nextMonday.Year(), nextMonday.Month(), nextMonday.Day(), 0, 0, 0, 0, time.Local)
return resetTime.Sub(t)
},
Prefix: "weekly:",
}
// 周期实例 - 每月
var MonthlyPeriod = Period{
Type: PeriodMonthly,
Format: func(t time.Time) string {
return t.Format("200601") // 格式: YYYYMM
},
Expire: func(t time.Time) time.Duration {
// 计算下月1日0点
nextMonth := t.AddDate(0, 1, 0)
resetTime := time.Date(nextMonth.Year(), nextMonth.Month(), 1, 0, 0, 0, 0, time.Local)
return resetTime.Sub(t)
},
Prefix: "monthly:",
}
// 周期实例 - 每季
var QuarterlyPeriod = Period{
Type: PeriodQuarter,
Format: func(t time.Time) string {
quarter := (int(t.Month())-1)/3 + 1
return fmt.Sprintf("%dQ%d", t.Year(), quarter) // 格式: YYYYQq
},
Expire: func(t time.Time) time.Duration {
// 计算下季首月1日0点
quarter := (int(t.Month())-1)/3 + 1
nextQuarterFirstMonth := quarter*3 + 1
year := t.Year()
if nextQuarterFirstMonth > 12 {
nextQuarterFirstMonth = 1
year++
}
resetTime := time.Date(year, time.Month(nextQuarterFirstMonth), 1, 0, 0, 0, 0, time.Local)
return resetTime.Sub(t)
},
Prefix: "quarterly:",
}
// 周期映射表,用于快速查找周期实例
var periodMap = map[PeriodType]*Period{
PeriodDaily: &DailyPeriod,
PeriodWeekly: &WeeklyPeriod,
PeriodMonthly: &MonthlyPeriod,
PeriodQuarter: &QuarterlyPeriod,
}
// GetPeriodByType 根据周期类型获取周期实例
func GetPeriodByType(periodType PeriodType) (*Period, error) {
period, ok := periodMap[periodType]
if !ok {
return nil, fmt.Errorf("不支持的周期类型: %d", periodType)
}
return period, nil
}
// newCounterStore 创建计数器缓存实例
func newCounterStore(period *Period) *cacheStore[int64] {
return &cacheStore[int64]{
manager: cool.CacheManager,
prefix: fmt.Sprintf("blazing:counter:%s", period.Prefix),
}
}
// newLimitStore 创建限制配置缓存实例
func newLimitStore() *cacheStore[int64] {
return &cacheStore[int64]{
manager: cool.CacheManager,
prefix: "blazing:counterlimit:",
}
}
// CounterManager 计数器管理器
type CounterManager struct {
limitStore *cacheStore[int64] // 限制配置缓存
periods map[PeriodType]*cacheStore[int64] // 各周期计数器缓存
}
// NewCounterManager 创建计数器管理器实例
func NewCounterManager() *CounterManager {
// 初始化所有周期的缓存存储
periods := make(map[PeriodType]*cacheStore[int64])
for _, period := range periodMap {
periods[period.Type] = newCounterStore(period)
}
return &CounterManager{
limitStore: newLimitStore(),
periods: periods,
}
}
// genKey 生成计数器键
func (m *CounterManager) genKey(period *Period, userID uint32, rewardType uint32) string {
periodStr := period.Format(time.Now())
return fmt.Sprintf("%d:%d:%s", userID, rewardType, periodStr)
}
// genUserPrefix 生成用户某周期所有计数的键前缀
func (m *CounterManager) genUserPrefix(period *Period, userID uint32) string {
periodStr := period.Format(time.Now())
return fmt.Sprintf("%d:*:%s", userID, periodStr)
}
// GetCount 获取用户某周期某奖品的计数
func (m *CounterManager) GetCount(period *Period, userID uint32, rewardType uint32) (int64, error) {
store, ok := m.periods[period.Type]
if !ok {
return 0, fmt.Errorf("不支持的周期类型: %d", period.Type)
}
key := m.genKey(period, userID, rewardType)
return store.Get(context.Background(), key)
}
// IncrCount 增加用户某周期某奖品的计数(自动设置周期结束过期)
func (m *CounterManager) IncrCount(period *Period, userID uint32, rewardType uint32) (int64, error) {
store, ok := m.periods[period.Type]
if !ok {
return 0, fmt.Errorf("不支持的周期类型: %d", period.Type)
}
key := m.genKey(period, userID, rewardType)
// 获取当前计数不存在则视为0
current, err := store.Get(context.Background(), key)
if err != nil && err != ErrCacheMiss {
return 0, fmt.Errorf("获取当前计数失败: %w", err)
}
if err == ErrCacheMiss {
current = 0
}
// 计算新计数
newValue := current + 1
// 计算过期时间
ttl := period.Expire(time.Now())
// 保存新计数并设置过期时间
if err := store.Set(gctx.New(), key, newValue, ttl); err != nil {
return 0, fmt.Errorf("保存计数失败: %w", err)
}
return newValue, nil
}
// ResetCount 重置用户某周期某奖品的计数
func (m *CounterManager) ResetCount(period *Period, userID uint32, rewardType uint32) error {
store, ok := m.periods[period.Type]
if !ok {
return fmt.Errorf("不支持的周期类型: %d", period.Type)
}
key := m.genKey(period, userID, rewardType)
return store.Del(gctx.New(), key)
}
// SetLimit 设置某周期某奖品类型的限制
func (m *CounterManager) SetLimit(period *Period, rewardType uint32, limit int64) error {
key := fmt.Sprintf("%d:%d", period.Type, rewardType)
return m.limitStore.Set(gctx.New(), key, limit, time.Hour*24*7) // 限制配置保留7天
}
// GetLimit 获取某周期某奖品类型的限制
func (m *CounterManager) GetLimit(period *Period, rewardType uint32) (int64, error) {
key := fmt.Sprintf("%d:%d", period.Type, rewardType)
return m.limitStore.Get(context.Background(), key)
}
// IsExceedLimit 检查用户某周期某奖品的计数是否超过限制
func (m *CounterManager) IsExceedLimit(period *Period, userID uint32, rewardType uint32) (bool, error) {
count, err := m.GetCount(period, userID, rewardType)
if err != nil && err != ErrCacheMiss {
return false, fmt.Errorf("获取计数失败: %w", err)
}
// 计数不存在时视为0
if err == ErrCacheMiss {
count = 0
}
limit, err := m.GetLimit(period, rewardType)
if err != nil {
return false, fmt.Errorf("获取限制失败: %w", err)
}
return count >= limit, nil
}
// GetUserAllCounts 获取用户某周期所有奖品类型的计数
func (m *CounterManager) GetUserAllCounts(period *Period, userID uint32) (map[uint32]int64, error) {
store, ok := m.periods[period.Type]
if !ok {
return nil, fmt.Errorf("不支持的周期类型: %d", period.Type)
}
// 1. 生成扫描模式
periodStr := period.Format(time.Now())
matchPattern := fmt.Sprintf(
"%s%d:*:%s",
store.prefix,
userID,
periodStr,
)
// 2. 扫描所有匹配的键
keys, err := store.Scan(context.Background(), matchPattern)
if err != nil {
return nil, fmt.Errorf("扫描键失败: %w", err)
}
if len(keys) == 0 {
return make(map[uint32]int64), nil // 无数据时返回空map
}
// 3. 提取核心键(去除缓存前缀)
coreKeys := make([]string, 0, len(keys))
prefixLen := len(store.prefix)
for _, fullKey := range keys {
if strings.HasPrefix(fullKey, store.prefix) {
coreKey := fullKey[prefixLen:] // 核心键格式userID:rewardType:periodStr
coreKeys = append(coreKeys, coreKey)
}
}
// 4. 批量获取计数
values, err := store.MGet(context.Background(), coreKeys)
if err != nil {
return nil, fmt.Errorf("批量获取计数失败: %w", err)
}
// 5. 解析结果
result := make(map[uint32]int64, len(coreKeys))
for i, coreKey := range coreKeys {
parts := strings.Split(coreKey, ":")
if len(parts) != 3 {
fmt.Printf("[WARN] 忽略格式错误的键: %s\n", coreKey)
continue
}
// 验证用户ID
parsedUserID, err := strconv.ParseUint(parts[0], 10, 32)
if err != nil || uint32(parsedUserID) != userID {
fmt.Printf("[WARN] 忽略用户不匹配的键: %s\n", coreKey)
continue
}
// 验证周期
if parts[2] != periodStr {
fmt.Printf("[WARN] 忽略过期键: %s周期不匹配\n", coreKey)
continue
}
// 解析rewardType
rewardType, err := strconv.ParseUint(parts[1], 10, 32)
if err != nil {
fmt.Printf("[WARN] 解析rewardType失败: %s\n", coreKey)
continue
}
// 解析计数
val := values[i]
if val == nil {
continue
}
count := gconv.Int64(val)
result[uint32(rewardType)] = count
}
return result, nil
}
// 全局计数器管理器实例
var GlobalCounterManager = NewCounterManager()