refactor: 重构服务层和仓库层
This commit is contained in:
@@ -47,15 +47,23 @@ type DatabaseConfig struct {
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
|
||||
}
|
||||
|
||||
// RedisConfig Redis配置
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database int `mapstructure:"database"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database int `mapstructure:"database"`
|
||||
PoolSize int `mapstructure:"pool_size"` // 连接池大小
|
||||
MinIdleConns int `mapstructure:"min_idle_conns"` // 最小空闲连接数
|
||||
MaxRetries int `mapstructure:"max_retries"` // 最大重试次数
|
||||
DialTimeout time.Duration `mapstructure:"dial_timeout"` // 连接超时
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 读取超时
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 写入超时
|
||||
PoolTimeout time.Duration `mapstructure:"pool_timeout"` // 连接池超时
|
||||
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
|
||||
}
|
||||
|
||||
// RustFSConfig RustFS对象存储配置 (S3兼容)
|
||||
@@ -159,12 +167,20 @@ func setDefaults() {
|
||||
viper.SetDefault("database.max_idle_conns", 10)
|
||||
viper.SetDefault("database.max_open_conns", 100)
|
||||
viper.SetDefault("database.conn_max_lifetime", "1h")
|
||||
viper.SetDefault("database.conn_max_idle_time", "10m")
|
||||
|
||||
// Redis默认配置
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", 6379)
|
||||
viper.SetDefault("redis.database", 0)
|
||||
viper.SetDefault("redis.pool_size", 10)
|
||||
viper.SetDefault("redis.min_idle_conns", 5)
|
||||
viper.SetDefault("redis.max_retries", 3)
|
||||
viper.SetDefault("redis.dial_timeout", "5s")
|
||||
viper.SetDefault("redis.read_timeout", "3s")
|
||||
viper.SetDefault("redis.write_timeout", "3s")
|
||||
viper.SetDefault("redis.pool_timeout", "4s")
|
||||
viper.SetDefault("redis.conn_max_idle_time", "30m")
|
||||
|
||||
// RustFS默认配置
|
||||
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
|
||||
@@ -219,12 +235,24 @@ func setupEnvMappings() {
|
||||
viper.BindEnv("database.database", "DATABASE_NAME")
|
||||
viper.BindEnv("database.ssl_mode", "DATABASE_SSL_MODE")
|
||||
viper.BindEnv("database.timezone", "DATABASE_TIMEZONE")
|
||||
viper.BindEnv("database.max_idle_conns", "DATABASE_MAX_IDLE_CONNS")
|
||||
viper.BindEnv("database.max_open_conns", "DATABASE_MAX_OPEN_CONNS")
|
||||
viper.BindEnv("database.conn_max_lifetime", "DATABASE_CONN_MAX_LIFETIME")
|
||||
viper.BindEnv("database.conn_max_idle_time", "DATABASE_CONN_MAX_IDLE_TIME")
|
||||
|
||||
// Redis配置
|
||||
viper.BindEnv("redis.host", "REDIS_HOST")
|
||||
viper.BindEnv("redis.port", "REDIS_PORT")
|
||||
viper.BindEnv("redis.password", "REDIS_PASSWORD")
|
||||
viper.BindEnv("redis.database", "REDIS_DATABASE")
|
||||
viper.BindEnv("redis.pool_size", "REDIS_POOL_SIZE")
|
||||
viper.BindEnv("redis.min_idle_conns", "REDIS_MIN_IDLE_CONNS")
|
||||
viper.BindEnv("redis.max_retries", "REDIS_MAX_RETRIES")
|
||||
viper.BindEnv("redis.dial_timeout", "REDIS_DIAL_TIMEOUT")
|
||||
viper.BindEnv("redis.read_timeout", "REDIS_READ_TIMEOUT")
|
||||
viper.BindEnv("redis.write_timeout", "REDIS_WRITE_TIMEOUT")
|
||||
viper.BindEnv("redis.pool_timeout", "REDIS_POOL_TIMEOUT")
|
||||
viper.BindEnv("redis.conn_max_idle_time", "REDIS_CONN_MAX_IDLE_TIME")
|
||||
|
||||
// RustFS配置
|
||||
viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT")
|
||||
@@ -287,13 +315,61 @@ func overrideFromEnv(config *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Redis池大小
|
||||
if connMaxIdleTime := os.Getenv("DATABASE_CONN_MAX_IDLE_TIME"); connMaxIdleTime != "" {
|
||||
if val, err := time.ParseDuration(connMaxIdleTime); err == nil {
|
||||
config.Database.ConnMaxIdleTime = val
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Redis连接池配置
|
||||
if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" {
|
||||
if val, err := strconv.Atoi(poolSize); err == nil {
|
||||
config.Redis.PoolSize = val
|
||||
}
|
||||
}
|
||||
|
||||
if minIdleConns := os.Getenv("REDIS_MIN_IDLE_CONNS"); minIdleConns != "" {
|
||||
if val, err := strconv.Atoi(minIdleConns); err == nil {
|
||||
config.Redis.MinIdleConns = val
|
||||
}
|
||||
}
|
||||
|
||||
if maxRetries := os.Getenv("REDIS_MAX_RETRIES"); maxRetries != "" {
|
||||
if val, err := strconv.Atoi(maxRetries); err == nil {
|
||||
config.Redis.MaxRetries = val
|
||||
}
|
||||
}
|
||||
|
||||
if dialTimeout := os.Getenv("REDIS_DIAL_TIMEOUT"); dialTimeout != "" {
|
||||
if val, err := time.ParseDuration(dialTimeout); err == nil {
|
||||
config.Redis.DialTimeout = val
|
||||
}
|
||||
}
|
||||
|
||||
if readTimeout := os.Getenv("REDIS_READ_TIMEOUT"); readTimeout != "" {
|
||||
if val, err := time.ParseDuration(readTimeout); err == nil {
|
||||
config.Redis.ReadTimeout = val
|
||||
}
|
||||
}
|
||||
|
||||
if writeTimeout := os.Getenv("REDIS_WRITE_TIMEOUT"); writeTimeout != "" {
|
||||
if val, err := time.ParseDuration(writeTimeout); err == nil {
|
||||
config.Redis.WriteTimeout = val
|
||||
}
|
||||
}
|
||||
|
||||
if poolTimeout := os.Getenv("REDIS_POOL_TIMEOUT"); poolTimeout != "" {
|
||||
if val, err := time.ParseDuration(poolTimeout); err == nil {
|
||||
config.Redis.PoolTimeout = val
|
||||
}
|
||||
}
|
||||
|
||||
if connMaxIdleTime := os.Getenv("REDIS_CONN_MAX_IDLE_TIME"); connMaxIdleTime != "" {
|
||||
if val, err := time.ParseDuration(connMaxIdleTime); err == nil {
|
||||
config.Redis.ConnMaxIdleTime = val
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件上传配置
|
||||
if maxSize := os.Getenv("UPLOAD_MAX_SIZE"); maxSize != "" {
|
||||
if val, err := strconv.ParseInt(maxSize, 10, 64); err == nil {
|
||||
|
||||
@@ -296,6 +296,11 @@ func (b *CacheKeyBuilder) Texture(textureID int64) string {
|
||||
return fmt.Sprintf("%stexture:id:%d", b.prefix, textureID)
|
||||
}
|
||||
|
||||
// TextureByHash 构建材质hash缓存键
|
||||
func (b *CacheKeyBuilder) TextureByHash(hash string) string {
|
||||
return fmt.Sprintf("%stexture:hash:%s", b.prefix, hash)
|
||||
}
|
||||
|
||||
// TextureList 构建材质列表缓存键
|
||||
func (b *CacheKeyBuilder) TextureList(userID int64, page int) string {
|
||||
return fmt.Sprintf("%stexture:user:%d:page:%d", b.prefix, userID, page)
|
||||
|
||||
@@ -69,10 +69,15 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
connMaxLifetime = 1 * time.Hour
|
||||
}
|
||||
|
||||
connMaxIdleTime := cfg.ConnMaxIdleTime
|
||||
if connMaxIdleTime <= 0 {
|
||||
connMaxIdleTime = 10 * time.Minute
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(maxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns)
|
||||
sqlDB.SetConnMaxLifetime(connMaxLifetime)
|
||||
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
|
||||
sqlDB.SetConnMaxIdleTime(connMaxIdleTime)
|
||||
|
||||
// 测试连接
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
|
||||
@@ -20,15 +20,60 @@ type Client struct {
|
||||
|
||||
// New 创建Redis客户端
|
||||
func New(cfg config.RedisConfig, logger *zap.Logger) (*Client, error) {
|
||||
// 设置默认值
|
||||
poolSize := cfg.PoolSize
|
||||
if poolSize <= 0 {
|
||||
poolSize = 10
|
||||
}
|
||||
|
||||
minIdleConns := cfg.MinIdleConns
|
||||
if minIdleConns <= 0 {
|
||||
minIdleConns = 5
|
||||
}
|
||||
|
||||
maxRetries := cfg.MaxRetries
|
||||
if maxRetries <= 0 {
|
||||
maxRetries = 3
|
||||
}
|
||||
|
||||
dialTimeout := cfg.DialTimeout
|
||||
if dialTimeout <= 0 {
|
||||
dialTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
readTimeout := cfg.ReadTimeout
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = 3 * time.Second
|
||||
}
|
||||
|
||||
writeTimeout := cfg.WriteTimeout
|
||||
if writeTimeout <= 0 {
|
||||
writeTimeout = 3 * time.Second
|
||||
}
|
||||
|
||||
poolTimeout := cfg.PoolTimeout
|
||||
if poolTimeout <= 0 {
|
||||
poolTimeout = 4 * time.Second
|
||||
}
|
||||
|
||||
connMaxIdleTime := cfg.ConnMaxIdleTime
|
||||
if connMaxIdleTime <= 0 {
|
||||
connMaxIdleTime = 30 * time.Minute
|
||||
}
|
||||
|
||||
// 创建Redis客户端
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.Database,
|
||||
PoolSize: cfg.PoolSize,
|
||||
DialTimeout: 5 * time.Second,
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
Password: cfg.Password,
|
||||
DB: cfg.Database,
|
||||
PoolSize: poolSize,
|
||||
MinIdleConns: minIdleConns,
|
||||
MaxRetries: maxRetries,
|
||||
DialTimeout: dialTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
PoolTimeout: poolTimeout,
|
||||
ConnMaxIdleTime: connMaxIdleTime,
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
|
||||
@@ -3,6 +3,9 @@ package storage
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"carrotskin/pkg/config"
|
||||
@@ -137,3 +140,64 @@ func (s *StorageClient) BuildFileURL(bucketName, objectName string) string {
|
||||
func (s *StorageClient) GetPublicURL() string {
|
||||
return s.publicURL
|
||||
}
|
||||
|
||||
// ObjectInfo 对象信息
|
||||
type ObjectInfo struct {
|
||||
Size int64
|
||||
LastModified time.Time
|
||||
ContentType string
|
||||
ETag string
|
||||
}
|
||||
|
||||
// GetObject 获取对象内容和信息
|
||||
func (s *StorageClient) GetObject(ctx context.Context, bucketName, objectName string) (io.ReadCloser, *ObjectInfo, error) {
|
||||
obj, err := s.client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("获取对象失败: %w", err)
|
||||
}
|
||||
|
||||
stat, err := obj.Stat()
|
||||
if err != nil {
|
||||
obj.Close()
|
||||
return nil, nil, fmt.Errorf("获取对象信息失败: %w", err)
|
||||
}
|
||||
|
||||
info := &ObjectInfo{
|
||||
Size: stat.Size,
|
||||
LastModified: stat.LastModified,
|
||||
ContentType: stat.ContentType,
|
||||
ETag: stat.ETag,
|
||||
}
|
||||
|
||||
return obj, info, nil
|
||||
}
|
||||
|
||||
// ParseFileURL 从文件URL中解析出bucket和objectName
|
||||
// URL格式: {publicURL}/{bucket}/{objectName}
|
||||
func (s *StorageClient) ParseFileURL(fileURL string) (bucket, objectName string, err error) {
|
||||
// 移除 publicURL 前缀
|
||||
if !strings.HasPrefix(fileURL, s.publicURL) {
|
||||
return "", "", fmt.Errorf("URL格式不正确,必须以 %s 开头", s.publicURL)
|
||||
}
|
||||
|
||||
// 移除 publicURL 前缀和开头的 /
|
||||
path := strings.TrimPrefix(fileURL, s.publicURL)
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
|
||||
// 解析路径
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
return "", "", fmt.Errorf("URL格式不正确,无法解析bucket和objectName")
|
||||
}
|
||||
|
||||
bucket = parts[0]
|
||||
objectName = parts[1]
|
||||
|
||||
// URL解码 objectName
|
||||
decoded, err := url.PathUnescape(objectName)
|
||||
if err == nil {
|
||||
objectName = decoded
|
||||
}
|
||||
|
||||
return bucket, objectName, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user