feat: add task assignment sharing
This commit is contained in:
84
backend/internal/tasks/service.go
Normal file
84
backend/internal/tasks/service.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
62
backend/internal/tasks/service_test.go
Normal file
62
backend/internal/tasks/service_test.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user