From 530fc43cca3c2198997c908010ac4e991ec0c35a Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sat, 18 Jul 2026 16:01:25 +0800 Subject: [PATCH] feat: add task assignment sharing --- backend/internal/tasks/service.go | 84 ++++++++++++++++++++++++++ backend/internal/tasks/service_test.go | 62 +++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 backend/internal/tasks/service.go create mode 100644 backend/internal/tasks/service_test.go diff --git a/backend/internal/tasks/service.go b/backend/internal/tasks/service.go new file mode 100644 index 0000000..8bc65b4 --- /dev/null +++ b/backend/internal/tasks/service.go @@ -0,0 +1,84 @@ +package tasks + +import ( + "errors" + "fmt" + + "gorm.io/gorm" + "senlinai-agent/backend/internal/domain" +) + +type Service struct { + db *gorm.DB +} + +type LinkedObject struct { + ObjectType string `json:"object_type"` + ObjectID uint `json:"object_id"` +} + +func NewService(database *gorm.DB) *Service { + return &Service{db: database} +} + +func (s *Service) Assign(taskID uint, assigneeID uint) error { + return s.db.Transaction(func(tx *gorm.DB) error { + var task domain.Task + if err := tx.First(&task, taskID).Error; err != nil { + return err + } + if err := tx.Model(&task).Update("assignee_id", assigneeID).Error; err != nil { + return err + } + return tx.Create(&domain.ProjectEvent{ + ProjectID: task.ProjectID, + ActorID: task.CreatedBy, + EventType: "task_assigned", + EntityType: "task", + EntityID: task.ID, + Summary: fmt.Sprintf("Task assigned to user %d", assigneeID), + }).Error + }) +} + +func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) error { + if objectType != "note" && objectType != "source" { + return errors.New("unsupported shared object type") + } + return s.db.Transaction(func(tx *gorm.DB) error { + var task domain.Task + if err := tx.First(&task, taskID).Error; err != nil { + return err + } + if err := tx.Create(&domain.TaskShare{TaskID: taskID, ObjectType: objectType, ObjectID: objectID}).Error; err != nil { + return err + } + return tx.Create(&domain.ProjectEvent{ + ProjectID: task.ProjectID, + ActorID: task.CreatedBy, + EventType: "task_object_shared", + EntityType: "task", + EntityID: task.ID, + Summary: fmt.Sprintf("Shared %s %d with task", objectType, objectID), + }).Error + }) +} + +func (s *Service) VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObject, error) { + var task domain.Task + if err := s.db.First(&task, taskID).Error; err != nil { + return nil, err + } + if task.AssigneeID == nil || *task.AssigneeID != viewerID { + return []LinkedObject{}, nil + } + var shares []domain.TaskShare + if err := s.db.Where("task_id = ?", taskID).Find(&shares).Error; err != nil { + return nil, err + } + objects := make([]LinkedObject, 0, len(shares)) + for _, share := range shares { + objects = append(objects, LinkedObject{ObjectType: share.ObjectType, ObjectID: share.ObjectID}) + } + return objects, nil +} diff --git a/backend/internal/tasks/service_test.go b/backend/internal/tasks/service_test.go new file mode 100644 index 0000000..3a2eaf4 --- /dev/null +++ b/backend/internal/tasks/service_test.go @@ -0,0 +1,62 @@ +package tasks + +import ( + "fmt" + "testing" + + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + "senlinai-agent/backend/internal/domain" +) + +func TestAssigneeOnlySeesExplicitlySharedObjects(t *testing.T) { + database := newTestDB(t) + assigneeID := uint(2) + task := domain.Task{ProjectID: 1, CreatedBy: 1, AssigneeID: &assigneeID, Title: "处理合同"} + note := domain.Note{ProjectID: 1, CreatedBy: 1, Title: "合同背景", Markdown: "只在共享后可见"} + require.NoError(t, database.Create(&task).Error) + require.NoError(t, database.Create(¬e).Error) + service := NewService(database) + + before, err := service.VisibleLinkedObjects(task.ID, assigneeID) + require.NoError(t, err) + require.Empty(t, before) + + require.NoError(t, service.ShareObject(task.ID, "note", note.ID)) + after, err := service.VisibleLinkedObjects(task.ID, assigneeID) + require.NoError(t, err) + require.Len(t, after, 1) + require.Equal(t, "note", after[0].ObjectType) + require.Equal(t, note.ID, after[0].ObjectID) +} + +func TestShareObjectRejectsUnsupportedType(t *testing.T) { + database := newTestDB(t) + service := NewService(database) + + err := service.ShareObject(1, "ai_session", 9) + + require.ErrorContains(t, err, "unsupported shared object type") +} + +func TestAssignRecordsProjectEvent(t *testing.T) { + database := newTestDB(t) + task := domain.Task{ProjectID: 7, CreatedBy: 1, Title: "安排评审"} + require.NoError(t, database.Create(&task).Error) + service := NewService(database) + + require.NoError(t, service.Assign(task.ID, 2)) + + var event domain.ProjectEvent + require.NoError(t, database.Where("project_id = ? AND entity_type = ? AND entity_id = ?", 7, "task", task.ID).First(&event).Error) + require.Equal(t, "task_assigned", event.EventType) +} + +func newTestDB(t *testing.T) *gorm.DB { + t.Helper() + 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, domain.AutoMigrate(database)) + return database +}