| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- package admin
- import (
- "context"
- "encoding/json"
- "errors"
- "net/http"
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- var dashboardSnapshotV2Cache = newSnapshotCache(30 * time.Second)
- type dashboardSnapshotV2Stats struct {
- usagestats.DashboardStats
- Uptime int64 `json:"uptime"`
- }
- type dashboardSnapshotV2Response struct {
- GeneratedAt string `json:"generated_at"`
- StartDate string `json:"start_date"`
- EndDate string `json:"end_date"`
- Granularity string `json:"granularity"`
- Stats *dashboardSnapshotV2Stats `json:"stats,omitempty"`
- Trend []usagestats.TrendDataPoint `json:"trend,omitempty"`
- Models []usagestats.ModelStat `json:"models,omitempty"`
- Groups []usagestats.GroupStat `json:"groups,omitempty"`
- UsersTrend []usagestats.UserUsageTrendPoint `json:"users_trend,omitempty"`
- }
- type dashboardSnapshotV2Filters struct {
- UserID int64
- APIKeyID int64
- AccountID int64
- GroupID int64
- Model string
- RequestType *int16
- Stream *bool
- NativeCompactionV2 *bool
- BillingType *int8
- UpstreamModelMismatch *bool
- }
- type dashboardSnapshotV2CacheKey struct {
- StartTime string `json:"start_time"`
- EndTime string `json:"end_time"`
- Granularity string `json:"granularity"`
- UserID int64 `json:"user_id"`
- APIKeyID int64 `json:"api_key_id"`
- AccountID int64 `json:"account_id"`
- GroupID int64 `json:"group_id"`
- Model string `json:"model"`
- RequestType *int16 `json:"request_type"`
- Stream *bool `json:"stream"`
- NativeCompactionV2 *bool `json:"native_compaction_v2"`
- BillingType *int8 `json:"billing_type"`
- UpstreamModelMismatch *bool `json:"upstream_model_mismatch"`
- IncludeStats bool `json:"include_stats"`
- IncludeTrend bool `json:"include_trend"`
- IncludeModels bool `json:"include_models"`
- IncludeGroups bool `json:"include_groups"`
- IncludeUsersTrend bool `json:"include_users_trend"`
- UsersTrendLimit int `json:"users_trend_limit"`
- }
- func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
- startTime, endTime := parseTimeRange(c)
- granularity := strings.TrimSpace(c.DefaultQuery("granularity", "day"))
- if granularity != "hour" {
- granularity = "day"
- }
- includeStats := parseBoolQueryWithDefault(c.Query("include_stats"), true)
- includeTrend := parseBoolQueryWithDefault(c.Query("include_trend"), true)
- includeModels := parseBoolQueryWithDefault(c.Query("include_model_stats"), true)
- includeGroups := parseBoolQueryWithDefault(c.Query("include_group_stats"), false)
- includeUsersTrend := parseBoolQueryWithDefault(c.Query("include_users_trend"), false)
- usersTrendLimit := 12
- if raw := strings.TrimSpace(c.Query("users_trend_limit")); raw != "" {
- if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 50 {
- usersTrendLimit = parsed
- }
- }
- filters, err := parseDashboardSnapshotV2Filters(c)
- if err != nil {
- response.BadRequest(c, err.Error())
- return
- }
- keyRaw, _ := json.Marshal(dashboardSnapshotV2CacheKey{
- StartTime: startTime.UTC().Format(time.RFC3339),
- EndTime: endTime.UTC().Format(time.RFC3339),
- Granularity: granularity,
- UserID: filters.UserID,
- APIKeyID: filters.APIKeyID,
- AccountID: filters.AccountID,
- GroupID: filters.GroupID,
- Model: filters.Model,
- RequestType: filters.RequestType,
- Stream: filters.Stream,
- NativeCompactionV2: filters.NativeCompactionV2,
- BillingType: filters.BillingType,
- UpstreamModelMismatch: filters.UpstreamModelMismatch,
- IncludeStats: includeStats,
- IncludeTrend: includeTrend,
- IncludeModels: includeModels,
- IncludeGroups: includeGroups,
- IncludeUsersTrend: includeUsersTrend,
- UsersTrendLimit: usersTrendLimit,
- })
- cacheKey := string(keyRaw)
- cached, hit, err := dashboardSnapshotV2Cache.GetOrLoad(cacheKey, func() (any, error) {
- return h.buildSnapshotV2Response(
- c.Request.Context(),
- startTime,
- endTime,
- granularity,
- filters,
- includeStats,
- includeTrend,
- includeModels,
- includeGroups,
- includeUsersTrend,
- usersTrendLimit,
- )
- })
- if err != nil {
- response.Error(c, 500, err.Error())
- return
- }
- if cached.ETag != "" {
- c.Header("ETag", cached.ETag)
- c.Header("Vary", "If-None-Match")
- if ifNoneMatchMatched(c.GetHeader("If-None-Match"), cached.ETag) {
- c.Status(http.StatusNotModified)
- return
- }
- }
- c.Header("X-Snapshot-Cache", cacheStatusValue(hit))
- response.Success(c, cached.Payload)
- }
- func (h *DashboardHandler) buildSnapshotV2Response(
- ctx context.Context,
- startTime, endTime time.Time,
- granularity string,
- filters *dashboardSnapshotV2Filters,
- includeStats, includeTrend, includeModels, includeGroups, includeUsersTrend bool,
- usersTrendLimit int,
- ) (*dashboardSnapshotV2Response, error) {
- resp := &dashboardSnapshotV2Response{
- GeneratedAt: time.Now().UTC().Format(time.RFC3339),
- StartDate: startTime.Format("2006-01-02"),
- EndDate: endTime.Add(-24 * time.Hour).Format("2006-01-02"),
- Granularity: granularity,
- }
- if includeStats {
- stats, err := h.dashboardService.GetDashboardStats(ctx)
- if err != nil {
- return nil, errors.New("failed to get dashboard statistics")
- }
- resp.Stats = &dashboardSnapshotV2Stats{
- DashboardStats: *stats,
- Uptime: int64(time.Since(h.startTime).Seconds()),
- }
- }
- if includeTrend {
- trend, _, err := h.getUsageTrendCached(
- ctx,
- startTime,
- endTime,
- granularity,
- filters.UserID,
- filters.APIKeyID,
- filters.AccountID,
- filters.GroupID,
- filters.Model,
- filters.RequestType,
- filters.Stream,
- filters.NativeCompactionV2,
- filters.BillingType,
- filters.UpstreamModelMismatch,
- )
- if err != nil {
- return nil, errors.New("failed to get usage trend")
- }
- resp.Trend = trend
- }
- if includeModels {
- models, _, err := h.getModelStatsCached(
- ctx,
- startTime,
- endTime,
- filters.UserID,
- filters.APIKeyID,
- filters.AccountID,
- filters.GroupID,
- usagestats.ModelSourceRequested,
- filters.RequestType,
- filters.Stream,
- filters.NativeCompactionV2,
- filters.BillingType,
- filters.UpstreamModelMismatch,
- )
- if err != nil {
- return nil, errors.New("failed to get model statistics")
- }
- resp.Models = models
- }
- if includeGroups {
- groups, _, err := h.getGroupStatsCached(
- ctx,
- startTime,
- endTime,
- filters.UserID,
- filters.APIKeyID,
- filters.AccountID,
- filters.GroupID,
- filters.RequestType,
- filters.Stream,
- filters.NativeCompactionV2,
- filters.BillingType,
- filters.UpstreamModelMismatch,
- )
- if err != nil {
- return nil, errors.New("failed to get group statistics")
- }
- resp.Groups = groups
- }
- if includeUsersTrend {
- usersTrend, _, err := h.getUserUsageTrendCached(ctx, startTime, endTime, granularity, usersTrendLimit)
- if err != nil {
- return nil, errors.New("failed to get user usage trend")
- }
- resp.UsersTrend = usersTrend
- }
- return resp, nil
- }
- func parseDashboardSnapshotV2Filters(c *gin.Context) (*dashboardSnapshotV2Filters, error) {
- filters := &dashboardSnapshotV2Filters{
- Model: strings.TrimSpace(c.Query("model")),
- }
- if userIDStr := strings.TrimSpace(c.Query("user_id")); userIDStr != "" {
- id, err := strconv.ParseInt(userIDStr, 10, 64)
- if err != nil {
- return nil, err
- }
- filters.UserID = id
- }
- if apiKeyIDStr := strings.TrimSpace(c.Query("api_key_id")); apiKeyIDStr != "" {
- id, err := strconv.ParseInt(apiKeyIDStr, 10, 64)
- if err != nil {
- return nil, err
- }
- filters.APIKeyID = id
- }
- if accountIDStr := strings.TrimSpace(c.Query("account_id")); accountIDStr != "" {
- id, err := strconv.ParseInt(accountIDStr, 10, 64)
- if err != nil {
- return nil, err
- }
- filters.AccountID = id
- }
- if groupIDStr := strings.TrimSpace(c.Query("group_id")); groupIDStr != "" {
- id, err := strconv.ParseInt(groupIDStr, 10, 64)
- if err != nil {
- return nil, err
- }
- filters.GroupID = id
- }
- if requestTypeStr := strings.TrimSpace(c.Query("request_type")); requestTypeStr != "" {
- parsed, err := service.ParseUsageRequestType(requestTypeStr)
- if err != nil {
- return nil, err
- }
- value := int16(parsed)
- filters.RequestType = &value
- } else if streamStr := strings.TrimSpace(c.Query("stream")); streamStr != "" {
- streamVal, err := strconv.ParseBool(streamStr)
- if err != nil {
- return nil, err
- }
- filters.Stream = &streamVal
- }
- if nativeCompactionV2Str := strings.TrimSpace(c.Query("native_compaction_v2")); nativeCompactionV2Str != "" {
- value, err := strconv.ParseBool(nativeCompactionV2Str)
- if err != nil {
- return nil, err
- }
- filters.NativeCompactionV2 = &value
- }
- if billingTypeStr := strings.TrimSpace(c.Query("billing_type")); billingTypeStr != "" {
- v, err := strconv.ParseInt(billingTypeStr, 10, 8)
- if err != nil {
- return nil, err
- }
- bt := int8(v)
- filters.BillingType = &bt
- }
- if mismatchStr := strings.TrimSpace(c.Query("upstream_model_mismatch")); mismatchStr != "" {
- value, err := strconv.ParseBool(mismatchStr)
- if err != nil {
- return nil, err
- }
- filters.UpstreamModelMismatch = &value
- }
- return filters, nil
- }
|