Files
backend/internal/config/config.go
lan 0a0cbacbcc feat(schedule): add course table screens and navigation
Add complete schedule functionality including:
- Schedule screen with weekly course table view
- Course detail screen with transparent modal presentation
- New ScheduleStack navigator integrated into main tab bar
- Schedule service for API interactions
- Type definitions for course entities

Also includes bug fixes for group invite/request handlers
to include required groupId parameter.
2026-03-12 08:38:14 +08:00

475 lines
22 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
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Cache CacheConfig `mapstructure:"cache"`
S3 S3Config `mapstructure:"s3"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Upload UploadConfig `mapstructure:"upload"`
Gorse GorseConfig `mapstructure:"gorse"`
OpenAI OpenAIConfig `mapstructure:"openai"`
Email EmailConfig `mapstructure:"email"`
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
}
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type DatabaseConfig struct {
Type string `mapstructure:"type"`
SQLite SQLiteConfig `mapstructure:"sqlite"`
Postgres PostgresConfig `mapstructure:"postgres"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
LogLevel string `mapstructure:"log_level"`
SlowThresholdMs int `mapstructure:"slow_threshold_ms"`
IgnoreRecordNotFound bool `mapstructure:"ignore_record_not_found"`
ParameterizedQueries bool `mapstructure:"parameterized_queries"`
}
type SQLiteConfig struct {
Path string `mapstructure:"path"`
}
type PostgresConfig 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"`
}
func (d PostgresConfig) 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,
)
}
type RedisConfig struct {
Type string `mapstructure:"type"`
Redis RedisServerConfig `mapstructure:"redis"`
Miniredis MiniredisConfig `mapstructure:"miniredis"`
PoolSize int `mapstructure:"pool_size"`
}
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
KeyPrefix string `mapstructure:"key_prefix"`
DefaultTTL int `mapstructure:"default_ttl"`
NullTTL int `mapstructure:"null_ttl"`
JitterRatio float64 `mapstructure:"jitter_ratio"`
DisableFlushDB bool `mapstructure:"disable_flushdb"`
Modules CacheModuleTTL `mapstructure:"modules"`
}
type CacheModuleTTL struct {
PostList int `mapstructure:"post_list_ttl"`
Conversation int `mapstructure:"conversation_ttl"`
UnreadCount int `mapstructure:"unread_count_ttl"`
GroupMembers int `mapstructure:"group_members_ttl"`
}
type RedisServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
func (r RedisServerConfig) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
type MiniredisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type S3Config struct {
Endpoint string `mapstructure:"endpoint"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
UseSSL bool `mapstructure:"use_ssl"`
Region string `mapstructure:"region"`
Domain string `mapstructure:"domain"` // 自定义域名,如 s3.carrot.skin
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
AccessTokenExpire time.Duration `mapstructure:"access_token_expire"`
RefreshTokenExpire time.Duration `mapstructure:"refresh_token_expire"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Encoding string `mapstructure:"encoding"`
OutputPaths []string `mapstructure:"output_paths"`
}
type RateLimitConfig struct {
Enabled bool `mapstructure:"enabled"`
RequestsPerMinute int `mapstructure:"requests_per_minute"`
}
type UploadConfig struct {
MaxFileSize int64 `mapstructure:"max_file_size"`
AllowedTypes []string `mapstructure:"allowed_types"`
}
type GorseConfig struct {
Address string `mapstructure:"address"`
APIKey string `mapstructure:"api_key"`
Enabled bool `mapstructure:"enabled"`
Dashboard string `mapstructure:"dashboard"`
ImportPassword string `mapstructure:"import_password"`
EmbeddingAPIKey string `mapstructure:"embedding_api_key"`
EmbeddingURL string `mapstructure:"embedding_url"`
EmbeddingModel string `mapstructure:"embedding_model"`
}
type OpenAIConfig struct {
Enabled bool `mapstructure:"enabled"`
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
ModerationModel string `mapstructure:"moderation_model"`
ModerationMaxImagesPerRequest int `mapstructure:"moderation_max_images_per_request"`
RequestTimeout int `mapstructure:"request_timeout"`
StrictModeration bool `mapstructure:"strict_moderation"`
}
type EmailConfig struct {
Enabled bool `mapstructure:"enabled"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
FromAddress string `mapstructure:"from_address"`
FromName string `mapstructure:"from_name"`
UseTLS bool `mapstructure:"use_tls"`
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
Timeout int `mapstructure:"timeout"`
}
// ConversationCacheConfig 会话缓存配置
type ConversationCacheConfig struct {
// TTL 配置
DetailTTL string `mapstructure:"detail_ttl"`
ListTTL string `mapstructure:"list_ttl"`
ParticipantTTL string `mapstructure:"participant_ttl"`
UnreadTTL string `mapstructure:"unread_ttl"`
// 消息缓存配置
MessageDetailTTL string `mapstructure:"message_detail_ttl"`
MessageListTTL string `mapstructure:"message_list_ttl"`
MessageIndexTTL string `mapstructure:"message_index_ttl"`
MessageCountTTL string `mapstructure:"message_count_ttl"`
// 批量写入配置
BatchInterval string `mapstructure:"batch_interval"`
BatchThreshold int `mapstructure:"batch_threshold"`
BatchMaxSize int `mapstructure:"batch_max_size"`
BufferMaxSize int `mapstructure:"buffer_max_size"`
}
// ConversationCacheSettings 会话缓存运行时配置(用于传递给 cache 包)
type ConversationCacheSettings struct {
DetailTTL time.Duration
ListTTL time.Duration
ParticipantTTL time.Duration
UnreadTTL time.Duration
MessageDetailTTL time.Duration
MessageListTTL time.Duration
MessageIndexTTL time.Duration
MessageCountTTL time.Duration
BatchInterval time.Duration
BatchThreshold int
BatchMaxSize int
BufferMaxSize int
}
// ToSettings 将 ConversationCacheConfig 转换为 ConversationCacheSettings
func (c *ConversationCacheConfig) ToSettings() *ConversationCacheSettings {
return &ConversationCacheSettings{
DetailTTL: parseDuration(c.DetailTTL, 5*time.Minute),
ListTTL: parseDuration(c.ListTTL, 60*time.Second),
ParticipantTTL: parseDuration(c.ParticipantTTL, 5*time.Minute),
UnreadTTL: parseDuration(c.UnreadTTL, 30*time.Second),
MessageDetailTTL: parseDuration(c.MessageDetailTTL, 30*time.Minute),
MessageListTTL: parseDuration(c.MessageListTTL, 5*time.Minute),
MessageIndexTTL: parseDuration(c.MessageIndexTTL, 30*time.Minute),
MessageCountTTL: parseDuration(c.MessageCountTTL, 30*time.Minute),
BatchInterval: parseDuration(c.BatchInterval, 5*time.Second),
BatchThreshold: c.BatchThreshold,
BatchMaxSize: c.BatchMaxSize,
BufferMaxSize: c.BufferMaxSize,
}
}
// parseDuration 解析持续时间字符串,如果解析失败则返回默认值
func parseDuration(s string, defaultVal time.Duration) time.Duration {
if s == "" {
return defaultVal
}
d, err := time.ParseDuration(s)
if err != nil {
return defaultVal
}
return d
}
func Load(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 启用环境变量支持
viper.SetEnvPrefix("APP")
viper.AutomaticEnv()
// 允许环境变量使用下划线或连字符
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// Set default values
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.mode", "debug")
viper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("database.type", "sqlite")
viper.SetDefault("database.sqlite.path", "./data/carrot_bbs.db")
viper.SetDefault("database.max_idle_conns", 10)
viper.SetDefault("database.max_open_conns", 100)
viper.SetDefault("database.log_level", "warn")
viper.SetDefault("database.slow_threshold_ms", 200)
viper.SetDefault("database.ignore_record_not_found", true)
viper.SetDefault("database.parameterized_queries", true)
viper.SetDefault("redis.type", "miniredis")
viper.SetDefault("redis.redis.host", "localhost")
viper.SetDefault("redis.redis.port", 6379)
viper.SetDefault("redis.redis.password", "")
viper.SetDefault("redis.redis.db", 0)
viper.SetDefault("redis.miniredis.host", "localhost")
viper.SetDefault("redis.miniredis.port", 6379)
viper.SetDefault("redis.pool_size", 10)
viper.SetDefault("cache.enabled", true)
viper.SetDefault("cache.key_prefix", "")
viper.SetDefault("cache.default_ttl", 30)
viper.SetDefault("cache.null_ttl", 5)
viper.SetDefault("cache.jitter_ratio", 0.1)
viper.SetDefault("cache.disable_flushdb", true)
viper.SetDefault("cache.modules.post_list_ttl", 30)
viper.SetDefault("cache.modules.conversation_ttl", 60)
viper.SetDefault("cache.modules.unread_count_ttl", 30)
viper.SetDefault("cache.modules.group_members_ttl", 120)
viper.SetDefault("jwt.secret", "your-jwt-secret-key-change-in-production")
viper.SetDefault("jwt.access_token_expire", 86400)
viper.SetDefault("jwt.refresh_token_expire", 604800)
viper.SetDefault("log.level", "info")
viper.SetDefault("log.encoding", "json")
viper.SetDefault("log.output_paths", []string{"stdout", "./logs/app.log"})
viper.SetDefault("rate_limit.enabled", true)
viper.SetDefault("rate_limit.requests_per_minute", 60)
viper.SetDefault("upload.max_file_size", 10485760)
viper.SetDefault("upload.allowed_types", []string{"image/jpeg", "image/png", "image/gif", "image/webp"})
viper.SetDefault("s3.endpoint", "")
viper.SetDefault("s3.access_key", "")
viper.SetDefault("s3.secret_key", "")
viper.SetDefault("s3.bucket", "")
viper.SetDefault("s3.use_ssl", true)
viper.SetDefault("s3.region", "us-east-1")
viper.SetDefault("s3.domain", "")
viper.SetDefault("sensitive.enabled", true)
viper.SetDefault("sensitive.replace_str", "***")
viper.SetDefault("audit.enabled", false)
viper.SetDefault("audit.provider", "local")
viper.SetDefault("gorse.enabled", false)
viper.SetDefault("gorse.address", "http://localhost:8087")
viper.SetDefault("gorse.api_key", "")
viper.SetDefault("gorse.dashboard", "http://localhost:8088")
viper.SetDefault("gorse.import_password", "")
viper.SetDefault("gorse.embedding_api_key", "")
viper.SetDefault("gorse.embedding_url", "https://api.littlelan.cn/v1/embeddings")
viper.SetDefault("gorse.embedding_model", "BAAI/bge-m3")
viper.SetDefault("openai.enabled", true)
viper.SetDefault("openai.base_url", "https://api.littlelan.cn/")
viper.SetDefault("openai.api_key", "")
viper.SetDefault("openai.moderation_model", "qwen3.5-122b")
viper.SetDefault("openai.moderation_max_images_per_request", 1)
viper.SetDefault("openai.request_timeout", 30)
viper.SetDefault("openai.strict_moderation", false)
viper.SetDefault("email.enabled", false)
viper.SetDefault("email.host", "")
viper.SetDefault("email.port", 587)
viper.SetDefault("email.username", "")
viper.SetDefault("email.password", "")
viper.SetDefault("email.from_address", "")
viper.SetDefault("email.from_name", "Carrot BBS")
viper.SetDefault("email.use_tls", true)
viper.SetDefault("email.insecure_skip_verify", false)
viper.SetDefault("email.timeout", 15)
// ConversationCache 默认值
viper.SetDefault("conversation_cache.detail_ttl", "5m")
viper.SetDefault("conversation_cache.list_ttl", "60s")
viper.SetDefault("conversation_cache.participant_ttl", "5m")
viper.SetDefault("conversation_cache.unread_ttl", "30s")
viper.SetDefault("conversation_cache.message_detail_ttl", "30m")
viper.SetDefault("conversation_cache.message_list_ttl", "5m")
viper.SetDefault("conversation_cache.message_index_ttl", "30m")
viper.SetDefault("conversation_cache.message_count_ttl", "30m")
viper.SetDefault("conversation_cache.batch_interval", "5s")
viper.SetDefault("conversation_cache.batch_threshold", 100)
viper.SetDefault("conversation_cache.batch_max_size", 500)
viper.SetDefault("conversation_cache.buffer_max_size", 10000)
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Convert seconds to duration
cfg.JWT.AccessTokenExpire = time.Duration(viper.GetInt("jwt.access_token_expire")) * time.Second
cfg.JWT.RefreshTokenExpire = time.Duration(viper.GetInt("jwt.refresh_token_expire")) * time.Second
// 环境变量覆盖(显式处理敏感配置)
cfg.JWT.Secret = getEnvOrDefault("APP_JWT_SECRET", cfg.JWT.Secret)
cfg.Database.SQLite.Path = getEnvOrDefault("APP_DATABASE_SQLITE_PATH", cfg.Database.SQLite.Path)
cfg.Database.Postgres.Host = getEnvOrDefault("APP_DATABASE_POSTGRES_HOST", cfg.Database.Postgres.Host)
cfg.Database.Postgres.Port, _ = strconv.Atoi(getEnvOrDefault("APP_DATABASE_POSTGRES_PORT", fmt.Sprintf("%d", cfg.Database.Postgres.Port)))
cfg.Database.Postgres.User = getEnvOrDefault("APP_DATABASE_POSTGRES_USER", cfg.Database.Postgres.User)
cfg.Database.Postgres.Password = getEnvOrDefault("APP_DATABASE_POSTGRES_PASSWORD", cfg.Database.Postgres.Password)
cfg.Database.Postgres.DBName = getEnvOrDefault("APP_DATABASE_POSTGRES_DBNAME", cfg.Database.Postgres.DBName)
cfg.Database.LogLevel = getEnvOrDefault("APP_DATABASE_LOG_LEVEL", cfg.Database.LogLevel)
cfg.Database.SlowThresholdMs, _ = strconv.Atoi(getEnvOrDefault("APP_DATABASE_SLOW_THRESHOLD_MS", fmt.Sprintf("%d", cfg.Database.SlowThresholdMs)))
cfg.Database.IgnoreRecordNotFound, _ = strconv.ParseBool(getEnvOrDefault("APP_DATABASE_IGNORE_RECORD_NOT_FOUND", fmt.Sprintf("%t", cfg.Database.IgnoreRecordNotFound)))
cfg.Database.ParameterizedQueries, _ = strconv.ParseBool(getEnvOrDefault("APP_DATABASE_PARAMETERIZED_QUERIES", fmt.Sprintf("%t", cfg.Database.ParameterizedQueries)))
cfg.Redis.Redis.Host = getEnvOrDefault("APP_REDIS_REDIS_HOST", cfg.Redis.Redis.Host)
cfg.Redis.Redis.Port, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_REDIS_PORT", fmt.Sprintf("%d", cfg.Redis.Redis.Port)))
cfg.Redis.Redis.Password = getEnvOrDefault("APP_REDIS_REDIS_PASSWORD", cfg.Redis.Redis.Password)
cfg.Redis.Redis.DB, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_REDIS_DB", fmt.Sprintf("%d", cfg.Redis.Redis.DB)))
cfg.Redis.Miniredis.Host = getEnvOrDefault("APP_REDIS_MINIREDIS_HOST", cfg.Redis.Miniredis.Host)
cfg.Redis.Miniredis.Port, _ = strconv.Atoi(getEnvOrDefault("APP_REDIS_MINIREDIS_PORT", fmt.Sprintf("%d", cfg.Redis.Miniredis.Port)))
cfg.Redis.Type = getEnvOrDefault("APP_REDIS_TYPE", cfg.Redis.Type)
cfg.Cache.KeyPrefix = getEnvOrDefault("APP_CACHE_KEY_PREFIX", cfg.Cache.KeyPrefix)
cfg.Cache.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_CACHE_ENABLED", fmt.Sprintf("%t", cfg.Cache.Enabled)))
cfg.Cache.DisableFlushDB, _ = strconv.ParseBool(getEnvOrDefault("APP_CACHE_DISABLE_FLUSHDB", fmt.Sprintf("%t", cfg.Cache.DisableFlushDB)))
cfg.Cache.DefaultTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_DEFAULT_TTL", fmt.Sprintf("%d", cfg.Cache.DefaultTTL)))
cfg.Cache.NullTTL, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_NULL_TTL", fmt.Sprintf("%d", cfg.Cache.NullTTL)))
cfg.Cache.JitterRatio, _ = strconv.ParseFloat(getEnvOrDefault("APP_CACHE_JITTER_RATIO", fmt.Sprintf("%.2f", cfg.Cache.JitterRatio)), 64)
cfg.Cache.Modules.PostList, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_POST_LIST_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.PostList)))
cfg.Cache.Modules.Conversation, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_CONVERSATION_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.Conversation)))
cfg.Cache.Modules.UnreadCount, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_UNREAD_COUNT_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.UnreadCount)))
cfg.Cache.Modules.GroupMembers, _ = strconv.Atoi(getEnvOrDefault("APP_CACHE_MODULES_GROUP_MEMBERS_TTL", fmt.Sprintf("%d", cfg.Cache.Modules.GroupMembers)))
cfg.S3.Endpoint = getEnvOrDefault("APP_S3_ENDPOINT", cfg.S3.Endpoint)
cfg.S3.AccessKey = getEnvOrDefault("APP_S3_ACCESS_KEY", cfg.S3.AccessKey)
cfg.S3.SecretKey = getEnvOrDefault("APP_S3_SECRET_KEY", cfg.S3.SecretKey)
cfg.S3.Bucket = getEnvOrDefault("APP_S3_BUCKET", cfg.S3.Bucket)
cfg.S3.Domain = getEnvOrDefault("APP_S3_DOMAIN", cfg.S3.Domain)
cfg.Server.Host = getEnvOrDefault("APP_SERVER_HOST", cfg.Server.Host)
cfg.Server.Port, _ = strconv.Atoi(getEnvOrDefault("APP_SERVER_PORT", fmt.Sprintf("%d", cfg.Server.Port)))
cfg.Server.Mode = getEnvOrDefault("APP_SERVER_MODE", cfg.Server.Mode)
cfg.Gorse.Address = getEnvOrDefault("APP_GORSE_ADDRESS", cfg.Gorse.Address)
cfg.Gorse.APIKey = getEnvOrDefault("APP_GORSE_API_KEY", cfg.Gorse.APIKey)
cfg.Gorse.Dashboard = getEnvOrDefault("APP_GORSE_DASHBOARD", cfg.Gorse.Dashboard)
cfg.Gorse.ImportPassword = getEnvOrDefault("APP_GORSE_IMPORT_PASSWORD", cfg.Gorse.ImportPassword)
cfg.Gorse.EmbeddingAPIKey = getEnvOrDefault("APP_GORSE_EMBEDDING_API_KEY", cfg.Gorse.EmbeddingAPIKey)
cfg.Gorse.EmbeddingURL = getEnvOrDefault("APP_GORSE_EMBEDDING_URL", cfg.Gorse.EmbeddingURL)
cfg.Gorse.EmbeddingModel = getEnvOrDefault("APP_GORSE_EMBEDDING_MODEL", cfg.Gorse.EmbeddingModel)
cfg.OpenAI.BaseURL = getEnvOrDefault("APP_OPENAI_BASE_URL", cfg.OpenAI.BaseURL)
cfg.OpenAI.APIKey = getEnvOrDefault("APP_OPENAI_API_KEY", cfg.OpenAI.APIKey)
cfg.OpenAI.ModerationModel = getEnvOrDefault("APP_OPENAI_MODERATION_MODEL", cfg.OpenAI.ModerationModel)
cfg.OpenAI.ModerationMaxImagesPerRequest, _ = strconv.Atoi(getEnvOrDefault("APP_OPENAI_MODERATION_MAX_IMAGES_PER_REQUEST", fmt.Sprintf("%d", cfg.OpenAI.ModerationMaxImagesPerRequest)))
cfg.OpenAI.RequestTimeout, _ = strconv.Atoi(getEnvOrDefault("APP_OPENAI_REQUEST_TIMEOUT", fmt.Sprintf("%d", cfg.OpenAI.RequestTimeout)))
cfg.OpenAI.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_ENABLED", fmt.Sprintf("%t", cfg.OpenAI.Enabled)))
cfg.OpenAI.StrictModeration, _ = strconv.ParseBool(getEnvOrDefault("APP_OPENAI_STRICT_MODERATION", fmt.Sprintf("%t", cfg.OpenAI.StrictModeration)))
cfg.Email.Enabled, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_ENABLED", fmt.Sprintf("%t", cfg.Email.Enabled)))
cfg.Email.Host = getEnvOrDefault("APP_EMAIL_HOST", cfg.Email.Host)
cfg.Email.Port, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_PORT", fmt.Sprintf("%d", cfg.Email.Port)))
cfg.Email.Username = getEnvOrDefault("APP_EMAIL_USERNAME", cfg.Email.Username)
cfg.Email.Password = getEnvOrDefault("APP_EMAIL_PASSWORD", cfg.Email.Password)
cfg.Email.FromAddress = getEnvOrDefault("APP_EMAIL_FROM_ADDRESS", cfg.Email.FromAddress)
cfg.Email.FromName = getEnvOrDefault("APP_EMAIL_FROM_NAME", cfg.Email.FromName)
cfg.Email.UseTLS, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_USE_TLS", fmt.Sprintf("%t", cfg.Email.UseTLS)))
cfg.Email.InsecureSkipVerify, _ = strconv.ParseBool(getEnvOrDefault("APP_EMAIL_INSECURE_SKIP_VERIFY", fmt.Sprintf("%t", cfg.Email.InsecureSkipVerify)))
cfg.Email.Timeout, _ = strconv.Atoi(getEnvOrDefault("APP_EMAIL_TIMEOUT", fmt.Sprintf("%d", cfg.Email.Timeout)))
return &cfg, nil
}
// getEnvOrDefault 获取环境变量值,如果未设置则返回默认值
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// NewRedis 创建Redis客户端真实Redis
func NewRedis(cfg *RedisConfig) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: cfg.Redis.Addr(),
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.PoolSize,
})
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
return client, nil
}
// NewS3 创建S3客户端
func NewS3(cfg *S3Config) (*minio.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.UseSSL,
})
if err != nil {
return nil, fmt.Errorf("failed to create S3 client: %w", err)
}
exists, err := client.BucketExists(ctx, cfg.Bucket)
if err != nil {
return nil, fmt.Errorf("failed to check bucket: %w", err)
}
if !exists {
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
Region: cfg.Region,
}); err != nil {
return nil, fmt.Errorf("failed to create bucket: %w", err)
}
}
return client, nil
}