package httpapi import ( "net/http" "strings" "time" "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) type dailyTokenUsageSummary struct { CumulativeTokens int64 `json:"cumulativeTokens"` PeakDailyTokens int64 `json:"peakDailyTokens"` CurrentStreakDays int `json:"currentStreakDays"` LongestStreakDays int `json:"longestStreakDays"` } // dailyTokenUsage godoc // @Summary 查询每日 Token 与资源点用量 // @Description 按 IANA 时区返回连续自然日用量;API Key 仅统计当前 Key,JWT 统计当前用户。 // @Tags workspace // @Produce json // @Security BearerAuth // @Param from query string true "开始日期 YYYY-MM-DD" // @Param to query string true "结束日期 YYYY-MM-DD" // @Param timezone query string false "IANA 时区" default(UTC) // @Success 200 {object} DailyTokenUsageResponse // @Failure 400 {object} ErrorEnvelope // @Failure 401 {object} ErrorEnvelope // @Failure 500 {object} ErrorEnvelope // @Router /api/workspace/token-usage/daily [get] func (s *Server) dailyTokenUsage(w http.ResponseWriter, r *http.Request) { user, ok := auth.UserFromContext(r.Context()) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } timezone := strings.TrimSpace(r.URL.Query().Get("timezone")) if timezone == "" { timezone = "UTC" } location, err := time.LoadLocation(timezone) if err != nil { writeError(w, http.StatusBadRequest, "invalid IANA timezone") return } from, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(r.URL.Query().Get("from")), location) if err != nil { writeError(w, http.StatusBadRequest, "invalid from date") return } to, err := time.ParseInLocation("2006-01-02", strings.TrimSpace(r.URL.Query().Get("to")), location) if err != nil || to.Before(from) || to.Sub(from) > 399*24*time.Hour { writeError(w, http.StatusBadRequest, "invalid to date or date range exceeds 400 days") return } stored, err := s.store.ListDailyTokenUsage(r.Context(), user, from, to, timezone) if err != nil { s.logger.Error("list daily token usage failed", "error", err) writeError(w, http.StatusInternalServerError, "list daily token usage failed") return } byDate := make(map[string]store.DailyTokenUsage, len(stored)) for _, item := range stored { byDate[item.Date] = item } items, summary := fillDailyTokenUsageDays(byDate, from, to) writeJSON(w, http.StatusOK, map[string]any{ "items": items, "tokenDays": items, "summary": summary, "range": map[string]string{"from": from.Format("2006-01-02"), "to": to.Format("2006-01-02"), "timezone": timezone}, }) } func fillDailyTokenUsageDays(byDate map[string]store.DailyTokenUsage, from, to time.Time) ([]store.DailyTokenUsage, dailyTokenUsageSummary) { items := make([]store.DailyTokenUsage, 0, 32) summary := dailyTokenUsageSummary{} currentRun := 0 for day := from; !day.After(to); day = day.AddDate(0, 0, 1) { key := day.Format("2006-01-02") item, found := byDate[key] if !found { item.Date = key } items = append(items, item) summary.CumulativeTokens += item.TotalTokens if item.TotalTokens > summary.PeakDailyTokens { summary.PeakDailyTokens = item.TotalTokens } if item.TaskCount > 0 { currentRun++ if currentRun > summary.LongestStreakDays { summary.LongestStreakDays = currentRun } } else { currentRun = 0 } } summary.CurrentStreakDays = currentRun return items, summary }