- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
328 lines
9.5 KiB
Go
328 lines
9.5 KiB
Go
package wire
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
|
||
"with_you/internal/cache"
|
||
"with_you/internal/config"
|
||
"with_you/internal/database"
|
||
"with_you/internal/pkg/crypto"
|
||
"with_you/internal/pkg/email"
|
||
"with_you/internal/pkg/hook"
|
||
"with_you/internal/pkg/jpush"
|
||
"with_you/internal/pkg/openai"
|
||
"with_you/internal/pkg/redis"
|
||
"with_you/internal/pkg/s3"
|
||
"with_you/internal/pkg/tencent"
|
||
"with_you/internal/pkg/ws"
|
||
"with_you/internal/repository"
|
||
"with_you/internal/service"
|
||
|
||
"github.com/google/wire"
|
||
redisPkg "github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// InfrastructureSet 基础设施 Provider Set
|
||
var InfrastructureSet = wire.NewSet(
|
||
// 配置
|
||
ProvideConfig,
|
||
|
||
// 日志
|
||
ProvideLogger,
|
||
|
||
// 数据库
|
||
ProvideDB,
|
||
|
||
// 事务管理器
|
||
ProvideTransactionManager,
|
||
|
||
// Redis 和缓存
|
||
ProvideRedisClient,
|
||
ProvideCache,
|
||
|
||
// 钩子系统
|
||
ProvideHookManager,
|
||
ProvideBuiltinHooks,
|
||
ProvideModerationHooks,
|
||
|
||
// WebSocket MessagePublisher
|
||
ProvideWSMessagePublisher,
|
||
|
||
// Seq 预分配管理器
|
||
ProvideSeqBufferManager,
|
||
|
||
// 推送 Worker
|
||
ProvidePushWorker,
|
||
|
||
// 外部服务客户端
|
||
ProvideOpenAIClient,
|
||
ProvideTencentClient,
|
||
ProvideEmailClient,
|
||
ProvideS3Client,
|
||
ProvideJPushClient,
|
||
|
||
// 消息加密器
|
||
ProvideMessageEncryptor,
|
||
)
|
||
|
||
// ProvideConfig 加载配置
|
||
func ProvideConfig() (*config.Config, error) {
|
||
return config.Load("configs/config.yaml")
|
||
}
|
||
|
||
// ProvideDB 提供数据库连接
|
||
func ProvideDB(cfg *config.Config) (*gorm.DB, error) {
|
||
return database.NewDB(&cfg.Database)
|
||
}
|
||
|
||
// ProvideRedisClient 提供 Redis 客户端
|
||
func ProvideRedisClient(cfg *config.Config) *redis.Client {
|
||
client, err := redis.New(&cfg.Redis)
|
||
if err != nil {
|
||
zap.L().Warn("Failed to connect to Redis, falling back to memory cache",
|
||
zap.Error(err),
|
||
)
|
||
return nil
|
||
}
|
||
return client
|
||
}
|
||
|
||
// ProvideCache 提供缓存实例
|
||
func ProvideCache(cfg *config.Config, redisClient *redis.Client) cache.Cache {
|
||
cache.Configure(cache.Settings{
|
||
Enabled: cfg.Cache.Enabled,
|
||
KeyPrefix: cfg.Cache.KeyPrefix,
|
||
DefaultTTL: time.Duration(cfg.Cache.DefaultTTL) * time.Second,
|
||
NullTTL: time.Duration(cfg.Cache.NullTTL) * time.Second,
|
||
JitterRatio: cfg.Cache.JitterRatio,
|
||
PostListTTL: time.Duration(cfg.Cache.Modules.PostList) * time.Second,
|
||
ConversationTTL: time.Duration(cfg.Cache.Modules.Conversation) * time.Second,
|
||
UnreadCountTTL: time.Duration(cfg.Cache.Modules.UnreadCount) * time.Second,
|
||
GroupMembersTTL: time.Duration(cfg.Cache.Modules.GroupMembers) * time.Second,
|
||
DisableFlushDB: cfg.Cache.DisableFlushDB,
|
||
})
|
||
|
||
if cfg.Cache.Local.Enabled {
|
||
layeredCache := cache.NewLayeredCache(redisClient, &cache.LayeredCacheOptions{
|
||
LocalSize: cfg.Cache.Local.Size,
|
||
LocalBuckets: cfg.Cache.Local.Buckets,
|
||
LocalDefaultTTL: time.Duration(cfg.Cache.Local.DefaultTTL) * time.Second,
|
||
KeyPrefix: cfg.Cache.KeyPrefix,
|
||
Enabled: cfg.Cache.Enabled,
|
||
})
|
||
zap.L().Info("Initialized layered cache (local LRU + Redis)")
|
||
return layeredCache
|
||
}
|
||
|
||
if redisClient == nil {
|
||
zap.L().Warn("Redis client is nil, cache will not be available")
|
||
return nil
|
||
}
|
||
|
||
cacheInstance := cache.NewRedisCache(redisClient)
|
||
zap.L().Info("Initialized Redis cache via dependency injection")
|
||
return cacheInstance
|
||
}
|
||
|
||
// ProvideHookManager 提供钩子管理器
|
||
func ProvideHookManager() *hook.Manager {
|
||
return hook.NewManager(&hook.ManagerOptions{
|
||
MaxConcurrentAsync: 100,
|
||
Enabled: true,
|
||
})
|
||
}
|
||
|
||
// ProvideBuiltinHooks 注册内置钩子
|
||
func ProvideBuiltinHooks(manager *hook.Manager) *hook.BuiltinHooks {
|
||
builtin := hook.NewBuiltinHooks(manager)
|
||
builtin.RegisterAll()
|
||
zap.L().Info("Registered builtin hooks")
|
||
return builtin
|
||
}
|
||
|
||
// ProvideModerationHooks 注册审核钩子
|
||
func ProvideModerationHooks(
|
||
manager *hook.Manager,
|
||
postAIService service.PostAIService,
|
||
postRepo repository.PostRepository,
|
||
commentRepo repository.CommentRepository,
|
||
userRepo repository.UserRepository,
|
||
sensitiveService service.SensitiveService,
|
||
cfg *config.Config,
|
||
) *hook.ModerationHooks {
|
||
strictMode := cfg.OpenAI.StrictModeration
|
||
|
||
moderationHooks := hook.NewModerationHooks(
|
||
postAIService,
|
||
sensitiveService,
|
||
postRepo,
|
||
commentRepo,
|
||
userRepo,
|
||
strictMode,
|
||
"***",
|
||
)
|
||
moderationHooks.RegisterAll(manager)
|
||
zap.L().Info("Registered moderation hooks",
|
||
zap.Bool("strict_mode", strictMode),
|
||
)
|
||
return moderationHooks
|
||
}
|
||
|
||
// ProvideWSMessagePublisher 提供 WebSocket 消息发布器
|
||
// standalone 模式返回 *Hub,cluster 模式返回 *Bus(组合 Hub + Redis Pub/Sub)
|
||
func ProvideWSMessagePublisher(cfg *config.Config, redisClient *redis.Client) ws.MessagePublisher {
|
||
hub := ws.NewHub()
|
||
|
||
if cfg.WebSocket.Mode == "cluster" && redisClient != nil {
|
||
bus := ws.NewBus(hub, redisClient.GetClient(), &ws.WSClusterConfig{
|
||
InstanceID: cfg.WebSocket.Cluster.InstanceID,
|
||
MsgChannel: cfg.WebSocket.Cluster.MsgChannel,
|
||
OnlineTTL: cfg.WebSocket.Cluster.OnlineTTL,
|
||
HeartbeatInterval: cfg.WebSocket.Cluster.HeartbeatInterval,
|
||
})
|
||
if err := bus.Start(); err != nil {
|
||
zap.L().Error("Failed to start WebSocket Bus, falling back to standalone mode",
|
||
zap.Error(err),
|
||
)
|
||
return hub
|
||
}
|
||
zap.L().Info("WebSocket running in cluster mode",
|
||
zap.String("instance_id", bus.InstanceID()),
|
||
)
|
||
return bus
|
||
}
|
||
|
||
zap.L().Info("WebSocket running in standalone mode")
|
||
return hub
|
||
}
|
||
|
||
// ProvideOpenAIClient 提供 OpenAI 客户端
|
||
func ProvideOpenAIClient(cfg *config.Config) openai.Client {
|
||
return openai.NewClient(openai.ConfigFromAppConfig(&cfg.OpenAI))
|
||
}
|
||
|
||
// ProvideTencentClient 提供腾讯云TMS客户端
|
||
func ProvideTencentClient(cfg *config.Config) tencent.Client {
|
||
return tencent.NewClient(tencent.Config{
|
||
Enabled: cfg.TencentTMS.Enabled,
|
||
SecretID: cfg.TencentTMS.SecretID,
|
||
SecretKey: cfg.TencentTMS.SecretKey,
|
||
Region: cfg.TencentTMS.Region,
|
||
BizType: cfg.TencentTMS.BizType,
|
||
Timeout: cfg.TencentTMS.Timeout,
|
||
})
|
||
}
|
||
|
||
// ProvideEmailClient 提供邮件客户端
|
||
func ProvideEmailClient(cfg *config.Config) email.Client {
|
||
return email.NewClient(email.ConfigFromAppConfig(&cfg.Email))
|
||
}
|
||
|
||
// ProvideS3Client 提供 S3 客户端
|
||
func ProvideS3Client(cfg *config.Config) (*s3.Client, error) {
|
||
return s3.New(&cfg.S3)
|
||
}
|
||
|
||
// ProvideJPushClient 提供极光推送客户端
|
||
func ProvideJPushClient(cfg *config.Config, logger *zap.Logger) *jpush.Client {
|
||
if !cfg.JPush.Enabled {
|
||
logger.Info("JPush is disabled")
|
||
return nil
|
||
}
|
||
if cfg.JPush.AppKey == "" || cfg.JPush.MasterSecret == "" {
|
||
logger.Warn("JPush is enabled but app_key or master_secret is not configured")
|
||
return nil
|
||
}
|
||
client := jpush.NewClient(cfg.JPush.AppKey, cfg.JPush.MasterSecret, cfg.JPush.Production, logger)
|
||
logger.Info("JPush client initialized",
|
||
zap.Bool("production", cfg.JPush.Production),
|
||
)
|
||
return client
|
||
}
|
||
|
||
// ProvideTransactionManager 提供事务管理器
|
||
func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
|
||
return repository.NewTransactionManager(db)
|
||
}
|
||
|
||
// ProvideMessageEncryptor 提供消息加密器
|
||
// 加密启用时密钥必须是 32 字节(AES-256),否则视为致命配置错误,启动期 fail-fast。
|
||
func ProvideMessageEncryptor(cfg *config.Config) error {
|
||
if !cfg.Encryption.Enabled {
|
||
zap.L().Info("Message encryption is disabled")
|
||
return nil
|
||
}
|
||
|
||
if len(cfg.Encryption.Key) != 32 {
|
||
// 致命安全配置:不允许降级为「加密已启用但实际不加密」
|
||
return fmt.Errorf("encryption is enabled but key must be 32 bytes for AES-256, got %d", len(cfg.Encryption.Key))
|
||
}
|
||
|
||
if err := crypto.InitMessageEncryptor(cfg.Encryption.Key, cfg.Encryption.KeyVersion); err != nil {
|
||
return fmt.Errorf("failed to initialize message encryptor: %w", err)
|
||
}
|
||
|
||
zap.L().Info("Message encryption initialized successfully",
|
||
zap.Int("key_version", cfg.Encryption.KeyVersion),
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// ProvideLogger 提供 Zap Logger
|
||
func ProvideLogger() *zap.Logger {
|
||
logger, err := zap.NewProduction()
|
||
if err != nil {
|
||
panic("Failed to create logger: " + err.Error())
|
||
}
|
||
// 设置为全局 logger,以便在其他地方使用 zap.L()
|
||
zap.ReplaceGlobals(logger)
|
||
return logger
|
||
}
|
||
|
||
// ProvideSeqBufferManager 提供 Seq 预分配管理器
|
||
func ProvideSeqBufferManager(cfg *config.Config, redisClient *redis.Client, cacheBackend cache.Cache) *cache.SeqBufferManager {
|
||
seqCfg := cache.SeqBufferConfig{
|
||
Enabled: cfg.SeqBuffer.Enabled,
|
||
PrivateBufferSize: cfg.SeqBuffer.PrivateBufferSize,
|
||
GroupBufferSize: cfg.SeqBuffer.GroupBufferSize,
|
||
LockTimeoutMs: cfg.SeqBuffer.LockTimeoutMs,
|
||
MaxRetries: cfg.SeqBuffer.MaxRetries,
|
||
}
|
||
if !seqCfg.Enabled {
|
||
return cache.NewSeqBufferManager(nil, seqCfg, nil, nil)
|
||
}
|
||
return cache.NewSeqBufferManagerFromRedisPkg(redisClient, seqCfg, nil, nil)
|
||
}
|
||
|
||
// ProvidePushWorker 提供推送 Worker(Redis Stream 异步推送)
|
||
func ProvidePushWorker(
|
||
cfg *config.Config,
|
||
redisClient *redis.Client,
|
||
publisher ws.MessagePublisher,
|
||
pushSvc service.PushService,
|
||
msgRepo repository.MessageRepository,
|
||
userRepo repository.UserRepository,
|
||
) *service.PushWorker {
|
||
pushCfg := service.PushWorkerConfig{
|
||
Enabled: cfg.PushWorker.Enabled,
|
||
Stream: cfg.PushWorker.Stream,
|
||
Group: cfg.PushWorker.Group,
|
||
BatchSize: cfg.PushWorker.BatchSize,
|
||
PollTimeoutMs: cfg.PushWorker.PollTimeoutMs,
|
||
MaxRetries: cfg.PushWorker.MaxRetries,
|
||
MaxStreamLen: cfg.PushWorker.MaxStreamLen,
|
||
IdleTimeoutMs: cfg.PushWorker.IdleTimeoutMs,
|
||
}
|
||
if !pushCfg.Enabled {
|
||
return service.NewPushWorker(nil, nil, nil, nil, nil, pushCfg)
|
||
}
|
||
var rdb *redisPkg.Client
|
||
if redisClient != nil {
|
||
rdb = redisClient.GetClient()
|
||
}
|
||
return service.NewPushWorker(rdb, publisher, pushSvc, msgRepo, userRepo, pushCfg)
|
||
}
|