修复缓存
Build docker and publish / build (20.15.1) (push) Successful in 7m26s

This commit is contained in:
2026-03-06 21:58:29 -08:00
parent 19932bb9f7
commit 4d913c1728
175 changed files with 437 additions and 12104 deletions
-28
View File
@@ -1,28 +0,0 @@
package cache
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCacheOptions(t *testing.T) {
t.Run("default options", func(t *testing.T) {
o := newOptions()
assert.Equal(t, defaultExpiry, o.Expiry)
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
})
t.Run("with expiry", func(t *testing.T) {
o := newOptions(WithExpiry(time.Second))
assert.Equal(t, time.Second, o.Expiry)
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
})
t.Run("with not found expiry", func(t *testing.T) {
o := newOptions(WithNotFoundExpiry(time.Second))
assert.Equal(t, defaultExpiry, o.Expiry)
assert.Equal(t, time.Second, o.NotFoundExpiry)
})
}
+7
View File
@@ -109,6 +109,13 @@ func (cc CachedConn) QueryCtx(ctx context.Context, v interface{}, key string, qu
}
return cc.SetCache(key, v)
}
// Cache data corrupted (e.g. bad JSON), delete key and fall through to DB
_ = cc.DelCache(key)
err = query(cc.db.WithContext(ctx), v)
if err != nil {
return err
}
return cc.SetCache(key, v)
}
return
}
+168 -51
View File
@@ -2,65 +2,182 @@ package cache
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"github.com/perfect-panel/server/pkg/orm"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
)
type User struct {
Id int64 `gorm:"primarykey"`
Email string `gorm:"index:idx_email;type:varchar(100);unique;not null;comment:电子邮箱"`
Password string `gorm:"type:varchar(100);comment:用户密码;not null"`
Avatar string `gorm:"type:varchar(200);default:'';comment:用户头像"`
Balance int64 `gorm:"default:0;comment:用户余额"`
Telegram int64 `gorm:"default:null;comment:Telegram账号"`
ReferCode string `gorm:"type:varchar(20);default:'';comment:推荐码"`
RefererId int64 `gorm:"comment:推荐人ID"`
Enable bool `gorm:"default:true;not null;comment:账户是否可用"`
IsAdmin bool `gorm:"default:false;not null;comment:是否管理员"`
ValidEmail bool `gorm:"default:false;not null;comment:是否验证邮箱"`
EnableEmailNotify bool `gorm:"default:false;not null;comment:是否启用邮件通知"`
EnableTelegramNotify bool `gorm:"default:false;not null;comment:是否启用Telegram通知"`
EnableBalanceNotify bool `gorm:"default:false;not null;comment:是否启用余额变动通知"`
EnableLoginNotify bool `gorm:"default:false;not null;comment:是否启用登录通知"`
EnableSubscribeNotify bool `gorm:"default:false;not null;comment:是否启用订阅通知"`
EnableTradeNotify bool `gorm:"default:false;not null;comment:是否启用交易通知"`
CreatedAt time.Time `gorm:"<-:create;comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
DeletedAt gorm.DeletedAt `gorm:"default:null;comment:删除时间"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1:正常 0:删除"` // Use `1` `0` to identify
// testUser is a simple struct used across all QueryCtx tests.
type testUser struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func TestGormCacheCtx(t *testing.T) {
t.Skipf("skip TestGormCacheCtx test")
db, err := orm.ConnectMysql(orm.Mysql{
Config: orm.Config{
Addr: "localhost:3306",
Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
Dbname: "vpnboard",
Username: "root",
Password: "mylove520",
},
// setupCachedConn creates a CachedConn backed by a real miniredis instance
// and a bare *gorm.DB (no real database connection needed because the
// QueryCtxFn callback is fully under our control).
func setupCachedConn(t *testing.T) (CachedConn, *miniredis.Miniredis) {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
if err != nil {
t.Error(err)
}
rds := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
conn := NewConn(db, rds)
var u User
key := "user:id"
err = conn.QueryCtx(context.Background(), &u, key, func(conn *gorm.DB, v interface{}) error {
return conn.Where("id = ?", 1).First(v).Error
})
if err != nil {
t.Error(err)
return
}
t.Logf("get cache success %+v", u)
t.Cleanup(func() { rdb.Close() })
// Use SQLite in-memory to get a properly initialized *gorm.DB.
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
cc := NewConn(db, rdb, WithExpiry(time.Minute))
return cc, mr
}
func TestQueryCtx_CacheHit(t *testing.T) {
cc, mr := setupCachedConn(t)
ctx := context.Background()
key := "cache:user:1"
// Pre-populate the cache with valid JSON.
expected := testUser{ID: 1, Name: "Alice"}
data, err := json.Marshal(expected)
require.NoError(t, err)
mr.Set(key, string(data))
// Track whether the DB query function is called.
dbCalled := false
queryFn := func(conn *gorm.DB, v interface{}) error {
dbCalled = true
return nil
}
var result testUser
err = cc.QueryCtx(ctx, &result, key, queryFn)
assert.NoError(t, err)
assert.False(t, dbCalled, "DB query should NOT be called on cache hit")
assert.Equal(t, expected.ID, result.ID)
assert.Equal(t, expected.Name, result.Name)
}
func TestQueryCtx_CacheMiss_QueriesDB_SetsCache(t *testing.T) {
cc, mr := setupCachedConn(t)
ctx := context.Background()
key := "cache:user:2"
// Do NOT pre-populate the cache -- this is a cache miss scenario.
dbCalled := false
queryFn := func(conn *gorm.DB, v interface{}) error {
dbCalled = true
u := v.(*testUser)
u.ID = 2
u.Name = "Bob"
return nil
}
var result testUser
err := cc.QueryCtx(ctx, &result, key, queryFn)
assert.NoError(t, err)
assert.True(t, dbCalled, "DB query should be called on cache miss")
assert.Equal(t, int64(2), result.ID)
assert.Equal(t, "Bob", result.Name)
// Verify the value was written back to cache.
cached, cacheErr := mr.Get(key)
require.NoError(t, cacheErr)
var cachedUser testUser
require.NoError(t, json.Unmarshal([]byte(cached), &cachedUser))
assert.Equal(t, int64(2), cachedUser.ID)
assert.Equal(t, "Bob", cachedUser.Name)
}
func TestQueryCtx_CorruptedCache_SelfHeals(t *testing.T) {
cc, mr := setupCachedConn(t)
ctx := context.Background()
key := "cache:user:3"
// Store invalid JSON in the cache to simulate corruption.
mr.Set(key, "THIS IS NOT VALID JSON{{{")
dbCalled := false
queryFn := func(conn *gorm.DB, v interface{}) error {
dbCalled = true
u := v.(*testUser)
u.ID = 3
u.Name = "Charlie"
return nil
}
var result testUser
err := cc.QueryCtx(ctx, &result, key, queryFn)
assert.NoError(t, err)
assert.True(t, dbCalled, "DB query should be called when cache is corrupted")
assert.Equal(t, int64(3), result.ID)
assert.Equal(t, "Charlie", result.Name)
// Verify the corrupt key was replaced with valid data.
cached, cacheErr := mr.Get(key)
require.NoError(t, cacheErr)
var cachedUser testUser
require.NoError(t, json.Unmarshal([]byte(cached), &cachedUser))
assert.Equal(t, int64(3), cachedUser.ID)
assert.Equal(t, "Charlie", cachedUser.Name)
}
func TestQueryCtx_CacheMiss_DBFails_ReturnsError(t *testing.T) {
cc, mr := setupCachedConn(t)
ctx := context.Background()
key := "cache:user:4"
// No cache entry -- this is a miss.
dbErr := errors.New("connection refused")
queryFn := func(conn *gorm.DB, v interface{}) error {
return dbErr
}
var result testUser
err := cc.QueryCtx(ctx, &result, key, queryFn)
assert.Error(t, err)
assert.Equal(t, dbErr, err)
// Cache should remain empty -- no value was written.
assert.False(t, mr.Exists(key), "cache should NOT be set when DB query fails")
}
func TestQueryCtx_CorruptedCache_DBFails_ReturnsError(t *testing.T) {
cc, mr := setupCachedConn(t)
ctx := context.Background()
key := "cache:user:5"
// Store invalid JSON to trigger the corruption branch.
mr.Set(key, "<<<CORRUPT>>>")
dbErr := errors.New("database is down")
queryFn := func(conn *gorm.DB, v interface{}) error {
return dbErr
}
var result testUser
err := cc.QueryCtx(ctx, &result, key, queryFn)
assert.Error(t, err)
assert.Equal(t, dbErr, err)
// The corrupt key should have been deleted (DelCache was called),
// and no new value was set because the DB query failed.
assert.False(t, mr.Exists(key), "corrupt key should be deleted even when DB fails")
}