package api import ( "strconv" "time" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" ) // AdminStatsOverview GET /api/v1/admin/stats/overview — 运营总览。 func (h *Handler) AdminStatsOverview(c *gin.Context) { now := time.Now().UTC() today := now.Format("2006-01-02") month := now.Format("2006-01") var totalUsers int64 h.a.DB.Model(&store.User{}).Count(&totalUsers) var totalKeys int64 h.a.DB.Model(&store.APIKey{}).Count(&totalKeys) var totalChannels int64 h.a.DB.Model(&store.Channel{}).Count(&totalChannels) var totalModels int64 h.a.DB.Model(&store.Model{}).Count(&totalModels) // 全局今日/本月汇总(跨用户) var todayReq, monthReq int64 var todayCost, monthCost float64 var todayTokens, monthTokens int64 h.a.DB.Model(&store.UsageDaily{}).Where("date = ?", today). Select("COALESCE(SUM(requests),0), COALESCE(SUM(cost),0), COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens),0)"). Row().Scan(&todayReq, &todayCost, &todayTokens) h.a.DB.Model(&store.UsageDaily{}).Where("date LIKE ?", month+"%"). Select("COALESCE(SUM(requests),0), COALESCE(SUM(cost),0), COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens),0)"). Row().Scan(&monthReq, &monthCost, &monthTokens) // 近 14 天趋势(日粒度) var days []struct { Date string Req int64 Cost float64 } h.a.DB.Model(&store.UsageDaily{}). Select("date, SUM(requests) req, SUM(cost) cost"). Where("date >= ?", now.AddDate(0, 0, -13).Format("2006-01-02")). Group("date").Order("date").Scan(&days) trend := make([]gin.H, 0, len(days)) for _, d := range days { trend = append(trend, gin.H{"date": d.Date, "requests": d.Req, "cost": d.Cost}) } resp.OK(c, gin.H{ "total_users": totalUsers, "total_keys": totalKeys, "total_channels": totalChannels, "total_models": totalModels, "today": gin.H{"requests": todayReq, "cost": todayCost, "tokens": todayTokens}, "month": gin.H{"requests": monthReq, "cost": monthCost, "tokens": monthTokens}, "trend_14d": trend, }) } // AdminUsage GET /api/v1/admin/usage — 全局用量日志(分页 + 过滤)。 func (h *Handler) AdminUsage(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if size < 1 || size > 100 { size = 20 } q := h.a.DB.Model(&store.UsageLog{}) if from := c.Query("from"); from != "" { q = q.Where("created_at >= ?", from+" 00:00:00") } if to := c.Query("to"); to != "" { q = q.Where("created_at <= ?", to+" 23:59:59") } if model := c.Query("model"); model != "" { q = q.Where("model_name = ?", model) } if user := c.Query("user_id"); user != "" { q = q.Where("user_id = ?", user) } var total int64 q.Count(&total) var logs []store.UsageLog q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&logs) out := make([]gin.H, 0, len(logs)) for _, l := range logs { var uname string h.a.DB.Model(&store.User{}).Where("id = ?", l.UserID).Pluck("username", &uname) var chName string h.a.DB.Model(&store.Channel{}).Where("id = ?", l.ChannelID).Pluck("name", &chName) out = append(out, gin.H{ "id": l.ID, "user": uname, "user_id": l.UserID, "model": l.ModelName, "protocol": l.Protocol, "channel_id": l.ChannelID, "channel": chName, "input_tokens": l.InputTokens, "output_tokens": l.OutputTokens, "cache_read_tokens": l.CacheReadTokens, "cost": l.Cost, "latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode, "raw_request": l.RawRequest, "raw_response": l.RawResponse, "created_at": l.CreatedAt, }) } resp.OK(c, gin.H{"items": out, "total": total, "page": page, "page_size": size}) }