// 创作媒体服务,封装媒体上传前的所有权检查、关联更新和旧媒体清理事务。 package service import ( "strings" "juhe-factory/api/internal/model" "github.com/google/uuid" "gorm.io/gorm" ) // MediaUploadTarget 汇总上传前需要的业务对象及待替换媒体信息。 type MediaUploadTarget struct { Project *model.CreativeProject Episode *model.ProjectEpisode Asset *model.ProjectAsset OldMediaID *uuid.UUID OldObjectKey string } // MediaObject 查询当前用户有权播放或下载的媒体对象键。 func (s *Creative) MediaObject(userID, mediaID uuid.UUID) (string, error) { var objectKey string err := s.DB.Table("media_assets"). Where("id=? AND owner_user_id=? AND deleted_at IS NULL", mediaID, userID). Pluck("object_key", &objectKey).Error if err != nil { return "", err } if strings.TrimSpace(objectKey) == "" { return "", gorm.ErrRecordNotFound } return objectKey, nil } // PrepareRedrawMediaUpload 校验重绘项目所有权,并返回指定媒体类型的替换信息。 func (s *Creative) PrepareRedrawMediaUpload(userID, projectID uuid.UUID, kind string) (MediaUploadTarget, error) { var project model.CreativeProject if err := s.DB.Where("id=? AND user_id=? AND project_type='video_redraw' AND deleted_at IS NULL", projectID, userID).Take(&project).Error; err != nil { return MediaUploadTarget{}, err } oldMediaID := project.SourceVideoAssetID if kind == "subtitle" { oldMediaID = project.SubtitleAssetID } oldObjectKey, err := s.mediaObjectKey(oldMediaID) if err != nil { return MediaUploadTarget{}, err } return MediaUploadTarget{Project: &project, OldMediaID: oldMediaID, OldObjectKey: oldObjectKey}, nil } // PersistRedrawMediaUpload 在事务中保存新媒体、更新项目关联并删除旧媒体记录。 func (s *Creative) PersistRedrawMediaUpload(projectID uuid.UUID, kind, currentStatus string, media *model.MediaAsset, oldMediaID *uuid.UUID) error { column, status := "source_video_asset_id", "uploaded" if kind == "subtitle" { column, status = "subtitle_asset_id", currentStatus } return s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(media).Error; err != nil { return err } updates := map[string]any{column: media.ID, "redraw_status": status} if err := tx.Table("creative_projects").Where("id=?", projectID).Updates(updates).Error; err != nil { return err } return deleteReplacedMedia(tx, oldMediaID) }) } // PrepareEpisodeMediaUpload 校验剧集与项目所有权,并返回指定媒体类型的替换信息。 func (s *Creative) PrepareEpisodeMediaUpload(userID, projectID, episodeID uuid.UUID, kind string) (MediaUploadTarget, error) { var episode model.ProjectEpisode err := s.DB.Table("project_episodes e").Joins("JOIN creative_projects p ON p.id=e.project_id"). Where("e.id=? AND e.project_id=? AND e.deleted_at IS NULL AND p.user_id=? AND p.deleted_at IS NULL", episodeID, projectID, userID). Select("e.*").First(&episode).Error if err != nil { return MediaUploadTarget{}, err } oldMediaID := episode.SourceVideoAssetID if kind == "subtitle" { oldMediaID = episode.SubtitleAssetID } oldObjectKey, err := s.mediaObjectKey(oldMediaID) if err != nil { return MediaUploadTarget{}, err } return MediaUploadTarget{Episode: &episode, OldMediaID: oldMediaID, OldObjectKey: oldObjectKey}, nil } // PersistEpisodeMediaUpload 在事务中保存新媒体、更新剧集关联并删除旧媒体记录。 func (s *Creative) PersistEpisodeMediaUpload(episodeID uuid.UUID, kind string, status string, media *model.MediaAsset, oldMediaID *uuid.UUID) error { column := "source_video_asset_id" if kind == "subtitle" { column = "subtitle_asset_id" } return s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(media).Error; err != nil { return err } if err := tx.Table("project_episodes").Where("id=?", episodeID).Updates(map[string]any{column: media.ID, "status": status}).Error; err != nil { return err } return deleteReplacedMedia(tx, oldMediaID) }) } // PrepareAssetMediaUpload 校验普通创作项目中的资产所有权并返回资产信息。 func (s *Creative) PrepareAssetMediaUpload(userID, projectID, assetID uuid.UUID) (MediaUploadTarget, error) { var asset model.ProjectAsset err := s.DB.Table("project_assets a").Joins("JOIN creative_projects p ON p.id=a.project_id"). Where("a.id=? AND a.project_id=? AND a.deleted_at IS NULL AND p.user_id=? AND p.project_type<>'video_redraw' AND p.deleted_at IS NULL", assetID, projectID, userID). Select("a.*").First(&asset).Error if err != nil { return MediaUploadTarget{}, err } return MediaUploadTarget{Asset: &asset}, nil } // AttachUploadedAssetAudio 在事务中保存上传音频并更新资产关联。 func (s *Creative) AttachUploadedAssetAudio(assetID uuid.UUID, media *model.MediaAsset) error { return s.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Create(media).Error; err != nil { return err } return tx.Table("project_assets").Where("id=?", assetID).Update("audio_asset_id", media.ID).Error }) } // mediaObjectKey 查询待替换媒体的对象键;空媒体标识表示无需清理。 func (s *Creative) mediaObjectKey(mediaID *uuid.UUID) (string, error) { if mediaID == nil { return "", nil } var objectKey string if err := s.DB.Table("media_assets").Where("id=?", *mediaID).Pluck("object_key", &objectKey).Error; err != nil { return "", err } return objectKey, nil } // deleteReplacedMedia 删除旧媒体的渠道缓存和媒体记录,供上传事务复用。 func deleteReplacedMedia(tx *gorm.DB, mediaID *uuid.UUID) error { if mediaID == nil { return nil } if err := tx.Exec("DELETE FROM channel_asset_cache WHERE media_asset_id=?", *mediaID).Error; err != nil { return err } return tx.Exec("DELETE FROM media_assets WHERE id=?", *mediaID).Error }