package data import ( "context" "errors" "fmt" "kra/internal/biz/system" configpkg "kra/internal/config" "kra/internal/integration/storage" "github.com/google/uuid" "gorm.io/gorm" ) func (d *Data) Config() *configpkg.Config { if d == nil || d.runtime == nil { return nil } return d.runtime.Snapshot() } func refreshDatabaseSources(value *configpkg.Data) error { if value == nil { return nil } if err := refreshDatabaseSource(value.Database); err != nil { return err } for _, database := range value.DatabaseList { if database == nil || database.Disable { continue } if err := refreshDatabaseSource(database); err != nil { return err } } return nil } func refreshDatabaseSource(database *configpkg.Database) error { if database == nil { return nil } hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != "" if !hasStructuredConfig { return nil } previousSource := database.Source database.Source = "" source, err := databaseDSN(database, "") if err != nil { database.Source = previousSource return err } database.Source = source return nil } func (d *Data) PersistConfig(context.Context) error { return d.persistConfig() } func (d *Data) PersistRuntimeConfig(ctx context.Context, value *configpkg.Config) error { current := d.Config() if current == nil { current = &configpkg.Config{} } next := configpkg.Clone(value) if next == nil { next = configpkg.Clone(current) } if next.Data == nil { next.Data = configpkg.CloneData(current.Data) } if next.Admin == nil { next.Admin = configpkg.CloneAdmin(current.Admin) } if next.Admin != nil && current.Admin != nil { if next.Admin.Storage == nil { next.Admin.Storage = configpkg.CloneStorage(current.Admin.Storage) } if next.Admin.Email == nil { next.Admin.Email = configpkg.CloneEmail(current.Admin.Email) } next.Admin.ConfigPath = current.Admin.ConfigPath } if next.Data == nil { next.Data = &configpkg.Data{} } if err := refreshDatabaseSources(next.Data); err != nil { return err } candidateStorage, err := storage.New(next.Admin) if err != nil { return err } if err := d.persistStorageIntegrationConfig(ctx, next.Admin.Storage); err != nil { return err } if err := d.persistEmailIntegrationConfig(ctx, next.Admin.Email); err != nil { return err } if err := d.persistConfigValues(next.Data, next.Admin); err != nil { return err } d.runtime.Replace(next) if d.storage != nil { d.storage.Replace(candidateStorage) } d.notifyResources() return nil } func (d *Data) ReloadConfig(ctx context.Context) error { return d.reloadConfig(ctx) } func (d *Data) IsInitialized(context.Context) (bool, error) { return d.databaseReady.Load(), nil } // InitializeDatabase opens and activates the configured database. The callback // is the application-level first-install hook; data owns only lifecycle and // schema migration, while initialize owns system seed orchestration. func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseConfig, seed func(context.Context, *gorm.DB) error) error { database := &configpkg.Database{} if current := d.runtime.Data(); current != nil && current.Database != nil { database = configpkg.CloneDatabase(current.Database) } database.Driver = input.Driver database.Host = input.Host database.Port = input.Port database.User = input.User database.Password = input.Password database.Name = input.Name database.Path = input.Path database.Config = input.Config database.Source = "" source, err := databaseDSN(database, "") if err != nil { return err } database.Source = source d.initMu.Lock() defer d.initMu.Unlock() initialized, err := d.IsInitialized(ctx) if err != nil { return err } if initialized { return errors.New("数据库已初始化,无需重复初始化") } candidate, err := openDatabase(database, true, input.Template, d.logger()) if err != nil { return err } activated := false defer func() { if !activated { if sqlDB, closeErr := candidate.DB(); closeErr == nil { _ = sqlDB.Close() } } }() db := candidate.WithContext(ctx) if err := migrateAll(db, d.catalog); err != nil { return err } if seed != nil { if err := seed(ctx, db); err != nil { return err } } currentAdmin := d.runtime.Admin() var legacyStorage *configpkg.Storage if currentAdmin != nil { legacyStorage = currentAdmin.Storage } storageConfig, err := resolveStorageIntegrationConfig(candidate.WithContext(ctx), legacyStorage) if err != nil { return fmt.Errorf("initialize storage integration configuration: %w", err) } var legacyEmail *configpkg.Email if currentAdmin != nil { legacyEmail = currentAdmin.Email } emailConfig, err := resolveEmailIntegrationConfig(candidate.WithContext(ctx), legacyEmail) if err != nil { return fmt.Errorf("initialize email integration configuration: %w", err) } signingKey := uuid.NewString() if err := d.persistDatabaseConfig(database, signingKey); err != nil { return fmt.Errorf("persist database configuration: %w", err) } integrationConfigs, err := readIntegrationRuntime(candidate) if err != nil { return fmt.Errorf("initialize integration runtime: %w", err) } d.activateDatabase(candidate, database) activated = true current := d.Config() if current == nil { current = &configpkg.Config{} } currentData, currentAdmin := current.Data, current.Admin if currentAdmin == nil { currentAdmin = &configpkg.Admin{} } if currentAdmin.JWT == nil { currentAdmin.JWT = &configpkg.JWT{} } currentAdmin.JWT.SigningKey = signingKey currentAdmin.Storage = storageConfig currentAdmin.Email = emailConfig d.runtime.Replace(&configpkg.Config{Server: current.Server, Data: currentData, Admin: currentAdmin}) if d.integrations != nil { d.integrations.Replace(integrationConfigs) } d.notifyResources() return nil }