diff --git a/backend/internal/logic/dataset/service.go b/backend/internal/logic/dataset/service.go index 65ab8c7..ee85b32 100644 --- a/backend/internal/logic/dataset/service.go +++ b/backend/internal/logic/dataset/service.go @@ -366,7 +366,7 @@ func (s *Service) DepositItem(userID uint, itemIdentity, projectIdentity string) var document models.SaDocumentTree err = s.db.Transaction(func(tx *gorm.DB) error { document = models.SaDocumentTree{ - ProjectID: project.ID, CreatedBy: userID, Kind: models.DocumentKindFile, + ProjectID: project.ID, OwnerID: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(body))), Revision: 1, ScanStatus: "not_scanned", diff --git a/backend/internal/logic/documents/service.go b/backend/internal/logic/documents/service.go index 0a6e67d..d5ac7d6 100644 --- a/backend/internal/logic/documents/service.go +++ b/backend/internal/logic/documents/service.go @@ -153,7 +153,7 @@ func (s *Service) createNode(userID uint, projectIdentity string, input CreateIn ProjectID: project.ID, ProjectIdentity: project.Identity, ParentID: parentID, ParentIdentity: parentIdentity, ParentScope: identityValue(parentIdentity), - CreatedBy: userID, SourceInboxItemID: input.SourceInboxItemID, + OwnerID: userID, SourceInboxItemID: input.SourceInboxItemID, Kind: kind, Name: name, NormalizedName: normalizeName(name), Extension: extension, MimeType: mimeType, Size: size, Revision: 1, ScanStatus: "not_scanned", diff --git a/backend/internal/logic/inbox/service.go b/backend/internal/logic/inbox/service.go index a8540ab..4c7756b 100644 --- a/backend/internal/logic/inbox/service.go +++ b/backend/internal/logic/inbox/service.go @@ -280,7 +280,7 @@ func createConfirmedObject(tx *gorm.DB, item models.SaInboxItem, suggestion mode name += ".md" } document := models.SaDocumentTree{ - ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy, + ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, OwnerID: item.CreatedBy, SourceInboxItemID: &sourceInboxItemID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(suggestion.Body))), Revision: 1, ScanStatus: "not_scanned", diff --git a/backend/internal/logic/inbox/service_test.go b/backend/internal/logic/inbox/service_test.go index 4e08e69..161f802 100644 --- a/backend/internal/logic/inbox/service_test.go +++ b/backend/internal/logic/inbox/service_test.go @@ -162,7 +162,8 @@ func TestConfirmCreatesOnlySelectedSavedSuggestionsWithInboxIdentity(t *testing. require.Equal(t, "背景资料.md", documents[1].Name) for _, document := range documents { require.Equal(t, fixture.project.ID, document.ProjectID) - require.Equal(t, fixture.owner.ID, document.CreatedBy) + require.Equal(t, fixture.owner.ID, document.OwnerID) + require.Equal(t, fixture.owner.Identity, document.OwnerIdentity) requireSourceInboxIdentity(t, item, document.SourceInboxItemID, document.SourceInboxItemIdentity) } var secondContent models.SaDocumentContent @@ -267,7 +268,7 @@ func TestRepeatedConfirmReturnsPersistedResultAfterRelatedObjectsChange(t *testi sourceInboxItemID := item.ID extraDocument := models.SaDocumentTree{ - ProjectID: item.ProjectID, CreatedBy: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID, + ProjectID: item.ProjectID, OwnerID: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID, Kind: models.DocumentKindFile, Name: "后续关联资料.md", NormalizedName: "后续关联资料.md", Extension: ".md", Revision: 1, } diff --git a/backend/internal/logic/projects/service_test.go b/backend/internal/logic/projects/service_test.go index 2b55a43..999ebf3 100644 --- a/backend/internal/logic/projects/service_test.go +++ b/backend/internal/logic/projects/service_test.go @@ -127,8 +127,8 @@ func TestWorkspaceMatchesFrontendContract(t *testing.T) { require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Confirm IA", Description: "Review channel layout.", Status: "open", DueAt: &nextRun}).Error) require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: 1, Title: "Archive notes", Description: "Move outdated notes.", Status: "done"}).Error) require.NoError(t, database.Create(&models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI prototype critique", Context: "Discuss channel behavior."}).Error) - require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", Revision: 1}).Error) - require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", Revision: 1}).Error) + require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", Revision: 1}).Error) + require.NoError(t, database.Create(&models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", Revision: 1}).Error) require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap-a1", SortOrder: 7}).Error) require.NoError(t, database.Create(&models.SaProjectChannel{ProjectID: other.ID, Title: "Other Board", Icon: "link", URL: "https://example.com/other", SortOrder: 7}).Error) require.NoError(t, database.Create(&models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly inbox review reminder", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, LastResult: "Not run yet"}).Error) diff --git a/backend/internal/logic/projects/workspace_test.go b/backend/internal/logic/projects/workspace_test.go index 227ce8f..5a6b977 100644 --- a/backend/internal/logic/projects/workspace_test.go +++ b/backend/internal/logic/projects/workspace_test.go @@ -33,8 +33,8 @@ func TestWorkspaceUsesIdentitiesUTCTimesAndProjectScopedTags(t *testing.T) { inbox := models.SaInboxItem{ProjectID: project.ID, CreatedBy: 1, SourceType: "Manual", Title: "Collect notes", Body: "Turn research into tasks.", Status: "open", CreatedAt: createdAt, UpdatedAt: updatedAt} task := models.SaTask{ProjectID: project.ID, CreatedBy: 1, TagID: &projectTag.ID, Title: "Confirm IA", Description: "Review channel layout.", Status: "done", DueAt: &nextRun, CreatedAt: createdAt, UpdatedAt: updatedAt} session := models.SaAISession{ProjectID: project.ID, CreatedBy: 1, Title: "UI critique", Context: "Discuss channel behavior.", CreatedAt: createdAt, UpdatedAt: updatedAt} - firstDocument := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", MimeType: "text/markdown", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt} - secondDocument := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", MimeType: "application/pdf", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt} + firstDocument := models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Workbench principles.md", NormalizedName: "workbench principles.md", Extension: ".md", MimeType: "text/markdown", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt} + secondDocument := models.SaDocumentTree{ProjectID: project.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Reference board.pdf", NormalizedName: "reference board.pdf", Extension: ".pdf", MimeType: "application/pdf", Revision: 1, CreatedAt: createdAt, UpdatedAt: updatedAt} channel := models.SaProjectChannel{ProjectID: project.ID, Title: "Roadmap Board", Icon: "link", URL: "https://example.com/roadmap", SortOrder: 7, CreatedAt: createdAt, UpdatedAt: updatedAt} plan := models.SaCronPlan{ProjectID: project.ID, CreatedBy: 1, Title: "Weekly review", Schedule: "0 9 * * 1", NextRunAt: &nextRun, Enabled: true, CreatedAt: createdAt, UpdatedAt: updatedAt} for _, record := range []any{&inbox, &task, &session, &firstDocument, &secondDocument, &channel, &plan} { diff --git a/backend/internal/logic/search/service_postgres_test.go b/backend/internal/logic/search/service_postgres_test.go index b76c5ae..26576e5 100644 --- a/backend/internal/logic/search/service_postgres_test.go +++ b/backend/internal/logic/search/service_postgres_test.go @@ -116,7 +116,7 @@ func createPostgresSearchDocument( t.Helper() document.ProjectID = project.ID document.ProjectIdentity = project.Identity - document.CreatedBy = ownerID + document.OwnerID = ownerID document.Kind = models.DocumentKindFile document.Extension = ".md" document.MimeType = "text/markdown; charset=utf-8" diff --git a/backend/internal/logic/search/service_test.go b/backend/internal/logic/search/service_test.go index fc88ce4..8822df5 100644 --- a/backend/internal/logic/search/service_test.go +++ b/backend/internal/logic/search/service_test.go @@ -55,7 +55,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) { document := models.SaDocumentTree{ Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index), ProjectID: project.ID, - CreatedBy: 7, + OwnerID: 7, Kind: models.DocumentKindFile, Name: fmt.Sprintf("稳定排序文档 %02d.md", index), NormalizedName: fmt.Sprintf("稳定排序文档 %02d.md", index), @@ -139,7 +139,7 @@ func TestSearchOnlyReturnsNavigableProjectTaskAndDocumentResults(t *testing.T) { func createSearchDocument(t *testing.T, database *gorm.DB, projectID, userID uint, name, markdown, identity string, updatedAt time.Time) models.SaDocumentTree { t.Helper() document := models.SaDocumentTree{ - Identity: identity, ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile, + Identity: identity, ProjectID: projectID, OwnerID: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown", Size: int64(len([]byte(markdown))), Revision: 1, UpdatedAt: updatedAt, diff --git a/backend/internal/logic/tasks/concurrency_postgres_test.go b/backend/internal/logic/tasks/concurrency_postgres_test.go index 24f99dc..328aa78 100644 --- a/backend/internal/logic/tasks/concurrency_postgres_test.go +++ b/backend/internal/logic/tasks/concurrency_postgres_test.go @@ -29,7 +29,7 @@ func TestPostgresMovePreventsOldProjectShareFromBeingInsertedConcurrently(t *tes require.NoError(t, database.Create(&first).Error) require.NoError(t, database.Create(&second).Error) task := models.SaTask{ProjectID: first.ID, CreatedBy: owner.ID, Title: "Move", Status: "open"} - document := models.SaDocumentTree{ProjectID: first.ID, CreatedBy: owner.ID, Kind: models.DocumentKindFile, Name: "Old context.md", NormalizedName: "old context.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: first.ID, OwnerID: owner.ID, Kind: models.DocumentKindFile, Name: "Old context.md", NormalizedName: "old context.md", Extension: ".md", Revision: 1} require.NoError(t, database.Create(&task).Error) require.NoError(t, database.Create(&document).Error) t.Cleanup(func() { diff --git a/backend/internal/logic/tasks/handlers_test.go b/backend/internal/logic/tasks/handlers_test.go index cb41180..5a4ba43 100644 --- a/backend/internal/logic/tasks/handlers_test.go +++ b/backend/internal/logic/tasks/handlers_test.go @@ -50,7 +50,7 @@ func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T require.NoError(t, database.Create(&tag).Error) task := models.SaTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"} require.NoError(t, database.Create(&task).Error) - document := models.SaDocumentTree{ProjectID: first.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: first.ID, OwnerID: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1} require.NoError(t, database.Create(&document).Error) require.NoError(t, NewService(database).ShareObject(task.ID, "document", document.ID)) body := bytes.NewBufferString(fmt.Sprintf(`{"title":"迁移任务","description":"已移动","completed":false,"nextProjectId":%q}`, strings.ToUpper(second.Identity))) diff --git a/backend/internal/logic/tasks/service_test.go b/backend/internal/logic/tasks/service_test.go index fe2719c..f24b550 100644 --- a/backend/internal/logic/tasks/service_test.go +++ b/backend/internal/logic/tasks/service_test.go @@ -24,7 +24,7 @@ func TestUpdateLocksTaskBeforeProjectValidation(t *testing.T) { func TestShareObjectLocksTaskBeforeObjectValidation(t *testing.T) { database, _, project, task := newTaskLockFixture(t) - document := models.SaDocumentTree{ProjectID: project.ID, CreatedBy: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: project.ID, OwnerID: task.CreatedBy, Kind: models.DocumentKindFile, Name: "Context.md", NormalizedName: "context.md", Extension: ".md", Revision: 1} require.NoError(t, database.Create(&document).Error) queries := captureTaskQueryOrder(t, database) @@ -39,7 +39,7 @@ func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) { database := newTestDB(t) assigneeID := uint(2) task := models.SaTask{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"} - document := models.SaDocumentTree{ProjectID: 1, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: 1, OwnerID: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1} require.NoError(t, database.Create(&task).Error) require.NoError(t, database.Create(&document).Error) service := NewService() @@ -68,7 +68,7 @@ func TestShareObjectRejectsUnsupportedType(t *testing.T) { func TestShareObjectRejectsObjectFromAnotherProject(t *testing.T) { database := newTestDB(t) task := models.SaTask{ProjectID: 1, CreatedBy: 1, Title: "Review"} - document := models.SaDocumentTree{ProjectID: 2, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: 2, OwnerID: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1} require.NoError(t, database.Create(&task).Error) require.NoError(t, database.Create(&document).Error) service := NewService() diff --git a/backend/internal/models/document_tree.go b/backend/internal/models/document_tree.go index 336f742..aafb930 100644 --- a/backend/internal/models/document_tree.go +++ b/backend/internal/models/document_tree.go @@ -19,8 +19,8 @@ type SaDocumentTree struct { ParentID *uint `gorm:"index"` ParentIdentity *string `gorm:"type:char(36);index"` ParentScope string `gorm:"type:char(36);not null;default:'';uniqueIndex:uidx_sa_document_sibling,priority:2"` - CreatedBy uint `gorm:"index;not null"` - CreatedByIdentity string `gorm:"type:char(36);index"` + OwnerID uint `gorm:"index;not null"` + OwnerIdentity string `gorm:"type:char(36);index"` SourceInboxItemID *uint `gorm:"index"` SourceInboxItemIdentity *string `gorm:"type:char(36);index"` Kind string `gorm:"size:16;not null"` diff --git a/backend/internal/models/identity.go b/backend/internal/models/identity.go index 71ff87f..80a9b88 100644 --- a/backend/internal/models/identity.go +++ b/backend/internal/models/identity.go @@ -82,7 +82,7 @@ func (m *SaDocumentTree) BeforeCreate(tx *gorm.DB) error { if err := resolveIdentity(tx, &SaProject{}, m.ProjectID, &m.ProjectIdentity); err != nil { return err } - if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil { + if err := resolveIdentity(tx, &SaUser{}, m.OwnerID, &m.OwnerIdentity); err != nil { return err } if err := resolveOptionalIdentity(tx, &SaDocumentTree{}, m.ParentID, &m.ParentIdentity); err != nil { diff --git a/backend/internal/models/legacy_tables.go b/backend/internal/models/legacy_tables.go index 0fe6844..c3706c3 100644 --- a/backend/internal/models/legacy_tables.go +++ b/backend/internal/models/legacy_tables.go @@ -44,5 +44,21 @@ func renameLegacyTables(database *gorm.DB) error { return fmt.Errorf("rename legacy table %s to %s: %w", table.legacy, table.current, err) } } + if migrator.HasTable("sa_documents") { + for _, column := range []struct { + legacy string + current string + }{ + {legacy: "created_by", current: "owner_id"}, + {legacy: "created_by_identity", current: "owner_identity"}, + } { + if !migrator.HasColumn("sa_documents", column.legacy) || migrator.HasColumn("sa_documents", column.current) { + continue + } + if err := migrator.RenameColumn("sa_documents", column.legacy, column.current); err != nil { + return fmt.Errorf("rename sa_documents column %s to %s: %w", column.legacy, column.current, err) + } + } + } return nil } diff --git a/backend/internal/models/migrations_test.go b/backend/internal/models/migrations_test.go index b584a8b..417ac53 100644 --- a/backend/internal/models/migrations_test.go +++ b/backend/internal/models/migrations_test.go @@ -87,6 +87,36 @@ func TestAutoMigrateRejectsAmbiguousLegacyAndCurrentTables(t *testing.T) { require.EqualError(t, err, "cannot rename legacy table senlin_agent_users: target table sa_users already exists") } +func TestRenameLegacyTablesRenamesDocumentOwnerColumns(t *testing.T) { + database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, database.Exec(` + CREATE TABLE sa_documents ( + id integer primary key, + created_by integer not null, + created_by_identity text + ) + `).Error) + require.NoError(t, database.Exec(` + INSERT INTO sa_documents (id, created_by, created_by_identity) + VALUES (1, 7, 'owner-identity') + `).Error) + + require.NoError(t, renameLegacyTables(database)) + require.False(t, database.Migrator().HasColumn("sa_documents", "created_by")) + require.False(t, database.Migrator().HasColumn("sa_documents", "created_by_identity")) + require.True(t, database.Migrator().HasColumn("sa_documents", "owner_id")) + require.True(t, database.Migrator().HasColumn("sa_documents", "owner_identity")) + + var stored struct { + OwnerID uint + OwnerIdentity string + } + require.NoError(t, database.Table("sa_documents").Where("id = ?", 1).Take(&stored).Error) + require.Equal(t, uint(7), stored.OwnerID) + require.Equal(t, "owner-identity", stored.OwnerIdentity) +} + func projectIdentifiers(projects []SaProject) []string { result := make([]string, 0, len(projects)) for _, project := range projects { diff --git a/backend/internal/seed/demo.go b/backend/internal/seed/demo.go index dbd4904..2814ae2 100644 --- a/backend/internal/seed/demo.go +++ b/backend/internal/seed/demo.go @@ -266,7 +266,7 @@ func firstOrCreateDocument(tx *gorm.DB, projectID, userID uint, name, markdown s return err } document = models.SaDocumentTree{ - ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile, + ProjectID: projectID, OwnerID: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(markdown))), Revision: 1, ScanStatus: "not_scanned",