242 lines
7.5 KiB
Go
242 lines
7.5 KiB
Go
// Package config 读取并校验 API 服务运行所需的环境变量。
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Config 汇总数据库、缓存、认证、腾讯云 COS 和本地媒体处理配置。
|
|
type Config struct {
|
|
AppEnv string
|
|
Debug bool
|
|
ServerAddress string
|
|
DatabaseURL string
|
|
RedisAddress string
|
|
RedisPassword string
|
|
RedisDB int
|
|
AsynqRedisDB int
|
|
CORSOrigins []string
|
|
StaticDirectory string
|
|
JWTSecretKey string
|
|
JWTAccessMinutes int
|
|
JWTRefreshHours int
|
|
BootstrapAdminUsername string
|
|
BootstrapAdminPassword string
|
|
Argon2Time uint32
|
|
Argon2Memory uint32
|
|
Argon2Parallelism uint8
|
|
Argon2HashLength uint32
|
|
Argon2SaltLength uint32
|
|
EncryptionKeyVersion string
|
|
EncryptionKey string
|
|
COSSecretID string
|
|
COSSecretKey string
|
|
COSBucket string
|
|
COSRegion string
|
|
COSEndpoint string
|
|
COSPublicBaseURL string
|
|
COSMaxImageSizeMB int
|
|
COSMaxVideoSizeMB int
|
|
COSMaxAudioSizeMB int
|
|
AIWorkerConcurrency int
|
|
AIPollIntervalSeconds int
|
|
AIHTTPMaxConnections int
|
|
FFmpegPath string
|
|
FFprobePath string
|
|
PythonPath string
|
|
ASRScriptPath string
|
|
}
|
|
|
|
// Load 从本地环境文件和进程环境读取配置,并校验数值范围。
|
|
func Load() (Config, error) {
|
|
_ = godotenv.Load()
|
|
|
|
databasePort, err := envInt("DATABASE_PORT", 25432)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
redisPort, err := envInt("REDIS_PORT", 26379)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
redisDB, err := envInt("REDIS_DB", 0)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
asynqRedisDB, err := envInt("ASYNQ_REDIS_DB", 1)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
jwtAccessMinutes, err := envInt("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", 60)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
jwtRefreshHours, err := envInt("JWT_REFRESH_TOKEN_EXPIRE_HOURS", 168)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
argon2Time, err := envInt("ARGON2_TIME_COST", 2)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
argon2Memory, err := envInt("ARGON2_MEMORY_COST", 19456)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
argon2Parallelism, err := envInt("ARGON2_PARALLELISM", 1)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
argon2HashLength, err := envInt("ARGON2_HASH_LENGTH", 32)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
argon2SaltLength, err := envInt("ARGON2_SALT_LENGTH", 16)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cosMaxImageSizeMB, err := envInt("COS_MAX_IMAGE_SIZE_MB", 20)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cosMaxVideoSizeMB, err := envInt("COS_MAX_VIDEO_SIZE_MB", 100)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cosMaxAudioSizeMB, err := envInt("COS_MAX_AUDIO_SIZE_MB", 30)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
aiWorkerConcurrency, err := envInt("AI_WORKER_CONCURRENCY", 100)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
aiPollIntervalSeconds, err := envInt("AI_POLL_INTERVAL_SECONDS", 10)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
aiHTTPMaxConnections, err := envInt("AI_HTTP_MAX_CONNECTIONS", 200)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if cosMaxImageSizeMB < 1 || cosMaxVideoSizeMB < 1 || cosMaxAudioSizeMB < 1 {
|
|
return Config{}, fmt.Errorf("COS 文件大小限制必须为正整数")
|
|
}
|
|
if aiWorkerConcurrency < 1 || aiWorkerConcurrency > 1000 {
|
|
return Config{}, fmt.Errorf("AI_WORKER_CONCURRENCY 必须在 1 至 1000 之间")
|
|
}
|
|
if aiPollIntervalSeconds < 2 || aiPollIntervalSeconds > 300 {
|
|
return Config{}, fmt.Errorf("AI_POLL_INTERVAL_SECONDS 必须在 2 至 300 之间")
|
|
}
|
|
if aiHTTPMaxConnections < 10 || aiHTTPMaxConnections > 5000 {
|
|
return Config{}, fmt.Errorf("AI_HTTP_MAX_CONNECTIONS 必须在 10 至 5000 之间")
|
|
}
|
|
debug, err := strconv.ParseBool(env("DEBUG", "true"))
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("DEBUG 必须是布尔值: %w", err)
|
|
}
|
|
|
|
databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL"))
|
|
if databaseURL == "" {
|
|
databaseURL = buildDatabaseURL(databasePort)
|
|
} else if parsed, parseErr := url.Parse(databaseURL); parseErr == nil {
|
|
query := parsed.Query()
|
|
if query.Get("TimeZone") == "" {
|
|
query.Set("TimeZone", "Asia/Shanghai")
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
databaseURL = parsed.String()
|
|
}
|
|
|
|
return Config{
|
|
AppEnv: env("APP_ENV", "development"),
|
|
Debug: debug,
|
|
ServerAddress: env("SERVER_ADDRESS", ":8900"),
|
|
DatabaseURL: databaseURL,
|
|
RedisAddress: fmt.Sprintf("%s:%d", env("REDIS_HOST", "127.0.0.1"), redisPort),
|
|
RedisPassword: os.Getenv("REDIS_PASSWORD"),
|
|
RedisDB: redisDB,
|
|
AsynqRedisDB: asynqRedisDB,
|
|
CORSOrigins: splitCSV(env("CORS_ORIGINS", "http://localhost:5500,http://localhost:5501")),
|
|
StaticDirectory: env("STATIC_DIRECTORY", "static"),
|
|
JWTSecretKey: env("JWT_SECRET_KEY", "replace-with-a-strong-random-secret"),
|
|
JWTAccessMinutes: jwtAccessMinutes,
|
|
JWTRefreshHours: jwtRefreshHours,
|
|
BootstrapAdminUsername: env("ADMIN_BOOTSTRAP_USERNAME", "admin"),
|
|
BootstrapAdminPassword: os.Getenv("ADMIN_BOOTSTRAP_PASSWORD"),
|
|
Argon2Time: uint32(argon2Time),
|
|
Argon2Memory: uint32(argon2Memory),
|
|
Argon2Parallelism: uint8(argon2Parallelism),
|
|
Argon2HashLength: uint32(argon2HashLength),
|
|
Argon2SaltLength: uint32(argon2SaltLength),
|
|
EncryptionKeyVersion: env("CONFIG_ENCRYPTION_KEY_VERSION", "v1"),
|
|
EncryptionKey: os.Getenv("CONFIG_ENCRYPTION_KEY"),
|
|
COSSecretID: os.Getenv("COS_SECRET_ID"),
|
|
COSSecretKey: os.Getenv("COS_SECRET_KEY"),
|
|
COSBucket: os.Getenv("COS_BUCKET"),
|
|
COSRegion: env("COS_REGION", "ap-chengdu"),
|
|
COSEndpoint: os.Getenv("COS_ENDPOINT"),
|
|
COSPublicBaseURL: strings.TrimRight(os.Getenv("COS_PUBLIC_BASE_URL"), "/"),
|
|
COSMaxImageSizeMB: cosMaxImageSizeMB,
|
|
COSMaxVideoSizeMB: cosMaxVideoSizeMB,
|
|
COSMaxAudioSizeMB: cosMaxAudioSizeMB,
|
|
AIWorkerConcurrency: aiWorkerConcurrency,
|
|
AIPollIntervalSeconds: aiPollIntervalSeconds,
|
|
AIHTTPMaxConnections: aiHTTPMaxConnections,
|
|
FFmpegPath: env("FFMPEG_PATH", "ffmpeg"),
|
|
FFprobePath: env("FFPROBE_PATH", "ffprobe"),
|
|
PythonPath: env("PYTHON_PATH", "python3"),
|
|
ASRScriptPath: env("ASR_SCRIPT_PATH", "workers/transcribe.py"),
|
|
}, nil
|
|
}
|
|
|
|
func buildDatabaseURL(port int) string {
|
|
credentials := url.UserPassword(
|
|
env("DATABASE_USER", "postgres"),
|
|
env("DATABASE_PASSWORD", "juchuang_dev"),
|
|
).String()
|
|
return fmt.Sprintf(
|
|
"postgres://%s@%s:%d/%s?sslmode=disable&TimeZone=Asia/Shanghai",
|
|
credentials,
|
|
env("DATABASE_HOST", "127.0.0.1"),
|
|
port,
|
|
url.PathEscape(env("DATABASE_NAME", "juchuang_factory")),
|
|
)
|
|
}
|
|
|
|
func env(key, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func envInt(key string, fallback int) (int, error) {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s 必须是整数: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func splitCSV(value string) []string {
|
|
items := strings.Split(value, ",")
|
|
result := make([]string, 0, len(items))
|
|
for _, item := range items {
|
|
if item = strings.TrimSpace(item); item != "" {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|