Files
assetx/webapp-back/holders/routers.go
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

133 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
})
}