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()