Remove InfoGenie-frontend and Go .env from version control; add .env.example templates; ignore .claude local settings. Admin UI reads site gate from env only. Note: rotate secrets if repo history was ever public. Made-with: Cursor
303 lines
7.9 KiB
Go
303 lines
7.9 KiB
Go
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/joho/godotenv"
|
||
)
|
||
|
||
type AppConfig struct {
|
||
Env string
|
||
Port string
|
||
|
||
DB DBConfig
|
||
Mail MailConfig
|
||
AI AIConfig
|
||
AuthCenter AuthCenterConfig
|
||
Redis RedisConfig
|
||
// SiteAdminToken 与前端管理员口令一致,用于更新站点展示配置(如 60s 功能开关);为空则禁止写入
|
||
SiteAdminToken string
|
||
}
|
||
|
||
// RedisConfig 可选缓存;Enabled 为 false 时不连接 Redis,行为与未接入缓存时一致
|
||
type RedisConfig struct {
|
||
Enabled bool
|
||
Addr string
|
||
Password string
|
||
DB int
|
||
KeyPrefix string
|
||
SiteTTL time.Duration
|
||
}
|
||
|
||
type DBConfig struct {
|
||
Host string
|
||
Port string
|
||
Name string
|
||
User string
|
||
Password string
|
||
}
|
||
|
||
func (d DBConfig) DSN() string {
|
||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s",
|
||
d.User, d.Password, d.Host, d.Port, d.Name)
|
||
}
|
||
|
||
type MailConfig struct {
|
||
Host string
|
||
Port int
|
||
Username string
|
||
Password string
|
||
}
|
||
|
||
type AuthCenterConfig struct {
|
||
APIURL string
|
||
AdminToken string
|
||
}
|
||
|
||
type AIProviderConfig struct {
|
||
APIKey string `json:"api_key"`
|
||
APIBase string `json:"api_base"`
|
||
Models []string `json:"model"`
|
||
}
|
||
|
||
type AIConfig struct {
|
||
Providers map[string]AIProviderConfig
|
||
}
|
||
|
||
var Cfg *AppConfig
|
||
|
||
const (
|
||
envDevelopment = "development"
|
||
envProduction = "production"
|
||
|
||
defaultDevDBHost = "10.1.1.100"
|
||
defaultDevDBPort = "3306"
|
||
defaultDevDBName = "infogenie-test"
|
||
defaultDevDBUser = "infogenie-test"
|
||
defaultDevDBPassword = "infogenie-test"
|
||
|
||
defaultDevRedisAddr = "10.1.1.100:6379"
|
||
defaultRedisDB = 10
|
||
defaultRedisKeyPrefix = "infogenie:go:v1:"
|
||
)
|
||
|
||
func Load() (*AppConfig, error) {
|
||
env := normalizeEnv(os.Getenv("APP_ENV"))
|
||
if env != envDevelopment && env != envProduction {
|
||
return nil, fmt.Errorf("不支持的APP_ENV: %s", env)
|
||
}
|
||
|
||
if err := loadEnvFile(env); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
mailPort, _ := strconv.Atoi(getEnv("MAIL_PORT", "465"))
|
||
|
||
dbHost, err := getEnvByEnvironment(env, "DB_HOST", defaultDevDBHost)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
dbPort, err := getEnvByEnvironment(env, "DB_PORT", defaultDevDBPort)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
dbName, err := getEnvByEnvironment(env, "DB_NAME", defaultDevDBName)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
dbUser, err := getEnvByEnvironment(env, "DB_USER", defaultDevDBUser)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
dbPassword, err := getEnvByEnvironment(env, "DB_PASSWORD", defaultDevDBPassword)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
cfg := &AppConfig{
|
||
Env: env,
|
||
Port: getEnv("APP_PORT", "5002"),
|
||
DB: DBConfig{
|
||
Host: dbHost,
|
||
Port: dbPort,
|
||
Name: dbName,
|
||
User: dbUser,
|
||
Password: dbPassword,
|
||
},
|
||
Mail: MailConfig{
|
||
Host: getEnv("MAIL_HOST", "smtp.qq.com"),
|
||
Port: mailPort,
|
||
Username: getEnv("MAIL_USERNAME", ""),
|
||
Password: getEnv("MAIL_PASSWORD", ""),
|
||
},
|
||
AuthCenter: AuthCenterConfig{
|
||
APIURL: getEnv("AUTH_CENTER_API_URL", "https://auth.api.shumengya.top"),
|
||
AdminToken: getEnv("AUTH_CENTER_ADMIN_TOKEN", ""),
|
||
},
|
||
SiteAdminToken: getEnv("INFOGENIE_SITE_ADMIN_TOKEN", ""),
|
||
}
|
||
|
||
if err := validateDBConfig(cfg); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
redisCfg, err := loadRedisConfig(env)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cfg.Redis = redisCfg
|
||
|
||
// AI配置现在完全从数据库读取,不再加载ai_config.json文件
|
||
cfg.AI = AIConfig{Providers: make(map[string]AIProviderConfig)}
|
||
|
||
Cfg = cfg
|
||
return cfg, nil
|
||
}
|
||
|
||
func loadRedisConfig(env string) (RedisConfig, error) {
|
||
if !parseBoolEnv(os.Getenv("REDIS_ENABLED")) {
|
||
return RedisConfig{}, nil
|
||
}
|
||
var addr string
|
||
var err error
|
||
if env == envProduction {
|
||
addr, err = getEnvByEnvironment(env, "REDIS_ADDR", "")
|
||
if err != nil {
|
||
return RedisConfig{}, err
|
||
}
|
||
} else {
|
||
addr = getEnv("REDIS_ADDR", defaultDevRedisAddr)
|
||
}
|
||
if strings.TrimSpace(addr) == "" {
|
||
return RedisConfig{}, fmt.Errorf("已启用 REDIS_ENABLED 但 REDIS_ADDR 为空")
|
||
}
|
||
dbIdx, err := strconv.Atoi(strings.TrimSpace(getEnv("REDIS_DB", strconv.Itoa(defaultRedisDB))))
|
||
if err != nil || dbIdx < 0 {
|
||
return RedisConfig{}, fmt.Errorf("无效的 REDIS_DB: %s", getEnv("REDIS_DB", ""))
|
||
}
|
||
ttlSec, err := strconv.Atoi(strings.TrimSpace(getEnv("REDIS_SITE_TTL", "60")))
|
||
if err != nil || ttlSec < 1 {
|
||
return RedisConfig{}, fmt.Errorf("无效的 REDIS_SITE_TTL: %s", getEnv("REDIS_SITE_TTL", ""))
|
||
}
|
||
prefix := strings.TrimSpace(getEnv("REDIS_KEY_PREFIX", defaultRedisKeyPrefix))
|
||
if prefix == "" {
|
||
prefix = defaultRedisKeyPrefix
|
||
}
|
||
if !strings.HasSuffix(prefix, ":") {
|
||
prefix += ":"
|
||
}
|
||
return RedisConfig{
|
||
Enabled: true,
|
||
Addr: addr,
|
||
Password: getEnv("REDIS_PASSWORD", ""),
|
||
DB: dbIdx,
|
||
KeyPrefix: prefix,
|
||
SiteTTL: time.Duration(ttlSec) * time.Second,
|
||
}, nil
|
||
}
|
||
|
||
func parseBoolEnv(raw string) bool {
|
||
s := strings.ToLower(strings.TrimSpace(raw))
|
||
return s == "1" || s == "true" || s == "yes" || s == "on"
|
||
}
|
||
|
||
func loadEnvFile(env string) error {
|
||
envFile := fmt.Sprintf(".env.%s", env)
|
||
if _, err := os.Stat(envFile); err == nil {
|
||
return godotenv.Load(envFile)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateDBConfig(cfg *AppConfig) error {
|
||
switch cfg.Env {
|
||
case envDevelopment:
|
||
if !isDevelopmentDBTarget(cfg.DB) {
|
||
return fmt.Errorf("开发环境必须使用测试数据库: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||
}
|
||
if looksProductionLike(cfg.DB) {
|
||
return fmt.Errorf("开发环境检测到生产数据库配置: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||
}
|
||
case envProduction:
|
||
missing := make([]string, 0, 4)
|
||
if strings.TrimSpace(cfg.DB.Host) == "" {
|
||
missing = append(missing, "DB_HOST")
|
||
}
|
||
if strings.TrimSpace(cfg.DB.Port) == "" {
|
||
missing = append(missing, "DB_PORT")
|
||
}
|
||
if strings.TrimSpace(cfg.DB.Name) == "" {
|
||
missing = append(missing, "DB_NAME")
|
||
}
|
||
if strings.TrimSpace(cfg.DB.User) == "" {
|
||
missing = append(missing, "DB_USER")
|
||
}
|
||
if strings.TrimSpace(cfg.DB.Password) == "" {
|
||
missing = append(missing, "DB_PASSWORD")
|
||
}
|
||
if len(missing) > 0 {
|
||
return fmt.Errorf("生产环境缺少必需数据库配置: %s", strings.Join(missing, ", "))
|
||
}
|
||
if isDevelopmentDBTarget(cfg.DB) {
|
||
return fmt.Errorf("生产环境数据库配置看起来像开发/测试环境: host=%s name=%s", cfg.DB.Host, cfg.DB.Name)
|
||
}
|
||
default:
|
||
return fmt.Errorf("不支持的APP_ENV: %s", cfg.Env)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func isDevelopmentDBTarget(db DBConfig) bool {
|
||
host := strings.ToLower(strings.TrimSpace(db.Host))
|
||
name := strings.ToLower(strings.TrimSpace(db.Name))
|
||
|
||
if host == "localhost" || strings.HasPrefix(host, "127.") || strings.HasPrefix(host, "10.1.1.") {
|
||
return true
|
||
}
|
||
if strings.Contains(host, "dev") || strings.Contains(host, "test") || strings.Contains(host, "local") {
|
||
return true
|
||
}
|
||
if strings.Contains(name, "test") || strings.Contains(name, "dev") || strings.Contains(name, "local") {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func looksProductionLike(db DBConfig) bool {
|
||
host := strings.ToLower(strings.TrimSpace(db.Host))
|
||
name := strings.ToLower(strings.TrimSpace(db.Name))
|
||
return strings.Contains(host, "bigmengya") || strings.Contains(host, "shumengya.top") || strings.Contains(name, "prod") || strings.Contains(name, "production")
|
||
}
|
||
|
||
func normalizeEnv(raw string) string {
|
||
env := strings.ToLower(strings.TrimSpace(raw))
|
||
if env == "" {
|
||
return envDevelopment
|
||
}
|
||
return env
|
||
}
|
||
|
||
func getEnv(key, fallback string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func getEnvByEnvironment(env, key, devFallback string) (string, error) {
|
||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||
return v, nil
|
||
}
|
||
|
||
if env == envProduction {
|
||
return "", fmt.Errorf("生产环境缺少必需配置: %s", key)
|
||
}
|
||
|
||
return devFallback, nil
|
||
}
|