expired.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. // ExpiredInfo 包含了关闭套餐的详细信息
  11. type ExpiredInfo struct {
  12. HostID int64 `json:"host_id"` // hostid, a.k.a. planid
  13. Expiry time.Time `json:"expiry"` // 过期时间
  14. }
  15. // ExpiredRepository 定义了与过期套餐相关的操作接口
  16. type ExpiredRepository interface {
  17. // AddClosePlans 批量添加要关闭的套餐信息,并设置过期时间
  18. AddClosePlans(ctx context.Context, infos ...ExpiredInfo) error
  19. // GetClosePlanInfo 获取单个已关闭套餐的详细信息
  20. GetClosePlanInfo(ctx context.Context, planId int64) (*ExpiredInfo, error)
  21. // RemoveClosePlanIds 批量移除已关闭的套餐
  22. RemoveClosePlanIds(ctx context.Context, planIds ...int64) error
  23. // GetAllClosePlanIds 获取所有当前套餐ID
  24. GetAllClosePlanIds(ctx context.Context) ([]int64, error)
  25. // IsPlanClosed 检查一个套餐是否被标记为关闭
  26. IsPlanClosed(ctx context.Context, planId int64) (bool, error)
  27. }
  28. func NewExpiredRepository(
  29. repository *Repository,
  30. ) ExpiredRepository {
  31. return &expiredRepository{
  32. Repository: repository,
  33. }
  34. }
  35. type expiredRepository struct {
  36. *Repository
  37. }
  38. // Key的前缀,用于标识所有已关闭套餐的Key
  39. const closePlanIdKeyPrefix = "waf:closed_plan:"
  40. // 辅助函数:根据 planId 生成对应的 Redis Key
  41. func (r *expiredRepository) getPlanKey(planId int64) string {
  42. return fmt.Sprintf("%s%d", closePlanIdKeyPrefix, planId)
  43. }
  44. // AddClosePlans 为每个套餐创建一个独立的 key,并将详细信息作为 value 存储
  45. func (r *expiredRepository) AddClosePlans(ctx context.Context, infos ...ExpiredInfo) error {
  46. if len(infos) == 0 {
  47. return nil
  48. }
  49. pipe := r.rdb.Pipeline()
  50. for _, info := range infos {
  51. key := r.getPlanKey(info.HostID)
  52. // 将结构体序列化为 JSON 字符串
  53. value, err := json.Marshal(info)
  54. if err != nil {
  55. // 在实际应用中,这里应该记录错误日志
  56. // log.Printf("Error marshalling ExpiredInfo for plan %d: %v", info.HostID, err)
  57. continue // 跳过这个错误的数据
  58. }
  59. // 设置一个固定的7天过期时间,用于记录关闭状态
  60. // 这样可以确保在7天内,该套餐的状态是“已关闭”,可以被恢复
  61. // 7天后,该key会自动过期
  62. const sevenDays = 7 * 24 * time.Hour
  63. pipe.Set(ctx, key, value, sevenDays)
  64. }
  65. _, err := pipe.Exec(ctx)
  66. return err
  67. }
  68. // GetClosePlanInfo 获取并解析单个套餐的信息
  69. func (r *expiredRepository) GetClosePlanInfo(ctx context.Context, planId int64) (*ExpiredInfo, error) {
  70. key := r.getPlanKey(planId)
  71. value, err := r.rdb.Get(ctx, key).Result()
  72. if err != nil {
  73. return nil, err // 包括 redis.Nil 的情况
  74. }
  75. var info ExpiredInfo
  76. if err := json.Unmarshal([]byte(value), &info); err != nil {
  77. return nil, fmt.Errorf("failed to unmarshal plan info for key %s: %w", key, err)
  78. }
  79. return &info, nil
  80. }
  81. // RemoveClosePlanIds 删除每个 planId 对应的 key
  82. func (r *expiredRepository) RemoveClosePlanIds(ctx context.Context, planIds ...int64) error {
  83. if len(planIds) == 0 {
  84. return nil
  85. }
  86. // 生成所有需要删除的 key
  87. keys := make([]string, len(planIds))
  88. for i, id := range planIds {
  89. keys[i] = r.getPlanKey(id)
  90. }
  91. // DEL 命令可以一次性删除多个 key
  92. return r.rdb.Del(ctx, keys...).Err()
  93. }
  94. // GetAllClosePlanIds 使用 SCAN 遍历所有匹配的 key 并解析出 planId
  95. func (r *expiredRepository) GetAllClosePlanIds(ctx context.Context) ([]int64, error) {
  96. var cursor uint64
  97. var allKeys []string
  98. // 使用 SCAN 命令来安全地遍历大量的 key,避免阻塞 Redis
  99. // KEYS 命令会导致性能问题,在生产环境中严禁使用
  100. scanPattern := closePlanIdKeyPrefix + "*"
  101. for {
  102. var keys []string
  103. var err error
  104. keys, cursor, err = r.rdb.Scan(ctx, cursor, scanPattern, 100).Result() // 每次扫描100个
  105. if err != nil {
  106. return nil, err
  107. }
  108. allKeys = append(allKeys, keys...)
  109. // 如果 cursor 回到 0,表示遍历完成
  110. if cursor == 0 {
  111. break
  112. }
  113. }
  114. // 从 key 的字符串中解析出 planId
  115. planIds := make([]int64, 0, len(allKeys))
  116. for _, key := range allKeys {
  117. // key 的格式是 "waf:closed_plan:12345"
  118. // 我们需要移除前缀 "waf:closed_plan:" 来获取ID部分
  119. idStr := strings.TrimPrefix(key, closePlanIdKeyPrefix)
  120. id, err := strconv.ParseInt(idStr, 10, 64)
  121. if err != nil {
  122. // 如果有无法解析的key,最好记录日志并跳过
  123. // log.Printf("Warning: could not parse planId from key '%s': %v", key, err)
  124. continue
  125. }
  126. planIds = append(planIds, id)
  127. }
  128. return planIds, nil
  129. }
  130. // IsPlanClosed 检查 planId 对应的 key 是否存在
  131. func (r *expiredRepository) IsPlanClosed(ctx context.Context, planId int64) (bool, error) {
  132. key := r.getPlanKey(planId)
  133. // EXISTS 命令是 O(1) 的高效操作,返回存在的key的数量
  134. count, err := r.rdb.Exists(ctx, key).Result()
  135. if err != nil {
  136. return false, err
  137. }
  138. // 如果 count > 0,说明key存在,即套餐已关闭
  139. return count > 0, nil
  140. }