Merge remote-tracking branch 'origin/feature/redis-auth-integration' into dev
# Conflicts: # go.mod # go.sum # internal/container/container.go # internal/repository/interfaces.go # internal/service/mocks_test.go # internal/service/texture_service_test.go # internal/service/token_service_test.go # pkg/redis/manager.go
This commit is contained in:
@@ -14,12 +14,24 @@ type CacheConfig struct {
|
||||
Prefix string // 缓存键前缀
|
||||
Expiration time.Duration // 过期时间
|
||||
Enabled bool // 是否启用缓存
|
||||
Policy CachePolicy // 缓存策略(可选,不配置则回落到 Expiration)
|
||||
}
|
||||
|
||||
// CachePolicy 缓存策略,用于为不同实体设置默认 TTL
|
||||
type CachePolicy struct {
|
||||
UserTTL time.Duration
|
||||
UserEmailTTL time.Duration
|
||||
ProfileTTL time.Duration
|
||||
ProfileListTTL time.Duration
|
||||
TextureTTL time.Duration
|
||||
TextureListTTL time.Duration
|
||||
}
|
||||
|
||||
// CacheManager 缓存管理器
|
||||
type CacheManager struct {
|
||||
redis *redis.Client
|
||||
config CacheConfig
|
||||
Policy CachePolicy
|
||||
}
|
||||
|
||||
// NewCacheManager 创建缓存管理器
|
||||
@@ -31,9 +43,33 @@ func NewCacheManager(redisClient *redis.Client, config CacheConfig) *CacheManage
|
||||
config.Expiration = 5 * time.Minute
|
||||
}
|
||||
|
||||
// 填充默认策略(未配置时退回全局过期时间)
|
||||
applyPolicyDefaults := func(p *CachePolicy) {
|
||||
if p.UserTTL == 0 {
|
||||
p.UserTTL = config.Expiration
|
||||
}
|
||||
if p.UserEmailTTL == 0 {
|
||||
p.UserEmailTTL = config.Expiration
|
||||
}
|
||||
if p.ProfileTTL == 0 {
|
||||
p.ProfileTTL = config.Expiration
|
||||
}
|
||||
if p.ProfileListTTL == 0 {
|
||||
p.ProfileListTTL = config.Expiration
|
||||
}
|
||||
if p.TextureTTL == 0 {
|
||||
p.TextureTTL = config.Expiration
|
||||
}
|
||||
if p.TextureListTTL == 0 {
|
||||
p.TextureListTTL = config.Expiration
|
||||
}
|
||||
}
|
||||
applyPolicyDefaults(&config.Policy)
|
||||
|
||||
return &CacheManager{
|
||||
redis: redisClient,
|
||||
config: config,
|
||||
Policy: config.Policy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +92,14 @@ func (cm *CacheManager) Get(ctx context.Context, key string, dest interface{}) e
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
// TryGet 获取缓存,命中时返回 true,不视为错误
|
||||
func (cm *CacheManager) TryGet(ctx context.Context, key string, dest interface{}) (bool, error) {
|
||||
if err := cm.Get(ctx, key, dest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Set 设置缓存
|
||||
func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}, expiration ...time.Duration) error {
|
||||
if !cm.config.Enabled || cm.redis == nil {
|
||||
@@ -75,6 +119,13 @@ func (cm *CacheManager) Set(ctx context.Context, key string, value interface{},
|
||||
return cm.redis.Set(ctx, cm.buildKey(key), data, exp)
|
||||
}
|
||||
|
||||
// SetAsync 异步设置缓存,避免在主请求链路阻塞
|
||||
func (cm *CacheManager) SetAsync(ctx context.Context, key string, value interface{}, expiration ...time.Duration) {
|
||||
go func() {
|
||||
_ = cm.Set(ctx, key, value, expiration...)
|
||||
}()
|
||||
}
|
||||
|
||||
// Delete 删除缓存
|
||||
func (cm *CacheManager) Delete(ctx context.Context, keys ...string) error {
|
||||
if !cm.config.Enabled || cm.redis == nil {
|
||||
@@ -187,11 +238,7 @@ func Cached[T any](
|
||||
}
|
||||
|
||||
// 设置缓存(异步,不阻塞)
|
||||
go func() {
|
||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = cache.Set(cacheCtx, key, data, expiration...)
|
||||
}()
|
||||
cache.SetAsync(context.Background(), key, data, expiration...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -217,11 +264,7 @@ func CachedList[T any](
|
||||
}
|
||||
|
||||
// 设置缓存(异步,不阻塞)
|
||||
go func() {
|
||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = cache.Set(cacheCtx, key, data, expiration...)
|
||||
}()
|
||||
cache.SetAsync(context.Background(), key, data, expiration...)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -306,6 +349,11 @@ func (b *CacheKeyBuilder) TextureList(userID int64, page int) string {
|
||||
return fmt.Sprintf("%stexture:user:%d:page:%d", b.prefix, userID, page)
|
||||
}
|
||||
|
||||
// TextureListPattern 构建材质列表缓存键模式(用于批量失效)
|
||||
func (b *CacheKeyBuilder) TextureListPattern(userID int64) string {
|
||||
return fmt.Sprintf("%stexture:user:%d:*", b.prefix, userID)
|
||||
}
|
||||
|
||||
// Token 构建令牌缓存键
|
||||
func (b *CacheKeyBuilder) Token(accessToken string) string {
|
||||
return fmt.Sprintf("%stoken:%s", b.prefix, accessToken)
|
||||
|
||||
184
pkg/database/cache_test.go
Normal file
184
pkg/database/cache_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pkgRedis "carrotskin/pkg/redis"
|
||||
|
||||
miniredis "github.com/alicebob/miniredis/v2"
|
||||
goRedis "github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func newCacheWithMiniRedis(t *testing.T) (*CacheManager, func()) {
|
||||
t.Helper()
|
||||
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start miniredis: %v", err)
|
||||
}
|
||||
|
||||
rdb := goRedis.NewClient(&goRedis.Options{
|
||||
Addr: mr.Addr(),
|
||||
})
|
||||
client := &pkgRedis.Client{Client: rdb}
|
||||
|
||||
cache := NewCacheManager(client, CacheConfig{
|
||||
Prefix: "t:",
|
||||
Expiration: time.Minute,
|
||||
Enabled: true,
|
||||
Policy: CachePolicy{
|
||||
UserTTL: 2 * time.Minute,
|
||||
UserEmailTTL: 3 * time.Minute,
|
||||
ProfileTTL: 2 * time.Minute,
|
||||
ProfileListTTL: 90 * time.Second,
|
||||
TextureTTL: 2 * time.Minute,
|
||||
TextureListTTL: 45 * time.Second,
|
||||
},
|
||||
})
|
||||
|
||||
cleanup := func() {
|
||||
_ = rdb.Close()
|
||||
mr.Close()
|
||||
}
|
||||
return cache, cleanup
|
||||
}
|
||||
|
||||
func TestCacheManager_GetSet_TryGet(t *testing.T) {
|
||||
cache, cleanup := newCacheWithMiniRedis(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
u := User{ID: 1, Name: "alice"}
|
||||
if err := cache.Set(ctx, "user:1", u, 10*time.Second); err != nil {
|
||||
t.Fatalf("Set err: %v", err)
|
||||
}
|
||||
|
||||
var got User
|
||||
if err := cache.Get(ctx, "user:1", &got); err != nil {
|
||||
t.Fatalf("Get err: %v", err)
|
||||
}
|
||||
if got != u {
|
||||
t.Fatalf("unexpected value: %+v", got)
|
||||
}
|
||||
|
||||
var got2 User
|
||||
ok, err := cache.TryGet(ctx, "user:1", &got2)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("TryGet failed, ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got2 != u {
|
||||
t.Fatalf("unexpected TryGet: %+v", got2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheManager_DeletePattern(t *testing.T) {
|
||||
cache, cleanup := newCacheWithMiniRedis(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
_ = cache.Set(ctx, "user:1", "a", 0)
|
||||
_ = cache.Set(ctx, "user:2", "b", 0)
|
||||
_ = cache.Set(ctx, "profile:1", "c", 0)
|
||||
|
||||
// 删除 user:* 键
|
||||
if err := cache.DeletePattern(ctx, "user:*"); err != nil {
|
||||
t.Fatalf("DeletePattern err: %v", err)
|
||||
}
|
||||
|
||||
var v string
|
||||
ok, _ := cache.TryGet(ctx, "user:1", &v)
|
||||
if ok {
|
||||
t.Fatalf("expected user:1 deleted")
|
||||
}
|
||||
ok, _ = cache.TryGet(ctx, "user:2", &v)
|
||||
if ok {
|
||||
t.Fatalf("expected user:2 deleted")
|
||||
}
|
||||
ok, _ = cache.TryGet(ctx, "profile:1", &v)
|
||||
if !ok {
|
||||
t.Fatalf("expected profile:1 kept")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedAndCachedList(t *testing.T) {
|
||||
cache, cleanup := newCacheWithMiniRedis(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
callCount := 0
|
||||
result, err := Cached(ctx, cache, "key1", func() (*string, error) {
|
||||
callCount++
|
||||
val := "hello"
|
||||
return &val, nil
|
||||
}, cache.Policy.UserTTL)
|
||||
if err != nil || *result != "hello" || callCount != 1 {
|
||||
t.Fatalf("Cached first call failed")
|
||||
}
|
||||
// 等待缓存写入完成
|
||||
for i := 0; i < 10; i++ {
|
||||
var tmp string
|
||||
if ok, _ := cache.TryGet(ctx, "key1", &tmp); ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 第二次应命中缓存
|
||||
_, err = Cached(ctx, cache, "key1", func() (*string, error) {
|
||||
callCount++
|
||||
val := "world"
|
||||
return &val, nil
|
||||
}, cache.Policy.UserTTL)
|
||||
if err != nil || callCount != 1 {
|
||||
t.Fatalf("Cached should hit cache, callCount=%d err=%v", callCount, err)
|
||||
}
|
||||
|
||||
listCall := 0
|
||||
_, err = CachedList(ctx, cache, "list", func() ([]string, error) {
|
||||
listCall++
|
||||
return []string{"a", "b"}, nil
|
||||
}, cache.Policy.ProfileListTTL)
|
||||
if err != nil || listCall != 1 {
|
||||
t.Fatalf("CachedList first call failed")
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
var tmp []string
|
||||
if ok, _ := cache.TryGet(ctx, "list", &tmp); ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
_, err = CachedList(ctx, cache, "list", func() ([]string, error) {
|
||||
listCall++
|
||||
return []string{"c"}, nil
|
||||
}, cache.Policy.ProfileListTTL)
|
||||
if err != nil || listCall != 1 {
|
||||
t.Fatalf("CachedList should hit cache, calls=%d err=%v", listCall, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementWithExpire(t *testing.T) {
|
||||
cache, cleanup := newCacheWithMiniRedis(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
val, err := cache.IncrementWithExpire(ctx, "counter", time.Second)
|
||||
if err != nil || val != 1 {
|
||||
t.Fatalf("first increment failed, val=%d err=%v", val, err)
|
||||
}
|
||||
val, err = cache.IncrementWithExpire(ctx, "counter", time.Second)
|
||||
if err != nil || val != 2 {
|
||||
t.Fatalf("second increment failed, val=%d err=%v", val, err)
|
||||
}
|
||||
ttl, err := cache.TTL(ctx, "counter")
|
||||
if err != nil || ttl <= 0 {
|
||||
t.Fatalf("TTL not set: ttl=%v err=%v", ttl, err)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,6 @@ func AutoMigrate(logger *zap.Logger) error {
|
||||
&model.TextureDownloadLog{},
|
||||
|
||||
// 认证相关表
|
||||
&model.Token{},
|
||||
&model.Client{}, // Client表用于管理Token版本
|
||||
|
||||
// Yggdrasil相关表(在User之后创建,因为它引用User)
|
||||
|
||||
24
pkg/database/manager_sqlite_test.go
Normal file
24
pkg/database/manager_sqlite_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 使用内存 sqlite 验证 AutoMigrate 关键路径,无需真实 Postgres
|
||||
func TestAutoMigrate_WithSQLite(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite err: %v", err)
|
||||
}
|
||||
dbInstance = db
|
||||
defer func() { dbInstance = nil }()
|
||||
|
||||
logger := zaptest.NewLogger(t)
|
||||
if err := AutoMigrate(logger); err != nil {
|
||||
t.Fatalf("AutoMigrate sqlite err: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
|
||||
// TestGetDB_NotInitialized 测试未初始化时获取数据库实例
|
||||
func TestGetDB_NotInitialized(t *testing.T) {
|
||||
dbInstance = nil
|
||||
_, err := GetDB()
|
||||
if err == nil {
|
||||
t.Error("未初始化时应该返回错误")
|
||||
}
|
||||
|
||||
|
||||
expectedError := "数据库未初始化,请先调用 database.Init()"
|
||||
if err.Error() != expectedError {
|
||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
||||
@@ -22,17 +23,19 @@ func TestGetDB_NotInitialized(t *testing.T) {
|
||||
|
||||
// TestMustGetDB_Panic 测试MustGetDB在未初始化时panic
|
||||
func TestMustGetDB_Panic(t *testing.T) {
|
||||
dbInstance = nil
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("MustGetDB 应该在未初始化时panic")
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
_ = MustGetDB()
|
||||
}
|
||||
|
||||
// TestInit_Database 测试数据库初始化逻辑
|
||||
func TestInit_Database(t *testing.T) {
|
||||
dbInstance = nil
|
||||
cfg := config.DatabaseConfig{
|
||||
Driver: "postgres",
|
||||
Host: "localhost",
|
||||
@@ -46,21 +49,21 @@ func TestInit_Database(t *testing.T) {
|
||||
MaxOpenConns: 100,
|
||||
ConnMaxLifetime: 0,
|
||||
}
|
||||
|
||||
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
|
||||
// 验证Init函数存在且可调用
|
||||
// 注意:实际连接可能失败,这是可以接受的
|
||||
err := Init(cfg, logger)
|
||||
if err != nil {
|
||||
t.Logf("Init() 返回错误(可能正常,如果数据库未运行): %v", err)
|
||||
t.Skipf("数据库未运行,跳过连接测试: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoMigrate_ErrorHandling 测试AutoMigrate的错误处理逻辑
|
||||
func TestAutoMigrate_ErrorHandling(t *testing.T) {
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
|
||||
// 测试未初始化时的错误处理
|
||||
err := AutoMigrate(logger)
|
||||
if err == nil {
|
||||
@@ -82,4 +85,3 @@ func TestClose_NotInitialized(t *testing.T) {
|
||||
t.Errorf("Close() 在未初始化时应该返回nil,实际返回: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user