diff --git a/backend/internal/logic/ai/handlers_test.go b/backend/internal/logic/ai/handlers_test.go index 6c32e8e..58cd258 100644 --- a/backend/internal/logic/ai/handlers_test.go +++ b/backend/internal/logic/ai/handlers_test.go @@ -159,7 +159,7 @@ func TestCreateAISessionWithoutKeyReturnsAuditedErrorAndCreatesNoFormalObjects(t for _, model := range []any{ &models.SaAISession{}, &models.SaTask{}, - &models.SaDocument{}, + &models.SaDocumentTree{}, } { var count int64 require.NoError(t, database.Model(model).Count(&count).Error) @@ -272,7 +272,7 @@ func TestCreateAISessionReturnsIdentityDTOAndCompleteAuditWithoutAutomaticObject require.Equal(t, "ai_session_create", call.Action) require.Equal(t, "ready", call.Status) require.Empty(t, call.Error) - for _, model := range []any{&models.SaTask{}, &models.SaDocument{}} { + for _, model := range []any{&models.SaTask{}, &models.SaDocumentTree{}} { var count int64 require.NoError(t, database.Model(model).Count(&count).Error) require.Zero(t, count) diff --git a/backend/internal/logic/dataset/handlers_test.go b/backend/internal/logic/dataset/handlers_test.go index 71f4fef..fd47575 100644 --- a/backend/internal/logic/dataset/handlers_test.go +++ b/backend/internal/logic/dataset/handlers_test.go @@ -90,7 +90,7 @@ func TestDatasetAPIConnectsSourcesItemsAndSyncTasks(t *testing.T) { "projectId": project.Identity, }) require.Equal(t, http.StatusCreated, deposited.Code) - var depositedDocument models.SaDocument + var depositedDocument models.SaDocumentTree require.NoError(t, database.Where("project_id = ?", project.ID).First(&depositedDocument).Error) require.Equal(t, item.Title+".md", depositedDocument.Name) var depositedContent models.SaDocumentContent @@ -241,7 +241,7 @@ func newDatasetTestDatabase(t *testing.T) *gorm.DB { &models.SaDatasetSource{}, &models.SaDatasetItem{}, &models.SaProject{}, - &models.SaDocument{}, + &models.SaDocumentTree{}, &models.SaDocumentContent{}, )) return database diff --git a/backend/internal/logic/dataset/service.go b/backend/internal/logic/dataset/service.go index 5cc864f..65ab8c7 100644 --- a/backend/internal/logic/dataset/service.go +++ b/backend/internal/logic/dataset/service.go @@ -75,7 +75,7 @@ type ItemPatch struct { type DepositResult struct { DocumentIdentity string - ProjectIdentity string + ProjectIdentity string } type SourceRecord struct { @@ -363,9 +363,9 @@ func (s *Service) DepositItem(userID uint, itemIdentity, projectIdentity string) if !strings.HasSuffix(strings.ToLower(name), ".md") { name += ".md" } - var document models.SaDocument + var document models.SaDocumentTree err = s.db.Transaction(func(tx *gorm.DB) error { - document = models.SaDocument{ + document = models.SaDocumentTree{ ProjectID: project.ID, CreatedBy: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(body))), diff --git a/backend/internal/logic/documents/handlers.go b/backend/internal/logic/documents/handlers.go index 9b09e42..63e7ad8 100644 --- a/backend/internal/logic/documents/handlers.go +++ b/backend/internal/logic/documents/handlers.go @@ -145,7 +145,7 @@ func (h *Handler) create(c *gin.Context, folder bool) { } input.SourceInboxItemID = &inbox.ID } - var document *models.SaDocument + var document *models.SaDocumentTree var err error if folder { document, err = h.service.CreateFolder(userID, projectID, input) @@ -496,7 +496,7 @@ func requestScope(c *gin.Context) (uint, string, bool) { return userID, projectID, ok } -func documentDTO(document models.SaDocument, markdown *string, hasBlob bool) DocumentDTO { +func documentDTO(document models.SaDocumentTree, markdown *string, hasBlob bool) DocumentDTO { return DocumentDTO{ ID: document.Identity, ProjectID: document.ProjectIdentity, ParentID: document.ParentIdentity, Kind: document.Kind, Name: document.Name, Extension: document.Extension, MimeType: document.MimeType, diff --git a/backend/internal/logic/documents/handlers_test.go b/backend/internal/logic/documents/handlers_test.go index ec747cd..9b14c82 100644 --- a/backend/internal/logic/documents/handlers_test.go +++ b/backend/internal/logic/documents/handlers_test.go @@ -10,7 +10,7 @@ import ( func TestSharedPreviewBodySanitizesMarkdownHTMLAndUnsafeLinks(t *testing.T) { body := sharedPreviewBody(SharedDocument{ - Document: models.SaDocument{Name: "shared.md", Extension: ".md"}, + Document: models.SaDocumentTree{Name: "shared.md", Extension: ".md"}, Content: &models.SaDocumentContent{ Markdown: "# 标题\n\n\n\n[危险链接](javascript:alert(2))", }, @@ -23,7 +23,7 @@ func TestSharedPreviewBodySanitizesMarkdownHTMLAndUnsafeLinks(t *testing.T) { func TestSharedPreviewBodyEmbedsSupportedBlobWithoutDownloadLink(t *testing.T) { body := sharedPreviewBody(SharedDocument{ - Document: models.SaDocument{Name: "manual.pdf", Extension: ".pdf", MimeType: "application/pdf"}, + Document: models.SaDocumentTree{Name: "manual.pdf", Extension: ".pdf", MimeType: "application/pdf"}, Blob: &models.SaDocumentBlob{StorageKey: "blob"}, }, "https://example.test/content") diff --git a/backend/internal/logic/documents/service.go b/backend/internal/logic/documents/service.go index 50bc7c6..0a6e67d 100644 --- a/backend/internal/logic/documents/service.go +++ b/backend/internal/logic/documents/service.go @@ -69,7 +69,7 @@ type StoredBlob struct { } type SharedDocument struct { - Document models.SaDocument + Document models.SaDocumentTree Content *models.SaDocumentContent Blob *models.SaDocumentBlob } @@ -94,26 +94,26 @@ func (s *Service) database() *gorm.DB { return models.DBService } -func (s *Service) List(userID uint, projectIdentity string) ([]models.SaDocument, error) { +func (s *Service) List(userID uint, projectIdentity string) ([]models.SaDocumentTree, error) { project, err := s.ownedProject(userID, projectIdentity) if err != nil { return nil, err } - var documents []models.SaDocument + var documents []models.SaDocumentTree err = s.database().Where("project_id = ?", project.ID). Order("kind asc, normalized_name asc, id asc").Find(&documents).Error return documents, err } -func (s *Service) CreateFolder(userID uint, projectIdentity string, input CreateInput) (*models.SaDocument, error) { +func (s *Service) CreateFolder(userID uint, projectIdentity string, input CreateInput) (*models.SaDocumentTree, error) { return s.createNode(userID, projectIdentity, input, models.DocumentKindFolder, "", "", 0) } -func (s *Service) CreateMarkdown(userID uint, projectIdentity string, input CreateInput) (*models.SaDocument, error) { +func (s *Service) CreateMarkdown(userID uint, projectIdentity string, input CreateInput) (*models.SaDocumentTree, error) { if strings.TrimSpace(filepath.Ext(input.Name)) == "" { input.Name += ".md" } - var created *models.SaDocument + var created *models.SaDocumentTree err := s.database().Transaction(func(tx *gorm.DB) error { service := NewService(tx, s.root) document, err := service.createNode(userID, projectIdentity, input, models.DocumentKindFile, ".md", "text/markdown; charset=utf-8", int64(len([]byte(input.Markdown)))) @@ -129,7 +129,7 @@ func (s *Service) CreateMarkdown(userID uint, projectIdentity string, input Crea return created, err } -func (s *Service) createNode(userID uint, projectIdentity string, input CreateInput, kind, extension, mimeType string, size int64) (*models.SaDocument, error) { +func (s *Service) createNode(userID uint, projectIdentity string, input CreateInput, kind, extension, mimeType string, size int64) (*models.SaDocumentTree, error) { project, err := s.ownedProject(userID, projectIdentity) if err != nil { return nil, err @@ -149,11 +149,11 @@ func (s *Service) createNode(userID uint, projectIdentity string, input CreateIn if kind == models.DocumentKindFile { extension = strings.ToLower(filepath.Ext(name)) } - document := &models.SaDocument{ + document := &models.SaDocumentTree{ ProjectID: project.ID, ProjectIdentity: project.Identity, ParentID: parentID, ParentIdentity: parentIdentity, ParentScope: identityValue(parentIdentity), - CreatedBy: userID, SourceInboxItemID: input.SourceInboxItemID, + CreatedBy: userID, SourceInboxItemID: input.SourceInboxItemID, Kind: kind, Name: name, NormalizedName: normalizeName(name), Extension: extension, MimeType: mimeType, Size: size, Revision: 1, ScanStatus: "not_scanned", @@ -167,8 +167,8 @@ func (s *Service) createNode(userID uint, projectIdentity string, input CreateIn return document, nil } -func (s *Service) CreateUploaded(userID uint, projectIdentity, parentIdentity string, stored StoredBlob) (*models.SaDocument, error) { - var created *models.SaDocument +func (s *Service) CreateUploaded(userID uint, projectIdentity, parentIdentity string, stored StoredBlob) (*models.SaDocumentTree, error) { + var created *models.SaDocumentTree err := s.database().Transaction(func(tx *gorm.DB) error { service := NewService(tx, s.root) document, err := service.createNode(userID, projectIdentity, CreateInput{ @@ -197,7 +197,7 @@ func (s *Service) Get(userID uint, projectIdentity, documentIdentity string) (*S if err != nil { return nil, err } - var document models.SaDocument + var document models.SaDocumentTree if err := s.database().Where("identity = ? AND project_id = ?", strings.TrimSpace(documentIdentity), project.ID).First(&document).Error; err != nil { return nil, err } @@ -212,7 +212,7 @@ func (s *Service) Update(userID uint, projectIdentity, documentIdentity string, if err != nil { return err } - var document models.SaDocument + var document models.SaDocumentTree if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("identity = ? AND project_id = ?", strings.TrimSpace(documentIdentity), project.ID). First(&document).Error; err != nil { @@ -306,7 +306,7 @@ func (s *Service) Delete(userID uint, projectIdentity, documentIdentity string) if err != nil { return err } - var document models.SaDocument + var document models.SaDocumentTree if err := s.database().Where("identity = ? AND project_id = ?", documentIdentity, project.ID).First(&document).Error; err != nil { return err } @@ -314,7 +314,7 @@ func (s *Service) Delete(userID uint, projectIdentity, documentIdentity string) if err != nil { return err } - return s.database().Where("id IN ?", ids).Delete(&models.SaDocument{}).Error + return s.database().Where("id IN ?", ids).Delete(&models.SaDocumentTree{}).Error } func (s *Service) Store(projectIdentity, originalName string, source io.Reader) (StoredBlob, error) { @@ -482,7 +482,7 @@ func (s *Service) ResolveShare(token string) (*SharedDocument, error) { ).First(&share).Error; err != nil { return nil, ErrShareNotFound } - var document models.SaDocument + var document models.SaDocumentTree if err := s.database().Where("id = ?", share.DocumentID).First(&document).Error; err != nil { return nil, ErrShareNotFound } @@ -508,7 +508,7 @@ func (s *Service) Export(document SharedDocument, format string) ([]byte, string } } -func (s *Service) loadDocument(document models.SaDocument) (*SharedDocument, error) { +func (s *Service) loadDocument(document models.SaDocumentTree) (*SharedDocument, error) { result := &SharedDocument{Document: document} var content models.SaDocumentContent if err := s.database().Where("document_id = ?", document.ID).First(&content).Error; err == nil { @@ -536,7 +536,7 @@ func (s *Service) resolveParent(projectID uint, identity string) (*uint, *string if identity == "" { return nil, nil, nil } - var parent models.SaDocument + var parent models.SaDocumentTree if err := s.database().Where("identity = ? AND project_id = ? AND kind = ?", identity, projectID, models.DocumentKindFolder).First(&parent).Error; err != nil { return nil, nil, ErrInvalidParent } @@ -559,7 +559,7 @@ func (s *Service) availableName(projectID uint, parentID *uint, name string) (st } func (s *Service) nameExists(projectID uint, parentID *uint, normalized string, exceptID uint) bool { - query := s.database().Model(&models.SaDocument{}).Where("project_id = ? AND normalized_name = ?", projectID, normalized) + query := s.database().Model(&models.SaDocumentTree{}).Where("project_id = ? AND normalized_name = ?", projectID, normalized) if parentID == nil { query = query.Where("parent_id IS NULL") } else { @@ -578,7 +578,7 @@ func (s *Service) isDescendant(ancestorID, candidateID uint) (bool, error) { if current == ancestorID { return true, nil } - var document models.SaDocument + var document models.SaDocumentTree if err := s.database().Unscoped().Select("parent_id").Where("id = ?", current).First(&document).Error; err != nil { return false, err } @@ -594,7 +594,7 @@ func (s *Service) subtreeIDs(projectID, rootID uint) ([]uint, error) { result := []uint{rootID} frontier := []uint{rootID} for len(frontier) > 0 { - var children []models.SaDocument + var children []models.SaDocumentTree if err := s.database().Select("id").Where("project_id = ? AND parent_id IN ?", projectID, frontier).Find(&children).Error; err != nil { return nil, err } diff --git a/backend/internal/logic/documents/service_test.go b/backend/internal/logic/documents/service_test.go index 70115d1..92ce806 100644 --- a/backend/internal/logic/documents/service_test.go +++ b/backend/internal/logic/documents/service_test.go @@ -60,10 +60,10 @@ func TestDocumentTreeRevisionMoveDeleteAndShareLifecycle(t *testing.T) { require.NoError(t, service.Delete(user.ID, project.Identity, folder.Identity)) var visible int64 - require.NoError(t, database.Model(&models.SaDocument{}).Where("project_id = ?", project.ID).Count(&visible).Error) + require.NoError(t, database.Model(&models.SaDocumentTree{}).Where("project_id = ?", project.ID).Count(&visible).Error) require.Zero(t, visible) var retained int64 - require.NoError(t, database.Unscoped().Model(&models.SaDocument{}).Where("project_id = ?", project.ID).Count(&retained).Error) + require.NoError(t, database.Unscoped().Model(&models.SaDocumentTree{}).Where("project_id = ?", project.ID).Count(&retained).Error) require.Equal(t, int64(3), retained) } diff --git a/backend/internal/logic/inbox/service.go b/backend/internal/logic/inbox/service.go index 6c1688d..a8540ab 100644 --- a/backend/internal/logic/inbox/service.go +++ b/backend/internal/logic/inbox/service.go @@ -279,7 +279,7 @@ func createConfirmedObject(tx *gorm.DB, item models.SaInboxItem, suggestion mode if !strings.HasSuffix(strings.ToLower(name), ".md") { name += ".md" } - document := models.SaDocument{ + document := models.SaDocumentTree{ ProjectID: item.ProjectID, ProjectIdentity: item.ProjectIdentity, CreatedBy: item.CreatedBy, SourceInboxItemID: &sourceInboxItemID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", diff --git a/backend/internal/logic/inbox/service_test.go b/backend/internal/logic/inbox/service_test.go index 22e6de6..4e08e69 100644 --- a/backend/internal/logic/inbox/service_test.go +++ b/backend/internal/logic/inbox/service_test.go @@ -155,7 +155,7 @@ func TestConfirmCreatesOnlySelectedSavedSuggestionsWithInboxIdentity(t *testing. require.Equal(t, fixture.owner.ID, task.CreatedBy) requireSourceInboxIdentity(t, item, task.SourceInboxItemID, task.SourceInboxItemIdentity) - var documents []models.SaDocument + var documents []models.SaDocumentTree require.NoError(t, fixture.database.Order("id asc").Find(&documents).Error) require.Len(t, documents, 2) require.Equal(t, "会议纪要.md", documents[0].Name) @@ -231,7 +231,7 @@ func TestConfirmIsTransactionalWhenASelectedWriteFails(t *testing.T) { const callbackName = "test:fail_inbox_document_create" require.NoError(t, fixture.database.Callback().Create().Before("gorm:create").Register(callbackName, func(tx *gorm.DB) { - document, ok := tx.Statement.Dest.(*models.SaDocument) + document, ok := tx.Statement.Dest.(*models.SaDocumentTree) if ok && document.Name == "失败资料.md" { tx.AddError(errors.New("simulated source write failure")) } @@ -266,7 +266,7 @@ func TestRepeatedConfirmReturnsPersistedResultAfterRelatedObjectsChange(t *testi requireFormalObjectCounts(t, fixture.database, 1, 1, 0) sourceInboxItemID := item.ID - extraDocument := models.SaDocument{ + extraDocument := models.SaDocumentTree{ ProjectID: item.ProjectID, CreatedBy: fixture.owner.ID, SourceInboxItemID: &sourceInboxItemID, Kind: models.DocumentKindFile, Name: "后续关联资料.md", NormalizedName: "后续关联资料.md", Extension: ".md", Revision: 1, @@ -348,7 +348,7 @@ func requireFormalObjectCounts(t *testing.T, database *gorm.DB, tasks, notes, so want int64 }{ {model: &models.SaTask{}, want: tasks}, - {model: &models.SaDocument{}, want: notes + sources}, + {model: &models.SaDocumentTree{}, want: notes + sources}, } { var count int64 require.NoError(t, database.Model(check.model).Count(&count).Error) diff --git a/backend/internal/logic/projects/service_test.go b/backend/internal/logic/projects/service_test.go index 9d94a4f..2b55a43 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.SaDocument{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.SaDocument{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, 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.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.go b/backend/internal/logic/projects/workspace.go index a222bb0..f23e0ee 100644 --- a/backend/internal/logic/projects/workspace.go +++ b/backend/internal/logic/projects/workspace.go @@ -10,11 +10,11 @@ import ( ) type workspaceCounts struct { - inbox int64 - tasks int64 - aiSessions int64 - documents int64 - cronPlans int64 + inbox int64 + tasks int64 + aiSessions int64 + documents int64 + cronPlans int64 } // Workspace 在确认项目归属后执行首屏聚合;后续子查询只接收已授权的内部主键。 @@ -90,7 +90,7 @@ func (s *Service) workspaceCounts(projectID uint) (workspaceCounts, error) { if err := models.DBService.Model(&models.SaAISession{}).Where("project_id = ?", projectID).Count(&counts.aiSessions).Error; err != nil { return counts, err } - if err := models.DBService.Model(&models.SaDocument{}).Where("project_id = ?", projectID).Count(&counts.documents).Error; err != nil { + if err := models.DBService.Model(&models.SaDocumentTree{}).Where("project_id = ?", projectID).Count(&counts.documents).Error; err != nil { return counts, err } if err := models.DBService.Model(&models.SaCronPlan{}).Where("project_id = ?", projectID).Count(&counts.cronPlans).Error; err != nil { @@ -224,7 +224,7 @@ func (s *Service) workspaceAISessions(projectID uint, projectIdentity string) ([ } func (s *Service) workspaceDocuments(projectID uint, projectIdentity string) ([]WorkspaceDocumentDTO, error) { - var documents []models.SaDocument + var documents []models.SaDocumentTree if err := models.DBService.Where("project_id = ?", projectID).Order("updated_at desc, id desc").Limit(50).Find(&documents).Error; err != nil { return nil, err } diff --git a/backend/internal/logic/projects/workspace_test.go b/backend/internal/logic/projects/workspace_test.go index f5e26ed..227ce8f 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.SaDocument{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.SaDocument{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, 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} 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.go b/backend/internal/logic/search/service.go index a3f39d0..641b010 100644 --- a/backend/internal/logic/search/service.go +++ b/backend/internal/logic/search/service.go @@ -92,7 +92,7 @@ func (s *Service) Search(userID uint, query string) ([]SearchResultDTO, error) { }) } - var documents []models.SaDocument + var documents []models.SaDocumentTree documentFilter := fmt.Sprintf(`(sa_projects.owner_id = ? OR EXISTS ( SELECT 1 FROM sa_task_shares JOIN sa_tasks ON sa_tasks.id = sa_task_shares.task_id diff --git a/backend/internal/logic/search/service_postgres_test.go b/backend/internal/logic/search/service_postgres_test.go index 14f7896..b76c5ae 100644 --- a/backend/internal/logic/search/service_postgres_test.go +++ b/backend/internal/logic/search/service_postgres_test.go @@ -21,7 +21,7 @@ func TestPostgresSearchMatchesChinesePartialKeywords(t *testing.T) { project := models.SaProject{OwnerID: owner.ID, Name: "中文回调平台", Identifier: "PG-CALLBACK"} require.NoError(t, database.Create(&project).Error) require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "回调任务", Status: "open"}).Error) - createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocument{Name: "说明.md"}, "回调验签流程") + createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocumentTree{Name: "说明.md"}, "回调验签流程") results, err := NewService(database).Search(owner.ID, "回调") @@ -38,8 +38,8 @@ func TestPostgresSearchKeepsAssignmentAndExplicitShareBoundaries(t *testing.T) { assigned := models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, AssigneeID: &viewer.ID, Title: "边界词已指派", Status: "open"} require.NoError(t, database.Create(&assigned).Error) require.NoError(t, database.Create(&models.SaTask{ProjectID: project.ID, CreatedBy: owner.ID, Title: "边界词私有任务", Status: "open"}).Error) - sharedDocument := createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocument{Name: "边界词共享笔记.md"}, "") - createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocument{Name: "边界词私有笔记.md"}, "") + sharedDocument := createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocumentTree{Name: "边界词共享笔记.md"}, "") + createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocumentTree{Name: "边界词私有笔记.md"}, "") require.NoError(t, database.Create(&models.SaTaskShare{TaskID: assigned.ID, ObjectType: "document", ObjectID: sharedDocument.ID}).Error) results, err := NewService(database).Search(viewer.ID, "边界词") @@ -70,7 +70,7 @@ func TestPostgresSearchUsesStableFairLimitAndRuneBoundedSnippets(t *testing.T) { Status: "open", UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute), }).Error) - createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocument{ + createPostgresSearchDocument(t, database, project, owner.ID, models.SaDocumentTree{ Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index), Name: fmt.Sprintf("稳定排序笔记 %02d.md", index), UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute), @@ -110,9 +110,9 @@ func createPostgresSearchDocument( database *gorm.DB, project models.SaProject, ownerID uint, - document models.SaDocument, + document models.SaDocumentTree, markdown string, -) models.SaDocument { +) models.SaDocumentTree { t.Helper() document.ProjectID = project.ID document.ProjectIdentity = project.Identity diff --git a/backend/internal/logic/search/service_test.go b/backend/internal/logic/search/service_test.go index cc2ebaf..fc88ce4 100644 --- a/backend/internal/logic/search/service_test.go +++ b/backend/internal/logic/search/service_test.go @@ -52,7 +52,7 @@ func TestSearchAppliesStableFairGlobalLimitAcrossResultTypes(t *testing.T) { Status: "open", UpdatedAt: baseTime.Add(time.Duration(index) * time.Minute), }).Error) - document := models.SaDocument{ + document := models.SaDocumentTree{ Identity: fmt.Sprintf("00000000-0000-7003-8000-%012d", index), ProjectID: project.ID, CreatedBy: 7, @@ -136,9 +136,9 @@ func TestSearchOnlyReturnsNavigableProjectTaskAndDocumentResults(t *testing.T) { require.Len(t, results, 3) } -func createSearchDocument(t *testing.T, database *gorm.DB, projectID, userID uint, name, markdown, identity string, updatedAt time.Time) models.SaDocument { +func createSearchDocument(t *testing.T, database *gorm.DB, projectID, userID uint, name, markdown, identity string, updatedAt time.Time) models.SaDocumentTree { t.Helper() - document := models.SaDocument{ + document := models.SaDocumentTree{ Identity: identity, ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown", Size: int64(len([]byte(markdown))), Revision: 1, diff --git a/backend/internal/logic/tasks/concurrency_postgres_test.go b/backend/internal/logic/tasks/concurrency_postgres_test.go index 682b98e..24f99dc 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.SaDocument{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, CreatedBy: 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 857fcc4..cb41180 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.SaDocument{ProjectID: first.ID, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "旧项目资料.md", NormalizedName: "旧项目资料.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: first.ID, CreatedBy: 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.go b/backend/internal/logic/tasks/service.go index 3135a9c..576cf17 100644 --- a/backend/internal/logic/tasks/service.go +++ b/backend/internal/logic/tasks/service.go @@ -269,7 +269,7 @@ func utcOptionalTime(value *time.Time) *time.Time { func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectID uint) error { var count int64 - if err := tx.Model(&models.SaDocument{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil { + if err := tx.Model(&models.SaDocumentTree{}).Where("id = ? AND project_id = ?", objectID, projectID).Count(&count).Error; err != nil { return err } if count == 0 { diff --git a/backend/internal/logic/tasks/service_test.go b/backend/internal/logic/tasks/service_test.go index 535bdac..fe2719c 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.SaDocument{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, CreatedBy: 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.SaDocument{ProjectID: 1, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "合同背景.md", NormalizedName: "合同背景.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: 1, CreatedBy: 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.SaDocument{ProjectID: 2, CreatedBy: 1, Kind: models.DocumentKindFile, Name: "Other project.md", NormalizedName: "other project.md", Extension: ".md", Revision: 1} + document := models.SaDocumentTree{ProjectID: 2, CreatedBy: 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.go b/backend/internal/models/document_tree.go similarity index 95% rename from backend/internal/models/document.go rename to backend/internal/models/document_tree.go index ac0aca8..336f742 100644 --- a/backend/internal/models/document.go +++ b/backend/internal/models/document_tree.go @@ -11,7 +11,7 @@ const ( DocumentKindFile = "file" ) -type SaDocument struct { +type SaDocumentTree struct { ID uint `gorm:"primaryKey"` Identity string `gorm:"type:char(36);uniqueIndex"` ProjectID uint `gorm:"index;not null;uniqueIndex:uidx_sa_document_sibling,priority:1"` @@ -37,6 +37,6 @@ type SaDocument struct { DeletedAt gorm.DeletedAt `gorm:"index"` } -func (SaDocument) TableName() string { +func (SaDocumentTree) TableName() string { return "sa_documents" } diff --git a/backend/internal/models/identity.go b/backend/internal/models/identity.go index 0b13aca..71ff87f 100644 --- a/backend/internal/models/identity.go +++ b/backend/internal/models/identity.go @@ -75,7 +75,7 @@ func (m *SaTask) BeforeCreate(tx *gorm.DB) error { return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity) } -func (m *SaDocument) BeforeCreate(tx *gorm.DB) error { +func (m *SaDocumentTree) BeforeCreate(tx *gorm.DB) error { if err := ensureIdentity(&m.Identity); err != nil { return err } @@ -85,7 +85,7 @@ func (m *SaDocument) BeforeCreate(tx *gorm.DB) error { if err := resolveIdentity(tx, &SaUser{}, m.CreatedBy, &m.CreatedByIdentity); err != nil { return err } - if err := resolveOptionalIdentity(tx, &SaDocument{}, m.ParentID, &m.ParentIdentity); err != nil { + if err := resolveOptionalIdentity(tx, &SaDocumentTree{}, m.ParentID, &m.ParentIdentity); err != nil { return err } return resolveOptionalIdentity(tx, &SaInboxItem{}, m.SourceInboxItemID, &m.SourceInboxItemIdentity) @@ -95,7 +95,7 @@ func (m *SaDocumentShare) BeforeCreate(tx *gorm.DB) error { if err := ensureIdentity(&m.Identity); err != nil { return err } - if err := resolveIdentity(tx, &SaDocument{}, m.DocumentID, &m.DocumentIdentity); err != nil { + if err := resolveIdentity(tx, &SaDocumentTree{}, m.DocumentID, &m.DocumentIdentity); err != nil { return err } return nil @@ -234,7 +234,7 @@ func resolveEntityIdentity(tx *gorm.DB, entityType string, entityID uint, identi case "task": return resolveIdentity(tx, &SaTask{}, entityID, identity) case "document": - return resolveIdentity(tx, &SaDocument{}, entityID, identity) + return resolveIdentity(tx, &SaDocumentTree{}, entityID, identity) case "ai_session": return resolveIdentity(tx, &SaAISession{}, entityID, identity) case "tag": diff --git a/backend/internal/models/new.go b/backend/internal/models/new.go index 0c224cd..cdf99b4 100644 --- a/backend/internal/models/new.go +++ b/backend/internal/models/new.go @@ -36,7 +36,7 @@ func AutoMigrate(database *gorm.DB) error { &SaInboxItem{}, &SaInboxSuggestion{}, &SaTask{}, - &SaDocument{}, + &SaDocumentTree{}, &SaDocumentContent{}, &SaDocumentBlob{}, &SaDocumentShare{}, diff --git a/backend/internal/seed/demo.go b/backend/internal/seed/demo.go index 5f65e38..dbd4904 100644 --- a/backend/internal/seed/demo.go +++ b/backend/internal/seed/demo.go @@ -257,7 +257,7 @@ func firstOrCreateAISession(tx *gorm.DB, session models.SaAISession) error { } func firstOrCreateDocument(tx *gorm.DB, projectID, userID uint, name, markdown string) error { - var document models.SaDocument + var document models.SaDocumentTree err := tx.Where("project_id = ? AND normalized_name = ?", projectID, strings.ToLower(name)).First(&document).Error if err == nil { return nil @@ -265,7 +265,7 @@ func firstOrCreateDocument(tx *gorm.DB, projectID, userID uint, name, markdown s if err != gorm.ErrRecordNotFound { return err } - document = models.SaDocument{ + document = models.SaDocumentTree{ ProjectID: projectID, CreatedBy: userID, Kind: models.DocumentKindFile, Name: name, NormalizedName: strings.ToLower(name), Extension: ".md", MimeType: "text/markdown; charset=utf-8", Size: int64(len([]byte(markdown))), diff --git a/backend/internal/seed/demo_postgres_test.go b/backend/internal/seed/demo_postgres_test.go index 4042068..d6e5468 100644 --- a/backend/internal/seed/demo_postgres_test.go +++ b/backend/internal/seed/demo_postgres_test.go @@ -147,7 +147,7 @@ func TestPostgresDemoSeedSerializesConcurrentRunsForSameOwner(t *testing.T) { assertPostgresDemoCount(t, database, &models.SaTask{}, "project_id IN ?", []any{projectIDs}, 6) assertPostgresDemoCount(t, database, &models.SaInboxItem{}, "project_id IN ?", []any{projectIDs}, 7) assertPostgresDemoCount(t, database, &models.SaAISession{}, "project_id = ?", []any{projects[0].ID}, 4) - assertPostgresDemoCount(t, database, &models.SaDocument{}, "project_id = ?", []any{projects[0].ID}, 2) + assertPostgresDemoCount(t, database, &models.SaDocumentTree{}, "project_id = ?", []any{projects[0].ID}, 2) assertPostgresDemoCount(t, database, &models.SaCronPlan{}, "project_id = ?", []any{projects[0].ID}, 3) assertPostgresDemoCount(t, database, &models.SaProjectChannel{}, "project_id = ?", []any{projects[0].ID}, 2) } @@ -216,7 +216,7 @@ func cleanupPostgresDemoSeed(database *gorm.DB, userID uint) { } database.Where("project_id IN ?", projectIDs).Delete(&models.SaProjectEvent{}) database.Where("project_id IN ?", projectIDs).Delete(&models.SaTask{}) - database.Where("project_id IN ?", projectIDs).Delete(&models.SaDocument{}) + database.Where("project_id IN ?", projectIDs).Delete(&models.SaDocumentTree{}) database.Where("project_id IN ?", projectIDs).Delete(&models.SaAISession{}) database.Where("project_id IN ?", projectIDs).Delete(&models.SaTag{}) database.Where("project_id IN ?", projectIDs).Delete(&models.SaProjectChannel{})