优化结构

This commit is contained in:
Yvan
2026-08-27 16:20:37 +08:00
parent 001e39da50
commit 653414a1da
54 changed files with 1257 additions and 3450 deletions
+79 -106
View File
@@ -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
}