347 lines
9.5 KiB
Go
347 lines
9.5 KiB
Go
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()
|