package dataset import ( "errors" "net/http" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "senlinai-agent/backend/internal/httpx" "senlinai-agent/backend/internal/logic/auth" "senlinai-agent/backend/internal/models" ) type Handler struct { service *Service } type SourceDTO struct { ID string `json:"id"` Name string `json:"name"` Kind string `json:"kind"` URL string `json:"url"` IconURL string `json:"iconUrl"` Description string `json:"description"` Enabled bool `json:"enabled"` LastSyncedAt *time.Time `json:"lastSyncedAt"` ItemCount int64 `json:"itemCount"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type ItemDTO struct { ID string `json:"id"` SourceID string `json:"sourceId"` Title string `json:"title"` Summary string `json:"summary"` Content string `json:"content"` URL string `json:"url"` Status string `json:"status"` Starred bool `json:"starred"` PublishedAt *time.Time `json:"publishedAt"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type CronDTO struct { ID string `json:"id"` SourceID string `json:"sourceId"` Schedule string `json:"schedule"` Status string `json:"status"` Enabled bool `json:"enabled"` NextRunAt *time.Time `json:"nextRunAt"` LastRunAt *time.Time `json:"lastRunAt"` LastResult string `json:"lastResult"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type sourceRequest struct { Name string `json:"name"` Kind string `json:"kind"` URL string `json:"url"` IconURL string `json:"iconUrl"` Description string `json:"description"` Enabled *bool `json:"enabled"` } func NewHandler(service *Service) *Handler { return &Handler{service: service} } func (h *Handler) Register(router gin.IRouter) { router.GET("/dataset-sources", h.listSources) router.POST("/dataset-sources", h.createSource) router.PATCH("/dataset-sources/:id", h.updateSource) router.DELETE("/dataset-sources/:id", h.deleteSource) router.GET("/dataset-items", h.listItems) router.POST("/dataset-items", h.createItem) router.PATCH("/dataset-items/:id", h.updateItem) router.GET("/dataset-crons", h.listCrons) router.POST("/dataset-crons/sync", h.queueSync) } func (h *Handler) listSources(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } records, err := h.service.ListSources(userID) if err != nil { writeError(c, err) return } items := make([]SourceDTO, 0, len(records)) for _, record := range records { items = append(items, sourceDTO(record.Source, record.ItemCount)) } c.JSON(http.StatusOK, items) } func (h *Handler) createSource(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } var input sourceRequest if err := c.ShouldBindJSON(&input); err != nil { writeInvalidRequest(c) return } enabled := true if input.Enabled != nil { enabled = *input.Enabled } source, err := h.service.CreateSource(userID, SourceInput{ Name: input.Name, Kind: input.Kind, URL: input.URL, IconURL: input.IconURL, Description: input.Description, Enabled: enabled, }) if err != nil { writeError(c, err) return } c.JSON(http.StatusCreated, sourceDTO(*source, 0)) } func (h *Handler) updateSource(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } identity, ok := httpx.IdentityParam(c, "id") if !ok { return } var input sourceRequest if err := c.ShouldBindJSON(&input); err != nil { writeInvalidRequest(c) return } enabled := true if input.Enabled != nil { enabled = *input.Enabled } source, err := h.service.UpdateSource(userID, identity, SourceInput{ Name: input.Name, Kind: input.Kind, URL: input.URL, IconURL: input.IconURL, Description: input.Description, Enabled: enabled, }) if err != nil { writeError(c, err) return } itemCount, err := h.service.CountItems(userID, source.ID) if err != nil { writeError(c, err) return } c.JSON(http.StatusOK, sourceDTO(*source, itemCount)) } func (h *Handler) deleteSource(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } identity, ok := httpx.IdentityParam(c, "id") if !ok { return } if err := h.service.DeleteSource(userID, identity); err != nil { writeError(c, err) return } c.Status(http.StatusNoContent) } func (h *Handler) listItems(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } items, err := h.service.ListItems(userID, c.Query("sourceId")) if err != nil { writeError(c, err) return } result := make([]ItemDTO, 0, len(items)) for _, item := range items { result = append(result, itemDTO(item)) } c.JSON(http.StatusOK, result) } func (h *Handler) createItem(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } var input struct { SourceID string `json:"sourceId"` Title string `json:"title"` Summary string `json:"summary"` Content string `json:"content"` URL string `json:"url"` PublishedAt string `json:"publishedAt"` } if err := c.ShouldBindJSON(&input); err != nil { writeInvalidRequest(c) return } publishedAt, err := parseOptionalTime(input.PublishedAt) if err != nil { writeInvalidRequest(c) return } item, err := h.service.CreateItem(userID, ItemInput{ SourceIdentity: input.SourceID, Title: input.Title, Summary: input.Summary, Content: input.Content, URL: input.URL, PublishedAt: publishedAt, }) if err != nil { writeError(c, err) return } c.JSON(http.StatusCreated, itemDTO(*item)) } func (h *Handler) updateItem(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } identity, ok := httpx.IdentityParam(c, "id") if !ok { return } var input struct { Status string `json:"status"` Starred bool `json:"starred"` } if err := c.ShouldBindJSON(&input); err != nil { writeInvalidRequest(c) return } item, err := h.service.UpdateItem(userID, identity, ItemUpdate{Status: input.Status, Starred: input.Starred}) if err != nil { writeError(c, err) return } c.JSON(http.StatusOK, itemDTO(*item)) } func (h *Handler) listCrons(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } crons, err := h.service.ListCrons(userID) if err != nil { writeError(c, err) return } result := make([]CronDTO, 0, len(crons)) for _, cron := range crons { result = append(result, cronDTO(cron)) } c.JSON(http.StatusOK, result) } func (h *Handler) queueSync(c *gin.Context) { userID, ok := currentUser(c) if !ok { return } crons, err := h.service.QueueSync(userID) if err != nil { writeError(c, err) return } result := make([]CronDTO, 0, len(crons)) for _, cron := range crons { result = append(result, cronDTO(cron)) } c.JSON(http.StatusAccepted, result) } 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 writeError(c *gin.Context, err error) { switch { case errors.Is(err, gorm.ErrRecordNotFound): httpx.Error(c, http.StatusNotFound, "not_found", "数据源或数据条目不存在") case errors.Is(err, ErrNameRequired), errors.Is(err, ErrKindInvalid), errors.Is(err, ErrURLInvalid), errors.Is(err, ErrTitleRequired), errors.Is(err, ErrStatusInvalid): writeInvalidRequest(c) default: httpx.Error(c, http.StatusInternalServerError, "internal_error", "探索数据操作失败") } } func writeInvalidRequest(c *gin.Context) { httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效") } func sourceDTO(source models.SaDatasetSource, itemCount int64) SourceDTO { return SourceDTO{ ID: source.Identity, Name: source.Name, Kind: source.Kind, URL: source.URL, IconURL: source.IconURL, Description: source.Description, Enabled: source.Enabled, LastSyncedAt: utcTime(source.LastSyncedAt), ItemCount: itemCount, CreatedAt: source.CreatedAt.UTC(), UpdatedAt: source.UpdatedAt.UTC(), } } func itemDTO(item models.SaDatasetItem) ItemDTO { return ItemDTO{ ID: item.Identity, SourceID: item.SourceIdentity, Title: item.Title, Summary: item.Summary, Content: item.Content, URL: item.URL, Status: item.Status, Starred: item.Starred, PublishedAt: utcTime(item.PublishedAt), CreatedAt: item.CreatedAt.UTC(), UpdatedAt: item.UpdatedAt.UTC(), } } func cronDTO(cron models.SaDatasetCron) CronDTO { return CronDTO{ ID: cron.Identity, SourceID: cron.SourceIdentity, Schedule: cron.Schedule, Status: cron.Status, Enabled: cron.Enabled, NextRunAt: utcTime(cron.NextRunAt), LastRunAt: utcTime(cron.LastRunAt), LastResult: cron.LastResult, CreatedAt: cron.CreatedAt.UTC(), UpdatedAt: cron.UpdatedAt.UTC(), } } func parseOptionalTime(value string) (*time.Time, error) { if strings.TrimSpace(value) == "" { return nil, nil } parsed, err := time.Parse(time.RFC3339, value) if err != nil { return nil, err } parsed = parsed.UTC() return &parsed, nil } func utcTime(value *time.Time) *time.Time { if value == nil { return nil } result := value.UTC() return &result }