- Simplified caching logic by removing unnecessary nil check before setting cache. - Enhanced error handling in texture upload process to return the original texture object if fetching the uploader information fails or returns nil.
406 lines
11 KiB
Go
406 lines
11 KiB
Go
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("材质已删除")
|
||
}
|
||
|
||
// 存入缓存(异步)
|
||
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 关联
|
||
textureWithUploader, err := s.textureRepo.FindByID(ctx, texture.ID)
|
||
if err != nil {
|
||
// 如果查询失败,返回原始创建的 texture 对象(虽然可能没有 Uploader 信息)
|
||
return texture, nil
|
||
}
|
||
if textureWithUploader == nil {
|
||
// 如果查询返回 nil(极端情况,如数据库复制延迟),返回原始创建的 texture 对象
|
||
return texture, nil
|
||
}
|
||
return textureWithUploader, 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("无效的材质类型")
|
||
}
|
||
}
|