mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-09 01:24:46 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
876e85e7ad | ||
|
|
2e7818d688 | ||
|
|
836c4dda2b | ||
|
|
e65e9587b4 | ||
|
|
aaadd6ed04 |
@@ -15,11 +15,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/handler"
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
"sub2api/internal/middleware"
|
"github.com/Wei-Shaw/sub2api/internal/middleware"
|
||||||
"sub2api/internal/setup"
|
"github.com/Wei-Shaw/sub2api/internal/setup"
|
||||||
"sub2api/internal/web"
|
"github.com/Wei-Shaw/sub2api/internal/web"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/handler"
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
"sub2api/internal/infrastructure"
|
"github.com/Wei-Shaw/sub2api/internal/infrastructure"
|
||||||
"sub2api/internal/repository"
|
"github.com/Wei-Shaw/sub2api/internal/repository"
|
||||||
"sub2api/internal/server"
|
"github.com/Wei-Shaw/sub2api/internal/server"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/infrastructure"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/repository"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2api/internal/config"
|
|
||||||
"sub2api/internal/handler"
|
|
||||||
"sub2api/internal/handler/admin"
|
|
||||||
"sub2api/internal/infrastructure"
|
|
||||||
"sub2api/internal/repository"
|
|
||||||
"sub2api/internal/server"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module sub2api
|
module github.com/Wei-Shaw/sub2api
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2api/internal/pkg/response"
|
|
||||||
"sub2api/internal/pkg/timezone"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -107,6 +107,10 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
|||||||
// 系统运行统计
|
// 系统运行统计
|
||||||
"average_duration_ms": stats.AverageDurationMs,
|
"average_duration_ms": stats.AverageDurationMs,
|
||||||
"uptime": uptime,
|
"uptime": uptime,
|
||||||
|
|
||||||
|
// 性能指标
|
||||||
|
"rpm": stats.Rpm,
|
||||||
|
"tpm": stats.Tpm,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -60,6 +60,7 @@ type UpdateSettingsRequest struct {
|
|||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
ApiBaseUrl string `json:"api_base_url"`
|
ApiBaseUrl string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
|
DocUrl string `json:"doc_url"`
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
DefaultConcurrency int `json:"default_concurrency"`
|
DefaultConcurrency int `json:"default_concurrency"`
|
||||||
@@ -104,6 +105,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
SiteSubtitle: req.SiteSubtitle,
|
SiteSubtitle: req.SiteSubtitle,
|
||||||
ApiBaseUrl: req.ApiBaseUrl,
|
ApiBaseUrl: req.ApiBaseUrl,
|
||||||
ContactInfo: req.ContactInfo,
|
ContactInfo: req.ContactInfo,
|
||||||
|
DocUrl: req.DocUrl,
|
||||||
DefaultConcurrency: req.DefaultConcurrency,
|
DefaultConcurrency: req.DefaultConcurrency,
|
||||||
DefaultBalance: req.DefaultBalance,
|
DefaultBalance: req.DefaultBalance,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/pkg/sysutil"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/sysutil"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
"sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/middleware"
|
"github.com/Wei-Shaw/sub2api/internal/middleware"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/handler/admin"
|
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AdminHandlers contains all admin-related HTTP handlers
|
// AdminHandlers contains all admin-related HTTP handlers
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/middleware"
|
"github.com/Wei-Shaw/sub2api/internal/middleware"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/handler/admin"
|
"github.com/Wei-Shaw/sub2api/internal/handler/admin"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package infrastructure
|
package infrastructure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
|
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package infrastructure
|
package infrastructure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package infrastructure
|
package infrastructure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2api/internal/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const (
|
|||||||
SettingKeySiteSubtitle = "site_subtitle" // 网站副标题
|
SettingKeySiteSubtitle = "site_subtitle" // 网站副标题
|
||||||
SettingKeyApiBaseUrl = "api_base_url" // API端点地址(用于客户端配置和导入)
|
SettingKeyApiBaseUrl = "api_base_url" // API端点地址(用于客户端配置和导入)
|
||||||
SettingKeyContactInfo = "contact_info" // 客服联系方式
|
SettingKeyContactInfo = "contact_info" // 客服联系方式
|
||||||
|
SettingKeyDocUrl = "doc_url" // 文档链接
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
|
SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量
|
||||||
@@ -80,6 +81,7 @@ type SystemSettings struct {
|
|||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
ApiBaseUrl string `json:"api_base_url"`
|
ApiBaseUrl string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
|
DocUrl string `json:"doc_url"`
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
DefaultConcurrency int `json:"default_concurrency"`
|
DefaultConcurrency int `json:"default_concurrency"`
|
||||||
@@ -97,5 +99,6 @@ type PublicSettings struct {
|
|||||||
SiteSubtitle string `json:"site_subtitle"`
|
SiteSubtitle string `json:"site_subtitle"`
|
||||||
ApiBaseUrl string `json:"api_base_url"`
|
ApiBaseUrl string `json:"api_base_url"`
|
||||||
ContactInfo string `json:"contact_info"`
|
ContactInfo string `json:"contact_info"`
|
||||||
|
DocUrl string `json:"doc_url"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ type DashboardStats struct {
|
|||||||
AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间
|
AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间
|
||||||
|
|
||||||
// 性能指标
|
// 性能指标
|
||||||
Rpm int64 `json:"rpm"` // 最近1分钟的请求数
|
Rpm int64 `json:"rpm"` // 近5分钟平均每分钟请求数
|
||||||
Tpm int64 `json:"tpm"` // 最近1分钟的Token数
|
Tpm int64 `json:"tpm"` // 近5分钟平均每分钟Token数
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrendDataPoint represents a single point in trend data
|
// TrendDataPoint represents a single point in trend data
|
||||||
@@ -121,8 +121,8 @@ type UserDashboardStats struct {
|
|||||||
AverageDurationMs float64 `json:"average_duration_ms"`
|
AverageDurationMs float64 `json:"average_duration_ms"`
|
||||||
|
|
||||||
// 性能指标
|
// 性能指标
|
||||||
Rpm int64 `json:"rpm"` // 最近1分钟的请求数
|
Rpm int64 `json:"rpm"` // 近5分钟平均每分钟请求数
|
||||||
Tpm int64 `json:"tpm"` // 最近1分钟的Token数
|
Tpm int64 `json:"tpm"` // 近5分钟平均每分钟Token数
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsageLogFilters represents filters for usage log queries
|
// UsageLogFilters represents filters for usage log queries
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/pkg/oauth"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type claudeUsageService struct{}
|
type claudeUsageService struct{}
|
||||||
|
|||||||
@@ -5,60 +5,95 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
accountConcurrencyKeyPrefix = "concurrency:account:"
|
// Key prefixes for independent slot keys
|
||||||
userConcurrencyKeyPrefix = "concurrency:user:"
|
// Format: concurrency:account:{accountID}:{requestID}
|
||||||
waitQueueKeyPrefix = "concurrency:wait:"
|
accountSlotKeyPrefix = "concurrency:account:"
|
||||||
concurrencyTTL = 5 * time.Minute
|
// Format: concurrency:user:{userID}:{requestID}
|
||||||
|
userSlotKeyPrefix = "concurrency:user:"
|
||||||
|
// Wait queue keeps counter format: concurrency:wait:{userID}
|
||||||
|
waitQueueKeyPrefix = "concurrency:wait:"
|
||||||
|
|
||||||
|
// Slot TTL - each slot expires independently
|
||||||
|
slotTTL = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// acquireScript uses SCAN to count existing slots and creates new slot if under limit
|
||||||
|
// KEYS[1] = pattern for SCAN (e.g., "concurrency:account:2:*")
|
||||||
|
// KEYS[2] = full slot key (e.g., "concurrency:account:2:req_xxx")
|
||||||
|
// ARGV[1] = maxConcurrency
|
||||||
|
// ARGV[2] = TTL in seconds
|
||||||
acquireScript = redis.NewScript(`
|
acquireScript = redis.NewScript(`
|
||||||
local current = redis.call('GET', KEYS[1])
|
local pattern = KEYS[1]
|
||||||
if current == false then
|
local slotKey = KEYS[2]
|
||||||
current = 0
|
local maxConcurrency = tonumber(ARGV[1])
|
||||||
else
|
local ttl = tonumber(ARGV[2])
|
||||||
current = tonumber(current)
|
|
||||||
end
|
-- Count existing slots using SCAN
|
||||||
if current < tonumber(ARGV[1]) then
|
local cursor = "0"
|
||||||
redis.call('INCR', KEYS[1])
|
local count = 0
|
||||||
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
repeat
|
||||||
|
local result = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', 100)
|
||||||
|
cursor = result[1]
|
||||||
|
count = count + #result[2]
|
||||||
|
until cursor == "0"
|
||||||
|
|
||||||
|
-- Check if we can acquire a slot
|
||||||
|
if count < maxConcurrency then
|
||||||
|
redis.call('SET', slotKey, '1', 'EX', ttl)
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
`)
|
`)
|
||||||
|
|
||||||
releaseScript = redis.NewScript(`
|
// getCountScript counts slots using SCAN
|
||||||
local current = redis.call('GET', KEYS[1])
|
// KEYS[1] = pattern for SCAN
|
||||||
if current ~= false and tonumber(current) > 0 then
|
getCountScript = redis.NewScript(`
|
||||||
redis.call('DECR', KEYS[1])
|
local pattern = KEYS[1]
|
||||||
end
|
local cursor = "0"
|
||||||
return 1
|
local count = 0
|
||||||
|
repeat
|
||||||
|
local result = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', 100)
|
||||||
|
cursor = result[1]
|
||||||
|
count = count + #result[2]
|
||||||
|
until cursor == "0"
|
||||||
|
return count
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// incrementWaitScript - only sets TTL on first creation to avoid refreshing
|
||||||
|
// KEYS[1] = wait queue key
|
||||||
|
// ARGV[1] = maxWait
|
||||||
|
// ARGV[2] = TTL in seconds
|
||||||
incrementWaitScript = redis.NewScript(`
|
incrementWaitScript = redis.NewScript(`
|
||||||
local waitKey = KEYS[1]
|
local current = redis.call('GET', KEYS[1])
|
||||||
local maxWait = tonumber(ARGV[1])
|
|
||||||
local ttl = tonumber(ARGV[2])
|
|
||||||
local current = redis.call('GET', waitKey)
|
|
||||||
if current == false then
|
if current == false then
|
||||||
current = 0
|
current = 0
|
||||||
else
|
else
|
||||||
current = tonumber(current)
|
current = tonumber(current)
|
||||||
end
|
end
|
||||||
if current >= maxWait then
|
|
||||||
|
if current >= tonumber(ARGV[1]) then
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
redis.call('INCR', waitKey)
|
|
||||||
redis.call('EXPIRE', waitKey, ttl)
|
local newVal = redis.call('INCR', KEYS[1])
|
||||||
|
|
||||||
|
-- Only set TTL on first creation to avoid refreshing zombie data
|
||||||
|
if newVal == 1 then
|
||||||
|
redis.call('EXPIRE', KEYS[1], ARGV[2])
|
||||||
|
end
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// decrementWaitScript - same as before
|
||||||
decrementWaitScript = redis.NewScript(`
|
decrementWaitScript = redis.NewScript(`
|
||||||
local current = redis.call('GET', KEYS[1])
|
local current = redis.call('GET', KEYS[1])
|
||||||
if current ~= false and tonumber(current) > 0 then
|
if current ~= false and tonumber(current) > 0 then
|
||||||
@@ -76,49 +111,86 @@ func NewConcurrencyCache(rdb *redis.Client) ports.ConcurrencyCache {
|
|||||||
return &concurrencyCache{rdb: rdb}
|
return &concurrencyCache{rdb: rdb}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (bool, error) {
|
// Helper functions for key generation
|
||||||
key := fmt.Sprintf("%s%d", accountConcurrencyKeyPrefix, accountID)
|
func accountSlotKey(accountID int64, requestID string) string {
|
||||||
result, err := acquireScript.Run(ctx, c.rdb, []string{key}, maxConcurrency, int(concurrencyTTL.Seconds())).Int()
|
return fmt.Sprintf("%s%d:%s", accountSlotKeyPrefix, accountID, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountSlotPattern(accountID int64) string {
|
||||||
|
return fmt.Sprintf("%s%d:*", accountSlotKeyPrefix, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSlotKey(userID int64, requestID string) string {
|
||||||
|
return fmt.Sprintf("%s%d:%s", userSlotKeyPrefix, userID, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSlotPattern(userID int64) string {
|
||||||
|
return fmt.Sprintf("%s%d:*", userSlotKeyPrefix, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitQueueKey(userID int64) string {
|
||||||
|
return fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account slot operations
|
||||||
|
|
||||||
|
func (c *concurrencyCache) AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int, requestID string) (bool, error) {
|
||||||
|
pattern := accountSlotPattern(accountID)
|
||||||
|
slotKey := accountSlotKey(accountID, requestID)
|
||||||
|
|
||||||
|
result, err := acquireScript.Run(ctx, c.rdb, []string{pattern, slotKey}, maxConcurrency, int(slotTTL.Seconds())).Int()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return result == 1, nil
|
return result == 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) ReleaseAccountSlot(ctx context.Context, accountID int64) error {
|
func (c *concurrencyCache) ReleaseAccountSlot(ctx context.Context, accountID int64, requestID string) error {
|
||||||
key := fmt.Sprintf("%s%d", accountConcurrencyKeyPrefix, accountID)
|
slotKey := accountSlotKey(accountID, requestID)
|
||||||
_, err := releaseScript.Run(ctx, c.rdb, []string{key}).Result()
|
return c.rdb.Del(ctx, slotKey).Err()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) GetAccountConcurrency(ctx context.Context, accountID int64) (int, error) {
|
func (c *concurrencyCache) GetAccountConcurrency(ctx context.Context, accountID int64) (int, error) {
|
||||||
key := fmt.Sprintf("%s%d", accountConcurrencyKeyPrefix, accountID)
|
pattern := accountSlotPattern(accountID)
|
||||||
return c.rdb.Get(ctx, key).Int()
|
result, err := getCountScript.Run(ctx, c.rdb, []string{pattern}).Int()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int) (bool, error) {
|
// User slot operations
|
||||||
key := fmt.Sprintf("%s%d", userConcurrencyKeyPrefix, userID)
|
|
||||||
result, err := acquireScript.Run(ctx, c.rdb, []string{key}, maxConcurrency, int(concurrencyTTL.Seconds())).Int()
|
func (c *concurrencyCache) AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int, requestID string) (bool, error) {
|
||||||
|
pattern := userSlotPattern(userID)
|
||||||
|
slotKey := userSlotKey(userID, requestID)
|
||||||
|
|
||||||
|
result, err := acquireScript.Run(ctx, c.rdb, []string{pattern, slotKey}, maxConcurrency, int(slotTTL.Seconds())).Int()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return result == 1, nil
|
return result == 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) ReleaseUserSlot(ctx context.Context, userID int64) error {
|
func (c *concurrencyCache) ReleaseUserSlot(ctx context.Context, userID int64, requestID string) error {
|
||||||
key := fmt.Sprintf("%s%d", userConcurrencyKeyPrefix, userID)
|
slotKey := userSlotKey(userID, requestID)
|
||||||
_, err := releaseScript.Run(ctx, c.rdb, []string{key}).Result()
|
return c.rdb.Del(ctx, slotKey).Err()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) GetUserConcurrency(ctx context.Context, userID int64) (int, error) {
|
func (c *concurrencyCache) GetUserConcurrency(ctx context.Context, userID int64) (int, error) {
|
||||||
key := fmt.Sprintf("%s%d", userConcurrencyKeyPrefix, userID)
|
pattern := userSlotPattern(userID)
|
||||||
return c.rdb.Get(ctx, key).Int()
|
result, err := getCountScript.Run(ctx, c.rdb, []string{pattern}).Int()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait queue operations
|
||||||
|
|
||||||
func (c *concurrencyCache) IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error) {
|
func (c *concurrencyCache) IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error) {
|
||||||
key := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
|
key := waitQueueKey(userID)
|
||||||
result, err := incrementWaitScript.Run(ctx, c.rdb, []string{key}, maxWait, int(concurrencyTTL.Seconds())).Int()
|
result, err := incrementWaitScript.Run(ctx, c.rdb, []string{key}, maxWait, int(slotTTL.Seconds())).Int()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -126,7 +198,7 @@ func (c *concurrencyCache) IncrementWaitCount(ctx context.Context, userID int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *concurrencyCache) DecrementWaitCount(ctx context.Context, userID int64) error {
|
func (c *concurrencyCache) DecrementWaitCount(ctx context.Context, userID int64) error {
|
||||||
key := fmt.Sprintf("%s%d", waitQueueKeyPrefix, userID)
|
key := waitQueueKey(userID)
|
||||||
_, err := decrementWaitScript.Run(ctx, c.rdb, []string{key}).Result()
|
_, err := decrementWaitScript.Run(ctx, c.rdb, []string{key}).Result()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type githubReleaseClient struct {
|
type githubReleaseClient struct {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// httpUpstreamService is a generic HTTP upstream service that can be used for
|
// httpUpstreamService is a generic HTTP upstream service that can be used for
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/imroc/req/v3"
|
"github.com/imroc/req/v3"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pricingRemoteClient struct {
|
type pricingRemoteClient struct {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"golang.org/x/net/proxy"
|
"golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
const turnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
const turnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
"sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -19,9 +19,9 @@ func NewUsageLogRepository(db *gorm.DB) *UsageLogRepository {
|
|||||||
return &UsageLogRepository{db: db}
|
return &UsageLogRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPerformanceStats 获取 RPM 和 TPM(可选按用户过滤)
|
// getPerformanceStats 获取 RPM 和 TPM(近5分钟平均值,可选按用户过滤)
|
||||||
func (r *UsageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64) {
|
func (r *UsageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64) {
|
||||||
oneMinuteAgo := time.Now().Add(-1 * time.Minute)
|
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
|
||||||
var perfStats struct {
|
var perfStats struct {
|
||||||
RequestCount int64 `gorm:"column:request_count"`
|
RequestCount int64 `gorm:"column:request_count"`
|
||||||
TokenCount int64 `gorm:"column:token_count"`
|
TokenCount int64 `gorm:"column:token_count"`
|
||||||
@@ -32,14 +32,15 @@ func (r *UsageLogRepository) getPerformanceStats(ctx context.Context, userID int
|
|||||||
COUNT(*) as request_count,
|
COUNT(*) as request_count,
|
||||||
COALESCE(SUM(input_tokens + output_tokens), 0) as token_count
|
COALESCE(SUM(input_tokens + output_tokens), 0) as token_count
|
||||||
`).
|
`).
|
||||||
Where("created_at >= ?", oneMinuteAgo)
|
Where("created_at >= ?", fiveMinutesAgo)
|
||||||
|
|
||||||
if userID > 0 {
|
if userID > 0 {
|
||||||
db = db.Where("user_id = ?", userID)
|
db = db.Where("user_id = ?", userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Scan(&perfStats)
|
db.Scan(&perfStats)
|
||||||
return perfStats.RequestCount, perfStats.TokenCount
|
// 返回5分钟平均值
|
||||||
|
return perfStats.RequestCount / 5, perfStats.TokenCount / 5
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UsageLogRepository) Create(ctx context.Context, log *model.UsageLog) error {
|
func (r *UsageLogRepository) Create(ctx context.Context, log *model.UsageLog) error {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/repository"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2api/internal/config"
|
|
||||||
"sub2api/internal/handler"
|
|
||||||
"sub2api/internal/repository"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/middleware"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/repository"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2api/internal/config"
|
|
||||||
"sub2api/internal/handler"
|
|
||||||
"sub2api/internal/middleware"
|
|
||||||
"sub2api/internal/repository"
|
|
||||||
"sub2api/internal/service"
|
|
||||||
"sub2api/internal/web"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// usageCache 用于缓存usage数据
|
// usageCache 用于缓存usage数据
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"log"
|
"log"
|
||||||
"sub2api/internal/config"
|
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/service/ports"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误定义
|
// 错误定义
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2api/internal/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// generateRequestID generates a unique request ID for concurrency slot tracking
|
||||||
|
// Uses 8 random bytes (16 hex chars) for uniqueness
|
||||||
|
func generateRequestID() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
// Fallback to nanosecond timestamp (extremely rare case)
|
||||||
|
return fmt.Sprintf("%x", time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Default extra wait slots beyond concurrency limit
|
// Default extra wait slots beyond concurrency limit
|
||||||
defaultExtraWaitSlots = 20
|
defaultExtraWaitSlots = 20
|
||||||
@@ -41,7 +55,10 @@ func (s *ConcurrencyService) AcquireAccountSlot(ctx context.Context, accountID i
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
acquired, err := s.cache.AcquireAccountSlot(ctx, accountID, maxConcurrency)
|
// Generate unique request ID for this slot
|
||||||
|
requestID := generateRequestID()
|
||||||
|
|
||||||
|
acquired, err := s.cache.AcquireAccountSlot(ctx, accountID, maxConcurrency, requestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -52,8 +69,8 @@ func (s *ConcurrencyService) AcquireAccountSlot(ctx context.Context, accountID i
|
|||||||
ReleaseFunc: func() {
|
ReleaseFunc: func() {
|
||||||
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.cache.ReleaseAccountSlot(bgCtx, accountID); err != nil {
|
if err := s.cache.ReleaseAccountSlot(bgCtx, accountID, requestID); err != nil {
|
||||||
log.Printf("Warning: failed to release account slot for %d: %v", accountID, err)
|
log.Printf("Warning: failed to release account slot for %d (req=%s): %v", accountID, requestID, err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
@@ -77,7 +94,10 @@ func (s *ConcurrencyService) AcquireUserSlot(ctx context.Context, userID int64,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
acquired, err := s.cache.AcquireUserSlot(ctx, userID, maxConcurrency)
|
// Generate unique request ID for this slot
|
||||||
|
requestID := generateRequestID()
|
||||||
|
|
||||||
|
acquired, err := s.cache.AcquireUserSlot(ctx, userID, maxConcurrency, requestID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -88,8 +108,8 @@ func (s *ConcurrencyService) AcquireUserSlot(ctx context.Context, userID int64,
|
|||||||
ReleaseFunc: func() {
|
ReleaseFunc: func() {
|
||||||
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := s.cache.ReleaseUserSlot(bgCtx, userID); err != nil {
|
if err := s.cache.ReleaseUserSlot(bgCtx, userID, requestID); err != nil {
|
||||||
log.Printf("Warning: failed to release user slot for %d: %v", userID, err)
|
log.Printf("Warning: failed to release user slot for %d (req=%s): %v", userID, requestID, err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DashboardService provides aggregated statistics for admin dashboard.
|
// DashboardService provides aggregated statistics for admin dashboard.
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/service/ports"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2api/internal/service/ports"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/oauth"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClaudeOAuthClient handles HTTP requests for Claude OAuth flows
|
// ClaudeOAuthClient handles HTTP requests for Claude OAuth flows
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenAIOAuthService handles OpenAI OAuth authentication flows
|
// OpenAIOAuthService handles OpenAI OAuth authentication flows
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccountRepository interface {
|
type AccountRepository interface {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApiKeyRepository interface {
|
type ApiKeyRepository interface {
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ package ports
|
|||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
// ConcurrencyCache defines cache operations for concurrency service
|
// ConcurrencyCache defines cache operations for concurrency service
|
||||||
|
// Uses independent keys per request slot with native Redis TTL for automatic cleanup
|
||||||
type ConcurrencyCache interface {
|
type ConcurrencyCache interface {
|
||||||
// Slot management
|
// Account slot management - each slot is a separate key with independent TTL
|
||||||
AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (bool, error)
|
// Key format: concurrency:account:{accountID}:{requestID}
|
||||||
ReleaseAccountSlot(ctx context.Context, accountID int64) error
|
AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int, requestID string) (bool, error)
|
||||||
|
ReleaseAccountSlot(ctx context.Context, accountID int64, requestID string) error
|
||||||
GetAccountConcurrency(ctx context.Context, accountID int64) (int, error)
|
GetAccountConcurrency(ctx context.Context, accountID int64) (int, error)
|
||||||
|
|
||||||
AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int) (bool, error)
|
// User slot management - each slot is a separate key with independent TTL
|
||||||
ReleaseUserSlot(ctx context.Context, userID int64) error
|
// Key format: concurrency:user:{userID}:{requestID}
|
||||||
|
AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int, requestID string) (bool, error)
|
||||||
|
ReleaseUserSlot(ctx context.Context, userID int64, requestID string) error
|
||||||
GetUserConcurrency(ctx context.Context, userID int64) (int, error)
|
GetUserConcurrency(ctx context.Context, userID int64) (int, error)
|
||||||
|
|
||||||
// Wait queue
|
// Wait queue - uses counter with TTL set only on creation
|
||||||
IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error)
|
IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error)
|
||||||
DecrementWaitCount(ctx context.Context, userID int64) error
|
DecrementWaitCount(ctx context.Context, userID int64) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenAIOAuthClient interface for OpenAI OAuth operations
|
// OpenAIOAuthClient interface for OpenAI OAuth operations
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxyRepository interface {
|
type ProxyRepository interface {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedeemCodeRepository interface {
|
type RedeemCodeRepository interface {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SettingRepository interface {
|
type SettingRepository interface {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UsageLogRepository interface {
|
type UsageLogRepository interface {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package ports
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserSubscriptionRepository interface {
|
type UserSubscriptionRepository interface {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LiteLLMModelPricing LiteLLM价格数据结构
|
// LiteLLMModelPricing LiteLLM价格数据结构
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RateLimitService 处理限流和过载状态管理
|
// RateLimitService 处理限流和过载状态管理
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/pkg/pagination"
|
|
||||||
"sub2api/internal/service/ports"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2api/internal/config"
|
|
||||||
"sub2api/internal/model"
|
|
||||||
"sub2api/internal/service/ports"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -54,6 +54,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*model.PublicSe
|
|||||||
model.SettingKeySiteSubtitle,
|
model.SettingKeySiteSubtitle,
|
||||||
model.SettingKeyApiBaseUrl,
|
model.SettingKeyApiBaseUrl,
|
||||||
model.SettingKeyContactInfo,
|
model.SettingKeyContactInfo,
|
||||||
|
model.SettingKeyDocUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||||
@@ -71,6 +72,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*model.PublicSe
|
|||||||
SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
||||||
ApiBaseUrl: settings[model.SettingKeyApiBaseUrl],
|
ApiBaseUrl: settings[model.SettingKeyApiBaseUrl],
|
||||||
ContactInfo: settings[model.SettingKeyContactInfo],
|
ContactInfo: settings[model.SettingKeyContactInfo],
|
||||||
|
DocUrl: settings[model.SettingKeyDocUrl],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +108,7 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *model.Sys
|
|||||||
updates[model.SettingKeySiteSubtitle] = settings.SiteSubtitle
|
updates[model.SettingKeySiteSubtitle] = settings.SiteSubtitle
|
||||||
updates[model.SettingKeyApiBaseUrl] = settings.ApiBaseUrl
|
updates[model.SettingKeyApiBaseUrl] = settings.ApiBaseUrl
|
||||||
updates[model.SettingKeyContactInfo] = settings.ContactInfo
|
updates[model.SettingKeyContactInfo] = settings.ContactInfo
|
||||||
|
updates[model.SettingKeyDocUrl] = settings.DocUrl
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
updates[model.SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
updates[model.SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency)
|
||||||
@@ -210,6 +213,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *model.System
|
|||||||
SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
||||||
ApiBaseUrl: settings[model.SettingKeyApiBaseUrl],
|
ApiBaseUrl: settings[model.SettingKeyApiBaseUrl],
|
||||||
ContactInfo: settings[model.SettingKeyContactInfo],
|
ContactInfo: settings[model.SettingKeyContactInfo],
|
||||||
|
DocUrl: settings[model.SettingKeyDocUrl],
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析整数类型
|
// 解析整数类型
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"sub2api/internal/service/ports"
|
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenRefreshService OAuth token自动刷新服务
|
// TokenRefreshService OAuth token自动刷新服务
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenRefresher 定义平台特定的token刷新策略接口
|
// TokenRefresher 定义平台特定的token刷新策略接口
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user