Files
2026-07-14 10:24:10 +08:00

110 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"strings"
"time"
)
// Validate 校验配置完整性与取值合法性(#16)。
// 在 Manager.Load 解析后自动调用,把"运行时第一次请求才暴露"的配置错误
// 提前到进程启动期。返回的 error 描述具体字段,便于定位。
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("配置为空")
}
var problems []string
// Server
if c.Server.Port < 0 || c.Server.Port > 65535 {
problems = append(problems, fmt.Sprintf("server.port 超出范围(0-65535): %d", c.Server.Port))
}
if c.Server.TLS.Enabled {
if strings.TrimSpace(c.Server.TLS.CertFile) == "" || strings.TrimSpace(c.Server.TLS.KeyFile) == "" {
problems = append(problems, "server.tls 启用后必须同时配置 cert_file 与 key_file")
}
}
if !validDuration(c.Server.ReadTimeout) || !validDuration(c.Server.WriteTimeout) ||
!validDuration(c.Server.IdleTimeout) || !validDuration(c.Server.ShutdownTimeout) {
problems = append(problems, "server 的 timeout 配置不能为负值")
}
// JWT:仅当配置了 secret 时校验(未启用 jwt 的项目可留空)
if c.JWT.Secret != "" {
if len(c.JWT.Secret) < 32 {
problems = append(problems, fmt.Sprintf("jwt.secret 长度不足 32 字节(当前 %d),HMAC 密钥过短不安全", len(c.JWT.Secret)))
}
if c.JWT.Expire < 0 || c.JWT.RefreshExpire < 0 {
problems = append(problems, "jwt.expire / jwt.refresh_expire 不能为负值")
}
}
// Database:出现任一数据库字段时视为启用;driver 为空按 MySQL 兼容处理。
if c.Database.isConfigured() {
driver := strings.TrimSpace(c.Database.Driver)
if driver != "" {
if _, ok := LookupDSNBuilder(driver); !ok {
problems = append(problems, fmt.Sprintf("database.driver 未注册: %s", driver))
}
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && strings.TrimSpace(c.Database.Host) == "" {
problems = append(problems, "database.host 启用数据库后必填")
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && strings.TrimSpace(c.Database.Name) == "" {
problems = append(problems, "database.name 启用数据库后必填")
}
if strings.TrimSpace(c.Database.CustomDSN) == "" && (c.Database.Port <= 0 || c.Database.Port > 65535) {
problems = append(problems, fmt.Sprintf("database.port 超出范围(1-65535): %d", c.Database.Port))
}
// L-config-3:连接池配置交叉校验。MaxOpenConns>0 时 MaxIdleConns 不应超过它,
// 否则 database/sql 会按 MaxOpenConns 截断空闲连接,配置意图与实际不符。
if c.Database.MaxOpenConns > 0 && c.Database.MaxIdleConns > c.Database.MaxOpenConns {
problems = append(problems, fmt.Sprintf(
"database.max_idle_conns(%d) 不应大于 max_open_conns(%d)", c.Database.MaxIdleConns, c.Database.MaxOpenConns))
}
// M-config-2Postgres SSLMode 合法性(非空时校验,空由 PostgresDSN 默认 prefer)。
if sslmode := strings.TrimSpace(c.Database.SSLMode); sslmode != "" {
if !validPostgresSSLMode(sslmode) {
problems = append(problems, fmt.Sprintf(
"database.ssl_mode 非法: %s(允许: disable/allow/prefer/require/verify-ca/verify-full", sslmode))
}
}
// M-config-2TLSRootCA 仅在 TLS=true 时生效,单独配置而无 tls:true 视为配置不一致。
if strings.TrimSpace(c.Database.TLSRootCA) != "" && !c.Database.TLS {
problems = append(problems, "database.tls_root_ca 需配合 tls: true 才生效")
}
}
// Redis:仅当配置了 host 时校验
if strings.TrimSpace(c.Redis.Host) != "" {
if c.Redis.Port <= 0 || c.Redis.Port > 65535 {
problems = append(problems, fmt.Sprintf("redis.port 超出范围(1-65535): %d", c.Redis.Port))
}
}
// Trace:仅当启用时校验采样比例。ExporterType/Propagator 的枚举校验委托 trace.Init
// (随 OTel 版本可能扩展,避免 config 与 trace 双源枚举漂移)。
if c.Trace.Enabled {
if c.Trace.SampleRatio < 0 || c.Trace.SampleRatio > 1 {
problems = append(problems, fmt.Sprintf("trace.sample_ratio 超出范围(0.0-1.0): %v", c.Trace.SampleRatio))
}
}
if len(problems) > 0 {
return fmt.Errorf("配置校验失败: %s", strings.Join(problems, "; "))
}
return nil
}
// validDuration 校验 Duration 非负(0 表示未配置/用默认,合法)。
func validDuration(d time.Duration) bool { return d >= 0 }
// validPostgresSSLMode 校验 PostgreSQL sslmode 取值(M-config-2)。
func validPostgresSSLMode(s string) bool {
switch s {
case "disable", "allow", "prefer", "require", "verify-ca", "verify-full":
return true
}
return false
}