package handler import ( "net/http" "strconv" "spider/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // MerchantHandler handles merchant queries. type MerchantHandler struct { db *gorm.DB } // Stats returns aggregate statistics for merchants. // GET /merchants/stats func (h *MerchantHandler) Stats(c *gin.Context) { type countRow struct { Key string `json:"key"` Count int64 `json:"count"` } var rawTotal int64 h.db.Model(&model.MerchantRaw{}).Count(&rawTotal) var cleanTotal int64 h.db.Model(&model.MerchantClean{}).Count(&cleanTotal) // Count by status in clean table. statusCounts := map[string]int64{} var statusRows []struct { Status string Cnt int64 } h.db.Model(&model.MerchantClean{}). Select("status, count(*) as cnt"). Group("status"). Scan(&statusRows) for _, r := range statusRows { statusCounts[r.Status] = r.Cnt } // Count by source_type in raw table. var sourceRows []struct { SourceType string Cnt int64 } h.db.Model(&model.MerchantRaw{}). Select("source_type, count(*) as cnt"). Group("source_type"). Scan(&sourceRows) bySource := map[string]int64{} for _, r := range sourceRows { bySource[r.SourceType] = r.Cnt } // Count by industry in clean table. var industryRows []struct { Industry string Cnt int64 } h.db.Model(&model.MerchantClean{}). Select("industry, count(*) as cnt"). Group("industry"). Scan(&industryRows) byIndustry := map[string]int64{} for _, r := range industryRows { byIndustry[r.Industry] = r.Cnt } OK(c, gin.H{ "raw_total": rawTotal, "clean_total": cleanTotal, "valid": statusCounts["valid"], "invalid": statusCounts["invalid"], "bot": statusCounts["bot"], "duplicate": statusCounts["duplicate"], "group": statusCounts["group"], "by_source": bySource, "by_industry": byIndustry, }) } // ListRaw returns raw merchants with filters and pagination. // GET /merchants/raw?status=&source_type=&page=&page_size= func (h *MerchantHandler) ListRaw(c *gin.Context) { page, pageSize, offset := parsePage(c) query := h.db.Model(&model.MerchantRaw{}) if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } if sourceType := c.Query("source_type"); sourceType != "" { query = query.Where("source_type = ?", sourceType) } var total int64 if err := query.Count(&total).Error; err != nil { Fail(c, 500, err.Error()) return } var items []model.MerchantRaw if err := query.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&items).Error; err != nil { Fail(c, 500, err.Error()) return } PageOK(c, items, total, page, pageSize) } // ListClean returns clean merchants with filters and pagination. // GET /merchants/clean?status=&industry=&min_score=&sort=quality_score&order=desc&page=&page_size= func (h *MerchantHandler) ListClean(c *gin.Context) { page, pageSize, offset := parsePage(c) query := h.db.Model(&model.MerchantClean{}) if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } if industry := c.Query("industry"); industry != "" { query = query.Where("industry = ?", industry) } if minScore := c.Query("min_score"); minScore != "" { if score, err := strconv.ParseFloat(minScore, 64); err == nil { query = query.Where("quality_score >= ?", score) } } sortField := c.DefaultQuery("sort", "quality_score") // whitelist sort fields to prevent SQL injection allowedSort := map[string]bool{ "quality_score": true, "created_at": true, "updated_at": true, "member_count": true, } if !allowedSort[sortField] { sortField = "quality_score" } order := c.DefaultQuery("order", "desc") if order != "asc" && order != "desc" { order = "desc" } var total int64 if err := query.Count(&total).Error; err != nil { Fail(c, 500, err.Error()) return } var items []model.MerchantClean if err := query.Order(sortField + " " + order).Limit(pageSize).Offset(offset).Find(&items).Error; err != nil { Fail(c, 500, err.Error()) return } PageOK(c, items, total, page, pageSize) } // GetByID fetches a merchant by ID, checking clean table first then raw. // GET /merchants/:id func (h *MerchantHandler) GetByID(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { Fail(c, http.StatusBadRequest, "invalid id") return } var clean model.MerchantClean if err := h.db.First(&clean, id).Error; err == nil { OK(c, gin.H{"source": "clean", "data": clean}) return } var raw model.MerchantRaw if err := h.db.First(&raw, id).Error; err == nil { OK(c, gin.H{"source": "raw", "data": raw}) return } Fail(c, 404, "merchant not found") }