From 57fd172287b80227fb0534f5374f4316e5cdf254 Mon Sep 17 00:00:00 2001 From: Forest Date: Fri, 26 Dec 2025 10:42:08 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=B0=83=E6=95=B4=20server=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/server/main.go | 2 +- backend/cmd/server/wire.go | 28 +- backend/cmd/server/wire_gen.go | 70 +--- backend/internal/handler/gateway_handler.go | 22 +- .../handler/openai_gateway_handler.go | 8 +- backend/internal/repository/repository.go | 16 - backend/internal/repository/wire.go | 1 - backend/internal/server/http.go | 21 +- .../{ => server}/middleware/admin_auth.go | 35 +- .../{ => server}/middleware/admin_only.go | 0 .../{ => server}/middleware/api_key_auth.go | 31 +- .../internal/{ => server}/middleware/cors.go | 0 .../{ => server}/middleware/jwt_auth.go | 14 +- .../{ => server}/middleware/logger.go | 0 .../{ => server}/middleware/middleware.go | 0 .../{ => server}/middleware/recovery.go | 0 .../{ => server}/middleware/recovery_test.go | 0 backend/internal/server/middleware/wire.go | 22 ++ backend/internal/server/router.go | 314 ++---------------- backend/internal/server/routes/admin.go | 221 ++++++++++++ backend/internal/server/routes/auth.go | 36 ++ backend/internal/server/routes/common.go | 32 ++ backend/internal/server/routes/gateway.go | 30 ++ backend/internal/server/routes/user.go | 72 ++++ backend/internal/service/service.go | 33 -- backend/internal/service/user_service.go | 9 + backend/internal/service/wire.go | 3 - 27 files changed, 548 insertions(+), 472 deletions(-) delete mode 100644 backend/internal/repository/repository.go rename backend/internal/{ => server}/middleware/admin_auth.go (79%) rename backend/internal/{ => server}/middleware/admin_only.go (100%) rename backend/internal/{ => server}/middleware/api_key_auth.go (77%) rename backend/internal/{ => server}/middleware/cors.go (100%) rename backend/internal/{ => server}/middleware/jwt_auth.go (79%) rename backend/internal/{ => server}/middleware/logger.go (100%) rename backend/internal/{ => server}/middleware/middleware.go (100%) rename backend/internal/{ => server}/middleware/recovery.go (100%) rename backend/internal/{ => server}/middleware/recovery_test.go (100%) create mode 100644 backend/internal/server/middleware/wire.go create mode 100644 backend/internal/server/routes/admin.go create mode 100644 backend/internal/server/routes/auth.go create mode 100644 backend/internal/server/routes/common.go create mode 100644 backend/internal/server/routes/gateway.go create mode 100644 backend/internal/server/routes/user.go delete mode 100644 backend/internal/service/service.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index d035d9a7..a81a572e 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -17,7 +17,7 @@ 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/server/middleware" "github.com/Wei-Shaw/sub2api/internal/setup" "github.com/Wei-Shaw/sub2api/internal/web" diff --git a/backend/cmd/server/wire.go b/backend/cmd/server/wire.go index 94d02be5..7d6ec065 100644 --- a/backend/cmd/server/wire.go +++ b/backend/cmd/server/wire.go @@ -4,18 +4,19 @@ package main import ( + "context" + "log" + "net/http" + "time" + "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/handler" "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/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" - "context" - "log" - "net/http" - "time" - "github.com/google/wire" "github.com/redis/go-redis/v9" "gorm.io/gorm" @@ -35,6 +36,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { // 业务层 ProviderSets repository.ProviderSet, service.ProviderSet, + middleware.ProviderSet, handler.ProviderSet, // 服务器层 ProviderSet @@ -62,7 +64,11 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo { func provideCleanup( db *gorm.DB, rdb *redis.Client, - services *service.Services, + tokenRefresh *service.TokenRefreshService, + pricing *service.PricingService, + emailQueue *service.EmailQueueService, + oauth *service.OAuthService, + openaiOAuth *service.OpenAIOAuthService, ) func() { return func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -74,23 +80,23 @@ func provideCleanup( fn func() error }{ {"TokenRefreshService", func() error { - services.TokenRefresh.Stop() + tokenRefresh.Stop() return nil }}, {"PricingService", func() error { - services.Pricing.Stop() + pricing.Stop() return nil }}, {"EmailQueueService", func() error { - services.EmailQueue.Stop() + emailQueue.Stop() return nil }}, {"OAuthService", func() error { - services.OAuth.Stop() + oauth.Stop() return nil }}, {"OpenAIOAuthService", func() error { - services.OpenAIOAuth.Stop() + openaiOAuth.Stop() return nil }}, {"Redis", func() error { diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 8a887c1b..e72e5f6e 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -14,6 +14,7 @@ import ( "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/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "gorm.io/gorm" @@ -116,54 +117,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService) handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo) handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler) - groupService := service.NewGroupService(groupRepository) - accountService := service.NewAccountService(accountRepository, groupRepository) - proxyService := service.NewProxyService(proxyRepository) - tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, configConfig) - services := &service.Services{ - Auth: authService, - User: userService, - ApiKey: apiKeyService, - Group: groupService, - Account: accountService, - Proxy: proxyService, - Redeem: redeemService, - Usage: usageService, - Pricing: pricingService, - Billing: billingService, - BillingCache: billingCacheService, - Admin: adminService, - Gateway: gatewayService, - OpenAIGateway: openAIGatewayService, - OAuth: oAuthService, - OpenAIOAuth: openAIOAuthService, - RateLimit: rateLimitService, - AccountUsage: accountUsageService, - AccountTest: accountTestService, - Setting: settingService, - Email: emailService, - EmailQueue: emailQueueService, - Turnstile: turnstileService, - Subscription: subscriptionService, - Concurrency: concurrencyService, - Identity: identityService, - Update: updateService, - TokenRefresh: tokenRefreshService, - } - repositories := &repository.Repositories{ - User: userRepository, - ApiKey: apiKeyRepository, - Group: groupRepository, - Account: accountRepository, - Proxy: proxyRepository, - RedeemCode: redeemCodeRepository, - UsageLog: usageLogRepository, - Setting: settingRepository, - UserSubscription: userSubscriptionRepository, - } - engine := server.ProvideRouter(configConfig, handlers, services, repositories) + jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService) + adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService) + apiKeyAuthMiddleware := middleware.NewApiKeyAuthMiddleware(apiKeyService, subscriptionService) + engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware) httpServer := server.ProvideHTTPServer(configConfig, engine) - v := provideCleanup(db, client, services) + tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, configConfig) + v := provideCleanup(db, client, tokenRefreshService, pricingService, emailQueueService, oAuthService, openAIOAuthService) application := &Application{ Server: httpServer, Cleanup: v, @@ -188,7 +148,11 @@ func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo { func provideCleanup( db *gorm.DB, rdb *redis.Client, - services *service.Services, + tokenRefresh *service.TokenRefreshService, + pricing *service.PricingService, + emailQueue *service.EmailQueueService, + oauth *service.OAuthService, + openaiOAuth *service.OpenAIOAuthService, ) func() { return func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -199,23 +163,23 @@ func provideCleanup( fn func() error }{ {"TokenRefreshService", func() error { - services.TokenRefresh.Stop() + tokenRefresh.Stop() return nil }}, {"PricingService", func() error { - services.Pricing.Stop() + pricing.Stop() return nil }}, {"EmailQueueService", func() error { - services.EmailQueue.Stop() + emailQueue.Stop() return nil }}, {"OAuthService", func() error { - services.OAuth.Stop() + oauth.Stop() return nil }}, {"OpenAIOAuthService", func() error { - services.OpenAIOAuth.Stop() + openaiOAuth.Stop() return nil }}, {"Redis", func() error { diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index 9a9792ce..bfb8b6fd 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -10,10 +10,10 @@ import ( "strings" "time" - "github.com/Wei-Shaw/sub2api/internal/middleware" "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" + middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" @@ -41,13 +41,13 @@ func NewGatewayHandler(gatewayService *service.GatewayService, userService *serv // POST /v1/messages func (h *GatewayHandler) Messages(c *gin.Context) { // 从context获取apiKey和user(ApiKeyAuth中间件已设置) - apiKey, ok := middleware.GetApiKeyFromContext(c) + apiKey, ok := middleware2.GetApiKeyFromContext(c) if !ok { h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") return } - user, ok := middleware.GetUserFromContext(c) + user, ok := middleware2.GetUserFromContext(c) if !ok { h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found") return @@ -79,7 +79,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { streamStarted := false // 获取订阅信息(可能为nil)- 提前获取用于后续检查 - subscription, _ := middleware.GetSubscriptionFromContext(c) + subscription, _ := middleware2.GetSubscriptionFromContext(c) // 0. 检查wait队列是否已满 maxWait := service.CalculateMaxWait(user.Concurrency) @@ -171,7 +171,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { // GET /v1/models // Returns different model lists based on the API key's group platform func (h *GatewayHandler) Models(c *gin.Context) { - apiKey, _ := middleware.GetApiKeyFromContext(c) + apiKey, _ := middleware2.GetApiKeyFromContext(c) // Return OpenAI models for OpenAI platform groups if apiKey != nil && apiKey.Group != nil && apiKey.Group.Platform == "openai" { @@ -192,13 +192,13 @@ func (h *GatewayHandler) Models(c *gin.Context) { // Usage handles getting account balance for CC Switch integration // GET /v1/usage func (h *GatewayHandler) Usage(c *gin.Context) { - apiKey, ok := middleware.GetApiKeyFromContext(c) + apiKey, ok := middleware2.GetApiKeyFromContext(c) if !ok { h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") return } - user, ok := middleware.GetUserFromContext(c) + user, ok := middleware2.GetUserFromContext(c) if !ok { h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") return @@ -206,7 +206,7 @@ func (h *GatewayHandler) Usage(c *gin.Context) { // 订阅模式:返回订阅限额信息 if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() { - subscription, ok := middleware.GetSubscriptionFromContext(c) + subscription, ok := middleware2.GetSubscriptionFromContext(c) if !ok { h.errorResponse(c, http.StatusForbidden, "subscription_error", "No active subscription") return @@ -328,13 +328,13 @@ func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, mess // 特点:校验订阅/余额,但不计算并发、不记录使用量 func (h *GatewayHandler) CountTokens(c *gin.Context) { // 从context获取apiKey和user(ApiKeyAuth中间件已设置) - apiKey, ok := middleware.GetApiKeyFromContext(c) + apiKey, ok := middleware2.GetApiKeyFromContext(c) if !ok { h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") return } - user, ok := middleware.GetUserFromContext(c) + user, ok := middleware2.GetUserFromContext(c) if !ok { h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found") return @@ -362,7 +362,7 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) { } // 获取订阅信息(可能为nil) - subscription, _ := middleware.GetSubscriptionFromContext(c) + subscription, _ := middleware2.GetSubscriptionFromContext(c) // 校验 billing eligibility(订阅/余额) // 【注意】不计算并发,但需要校验订阅/余额 diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index 2ba78b53..d1956eca 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -9,8 +9,8 @@ import ( "net/http" "time" - "github.com/Wei-Shaw/sub2api/internal/middleware" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" + middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" @@ -40,13 +40,13 @@ func NewOpenAIGatewayHandler( // POST /openai/v1/responses func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { // Get apiKey and user from context (set by ApiKeyAuth middleware) - apiKey, ok := middleware.GetApiKeyFromContext(c) + apiKey, ok := middleware2.GetApiKeyFromContext(c) if !ok { h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") return } - user, ok := middleware.GetUserFromContext(c) + user, ok := middleware2.GetUserFromContext(c) if !ok { h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found") return @@ -91,7 +91,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { streamStarted := false // Get subscription info (may be nil) - subscription, _ := middleware.GetSubscriptionFromContext(c) + subscription, _ := middleware2.GetSubscriptionFromContext(c) // 0. Check if wait queue is full maxWait := service.CalculateMaxWait(user.Concurrency) diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go deleted file mode 100644 index b76c0d82..00000000 --- a/backend/internal/repository/repository.go +++ /dev/null @@ -1,16 +0,0 @@ -package repository - -import "github.com/Wei-Shaw/sub2api/internal/service" - -// Repositories 所有仓库的集合 -type Repositories struct { - User service.UserRepository - ApiKey service.ApiKeyRepository - Group service.GroupRepository - Account service.AccountRepository - Proxy service.ProxyRepository - RedeemCode service.RedeemCodeRepository - UsageLog service.UsageLogRepository - Setting service.SettingRepository - UserSubscription service.UserSubscriptionRepository -} diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go index 6dff6407..ceeb82fc 100644 --- a/backend/internal/repository/wire.go +++ b/backend/internal/repository/wire.go @@ -15,7 +15,6 @@ var ProviderSet = wire.NewSet( NewUsageLogRepository, NewSettingRepository, NewUserSubscriptionRepository, - wire.Struct(new(Repositories), "*"), // Cache implementations NewGatewayCache, diff --git a/backend/internal/server/http.go b/backend/internal/server/http.go index f9ab1174..f673925d 100644 --- a/backend/internal/server/http.go +++ b/backend/internal/server/http.go @@ -1,14 +1,13 @@ package server 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" "net/http" "time" + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/handler" + middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" + "github.com/gin-gonic/gin" "github.com/google/wire" ) @@ -20,15 +19,21 @@ var ProviderSet = wire.NewSet( ) // ProvideRouter 提供路由器 -func ProvideRouter(cfg *config.Config, handlers *handler.Handlers, services *service.Services, repos *repository.Repositories) *gin.Engine { +func ProvideRouter( + cfg *config.Config, + handlers *handler.Handlers, + jwtAuth middleware2.JWTAuthMiddleware, + adminAuth middleware2.AdminAuthMiddleware, + apiKeyAuth middleware2.ApiKeyAuthMiddleware, +) *gin.Engine { if cfg.Server.Mode == "release" { gin.SetMode(gin.ReleaseMode) } r := gin.New() - r.Use(middleware.Recovery()) + r.Use(middleware2.Recovery()) - return SetupRouter(r, cfg, handlers, services, repos) + return SetupRouter(r, handlers, jwtAuth, adminAuth, apiKeyAuth) } // ProvideHTTPServer 提供 HTTP 服务器 diff --git a/backend/internal/middleware/admin_auth.go b/backend/internal/server/middleware/admin_auth.go similarity index 79% rename from backend/internal/middleware/admin_auth.go rename to backend/internal/server/middleware/admin_auth.go index 8d1e819b..70e2f230 100644 --- a/backend/internal/middleware/admin_auth.go +++ b/backend/internal/server/middleware/admin_auth.go @@ -1,7 +1,6 @@ package middleware import ( - "context" "crypto/subtle" "errors" "strings" @@ -12,23 +11,29 @@ import ( "github.com/gin-gonic/gin" ) -// AdminAuth 管理员认证中间件 +// NewAdminAuthMiddleware 创建管理员认证中间件 +func NewAdminAuthMiddleware( + authService *service.AuthService, + userService *service.UserService, + settingService *service.SettingService, +) AdminAuthMiddleware { + return AdminAuthMiddleware(adminAuth(authService, userService, settingService)) +} + +// adminAuth 管理员认证中间件实现 // 支持两种认证方式(通过不同的 header 区分): // 1. Admin API Key: x-api-key: // 2. JWT Token: Authorization: Bearer (需要管理员角色) -func AdminAuth( +func adminAuth( authService *service.AuthService, - userRepo interface { - GetByID(ctx context.Context, id int64) (*model.User, error) - GetFirstAdmin(ctx context.Context) (*model.User, error) - }, + userService *service.UserService, settingService *service.SettingService, ) gin.HandlerFunc { return func(c *gin.Context) { // 检查 x-api-key header(Admin API Key 认证) apiKey := c.GetHeader("x-api-key") if apiKey != "" { - if !validateAdminApiKey(c, apiKey, settingService, userRepo) { + if !validateAdminApiKey(c, apiKey, settingService, userService) { return } c.Next() @@ -40,7 +45,7 @@ func AdminAuth( if authHeader != "" { parts := strings.SplitN(authHeader, " ", 2) if len(parts) == 2 && parts[0] == "Bearer" { - if !validateJWTForAdmin(c, parts[1], authService, userRepo) { + if !validateJWTForAdmin(c, parts[1], authService, userService) { return } c.Next() @@ -58,9 +63,7 @@ func validateAdminApiKey( c *gin.Context, key string, settingService *service.SettingService, - userRepo interface { - GetFirstAdmin(ctx context.Context) (*model.User, error) - }, + userService *service.UserService, ) bool { storedKey, err := settingService.GetAdminApiKey(c.Request.Context()) if err != nil { @@ -75,7 +78,7 @@ func validateAdminApiKey( } // 获取真实的管理员用户 - admin, err := userRepo.GetFirstAdmin(c.Request.Context()) + admin, err := userService.GetFirstAdmin(c.Request.Context()) if err != nil { AbortWithError(c, 500, "INTERNAL_ERROR", "No admin user found") return false @@ -91,9 +94,7 @@ func validateJWTForAdmin( c *gin.Context, token string, authService *service.AuthService, - userRepo interface { - GetByID(ctx context.Context, id int64) (*model.User, error) - }, + userService *service.UserService, ) bool { // 验证 JWT token claims, err := authService.ValidateToken(token) @@ -107,7 +108,7 @@ func validateJWTForAdmin( } // 从数据库获取用户 - user, err := userRepo.GetByID(c.Request.Context(), claims.UserID) + user, err := userService.GetByID(c.Request.Context(), claims.UserID) if err != nil { AbortWithError(c, 401, "USER_NOT_FOUND", "User not found") return false diff --git a/backend/internal/middleware/admin_only.go b/backend/internal/server/middleware/admin_only.go similarity index 100% rename from backend/internal/middleware/admin_only.go rename to backend/internal/server/middleware/admin_only.go diff --git a/backend/internal/middleware/api_key_auth.go b/backend/internal/server/middleware/api_key_auth.go similarity index 77% rename from backend/internal/middleware/api_key_auth.go rename to backend/internal/server/middleware/api_key_auth.go index 2c97ba7e..3a19e664 100644 --- a/backend/internal/middleware/api_key_auth.go +++ b/backend/internal/server/middleware/api_key_auth.go @@ -1,37 +1,24 @@ package middleware import ( - "context" "errors" - "github.com/Wei-Shaw/sub2api/internal/model" "log" "strings" + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" "gorm.io/gorm" ) -// ApiKeyAuthService 定义API Key认证服务需要的接口 -type ApiKeyAuthService interface { - GetByKey(ctx context.Context, key string) (*model.ApiKey, error) +// NewApiKeyAuthMiddleware 创建 API Key 认证中间件 +func NewApiKeyAuthMiddleware(apiKeyService *service.ApiKeyService, subscriptionService *service.SubscriptionService) ApiKeyAuthMiddleware { + return ApiKeyAuthMiddleware(apiKeyAuthWithSubscription(apiKeyService, subscriptionService)) } -// SubscriptionAuthService 定义订阅认证服务需要的接口 -type SubscriptionAuthService interface { - GetActiveSubscription(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) - ValidateSubscription(ctx context.Context, sub *model.UserSubscription) error - CheckAndActivateWindow(ctx context.Context, sub *model.UserSubscription) error - CheckAndResetWindows(ctx context.Context, sub *model.UserSubscription) error - CheckUsageLimits(ctx context.Context, sub *model.UserSubscription, group *model.Group, additionalCost float64) error -} - -// ApiKeyAuth API Key认证中间件 -func ApiKeyAuth(apiKeyRepo ApiKeyAuthService) gin.HandlerFunc { - return ApiKeyAuthWithSubscription(apiKeyRepo, nil) -} - -// ApiKeyAuthWithSubscription API Key认证中间件(支持订阅验证) -func ApiKeyAuthWithSubscription(apiKeyRepo ApiKeyAuthService, subscriptionService SubscriptionAuthService) gin.HandlerFunc { +// apiKeyAuthWithSubscription API Key认证中间件(支持订阅验证) +func apiKeyAuthWithSubscription(apiKeyService *service.ApiKeyService, subscriptionService *service.SubscriptionService) gin.HandlerFunc { return func(c *gin.Context) { // 尝试从Authorization header中提取API key (Bearer scheme) authHeader := c.GetHeader("Authorization") @@ -57,7 +44,7 @@ func ApiKeyAuthWithSubscription(apiKeyRepo ApiKeyAuthService, subscriptionServic } // 从数据库验证API key - apiKey, err := apiKeyRepo.GetByKey(c.Request.Context(), apiKeyString) + apiKey, err := apiKeyService.GetByKey(c.Request.Context(), apiKeyString) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { AbortWithError(c, 401, "INVALID_API_KEY", "Invalid API key") diff --git a/backend/internal/middleware/cors.go b/backend/internal/server/middleware/cors.go similarity index 100% rename from backend/internal/middleware/cors.go rename to backend/internal/server/middleware/cors.go diff --git a/backend/internal/middleware/jwt_auth.go b/backend/internal/server/middleware/jwt_auth.go similarity index 79% rename from backend/internal/middleware/jwt_auth.go rename to backend/internal/server/middleware/jwt_auth.go index cd8dd7f6..10ea8e7e 100644 --- a/backend/internal/middleware/jwt_auth.go +++ b/backend/internal/server/middleware/jwt_auth.go @@ -1,7 +1,6 @@ package middleware import ( - "context" "errors" "strings" @@ -11,10 +10,13 @@ import ( "github.com/gin-gonic/gin" ) -// JWTAuth JWT认证中间件 -func JWTAuth(authService *service.AuthService, userRepo interface { - GetByID(ctx context.Context, id int64) (*model.User, error) -}) gin.HandlerFunc { +// NewJWTAuthMiddleware 创建 JWT 认证中间件 +func NewJWTAuthMiddleware(authService *service.AuthService, userService *service.UserService) JWTAuthMiddleware { + return JWTAuthMiddleware(jwtAuth(authService, userService)) +} + +// jwtAuth JWT认证中间件实现 +func jwtAuth(authService *service.AuthService, userService *service.UserService) gin.HandlerFunc { return func(c *gin.Context) { // 从Authorization header中提取token authHeader := c.GetHeader("Authorization") @@ -48,7 +50,7 @@ func JWTAuth(authService *service.AuthService, userRepo interface { } // 从数据库获取最新的用户信息 - user, err := userRepo.GetByID(c.Request.Context(), claims.UserID) + user, err := userService.GetByID(c.Request.Context(), claims.UserID) if err != nil { AbortWithError(c, 401, "USER_NOT_FOUND", "User not found") return diff --git a/backend/internal/middleware/logger.go b/backend/internal/server/middleware/logger.go similarity index 100% rename from backend/internal/middleware/logger.go rename to backend/internal/server/middleware/logger.go diff --git a/backend/internal/middleware/middleware.go b/backend/internal/server/middleware/middleware.go similarity index 100% rename from backend/internal/middleware/middleware.go rename to backend/internal/server/middleware/middleware.go diff --git a/backend/internal/middleware/recovery.go b/backend/internal/server/middleware/recovery.go similarity index 100% rename from backend/internal/middleware/recovery.go rename to backend/internal/server/middleware/recovery.go diff --git a/backend/internal/middleware/recovery_test.go b/backend/internal/server/middleware/recovery_test.go similarity index 100% rename from backend/internal/middleware/recovery_test.go rename to backend/internal/server/middleware/recovery_test.go diff --git a/backend/internal/server/middleware/wire.go b/backend/internal/server/middleware/wire.go new file mode 100644 index 00000000..3ed79f37 --- /dev/null +++ b/backend/internal/server/middleware/wire.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/google/wire" +) + +// JWTAuthMiddleware JWT 认证中间件类型 +type JWTAuthMiddleware gin.HandlerFunc + +// AdminAuthMiddleware 管理员认证中间件类型 +type AdminAuthMiddleware gin.HandlerFunc + +// ApiKeyAuthMiddleware API Key 认证中间件类型 +type ApiKeyAuthMiddleware gin.HandlerFunc + +// ProviderSet 中间件层的依赖注入 +var ProviderSet = wire.NewSet( + NewJWTAuthMiddleware, + NewAdminAuthMiddleware, + NewApiKeyAuthMiddleware, +) diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index 61fc4146..226fe99b 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -1,312 +1,54 @@ package server 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" + middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" + "github.com/Wei-Shaw/sub2api/internal/server/routes" "github.com/Wei-Shaw/sub2api/internal/web" - "net/http" "github.com/gin-gonic/gin" ) // SetupRouter 配置路由器中间件和路由 -func SetupRouter(r *gin.Engine, cfg *config.Config, handlers *handler.Handlers, services *service.Services, repos *repository.Repositories) *gin.Engine { +func SetupRouter( + r *gin.Engine, + handlers *handler.Handlers, + jwtAuth middleware2.JWTAuthMiddleware, + adminAuth middleware2.AdminAuthMiddleware, + apiKeyAuth middleware2.ApiKeyAuthMiddleware, +) *gin.Engine { // 应用中间件 - r.Use(middleware.Logger()) - r.Use(middleware.CORS()) - - // 注册路由 - registerRoutes(r, handlers, services, repos) + r.Use(middleware2.Logger()) + r.Use(middleware2.CORS()) // Serve embedded frontend if available if web.HasEmbeddedFrontend() { r.Use(web.ServeEmbeddedFrontend()) } + // 注册路由 + registerRoutes(r, handlers, jwtAuth, adminAuth, apiKeyAuth) + return r } // registerRoutes 注册所有 HTTP 路由 -func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, repos *repository.Repositories) { - // 健康检查 - r.GET("/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - }) - - // Claude Code 遥测日志(忽略,直接返回200) - r.POST("/api/event_logging/batch", func(c *gin.Context) { - c.Status(http.StatusOK) - }) - - // Setup status endpoint (always returns needs_setup: false in normal mode) - // This is used by the frontend to detect when the service has restarted after setup - r.GET("/setup/status", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "code": 0, - "data": gin.H{ - "needs_setup": false, - "step": "completed", - }, - }) - }) +func registerRoutes( + r *gin.Engine, + h *handler.Handlers, + jwtAuth middleware2.JWTAuthMiddleware, + adminAuth middleware2.AdminAuthMiddleware, + apiKeyAuth middleware2.ApiKeyAuthMiddleware, +) { + // 通用路由(健康检查、状态等) + routes.RegisterCommonRoutes(r) // API v1 v1 := r.Group("/api/v1") - { - // 公开接口 - auth := v1.Group("/auth") - { - auth.POST("/register", h.Auth.Register) - auth.POST("/login", h.Auth.Login) - auth.POST("/send-verify-code", h.Auth.SendVerifyCode) - } - // 公开设置(无需认证) - settings := v1.Group("/settings") - { - settings.GET("/public", h.Setting.GetPublicSettings) - } - - // 需要认证的接口 - authenticated := v1.Group("") - authenticated.Use(middleware.JWTAuth(s.Auth, repos.User)) - { - // 当前用户信息 - authenticated.GET("/auth/me", h.Auth.GetCurrentUser) - - // 用户接口 - user := authenticated.Group("/user") - { - user.GET("/profile", h.User.GetProfile) - user.PUT("/password", h.User.ChangePassword) - user.PUT("", h.User.UpdateProfile) - } - - // API Key管理 - keys := authenticated.Group("/keys") - { - keys.GET("", h.APIKey.List) - keys.GET("/:id", h.APIKey.GetByID) - keys.POST("", h.APIKey.Create) - keys.PUT("/:id", h.APIKey.Update) - keys.DELETE("/:id", h.APIKey.Delete) - } - - // 用户可用分组(非管理员接口) - groups := authenticated.Group("/groups") - { - groups.GET("/available", h.APIKey.GetAvailableGroups) - } - - // 使用记录 - usage := authenticated.Group("/usage") - { - usage.GET("", h.Usage.List) - usage.GET("/:id", h.Usage.GetByID) - usage.GET("/stats", h.Usage.Stats) - // User dashboard endpoints - usage.GET("/dashboard/stats", h.Usage.DashboardStats) - usage.GET("/dashboard/trend", h.Usage.DashboardTrend) - usage.GET("/dashboard/models", h.Usage.DashboardModels) - usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardApiKeysUsage) - } - - // 卡密兑换 - redeem := authenticated.Group("/redeem") - { - redeem.POST("", h.Redeem.Redeem) - redeem.GET("/history", h.Redeem.GetHistory) - } - - // 用户订阅 - subscriptions := authenticated.Group("/subscriptions") - { - subscriptions.GET("", h.Subscription.List) - subscriptions.GET("/active", h.Subscription.GetActive) - subscriptions.GET("/progress", h.Subscription.GetProgress) - subscriptions.GET("/summary", h.Subscription.GetSummary) - } - } - - // 管理员接口 - admin := v1.Group("/admin") - admin.Use(middleware.AdminAuth(s.Auth, repos.User, s.Setting)) - { - // 仪表盘 - dashboard := admin.Group("/dashboard") - { - dashboard.GET("/stats", h.Admin.Dashboard.GetStats) - dashboard.GET("/realtime", h.Admin.Dashboard.GetRealtimeMetrics) - dashboard.GET("/trend", h.Admin.Dashboard.GetUsageTrend) - dashboard.GET("/models", h.Admin.Dashboard.GetModelStats) - dashboard.GET("/api-keys-trend", h.Admin.Dashboard.GetApiKeyUsageTrend) - dashboard.GET("/users-trend", h.Admin.Dashboard.GetUserUsageTrend) - dashboard.POST("/users-usage", h.Admin.Dashboard.GetBatchUsersUsage) - dashboard.POST("/api-keys-usage", h.Admin.Dashboard.GetBatchApiKeysUsage) - } - - // 用户管理 - users := admin.Group("/users") - { - users.GET("", h.Admin.User.List) - users.GET("/:id", h.Admin.User.GetByID) - users.POST("", h.Admin.User.Create) - users.PUT("/:id", h.Admin.User.Update) - users.DELETE("/:id", h.Admin.User.Delete) - users.POST("/:id/balance", h.Admin.User.UpdateBalance) - users.GET("/:id/api-keys", h.Admin.User.GetUserAPIKeys) - users.GET("/:id/usage", h.Admin.User.GetUserUsage) - } - - // 分组管理 - groups := admin.Group("/groups") - { - groups.GET("", h.Admin.Group.List) - groups.GET("/all", h.Admin.Group.GetAll) - groups.GET("/:id", h.Admin.Group.GetByID) - groups.POST("", h.Admin.Group.Create) - groups.PUT("/:id", h.Admin.Group.Update) - groups.DELETE("/:id", h.Admin.Group.Delete) - groups.GET("/:id/stats", h.Admin.Group.GetStats) - groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys) - } - - // 账号管理 - accounts := admin.Group("/accounts") - { - accounts.GET("", h.Admin.Account.List) - accounts.GET("/:id", h.Admin.Account.GetByID) - accounts.POST("", h.Admin.Account.Create) - accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS) - accounts.PUT("/:id", h.Admin.Account.Update) - accounts.DELETE("/:id", h.Admin.Account.Delete) - accounts.POST("/:id/test", h.Admin.Account.Test) - accounts.POST("/:id/refresh", h.Admin.Account.Refresh) - accounts.GET("/:id/stats", h.Admin.Account.GetStats) - accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) - accounts.GET("/:id/usage", h.Admin.Account.GetUsage) - accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) - accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit) - accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable) - accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels) - accounts.POST("/batch", h.Admin.Account.BatchCreate) - accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials) - accounts.POST("/bulk-update", h.Admin.Account.BulkUpdate) - - // Claude OAuth routes - accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL) - accounts.POST("/generate-setup-token-url", h.Admin.OAuth.GenerateSetupTokenURL) - accounts.POST("/exchange-code", h.Admin.OAuth.ExchangeCode) - accounts.POST("/exchange-setup-token-code", h.Admin.OAuth.ExchangeSetupTokenCode) - accounts.POST("/cookie-auth", h.Admin.OAuth.CookieAuth) - accounts.POST("/setup-token-cookie-auth", h.Admin.OAuth.SetupTokenCookieAuth) - } - - // OpenAI OAuth routes - openai := admin.Group("/openai") - { - openai.POST("/generate-auth-url", h.Admin.OpenAIOAuth.GenerateAuthURL) - openai.POST("/exchange-code", h.Admin.OpenAIOAuth.ExchangeCode) - openai.POST("/refresh-token", h.Admin.OpenAIOAuth.RefreshToken) - openai.POST("/accounts/:id/refresh", h.Admin.OpenAIOAuth.RefreshAccountToken) - openai.POST("/create-from-oauth", h.Admin.OpenAIOAuth.CreateAccountFromOAuth) - } - - // 代理管理 - proxies := admin.Group("/proxies") - { - proxies.GET("", h.Admin.Proxy.List) - proxies.GET("/all", h.Admin.Proxy.GetAll) - proxies.GET("/:id", h.Admin.Proxy.GetByID) - proxies.POST("", h.Admin.Proxy.Create) - proxies.PUT("/:id", h.Admin.Proxy.Update) - proxies.DELETE("/:id", h.Admin.Proxy.Delete) - proxies.POST("/:id/test", h.Admin.Proxy.Test) - proxies.GET("/:id/stats", h.Admin.Proxy.GetStats) - proxies.GET("/:id/accounts", h.Admin.Proxy.GetProxyAccounts) - proxies.POST("/batch", h.Admin.Proxy.BatchCreate) - } - - // 卡密管理 - codes := admin.Group("/redeem-codes") - { - codes.GET("", h.Admin.Redeem.List) - codes.GET("/stats", h.Admin.Redeem.GetStats) - codes.GET("/export", h.Admin.Redeem.Export) - codes.GET("/:id", h.Admin.Redeem.GetByID) - codes.POST("/generate", h.Admin.Redeem.Generate) - codes.DELETE("/:id", h.Admin.Redeem.Delete) - codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete) - codes.POST("/:id/expire", h.Admin.Redeem.Expire) - } - - // 系统设置 - adminSettings := admin.Group("/settings") - { - adminSettings.GET("", h.Admin.Setting.GetSettings) - adminSettings.PUT("", h.Admin.Setting.UpdateSettings) - adminSettings.POST("/test-smtp", h.Admin.Setting.TestSmtpConnection) - adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) - // Admin API Key 管理 - adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminApiKey) - adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminApiKey) - adminSettings.DELETE("/admin-api-key", h.Admin.Setting.DeleteAdminApiKey) - } - - // 系统管理 - system := admin.Group("/system") - { - system.GET("/version", h.Admin.System.GetVersion) - system.GET("/check-updates", h.Admin.System.CheckUpdates) - system.POST("/update", h.Admin.System.PerformUpdate) - system.POST("/rollback", h.Admin.System.Rollback) - system.POST("/restart", h.Admin.System.RestartService) - } - - // 订阅管理 - subscriptions := admin.Group("/subscriptions") - { - subscriptions.GET("", h.Admin.Subscription.List) - subscriptions.GET("/:id", h.Admin.Subscription.GetByID) - subscriptions.GET("/:id/progress", h.Admin.Subscription.GetProgress) - subscriptions.POST("/assign", h.Admin.Subscription.Assign) - subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign) - subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend) - subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke) - } - - // 分组下的订阅列表 - admin.GET("/groups/:id/subscriptions", h.Admin.Subscription.ListByGroup) - - // 用户下的订阅列表 - admin.GET("/users/:id/subscriptions", h.Admin.Subscription.ListByUser) - - // 使用记录管理 - usage := admin.Group("/usage") - { - usage.GET("", h.Admin.Usage.List) - usage.GET("/stats", h.Admin.Usage.Stats) - usage.GET("/search-users", h.Admin.Usage.SearchUsers) - usage.GET("/search-api-keys", h.Admin.Usage.SearchApiKeys) - } - } - } - - // API网关(Claude API兼容) - gateway := r.Group("/v1") - gateway.Use(middleware.ApiKeyAuthWithSubscription(s.ApiKey, s.Subscription)) - { - gateway.POST("/messages", h.Gateway.Messages) - gateway.POST("/messages/count_tokens", h.Gateway.CountTokens) - gateway.GET("/models", h.Gateway.Models) - gateway.GET("/usage", h.Gateway.Usage) - // OpenAI Responses API - gateway.POST("/responses", h.OpenAIGateway.Responses) - } - - // OpenAI Responses API(不带v1前缀的别名) - r.POST("/responses", middleware.ApiKeyAuthWithSubscription(s.ApiKey, s.Subscription), h.OpenAIGateway.Responses) + // 注册各模块路由 + routes.RegisterAuthRoutes(v1, h, jwtAuth) + routes.RegisterUserRoutes(v1, h, jwtAuth) + routes.RegisterAdminRoutes(v1, h, adminAuth) + routes.RegisterGatewayRoutes(r, h, apiKeyAuth) } diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go new file mode 100644 index 00000000..06ef142a --- /dev/null +++ b/backend/internal/server/routes/admin.go @@ -0,0 +1,221 @@ +package routes + +import ( + "github.com/Wei-Shaw/sub2api/internal/handler" + "github.com/Wei-Shaw/sub2api/internal/server/middleware" + + "github.com/gin-gonic/gin" +) + +// RegisterAdminRoutes 注册管理员路由 +func RegisterAdminRoutes( + v1 *gin.RouterGroup, + h *handler.Handlers, + adminAuth middleware.AdminAuthMiddleware, +) { + admin := v1.Group("/admin") + admin.Use(gin.HandlerFunc(adminAuth)) + { + // 仪表盘 + registerDashboardRoutes(admin, h) + + // 用户管理 + registerUserManagementRoutes(admin, h) + + // 分组管理 + registerGroupRoutes(admin, h) + + // 账号管理 + registerAccountRoutes(admin, h) + + // OpenAI OAuth + registerOpenAIOAuthRoutes(admin, h) + + // 代理管理 + registerProxyRoutes(admin, h) + + // 卡密管理 + registerRedeemCodeRoutes(admin, h) + + // 系统设置 + registerSettingsRoutes(admin, h) + + // 系统管理 + registerSystemRoutes(admin, h) + + // 订阅管理 + registerSubscriptionRoutes(admin, h) + + // 使用记录管理 + registerUsageRoutes(admin, h) + } +} + +func registerDashboardRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + dashboard := admin.Group("/dashboard") + { + dashboard.GET("/stats", h.Admin.Dashboard.GetStats) + dashboard.GET("/realtime", h.Admin.Dashboard.GetRealtimeMetrics) + dashboard.GET("/trend", h.Admin.Dashboard.GetUsageTrend) + dashboard.GET("/models", h.Admin.Dashboard.GetModelStats) + dashboard.GET("/api-keys-trend", h.Admin.Dashboard.GetApiKeyUsageTrend) + dashboard.GET("/users-trend", h.Admin.Dashboard.GetUserUsageTrend) + dashboard.POST("/users-usage", h.Admin.Dashboard.GetBatchUsersUsage) + dashboard.POST("/api-keys-usage", h.Admin.Dashboard.GetBatchApiKeysUsage) + } +} + +func registerUserManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + users := admin.Group("/users") + { + users.GET("", h.Admin.User.List) + users.GET("/:id", h.Admin.User.GetByID) + users.POST("", h.Admin.User.Create) + users.PUT("/:id", h.Admin.User.Update) + users.DELETE("/:id", h.Admin.User.Delete) + users.POST("/:id/balance", h.Admin.User.UpdateBalance) + users.GET("/:id/api-keys", h.Admin.User.GetUserAPIKeys) + users.GET("/:id/usage", h.Admin.User.GetUserUsage) + } +} + +func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + groups := admin.Group("/groups") + { + groups.GET("", h.Admin.Group.List) + groups.GET("/all", h.Admin.Group.GetAll) + groups.GET("/:id", h.Admin.Group.GetByID) + groups.POST("", h.Admin.Group.Create) + groups.PUT("/:id", h.Admin.Group.Update) + groups.DELETE("/:id", h.Admin.Group.Delete) + groups.GET("/:id/stats", h.Admin.Group.GetStats) + groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys) + } +} + +func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + accounts := admin.Group("/accounts") + { + accounts.GET("", h.Admin.Account.List) + accounts.GET("/:id", h.Admin.Account.GetByID) + accounts.POST("", h.Admin.Account.Create) + accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS) + accounts.PUT("/:id", h.Admin.Account.Update) + accounts.DELETE("/:id", h.Admin.Account.Delete) + accounts.POST("/:id/test", h.Admin.Account.Test) + accounts.POST("/:id/refresh", h.Admin.Account.Refresh) + accounts.GET("/:id/stats", h.Admin.Account.GetStats) + accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) + accounts.GET("/:id/usage", h.Admin.Account.GetUsage) + accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) + accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit) + accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable) + accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels) + accounts.POST("/batch", h.Admin.Account.BatchCreate) + accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials) + accounts.POST("/bulk-update", h.Admin.Account.BulkUpdate) + + // Claude OAuth routes + accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL) + accounts.POST("/generate-setup-token-url", h.Admin.OAuth.GenerateSetupTokenURL) + accounts.POST("/exchange-code", h.Admin.OAuth.ExchangeCode) + accounts.POST("/exchange-setup-token-code", h.Admin.OAuth.ExchangeSetupTokenCode) + accounts.POST("/cookie-auth", h.Admin.OAuth.CookieAuth) + accounts.POST("/setup-token-cookie-auth", h.Admin.OAuth.SetupTokenCookieAuth) + } +} + +func registerOpenAIOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + openai := admin.Group("/openai") + { + openai.POST("/generate-auth-url", h.Admin.OpenAIOAuth.GenerateAuthURL) + openai.POST("/exchange-code", h.Admin.OpenAIOAuth.ExchangeCode) + openai.POST("/refresh-token", h.Admin.OpenAIOAuth.RefreshToken) + openai.POST("/accounts/:id/refresh", h.Admin.OpenAIOAuth.RefreshAccountToken) + openai.POST("/create-from-oauth", h.Admin.OpenAIOAuth.CreateAccountFromOAuth) + } +} + +func registerProxyRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + proxies := admin.Group("/proxies") + { + proxies.GET("", h.Admin.Proxy.List) + proxies.GET("/all", h.Admin.Proxy.GetAll) + proxies.GET("/:id", h.Admin.Proxy.GetByID) + proxies.POST("", h.Admin.Proxy.Create) + proxies.PUT("/:id", h.Admin.Proxy.Update) + proxies.DELETE("/:id", h.Admin.Proxy.Delete) + proxies.POST("/:id/test", h.Admin.Proxy.Test) + proxies.GET("/:id/stats", h.Admin.Proxy.GetStats) + proxies.GET("/:id/accounts", h.Admin.Proxy.GetProxyAccounts) + proxies.POST("/batch", h.Admin.Proxy.BatchCreate) + } +} + +func registerRedeemCodeRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + codes := admin.Group("/redeem-codes") + { + codes.GET("", h.Admin.Redeem.List) + codes.GET("/stats", h.Admin.Redeem.GetStats) + codes.GET("/export", h.Admin.Redeem.Export) + codes.GET("/:id", h.Admin.Redeem.GetByID) + codes.POST("/generate", h.Admin.Redeem.Generate) + codes.DELETE("/:id", h.Admin.Redeem.Delete) + codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete) + codes.POST("/:id/expire", h.Admin.Redeem.Expire) + } +} + +func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + adminSettings := admin.Group("/settings") + { + adminSettings.GET("", h.Admin.Setting.GetSettings) + adminSettings.PUT("", h.Admin.Setting.UpdateSettings) + adminSettings.POST("/test-smtp", h.Admin.Setting.TestSmtpConnection) + adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) + // Admin API Key 管理 + adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminApiKey) + adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminApiKey) + adminSettings.DELETE("/admin-api-key", h.Admin.Setting.DeleteAdminApiKey) + } +} + +func registerSystemRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + system := admin.Group("/system") + { + system.GET("/version", h.Admin.System.GetVersion) + system.GET("/check-updates", h.Admin.System.CheckUpdates) + system.POST("/update", h.Admin.System.PerformUpdate) + system.POST("/rollback", h.Admin.System.Rollback) + system.POST("/restart", h.Admin.System.RestartService) + } +} + +func registerSubscriptionRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + subscriptions := admin.Group("/subscriptions") + { + subscriptions.GET("", h.Admin.Subscription.List) + subscriptions.GET("/:id", h.Admin.Subscription.GetByID) + subscriptions.GET("/:id/progress", h.Admin.Subscription.GetProgress) + subscriptions.POST("/assign", h.Admin.Subscription.Assign) + subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign) + subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend) + subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke) + } + + // 分组下的订阅列表 + admin.GET("/groups/:id/subscriptions", h.Admin.Subscription.ListByGroup) + + // 用户下的订阅列表 + admin.GET("/users/:id/subscriptions", h.Admin.Subscription.ListByUser) +} + +func registerUsageRoutes(admin *gin.RouterGroup, h *handler.Handlers) { + usage := admin.Group("/usage") + { + usage.GET("", h.Admin.Usage.List) + usage.GET("/stats", h.Admin.Usage.Stats) + usage.GET("/search-users", h.Admin.Usage.SearchUsers) + usage.GET("/search-api-keys", h.Admin.Usage.SearchApiKeys) + } +} diff --git a/backend/internal/server/routes/auth.go b/backend/internal/server/routes/auth.go new file mode 100644 index 00000000..196d8bdb --- /dev/null +++ b/backend/internal/server/routes/auth.go @@ -0,0 +1,36 @@ +package routes + +import ( + "github.com/Wei-Shaw/sub2api/internal/handler" + "github.com/Wei-Shaw/sub2api/internal/server/middleware" + + "github.com/gin-gonic/gin" +) + +// RegisterAuthRoutes 注册认证相关路由 +func RegisterAuthRoutes( + v1 *gin.RouterGroup, + h *handler.Handlers, + jwtAuth middleware.JWTAuthMiddleware, +) { + // 公开接口 + auth := v1.Group("/auth") + { + auth.POST("/register", h.Auth.Register) + auth.POST("/login", h.Auth.Login) + auth.POST("/send-verify-code", h.Auth.SendVerifyCode) + } + + // 公开设置(无需认证) + settings := v1.Group("/settings") + { + settings.GET("/public", h.Setting.GetPublicSettings) + } + + // 需要认证的当前用户信息 + authenticated := v1.Group("") + authenticated.Use(gin.HandlerFunc(jwtAuth)) + { + authenticated.GET("/auth/me", h.Auth.GetCurrentUser) + } +} diff --git a/backend/internal/server/routes/common.go b/backend/internal/server/routes/common.go new file mode 100644 index 00000000..4989358d --- /dev/null +++ b/backend/internal/server/routes/common.go @@ -0,0 +1,32 @@ +package routes + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// RegisterCommonRoutes 注册通用路由(健康检查、状态等) +func RegisterCommonRoutes(r *gin.Engine) { + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // Claude Code 遥测日志(忽略,直接返回200) + r.POST("/api/event_logging/batch", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // Setup status endpoint (always returns needs_setup: false in normal mode) + // This is used by the frontend to detect when the service has restarted after setup + r.GET("/setup/status", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "data": gin.H{ + "needs_setup": false, + "step": "completed", + }, + }) + }) +} diff --git a/backend/internal/server/routes/gateway.go b/backend/internal/server/routes/gateway.go new file mode 100644 index 00000000..3f25b6d2 --- /dev/null +++ b/backend/internal/server/routes/gateway.go @@ -0,0 +1,30 @@ +package routes + +import ( + "github.com/Wei-Shaw/sub2api/internal/handler" + "github.com/Wei-Shaw/sub2api/internal/server/middleware" + + "github.com/gin-gonic/gin" +) + +// RegisterGatewayRoutes 注册 API 网关路由(Claude/OpenAI 兼容) +func RegisterGatewayRoutes( + r *gin.Engine, + h *handler.Handlers, + apiKeyAuth middleware.ApiKeyAuthMiddleware, +) { + // API网关(Claude API兼容) + gateway := r.Group("/v1") + gateway.Use(gin.HandlerFunc(apiKeyAuth)) + { + gateway.POST("/messages", h.Gateway.Messages) + gateway.POST("/messages/count_tokens", h.Gateway.CountTokens) + gateway.GET("/models", h.Gateway.Models) + gateway.GET("/usage", h.Gateway.Usage) + // OpenAI Responses API + gateway.POST("/responses", h.OpenAIGateway.Responses) + } + + // OpenAI Responses API(不带v1前缀的别名) + r.POST("/responses", gin.HandlerFunc(apiKeyAuth), h.OpenAIGateway.Responses) +} diff --git a/backend/internal/server/routes/user.go b/backend/internal/server/routes/user.go new file mode 100644 index 00000000..31a354fa --- /dev/null +++ b/backend/internal/server/routes/user.go @@ -0,0 +1,72 @@ +package routes + +import ( + "github.com/Wei-Shaw/sub2api/internal/handler" + "github.com/Wei-Shaw/sub2api/internal/server/middleware" + + "github.com/gin-gonic/gin" +) + +// RegisterUserRoutes 注册用户相关路由(需要认证) +func RegisterUserRoutes( + v1 *gin.RouterGroup, + h *handler.Handlers, + jwtAuth middleware.JWTAuthMiddleware, +) { + authenticated := v1.Group("") + authenticated.Use(gin.HandlerFunc(jwtAuth)) + { + // 用户接口 + user := authenticated.Group("/user") + { + user.GET("/profile", h.User.GetProfile) + user.PUT("/password", h.User.ChangePassword) + user.PUT("", h.User.UpdateProfile) + } + + // API Key管理 + keys := authenticated.Group("/keys") + { + keys.GET("", h.APIKey.List) + keys.GET("/:id", h.APIKey.GetByID) + keys.POST("", h.APIKey.Create) + keys.PUT("/:id", h.APIKey.Update) + keys.DELETE("/:id", h.APIKey.Delete) + } + + // 用户可用分组(非管理员接口) + groups := authenticated.Group("/groups") + { + groups.GET("/available", h.APIKey.GetAvailableGroups) + } + + // 使用记录 + usage := authenticated.Group("/usage") + { + usage.GET("", h.Usage.List) + usage.GET("/:id", h.Usage.GetByID) + usage.GET("/stats", h.Usage.Stats) + // User dashboard endpoints + usage.GET("/dashboard/stats", h.Usage.DashboardStats) + usage.GET("/dashboard/trend", h.Usage.DashboardTrend) + usage.GET("/dashboard/models", h.Usage.DashboardModels) + usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardApiKeysUsage) + } + + // 卡密兑换 + redeem := authenticated.Group("/redeem") + { + redeem.POST("", h.Redeem.Redeem) + redeem.GET("/history", h.Redeem.GetHistory) + } + + // 用户订阅 + subscriptions := authenticated.Group("/subscriptions") + { + subscriptions.GET("", h.Subscription.List) + subscriptions.GET("/active", h.Subscription.GetActive) + subscriptions.GET("/progress", h.Subscription.GetProgress) + subscriptions.GET("/summary", h.Subscription.GetSummary) + } + } +} diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go deleted file mode 100644 index 638aede7..00000000 --- a/backend/internal/service/service.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -// Services 服务集合容器 -type Services struct { - Auth *AuthService - User *UserService - ApiKey *ApiKeyService - Group *GroupService - Account *AccountService - Proxy *ProxyService - Redeem *RedeemService - Usage *UsageService - Pricing *PricingService - Billing *BillingService - BillingCache *BillingCacheService - Admin AdminService - Gateway *GatewayService - OpenAIGateway *OpenAIGatewayService - OAuth *OAuthService - OpenAIOAuth *OpenAIOAuthService - RateLimit *RateLimitService - AccountUsage *AccountUsageService - AccountTest *AccountTestService - Setting *SettingService - Email *EmailService - EmailQueue *EmailQueueService - Turnstile *TurnstileService - Subscription *SubscriptionService - Concurrency *ConcurrencyService - Identity *IdentityService - Update *UpdateService - TokenRefresh *TokenRefreshService -} diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 5c314382..3830bc67 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -60,6 +60,15 @@ func NewUserService(userRepo UserRepository) *UserService { } } +// GetFirstAdmin 获取首个管理员用户(用于 Admin API Key 认证) +func (s *UserService) GetFirstAdmin(ctx context.Context) (*model.User, error) { + admin, err := s.userRepo.GetFirstAdmin(ctx) + if err != nil { + return nil, fmt.Errorf("get first admin: %w", err) + } + return admin, nil +} + // GetProfile 获取用户资料 func (s *UserService) GetProfile(ctx context.Context, userID int64) (*model.User, error) { user, err := s.userRepo.GetByID(ctx, userID) diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 050563a6..02ef2392 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -76,7 +76,4 @@ var ProviderSet = wire.NewSet( NewCRSSyncService, ProvideUpdateService, ProvideTokenRefreshService, - - // Provide the Services container struct - wire.Struct(new(Services), "*"), )