From d83720c8fd4e2969e58e051fdc121224f64288fb Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Fri, 4 Sep 2026 16:51:03 +0800 Subject: [PATCH] jenkins --- internal/biz/system/security.go | 32 ++++++++++++++++++ internal/biz/system/security_test.go | 49 ++++++++++++++++++++++++++++ internal/data/system/security.go | 4 +-- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/internal/biz/system/security.go b/internal/biz/system/security.go index 36f4aea..431b1f0 100644 --- a/internal/biz/system/security.go +++ b/internal/biz/system/security.go @@ -37,6 +37,29 @@ type SecurityConfig struct { ForceNewUserChangePassword bool } +// ErrDatabaseNotInitialized is returned by storage-backed settings that are +// intentionally unavailable during the first-install bootstrap phase. +var ErrDatabaseNotInitialized = errors.New("database not initialized") + +// DefaultSecurityConfig matches the seed values used for a fresh installation. +// It is owned by biz so bootstrap callers do not need to depend on data. +func DefaultSecurityConfig() *SecurityConfig { + return &SecurityConfig{ + ID: 1, + CaptchaTimeout: 3600, + KeyLong: 6, + ImgWidth: 240, + ImgHeight: 80, + PwdMinLength: 8, + LimitWindow: 60, + LimitCount: 30, + LockThreshold: 5, + LockDuration: 30, + PwdExpireDays: 90, + ForceNewUserChangePassword: false, + } +} + type SecurityRepo interface { SecurityConfig(context.Context) (*SecurityConfig, error) SaveSecurityConfig(context.Context, *SecurityConfig) error @@ -99,8 +122,17 @@ func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error) uc.mu.RUnlock() value, err := uc.repo.SecurityConfig(ctx) if err != nil { + // The login page and captcha are also the entry point for first install. + // Do not cache this fallback: after initdb succeeds the next request must + // load the persisted row instead of serving bootstrap defaults forever. + if errors.Is(err, ErrDatabaseNotInitialized) { + return DefaultSecurityConfig(), nil + } return nil, err } + if value == nil { + return nil, errors.New("security config is nil") + } uc.mu.Lock() copy := *value uc.cachedConfig = © diff --git a/internal/biz/system/security_test.go b/internal/biz/system/security_test.go index b22a797..540e14f 100644 --- a/internal/biz/system/security_test.go +++ b/internal/biz/system/security_test.go @@ -15,6 +15,27 @@ type securityUpdateRepo struct { persistedCopy *SecurityConfig } +type bootstrapSecurityRepo struct { + initialized bool + calls int +} + +func (r *bootstrapSecurityRepo) SecurityConfig(context.Context) (*SecurityConfig, error) { + r.calls++ + if !r.initialized { + return nil, ErrDatabaseNotInitialized + } + return &SecurityConfig{ID: 1, LimitCount: 42}, nil +} + +func (*bootstrapSecurityRepo) SaveSecurityConfig(context.Context, *SecurityConfig) error { + return nil +} + +func (*bootstrapSecurityRepo) BackfillPasswordUpdatedAt(context.Context, time.Time) error { + return nil +} + func (r *securityUpdateRepo) SecurityConfig(context.Context) (*SecurityConfig, error) { r.callOrder = append(r.callOrder, "get") copy := *r.current @@ -110,3 +131,31 @@ func TestSecurityReadsPersistedValueWhileCurrentUsesCache(t *testing.T) { t.Fatalf("Security() returned stale value: %d", fresh.LimitCount) } } + +func TestCurrentUsesBootstrapDefaultsWithoutCachingThem(t *testing.T) { + repo := &bootstrapSecurityRepo{} + uc := NewSecurityUsecase(repo, nil, nil, nil) + + bootstrap, err := uc.Current(context.Background()) + if err != nil { + t.Fatalf("Current() during bootstrap error = %v", err) + } + if bootstrap.ID != 1 || bootstrap.KeyLong != 6 || bootstrap.ImgWidth != 240 || bootstrap.ImgHeight != 80 || bootstrap.LimitEnable { + t.Fatalf("bootstrap config = %+v", bootstrap) + } + if repo.calls != 1 { + t.Fatalf("bootstrap repository calls = %d, want 1", repo.calls) + } + + repo.initialized = true + active, err := uc.Current(context.Background()) + if err != nil { + t.Fatalf("Current() after initialization error = %v", err) + } + if active.LimitCount != 42 { + t.Fatalf("active config = %+v, want persisted config", active) + } + if repo.calls != 2 { + t.Fatalf("repository calls = %d, want fallback to be uncached", repo.calls) + } +} diff --git a/internal/data/system/security.go b/internal/data/system/security.go index 61321f1..ca64702 100644 --- a/internal/data/system/security.go +++ b/internal/data/system/security.go @@ -43,7 +43,7 @@ type SecurityConfigPO struct { func (SecurityConfigPO) TableName() string { return "sys_security_config" } func DefaultSecurityConfig() SecurityConfigPO { - return SecurityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90} + return securityToPO(system.DefaultSecurityConfig()) } type securityRepo struct{ data DatabaseProvider } @@ -60,7 +60,7 @@ func securityToPO(v *system.SecurityConfig) SecurityConfigPO { func (r *securityRepo) SecurityConfig(ctx context.Context) (*system.SecurityConfig, error) { if !r.data.DatabaseReady() { - return nil, errors.New("数据库未初始化") + return nil, system.ErrDatabaseNotInitialized } db := r.data.DB().WithContext(ctx) var po SecurityConfigPO