feat(media): 素材删除 - 安全清理未引用的存储资源
删除素材前检查项目、画布、任务、其他素材、工作流、风格和声音引用;仅清理未复用的本地或 OSS/COS 对象。
This commit is contained in:
273
backend/internal/repository/resource_reference.go
Normal file
273
backend/internal/repository/resource_reference.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ResourceReferenceDocument 是资源删除校验使用的只读业务文档快照。
|
||||
// JSON 中的精确资源 ID 识别由 service 完成,repository 只负责缩小候选记录。
|
||||
type ResourceReferenceDocument struct {
|
||||
Kind string
|
||||
ID string
|
||||
Title string
|
||||
PrimaryJSON string
|
||||
SecondaryJSON string
|
||||
}
|
||||
|
||||
type ResourceDirectReference struct {
|
||||
Kind string
|
||||
ID string
|
||||
Title string
|
||||
ResourceID string
|
||||
}
|
||||
|
||||
type ResourceReferenceSnapshot struct {
|
||||
Documents []ResourceReferenceDocument
|
||||
Direct []ResourceDirectReference
|
||||
}
|
||||
|
||||
func (r *Repository) AssetResourceRecords(assetID string) ([]model.AssetVersion, []model.AssetRepresentation, error) {
|
||||
var versions []model.AssetVersion
|
||||
if err := r.db.Where("asset_id = ?", assetID).Find(&versions).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
return versions, nil, nil
|
||||
}
|
||||
versionIDs := make([]string, 0, len(versions))
|
||||
for _, version := range versions {
|
||||
versionIDs = append(versionIDs, version.ID)
|
||||
}
|
||||
var representations []model.AssetRepresentation
|
||||
if err := r.db.Where("asset_version_id IN ?", versionIDs).Find(&representations).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return versions, representations, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ResourcesForUserIDs(userID string, resourceIDs []string) ([]model.Resource, error) {
|
||||
if len(resourceIDs) == 0 {
|
||||
return []model.Resource{}, nil
|
||||
}
|
||||
var resources []model.Resource
|
||||
err := r.db.Where("user_id = ? AND id IN ?", userID, resourceIDs).Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// ResourceStorageReferenceCount 返回未包含在 excludedResourceIDs 中、但指向同一物理对象的资源记录数。
|
||||
// 这用于兼容历史数据中多个 Resource 行复用一个对象路径的情况。
|
||||
func (r *Repository) ResourceStorageReferenceCount(resource *model.Resource, excludedResourceIDs []string) (int64, error) {
|
||||
if resource == nil {
|
||||
return 0, nil
|
||||
}
|
||||
query := r.db.Model(&model.Resource{}).
|
||||
Where("endpoint = ? AND bucket = ? AND object_key = ?", resource.Endpoint, resource.Bucket, resource.ObjectKey)
|
||||
if strings.TrimSpace(resource.Provider) == "" || strings.EqualFold(strings.TrimSpace(resource.Provider), "local") {
|
||||
query = query.Where("provider IN ?", []string{"", "local"})
|
||||
} else {
|
||||
query = query.Where("provider = ?", resource.Provider)
|
||||
}
|
||||
if len(excludedResourceIDs) > 0 {
|
||||
query = query.Where("id NOT IN ?", excludedResourceIDs)
|
||||
}
|
||||
var count int64
|
||||
err := query.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *Repository) ResourceReferenceSnapshot(userID string, excludingAssetID string, resourceIDs []string) (ResourceReferenceSnapshot, error) {
|
||||
snapshot := ResourceReferenceSnapshot{Documents: []ResourceReferenceDocument{}, Direct: []ResourceDirectReference{}}
|
||||
if len(resourceIDs) == 0 {
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
var assets []model.Asset
|
||||
assetQuery := r.db.Where("user_id = ? AND id <> ?", userID, excludingAssetID)
|
||||
if err := resourceTextQuery(assetQuery, []string{"payload_json"}, resourceIDs).Find(&assets).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, asset := range assets {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "素材", ID: asset.ID, Title: asset.Title, PrimaryJSON: asset.PayloadJSON})
|
||||
}
|
||||
|
||||
var canvases []model.CanvasProject
|
||||
if err := resourceTextQuery(r.db.Where("user_id = ?", userID), []string{"payload_json"}, resourceIDs).Find(&canvases).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, canvas := range canvases {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "画布", ID: canvas.ID, Title: canvas.Title, PrimaryJSON: canvas.PayloadJSON})
|
||||
}
|
||||
|
||||
var tasks []model.Task
|
||||
if err := resourceTextQuery(r.db.Where("user_id = ?", userID), []string{"input_json", "result_json"}, resourceIDs).Find(&tasks).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, task := range tasks {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "任务", ID: task.ID, Title: task.Prompt, PrimaryJSON: task.InputJSON, SecondaryJSON: task.ResultJSON})
|
||||
}
|
||||
|
||||
var projects []model.Project
|
||||
if err := resourceTextQuery(r.db.Where("user_id = ?", userID), []string{"style_profile_json"}, resourceIDs).Find(&projects).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, project := range projects {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "项目", ID: project.ID, Title: project.Name, PrimaryJSON: project.StyleProfileJSON})
|
||||
}
|
||||
|
||||
var styles []model.StyleProfile
|
||||
if err := resourceTextQuery(r.db.Where("user_id = ?", userID), []string{"cover_url", "profile_json"}, resourceIDs).Find(&styles).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, style := range styles {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "风格", ID: style.ID, Title: style.Name, PrimaryJSON: style.CoverURL, SecondaryJSON: style.ProfileJSON})
|
||||
}
|
||||
|
||||
type joinedDocument struct {
|
||||
ID string
|
||||
Title string
|
||||
PrimaryJSON string
|
||||
SecondaryJSON string
|
||||
}
|
||||
var versions []joinedDocument
|
||||
versionQuery := r.db.Table("asset_versions").
|
||||
Select("asset_versions.id, assets.title, asset_versions.definition_json AS primary_json").
|
||||
Joins("JOIN assets ON assets.id = asset_versions.asset_id").
|
||||
Where("assets.user_id = ? AND assets.id <> ?", userID, excludingAssetID)
|
||||
if err := resourceTextQuery(versionQuery, []string{"asset_versions.definition_json"}, resourceIDs).Scan(&versions).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, version := range versions {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "素材", ID: version.ID, Title: version.Title, PrimaryJSON: version.PrimaryJSON})
|
||||
}
|
||||
|
||||
var candidates []joinedDocument
|
||||
candidateQuery := r.db.Table("project_asset_candidates").
|
||||
Select("project_asset_candidates.id, projects.name AS title, project_asset_candidates.details_json AS primary_json").
|
||||
Joins("JOIN projects ON projects.id = project_asset_candidates.project_id").
|
||||
Where("projects.user_id = ?", userID)
|
||||
if err := resourceTextQuery(candidateQuery, []string{"project_asset_candidates.details_json"}, resourceIDs).Scan(&candidates).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "项目", ID: candidate.ID, Title: candidate.Title, PrimaryJSON: candidate.PrimaryJSON})
|
||||
}
|
||||
|
||||
var steps []joinedDocument
|
||||
stepQuery := r.db.Table("workflow_step_instances").
|
||||
Select("workflow_step_instances.id, workflow_step_instances.name AS title, workflow_step_instances.input_json AS primary_json, workflow_step_instances.output_json AS secondary_json").
|
||||
Joins("JOIN workflow_instances ON workflow_instances.id = workflow_step_instances.workflow_instance_id").
|
||||
Joins("JOIN projects ON projects.id = workflow_instances.project_id").
|
||||
Where("projects.user_id = ?", userID)
|
||||
if err := resourceTextQuery(stepQuery, []string{"workflow_step_instances.input_json", "workflow_step_instances.output_json"}, resourceIDs).Scan(&steps).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, step := range steps {
|
||||
snapshot.Documents = append(snapshot.Documents, ResourceReferenceDocument{Kind: "工作流", ID: step.ID, Title: step.Title, PrimaryJSON: step.PrimaryJSON, SecondaryJSON: step.SecondaryJSON})
|
||||
}
|
||||
|
||||
type joinedRepresentation struct {
|
||||
ID string
|
||||
Title string
|
||||
ResourceID string
|
||||
}
|
||||
var representations []joinedRepresentation
|
||||
if err := r.db.Table("asset_representations").
|
||||
Select("asset_representations.id, assets.title, asset_representations.resource_id").
|
||||
Joins("JOIN asset_versions ON asset_versions.id = asset_representations.asset_version_id").
|
||||
Joins("JOIN assets ON assets.id = asset_versions.asset_id").
|
||||
Where("assets.user_id = ? AND assets.id <> ? AND asset_representations.resource_id IN ?", userID, excludingAssetID, resourceIDs).
|
||||
Scan(&representations).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, representation := range representations {
|
||||
snapshot.Direct = append(snapshot.Direct, ResourceDirectReference{Kind: "素材", ID: representation.ID, Title: representation.Title, ResourceID: representation.ResourceID})
|
||||
}
|
||||
|
||||
var voices []model.VoiceProfile
|
||||
if err := r.db.Where("user_id = ? AND sample_resource_id IN ?", userID, resourceIDs).Find(&voices).Error; err != nil {
|
||||
return snapshot, err
|
||||
}
|
||||
for _, voice := range voices {
|
||||
snapshot.Direct = append(snapshot.Direct, ResourceDirectReference{Kind: "声音", ID: voice.ID, Title: voice.Name, ResourceID: voice.SampleResourceID})
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AssetBusinessReferences(userID string, assetID string) ([]ResourceDirectReference, error) {
|
||||
type projectReference struct {
|
||||
ID string
|
||||
Title string
|
||||
}
|
||||
var projects []projectReference
|
||||
if err := r.db.Table("project_asset_links").
|
||||
Distinct("projects.id, projects.name AS title").
|
||||
Joins("JOIN projects ON projects.id = project_asset_links.project_id").
|
||||
Where("projects.user_id = ? AND project_asset_links.asset_id = ?", userID, assetID).
|
||||
Scan(&projects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var shotProjects []projectReference
|
||||
if err := r.db.Table("shot_asset_references").
|
||||
Distinct("projects.id, projects.name AS title").
|
||||
Joins("JOIN asset_versions ON asset_versions.id = shot_asset_references.asset_version_id").
|
||||
Joins("JOIN shots ON shots.id = shot_asset_references.shot_id").
|
||||
Joins("JOIN projects ON projects.id = shots.project_id").
|
||||
Where("projects.user_id = ? AND asset_versions.asset_id = ?", userID, assetID).
|
||||
Scan(&shotProjects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var candidateProjects []projectReference
|
||||
if err := r.db.Table("project_asset_candidates").
|
||||
Distinct("projects.id, projects.name AS title").
|
||||
Joins("JOIN projects ON projects.id = project_asset_candidates.project_id").
|
||||
Where("projects.user_id = ? AND project_asset_candidates.resolved_asset_id = ?", userID, assetID).
|
||||
Scan(&candidateProjects).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]ResourceDirectReference, 0, len(projects)+len(shotProjects)+len(candidateProjects))
|
||||
for _, project := range append(append(projects, shotProjects...), candidateProjects...) {
|
||||
result = append(result, ResourceDirectReference{Kind: "项目", ID: project.ID, Title: project.Title})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteAssetAndResources(userID string, assetID string, resourceIDs []string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
versionIDs := tx.Model(&model.AssetVersion{}).Select("id").Where("asset_id = ?", assetID)
|
||||
if err := tx.Where("asset_version_id IN (?)", versionIDs).Delete(&model.CharacterVoiceBinding{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_version_id IN (?)", versionIDs).Delete(&model.AssetRepresentation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("asset_id = ?", assetID).Delete(&model.AssetVersion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&model.Asset{}, "id = ? AND user_id = ?", assetID, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resourceIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Where("user_id = ? AND id IN ?", userID, resourceIDs).Delete(&model.Resource{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func resourceTextQuery(query *gorm.DB, columns []string, resourceIDs []string) *gorm.DB {
|
||||
conditions := make([]string, 0, len(columns)*len(resourceIDs))
|
||||
args := make([]any, 0, len(columns)*len(resourceIDs))
|
||||
for _, column := range columns {
|
||||
for _, resourceID := range resourceIDs {
|
||||
conditions = append(conditions, column+" LIKE ?")
|
||||
args = append(args, "%"+resourceID+"%")
|
||||
}
|
||||
}
|
||||
if len(conditions) == 0 {
|
||||
return query
|
||||
}
|
||||
return query.Where("("+strings.Join(conditions, " OR ")+")", args...)
|
||||
}
|
||||
Reference in New Issue
Block a user