- Refactored AuthHandler, UserHandler, TextureHandler, ProfileHandler, CaptchaHandler, and YggdrasilHandler to use dependency injection. - Removed direct instantiation of services and repositories within handlers, replacing them with constructor injection. - Updated the container to initialize service instances and provide them to handlers. - Enhanced code structure for better testability and adherence to Go best practices.
172 lines
5.1 KiB
Go
172 lines
5.1 KiB
Go
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过期时间
|
||
}
|
||
|
||
// 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: 1024, // 1KB
|
||
MaxSize: 5 * 1024 * 1024, // 5MB
|
||
Expires: 15 * time.Minute,
|
||
}
|
||
case FileTypeTexture:
|
||
return &UploadConfig{
|
||
AllowedExts: map[string]bool{
|
||
".png": true,
|
||
},
|
||
MinSize: 1024, // 1KB
|
||
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
|
||
}
|
||
|
||
// uploadStorageClient 为上传服务定义的最小依赖接口,便于单元测试注入 mock
|
||
type uploadStorageClient interface {
|
||
GetBucket(name string) (string, error)
|
||
GeneratePresignedPostURL(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*storage.PresignedPostPolicyResult, error)
|
||
}
|
||
|
||
// GenerateAvatarUploadURL 生成头像上传URL(对外导出)
|
||
func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.StorageClient, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error) {
|
||
return generateAvatarUploadURLWithClient(ctx, storageClient, userID, fileName)
|
||
}
|
||
|
||
// generateAvatarUploadURLWithClient 使用接口类型的内部实现,方便测试
|
||
func generateAvatarUploadURLWithClient(ctx context.Context, storageClient uploadStorageClient, 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 := storageClient.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 := storageClient.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 GenerateTextureUploadURL(ctx context.Context, storageClient *storage.StorageClient, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error) {
|
||
return generateTextureUploadURLWithClient(ctx, storageClient, userID, fileName, textureType)
|
||
}
|
||
|
||
// generateTextureUploadURLWithClient 使用接口类型的内部实现,方便测试
|
||
func generateTextureUploadURLWithClient(ctx context.Context, storageClient uploadStorageClient, 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 := storageClient.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 := storageClient.GeneratePresignedPostURL(
|
||
ctx,
|
||
bucketName,
|
||
objectName,
|
||
uploadConfig.MinSize,
|
||
uploadConfig.MaxSize,
|
||
uploadConfig.Expires,
|
||
)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("生成上传URL失败: %w", err)
|
||
}
|
||
|
||
return result, nil
|
||
}
|