Files
easyai-ai-gateway/apps/api/internal/httpapi/daily_usage_handlers.go
T
easyai 3056cf8fca
ci / verify (pull_request) Successful in 15m34s
feat(gateway): 补齐桌面端高级媒体直连接口
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。

已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
2026-07-22 14:02:53 +08:00

102 lines
3.4 KiB
Go

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
}