package holders import ( "log" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gothinkster/golang-gin-realworld-example-app/common" "github.com/gothinkster/golang-gin-realworld-example-app/models" ) // RegisterRoutes registers holders API routes func RegisterRoutes(router *gin.RouterGroup) { router.GET("/stats", GetStats) router.GET("/:tokenType", GetHoldersByType) router.POST("/update", UpdateHolders) } // GetStats returns aggregated statistics for all token types func GetStats(c *gin.Context) { db := common.GetDB() log.Printf("📊 [API] 收到获取统计数据请求 - IP: %s", c.ClientIP()) var stats []models.HolderStats err := db.Raw(` SELECT token_type, COUNT(DISTINCT holder_address) as holder_count FROM holder_snapshots WHERE balance != '0' AND balance != '' AND LENGTH(balance) > 0 GROUP BY token_type `).Scan(&stats).Error if err != nil { log.Printf("❌ [API] 查询统计数据失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Failed to fetch stats", }) return } log.Printf("✅ [API] 成功返回统计数据 - %d 个token类型", len(stats)) for _, stat := range stats { log.Printf(" - %s: %d 个持有者", stat.TokenType, stat.HolderCount) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": stats, }) } // GetHoldersByType returns holders for a specific token type func GetHoldersByType(c *gin.Context) { tokenType := c.Param("tokenType") db := common.GetDB() // 记录请求日志 log.Printf("🔍 [API] 收到获取持有者请求 - Token类型: %s, IP: %s", tokenType, c.ClientIP()) var holders []models.HolderSnapshot err := db.Where("token_type = ? AND balance != ? AND balance != '' AND LENGTH(balance) > 0", tokenType, "0"). Order("LENGTH(balance) DESC, balance DESC"). Find(&holders).Error if err != nil { log.Printf("❌ [API] 查询持有者失败 - Token类型: %s, 错误: %v", tokenType, err) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "Failed to fetch holders", }) return } log.Printf("✅ [API] 成功返回 %d 个持有者 - Token类型: %s", len(holders), tokenType) c.JSON(http.StatusOK, gin.H{ "success": true, "data": holders, }) } // Global scanner instance var globalScanner *Scanner // StartAllScanners 为所有支持的链启动 holder 扫描器(后台常驻) func StartAllScanners() { supportedChains := []int64{97, 421614} for _, chainID := range supportedChains { config, err := LoadConfigFromDB(chainID) if err != nil { log.Printf("[HolderScanner] Chain %d 配置加载失败: %v", chainID, err) continue } if len(config.YTVaults) == 0 && config.YTLPAddress == "" { log.Printf("[HolderScanner] Chain %d 无活跃资产,跳过", chainID) continue } go func(cfg Config) { log.Printf("[HolderScanner] Chain %d 启动,YTVaults=%d ytLP=%s", cfg.ChainID, len(cfg.YTVaults), cfg.YTLPAddress) if err := Start(cfg); err != nil { log.Printf("[HolderScanner] Chain %d 异常退出: %v", cfg.ChainID, err) } }(config) } } // UpdateHolders triggers blockchain data update (admin only) func UpdateHolders(c *gin.Context) { if globalScanner == nil { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Scanner is running in background. It will process new blocks on next tick.", "timestamp": time.Now().Unix(), }) return } go func() { if err := globalScanner.incrementalScan(c.Request.Context()); err != nil { log.Printf("[HolderScanner] Manual update error: %v", err) } }() c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Manual scan triggered.", "timestamp": time.Now().Unix(), }) }