Files
EchoChat/backend/go-service/config/config.go
2026-05-26 22:41:53 +08:00

238 lines
9.9 KiB
Go
Raw 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 提供应用配置管理功能
// 使用 Viper 读取 YAML 配置文件,支持环境变量覆盖
package config
import (
"fmt"
"strings"
"github.com/spf13/viper"
)
// Config 应用全局配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
Minio MinioConfig `mapstructure:"minio"`
MediaServer MediaServerConfig `mapstructure:"media_server"`
Meeting MeetingConfig `mapstructure:"meeting"`
LLMSource LLMSourceConfig `mapstructure:"llm_source"` // Phase B外部 LLM 配置库MySQLt_llm_provider/model/key
OAuth OAuthConfig `mapstructure:"oauth"`
}
type OAuthConfig struct {
FrontendCallbackURL string `mapstructure:"frontend_callback_url"`
Providers map[string]OAuthApp `mapstructure:"providers"`
}
type OAuthApp struct {
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
RedirectURI string `mapstructure:"redirect_uri"`
Enabled bool `mapstructure:"enabled"`
}
// LLMSourceConfig 外部 LLM/STT 配置中心 MySQL 连接
//
// 该库与本服务的 PostgreSQL 主库相互独立,仅做只读:
// - t_llm_provider 提供商列表base_url / api_protocol
// - t_llm_model 模型列表model_type=4 表示语音识别)
// - t_llm_key Key 池(按 weight DESC + today_count ASC 选 key
//
// Enabled=false 时跳过初始化,转写功能会以 "未配置 STT 服务" 返回 503
// 方便在没有外部 MySQL 的开发环境只跑会议管理只读视图。
type LLMSourceConfig struct {
Enabled bool `mapstructure:"enabled"` // false=跳过连接,转写不可用
Host string `mapstructure:"host"` // MySQL host
Port int `mapstructure:"port"` // MySQL port默认 3306
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
Charset string `mapstructure:"charset"` // 默认 utf8mb4
ParseTime bool `mapstructure:"parse_time"` // 默认 true
Loc string `mapstructure:"loc"` // 默认 Local
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
}
// DSN 生成 MySQL DSN
// charset/parse_time/loc 缺省时填充常见安全默认值
func (l *LLMSourceConfig) DSN() string {
charset := l.Charset
if charset == "" {
charset = "utf8mb4"
}
loc := l.Loc
if loc == "" {
loc = "Local"
}
parseTime := "True"
if !l.ParseTime {
parseTime = "False"
}
port := l.Port
if port == 0 {
port = 3306
}
return fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%s&loc=%s",
l.User, l.Password, l.Host, port, l.DBName, charset, parseTime, loc,
)
}
// MeetingConfig 会议模块生命周期参数Phase 2e-2 Task 8
// 所有字段均允许通过环境变量 ECHOCHAT_MEETING_* 覆盖,便于 E2E 测试以短时长加速场景触发
type MeetingConfig struct {
HostGraceSeconds int `mapstructure:"host_grace_seconds"` // host 掉线宽限期秒数,默认 120
EmptyRoomTTLSeconds int `mapstructure:"empty_room_ttl_seconds"` // 空房自动销毁 TTL 秒数,默认 300
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"` // 兜底扫描周期秒数,默认 30
// Phase Bmedia-server 失败回调 webhook 共享密钥,必须与 media-server 端 INTERNAL_WEBHOOK_SECRET 一致
// 留空时 /internal/meeting/recordings/failure 全部直接 403默认安全
InternalWebhookSecret string `mapstructure:"internal_webhook_secret"`
RecordingTranscodeEnabled bool `mapstructure:"recording_transcode_enabled"`
RecordingTranscodeFFmpegPath string `mapstructure:"recording_transcode_ffmpeg_path"`
RecordingTranscodeTimeoutSeconds int `mapstructure:"recording_transcode_timeout_seconds"`
StaleRoomHours int `mapstructure:"stale_room_hours"` // 活跃超过此小时且无成员视为 stale默认 4
CloudLawInternalToken string `mapstructure:"cloud_law_internal_token"`
}
// MediaServerConfig Node media-server 接入配置Phase 2e-2 Task 7
// 与 media-server/.env 中的 MEDIA_INTERNAL_TOKEN / HTTP_PORT 成对使用
// BaseURL 需精确到协议与端口http://host:port不含末尾斜杠
type MediaServerConfig struct {
BaseURL string `mapstructure:"base_url"` // 如 http://localhost:3300
InternalToken string `mapstructure:"internal_token"` // 与 Node 共享密钥
TimeoutMS int `mapstructure:"timeout_ms"` // 创建类接口超时(毫秒),默认 10000
CloseTimeoutMS int `mapstructure:"close_timeout_ms"` // 关闭类接口超时(毫秒),默认 2000
CloseRetry int `mapstructure:"close_retry"` // 关闭类接口失败重试次数,默认 2
CreateRouterRetry int `mapstructure:"create_router_retry"` // Task 16 NitCreateRouter 5xx/网络错误时的重试次数,默认 1
}
// MinioConfig MinIO 对象存储配置
type MinioConfig struct {
Endpoint string `mapstructure:"endpoint"` // MinIO 服务地址host:port
AccessKey string `mapstructure:"access_key"` // 访问密钥
SecretKey string `mapstructure:"secret_key"` // 密钥
Bucket string `mapstructure:"bucket"` // 存储桶名称
UseSSL bool `mapstructure:"use_ssl"` // 是否使用 HTTPS
PublicBaseURL string `mapstructure:"public_base_url"`
}
// ServerConfig HTTP 服务配置
type ServerConfig struct {
Port int `mapstructure:"port"` // 监听端口
Mode string `mapstructure:"mode"` // 运行模式: debug/release
// Task 16 NitWebSocket 升级握手 Origin 白名单(逗号分隔)
// - 空串 → dev 模式mode != release放行全部release 模式强制拒绝所有跨源(仅同源可建连)
// - 配置示例:"https://app.example.com,http://localhost:5173"
// - 环境变量覆盖ECHOCHAT_SERVER_WS_ALLOWED_ORIGINS="https://a.com,https://b.com"
WSAllowedOrigins string `mapstructure:"ws_allowed_origins"`
}
// AllowedOrigins 将逗号分隔的 WSAllowedOrigins 解析为 slice已去空并 trim
func (s *ServerConfig) AllowedOrigins() []string {
if s.WSAllowedOrigins == "" {
return nil
}
raw := strings.Split(s.WSAllowedOrigins, ",")
out := make([]string, 0, len(raw))
for _, o := range raw {
if v := strings.TrimSpace(o); v != "" {
out = append(out, v)
}
}
return out
}
// IsRelease 判断是否为生产运行模式
func (s *ServerConfig) IsRelease() bool {
return strings.EqualFold(s.Mode, "release")
}
// DatabaseConfig PostgreSQL 数据库配置
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
}
// DSN 生成 PostgreSQL 连接字符串
func (d *DatabaseConfig) DSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
)
}
// RedisConfig Redis 配置
type RedisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"` // 数据库编号
}
// Addr 生成 Redis 连接地址
func (r *RedisConfig) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
// JWTConfig JWT 认证配置
type JWTConfig struct {
Secret string `mapstructure:"secret"` // 签名密钥
AccessExpireMin int `mapstructure:"access_expire_min"` // Access Token 有效期(分钟)
RefreshExpireDay int `mapstructure:"refresh_expire_day"` // Refresh Token 有效期(天)
Issuer string `mapstructure:"issuer"` // 签发者
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"` // debug/info/warn/error
Format string `mapstructure:"format"` // text(开发)/json(生产)
OutputPath string `mapstructure:"output_path"` // stdout 或文件路径
File LogFileConfig `mapstructure:"file"` // 日志文件轮转配置
}
// LogFileConfig 日志文件轮转配置(基于 lumberjack
type LogFileConfig struct {
Enable bool `mapstructure:"enable"` // 是否启用文件日志
Dir string `mapstructure:"dir"` // 日志文件目录
MaxSize int `mapstructure:"max_size"` // 单个文件最大大小MB超过后自动切割
MaxBackups int `mapstructure:"max_backups"` // 保留的旧日志文件最大数量
MaxAge int `mapstructure:"max_age"` // 旧日志文件保留天数
Compress bool `mapstructure:"compress"` // 是否压缩归档的旧日志文件
}
// Load 加载配置文件并返回 Config 实例
// configPath 为配置文件所在目录configName 为文件名(不含扩展名)
func Load(configPath, configName string) (*Config, error) {
v := viper.New()
v.SetConfigName(configName)
v.SetConfigType("yaml")
v.AddConfigPath(configPath)
// 环境变量覆盖ECHOCHAT_SERVER_PORT → server.port
v.SetEnvPrefix("ECHOCHAT")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
return &cfg, nil
}