refactor: introduce Wire dependency injection and interface-based architecture

- Add Google Wire for compile-time dependency injection
- Replace concrete service types with interfaces across handlers
- Remove global state (DB, cache) in favor of constructor injection
- Split monolithic files into focused modules:
  - config: separate files for each config domain
  - dto: converters split by domain (user, post, message, group)
  - cache: separate metrics.go and redis_cache.go
- Introduce unified apperrors package with string-based error codes
- Add transaction support with context-aware repository methods
- Upgrade golang.org/x/crypto from 0.17.0 to 0.26.0
This commit is contained in:
2026-03-13 09:38:18 +08:00
parent 1216423350
commit cf36b1350d
49 changed files with 2571 additions and 2218 deletions

70
internal/config/cache.go Normal file
View File

@@ -0,0 +1,70 @@
package config
import "time"
// 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
}

View File

@@ -1,19 +1,16 @@
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"
)
// Config 主配置结构体
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
@@ -30,217 +27,7 @@ type Config struct {
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
}
// Load 加载配置文件
func Load(configPath string) (*Config, error) {
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
@@ -426,49 +213,3 @@ func getEnvOrDefault(key, defaultValue string) string {
}
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
}

View File

@@ -0,0 +1,39 @@
package config
import "fmt"
// DatabaseConfig 数据库配置
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"`
}
// SQLiteConfig SQLite 配置
type SQLiteConfig struct {
Path string `mapstructure:"path"`
}
// PostgresConfig PostgreSQL 配置
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"`
}
// DSN 返回 PostgreSQL 连接字符串
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,
)
}

15
internal/config/email.go Normal file
View File

@@ -0,0 +1,15 @@
package config
// EmailConfig 邮件配置
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"`
}

View File

@@ -0,0 +1,24 @@
package config
// GorseConfig Gorse 推荐系统配置
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"`
}
// OpenAIConfig OpenAI 配置
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"`
}

10
internal/config/jwt.go Normal file
View File

@@ -0,0 +1,10 @@
package config
import "time"
// JWTConfig JWT 配置
type JWTConfig struct {
Secret string `mapstructure:"secret"`
AccessTokenExpire time.Duration `mapstructure:"access_token_expire"`
RefreshTokenExpire time.Duration `mapstructure:"refresh_token_expire"`
}

71
internal/config/redis.go Normal file
View File

@@ -0,0 +1,71 @@
package config
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
// RedisConfig Redis 配置
type RedisConfig struct {
Type string `mapstructure:"type"`
Redis RedisServerConfig `mapstructure:"redis"`
Miniredis MiniredisConfig `mapstructure:"miniredis"`
PoolSize int `mapstructure:"pool_size"`
}
// CacheConfig 缓存配置
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"`
}
// CacheModuleTTL 缓存模块 TTL 配置
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"`
}
// RedisServerConfig Redis 服务器配置
type RedisServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// Addr 返回 Redis 服务器地址
func (r RedisServerConfig) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
// MiniredisConfig Miniredis 配置(用于测试)
type MiniredisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
// 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
}

27
internal/config/server.go Normal file
View File

@@ -0,0 +1,27 @@
package config
// ServerConfig 服务器配置
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `mapstructure:"level"`
Encoding string `mapstructure:"encoding"`
OutputPaths []string `mapstructure:"output_paths"`
}
// RateLimitConfig 限流配置
type RateLimitConfig struct {
Enabled bool `mapstructure:"enabled"`
RequestsPerMinute int `mapstructure:"requests_per_minute"`
}
// UploadConfig 上传配置
type UploadConfig struct {
MaxFileSize int64 `mapstructure:"max_file_size"`
AllowedTypes []string `mapstructure:"allowed_types"`
}

View File

@@ -0,0 +1,50 @@
package config
import (
"context"
"fmt"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// S3Config S3 存储配置
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
}
// 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
}