package handler import ( "context" "time" "spider/internal/model" "spider/internal/store" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" ) // DashboardHandler serves the dashboard summary endpoint. type DashboardHandler struct { store *store.Store rdb *redis.Client } // Get returns aggregated dashboard data. func (h *DashboardHandler) Get(c *gin.Context) { db := h.store.DB // Total counts (combined query for clean) var rawTotal int64 db.Model(&model.MerchantRaw{}).Count(&rawTotal) // Single query for clean total and valid total var cleanTotal, validTotal int64 db.Model(&model.MerchantClean{}).Count(&cleanTotal) db.Model(&model.MerchantClean{}).Where("status = ?", "valid").Count(&validTotal) // By level type kv struct { Key string `gorm:"column:key"` Count int64 `gorm:"column:count"` } var levelRows []kv db.Model(&model.MerchantClean{}). Select("level as `key`, count(*) as `count`"). Group("level"). Find(&levelRows) byLevel := gin.H{} for _, r := range levelRows { if r.Key == "" { r.Key = "Unknown" } byLevel[r.Key] = r.Count } // By status var statusRows []kv db.Model(&model.MerchantClean{}). Select("status as `key`, count(*) as `count`"). Group("status"). Find(&statusRows) byStatus := gin.H{} for _, r := range statusRows { byStatus[r.Key] = r.Count } // By source var sourceRows []kv db.Model(&model.MerchantRaw{}). Select("source_type as `key`, count(*) as `count`"). Group("source_type"). Find(&sourceRows) bySource := gin.H{} for _, r := range sourceRows { if r.Key == "" { r.Key = "unknown" } bySource[r.Key] = r.Count } // By industry var industryRows []kv db.Model(&model.MerchantClean{}). Select("industry_tag as `key`, count(*) as `count`"). Where("industry_tag != ''"). Group("industry_tag"). Find(&industryRows) byIndustry := gin.H{} for _, r := range industryRows { byIndustry[r.Key] = r.Count } // Today added now := time.Now() todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) var todayAdded int64 db.Model(&model.MerchantRaw{}).Where("created_at >= ?", todayStart).Count(&todayAdded) // Week added weekStart := todayStart.AddDate(0, 0, -int(now.Weekday())) if now.Weekday() == 0 { weekStart = todayStart.AddDate(0, 0, -6) } else { weekStart = todayStart.AddDate(0, 0, -int(now.Weekday())+1) } var weekAdded int64 db.Model(&model.MerchantRaw{}).Where("created_at >= ?", weekStart).Count(&weekAdded) // Recent tasks (last 5) var recentTasks []model.TaskLog db.Order("created_at DESC").Limit(5).Find(&recentTasks) // Daily trend (last 14 days) type trend struct { Date string `gorm:"column:date" json:"date"` Count int64 `gorm:"column:count" json:"count"` } var dailyTrend []trend fourteenDaysAgo := todayStart.AddDate(0, 0, -13) db.Model(&model.MerchantRaw{}). Select("DATE(created_at) as date, count(*) as `count`"). Where("created_at >= ?", fourteenDaysAgo). Group("DATE(created_at)"). Order("date ASC"). Find(&dailyTrend) OK(c, gin.H{ "raw_total": rawTotal, "clean_total": cleanTotal, "valid_total": validTotal, "by_level": byLevel, "by_status": byStatus, "by_source": bySource, "by_industry": byIndustry, "today_added": todayAdded, "week_added": weekAdded, "recent_tasks": recentTasks, "daily_trend": dailyTrend, }) } // Health returns system component health status. func (h *DashboardHandler) Health(c *gin.Context) { db := h.store.DB health := gin.H{} // MySQL sqlDB, err := db.DB() if err != nil { health["mysql"] = gin.H{"status": "error", "error": err.Error()} } else { ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second) defer cancel() if err := sqlDB.PingContext(ctx); err != nil { health["mysql"] = gin.H{"status": "error", "error": err.Error()} } else { stats := sqlDB.Stats() health["mysql"] = gin.H{ "status": "ok", "open_conns": stats.OpenConnections, "in_use": stats.InUse, "idle": stats.Idle, "max_open": stats.MaxOpenConnections, } } } // Redis ctx2, cancel2 := context.WithTimeout(c.Request.Context(), 3*time.Second) defer cancel2() if err := h.rdb.Ping(ctx2).Err(); err != nil { health["redis"] = gin.H{"status": "error", "error": err.Error()} } else { health["redis"] = gin.H{"status": "ok"} } // TG accounts type tgStatus struct { Status string Cnt int64 } var tgRows []tgStatus db.Model(&model.TgAccount{}).Select("status, count(*) as cnt").Group("status").Scan(&tgRows) tgSummary := gin.H{} for _, r := range tgRows { tgSummary[r.Status] = r.Cnt } health["tg_accounts"] = tgSummary // Tasks last 24h yesterday := time.Now().Add(-24 * time.Hour) type taskStatus struct { Status string Cnt int64 } var taskRows []taskStatus db.Model(&model.TaskLog{}).Select("status, count(*) as cnt"). Where("created_at >= ?", yesterday).Group("status").Scan(&taskRows) taskSummary := gin.H{} for _, r := range taskRows { taskSummary[r.Status] = r.Cnt } health["tasks_24h"] = taskSummary // Data counts var rawCount, cleanCount, detailCount int64 db.Model(&model.MerchantRaw{}).Count(&rawCount) db.Model(&model.MerchantClean{}).Count(&cleanCount) db.Model(&model.TaskDetail{}).Count(&detailCount) health["data"] = gin.H{ "merchants_raw": rawCount, "merchants_clean": cleanCount, "task_details": detailCount, } OK(c, health) }