Files
ttstd-infinite-canvas/backend/internal/canvas/repair_resources.go

133 lines
4.5 KiB
Go

package canvas
import (
"encoding/json"
"errors"
"infinite-canvas/backend/internal/assets"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type CanvasResourceRepairIssue struct {
CanvasID string `json:"canvasId"`
References []assets.DocumentResourceReference `json:"missingResources"`
Repaired bool `json:"repaired"`
Reason string `json:"reason,omitempty"`
}
type CanvasResourceRepairReport struct {
Apply bool `json:"apply"`
Scanned int `json:"scanned"`
Repairable int `json:"repairable"`
Repaired int `json:"repaired"`
Unresolved int `json:"unresolved"`
Issues []CanvasResourceRepairIssue `json:"issues"`
}
// RepairCanvasResources is an explicit maintenance operation. It only removes
// invalid derived video previews, never original media, and retains the preimage.
// Each canvas commits independently with the normal ownership/revision checks.
func RepairCanvasResources(db *gorm.DB, apply bool) (CanvasResourceRepairReport, error) {
report := CanvasResourceRepairReport{Apply: apply, Issues: []CanvasResourceRepairIssue{}}
db = db.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)})
repo := repository.New(db)
svc := New(repo, nil)
after := ""
for {
var rows []model.CanvasProject
if err := db.Where("id > ?", after).Order("id ASC").Limit(100).Find(&rows).Error; err != nil {
return report, errors.New("读取画布失败;此前已提交的修复保留,请按报告重试")
}
if len(rows) == 0 {
return report, nil
}
for _, project := range rows {
after = project.ID
report.Scanned++
issue := CanvasResourceRepairIssue{CanvasID: project.ID}
raw, err := canvasProjectPayload(project)
if err != nil {
issue.Reason = "画布 JSON 无效,需人工核查"
report.Issues = append(report.Issues, issue)
report.Unresolved++
continue
}
refs, err := assets.CollectDocumentResourceReferences(string(raw))
if err != nil {
return report, errors.New("收集资源引用失败")
}
ids := map[string]struct{}{}
for _, ref := range refs {
ids[ref.ResourceID] = struct{}{}
}
resources, err := repo.ResourcesForUserIDs(project.UserID, assets.SortedIDs(ids))
if err != nil {
return report, errors.New("读取资源状态失败;未把查询失败视为缺失")
}
for _, resource := range resources {
if resource.Status == model.ResourceStatusReady {
delete(ids, resource.ID)
}
}
if len(ids) == 0 {
continue
}
for _, ref := range refs {
if _, missing := ids[ref.ResourceID]; missing {
ref.Source = "current"
issue.References = append(issue.References, ref)
}
}
var payload map[string]json.RawMessage
var nodes []map[string]json.RawMessage
if json.Unmarshal(raw, &payload) != nil || json.Unmarshal(payload["nodes"], &nodes) != nil {
issue.Reason = "节点数据无效,需人工核查"
} else {
for _, node := range nodes {
var metadata map[string]json.RawMessage
if json.Unmarshal(node["metadata"], &metadata) != nil || metadata["videoPreview"] == nil {
continue
}
previewRefs, parseErr := assets.CollectDocumentResourceReferences(string(metadata["videoPreview"]))
if parseErr != nil {
continue
}
for _, ref := range previewRefs {
if _, missing := ids[ref.ResourceID]; missing {
delete(metadata, "videoPreview")
node["metadata"], _ = json.Marshal(metadata)
break
}
}
}
payload["nodes"], _ = json.Marshal(nodes)
repaired, marshalErr := json.Marshal(payload)
if marshalErr != nil {
return report, marshalErr
}
if err := svc.ValidateCanvasMediaAssets(project.UserID, repaired); err != nil {
issue.Reason = "仍有主媒体缺失或素材绑定错误;未修改画布,请恢复有效历史或重新上传"
} else {
report.Repairable++
if apply {
if _, err := svc.RepairUserCanvasProject(project.UserID, repaired); err != nil {
issue.Reason = "修复保存失败(版本冲突或资源再次变化);未覆盖,请重新扫描"
} else {
issue.Repaired = true
report.Repaired++
}
}
}
}
if issue.Reason != "" {
report.Unresolved++
}
report.Issues = append(report.Issues, issue)
}
}
}