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("无效的材质类型") } }