fix(backend): harden write registrar boundaries

This commit is contained in:
2026-07-21 16:12:20 +08:00
parent 1fcbb31301
commit 80bec26839
17 changed files with 639 additions and 42 deletions

View File

@@ -7,6 +7,7 @@ import (
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"senlinai-agent/backend/internal/models"
)
@@ -15,8 +16,8 @@ type Service struct {
}
type LinkedObject struct {
ObjectType string `json:"object_type"`
ObjectID uint `json:"object_id"`
ObjectType string `json:"objectType"`
ObjectID string `json:"objectId"`
}
// NewService 接受数据库或上层事务,确保任务写入、标签调整和分享检查使用同一依赖。
@@ -35,13 +36,20 @@ func (s *Service) database() *gorm.DB {
return models.DBService
}
func (s *Service) Assign(taskID uint, assigneeID uint) error {
// Assign 在同一事务内由用户 identity 解析内部主键,并同步两个指派字段,避免 DTO 返回陈旧 identity。
func (s *Service) Assign(taskID uint, assigneeIdentity string) error {
return s.database().Transaction(func(tx *gorm.DB) error {
var task models.SenlinAgentTask
if err := tx.First(&task, taskID).Error; err != nil {
return err
}
if err := tx.Model(&task).Update("assignee_id", assigneeID).Error; err != nil {
var assignee models.SenlinAgentUser
if err := tx.Where("identity = ?", assigneeIdentity).First(&assignee).Error; err != nil {
return err
}
if err := tx.Model(&task).Updates(map[string]any{
"assignee_id": assignee.ID, "assignee_identity": assignee.Identity,
}).Error; err != nil {
return err
}
return tx.Create(&models.SenlinAgentProjectEvent{
@@ -50,7 +58,7 @@ func (s *Service) Assign(taskID uint, assigneeID uint) error {
EventType: "task_assigned",
EntityType: "task",
EntityID: task.ID,
Summary: fmt.Sprintf("Task assigned to user %d", assigneeID),
Summary: fmt.Sprintf("Task assigned to user %s", assignee.Identity),
}).Error
})
}
@@ -61,8 +69,8 @@ func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) err
return errors.New("unsupported shared object type")
}
return s.database().Transaction(func(tx *gorm.DB) error {
var task models.SenlinAgentTask
if err := tx.First(&task, taskID).Error; err != nil {
task, err := lockTaskByID(tx, taskID)
if err != nil {
return err
}
if err := ensureSharedObjectInProject(tx, task.ProjectID, objectType, objectID); err != nil {
@@ -97,7 +105,7 @@ func (s *Service) VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObje
}
objects := make([]LinkedObject, 0, len(shares))
for _, share := range shares {
objects = append(objects, LinkedObject{ObjectType: share.ObjectType, ObjectID: share.ObjectID})
objects = append(objects, LinkedObject{ObjectType: share.ObjectType, ObjectID: share.ObjectIdentity})
}
return objects, nil
}
@@ -141,13 +149,16 @@ func (s *Service) Create(ownerID uint, projectIdentity string, input CreateTaskI
func (s *Service) Update(ownerID uint, projectIdentity, taskIdentity string, input UpdateTaskInput) (TaskDTO, error) {
var result TaskDTO
err := s.database().Transaction(func(tx *gorm.DB) error {
task, err := lockTaskByIdentity(tx, taskIdentity)
if err != nil {
return err
}
currentProject, err := findOwnedProject(tx, ownerID, projectIdentity)
if err != nil {
return err
}
var task models.SenlinAgentTask
if err := tx.Where("identity = ? AND project_id = ?", taskIdentity, currentProject.ID).First(&task).Error; err != nil {
return err
if task.ProjectID != currentProject.ID {
return gorm.ErrRecordNotFound
}
targetProject := currentProject
if strings.TrimSpace(input.NextProjectIdentity) != "" && input.NextProjectIdentity != currentProject.Identity {
@@ -187,15 +198,31 @@ func (s *Service) Update(ownerID uint, projectIdentity, taskIdentity string, inp
return err
}
}
if err := tx.Save(&task).Error; err != nil {
if err := tx.Save(task).Error; err != nil {
return err
}
result = makeTaskDTO(task, targetProject.Identity, tagIdentity, tagName)
result = makeTaskDTO(*task, targetProject.Identity, tagIdentity, tagName)
return nil
})
return result, err
}
func lockTaskByID(tx *gorm.DB, taskID uint) (*models.SenlinAgentTask, error) {
var task models.SenlinAgentTask
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&task, taskID).Error; err != nil {
return nil, err
}
return &task, nil
}
func lockTaskByIdentity(tx *gorm.DB, taskIdentity string) (*models.SenlinAgentTask, error) {
var task models.SenlinAgentTask
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", taskIdentity).First(&task).Error; err != nil {
return nil, err
}
return &task, nil
}
func findOwnedProject(tx *gorm.DB, ownerID uint, identity string) (*models.SenlinAgentProject, error) {
var project models.SenlinAgentProject
if err := tx.Where("owner_id = ? AND identity = ?", ownerID, identity).First(&project).Error; err != nil {