package service import ( "fmt" "github.com/google/uuid" "gorm.io/gorm" ) type deletionMediaRow struct { ID uuid.UUID ObjectKey string } type deletionMediaSet struct { rows map[uuid.UUID]string } func newDeletionMediaSet() *deletionMediaSet { return &deletionMediaSet{rows: make(map[uuid.UUID]string)} } func (set *deletionMediaSet) addQuery(query *gorm.DB) error { var rows []deletionMediaRow if err := query.Scan(&rows).Error; err != nil { return err } for _, row := range rows { if row.ID != uuid.Nil { set.rows[row.ID] = row.ObjectKey } } return nil } func (set *deletionMediaSet) objectKeys() []string { keys := make([]string, 0, len(set.rows)) seen := make(map[string]struct{}, len(set.rows)) for _, key := range set.rows { if key == "" { continue } if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} keys = append(keys, key) } return keys } func (set *deletionMediaSet) deleteRows(tx *gorm.DB) error { if len(set.rows) == 0 { return nil } ids := make([]uuid.UUID, 0, len(set.rows)) for id := range set.rows { ids = append(ids, id) } if err := tx.Exec("DELETE FROM channel_asset_cache WHERE media_asset_id IN ?", ids).Error; err != nil { return err } return tx.Exec("DELETE FROM media_assets WHERE id IN ?", ids).Error } func collectMediaByObjectKey(tx *gorm.DB, set *deletionMediaSet, pattern string) error { return set.addQuery(tx.Table("media_assets").Select("id,object_key").Where("object_key LIKE ?", pattern)) } // removeEpisodeDerivedContent removes everything produced from an episode analysis. // It intentionally keeps the episode source/subtitle and all project assets. func removeEpisodeDerivedContent(tx *gorm.DB, projectID, episodeID uuid.UUID, media *deletionMediaSet) error { // 封面来自本剧集的首帧,删除反推结果前先清理外键引用,避免随后删除媒体对象时产生约束错误。 if err := tx.Exec(`UPDATE creative_projects SET cover_asset_id=NULL WHERE id=? AND cover_asset_id=(SELECT cover_asset_id FROM project_episodes WHERE id=?)`, projectID, episodeID).Error; err != nil { return err } if err := tx.Exec("UPDATE project_episodes SET cover_asset_id=NULL WHERE id=?", episodeID).Error; err != nil { return err } taskScope := `(task_type IN ('prompt_reverse','video_generation') OR (task_type='image_generation' AND input_data->>'target_type'='storyboard')) AND (episode_id=? OR storyboard_id IN (SELECT id FROM episode_storyboards WHERE episode_id=?))` if err := media.addQuery(tx.Table("media_assets media").Select("DISTINCT media.id,media.object_key"). Joins("JOIN generation_outputs output ON output.media_asset_id=media.id"). Joins("JOIN generation_tasks task ON task.id=output.task_id"). Where(taskScope, episodeID, episodeID)); err != nil { return err } if err := media.addQuery(tx.Table("media_assets media").Select("DISTINCT media.id,media.object_key"). Joins("JOIN episode_storyboards storyboard ON storyboard.thumbnail_asset_id=media.id"). Where("storyboard.episode_id=?", episodeID)); err != nil { return err } derivedPrefix := fmt.Sprintf("juyou_ran/video-redraw/projects/%s/episodes/%s/storyboards/%%", projectID, episodeID) if err := collectMediaByObjectKey(tx, media, derivedPrefix); err != nil { return err } if err := tx.Exec("UPDATE episode_storyboards SET active_output_id=NULL WHERE episode_id=?", episodeID).Error; err != nil { return err } if err := tx.Exec("DELETE FROM generation_outputs WHERE task_id IN (SELECT id FROM generation_tasks WHERE "+taskScope+")", episodeID, episodeID).Error; err != nil { return err } if err := tx.Exec("DELETE FROM generation_tasks WHERE "+taskScope, episodeID, episodeID).Error; err != nil { return err } if err := tx.Exec(`UPDATE generation_tasks SET storyboard_id=NULL WHERE storyboard_id IN (SELECT id FROM episode_storyboards WHERE episode_id=?)`, episodeID).Error; err != nil { return err } return tx.Exec("DELETE FROM episode_storyboards WHERE episode_id=?", episodeID).Error }