初始化
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user