优化结构
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/system"
|
||||
"kra/internal/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Backend is the infrastructure boundary required by application
|
||||
// initialization and runtime configuration management.
|
||||
type Backend interface {
|
||||
IsInitialized(context.Context) (bool, error)
|
||||
InitializeDatabase(context.Context, *system.DatabaseConfig, func(context.Context, *gorm.DB) error) error
|
||||
PersistConfig(context.Context) error
|
||||
PersistRuntimeConfig(context.Context, *config.Config) error
|
||||
ReloadConfig(context.Context) error
|
||||
Config() *config.Config
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"kra/internal/config"
|
||||
)
|
||||
@@ -16,7 +18,7 @@ type configurationEnvelope struct {
|
||||
}
|
||||
|
||||
func (r *Repo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
current := r.backend.Config()
|
||||
current := r.Config()
|
||||
if current == nil {
|
||||
current = &config.Config{}
|
||||
}
|
||||
@@ -26,7 +28,7 @@ func (r *Repo) ConfigurationJSON() (json.RawMessage, error) {
|
||||
}
|
||||
|
||||
func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
|
||||
current := r.backend.Config()
|
||||
current := r.Config()
|
||||
if current == nil {
|
||||
current = &config.Config{}
|
||||
}
|
||||
@@ -56,14 +58,13 @@ func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) e
|
||||
if next.Admin != nil && current.Admin != nil {
|
||||
next.Admin.ConfigPath = current.Admin.ConfigPath
|
||||
}
|
||||
if err := refreshDatabaseSources(next.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
// The data backend rebuilds every DSN from the structured fields while
|
||||
// preserving a standalone Source, so no pre-clearing is needed here.
|
||||
return r.PersistRuntimeConfig(ctx, next)
|
||||
}
|
||||
|
||||
func (r *Repo) DiskMountPoints() []string {
|
||||
current := r.backend.Config()
|
||||
current := r.Config()
|
||||
if current == nil || current.Admin == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -85,74 +86,45 @@ func mergeJSON(raw json.RawMessage, target any) error {
|
||||
|
||||
func mergeDataJSON(raw json.RawMessage, target **config.Data) error {
|
||||
return mergeJSONMap(raw, target, func(values map[string]any) error {
|
||||
if redis := jsonObject(values["redis"]); redis != nil {
|
||||
if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeRedisDurations(jsonObject(values["redis"])); err != nil {
|
||||
return err
|
||||
}
|
||||
if items, ok := values["redis_list"].([]any); ok {
|
||||
for _, item := range items {
|
||||
redis := jsonObject(item)
|
||||
if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
items, _ := values["redis_list"].([]any)
|
||||
for _, item := range items {
|
||||
if err := normalizeRedisDurations(jsonObject(item)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeRedisDurations(values map[string]any) error {
|
||||
if err := normalizeDuration(values, "read_timeout"); err != nil {
|
||||
return err
|
||||
}
|
||||
return normalizeDuration(values, "write_timeout")
|
||||
}
|
||||
|
||||
// The admin page posts the camelCase shape managementConfig produced, so only
|
||||
// the duration strings still need converting once mergeJSONMap has restored the
|
||||
// snake_case key names the configuration structs declare.
|
||||
func mergeAdminJSON(raw json.RawMessage, target **config.Admin) error {
|
||||
return mergeJSONMap(raw, target, func(values map[string]any) error {
|
||||
moveJSONKey(values, "routerPrefix", "router_prefix")
|
||||
if jwt := jsonObject(values["jwt"]); jwt != nil {
|
||||
moveJSONKey(jwt, "signingKey", "signing_key")
|
||||
moveJSONKey(jwt, "expiresTime", "expires_time")
|
||||
moveJSONKey(jwt, "bufferTime", "buffer_time")
|
||||
if err := normalizeDuration(jwt, "expires_time", "expiresTime"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := normalizeDuration(jwt, "buffer_time", "bufferTime"); err != nil {
|
||||
return err
|
||||
}
|
||||
jwt := jsonObject(values["jwt"])
|
||||
if err := normalizeDuration(jwt, "expires_time"); err != nil {
|
||||
return err
|
||||
}
|
||||
if captcha := jsonObject(values["captcha"]); captcha != nil {
|
||||
moveJSONKey(captcha, "keyLong", "key_long")
|
||||
moveJSONKey(captcha, "imgWidth", "img_width")
|
||||
moveJSONKey(captcha, "imgHeight", "img_height")
|
||||
moveJSONKey(captcha, "storeExpiration", "store_expiration")
|
||||
return normalizeDuration(captcha, "store_expiration", "storeExpiration")
|
||||
if err := normalizeDuration(jwt, "buffer_time"); err != nil {
|
||||
return err
|
||||
}
|
||||
if local := jsonObject(values["local"]); local != nil {
|
||||
moveJSONKey(local, "storePath", "store_path")
|
||||
moveJSONKey(local, "pathPrefix", "path_prefix")
|
||||
}
|
||||
if media := jsonObject(values["media"]); media != nil {
|
||||
moveJSONKey(media, "sessionTtl", "session_ttl")
|
||||
moveJSONKey(media, "maxFileSize", "max_file_size")
|
||||
moveJSONKey(media, "chunkDir", "chunk_dir")
|
||||
}
|
||||
if system := jsonObject(values["system"]); system != nil {
|
||||
moveJSONKey(system, "useRedis", "use_redis")
|
||||
moveJSONKey(system, "useMultipoint", "use_multipoint")
|
||||
moveJSONKey(system, "useStrictAuth", "use_strict_auth")
|
||||
moveJSONKey(system, "disableAutoMigrate", "disable_auto_migrate")
|
||||
moveJSONKey(system, "useMongo", "use_mongo")
|
||||
moveJSONKey(system, "iplimitCount", "iplimit_count")
|
||||
moveJSONKey(system, "iplimitTime", "iplimit_time")
|
||||
}
|
||||
return nil
|
||||
return normalizeDuration(jsonObject(values["captcha"]), "store_expiration")
|
||||
})
|
||||
}
|
||||
|
||||
func mergeEmailJSON(raw json.RawMessage, target **config.Email) error {
|
||||
return mergeJSONMap(raw, target, func(values map[string]any) error {
|
||||
// Two legacy dashed keys the generic snake_case pass cannot derive.
|
||||
moveJSONKey(values, "is-ssl", "is_ssl")
|
||||
moveJSONKey(values, "is-loginauth", "is_login_auth")
|
||||
return nil
|
||||
@@ -167,6 +139,7 @@ func mergeJSONMap(raw json.RawMessage, target any, transform func(map[string]any
|
||||
if err := json.Unmarshal(raw, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
snakeCaseKeys(values)
|
||||
if transform != nil {
|
||||
if err := transform(values); err != nil {
|
||||
return err
|
||||
@@ -179,31 +152,62 @@ func mergeJSONMap(raw json.RawMessage, target any, transform func(map[string]any
|
||||
return mergeJSON(normalized, target)
|
||||
}
|
||||
|
||||
// snakeCaseKeys rewrites every camelCase key in the decoded payload to the
|
||||
// snake_case name the configuration structs declare. One recursive pass replaces
|
||||
// a hand-written rename list that had to grow with every new setting — and that
|
||||
// silently dropped whole sections when one of them was missed.
|
||||
func snakeCaseKeys(value any) {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
renamed := map[string]any{}
|
||||
for key, item := range typed {
|
||||
snakeCaseKeys(item)
|
||||
if snake := snakeCase(key); snake != key {
|
||||
renamed[snake] = item
|
||||
delete(typed, key)
|
||||
}
|
||||
}
|
||||
for key, item := range renamed {
|
||||
typed[key] = item
|
||||
}
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
snakeCaseKeys(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func snakeCase(key string) string {
|
||||
var builder strings.Builder
|
||||
for index, symbol := range key {
|
||||
if !unicode.IsUpper(symbol) {
|
||||
builder.WriteRune(symbol)
|
||||
continue
|
||||
}
|
||||
if index > 0 {
|
||||
builder.WriteByte('_')
|
||||
}
|
||||
builder.WriteRune(unicode.ToLower(symbol))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func jsonObject(value any) map[string]any {
|
||||
result, _ := value.(map[string]any)
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeDuration(values map[string]any, keys ...string) error {
|
||||
if values == nil {
|
||||
// A nil map reads as absent, so callers can pass jsonObject(...) straight in.
|
||||
func normalizeDuration(values map[string]any, key string) error {
|
||||
text, ok := values[key].(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
raw, ok := values[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
text, ok := raw.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
value, err := time.ParseDuration(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration %q: %w", text, err)
|
||||
}
|
||||
values[key] = int64(value)
|
||||
return nil
|
||||
value, err := time.ParseDuration(text)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration %q: %w", text, err)
|
||||
}
|
||||
values[key] = int64(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -458,34 +462,3 @@ func preserveStorageSecrets(next, current *config.Storage) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshDatabaseSources(value *config.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 *config.Database) error {
|
||||
if database == nil {
|
||||
return nil
|
||||
}
|
||||
hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != ""
|
||||
if hasStructuredConfig {
|
||||
// Driver-specific DSN construction remains in data. Clearing Source
|
||||
// makes the data backend rebuild it from structured values, while a
|
||||
// standalone DSN is preserved when no structured fields are supplied.
|
||||
database.Source = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestConfigurationJSONMasksSecretsWithoutMutatingConfig(t *testing.T) {
|
||||
Data: &config.Data{Database: &config.Database{Password: "database-secret", Source: "dsn"}, Redis: &config.Redis{Password: "redis-secret"}},
|
||||
Admin: &config.Admin{JWT: &config.JWT{SigningKey: "jwt-secret", ExpiresTime: 24 * time.Hour}, Email: &config.Email{Secret: "email-secret"}, Storage: &config.Storage{Qiniu: &config.Qiniu{SecretKey: "storage-secret"}}},
|
||||
}}
|
||||
raw, err := (&Repo{backend: backend}).ConfigurationJSON()
|
||||
raw, err := (&Repo{Backend: backend}).ConfigurationJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func TestSaveConfigurationJSONMergesPartialValuesAndPreservesSecrets(t *testing.
|
||||
Admin: &config.Admin{ConfigPath: "config.yaml", JWT: &config.JWT{SigningKey: "jwt-secret", Issuer: "old", ExpiresTime: time.Hour}, Captcha: &config.Captcha{StoreExpiration: time.Minute}, Email: &config.Email{Host: "old.smtp", Secret: "email-secret", IsSSL: true}},
|
||||
}}
|
||||
raw := json.RawMessage(`{"data":{"database":{"host":"new","password":"******"},"redis":{"read_timeout":"250ms"}},"admin":{"jwt":{"issuer":"new","signingKey":"******","expiresTime":"48h"},"captcha":{"storeExpiration":"5m"}},"email":{"host":"new.smtp","secret":"******","is-ssl":false}}`)
|
||||
if err := (&Repo{backend: backend}).SaveConfigurationJSON(context.Background(), raw); err != nil {
|
||||
if err := (&Repo{Backend: backend}).SaveConfigurationJSON(context.Background(), raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := backend.persisted
|
||||
@@ -81,7 +81,7 @@ func TestSaveConfigurationJSONPreservesStandaloneDSN(t *testing.T) {
|
||||
Data: &config.Data{Database: &config.Database{Driver: "mysql", Source: "user:secret@tcp(database.example:3306)/kra"}},
|
||||
Admin: &config.Admin{},
|
||||
}}
|
||||
if err := (&Repo{backend: backend}).SaveConfigurationJSON(context.Background(), json.RawMessage(`{"admin":{"routerPrefix":"/api"}}`)); err != nil {
|
||||
if err := (&Repo{Backend: backend}).SaveConfigurationJSON(context.Background(), json.RawMessage(`{"admin":{"routerPrefix":"/api"}}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := backend.persisted.Data.Database.Source; got != "user:secret@tcp(database.example:3306)/kra" {
|
||||
|
||||
@@ -12,36 +12,36 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Backend is the infrastructure boundary required by application
|
||||
// initialization and runtime configuration management.
|
||||
type Backend interface {
|
||||
IsInitialized(context.Context) (bool, error)
|
||||
InitializeDatabase(context.Context, *system.DatabaseConfig, func(context.Context, *gorm.DB) error) error
|
||||
PersistConfig(context.Context) error
|
||||
PersistRuntimeConfig(context.Context, *config.Config) error
|
||||
ReloadConfig(context.Context) error
|
||||
Config() *config.Config
|
||||
}
|
||||
|
||||
// Repo adapts Backend to system.InitializationRepo. Backend is embedded so the
|
||||
// four identically shaped methods are promoted instead of hand-forwarded; this
|
||||
// file adds only what the two boundaries genuinely disagree on.
|
||||
type Repo struct {
|
||||
backend Backend
|
||||
Backend
|
||||
catalog platformmodule.Catalog
|
||||
}
|
||||
|
||||
func NewRepo(backend Backend, catalog platformmodule.Catalog) system.InitializationRepo {
|
||||
return &Repo{backend: backend, catalog: catalog}
|
||||
}
|
||||
|
||||
func (r *Repo) IsInitialized(ctx context.Context) (bool, error) {
|
||||
return r.backend.IsInitialized(ctx)
|
||||
return &Repo{Backend: backend, catalog: catalog}
|
||||
}
|
||||
|
||||
// Initialize layers system and task seeding on top of the backend's database
|
||||
// lifecycle, which is the one place the two interfaces differ in shape.
|
||||
func (r *Repo) Initialize(ctx context.Context, input *system.DatabaseConfig) error {
|
||||
return r.backend.InitializeDatabase(ctx, input, func(ctx context.Context, db *gorm.DB) error {
|
||||
return r.InitializeDatabase(ctx, input, func(ctx context.Context, db *gorm.DB) error {
|
||||
if err := datasystem.SeedSystemWithCatalog(ctx, db, input, r.catalog); err != nil {
|
||||
return err
|
||||
}
|
||||
return datatask.SeedDefaults(ctx, db, r.catalog.DefaultTimedTasks())
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repo) PersistConfig(ctx context.Context) error {
|
||||
return r.backend.PersistConfig(ctx)
|
||||
}
|
||||
|
||||
func (r *Repo) PersistRuntimeConfig(ctx context.Context, value *config.Config) error {
|
||||
return r.backend.PersistRuntimeConfig(ctx, value)
|
||||
}
|
||||
|
||||
func (r *Repo) ReloadConfig(ctx context.Context) error {
|
||||
return r.backend.ReloadConfig(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user