package handler import ( "encoding/csv" "fmt" "net/http" "strconv" "spider/internal/model" "spider/internal/store" "github.com/gin-gonic/gin" ) // MerchantHandler handles merchant queries. type MerchantHandler struct { store *store.Store } // Stats returns aggregate statistics for merchants. func (h *MerchantHandler) Stats(c *gin.Context) { var rawTotal int64 h.store.DB.Model(&model.MerchantRaw{}).Count(&rawTotal) var cleanTotal int64 h.store.DB.Model(&model.MerchantClean{}).Count(&cleanTotal) // Count by status var statusRows []struct { Status string Cnt int64 } h.store.DB.Model(&model.MerchantClean{}). Select("status, count(*) as cnt"). Group("status"). Scan(&statusRows) byStatus := map[string]int64{} for _, r := range statusRows { byStatus[r.Status] = r.Cnt } // Count by level var levelRows []struct { Level string Cnt int64 } h.store.DB.Model(&model.MerchantClean{}). Select("level, count(*) as cnt"). Group("level"). Scan(&levelRows) byLevel := map[string]int64{} for _, r := range levelRows { byLevel[r.Level] = r.Cnt } // Count by source_type var sourceRows []struct { SourceType string Cnt int64 } h.store.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 } OK(c, gin.H{ "raw_total": rawTotal, "clean_total": cleanTotal, "by_status": byStatus, "by_level": byLevel, "by_source": bySource, }) } // ListRaw returns raw merchants with filters and pagination. func (h *MerchantHandler) ListRaw(c *gin.Context) { page, pageSize, offset := parsePage(c) query := h.store.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) } if search := c.Query("search"); search != "" { like := "%" + search + "%" query = query.Where("tg_username LIKE ? OR merchant_name LIKE ?", like, like) } var total int64 query.Count(&total) 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. func (h *MerchantHandler) ListClean(c *gin.Context) { page, pageSize, offset := parsePage(c) query := h.store.DB.Model(&model.MerchantClean{}) if status := c.Query("status"); status != "" { query = query.Where("status = ?", status) } if level := c.Query("level"); level != "" { query = query.Where("level = ?", level) } if industry := c.Query("industry_tag"); industry != "" { query = query.Where("industry_tag = ?", industry) } if search := c.Query("search"); search != "" { like := "%" + search + "%" query = query.Where("tg_username LIKE ? OR merchant_name LIKE ?", like, like) } sortField := c.DefaultQuery("sort", "created_at") allowedSort := map[string]bool{ "created_at": true, "updated_at": true, "source_count": true, "level": true, } if !allowedSort[sortField] { sortField = "created_at" } order := c.DefaultQuery("order", "desc") if order != "asc" && order != "desc" { order = "desc" } var total int64 query.Count(&total) 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) } // ExportCSV exports clean merchants as CSV. func (h *MerchantHandler) ExportCSV(c *gin.Context) { query := h.store.DB.Model(&model.MerchantClean{}).Where("status = ?", "valid") if level := c.Query("level"); level != "" { query = query.Where("level = ?", level) } var merchants []model.MerchantClean query.Order("level ASC, created_at DESC").Find(&merchants) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", "attachment; filename=merchants.csv") // Write BOM for Excel compatibility c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) w := csv.NewWriter(c.Writer) w.Write([]string{"商户名", "TG用户名", "TG链接", "网站", "邮箱", "电话", "行业", "等级", "来源数"}) for _, m := range merchants { w.Write([]string{ m.MerchantName, m.TgUsername, m.TgLink, m.Website, m.Email, m.Phone, m.IndustryTag, m.Level, fmt.Sprintf("%d", m.SourceCount), }) } w.Flush() } // GetByID fetches a merchant by 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.store.DB.First(&clean, id).Error; err == nil { OK(c, gin.H{"source": "clean", "data": clean}) return } var raw model.MerchantRaw if err := h.store.DB.First(&raw, id).Error; err == nil { OK(c, gin.H{"source": "raw", "data": raw}) return } Fail(c, 404, "merchant not found") }