Files
backend/internal/service/texture_service.go
lan 8858fd1ede 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.
2025-12-04 20:07:30 +08:00

440 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
)
// textureService TextureService的实现
type textureService struct {
textureRepo repository.TextureRepository
userRepo repository.UserRepository
storage *storage.StorageClient
cache *database.CacheManager
cacheKeys *database.CacheKeyBuilder
cacheInv *database.CacheInvalidator
logger *zap.Logger
}
// NewTextureService 创建TextureService实例
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),
logger: logger,
}
}
func (s *textureService) Create(ctx context.Context, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error) {
// 验证用户存在
user, err := s.userRepo.FindByID(ctx, uploaderID)
if err != nil || user == nil {
return nil, ErrUserNotFound
}
// 检查是否有任何用户上传过相同Hash的皮肤复用URL不重复保存文件
existingTexture, err := s.textureRepo.FindByHash(ctx, hash)
if err != nil {
return nil, err
}
// 如果已存在相同Hash的皮肤复用已存在的URL
finalURL := url
if existingTexture != nil {
finalURL = existingTexture.URL
}
// 转换材质类型
textureTypeEnum, err := parseTextureTypeInternal(textureType)
if err != nil {
return nil, err
}
// 创建材质记录即使Hash相同也创建新的数据库记录
texture := &model.Texture{
UploaderID: uploaderID,
Name: name,
Description: description,
Type: textureTypeEnum,
URL: finalURL, // 复用已存在的URL或使用新URL
Hash: hash,
Size: size,
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
}
func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture, error) {
// 尝试从缓存获取
cacheKey := s.cacheKeys.Texture(id)
var texture model.Texture
if err := s.cache.Get(ctx, cacheKey, &texture); err == nil {
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return &texture, nil
}
// 缓存未命中,从数据库查询
texture2, err := s.textureRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
}
// 存入缓存异步5分钟过期
if texture2 != nil {
go func() {
_ = s.cache.Set(context.Background(), cacheKey, texture2, 5*time.Minute)
}()
}
return texture2, nil
}
func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Texture, error) {
// 尝试从缓存获取
cacheKey := s.cacheKeys.TextureByHash(hash)
var texture model.Texture
if err := s.cache.Get(ctx, cacheKey, &texture); err == nil {
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return &texture, nil
}
// 缓存未命中,从数据库查询
texture2, err := s.textureRepo.FindByHash(ctx, hash)
if err != nil {
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
}
// 存入缓存异步5分钟过期
go func() {
_ = s.cache.Set(context.Background(), cacheKey, texture2, 5*time.Minute)
}()
return texture2, nil
}
func (s *textureService) GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)
// 尝试从缓存获取(包含分页参数)
cacheKey := s.cacheKeys.TextureList(uploaderID, page)
var cachedResult struct {
Textures []*model.Texture
Total int64
}
if err := s.cache.Get(ctx, cacheKey, &cachedResult); err == nil {
return cachedResult.Textures, cachedResult.Total, nil
}
// 缓存未命中,从数据库查询
textures, total, err := s.textureRepo.FindByUploaderID(ctx, uploaderID, page, pageSize)
if err != nil {
return nil, 0, err
}
// 存入缓存异步2分钟过期
go func() {
result := struct {
Textures []*model.Texture
Total int64
}{Textures: textures, Total: total}
_ = s.cache.Set(context.Background(), cacheKey, result, 2*time.Minute)
}()
return textures, total, nil
}
func (s *textureService) Search(ctx context.Context, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)
return s.textureRepo.Search(ctx, keyword, textureType, publicOnly, page, pageSize)
}
func (s *textureService) Update(ctx context.Context, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error) {
// 获取材质并验证权限
texture, err := s.textureRepo.FindByID(ctx, textureID)
if err != nil {
return nil, err
}
if texture == nil {
return nil, ErrTextureNotFound
}
if texture.UploaderID != uploaderID {
return nil, ErrTextureNoPermission
}
// 更新字段
updates := make(map[string]interface{})
if name != "" {
updates["name"] = name
}
if description != "" {
updates["description"] = description
}
if isPublic != nil {
updates["is_public"] = *isPublic
}
if len(updates) > 0 {
if err := s.textureRepo.UpdateFields(ctx, textureID, updates); err != nil {
return nil, err
}
}
// 清除 texture 缓存和用户列表缓存
s.cacheInv.OnUpdate(ctx, s.cacheKeys.Texture(textureID))
s.cacheInv.BatchInvalidate(ctx, fmt.Sprintf("texture:user:%d:*", uploaderID))
return s.textureRepo.FindByID(ctx, textureID)
}
func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64) error {
// 获取材质并验证权限
texture, err := s.textureRepo.FindByID(ctx, textureID)
if err != nil {
return err
}
if texture == nil {
return ErrTextureNotFound
}
if texture.UploaderID != uploaderID {
return ErrTextureNoPermission
}
err = s.textureRepo.Delete(ctx, textureID)
if err != nil {
return err
}
// 清除 texture 缓存和用户列表缓存
s.cacheInv.OnDelete(ctx, s.cacheKeys.Texture(textureID))
s.cacheInv.BatchInvalidate(ctx, fmt.Sprintf("texture:user:%d:*", uploaderID))
return nil
}
func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
// 确保材质存在
texture, err := s.textureRepo.FindByID(ctx, textureID)
if err != nil {
return false, err
}
if texture == nil {
return false, ErrTextureNotFound
}
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
if err != nil {
return false, err
}
if isFavorited {
// 已收藏 -> 取消收藏
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return false, nil
}
// 未收藏 -> 添加收藏
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return true, nil
}
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)
return s.textureRepo.GetUserFavorites(ctx, userID, page, pageSize)
}
func (s *textureService) CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error {
count, err := s.textureRepo.CountByUploaderID(ctx, uploaderID)
if err != nil {
return err
}
if count >= int64(maxTextures) {
return fmt.Errorf("已达到最大上传数量限制(%d)", maxTextures)
}
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 {
case "SKIN":
return model.TextureTypeSkin, nil
case "CAPE":
return model.TextureTypeCape, nil
default:
return "", errors.New("无效的材质类型")
}
}