feat: Enhance texture upload functionality and API response format
- Introduced a new upload endpoint for direct texture file uploads, allowing users to upload textures with validation for size and format. - Updated existing texture-related API responses to a standardized format, improving consistency across the application. - Refactored texture service methods to handle file uploads and reuse existing texture URLs based on hash checks. - Cleaned up Dockerfile and other files by removing unnecessary whitespace.
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/repository"
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/storage"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -16,6 +22,7 @@ import (
|
||||
type textureService struct {
|
||||
textureRepo repository.TextureRepository
|
||||
userRepo repository.UserRepository
|
||||
storage *storage.StorageClient
|
||||
cache *database.CacheManager
|
||||
cacheKeys *database.CacheKeyBuilder
|
||||
cacheInv *database.CacheInvalidator
|
||||
@@ -26,12 +33,14 @@ type textureService struct {
|
||||
func NewTextureService(
|
||||
textureRepo repository.TextureRepository,
|
||||
userRepo repository.UserRepository,
|
||||
storageClient *storage.StorageClient,
|
||||
cacheManager *database.CacheManager,
|
||||
logger *zap.Logger,
|
||||
) TextureService {
|
||||
return &textureService{
|
||||
textureRepo: textureRepo,
|
||||
userRepo: userRepo,
|
||||
storage: storageClient,
|
||||
cache: cacheManager,
|
||||
cacheKeys: database.NewCacheKeyBuilder(""),
|
||||
cacheInv: database.NewCacheInvalidator(cacheManager),
|
||||
@@ -46,13 +55,16 @@ func (s *textureService) Create(ctx context.Context, uploaderID int64, name, des
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// 检查Hash是否已存在
|
||||
// 检查是否有任何用户上传过相同Hash的皮肤(复用URL,不重复保存文件)
|
||||
existingTexture, err := s.textureRepo.FindByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果已存在相同Hash的皮肤,复用已存在的URL
|
||||
finalURL := url
|
||||
if existingTexture != nil {
|
||||
return nil, errors.New("该材质已存在")
|
||||
finalURL = existingTexture.URL
|
||||
}
|
||||
|
||||
// 转换材质类型
|
||||
@@ -61,13 +73,13 @@ func (s *textureService) Create(ctx context.Context, uploaderID int64, name, des
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建材质
|
||||
// 创建材质记录(即使Hash相同,也创建新的数据库记录)
|
||||
texture := &model.Texture{
|
||||
UploaderID: uploaderID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: textureTypeEnum,
|
||||
URL: url,
|
||||
URL: finalURL, // 复用已存在的URL或使用新URL
|
||||
Hash: hash,
|
||||
Size: size,
|
||||
IsPublic: isPublic,
|
||||
@@ -304,6 +316,116 @@ func (s *textureService) CheckUploadLimit(ctx context.Context, uploaderID int64,
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadTexture 直接上传材质文件
|
||||
func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, name, description, textureType string, fileData []byte, fileName string, isPublic, isSlim bool) (*model.Texture, error) {
|
||||
// 验证用户存在
|
||||
user, err := s.userRepo.FindByID(ctx, uploaderID)
|
||||
if err != nil || user == nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// 验证文件大小和扩展名
|
||||
fileSize := len(fileData)
|
||||
const minSize = 512 // 512B
|
||||
const maxSize = 10 * 1024 * 1024 // 10MB
|
||||
if int64(fileSize) < minSize || int64(fileSize) > maxSize {
|
||||
return nil, fmt.Errorf("文件大小必须在 %d 到 %d 字节之间", minSize, maxSize)
|
||||
}
|
||||
|
||||
// 验证文件扩展名(只支持PNG)
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
if ext != ".png" {
|
||||
return nil, fmt.Errorf("不支持的文件格式: %s,仅支持PNG格式", ext)
|
||||
}
|
||||
|
||||
// 验证材质类型
|
||||
if textureType != "SKIN" && textureType != "CAPE" {
|
||||
return nil, errors.New("无效的材质类型")
|
||||
}
|
||||
|
||||
// 计算文件SHA256哈希
|
||||
hashBytes := sha256.Sum256(fileData)
|
||||
hash := hex.EncodeToString(hashBytes[:])
|
||||
|
||||
// 检查是否有任何用户上传过相同Hash的皮肤(复用URL,不重复保存文件)
|
||||
existingTexture, err := s.textureRepo.FindByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finalURL string
|
||||
if existingTexture != nil {
|
||||
// 如果已存在相同Hash的皮肤,复用已存在的URL,不重复上传
|
||||
finalURL = existingTexture.URL
|
||||
s.logger.Info("复用已存在的材质文件",
|
||||
zap.String("hash", hash),
|
||||
zap.String("url", finalURL),
|
||||
)
|
||||
} else {
|
||||
// 如果不存在,上传到对象存储
|
||||
if s.storage == nil {
|
||||
return nil, errors.New("存储服务不可用")
|
||||
}
|
||||
|
||||
// 获取存储桶名称
|
||||
bucketName, err := s.storage.GetBucket("textures")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取存储桶失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成对象名称(路径)
|
||||
// 格式: hash/{hash[:2]}/{hash[2:4]}/{hash}.png
|
||||
// 使用哈希值作为路径,避免重复存储相同文件
|
||||
textureTypeFolder := strings.ToLower(textureType)
|
||||
objectName := fmt.Sprintf("%s/%s/%s/%s/%s%s", textureTypeFolder, hash[:2], hash[2:4], hash, hash, ext)
|
||||
|
||||
// 上传文件
|
||||
reader := bytes.NewReader(fileData)
|
||||
contentType := "image/png"
|
||||
if err := s.storage.UploadObject(ctx, bucketName, objectName, reader, int64(fileSize), contentType); err != nil {
|
||||
return nil, fmt.Errorf("上传文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建文件URL
|
||||
finalURL = s.storage.BuildFileURL(bucketName, objectName)
|
||||
s.logger.Info("上传新的材质文件",
|
||||
zap.String("hash", hash),
|
||||
zap.String("url", finalURL),
|
||||
)
|
||||
}
|
||||
|
||||
// 转换材质类型
|
||||
textureTypeEnum, err := parseTextureTypeInternal(textureType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建材质记录(即使Hash相同,也创建新的数据库记录)
|
||||
texture := &model.Texture{
|
||||
UploaderID: uploaderID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Type: textureTypeEnum,
|
||||
URL: finalURL,
|
||||
Hash: hash,
|
||||
Size: fileSize,
|
||||
IsPublic: isPublic,
|
||||
IsSlim: isSlim,
|
||||
Status: 1,
|
||||
DownloadCount: 0,
|
||||
FavoriteCount: 0,
|
||||
}
|
||||
|
||||
if err := s.textureRepo.Create(ctx, texture); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清除用户的 texture 列表缓存(所有分页)
|
||||
s.cacheInv.BatchInvalidate(ctx, fmt.Sprintf("texture:user:%d:*", uploaderID))
|
||||
|
||||
return texture, nil
|
||||
}
|
||||
|
||||
// parseTextureTypeInternal 解析材质类型
|
||||
func parseTextureTypeInternal(textureType string) (model.TextureType, error) {
|
||||
switch textureType {
|
||||
|
||||
Reference in New Issue
Block a user