feat: 完成短剧项目空间与结构化资产创作闭环 (#38)
* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP * feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环 Refs #30 * fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置 * feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环 Refs #30 --------- Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
@@ -17,6 +17,7 @@ var (
|
||||
ErrInsufficientCredits = errors.New("insufficient credits")
|
||||
ErrRedeemCodeInvalid = errors.New("redeem code invalid")
|
||||
ErrActiveTaskLimit = errors.New("active task limit reached")
|
||||
ErrProjectTaskLimit = errors.New("project active task limit reached")
|
||||
ErrTaskNotRetryable = errors.New("task is not retryable")
|
||||
ErrBillingStateConflict = errors.New("billing state conflict")
|
||||
)
|
||||
@@ -73,6 +74,12 @@ func (r *Repository) ChannelModels(channelID string, includeDisabled bool) ([]mo
|
||||
return items, query.Find(&items).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ChannelModelsIncludingDeleted(channelID string) ([]model.ChannelModel, error) {
|
||||
var items []model.ChannelModel
|
||||
err := r.db.Unscoped().Where("channel_id = ?", channelID).Order("created_at asc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (r *Repository) ChannelModelByID(channelID string, id string) (*model.ChannelModel, error) {
|
||||
var item model.ChannelModel
|
||||
if err := r.db.First(&item, "id = ? AND channel_id = ?", id, channelID).Error; err != nil {
|
||||
@@ -93,12 +100,39 @@ func (r *Repository) SaveChannelModel(item *model.ChannelModel) error {
|
||||
return r.db.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteChannelModel(channelID string, id string, modelsJSON string, now time.Time) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.ChannelModel{}).
|
||||
Where("id = ? AND channel_id = ?", id, channelID).
|
||||
Updates(map[string]any{"enabled": false, "price_version": gorm.Expr("price_version + 1"), "updated_at": now})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := tx.Where("id = ? AND channel_id = ?", id, channelID).Delete(&model.ChannelModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
channelResult := tx.Model(&model.ModelChannel{}).
|
||||
Where("id = ? AND scope = ?", channelID, model.ChannelScopeSystem).
|
||||
Updates(map[string]any{"models_json": modelsJSON, "updated_at": now})
|
||||
if channelResult.Error != nil {
|
||||
return channelResult.Error
|
||||
}
|
||||
if channelResult.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) CreateMissingChannelModels(items []model.ChannelModel) (int64, error) {
|
||||
if len(items) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// 拉取目录可能与其他管理员操作并发,唯一键冲突时保留已有定价配置。
|
||||
result := r.db.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "channel_id"}, {Name: "model_key"}}, DoNothing: true}).Create(&items)
|
||||
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&items)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
@@ -153,11 +187,14 @@ func (r *Repository) CreditLedgerReferenceExists(referenceKey string) (bool, err
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *model.BillingOrder, activeTaskLimit int) error {
|
||||
func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *model.BillingOrder, activeTaskLimit int, domainProjectID string, projectTaskLimit int) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enforceProjectTaskLimit(tx, task.UserID, domainProjectID, projectTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := reserveBillingOrder(tx, order); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,21 +202,27 @@ func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *mo
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) CreateTaskWithActiveLimit(task *model.Task, activeTaskLimit int) error {
|
||||
func (r *Repository) CreateTaskWithActiveLimit(task *model.Task, activeTaskLimit int, domainProjectID string, projectTaskLimit int) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enforceProjectTaskLimit(tx, task.UserID, domainProjectID, projectTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(task).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) RetryTaskWithBilling(userID string, taskID string, order *model.BillingOrder, activeTaskLimit int) (*model.Task, error) {
|
||||
func (r *Repository) RetryTaskWithBilling(userID string, taskID string, order *model.BillingOrder, activeTaskLimit int, domainProjectID string, projectTaskLimit int) (*model.Task, error) {
|
||||
var task model.Task
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := enforceActiveTaskLimit(tx, userID, activeTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enforceProjectTaskLimit(tx, userID, domainProjectID, projectTaskLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
if order != nil {
|
||||
if err := reserveBillingOrder(tx, order); err != nil {
|
||||
return err
|
||||
@@ -217,6 +260,23 @@ func enforceActiveTaskLimit(tx *gorm.DB, userID string, activeTaskLimit int) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func enforceProjectTaskLimit(tx *gorm.DB, userID string, projectID string, limit int) error {
|
||||
if projectID == "" || limit <= 0 {
|
||||
return nil
|
||||
}
|
||||
canvasIDs := tx.Model(&model.CanvasProject{}).Select("id").Where("user_id = ? AND project_id = ?", userID, projectID)
|
||||
var count int64
|
||||
if err := tx.Model(&model.Task{}).
|
||||
Where("user_id = ? AND status IN ? AND (project_id = ? OR project_id IN (?))", userID, []model.TaskStatus{model.TaskStatusQueued, model.TaskStatusRunning}, projectID, canvasIDs).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= int64(limit) {
|
||||
return ErrProjectTaskLimit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) ReserveBillingOrder(order *model.BillingOrder) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
return reserveBillingOrder(tx, order)
|
||||
|
||||
143
backend/internal/repository/project_character.go
Normal file
143
backend/internal/repository/project_character.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) ProjectCharacterAsset(userID string, projectID string, assetID string) (*model.Asset, error) {
|
||||
var asset model.Asset
|
||||
err := r.db.Table("assets").Select("assets.*").
|
||||
Joins("JOIN project_asset_links ON project_asset_links.asset_id = assets.id").
|
||||
Where("assets.id = ? AND assets.user_id = ? AND assets.category = ? AND project_asset_links.project_id = ?", assetID, userID, model.AssetCategoryCharacter, projectID).
|
||||
First(&asset).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &asset, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AssetVersion(id string) (*model.AssetVersion, error) {
|
||||
var version model.AssetVersion
|
||||
if err := r.db.First(&version, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &version, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AssetRepresentations(assetVersionID string) ([]model.AssetRepresentation, error) {
|
||||
var representations []model.AssetRepresentation
|
||||
err := r.db.Where("asset_version_id = ?", assetVersionID).Order("role asc, created_at asc").Find(&representations).Error
|
||||
return representations, err
|
||||
}
|
||||
|
||||
func (r *Repository) CharacterVoiceBinding(assetVersionID string) (*model.CharacterVoiceBinding, error) {
|
||||
var binding model.CharacterVoiceBinding
|
||||
if err := r.db.First(&binding, "asset_version_id = ?", assetVersionID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &binding, nil
|
||||
}
|
||||
|
||||
func (r *Repository) VoiceProfileForUser(userID string, id string) (*model.VoiceProfile, error) {
|
||||
var profile model.VoiceProfile
|
||||
if err := r.db.First(&profile, "id = ? AND user_id = ?", id, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *Repository) VoiceProfiles(userID string) ([]model.VoiceProfile, error) {
|
||||
var profiles []model.VoiceProfile
|
||||
err := r.db.Where("user_id = ? AND status = ?", userID, "active").Order("created_at asc").Find(&profiles).Error
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
func (r *Repository) EnsureVoiceProfiles(profiles []model.VoiceProfile) error {
|
||||
if len(profiles) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "provider"}, {Name: "voice_key"}},
|
||||
DoNothing: true,
|
||||
}).Create(&profiles).Error
|
||||
}
|
||||
|
||||
// CreateProjectCharacter 将角色身份、首版本和项目关联放入同一事务。
|
||||
func (r *Repository) CreateProjectCharacter(projectID string, asset *model.Asset, version *model.AssetVersion, link *model.ProjectAssetLink) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).
|
||||
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": asset.UpdatedAt}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// SaveCharacterVersion 通过不可变版本替换角色当前状态,旧镜头仍可继续引用历史版本。
|
||||
func (r *Repository) SaveCharacterVersion(projectID string, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := saveCharacterVersion(tx, asset, version, representations, voice); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).
|
||||
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmProjectCharacterCandidate 保证角色归并版本与候选确认同时生效,失败时不会留下孤立版本。
|
||||
func (r *Repository) ConfirmProjectCharacterCandidate(candidate *model.ProjectAssetCandidate, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := saveCharacterVersion(tx, asset, version, representations, voice); err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&model.ProjectAssetCandidate{}).
|
||||
Where("id = ? AND project_id = ? AND status = ?", candidate.ID, candidate.ProjectID, "pending_confirmation").
|
||||
Updates(map[string]any{"status": candidate.Status, "resolved_asset_id": candidate.ResolvedAssetID, "updated_at": candidate.UpdatedAt})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", candidate.ProjectID).
|
||||
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": candidate.UpdatedAt}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func saveCharacterVersion(tx *gorm.DB, asset *model.Asset, version *model.AssetVersion, representations []model.AssetRepresentation, voice *model.CharacterVoiceBinding) error {
|
||||
if err := tx.Create(version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(representations) > 0 {
|
||||
if err := tx.Create(&representations).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if voice != nil {
|
||||
if err := tx.Create(voice).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result := tx.Model(&model.Asset{}).Where("id = ? AND user_id = ?", asset.ID, asset.UserID).Updates(map[string]any{
|
||||
"kind": asset.Kind, "category": asset.Category, "status": asset.Status, "primary_version_id": asset.PrimaryVersionID,
|
||||
"title": asset.Title, "payload_json": asset.PayloadJSON, "updated_at": asset.UpdatedAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -387,7 +388,7 @@ func (r *Repository) Tasks(userID string, limit int, projectID string, activeOnl
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
query := r.db.Select("id", "session_id", "project_id", "type", "status", "stage", "progress", "prompt", "operation", "provider", "model", "billing_order_id", "attempts", "started_at", "completed_at", "created_at", "updated_at").
|
||||
query := r.db.Select("id", "session_id", "project_id", "type", "status", "stage", "progress", "prompt", "operation", "provider", "model", "result_json", "billing_order_id", "attempts", "started_at", "completed_at", "created_at", "updated_at").
|
||||
Where("user_id = ?", userID)
|
||||
if strings.TrimSpace(projectID) != "" {
|
||||
query = query.Where("project_id = ?", strings.TrimSpace(projectID))
|
||||
@@ -460,6 +461,12 @@ func (r *Repository) SystemChannels(includeDisabled bool) ([]model.ModelChannel,
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func (r *Repository) HistoricalSystemChannelReferences() ([]model.ModelChannel, error) {
|
||||
var channels []model.ModelChannel
|
||||
err := r.db.Unscoped().Select("id", "name").Where("scope = ?", model.ChannelScopeSystem).Order("created_at asc").Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func (r *Repository) AdminSystemChannels(keyword string, interfaceType string, status string, limit int, offset int) ([]model.ModelChannel, int64, error) {
|
||||
var channels []model.ModelChannel
|
||||
var total int64
|
||||
@@ -507,6 +514,28 @@ func (r *Repository) AdminSystemChannel(id string) (*model.ModelChannel, error)
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteSystemChannel(id string) error {
|
||||
now := time.Now()
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
channelResult := tx.Model(&model.ModelChannel{}).
|
||||
Where("id = ? AND scope = ?", id, model.ChannelScopeSystem).
|
||||
Updates(map[string]any{"api_key": "", "enabled": false, "updated_at": now})
|
||||
if channelResult.Error != nil {
|
||||
return channelResult.Error
|
||||
}
|
||||
if channelResult.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
if err := tx.Model(&model.ChannelModel{}).Where("channel_id = ?", id).Updates(map[string]any{"enabled": false, "updated_at": now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("channel_id = ?", id).Delete(&model.ChannelModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ? AND scope = ?", id, model.ChannelScopeSystem).Delete(&model.ModelChannel{}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ApiCallLogs(userID string, admin bool, limit int) ([]model.ApiCallLog, error) {
|
||||
var logs []model.ApiCallLog
|
||||
if limit <= 0 || limit > 200 {
|
||||
@@ -634,7 +663,7 @@ func (r *Repository) Assets(userID string) ([]model.Asset, error) {
|
||||
|
||||
func (r *Repository) AssetSummaries(userID string) ([]model.Asset, error) {
|
||||
var assets []model.Asset
|
||||
err := r.db.Select("id", "kind", "title", "created_at", "updated_at").Order("updated_at desc").Find(&assets, "user_id = ?", userID).Error
|
||||
err := r.db.Select("id", "kind", "category", "status", "primary_version_id", "title", "created_at", "updated_at").Order("updated_at desc").Find(&assets, "user_id = ?", userID).Error
|
||||
return assets, err
|
||||
}
|
||||
|
||||
@@ -649,7 +678,7 @@ func (r *Repository) AssetForUser(userID string, id string) (*model.Asset, error
|
||||
func (r *Repository) UpsertAsset(asset *model.Asset) error {
|
||||
result := r.db.Model(&model.Asset{}).
|
||||
Where("id = ? AND user_id = ?", asset.ID, asset.UserID).
|
||||
Updates(map[string]any{"kind": asset.Kind, "title": asset.Title, "payload_json": asset.PayloadJSON, "updated_at": asset.UpdatedAt})
|
||||
Updates(map[string]any{"kind": asset.Kind, "category": asset.Category, "status": asset.Status, "primary_version_id": asset.PrimaryVersionID, "title": asset.Title, "payload_json": asset.PayloadJSON, "updated_at": asset.UpdatedAt})
|
||||
if result.Error != nil || result.RowsAffected > 0 {
|
||||
return result.Error
|
||||
}
|
||||
@@ -657,7 +686,19 @@ func (r *Repository) UpsertAsset(asset *model.Asset) error {
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteAsset(userID string, id string) error {
|
||||
return r.db.Delete(&model.Asset{}, "id = ? AND user_id = ?", id, userID).Error
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
versionIDs := tx.Model(&model.AssetVersion{}).Select("id").Where("asset_id = ?", id)
|
||||
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 = ?", id).Delete(&model.AssetVersion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.Asset{}, "id = ? AND user_id = ?", id, userID).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ReplaceAssets(userID string, assets []model.Asset) error {
|
||||
@@ -695,7 +736,7 @@ func (r *Repository) CanvasProjectForUser(userID string, id string) (*model.Canv
|
||||
func (r *Repository) UpsertCanvasProject(project *model.CanvasProject) error {
|
||||
result := r.db.Model(&model.CanvasProject{}).
|
||||
Where("id = ? AND user_id = ?", project.ID, project.UserID).
|
||||
Updates(map[string]any{"title": project.Title, "payload_json": project.PayloadJSON, "updated_at": project.UpdatedAt})
|
||||
Updates(map[string]any{"project_id": project.ProjectID, "title": project.Title, "payload_json": project.PayloadJSON, "updated_at": project.UpdatedAt})
|
||||
if result.Error != nil || result.RowsAffected > 0 {
|
||||
return result.Error
|
||||
}
|
||||
@@ -706,6 +747,623 @@ func (r *Repository) DeleteCanvasProject(userID string, id string) error {
|
||||
return r.db.Delete(&model.CanvasProject{}, "id = ? AND user_id = ?", id, userID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) Projects(userID string) ([]model.Project, error) {
|
||||
var projects []model.Project
|
||||
err := r.db.Where("user_id = ?", userID).Order("updated_at desc").Find(&projects).Error
|
||||
return projects, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectForUser(userID string, id string) (*model.Project, error) {
|
||||
var project model.Project
|
||||
if err := r.db.First(&project, "id = ? AND user_id = ?", id, userID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateProject(project *model.Project) error {
|
||||
return r.db.Create(project).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateProject(project *model.Project) error {
|
||||
return r.db.Model(&model.Project{}).Where("id = ? AND user_id = ?", project.ID, project.UserID).Updates(map[string]any{
|
||||
"name": project.Name, "type": project.Type, "aspect_ratio": project.AspectRatio, "source_type": project.SourceType,
|
||||
"description": project.Description, "style_preset_id": project.StylePresetID, "active_task_limit": project.ActiveTaskLimit,
|
||||
"status": project.Status, "revision": project.Revision, "updated_at": project.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteProject(userID string, id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.CanvasProject{}).Where("user_id = ? AND project_id = ?", userID, id).Update("project_id", "").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.CanvasUnitLink{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
shotIDs := tx.Model(&model.Shot{}).Select("id").Where("project_id = ?", id)
|
||||
if err := tx.Where("shot_id IN (?)", shotIDs).Delete(&model.ShotAssetReference{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.Shot{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
instanceIDs := tx.Model(&model.WorkflowInstance{}).Select("id").Where("project_id = ?", id)
|
||||
stepIDs := tx.Model(&model.WorkflowStepInstance{}).Select("id").Where("workflow_instance_id IN (?)", instanceIDs)
|
||||
if err := tx.Where("workflow_step_id IN (?)", stepIDs).Delete(&model.WorkflowStepTask{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("workflow_instance_id IN (?)", instanceIDs).Delete(&model.WorkflowStepInstance{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.WorkflowInstance{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.ProjectAssetLink{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.ProjectAssetCandidate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ?", id).Delete(&model.ProjectUnit{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.Project{}, "id = ? AND user_id = ?", id, userID).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) BumpProjectRevision(projectID string) error {
|
||||
return r.db.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectUnits(projectID string) ([]model.ProjectUnit, error) {
|
||||
var units []model.ProjectUnit
|
||||
err := r.db.Where("project_id = ?", projectID).Order("position asc, created_at asc").Find(&units).Error
|
||||
return units, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectUnitSummaries(projectID string) ([]model.ProjectUnit, error) {
|
||||
var units []model.ProjectUnit
|
||||
err := r.db.Select("id", "project_id", "kind", "title", "status", "position", "created_at", "updated_at").Where("project_id = ?", projectID).Order("position asc, created_at asc").Find(&units).Error
|
||||
return units, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetCount(projectID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.ProjectAssetLink{}).Where("project_id = ?", projectID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *Repository) CreateProjectUnit(unit *model.ProjectUnit) error {
|
||||
return r.db.Create(unit).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ImportProjectUnits(projectID string, units []model.ProjectUnit) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 分批写入仍处于同一事务,避免两千章导入超过 SQLite/PostgreSQL 单语句参数上限。
|
||||
if err := tx.CreateInBatches(&units, 100).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ReorderProjectUnits(projectID string, unitIDs []string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
for position, unitID := range unitIDs {
|
||||
result := tx.Model(&model.ProjectUnit{}).Where("id = ? AND project_id = ?", unitID, projectID).Updates(map[string]any{"position": position, "updated_at": now})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": now}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectUnit(projectID string, id string) (*model.ProjectUnit, error) {
|
||||
var unit model.ProjectUnit
|
||||
if err := r.db.First(&unit, "id = ? AND project_id = ?", id, projectID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &unit, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateProjectUnit(unit *model.ProjectUnit) error {
|
||||
return r.db.Model(&model.ProjectUnit{}).Where("id = ? AND project_id = ?", unit.ID, unit.ProjectID).Updates(map[string]any{
|
||||
"parent_id": unit.ParentID, "title": unit.Title, "source_text": unit.SourceText, "status": unit.Status, "position": unit.Position, "updated_at": unit.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteProjectUnit(projectID string, id string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("project_id = ? AND unit_id = ?", projectID, id).Delete(&model.CanvasUnitLink{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
shotIDs := tx.Model(&model.Shot{}).Select("id").Where("project_id = ? AND unit_id = ?", projectID, id)
|
||||
if err := tx.Where("shot_id IN (?)", shotIDs).Delete(&model.ShotAssetReference{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND unit_id = ?", projectID, id).Delete(&model.Shot{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
instanceIDs := tx.Model(&model.WorkflowInstance{}).Select("id").Where("project_id = ? AND unit_id = ?", projectID, id)
|
||||
stepIDs := tx.Model(&model.WorkflowStepInstance{}).Select("id").Where("workflow_instance_id IN (?)", instanceIDs)
|
||||
if err := tx.Where("workflow_step_id IN (?)", stepIDs).Delete(&model.WorkflowStepTask{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("workflow_instance_id IN (?)", instanceIDs).Delete(&model.WorkflowStepInstance{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND unit_id = ?", projectID, id).Delete(&model.WorkflowInstance{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND unit_id = ?", projectID, id).Delete(&model.ProjectAssetCandidate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Delete(&model.ProjectUnit{}, "id = ? AND project_id = ?", id, projectID)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) CanvasUnitLink(projectID string, canvasID string, unitID string) (*model.CanvasUnitLink, error) {
|
||||
var link model.CanvasUnitLink
|
||||
if err := r.db.First(&link, "project_id = ? AND canvas_id = ? AND unit_id = ?", projectID, canvasID, unitID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &link, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertCanvasUnitLink(link *model.CanvasUnitLink) error {
|
||||
result := r.db.Model(&model.CanvasUnitLink{}).Where("project_id = ? AND canvas_id = ? AND unit_id = ?", link.ProjectID, link.CanvasID, link.UnitID).Updates(map[string]any{"role": link.Role})
|
||||
if result.Error != nil || result.RowsAffected > 0 {
|
||||
return result.Error
|
||||
}
|
||||
return r.db.Create(link).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectCanvasSummaries(userID string, projectID string) ([]model.CanvasProject, error) {
|
||||
var canvases []model.CanvasProject
|
||||
err := r.db.Select("id", "user_id", "project_id", "title", "created_at", "updated_at").Where("user_id = ? AND project_id = ?", userID, projectID).Order("updated_at desc").Find(&canvases).Error
|
||||
return canvases, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectCanvasDocuments(userID string, projectID string) ([]model.CanvasProject, error) {
|
||||
var canvases []model.CanvasProject
|
||||
err := r.db.Select("id", "title", "payload_json").Where("user_id = ? AND project_id = ?", userID, projectID).Find(&canvases).Error
|
||||
return canvases, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectCanvasUnitLinks(projectID string) ([]model.CanvasUnitLink, error) {
|
||||
var links []model.CanvasUnitLink
|
||||
err := r.db.Where("project_id = ?", projectID).Order("created_at asc").Find(&links).Error
|
||||
return links, err
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteCanvasUnitLink(projectID string, canvasID string, unitID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Delete(&model.CanvasUnitLink{}, "project_id = ? AND canvas_id = ? AND unit_id = ?", projectID, canvasID, unitID)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) AssignCanvasToProject(userID string, canvasID string, projectID string) error {
|
||||
return r.db.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ?", canvasID, userID).Update("project_id", projectID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) UnassignCanvasFromProject(userID string, projectID string, canvasID string, payloadJSON string, updatedAt time.Time) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("project_id = ? AND canvas_id = ?", projectID, canvasID).Delete(&model.CanvasUnitLink{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&model.CanvasProject{}).Where("id = ? AND user_id = ? AND project_id = ?", canvasID, userID, projectID).Updates(map[string]any{
|
||||
"project_id": "", "payload_json": payloadJSON, "updated_at": updatedAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": updatedAt}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssets(userID string, projectID string) ([]model.Asset, error) {
|
||||
var assets []model.Asset
|
||||
err := r.db.Table("assets").Select("assets.*").Joins("JOIN project_asset_links ON project_asset_links.asset_id = assets.id").Where("assets.user_id = ? AND project_asset_links.project_id = ?", userID, projectID).Order("assets.updated_at desc").Scan(&assets).Error
|
||||
return assets, err
|
||||
}
|
||||
|
||||
// LinkProjectAsset 将首版本、素材领域字段、项目引用和修订号原子提交,避免产生半关联资产。
|
||||
func (r *Repository) LinkProjectAsset(asset *model.Asset, version *model.AssetVersion, link *model.ProjectAssetLink) (bool, error) {
|
||||
createdLink := false
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
created := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "project_id"}, {Name: "asset_id"}}, DoNothing: true}).Create(link)
|
||||
if created.Error != nil {
|
||||
return created.Error
|
||||
}
|
||||
if created.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
createdLink = true
|
||||
if version != nil {
|
||||
if err := tx.Create(version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
result := tx.Model(&model.Asset{}).Where("id = ? AND user_id = ?", asset.ID, asset.UserID).Updates(map[string]any{
|
||||
"category": asset.Category, "status": asset.Status, "primary_version_id": asset.PrimaryVersionID, "updated_at": asset.UpdatedAt,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", link.ProjectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
return createdLink, err
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteProjectAssetLink(projectID string, assetID string) error {
|
||||
return r.db.Delete(&model.ProjectAssetLink{}, "project_id = ? AND asset_id = ?", projectID, assetID).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetShotReferenceCount(projectID string, assetID string) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Table("shot_asset_references").
|
||||
Joins("JOIN shots ON shots.id = shot_asset_references.shot_id").
|
||||
Joins("JOIN asset_versions ON asset_versions.id = shot_asset_references.asset_version_id").
|
||||
Where("shots.project_id = ? AND asset_versions.asset_id = ?", projectID, assetID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetLinked(projectID string, assetID string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.ProjectAssetLink{}).Where("project_id = ? AND asset_id = ?", projectID, assetID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *Repository) AssetReferenceCount(assetID string) (int64, error) {
|
||||
var projectLinks int64
|
||||
if err := r.db.Model(&model.ProjectAssetLink{}).Where("asset_id = ?", assetID).Count(&projectLinks).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var shotLinks int64
|
||||
err := r.db.Table("shot_asset_references").Joins("JOIN asset_versions ON asset_versions.id = shot_asset_references.asset_version_id").Where("asset_versions.asset_id = ?", assetID).Count(&shotLinks).Error
|
||||
return projectLinks + shotLinks, err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateAssetDomain(asset *model.Asset) error {
|
||||
return r.db.Model(&model.Asset{}).Where("id = ? AND user_id = ?", asset.ID, asset.UserID).Updates(map[string]any{"category": asset.Category, "status": asset.Status, "primary_version_id": asset.PrimaryVersionID, "updated_at": asset.UpdatedAt}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) AssetVersions(assetID string) ([]model.AssetVersion, error) {
|
||||
var versions []model.AssetVersion
|
||||
err := r.db.Where("asset_id = ?", assetID).Order("version desc").Find(&versions).Error
|
||||
return versions, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetUsageRoles(projectID string, assetID string) ([]string, error) {
|
||||
var shotRoles []string
|
||||
if err := r.db.Table("shot_asset_references").
|
||||
Distinct("shot_asset_references.role").
|
||||
Joins("JOIN shots ON shots.id = shot_asset_references.shot_id").
|
||||
Joins("JOIN asset_versions ON asset_versions.id = shot_asset_references.asset_version_id").
|
||||
Where("shots.project_id = ? AND asset_versions.asset_id = ?", projectID, assetID).
|
||||
Order("shot_asset_references.role asc").
|
||||
Pluck("shot_asset_references.role", &shotRoles).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var representationRoles []string
|
||||
if err := r.db.Table("asset_representations").
|
||||
Distinct("asset_representations.role").
|
||||
Joins("JOIN asset_versions ON asset_versions.id = asset_representations.asset_version_id").
|
||||
Joins("JOIN project_asset_links ON project_asset_links.asset_id = asset_versions.asset_id").
|
||||
Where("project_asset_links.project_id = ? AND asset_versions.asset_id = ?", projectID, assetID).
|
||||
Pluck("asset_representations.role", &representationRoles).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := make(map[string]struct{}, len(shotRoles)+len(representationRoles))
|
||||
for _, role := range append(shotRoles, representationRoles...) {
|
||||
if role != "" {
|
||||
seen[role] = struct{}{}
|
||||
}
|
||||
}
|
||||
roles := make([]string, 0, len(seen))
|
||||
for role := range seen {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
sort.Strings(roles)
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AssetVersionForProject(projectID string, versionID string) (*model.AssetVersion, error) {
|
||||
var version model.AssetVersion
|
||||
err := r.db.Table("asset_versions").Select("asset_versions.*").Joins("JOIN project_asset_links ON project_asset_links.asset_id = asset_versions.asset_id").Where("project_asset_links.project_id = ? AND asset_versions.id = ?", projectID, versionID).First(&version).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &version, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateAssetVersion(version *model.AssetVersion) error {
|
||||
return r.db.Create(version).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectShots(projectID string) ([]model.Shot, error) {
|
||||
var shots []model.Shot
|
||||
err := r.db.Where("project_id = ?", projectID).Order("unit_id asc, position asc").Find(&shots).Error
|
||||
return shots, err
|
||||
}
|
||||
|
||||
func (r *Repository) SaveShot(shot *model.Shot, create bool) error {
|
||||
if create {
|
||||
return r.db.Create(shot).Error
|
||||
}
|
||||
return r.db.Model(&model.Shot{}).Where("id = ? AND project_id = ?", shot.ID, shot.ProjectID).Updates(map[string]any{
|
||||
"unit_id": shot.UnitID, "title": shot.Title, "description": shot.Description, "position": shot.Position,
|
||||
"duration_ms": shot.DurationMs, "status": shot.Status, "updated_at": shot.UpdatedAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ReplaceProjectUnitShots(projectID string, unitID string, shots []model.Shot) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
shotIDs := tx.Model(&model.Shot{}).Select("id").Where("project_id = ? AND unit_id = ?", projectID, unitID)
|
||||
if err := tx.Where("shot_id IN (?)", shotIDs).Delete(&model.ShotAssetReference{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND shot_id IN (?)", projectID, shotIDs).Delete(&model.ProjectAssetCandidate{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND unit_id = ?", projectID, unitID).Delete(&model.Shot{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&shots).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": time.Now()}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ShotForProject(projectID string, shotID string) (*model.Shot, error) {
|
||||
var shot model.Shot
|
||||
if err := r.db.First(&shot, "id = ? AND project_id = ?", shotID, projectID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &shot, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpsertShotAssetReference(reference *model.ShotAssetReference) error {
|
||||
result := r.db.Model(&model.ShotAssetReference{}).Where("shot_id = ? AND asset_version_id = ? AND role = ?", reference.ShotID, reference.AssetVersionID, reference.Role).Updates(map[string]any{"status": reference.Status})
|
||||
if result.Error != nil || result.RowsAffected > 0 {
|
||||
return result.Error
|
||||
}
|
||||
return r.db.Create(reference).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectShotAssetReferences(projectID string) ([]model.ShotAssetReference, error) {
|
||||
var references []model.ShotAssetReference
|
||||
err := r.db.Table("shot_asset_references").Select("shot_asset_references.*").
|
||||
Joins("JOIN shots ON shots.id = shot_asset_references.shot_id").
|
||||
Where("shots.project_id = ?", projectID).
|
||||
Order("shot_asset_references.created_at asc").Scan(&references).Error
|
||||
return references, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetCandidates(projectID string) ([]model.ProjectAssetCandidate, error) {
|
||||
var candidates []model.ProjectAssetCandidate
|
||||
err := r.db.Where("project_id = ?", projectID).Order("created_at asc").Find(&candidates).Error
|
||||
return candidates, err
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectAssetCandidate(projectID string, candidateID string) (*model.ProjectAssetCandidate, error) {
|
||||
var candidate model.ProjectAssetCandidate
|
||||
if err := r.db.First(&candidate, "id = ? AND project_id = ?", candidateID, projectID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &candidate, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateProjectAssetCandidates(candidates []model.ProjectAssetCandidate) error {
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Create(&candidates).Error
|
||||
}
|
||||
|
||||
// ConfirmProjectAssetCandidate 将正式资产身份、首版本、项目引用和候选状态放在同一事务中,避免出现半确认数据。
|
||||
func (r *Repository) ConfirmProjectAssetCandidate(candidate *model.ProjectAssetCandidate, asset *model.Asset, version *model.AssetVersion, link *model.ProjectAssetLink, createAsset bool) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if createAsset {
|
||||
if err := tx.Create(asset).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := tx.First(&model.Asset{}, "id = ? AND user_id = ?", asset.ID, asset.UserID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("project_id = ? AND asset_id = ?", link.ProjectID, link.AssetID).FirstOrCreate(link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&model.ProjectAssetCandidate{}).
|
||||
Where("id = ? AND project_id = ? AND status = ?", candidate.ID, candidate.ProjectID, "pending_confirmation").
|
||||
Updates(map[string]any{"status": candidate.Status, "resolved_asset_id": candidate.ResolvedAssetID, "updated_at": candidate.UpdatedAt})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", candidate.ProjectID).
|
||||
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": candidate.UpdatedAt}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) WorkflowTemplateVersion(templateKey string, version int) (*model.WorkflowTemplateVersion, error) {
|
||||
var template model.WorkflowTemplateVersion
|
||||
if err := r.db.First(&template, "template_key = ? AND version = ?", templateKey, version).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateWorkflowTemplateVersion(template *model.WorkflowTemplateVersion) error {
|
||||
return r.db.Create(template).Error
|
||||
}
|
||||
|
||||
func (r *Repository) ProjectWorkflowInstances(projectID string) ([]model.WorkflowInstance, error) {
|
||||
var instances []model.WorkflowInstance
|
||||
err := r.db.Where("project_id = ?", projectID).Order("created_at asc").Find(&instances).Error
|
||||
return instances, err
|
||||
}
|
||||
|
||||
func (r *Repository) WorkflowInstanceForScope(projectID string, unitID string, templateVersionID string) (*model.WorkflowInstance, error) {
|
||||
var instance model.WorkflowInstance
|
||||
if err := r.db.First(&instance, "project_id = ? AND unit_id = ? AND template_version_id = ?", projectID, unitID, templateVersionID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
func (r *Repository) WorkflowInstance(id string) (*model.WorkflowInstance, error) {
|
||||
var instance model.WorkflowInstance
|
||||
if err := r.db.First(&instance, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
func (r *Repository) WorkflowSteps(instanceID string) ([]model.WorkflowStepInstance, error) {
|
||||
var steps []model.WorkflowStepInstance
|
||||
err := r.db.Where("workflow_instance_id = ?", instanceID).Order("position asc").Find(&steps).Error
|
||||
return steps, err
|
||||
}
|
||||
|
||||
func (r *Repository) NextWorkflowStep(instanceID string, position int) (*model.WorkflowStepInstance, error) {
|
||||
var step model.WorkflowStepInstance
|
||||
if err := r.db.Where("workflow_instance_id = ? AND position > ?", instanceID, position).Order("position asc").First(&step).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateWorkflowInstance(instance *model.WorkflowInstance, steps []model.WorkflowStepInstance) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(instance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(steps) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&steps).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) WorkflowStepForProject(projectID string, stepID string) (*model.WorkflowStepInstance, error) {
|
||||
var step model.WorkflowStepInstance
|
||||
err := r.db.Table("workflow_step_instances").Select("workflow_step_instances.*").Joins("JOIN workflow_instances ON workflow_instances.id = workflow_step_instances.workflow_instance_id").Where("workflow_instances.project_id = ? AND workflow_step_instances.id = ?", projectID, stepID).First(&step).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &step, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateWorkflowStep(step *model.WorkflowStepInstance) error {
|
||||
return r.db.Model(&model.WorkflowStepInstance{}).Where("id = ? AND workflow_instance_id = ?", step.ID, step.WorkflowInstanceID).Updates(map[string]any{"status": step.Status, "output_json": step.OutputJSON, "error": step.Error, "started_at": step.StartedAt, "completed_at": step.CompletedAt, "updated_at": step.UpdatedAt}).Error
|
||||
}
|
||||
|
||||
// UpdateWorkflowProgress 原子保存当前步骤、下一步骤和实例状态,确保刷新后流程依赖仍可恢复。
|
||||
func (r *Repository) UpdateWorkflowProgress(step *model.WorkflowStepInstance, next *model.WorkflowStepInstance, instance *model.WorkflowInstance, projectID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.WorkflowStepInstance{}).Where("id = ? AND workflow_instance_id = ?", step.ID, step.WorkflowInstanceID).Updates(map[string]any{
|
||||
"status": step.Status, "output_json": step.OutputJSON, "error": step.Error, "started_at": step.StartedAt,
|
||||
"completed_at": step.CompletedAt, "updated_at": step.UpdatedAt,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if next != nil {
|
||||
if err := tx.Model(&model.WorkflowStepInstance{}).Where("id = ? AND workflow_instance_id = ?", next.ID, next.WorkflowInstanceID).
|
||||
Updates(map[string]any{"status": next.Status, "updated_at": next.UpdatedAt}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&model.WorkflowInstance{}).Where("id = ? AND project_id = ?", instance.ID, projectID).
|
||||
Updates(map[string]any{"status": instance.Status, "revision": instance.Revision, "updated_at": instance.UpdatedAt}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.Project{}).Where("id = ?", projectID).
|
||||
Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": step.UpdatedAt}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterWorkflowTaskOutput 将成功任务、流程步骤和产物表示写入同一事务,重复回填使用任务与用途唯一键幂等。
|
||||
func (r *Repository) RegisterWorkflowTaskOutput(step *model.WorkflowStepInstance, next *model.WorkflowStepInstance, instance *model.WorkflowInstance, projectID string, link *model.WorkflowStepTask, representation *model.AssetRepresentation) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("workflow_step_id = ? AND task_id = ?", link.WorkflowStepID, link.TaskID).FirstOrCreate(link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if representation != nil {
|
||||
if err := tx.Where("task_id = ? AND role = ?", representation.TaskID, representation.Role).FirstOrCreate(representation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stepResult := tx.Model(&model.WorkflowStepInstance{}).Where("id = ? AND workflow_instance_id = ?", step.ID, step.WorkflowInstanceID).Updates(map[string]any{
|
||||
"status": step.Status, "output_json": step.OutputJSON, "error": step.Error, "started_at": step.StartedAt,
|
||||
"completed_at": step.CompletedAt, "updated_at": step.UpdatedAt,
|
||||
})
|
||||
if stepResult.Error != nil {
|
||||
return stepResult.Error
|
||||
}
|
||||
if stepResult.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
if next != nil {
|
||||
nextResult := tx.Model(&model.WorkflowStepInstance{}).Where("id = ? AND workflow_instance_id = ?", next.ID, next.WorkflowInstanceID).Updates(map[string]any{"status": next.Status, "updated_at": next.UpdatedAt})
|
||||
if nextResult.Error != nil {
|
||||
return nextResult.Error
|
||||
}
|
||||
if nextResult.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
}
|
||||
instanceResult := tx.Model(&model.WorkflowInstance{}).Where("id = ? AND project_id = ?", instance.ID, projectID).Updates(map[string]any{"status": instance.Status, "revision": instance.Revision, "updated_at": instance.UpdatedAt})
|
||||
if instanceResult.Error != nil {
|
||||
return instanceResult.Error
|
||||
}
|
||||
if instanceResult.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
projectResult := tx.Model(&model.Project{}).Where("id = ?", projectID).Updates(map[string]any{"revision": gorm.Expr("revision + 1"), "updated_at": step.UpdatedAt})
|
||||
if projectResult.Error != nil {
|
||||
return projectResult.Error
|
||||
}
|
||||
if projectResult.RowsAffected != 1 {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) CanvasShareForProject(userID string, projectID string) (*model.CanvasShare, error) {
|
||||
var share model.CanvasShare
|
||||
if err := r.db.First(&share, "user_id = ? AND project_id = ?", userID, projectID).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user