package transport import ( "strconv" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/geo-platform/tenant-api/internal/bootstrap" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/tenant/app" ) type MonitoringHandler struct { svc *app.MonitoringService } type monitoringCollectNowRequest struct { KeywordID *int64 `json:"keyword_id"` QuestionID *int64 `json:"question_id"` PlatformIDs []string `json:"platform_ids"` Preempt *bool `json:"preempt"` WaitForFirstDispatch *bool `json:"wait_for_first_dispatch"` TargetClientID *string `json:"target_client_id"` } type monitoringMarkedArticleRequest struct { ArticleTitle string `json:"article_title"` OriginalURL string `json:"original_url"` BrandID *int64 `json:"brand_id"` } func NewMonitoringHandler(a *bootstrap.App) *MonitoringHandler { return &MonitoringHandler{ svc: a.MonitoringService, } } func (h *MonitoringHandler) DashboardComposite(c *gin.Context) { brandID, err := parseOptionalInt64(c.Query("brand_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_brand_id", "brand_id must be a number")) return } keywordID, err := parseOptionalInt64Pointer(c.Query("keyword_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_keyword_id", "keyword_id must be a number")) return } questionID, err := parseOptionalInt64Pointer(c.Query("question_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_question_id", "question_id must be a number")) return } days, err := parseOptionalInt(c.Query("days")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_days", "days must be a number")) return } aiPlatformID := parseOptionalStringPointer(c.Query("ai_platform_id")) hotQuestionPage, err := parseMonitoringPageRequest(c, "hot_questions", true) if err != nil { response.Error(c, err) return } data, svcErr := h.svc.DashboardComposite(c.Request.Context(), brandID, keywordID, questionID, days, c.Query("business_date"), aiPlatformID, hotQuestionPage) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) CitationSummary(c *gin.Context) { days, err := parseOptionalInt(c.Query("days")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_days", "days must be a number")) return } brandID, err := parseOptionalInt64(c.Query("brand_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_brand_id", "brand_id must be a number")) return } keywordID, err := parseOptionalInt64Pointer(c.Query("keyword_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_keyword_id", "keyword_id must be a number")) return } questionID, err := parseOptionalInt64Pointer(c.Query("question_id")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_question_id", "question_id must be a number")) return } aiPlatformID := parseOptionalStringPointer(c.Query("ai_platform_id")) citedArticlePage, err := parseMonitoringPageRequest(c, "cited_articles", false) if err != nil { response.Error(c, err) return } data, svcErr := h.svc.CitationSummary(c.Request.Context(), days, brandID, keywordID, questionID, c.Query("business_date"), aiPlatformID, citedArticlePage) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) QuestionDetail(c *gin.Context) { brandID, err := strconv.ParseInt(c.Param("brand_id"), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_brand_id", "brand_id must be a number")) return } questionID, err := strconv.ParseInt(c.Param("question_id"), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_question_id", "question_id must be a number")) return } aiPlatformID := parseOptionalStringPointer(c.Query("ai_platform_id")) data, svcErr := h.svc.QuestionDetail( c.Request.Context(), brandID, questionID, c.Query("date_from"), c.Query("date_to"), c.Query("question_hash"), aiPlatformID, ) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) CollectNow(c *gin.Context) { brandID, err := strconv.ParseInt(c.Param("brand_id"), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_brand_id", "brand_id must be a number")) return } var req monitoringCollectNowRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_collect_now_request", "collect-now request is invalid")) return } var targetClientID *uuid.UUID if req.TargetClientID != nil && strings.TrimSpace(*req.TargetClientID) != "" { parsed, parseErr := uuid.Parse(strings.TrimSpace(*req.TargetClientID)) if parseErr != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_target_client_id", "target_client_id must be a uuid")) return } targetClientID = &parsed } data, svcErr := h.svc.CollectNow(c.Request.Context(), brandID, req.KeywordID, req.QuestionID, app.MonitoringCollectNowOptions{ PlatformIDs: req.PlatformIDs, Preempt: req.Preempt == nil || *req.Preempt, WaitForFirstDispatch: req.WaitForFirstDispatch != nil && *req.WaitForFirstDispatch, TargetClientID: targetClientID, }) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) ListMarkedArticles(c *gin.Context) { page, err := parseOptionalInt(c.Query("page")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_page", "page must be a number")) return } pageSize, err := parseOptionalInt(c.Query("page_size")) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_page_size", "page_size must be a number")) return } data, svcErr := h.svc.ListMarkedArticles(c.Request.Context(), app.MonitoringMarkedArticleListParams{ Domain: c.Query("domain"), Title: c.Query("title"), CreatedFrom: c.Query("created_from"), CreatedTo: c.Query("created_to"), Page: page, PageSize: pageSize, }) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) CreateMarkedArticle(c *gin.Context) { var req monitoringMarkedArticleRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_marked_article_request", "marked article request is invalid")) return } data, svcErr := h.svc.CreateMarkedArticle(c.Request.Context(), app.MonitoringMarkedArticleRequest{ ArticleTitle: req.ArticleTitle, OriginalURL: req.OriginalURL, BrandID: req.BrandID, }) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) UpdateMarkedArticle(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_marked_article_id", "marked article id must be a number")) return } var req monitoringMarkedArticleRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_marked_article_request", "marked article request is invalid")) return } data, svcErr := h.svc.UpdateMarkedArticle(c.Request.Context(), id, app.MonitoringMarkedArticleRequest{ ArticleTitle: req.ArticleTitle, OriginalURL: req.OriginalURL, BrandID: req.BrandID, }) if svcErr != nil { response.Error(c, svcErr) return } response.Success(c, data) } func (h *MonitoringHandler) DeleteMarkedArticle(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.Error(c, response.ErrBadRequest(40031, "invalid_marked_article_id", "marked article id must be a number")) return } if svcErr := h.svc.DeleteMarkedArticle(c.Request.Context(), id); svcErr != nil { response.Error(c, svcErr) return } response.Success(c, gin.H{"ok": true}) } func parseOptionalInt64(raw string) (int64, error) { if raw == "" { return 0, nil } return strconv.ParseInt(raw, 10, 64) } func parseOptionalInt64Pointer(raw string) (*int64, error) { if raw == "" { return nil, nil } value, err := strconv.ParseInt(raw, 10, 64) if err != nil { return nil, err } return &value, nil } func parseOptionalInt(raw string) (int, error) { if raw == "" { return 0, nil } return strconv.Atoi(raw) } func parseOptionalStringPointer(raw string) *string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil } value := trimmed return &value } func parseMonitoringPageRequest(c *gin.Context, prefix string, includeQuery bool) (app.MonitoringPageRequest, error) { req := app.MonitoringPageRequest{} if raw := c.Query(prefix + "_page"); raw != "" { value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return req, response.ErrBadRequest(40031, "invalid_page", prefix+"_page must be a positive number") } req.Page = value } if raw := c.Query(prefix + "_page_size"); raw != "" { value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return req, response.ErrBadRequest(40031, "invalid_page_size", prefix+"_page_size must be a positive number") } req.PageSize = value } if includeQuery { req.Query = strings.TrimSpace(c.Query(prefix + "_q")) } return req, nil }