package repository import ( "context" "encoding/json" "fmt" "strconv" "strings" "time" ) // ExpiredInfo 包含了关闭套餐的详细信息 type ExpiredInfo struct { HostID int64 `json:"host_id"` // hostid, a.k.a. planid Expiry time.Time `json:"expiry"` // 过期时间 } // ExpiredRepository 定义了与过期套餐相关的操作接口 type ExpiredRepository interface { // AddClosePlans 批量添加要关闭的套餐信息,并设置过期时间 AddClosePlans(ctx context.Context, infos ...ExpiredInfo) error // GetClosePlanInfo 获取单个已关闭套餐的详细信息 GetClosePlanInfo(ctx context.Context, planId int64) (*ExpiredInfo, error) // RemoveClosePlanIds 批量移除已关闭的套餐 RemoveClosePlanIds(ctx context.Context, planIds ...int64) error // GetAllClosePlanIds 获取所有当前套餐ID GetAllClosePlanIds(ctx context.Context) ([]int64, error) // IsPlanClosed 检查一个套餐是否被标记为关闭 IsPlanClosed(ctx context.Context, planId int64) (bool, error) } func NewExpiredRepository( repository *Repository, ) ExpiredRepository { return &expiredRepository{ Repository: repository, } } type expiredRepository struct { *Repository } // Key的前缀,用于标识所有已关闭套餐的Key const closePlanIdKeyPrefix = "waf:closed_plan:" // 辅助函数:根据 planId 生成对应的 Redis Key func (r *expiredRepository) getPlanKey(planId int64) string { return fmt.Sprintf("%s%d", closePlanIdKeyPrefix, planId) } // AddClosePlans 为每个套餐创建一个独立的 key,并将详细信息作为 value 存储 func (r *expiredRepository) AddClosePlans(ctx context.Context, infos ...ExpiredInfo) error { if len(infos) == 0 { return nil } pipe := r.rdb.Pipeline() for _, info := range infos { key := r.getPlanKey(info.HostID) // 将结构体序列化为 JSON 字符串 value, err := json.Marshal(info) if err != nil { // 在实际应用中,这里应该记录错误日志 // log.Printf("Error marshalling ExpiredInfo for plan %d: %v", info.HostID, err) continue // 跳过这个错误的数据 } // 设置一个固定的7天过期时间,用于记录关闭状态 // 这样可以确保在7天内,该套餐的状态是“已关闭”,可以被恢复 // 7天后,该key会自动过期 const sevenDays = 7 * 24 * time.Hour pipe.Set(ctx, key, value, sevenDays) } _, err := pipe.Exec(ctx) return err } // GetClosePlanInfo 获取并解析单个套餐的信息 func (r *expiredRepository) GetClosePlanInfo(ctx context.Context, planId int64) (*ExpiredInfo, error) { key := r.getPlanKey(planId) value, err := r.rdb.Get(ctx, key).Result() if err != nil { return nil, err // 包括 redis.Nil 的情况 } var info ExpiredInfo if err := json.Unmarshal([]byte(value), &info); err != nil { return nil, fmt.Errorf("failed to unmarshal plan info for key %s: %w", key, err) } return &info, nil } // RemoveClosePlanIds 删除每个 planId 对应的 key func (r *expiredRepository) RemoveClosePlanIds(ctx context.Context, planIds ...int64) error { if len(planIds) == 0 { return nil } // 生成所有需要删除的 key keys := make([]string, len(planIds)) for i, id := range planIds { keys[i] = r.getPlanKey(id) } // DEL 命令可以一次性删除多个 key return r.rdb.Del(ctx, keys...).Err() } // GetAllClosePlanIds 使用 SCAN 遍历所有匹配的 key 并解析出 planId func (r *expiredRepository) GetAllClosePlanIds(ctx context.Context) ([]int64, error) { var cursor uint64 var allKeys []string // 使用 SCAN 命令来安全地遍历大量的 key,避免阻塞 Redis // KEYS 命令会导致性能问题,在生产环境中严禁使用 scanPattern := closePlanIdKeyPrefix + "*" for { var keys []string var err error keys, cursor, err = r.rdb.Scan(ctx, cursor, scanPattern, 100).Result() // 每次扫描100个 if err != nil { return nil, err } allKeys = append(allKeys, keys...) // 如果 cursor 回到 0,表示遍历完成 if cursor == 0 { break } } // 从 key 的字符串中解析出 planId planIds := make([]int64, 0, len(allKeys)) for _, key := range allKeys { // key 的格式是 "waf:closed_plan:12345" // 我们需要移除前缀 "waf:closed_plan:" 来获取ID部分 idStr := strings.TrimPrefix(key, closePlanIdKeyPrefix) id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { // 如果有无法解析的key,最好记录日志并跳过 // log.Printf("Warning: could not parse planId from key '%s': %v", key, err) continue } planIds = append(planIds, id) } return planIds, nil } // IsPlanClosed 检查 planId 对应的 key 是否存在 func (r *expiredRepository) IsPlanClosed(ctx context.Context, planId int64) (bool, error) { key := r.getPlanKey(planId) // EXISTS 命令是 O(1) 的高效操作,返回存在的key的数量 count, err := r.rdb.Exists(ctx, key).Result() if err != nil { return false, err } // 如果 count > 0,说明key存在,即套餐已关闭 return count > 0, nil }