157 lines
4.1 KiB
Go
157 lines
4.1 KiB
Go
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
|
|
}
|
|
nextProjectIdentity := ""
|
|
if strings.TrimSpace(input.NextProjectID) != "" {
|
|
var valid bool
|
|
nextProjectIdentity, valid = parseIdentity(input.NextProjectID)
|
|
if !valid {
|
|
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: nextProjectIdentity, 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", "任务操作失败")
|
|
}
|
|
}
|