package service import ( "carrotskin/pkg/storage" "context" "fmt" "path/filepath" "strings" "time" ) // FileType 文件类型枚举 type FileType string const ( FileTypeAvatar FileType = "avatar" FileTypeTexture FileType = "texture" ) // UploadConfig 上传配置 type UploadConfig struct { AllowedExts map[string]bool // 允许的文件扩展名 MinSize int64 // 最小文件大小(字节) MaxSize int64 // 最大文件大小(字节) Expires time.Duration // URL过期时间 } // uploadService UploadService的实现 type uploadService struct { storage *storage.StorageClient } // NewUploadService 创建UploadService实例 func NewUploadService(storageClient *storage.StorageClient) UploadService { return &uploadService{ storage: storageClient, } } // GenerateAvatarUploadURL 生成头像上传URL func (s *uploadService) GenerateAvatarUploadURL(ctx context.Context, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error) { // 1. 验证文件名 if err := ValidateFileName(fileName, FileTypeAvatar); err != nil { return nil, err } // 2. 获取上传配置 uploadConfig := GetUploadConfig(FileTypeAvatar) // 3. 获取存储桶名称 bucketName, err := s.storage.GetBucket("avatars") if err != nil { return nil, fmt.Errorf("获取存储桶失败: %w", err) } // 4. 生成对象名称(路径) // 格式: user_{userId}/timestamp_{originalFileName} timestamp := time.Now().Format("20060102150405") objectName := fmt.Sprintf("user_%d/%s_%s", userID, timestamp, fileName) // 5. 生成预签名POST URL (使用存储客户端内置的 PublicURL) result, err := s.storage.GeneratePresignedPostURL( ctx, bucketName, objectName, uploadConfig.MinSize, uploadConfig.MaxSize, uploadConfig.Expires, ) if err != nil { return nil, fmt.Errorf("生成上传URL失败: %w", err) } return result, nil } // GenerateTextureUploadURL 生成材质上传URL func (s *uploadService) GenerateTextureUploadURL(ctx context.Context, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error) { // 1. 验证文件名 if err := ValidateFileName(fileName, FileTypeTexture); err != nil { return nil, err } // 2. 验证材质类型 if textureType != "SKIN" && textureType != "CAPE" { return nil, fmt.Errorf("无效的材质类型: %s", textureType) } // 3. 获取上传配置 uploadConfig := GetUploadConfig(FileTypeTexture) // 4. 获取存储桶名称 bucketName, err := s.storage.GetBucket("textures") if err != nil { return nil, fmt.Errorf("获取存储桶失败: %w", err) } // 5. 生成对象名称(路径) // 格式: user_{userId}/{textureType}/timestamp_{originalFileName} timestamp := time.Now().Format("20060102150405") textureTypeFolder := strings.ToLower(textureType) objectName := fmt.Sprintf("user_%d/%s/%s_%s", userID, textureTypeFolder, timestamp, fileName) // 6. 生成预签名POST URL (使用存储客户端内置的 PublicURL) result, err := s.storage.GeneratePresignedPostURL( ctx, bucketName, objectName, uploadConfig.MinSize, uploadConfig.MaxSize, uploadConfig.Expires, ) if err != nil { return nil, fmt.Errorf("生成上传URL失败: %w", err) } return result, nil } // GetUploadConfig 根据文件类型获取上传配置 func GetUploadConfig(fileType FileType) *UploadConfig { switch fileType { case FileTypeAvatar: return &UploadConfig{ AllowedExts: map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true, }, MinSize: 512, // 512B MaxSize: 5 * 1024 * 1024, // 5MB Expires: 15 * time.Minute, } case FileTypeTexture: return &UploadConfig{ AllowedExts: map[string]bool{ ".png": true, }, MinSize: 512, // 512B MaxSize: 10 * 1024 * 1024, // 10MB Expires: 15 * time.Minute, } default: return nil } } // ValidateFileName 验证文件名 func ValidateFileName(fileName string, fileType FileType) error { if fileName == "" { return fmt.Errorf("文件名不能为空") } uploadConfig := GetUploadConfig(fileType) if uploadConfig == nil { return fmt.Errorf("不支持的文件类型") } ext := strings.ToLower(filepath.Ext(fileName)) if !uploadConfig.AllowedExts[ext] { return fmt.Errorf("不支持的文件格式: %s", ext) } return nil }