123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- 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
- }
|