perf(frontend,config): parallelize auth, virtualize logs, cache public configs

Frontend layout renders immediately; pages gate via RequireAuth and
useAuthRedirect. Login/register skip getUserInfo. Admin log panel uses
useVirtualizer to avoid 2000-row DOM. ListVisibleSystemConfigs caches
visibility=1 configs in Redis with invalidation on create/update.
PERFORMANCE.md marks completed optimizations.
This commit is contained in:
ryan
2026-06-17 11:41:24 +08:00
parent 6a8f9a7aea
commit ca21e0eca7
13 changed files with 427 additions and 165 deletions
+60 -26
View File
@@ -4,23 +4,25 @@
package system_config
import ("context"
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/Rain-kl/Wavelet/internal/apps/upload"
"github.com/Rain-kl/Wavelet/internal/common/response"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/storage"
"github.com/Rain-kl/Wavelet/pkg/logger"
mail "github.com/Rain-kl/Wavelet/pkg/mail"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/Rain-kl/Wavelet/internal/common/response")
)
const maskedConfigValue = "******"
@@ -95,6 +97,10 @@ func CreateSystemConfig(c *gin.Context) {
return
}
if err := model.InvalidateVisibleSystemConfigsCache(c.Request.Context()); err != nil {
logger.WarnF(c.Request.Context(), "清理公共配置列表缓存失败: %v", err)
}
c.JSON(http.StatusOK, response.OKNil())
}
@@ -231,23 +237,13 @@ func UpdateSystemConfig(c *gin.Context) {
return err
}
if key == model.ConfigKeyStorageConfig && originalDriver != "" {
var newCfg storage.Config
if err := json.Unmarshal([]byte(req.Value), &newCfg); err == nil {
if newCfg.Driver == originalDriver {
// Mark failed storage:migrate task execution as succeeded
if err := tx.Model(&model.TaskExecution{}).
Where("task_type = ? AND status = ?", "storage:migrate", model.TaskExecutionStatusFailed).
Updates(map[string]any{
"status": model.TaskExecutionStatusSucceeded,
"result": "存储配置直接更新,故障迁移任务自动标记为已解决",
"finished_at": time.Now(),
}).Error; err != nil {
logger.ErrorF(c.Request.Context(), "自动更新迁移任务状态失败: %v", err)
}
}
}
}
resolveStorageMigrationTasksOnDirectDriverUpdate(
c.Request.Context(),
tx,
key,
originalDriver,
req.Value,
)
return nil
}); err != nil {
@@ -255,18 +251,56 @@ func UpdateSystemConfig(c *gin.Context) {
return
}
invalidateCachesAfterConfigUpdate(c.Request.Context(), key)
c.JSON(http.StatusOK, response.OKNil())
}
func resolveStorageMigrationTasksOnDirectDriverUpdate(
ctx context.Context,
tx *gorm.DB,
key string,
originalDriver storage.Driver,
newValue string,
) {
if key != model.ConfigKeyStorageConfig || originalDriver == "" {
return
}
var newCfg storage.Config
if err := json.Unmarshal([]byte(newValue), &newCfg); err != nil {
return
}
if newCfg.Driver != originalDriver {
return
}
if err := tx.Model(&model.TaskExecution{}).
Where("task_type = ? AND status = ?", "storage:migrate", model.TaskExecutionStatusFailed).
Updates(map[string]any{
"status": model.TaskExecutionStatusSucceeded,
"result": "存储配置直接更新,故障迁移任务自动标记为已解决",
"finished_at": time.Now(),
}).Error; err != nil {
logger.ErrorF(ctx, "自动更新迁移任务状态失败: %v", err)
}
}
func invalidateCachesAfterConfigUpdate(ctx context.Context, key string) {
if key == model.ConfigKeyStorageConfig {
upload.ResetAccessCaches()
upload.PublishAccessCacheInvalidation(c.Request.Context())
upload.PublishAccessCacheInvalidation(ctx)
storage.ResetCache()
storage.PublishCacheInvalidation(c.Request.Context())
storage.PublishCacheInvalidation(ctx)
}
if key == model.ConfigKeyFileAccessWhitelist {
upload.ResetAccessCaches()
upload.PublishAccessCacheInvalidation(c.Request.Context())
upload.PublishAccessCacheInvalidation(ctx)
}
c.JSON(http.StatusOK, response.OKNil())
if err := model.InvalidateVisibleSystemConfigsCache(ctx); err != nil {
logger.WarnF(ctx, "清理公共配置列表缓存失败: %v", err)
}
}
// TestSMTPRequest 测试 SMTP 配置请求
@@ -0,0 +1,75 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package config
import (
"context"
"testing"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/testhelper"
)
func TestListVisibleSystemConfigsUsesRedisCache(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
defer cleanup()
ctx := context.Background()
if err := model.InvalidateVisibleSystemConfigsCache(ctx); err != nil {
t.Fatalf("InvalidateVisibleSystemConfigsCache() error = %v", err)
}
if _, err := model.ListVisibleSystemConfigs(ctx); err != nil {
t.Fatalf("ListVisibleSystemConfigs() warm cache error = %v", err)
}
if err := dbConn.Create(&model.SystemConfig{
Key: "cache_probe_public_key",
Value: "cache_probe_public_value",
Type: "system",
Visibility: model.ConfigVisibilityVisible,
Description: "cache probe",
}).Error; err != nil {
t.Fatalf("Create(cache_probe_public_key) error = %v", err)
}
cached, err := model.ListVisibleSystemConfigs(ctx)
if err != nil {
t.Fatalf("ListVisibleSystemConfigs() cached call error = %v", err)
}
for _, item := range cached {
if item.Key == "cache_probe_public_key" {
t.Fatal("cached visible config list should be stale before invalidation")
}
}
exists, err := db.Redis.Exists(ctx, db.PrefixedKey(model.SystemConfigVisibleListRedisKey)).Result()
if err != nil {
t.Fatalf("Redis.Exists() error = %v", err)
}
if exists == 0 {
t.Fatal("expected visible config list cache key to exist")
}
if err := model.InvalidateVisibleSystemConfigsCache(ctx); err != nil {
t.Fatalf("InvalidateVisibleSystemConfigsCache() error = %v", err)
}
refreshed, err := model.ListVisibleSystemConfigs(ctx)
if err != nil {
t.Fatalf("ListVisibleSystemConfigs() refreshed call error = %v", err)
}
var found bool
for _, item := range refreshed {
if item.Key == "cache_probe_public_key" {
found = true
break
}
}
if !found {
t.Fatal("refreshed visible config list should include newly created public config")
}
}
+24 -1
View File
@@ -54,6 +54,8 @@ const (
const (
// SystemConfigRedisHashKey Redis Hash key,存储所有系统配置
SystemConfigRedisHashKey = "system:system_configs"
// SystemConfigVisibleListRedisKey Redis key,缓存所有 visibility=1 的公共配置列表
SystemConfigVisibleListRedisKey = "system:visible_configs"
)
const (
@@ -108,8 +110,25 @@ func (sc *SystemConfig) GetByKey(ctx context.Context, key string) error {
return nil
}
// ListVisibleSystemConfigs 查询所有可通过公共配置接口暴露的配置
// InvalidateVisibleSystemConfigsCache clears the cached public config list.
func InvalidateVisibleSystemConfigsCache(ctx context.Context) error {
if db.Redis == nil {
return nil
}
return db.Redis.Del(ctx, db.PrefixedKey(SystemConfigVisibleListRedisKey)).Err()
}
// ListVisibleSystemConfigs 查询所有可通过公共配置接口暴露的配置(带 Redis 列表缓存)
func ListVisibleSystemConfigs(ctx context.Context) ([]SystemConfig, error) {
if db.Redis != nil {
var cached []SystemConfig
if err := db.GetJSON(ctx, SystemConfigVisibleListRedisKey, &cached); err == nil {
return cached, nil
} else if !errors.Is(err, redis.Nil) {
return nil, err
}
}
database := db.DB(ctx)
if database == nil {
return nil, errors.New(errDatabaseNotInitialized)
@@ -120,6 +139,10 @@ func ListVisibleSystemConfigs(ctx context.Context) ([]SystemConfig, error) {
return nil, err
}
if db.Redis != nil {
_ = db.SetJSON(ctx, SystemConfigVisibleListRedisKey, configs, 0)
}
return configs, nil
}