262 lines
7.6 KiB
Go
262 lines
7.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func cloneAPIKeyWithGroup(apiKey *service.APIKey, group *service.Group) *service.APIKey {
|
|
if apiKey == nil {
|
|
return nil
|
|
}
|
|
cloned := *apiKey
|
|
cloned.Group = group
|
|
if group != nil {
|
|
cloned.GroupID = &group.ID
|
|
}
|
|
return &cloned
|
|
}
|
|
|
|
func (h *GatewayHandler) parseUsageDateRange(c *gin.Context) (time.Time, time.Time) {
|
|
now := timezone.Now()
|
|
endTime := now
|
|
startTime := now.AddDate(0, 0, -30)
|
|
|
|
if s := c.Query("start_date"); s != "" {
|
|
if t, err := timezone.ParseInLocation("2006-01-02", s); err == nil {
|
|
startTime = t
|
|
}
|
|
}
|
|
if s := c.Query("end_date"); s != "" {
|
|
if t, err := timezone.ParseInLocation("2006-01-02", s); err == nil {
|
|
endTime = t.AddDate(0, 0, 1)
|
|
}
|
|
}
|
|
return startTime, endTime
|
|
}
|
|
|
|
func (h *GatewayHandler) buildUsageData(ctx context.Context, apiKeyID int64) gin.H {
|
|
if h.usageService == nil {
|
|
return nil
|
|
}
|
|
dashStats, err := h.usageService.GetAPIKeyDashboardStats(ctx, apiKeyID)
|
|
if err != nil || dashStats == nil {
|
|
return nil
|
|
}
|
|
return gin.H{
|
|
"today": gin.H{
|
|
"requests": dashStats.TodayRequests,
|
|
"input_tokens": dashStats.TodayInputTokens,
|
|
"output_tokens": dashStats.TodayOutputTokens,
|
|
"cache_creation_tokens": dashStats.TodayCacheCreationTokens,
|
|
"cache_read_tokens": dashStats.TodayCacheReadTokens,
|
|
"total_tokens": dashStats.TodayTokens,
|
|
"cost": dashStats.TodayCost,
|
|
"actual_cost": dashStats.TodayActualCost,
|
|
},
|
|
"total": gin.H{
|
|
"requests": dashStats.TotalRequests,
|
|
"input_tokens": dashStats.TotalInputTokens,
|
|
"output_tokens": dashStats.TotalOutputTokens,
|
|
"cache_creation_tokens": dashStats.TotalCacheCreationTokens,
|
|
"cache_read_tokens": dashStats.TotalCacheReadTokens,
|
|
"total_tokens": dashStats.TotalTokens,
|
|
"cost": dashStats.TotalCost,
|
|
"actual_cost": dashStats.TotalActualCost,
|
|
},
|
|
"average_duration_ms": dashStats.AverageDurationMs,
|
|
"rpm": dashStats.Rpm,
|
|
"tpm": dashStats.Tpm,
|
|
}
|
|
}
|
|
|
|
func (h *GatewayHandler) usageQuotaLimited(c *gin.Context, ctx context.Context, apiKey *service.APIKey, usageData gin.H, modelStats any) {
|
|
resp := gin.H{
|
|
"mode": "quota_limited",
|
|
"isValid": apiKey.Status == service.StatusAPIKeyActive || apiKey.Status == service.StatusAPIKeyQuotaExhausted || apiKey.Status == service.StatusAPIKeyExpired,
|
|
"status": apiKey.Status,
|
|
}
|
|
|
|
if apiKey.Quota > 0 {
|
|
remaining := apiKey.GetQuotaRemaining()
|
|
resp["quota"] = gin.H{
|
|
"limit": apiKey.Quota,
|
|
"used": apiKey.QuotaUsed,
|
|
"remaining": remaining,
|
|
"unit": "USD",
|
|
}
|
|
resp["remaining"] = remaining
|
|
resp["unit"] = "USD"
|
|
}
|
|
|
|
if apiKey.HasRateLimits() && h.apiKeyService != nil {
|
|
rateLimitData, err := h.apiKeyService.GetRateLimitData(ctx, apiKey.ID)
|
|
if err == nil && rateLimitData != nil {
|
|
var rateLimits []gin.H
|
|
if apiKey.RateLimit5h > 0 {
|
|
used := rateLimitData.EffectiveUsage5h()
|
|
entry := gin.H{
|
|
"window": "5h",
|
|
"limit": apiKey.RateLimit5h,
|
|
"used": used,
|
|
"remaining": max(0, apiKey.RateLimit5h-used),
|
|
"window_start": rateLimitData.Window5hStart,
|
|
}
|
|
if rateLimitData.Window5hStart != nil && !service.IsWindowExpired(rateLimitData.Window5hStart, service.RateLimitWindow5h) {
|
|
entry["reset_at"] = rateLimitData.Window5hStart.Add(service.RateLimitWindow5h)
|
|
}
|
|
rateLimits = append(rateLimits, entry)
|
|
}
|
|
if apiKey.RateLimit1d > 0 {
|
|
used := rateLimitData.EffectiveUsage1d()
|
|
entry := gin.H{
|
|
"window": "1d",
|
|
"limit": apiKey.RateLimit1d,
|
|
"used": used,
|
|
"remaining": max(0, apiKey.RateLimit1d-used),
|
|
"window_start": rateLimitData.Window1dStart,
|
|
}
|
|
if rateLimitData.Window1dStart != nil && !service.IsWindowExpired(rateLimitData.Window1dStart, service.RateLimitWindow1d) {
|
|
entry["reset_at"] = rateLimitData.Window1dStart.Add(service.RateLimitWindow1d)
|
|
}
|
|
rateLimits = append(rateLimits, entry)
|
|
}
|
|
if apiKey.RateLimit7d > 0 {
|
|
used := rateLimitData.EffectiveUsage7d()
|
|
entry := gin.H{
|
|
"window": "7d",
|
|
"limit": apiKey.RateLimit7d,
|
|
"used": used,
|
|
"remaining": max(0, apiKey.RateLimit7d-used),
|
|
"window_start": rateLimitData.Window7dStart,
|
|
}
|
|
if rateLimitData.Window7dStart != nil && !service.IsWindowExpired(rateLimitData.Window7dStart, service.RateLimitWindow7d) {
|
|
entry["reset_at"] = rateLimitData.Window7dStart.Add(service.RateLimitWindow7d)
|
|
}
|
|
rateLimits = append(rateLimits, entry)
|
|
}
|
|
if len(rateLimits) > 0 {
|
|
resp["rate_limits"] = rateLimits
|
|
}
|
|
}
|
|
}
|
|
|
|
if apiKey.ExpiresAt != nil {
|
|
resp["expires_at"] = apiKey.ExpiresAt
|
|
resp["days_until_expiry"] = apiKey.GetDaysUntilExpiry()
|
|
}
|
|
|
|
if usageData != nil {
|
|
resp["usage"] = usageData
|
|
}
|
|
if modelStats != nil {
|
|
resp["model_stats"] = modelStats
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *GatewayHandler) usageUnrestricted(c *gin.Context, ctx context.Context, apiKey *service.APIKey, subject middleware2.AuthSubject, usageData gin.H, modelStats any) {
|
|
if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() {
|
|
resp := gin.H{
|
|
"mode": "unrestricted",
|
|
"isValid": true,
|
|
"planName": apiKey.Group.Name,
|
|
"unit": "USD",
|
|
}
|
|
|
|
subscription, ok := middleware2.GetSubscriptionFromContext(c)
|
|
if ok {
|
|
remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription)
|
|
resp["remaining"] = remaining
|
|
resp["subscription"] = gin.H{
|
|
"daily_usage_usd": subscription.DailyUsageUSD,
|
|
"weekly_usage_usd": subscription.WeeklyUsageUSD,
|
|
"monthly_usage_usd": subscription.MonthlyUsageUSD,
|
|
"daily_limit_usd": apiKey.Group.DailyLimitUSD,
|
|
"weekly_limit_usd": apiKey.Group.WeeklyLimitUSD,
|
|
"monthly_limit_usd": apiKey.Group.MonthlyLimitUSD,
|
|
"expires_at": subscription.ExpiresAt,
|
|
}
|
|
}
|
|
|
|
if usageData != nil {
|
|
resp["usage"] = usageData
|
|
}
|
|
if modelStats != nil {
|
|
resp["model_stats"] = modelStats
|
|
}
|
|
c.JSON(http.StatusOK, resp)
|
|
return
|
|
}
|
|
|
|
latestUser, err := h.userService.GetByID(ctx, subject.UserID)
|
|
if err != nil {
|
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info")
|
|
return
|
|
}
|
|
|
|
resp := gin.H{
|
|
"mode": "unrestricted",
|
|
"isValid": true,
|
|
"planName": "钱包余额",
|
|
"remaining": latestUser.Balance,
|
|
"unit": "USD",
|
|
"balance": latestUser.Balance,
|
|
}
|
|
if usageData != nil {
|
|
resp["usage"] = usageData
|
|
}
|
|
if modelStats != nil {
|
|
resp["model_stats"] = modelStats
|
|
}
|
|
c.JSON(http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *GatewayHandler) calculateSubscriptionRemaining(group *service.Group, sub *service.UserSubscription) float64 {
|
|
var remainingValues []float64
|
|
|
|
if group.HasDailyLimit() {
|
|
remaining := *group.DailyLimitUSD - sub.DailyUsageUSD
|
|
if remaining <= 0 {
|
|
return 0
|
|
}
|
|
remainingValues = append(remainingValues, remaining)
|
|
}
|
|
|
|
if group.HasWeeklyLimit() {
|
|
remaining := *group.WeeklyLimitUSD - sub.WeeklyUsageUSD
|
|
if remaining <= 0 {
|
|
return 0
|
|
}
|
|
remainingValues = append(remainingValues, remaining)
|
|
}
|
|
|
|
if group.HasMonthlyLimit() {
|
|
remaining := *group.MonthlyLimitUSD - sub.MonthlyUsageUSD
|
|
if remaining <= 0 {
|
|
return 0
|
|
}
|
|
remainingValues = append(remainingValues, remaining)
|
|
}
|
|
|
|
if len(remainingValues) == 0 {
|
|
return -1
|
|
}
|
|
|
|
minValue := remainingValues[0]
|
|
for _, v := range remainingValues[1:] {
|
|
if v < minValue {
|
|
minValue = v
|
|
}
|
|
}
|
|
return minValue
|
|
}
|