refactor(backend): split project write responsibilities
This commit is contained in:
39
backend/internal/logic/tasks/dto.go
Normal file
39
backend/internal/logic/tasks/dto.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package tasks
|
||||
|
||||
import "time"
|
||||
|
||||
// TaskDTO 是任务写接口的稳定响应,所有关联 ID 都使用公开 identity。
|
||||
type TaskDTO struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"projectId"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Completed bool `json:"completed"`
|
||||
DueAt *time.Time `json:"dueAt"`
|
||||
AssigneeID *string `json:"assigneeId"`
|
||||
TagID *string `json:"tagId"`
|
||||
Tag string `json:"tag"`
|
||||
SourceInboxItemID *string `json:"sourceInboxItemId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// CreateTaskInput 描述创建任务时允许写入的项目内字段。
|
||||
type CreateTaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Status string
|
||||
DueAt *time.Time
|
||||
Tag string
|
||||
}
|
||||
|
||||
// UpdateTaskInput 使用目标项目 identity 表达移动,避免 API 接触内部自增 ID。
|
||||
type UpdateTaskInput struct {
|
||||
Title string
|
||||
Description string
|
||||
Status string
|
||||
Completed bool
|
||||
NextProjectIdentity string
|
||||
Tag string
|
||||
}
|
||||
153
backend/internal/logic/tasks/handlers.go
Normal file
153
backend/internal/logic/tasks/handlers.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/httpx"
|
||||
"senlinai-agent/backend/internal/logic/auth"
|
||||
)
|
||||
|
||||
// Handler 只注册任务写接口,项目核心 handler 不再承担任务职责。
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
// NewHandler 创建任务 HTTP registrar。
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
// Register 将任务写接口注册到上层提供的 /api/v1 路由组。
|
||||
func (h *Handler) Register(router gin.IRouter) {
|
||||
router.POST("/projects/:id/tasks", h.create)
|
||||
router.PATCH("/projects/:id/tasks/:taskId", h.update)
|
||||
}
|
||||
|
||||
type createTaskRequest struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
DueAt string `json:"dueAt"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type updateTaskRequest struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
Completed bool `json:"completed"`
|
||||
NextProjectID string `json:"nextProjectId"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
func (h *Handler) create(c *gin.Context) {
|
||||
userID, ok := currentUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
projectIdentity, ok := httpx.IdentityParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input createTaskRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||||
return
|
||||
}
|
||||
dueAt, err := parseOptionalTime(input.DueAt)
|
||||
if err != nil {
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "截止时间格式无效")
|
||||
return
|
||||
}
|
||||
task, err := h.service.Create(userID, projectIdentity, CreateTaskInput{
|
||||
Title: input.Title, Description: input.Description, Status: input.Status, DueAt: dueAt, Tag: input.Tag,
|
||||
})
|
||||
if err != nil {
|
||||
writeTaskError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, task)
|
||||
}
|
||||
|
||||
func (h *Handler) update(c *gin.Context) {
|
||||
userID, ok := currentUser(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
projectIdentity, ok := httpx.IdentityParam(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
taskIdentity, ok := httpx.IdentityParam(c, "taskId")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input updateTaskRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(input.NextProjectID) != "" {
|
||||
if _, ok := parseIdentity(input.NextProjectID); !ok {
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_identity", "nextProjectId 必须是 UUIDv7")
|
||||
return
|
||||
}
|
||||
}
|
||||
task, err := h.service.Update(userID, projectIdentity, taskIdentity, UpdateTaskInput{
|
||||
Title: input.Title, Description: input.Description, Status: input.Status, Completed: input.Completed,
|
||||
NextProjectIdentity: input.NextProjectID, Tag: input.Tag,
|
||||
})
|
||||
if err != nil {
|
||||
writeTaskError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, task)
|
||||
}
|
||||
|
||||
func currentUser(c *gin.Context) (uint, bool) {
|
||||
userID, ok := auth.CurrentUserID(c)
|
||||
if !ok {
|
||||
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
|
||||
return 0, false
|
||||
}
|
||||
return userID, true
|
||||
}
|
||||
|
||||
func parseOptionalTime(value string) (*time.Time, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed = parsed.UTC()
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func parseIdentity(value string) (string, bool) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
identity, err := uuid.Parse(trimmed)
|
||||
if err != nil || identity.Version() != 7 || identity.Variant() != uuid.RFC4122 {
|
||||
return "", false
|
||||
}
|
||||
return identity.String(), true
|
||||
}
|
||||
|
||||
func writeTaskError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
httpx.Error(c, http.StatusNotFound, "not_found", "任务或项目不存在")
|
||||
case errors.Is(err, ErrTaskTitleRequired):
|
||||
httpx.Error(c, http.StatusBadRequest, "invalid_request", "任务标题不能为空")
|
||||
default:
|
||||
httpx.Error(c, http.StatusInternalServerError, "internal_error", "任务操作失败")
|
||||
}
|
||||
}
|
||||
109
backend/internal/logic/tasks/handlers_test.go
Normal file
109
backend/internal/logic/tasks/handlers_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/config"
|
||||
"senlinai-agent/backend/internal/httpx"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
func TestTaskRegistrarCreatesIdentityDTOForOwnedProject(t *testing.T) {
|
||||
router, database, project := newTaskHandlerTestRouter(t, 1)
|
||||
body := bytes.NewBufferString(`{"title":"整理访谈","description":"提取行动项","dueAt":"2026-07-22T08:00:00Z","tag":"客户"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+project.Identity+"/tasks", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
|
||||
var task models.SenlinAgentTask
|
||||
require.NoError(t, database.Where("project_id = ?", project.ID).First(&task).Error)
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, task.Identity, payload["id"])
|
||||
require.Equal(t, project.Identity, payload["projectId"])
|
||||
require.Equal(t, "客户", payload["tag"])
|
||||
require.NotEmpty(t, payload["tagId"])
|
||||
require.NotEmpty(t, payload["createdAt"])
|
||||
require.NotContains(t, payload, "ID")
|
||||
require.NotContains(t, payload, "ProjectID")
|
||||
}
|
||||
|
||||
func TestTaskRegistrarMovesTaskByIdentityAndClearsForeignProjectTag(t *testing.T) {
|
||||
router, database, first := newTaskHandlerTestRouter(t, 1)
|
||||
second := models.SenlinAgentProject{OwnerID: 1, Name: "Beta", Identifier: "BETA"}
|
||||
require.NoError(t, database.Create(&second).Error)
|
||||
tag := models.SenlinAgentTag{ProjectID: first.ID, Name: "仅 Alpha"}
|
||||
require.NoError(t, database.Create(&tag).Error)
|
||||
task := models.SenlinAgentTask{ProjectID: first.ID, CreatedBy: 1, TagID: &tag.ID, Title: "迁移任务", Status: "open"}
|
||||
require.NoError(t, database.Create(&task).Error)
|
||||
note := models.SenlinAgentNote{ProjectID: first.ID, CreatedBy: 1, Title: "旧项目资料", Markdown: "仅可在 Alpha 分享"}
|
||||
require.NoError(t, database.Create(¬e).Error)
|
||||
require.NoError(t, NewService(database).ShareObject(task.ID, "note", note.ID))
|
||||
body := bytes.NewBufferString(fmt.Sprintf(`{"title":"迁移任务","description":"已移动","completed":false,"nextProjectId":%q}`, second.Identity))
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/projects/"+first.Identity+"/tasks/"+task.Identity, body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
||||
require.NoError(t, database.First(&task, task.ID).Error)
|
||||
require.Equal(t, second.ID, task.ProjectID)
|
||||
require.Nil(t, task.TagID)
|
||||
var shareCount int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTaskShare{}).Where("task_id = ?", task.ID).Count(&shareCount).Error)
|
||||
require.Zero(t, shareCount, "移动项目后不能保留旧项目的显式分享")
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
|
||||
require.Equal(t, second.Identity, payload["projectId"])
|
||||
require.Nil(t, payload["tagId"])
|
||||
}
|
||||
|
||||
func TestTaskRegistrarRejectsProjectOwnedByAnotherUser(t *testing.T) {
|
||||
router, database, _ := newTaskHandlerTestRouter(t, 1)
|
||||
other := models.SenlinAgentProject{OwnerID: 2, Name: "Private", Identifier: "PRIVATE"}
|
||||
require.NoError(t, database.Create(&other).Error)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+other.Identity+"/tasks", bytes.NewBufferString(`{"title":"越权"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, rec.Code, rec.Body.String())
|
||||
var count int64
|
||||
require.NoError(t, database.Model(&models.SenlinAgentTask{}).Where("project_id = ?", other.ID).Count(&count).Error)
|
||||
require.Zero(t, count)
|
||||
}
|
||||
|
||||
func newTaskHandlerTestRouter(t *testing.T, currentUserID uint) (*gin.Engine, *gorm.DB, models.SenlinAgentProject) {
|
||||
t.Helper()
|
||||
database, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{TranslateError: true})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, models.AutoMigrate(database))
|
||||
models.DBService = database
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "owner@example.com", DisplayName: "Owner", PasswordHash: "hash"}).Error)
|
||||
require.NoError(t, database.Create(&models.SenlinAgentUser{Email: "other@example.com", DisplayName: "Other", PasswordHash: "hash"}).Error)
|
||||
project := models.SenlinAgentProject{OwnerID: 1, Name: "Alpha", Identifier: "ALPHA"}
|
||||
require.NoError(t, database.Create(&project).Error)
|
||||
router := httpx.NewProtectedRouter(
|
||||
config.Config{Env: "test"},
|
||||
func(string) (uint, error) { return currentUserID, nil },
|
||||
NewHandler(NewService(database)),
|
||||
)
|
||||
return router, database, project
|
||||
}
|
||||
@@ -3,12 +3,15 @@ package tasks
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"senlinai-agent/backend/internal/models"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type LinkedObject struct {
|
||||
@@ -16,12 +19,24 @@ type LinkedObject struct {
|
||||
ObjectID uint `json:"object_id"`
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
// NewService 接受数据库或上层事务,确保任务写入、标签调整和分享检查使用同一依赖。
|
||||
func NewService(databases ...*gorm.DB) *Service {
|
||||
var database *gorm.DB
|
||||
if len(databases) > 0 {
|
||||
database = databases[0]
|
||||
}
|
||||
return &Service{db: database}
|
||||
}
|
||||
|
||||
func (s *Service) database() *gorm.DB {
|
||||
if s.db != nil {
|
||||
return s.db
|
||||
}
|
||||
return models.DBService
|
||||
}
|
||||
|
||||
func (s *Service) Assign(taskID uint, assigneeID uint) error {
|
||||
return models.DBService.Transaction(func(tx *gorm.DB) 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
|
||||
@@ -40,11 +55,12 @@ func (s *Service) Assign(taskID uint, assigneeID uint) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ShareObject 只允许显式分享 note/source,并在同一事务中验证关联对象属于任务所在项目。
|
||||
func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) error {
|
||||
if objectType != "note" && objectType != "source" {
|
||||
return errors.New("unsupported shared object type")
|
||||
}
|
||||
return models.DBService.Transaction(func(tx *gorm.DB) 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
|
||||
@@ -66,16 +82,17 @@ func (s *Service) ShareObject(taskID uint, objectType string, objectID uint) err
|
||||
})
|
||||
}
|
||||
|
||||
// VisibleLinkedObjects 对被指派人也只返回显式分享记录,不因任务可见而扩大关联对象权限。
|
||||
func (s *Service) VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObject, error) {
|
||||
var task models.SenlinAgentTask
|
||||
if err := models.DBService.First(&task, taskID).Error; err != nil {
|
||||
if err := s.database().First(&task, taskID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if task.AssigneeID == nil || *task.AssigneeID != viewerID {
|
||||
return []LinkedObject{}, nil
|
||||
}
|
||||
var shares []models.SenlinAgentTaskShare
|
||||
if err := models.DBService.Where("task_id = ?", taskID).Find(&shares).Error; err != nil {
|
||||
if err := s.database().Where("task_id = ?", taskID).Find(&shares).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objects := make([]LinkedObject, 0, len(shares))
|
||||
@@ -85,6 +102,143 @@ func (s *Service) VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObje
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
var ErrTaskTitleRequired = errors.New("task title is required")
|
||||
|
||||
// Create 在同一事务中校验项目所有权、解析项目内标签并创建任务。
|
||||
func (s *Service) Create(ownerID uint, projectIdentity string, input CreateTaskInput) (TaskDTO, error) {
|
||||
var result TaskDTO
|
||||
err := s.database().Transaction(func(tx *gorm.DB) error {
|
||||
project, err := findOwnedProject(tx, ownerID, projectIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title := strings.TrimSpace(input.Title)
|
||||
if title == "" {
|
||||
return ErrTaskTitleRequired
|
||||
}
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
status = "open"
|
||||
}
|
||||
tagID, tagIdentity, tagName, err := findOrCreateProjectTag(tx, project.ID, input.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task := models.SenlinAgentTask{
|
||||
ProjectID: project.ID, CreatedBy: ownerID, TagID: tagID,
|
||||
Title: title, Description: strings.TrimSpace(input.Description), Status: status, DueAt: utcOptionalTime(input.DueAt),
|
||||
}
|
||||
if err := tx.Create(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result = makeTaskDTO(task, project.Identity, tagIdentity, tagName)
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Update 同时按项目和任务 identity 查询;移动项目时重新校验所有权,并重新解析目标项目标签。
|
||||
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 {
|
||||
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
|
||||
}
|
||||
targetProject := currentProject
|
||||
if strings.TrimSpace(input.NextProjectIdentity) != "" && input.NextProjectIdentity != currentProject.Identity {
|
||||
targetProject, err = findOwnedProject(tx, ownerID, input.NextProjectIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
title := strings.TrimSpace(input.Title)
|
||||
if title == "" {
|
||||
return ErrTaskTitleRequired
|
||||
}
|
||||
status := strings.TrimSpace(input.Status)
|
||||
if status == "" {
|
||||
if input.Completed {
|
||||
status = "done"
|
||||
} else {
|
||||
status = "open"
|
||||
}
|
||||
}
|
||||
|
||||
// 标签只能在目标项目内重新解析;未提供标签时会清空旧项目标签,不能跨项目沿用。
|
||||
tagID, tagIdentity, tagName, err := findOrCreateProjectTag(tx, targetProject.ID, input.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.ProjectID = targetProject.ID
|
||||
task.ProjectIdentity = targetProject.Identity
|
||||
task.Title = title
|
||||
task.Description = strings.TrimSpace(input.Description)
|
||||
task.Status = status
|
||||
task.TagID = tagID
|
||||
task.TagIdentity = tagIdentity
|
||||
if targetProject.ID != currentProject.ID {
|
||||
// 现有分享都在原项目边界内;移动后必须清空,避免旧项目 note/source 继续对被指派人可见。
|
||||
if err := tx.Where("task_id = ?", task.ID).Delete(&models.SenlinAgentTaskShare{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result = makeTaskDTO(task, targetProject.Identity, tagIdentity, tagName)
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
func findOrCreateProjectTag(tx *gorm.DB, projectID uint, value string) (*uint, *string, string, error) {
|
||||
name := strings.TrimSpace(value)
|
||||
if name == "" {
|
||||
return nil, nil, "", nil
|
||||
}
|
||||
var tag models.SenlinAgentTag
|
||||
err := tx.Where("project_id = ? AND name = ?", projectID, name).First(&tag).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tag = models.SenlinAgentTag{ProjectID: projectID, Name: name}
|
||||
if err := tx.Create(&tag).Error; err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, nil, "", err
|
||||
}
|
||||
return &tag.ID, &tag.Identity, tag.Name, nil
|
||||
}
|
||||
|
||||
func makeTaskDTO(task models.SenlinAgentTask, projectIdentity string, tagIdentity *string, tagName string) TaskDTO {
|
||||
return TaskDTO{
|
||||
ID: task.Identity, ProjectID: projectIdentity, Title: task.Title, Description: task.Description,
|
||||
Status: task.Status, Completed: task.Status == "done", DueAt: utcOptionalTime(task.DueAt),
|
||||
AssigneeID: task.AssigneeIdentity, TagID: tagIdentity, Tag: tagName,
|
||||
SourceInboxItemID: task.SourceInboxItemIdentity, CreatedAt: task.CreatedAt.UTC(), UpdatedAt: task.UpdatedAt.UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func utcOptionalTime(value *time.Time) *time.Time {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
result := value.UTC()
|
||||
return &result
|
||||
}
|
||||
|
||||
func ensureSharedObjectInProject(tx *gorm.DB, projectID uint, objectType string, objectID uint) error {
|
||||
switch objectType {
|
||||
case "note":
|
||||
|
||||
Reference in New Issue
Block a user