Files
backend/internal/service/texture_service.go
lafay c5db489d72 refactor: Enhance texture handling and configuration
- Removed Swagger documentation import from the main server file.
- Updated TextureInfo struct to include UploaderUsername for better texture metadata.
- Modified texture repository methods to preload Uploader information when fetching textures by hash.
- Improved texture service to handle cases where Uploader information is missing, ensuring proper caching and retrieval.
- Added Redis configuration options in the environment variable setup for better flexibility.
2026-01-10 03:15:27 +08:00

399 lines
11 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"
"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) GetByID(ctx context.Context, id int64) (*model.Texture, error) {
// 尝试从缓存获取
cacheKey := s.cacheKeys.Texture(id)
var texture model.Texture
if ok, _ := s.cache.TryGet(ctx, cacheKey, &texture); ok {
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
// 如果缓存中没有 Uploader 信息,重新查询数据库
if texture.Uploader == 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("材质已删除")
}
// 更新缓存
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
return texture2, nil
}
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("材质已删除")
}
// 存入缓存(异步)
if texture2 != nil {
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
}
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 ok, _ := s.cache.TryGet(ctx, cacheKey, &texture); ok {
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("材质已删除")
}
// 存入缓存(异步)
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
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 ok, _ := s.cache.TryGet(ctx, cacheKey, &cachedResult); ok {
return cachedResult.Textures, cachedResult.Total, nil
}
// 缓存未命中,从数据库查询
textures, total, err := s.textureRepo.FindByUploaderID(ctx, uploaderID, page, pageSize)
if err != nil {
return nil, 0, err
}
// 存入缓存(异步)
result := struct {
Textures []*model.Texture
Total int64
}{Textures: textures, Total: total}
s.cache.SetAsync(context.Background(), cacheKey, result, s.cache.Policy.TextureListTTL)
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, s.cacheKeys.TextureListPattern(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, s.cacheKeys.TextureListPattern(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))
// 重新查询以预加载 Uploader 关联
return s.textureRepo.FindByID(ctx, texture.ID)
}
// 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("无效的材质类型")
}
}