package api import ( "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" ) // UserProfile GET /api/v1/user/profile func (h *Handler) UserProfile(c *gin.Context) { u, _ := userFromContext(c) resp.OK(c, gin.H{"user": h.publicUser(u)}) } // UserBalance GET /api/v1/user/balance — 余额 + 近 30 日消耗。 func (h *Handler) UserBalance(c *gin.Context) { u, _ := userFromContext(c) var spent float64 h.a.DB.Model(&store.UsageLog{}). Where("user_id = ? AND status = ? AND created_at >= ?", u.ID, store.UsageStatusSuccess, time.Now().Add(-30*24*time.Hour)). Select("COALESCE(SUM(cost),0)").Scan(&spent) resp.OK(c, gin.H{ "balance": u.Balance, "spent_last_30d": spent, "today": h.todayUsage(c, u.ID), "models_available": h.availableModelCount(), }) } func (h *Handler) availableModelCount() int64 { var n int64 h.a.DB.Model(&store.Model{}).Where("enabled = ?", true).Count(&n) return n } func (h *Handler) todayUsage(c *gin.Context, userID uint64) gin.H { var requests int64 var tokens int64 var cost float64 today := time.Now().UTC().Format("2006-01-02") h.a.DB.Model(&store.UsageDaily{}). Where("user_id = ? AND date = ?", userID, today). Select("COALESCE(SUM(requests),0), COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens),0), COALESCE(SUM(cost),0)"). Row().Scan(&requests, &tokens, &cost) return gin.H{"requests": requests, "tokens": tokens, "cost": cost} } // UserModels GET /api/v1/user/models — 控制台可用模型列表(无需 API Key)。 func (h *Handler) UserModels(c *gin.Context) { var ms []store.Model if err := h.a.DB.Where("enabled = ?", true).Order("sort ASC, id ASC").Find(&ms).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load models") return } out := make([]string, 0, len(ms)) for _, m := range ms { out = append(out, m.Name) } resp.OK(c, gin.H{"items": out}) } // UsageSummary GET /api/v1/usage/summary — 今日/本月汇总。 func (h *Handler) UsageSummary(c *gin.Context) { u, _ := userFromContext(c) now := time.Now().UTC() today := now.Format("2006-01-02") month := now.Format("2006-01") var todayReq, monthReq int64 var todayTok, monthTok int64 var todayCost, monthCost float64 h.a.DB.Model(&store.UsageDaily{}).Where("user_id = ? AND date = ?", u.ID, today). Select("COALESCE(SUM(requests),0), COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens),0), COALESCE(SUM(cost),0)"). Row().Scan(&todayReq, &todayTok, &todayCost) h.a.DB.Model(&store.UsageDaily{}).Where("user_id = ? AND date LIKE ?", u.ID, month+"%"). Select("COALESCE(SUM(requests),0), COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens),0), COALESCE(SUM(cost),0)"). Row().Scan(&monthReq, &monthTok, &monthCost) resp.OK(c, gin.H{ "today": gin.H{"requests": todayReq, "tokens": todayTok, "cost": todayCost}, "month": gin.H{"requests": monthReq, "tokens": monthTok, "cost": monthCost}, }) } // UsageStats GET /api/v1/usage/stats?from&to&group=day|model func (h *Handler) UsageStats(c *gin.Context) { u, _ := userFromContext(c) from := c.DefaultQuery("from", time.Now().Add(-30*24*time.Hour).Format("2006-01-02")) to := c.DefaultQuery("to", time.Now().Format("2006-01-02")) group := c.DefaultQuery("group", "day") q := h.a.DB.Model(&store.UsageDaily{}).Where("user_id = ? AND date BETWEEN ? AND ?", u.ID, from, to) out := make([]gin.H, 0, 64) if group == "model" { var rows []struct { ModelID uint64 Requests int64 Tokens int64 Cost float64 } q.Select("model_id, SUM(requests) requests, SUM(input_tokens+output_tokens+cache_read_tokens) tokens, SUM(cost) cost"). Group("model_id").Scan(&rows) for _, r := range rows { var m store.Model name := strconv.FormatUint(r.ModelID, 10) if h.a.DB.First(&m, r.ModelID).Error == nil { name = m.Name } out = append(out, gin.H{"model": name, "requests": r.Requests, "tokens": r.Tokens, "cost": r.Cost}) } } else { var rows []struct { Date string Requests int64 Tokens int64 Cost float64 } q.Select("date, SUM(requests) requests, SUM(input_tokens+output_tokens+cache_read_tokens) tokens, SUM(cost) cost"). Group("date").Order("date").Scan(&rows) for _, r := range rows { out = append(out, gin.H{"date": r.Date, "requests": r.Requests, "tokens": r.Tokens, "cost": r.Cost}) } } resp.OK(c, gin.H{"items": out}) } // UsageLogs GET /api/v1/usage/logs?from&to&page&page_size&model func (h *Handler) UsageLogs(c *gin.Context) { u, _ := userFromContext(c) 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{}).Where("user_id = ?", u.ID) 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) } 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 { out = append(out, gin.H{ "id": l.ID, "request_id": l.RequestID, "model": l.ModelName, "protocol": l.Protocol, "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, "created_at": l.CreatedAt, }) } resp.OK(c, gin.H{"items": out, "total": total, "page": page, "page_size": size}) }