package store import ( "context" "time" "github.com/redis/go-redis/v9" "github.com/hhs/camtalk/internal/logger" ) // Redis key 前缀。 const ( refreshTokenPrefix = "auth:refresh:" // auth:refresh:{token_hash} → user_id userRefreshPrefix = "auth:user_refresh:" // auth:user_refresh:{user_id} → Set of token_hash ) // CachedUserRepository 装饰器,为 UserRepository 的 refresh token 操作增加 Redis 缓存。 // 读路径:Redis miss → DB → 回填 Redis。 // 写路径:同步双写 Redis + DB。 // 删路径:同步双删 Redis + DB。 // Redis 操作失败时降级到纯 DB,不阻断主流程。 type CachedUserRepository struct { inner UserRepository rdb *redis.Client backfillTTL time.Duration // DB 回填 Redis 时使用的默认 TTL } // NewCachedUserRepository 创建带 Redis 缓存的 UserRepository 装饰器。 // backfillTTL: 从 DB 回填 Redis 时使用的 TTL(因 DB 接口不返回 expiresAt)。 func NewCachedUserRepository(inner UserRepository, rdb *redis.Client, backfillTTL time.Duration) *CachedUserRepository { if backfillTTL <= 0 { backfillTTL = 24 * time.Hour } return &CachedUserRepository{ inner: inner, rdb: rdb, backfillTTL: backfillTTL, } } // refreshTokenKey 生成 refresh token 的 Redis key。 func refreshTokenKey(tokenHash string) string { return refreshTokenPrefix + tokenHash } // userRefreshKey 生成用户 refresh token 集合的 Redis key。 func userRefreshKey(userID string) string { return userRefreshPrefix + userID } // --- 委托方法(不做缓存) --- func (r *CachedUserRepository) Create(ctx context.Context, username, passwordHash string) (string, error) { return r.inner.Create(ctx, username, passwordHash) } func (r *CachedUserRepository) FindByUsername(ctx context.Context, username string) (*User, error) { return r.inner.FindByUsername(ctx, username) } func (r *CachedUserRepository) FindByID(ctx context.Context, id string) (*User, error) { return r.inner.FindByID(ctx, id) } // --- 缓存方法 --- // SaveRefreshToken Write-Through:先写 DB,再写 Redis。 func (r *CachedUserRepository) SaveRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error { // 先写 DB if err := r.inner.SaveRefreshToken(ctx, userID, tokenHash, expiresAt); err != nil { return err } // 写 Redis(SET + SADD),设置 TTL 为 token 剩余有效期 ttl := time.Until(expiresAt) if ttl <= 0 { return nil } key := refreshTokenKey(tokenHash) pipe := r.rdb.Pipeline() pipe.Set(ctx, key, userID, ttl) pipe.SAdd(ctx, userRefreshKey(userID), tokenHash) if _, err := pipe.Exec(ctx); err != nil { logger.Log.Warnw("Redis cache write failed for refresh token", "error", err) // 降级:DB 已写入成功,Redis 失败不影响正确性 } return nil } // FindRefreshToken Read-Through:先查 Redis,miss 时查 DB 并回填。 func (r *CachedUserRepository) FindRefreshToken(ctx context.Context, tokenHash string) (string, error) { key := refreshTokenKey(tokenHash) // 查 Redis userID, err := r.rdb.Get(ctx, key).Result() if err == nil { return userID, nil } // redis.Nil 表示 key 不存在,其他错误记录日志后降级到 DB if err != redis.Nil { logger.Log.Warnw("Redis cache read failed for refresh token", "error", err) } // 降级到 DB userID, err = r.inner.FindRefreshToken(ctx, tokenHash) if err != nil { return "", err } // 回填 Redis(SET + SADD),TTL 使用保守默认值 go func() { bgCtx := context.Background() pipe := r.rdb.Pipeline() pipe.Set(bgCtx, key, userID, r.backfillTTL) pipe.SAdd(bgCtx, userRefreshKey(userID), tokenHash) _, _ = pipe.Exec(bgCtx) }() return userID, nil } // DeleteRefreshToken 双删:先删 DB,再删 Redis。 func (r *CachedUserRepository) DeleteRefreshToken(ctx context.Context, tokenHash string) error { // 先从 Redis 获取 user_id(用于从集合中移除) userID, _ := r.rdb.Get(ctx, refreshTokenKey(tokenHash)).Result() // 删 DB if err := r.inner.DeleteRefreshToken(ctx, tokenHash); err != nil { return err } // 删 Redis key := refreshTokenKey(tokenHash) pipe := r.rdb.Pipeline() pipe.Del(ctx, key) if userID != "" { pipe.SRem(ctx, userRefreshKey(userID), tokenHash) } if _, err := pipe.Exec(ctx); err != nil { logger.Log.Warnw("Redis cache delete failed for refresh token", "error", err) } return nil } // DeleteUserRefreshTokens 批量清理:先从 Redis 获取集合,逐个删缓存,再删 DB。 func (r *CachedUserRepository) DeleteUserRefreshTokens(ctx context.Context, userID string) error { userKey := userRefreshKey(userID) // 从 Redis 获取该用户所有 token hash hashes, _ := r.rdb.SMembers(ctx, userKey).Result() // 批量删除 Redis 缓存 if len(hashes) > 0 { keys := make([]string, 0, len(hashes)+1) for _, h := range hashes { keys = append(keys, refreshTokenKey(h)) } keys = append(keys, userKey) if err := r.rdb.Del(ctx, keys...).Err(); err != nil { logger.Log.Warnw("Redis cache batch delete failed for user refresh tokens", "error", err, "userID", userID) } } // 删 DB(无论 Redis 是否成功都执行) return r.inner.DeleteUserRefreshTokens(ctx, userID) }