package gateway import ( "reflect" "strconv" "strings" ) // 本文件从 `internal/config` 的 env 标签里读出各键的**声明默认值**,供只读配置 // 事实面(admin_config_facts.go)如实展示「默认 / 已改」。 // // 为什么用标签而不是另一份默认值清单:`default:"…"` 标签本身就是配置层的唯一 // 真源(`config.lookupRaw` 在进程环境与 `.env` 都没给值时正是用它)。网关再抄 // 一份等于制造第二份事实,两边迟早对不上;而反射读标签既不需要 fopen `.env` // (主人红线:键名单读 `.env.example`,**绝不读 `.env`**),也不需要新增配置包 // 导出面。 // envTagName 与 config 包的内部约定一致:env 标签可带逗号分隔的选项, // 变量名是逗号前的部分。 func envTagName(tag string) string { name, _, _ := strings.Cut(tag, ",") return strings.TrimSpace(name) } // declaredEnvDefaults 遍历配置结构体,返回 `env:"KEY"` 字段的声明默认值 // (`default:"…"` 标签),按字段类型归一化成字符串,键与取值口径与 // `buildConfigFacts` 对齐: // // - bool → "true"/"false"(标签里的写法大小写、多余空白都被吸收); // - int → 十进制; // - float → 归一化小数(`0.80` ⇒ `0.8`); // - string→ 原样(去首尾空白)。 // // 没有 `default` 标签的字段不进表(其「未设置」语义由各自的 Set 标志单独表达, // 不是本事实面的内容);无法归一化的值原样保留,宁可显示原始文本也不猜。 func declaredEnvDefaults(root any) map[string]string { out := make(map[string]string) if root == nil { return out } collectEnvDefaults(reflect.ValueOf(root), out, 0) return out } // maxConfigDepth 是递归下探嵌套配置组的深度上限(防御性:配置是静态结构, // 但它必须是常量而不是魔法数字)。 const maxConfigDepth = 8 // collectEnvDefaults 递归收集默认值。非结构体、指针、不可寻址的字段一律跳过: // 配置结构体是静态的平面嵌套,够不到的东西不猜。 func collectEnvDefaults(v reflect.Value, out map[string]string, depth int) { if depth > maxConfigDepth { return } if v.Kind() == reflect.Pointer { if v.IsNil() { return } v = v.Elem() } if v.Kind() != reflect.Struct { return } t := v.Type() for i := 0; i < t.NumField(); i++ { field := t.Field(i) if !field.IsExported() { continue } fv := v.Field(i) tag, hasEnv := field.Tag.Lookup("env") if !hasEnv { // 没有 env 标签的字段是嵌套配置组。 collectEnvDefaults(fv, out, depth+1) continue } name := envTagName(tag) if name == "" { continue } def, hasDefault := field.Tag.Lookup("default") if !hasDefault { continue } out[name] = normalizeDefault(fv.Kind(), def) } } // normalizeDefault 把标签里的默认值按字段类型归一化,形状与事实面的生效值一致。 func normalizeDefault(kind reflect.Kind, raw string) string { text := strings.TrimSpace(raw) switch kind { case reflect.Bool: if b, err := strconv.ParseBool(strings.ToLower(text)); err == nil { return strconv.FormatBool(b) } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if n, err := strconv.ParseInt(text, 10, 64); err == nil { return strconv.FormatInt(n, 10) } case reflect.Float32, reflect.Float64: if f, err := strconv.ParseFloat(text, 64); err == nil { return formatRatio(f) } } return text }