统一文件上传方式为直接上传,更新环境变量示例
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +19,7 @@ import (
|
||||
"carrotskin/pkg/config"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/redis"
|
||||
"carrotskin/pkg/storage"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -28,6 +33,7 @@ type userService struct {
|
||||
cache *database.CacheManager
|
||||
cacheKeys *database.CacheKeyBuilder
|
||||
cacheInv *database.CacheInvalidator
|
||||
storage *storage.StorageClient
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
@@ -38,6 +44,7 @@ func NewUserService(
|
||||
jwtService *auth.JWTService,
|
||||
redisClient *redis.Client,
|
||||
cacheManager *database.CacheManager,
|
||||
storageClient *storage.StorageClient,
|
||||
logger *zap.Logger,
|
||||
) UserService {
|
||||
// CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀
|
||||
@@ -50,6 +57,7 @@ func NewUserService(
|
||||
cache: cacheManager,
|
||||
cacheKeys: database.NewCacheKeyBuilder(""),
|
||||
cacheInv: database.NewCacheInvalidator(cacheManager),
|
||||
storage: storageClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -347,6 +355,67 @@ func (s *userService) ValidateAvatarURL(ctx context.Context, avatarURL string) e
|
||||
return s.checkDomainAllowed(host, cfg.Security.AllowedDomains)
|
||||
}
|
||||
|
||||
func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error) {
|
||||
// 验证文件大小
|
||||
fileSize := len(fileData)
|
||||
const minSize = 512 // 512B
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
if int64(fileSize) < minSize || int64(fileSize) > maxSize {
|
||||
return "", fmt.Errorf("文件大小必须在 %d 到 %d 字节之间", minSize, maxSize)
|
||||
}
|
||||
|
||||
// 验证文件扩展名
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".webp": true}
|
||||
if !allowedExts[ext] {
|
||||
return "", fmt.Errorf("不支持的文件格式: %s,仅支持 jpg/jpeg/png/gif/webp", ext)
|
||||
}
|
||||
|
||||
// 检查存储服务
|
||||
if s.storage == nil {
|
||||
return "", errors.New("存储服务不可用")
|
||||
}
|
||||
|
||||
// 计算文件哈希
|
||||
hashBytes := sha256.Sum256(fileData)
|
||||
hash := hex.EncodeToString(hashBytes[:])
|
||||
|
||||
// 获取存储桶
|
||||
bucketName, err := s.storage.GetBucket("avatars")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取存储桶失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成对象路径: avatars/{hash[:2]}/{hash[2:4]}/{hash}{ext}
|
||||
objectName := fmt.Sprintf("%s/%s/%s%s", hash[:2], hash[2:4], hash, ext)
|
||||
|
||||
// 上传文件
|
||||
reader := bytes.NewReader(fileData)
|
||||
contentType := "image/" + strings.TrimPrefix(ext, ".")
|
||||
if ext == ".jpg" {
|
||||
contentType = "image/jpeg"
|
||||
}
|
||||
if err := s.storage.UploadObject(ctx, bucketName, objectName, reader, int64(fileSize), contentType); err != nil {
|
||||
return "", fmt.Errorf("上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建文件URL
|
||||
avatarURL := s.storage.BuildFileURL(bucketName, objectName)
|
||||
|
||||
// 更新用户头像
|
||||
if err := s.UpdateAvatar(ctx, userID, avatarURL); err != nil {
|
||||
return "", fmt.Errorf("更新用户头像失败: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("上传头像成功",
|
||||
zap.Int64("user_id", userID),
|
||||
zap.String("hash", hash),
|
||||
zap.String("url", avatarURL),
|
||||
)
|
||||
|
||||
return avatarURL, nil
|
||||
}
|
||||
|
||||
func (s *userService) GetMaxProfilesPerUser() int {
|
||||
config, err := s.configRepo.GetByKey(context.Background(), "max_profiles_per_user")
|
||||
if err != nil || config == nil {
|
||||
|
||||
Reference in New Issue
Block a user