初始化

This commit is contained in:
Ran
2026-08-25 17:59:42 +08:00
commit 4b7380dd9b
408 changed files with 327400 additions and 0 deletions
+241
View File
@@ -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
}