Commit f51ad2e1 authored by Forest's avatar Forest
Browse files

refactor: 删除 ports 目录

parent f57f12c6
...@@ -8,10 +8,49 @@ import ( ...@@ -8,10 +8,49 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
type UsageLogRepository interface {
Create(ctx context.Context, log *model.UsageLog) error
GetByID(ctx context.Context, id int64) (*model.UsageLog, error)
Delete(ctx context.Context, id int64) error
ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByApiKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error)
GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error)
// Admin dashboard stats
GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error)
GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]usagestats.TrendDataPoint, error)
GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) ([]usagestats.ModelStat, error)
GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.ApiKeyUsageTrendPoint, error)
GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error)
GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*usagestats.BatchUserUsageStats, error)
GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchApiKeyUsageStats, error)
// User dashboard stats
GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error)
GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error)
GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error)
// Admin usage listing/stats
ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]model.UsageLog, *pagination.PaginationResult, error)
GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error)
// Account stats
GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error)
}
// usageCache 用于缓存usage数据 // usageCache 用于缓存usage数据
type usageCache struct { type usageCache struct {
data *UsageInfo data *UsageInfo
...@@ -69,13 +108,13 @@ type ClaudeUsageFetcher interface { ...@@ -69,13 +108,13 @@ type ClaudeUsageFetcher interface {
// AccountUsageService 账号使用量查询服务 // AccountUsageService 账号使用量查询服务
type AccountUsageService struct { type AccountUsageService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
usageFetcher ClaudeUsageFetcher usageFetcher ClaudeUsageFetcher
} }
// NewAccountUsageService 创建AccountUsageService实例 // NewAccountUsageService 创建AccountUsageService实例
func NewAccountUsageService(accountRepo ports.AccountRepository, usageLogRepo ports.UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService { func NewAccountUsageService(accountRepo AccountRepository, usageLogRepo UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService {
return &AccountUsageService{ return &AccountUsageService{
accountRepo: accountRepo, accountRepo: accountRepo,
usageLogRepo: usageLogRepo, usageLogRepo: usageLogRepo,
......
...@@ -9,8 +9,6 @@ import ( ...@@ -9,8 +9,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
...@@ -221,24 +219,24 @@ type ProxyExitInfoProber interface { ...@@ -221,24 +219,24 @@ type ProxyExitInfoProber interface {
// adminServiceImpl implements AdminService // adminServiceImpl implements AdminService
type adminServiceImpl struct { type adminServiceImpl struct {
userRepo ports.UserRepository userRepo UserRepository
groupRepo ports.GroupRepository groupRepo GroupRepository
accountRepo ports.AccountRepository accountRepo AccountRepository
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
apiKeyRepo ports.ApiKeyRepository apiKeyRepo ApiKeyRepository
redeemCodeRepo ports.RedeemCodeRepository redeemCodeRepo RedeemCodeRepository
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
proxyProber ProxyExitInfoProber proxyProber ProxyExitInfoProber
} }
// NewAdminService creates a new AdminService // NewAdminService creates a new AdminService
func NewAdminService( func NewAdminService(
userRepo ports.UserRepository, userRepo UserRepository,
groupRepo ports.GroupRepository, groupRepo GroupRepository,
accountRepo ports.AccountRepository, accountRepo AccountRepository,
proxyRepo ports.ProxyRepository, proxyRepo ProxyRepository,
apiKeyRepo ports.ApiKeyRepository, apiKeyRepo ApiKeyRepository,
redeemCodeRepo ports.RedeemCodeRepository, redeemCodeRepo RedeemCodeRepository,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
proxyProber ProxyExitInfoProber, proxyProber ProxyExitInfoProber,
) AdminService { ) AdminService {
...@@ -734,7 +732,7 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp ...@@ -734,7 +732,7 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
} }
// Prepare bulk updates for columns and JSONB fields. // Prepare bulk updates for columns and JSONB fields.
repoUpdates := ports.AccountBulkUpdate{ repoUpdates := AccountBulkUpdate{
Credentials: input.Credentials, Credentials: input.Credentials,
Extra: input.Extra, Extra: input.Extra,
} }
......
...@@ -6,13 +6,12 @@ import ( ...@@ -6,13 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gorm.io/gorm" "gorm.io/gorm"
) )
...@@ -30,6 +29,32 @@ const ( ...@@ -30,6 +29,32 @@ const (
apiKeyMaxErrorsPerHour = 20 apiKeyMaxErrorsPerHour = 20
) )
type ApiKeyRepository interface {
Create(ctx context.Context, key *model.ApiKey) error
GetByID(ctx context.Context, id int64) (*model.ApiKey, error)
GetByKey(ctx context.Context, key string) (*model.ApiKey, error)
Update(ctx context.Context, key *model.ApiKey) error
Delete(ctx context.Context, id int64) error
ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
CountByUserID(ctx context.Context, userID int64) (int64, error)
ExistsByKey(ctx context.Context, key string) (bool, error)
ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error)
ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error)
CountByGroupID(ctx context.Context, groupID int64) (int64, error)
}
// ApiKeyCache defines cache operations for API key service
type ApiKeyCache interface {
GetCreateAttemptCount(ctx context.Context, userID int64) (int, error)
IncrementCreateAttemptCount(ctx context.Context, userID int64) error
DeleteCreateAttemptCount(ctx context.Context, userID int64) error
IncrementDailyUsage(ctx context.Context, apiKey string) error
SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error
}
// CreateApiKeyRequest 创建API Key请求 // CreateApiKeyRequest 创建API Key请求
type CreateApiKeyRequest struct { type CreateApiKeyRequest struct {
Name string `json:"name"` Name string `json:"name"`
...@@ -46,21 +71,21 @@ type UpdateApiKeyRequest struct { ...@@ -46,21 +71,21 @@ type UpdateApiKeyRequest struct {
// ApiKeyService API Key服务 // ApiKeyService API Key服务
type ApiKeyService struct { type ApiKeyService struct {
apiKeyRepo ports.ApiKeyRepository apiKeyRepo ApiKeyRepository
userRepo ports.UserRepository userRepo UserRepository
groupRepo ports.GroupRepository groupRepo GroupRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.ApiKeyCache cache ApiKeyCache
cfg *config.Config cfg *config.Config
} }
// NewApiKeyService 创建API Key服务实例 // NewApiKeyService 创建API Key服务实例
func NewApiKeyService( func NewApiKeyService(
apiKeyRepo ports.ApiKeyRepository, apiKeyRepo ApiKeyRepository,
userRepo ports.UserRepository, userRepo UserRepository,
groupRepo ports.GroupRepository, groupRepo GroupRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.ApiKeyCache, cache ApiKeyCache,
cfg *config.Config, cfg *config.Config,
) *ApiKeyService { ) *ApiKeyService {
return &ApiKeyService{ return &ApiKeyService{
......
...@@ -4,12 +4,12 @@ import ( ...@@ -4,12 +4,12 @@ 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"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -36,7 +36,7 @@ type JWTClaims struct { ...@@ -36,7 +36,7 @@ type JWTClaims struct {
// AuthService 认证服务 // AuthService 认证服务
type AuthService struct { type AuthService struct {
userRepo ports.UserRepository userRepo UserRepository
cfg *config.Config cfg *config.Config
settingService *SettingService settingService *SettingService
emailService *EmailService emailService *EmailService
...@@ -46,7 +46,7 @@ type AuthService struct { ...@@ -46,7 +46,7 @@ type AuthService struct {
// NewAuthService 创建认证服务实例 // NewAuthService 创建认证服务实例
func NewAuthService( func NewAuthService(
userRepo ports.UserRepository, userRepo UserRepository,
cfg *config.Config, cfg *config.Config,
settingService *SettingService, settingService *SettingService,
emailService *EmailService, emailService *EmailService,
......
package service
import (
"time"
)
// SubscriptionCacheData represents cached subscription data
type SubscriptionCacheData struct {
Status string
ExpiresAt time.Time
DailyUsage float64
WeeklyUsage float64
MonthlyUsage float64
Version int64
}
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// 错误定义 // 错误定义
...@@ -31,13 +30,13 @@ type subscriptionCacheData struct { ...@@ -31,13 +30,13 @@ type subscriptionCacheData struct {
// BillingCacheService 计费缓存服务 // BillingCacheService 计费缓存服务
// 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查 // 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查
type BillingCacheService struct { type BillingCacheService struct {
cache ports.BillingCache cache BillingCache
userRepo ports.UserRepository userRepo UserRepository
subRepo ports.UserSubscriptionRepository subRepo UserSubscriptionRepository
} }
// NewBillingCacheService 创建计费缓存服务 // NewBillingCacheService 创建计费缓存服务
func NewBillingCacheService(cache ports.BillingCache, userRepo ports.UserRepository, subRepo ports.UserSubscriptionRepository) *BillingCacheService { func NewBillingCacheService(cache BillingCache, userRepo UserRepository, subRepo UserSubscriptionRepository) *BillingCacheService {
return &BillingCacheService{ return &BillingCacheService{
cache: cache, cache: cache,
userRepo: userRepo, userRepo: userRepo,
...@@ -149,7 +148,7 @@ func (s *BillingCacheService) GetSubscriptionStatus(ctx context.Context, userID, ...@@ -149,7 +148,7 @@ func (s *BillingCacheService) GetSubscriptionStatus(ctx context.Context, userID,
return data, nil return data, nil
} }
func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCacheData) *subscriptionCacheData { func (s *BillingCacheService) convertFromPortsData(data *SubscriptionCacheData) *subscriptionCacheData {
return &subscriptionCacheData{ return &subscriptionCacheData{
Status: data.Status, Status: data.Status,
ExpiresAt: data.ExpiresAt, ExpiresAt: data.ExpiresAt,
...@@ -160,8 +159,8 @@ func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCache ...@@ -160,8 +159,8 @@ func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCache
} }
} }
func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *ports.SubscriptionCacheData { func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *SubscriptionCacheData {
return &ports.SubscriptionCacheData{ return &SubscriptionCacheData{
Status: data.Status, Status: data.Status,
ExpiresAt: data.ExpiresAt, ExpiresAt: data.ExpiresAt,
DailyUsage: data.DailyUsage, DailyUsage: data.DailyUsage,
......
package service package service
import ( import (
"context"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/config"
"log" "log"
"strings" "strings"
"github.com/Wei-Shaw/sub2api/internal/config"
) )
// BillingCache defines cache operations for billing service
type BillingCache interface {
// Balance operations
GetUserBalance(ctx context.Context, userID int64) (float64, error)
SetUserBalance(ctx context.Context, userID int64, balance float64) error
DeductUserBalance(ctx context.Context, userID int64, amount float64) error
InvalidateUserBalance(ctx context.Context, userID int64) error
// Subscription operations
GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error)
SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error
UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error
InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error
}
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致) // ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
type ModelPricing struct { type ModelPricing struct {
InputPricePerToken float64 // 每token输入价格 (USD) InputPricePerToken float64 // 每token输入价格 (USD)
......
...@@ -7,10 +7,28 @@ import ( ...@@ -7,10 +7,28 @@ import (
"fmt" "fmt"
"log" "log"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// ConcurrencyCache defines cache operations for concurrency service
// Uses independent keys per request slot with native Redis TTL for automatic cleanup
type ConcurrencyCache interface {
// Account slot management - each slot is a separate key with independent TTL
// Key format: concurrency:account:{accountID}:{requestID}
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)
// User slot management - each slot is a separate key with independent TTL
// 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)
// Wait queue - uses counter with TTL set only on creation
IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error)
DecrementWaitCount(ctx context.Context, userID int64) error
}
// generateRequestID generates a unique request ID for concurrency slot tracking // generateRequestID generates a unique request ID for concurrency slot tracking
// Uses 8 random bytes (16 hex chars) for uniqueness // Uses 8 random bytes (16 hex chars) for uniqueness
func generateRequestID() string { func generateRequestID() string {
...@@ -29,11 +47,11 @@ const ( ...@@ -29,11 +47,11 @@ const (
// ConcurrencyService manages concurrent request limiting for accounts and users // ConcurrencyService manages concurrent request limiting for accounts and users
type ConcurrencyService struct { type ConcurrencyService struct {
cache ports.ConcurrencyCache cache ConcurrencyCache
} }
// NewConcurrencyService creates a new ConcurrencyService // NewConcurrencyService creates a new ConcurrencyService
func NewConcurrencyService(cache ports.ConcurrencyCache) *ConcurrencyService { func NewConcurrencyService(cache ConcurrencyCache) *ConcurrencyService {
return &ConcurrencyService{cache: cache} return &ConcurrencyService{cache: cache}
} }
......
...@@ -13,19 +13,18 @@ import ( ...@@ -13,19 +13,18 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
type CRSSyncService struct { type CRSSyncService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthService *OAuthService oauthService *OAuthService
openaiOAuthService *OpenAIOAuthService openaiOAuthService *OpenAIOAuthService
} }
func NewCRSSyncService( func NewCRSSyncService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
proxyRepo ports.ProxyRepository, proxyRepo ProxyRepository,
oauthService *OAuthService, oauthService *OAuthService,
openaiOAuthService *OpenAIOAuthService, openaiOAuthService *OpenAIOAuthService,
) *CRSSyncService { ) *CRSSyncService {
......
...@@ -6,15 +6,14 @@ import ( ...@@ -6,15 +6,14 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// DashboardService provides aggregated statistics for admin dashboard. // DashboardService provides aggregated statistics for admin dashboard.
type DashboardService struct { type DashboardService struct {
usageRepo ports.UsageLogRepository usageRepo UsageLogRepository
} }
func NewDashboardService(usageRepo ports.UsageLogRepository) *DashboardService { func NewDashboardService(usageRepo UsageLogRepository) *DashboardService {
return &DashboardService{ return &DashboardService{
usageRepo: usageRepo, usageRepo: usageRepo,
} }
......
...@@ -6,12 +6,12 @@ import ( ...@@ -6,12 +6,12 @@ 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"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model"
) )
var ( var (
...@@ -21,6 +21,20 @@ var ( ...@@ -21,6 +21,20 @@ var (
ErrVerifyCodeMaxAttempts = errors.New("too many failed attempts, please request a new code") ErrVerifyCodeMaxAttempts = errors.New("too many failed attempts, please request a new code")
) )
// EmailCache defines cache operations for email service
type EmailCache interface {
GetVerificationCode(ctx context.Context, email string) (*VerificationCodeData, error)
SetVerificationCode(ctx context.Context, email string, data *VerificationCodeData, ttl time.Duration) error
DeleteVerificationCode(ctx context.Context, email string) error
}
// VerificationCodeData represents verification code data
type VerificationCodeData struct {
Code string
Attempts int
CreatedAt time.Time
}
const ( const (
verifyCodeTTL = 15 * time.Minute verifyCodeTTL = 15 * time.Minute
verifyCodeCooldown = 1 * time.Minute verifyCodeCooldown = 1 * time.Minute
...@@ -40,12 +54,12 @@ type SmtpConfig struct { ...@@ -40,12 +54,12 @@ type SmtpConfig struct {
// EmailService 邮件服务 // EmailService 邮件服务
type EmailService struct { type EmailService struct {
settingRepo ports.SettingRepository settingRepo SettingRepository
cache ports.EmailCache cache EmailCache
} }
// NewEmailService 创建邮件服务实例 // NewEmailService 创建邮件服务实例
func NewEmailService(settingRepo ports.SettingRepository, cache ports.EmailCache) *EmailService { func NewEmailService(settingRepo SettingRepository, cache EmailCache) *EmailService {
return &EmailService{ return &EmailService{
settingRepo: settingRepo, settingRepo: settingRepo,
cache: cache, cache: cache,
...@@ -205,7 +219,7 @@ func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName strin ...@@ -205,7 +219,7 @@ func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName strin
} }
// 保存验证码到 Redis // 保存验证码到 Redis
data := &ports.VerificationCodeData{ data := &VerificationCodeData{
Code: code, Code: code,
Attempts: 0, Attempts: 0,
CreatedAt: time.Now(), CreatedAt: time.Now(),
......
...@@ -19,7 +19,6 @@ import ( ...@@ -19,7 +19,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
...@@ -54,6 +53,13 @@ var allowedHeaders = map[string]bool{ ...@@ -54,6 +53,13 @@ var allowedHeaders = map[string]bool{
"content-type": true, "content-type": true,
} }
// GatewayCache defines cache operations for gateway service
type GatewayCache interface {
GetSessionAccountID(ctx context.Context, sessionHash string) (int64, error)
SetSessionAccountID(ctx context.Context, sessionHash string, accountID int64, ttl time.Duration) error
RefreshSessionTTL(ctx context.Context, sessionHash string, ttl time.Duration) error
}
// ClaudeUsage 表示Claude API返回的usage信息 // ClaudeUsage 表示Claude API返回的usage信息
type ClaudeUsage struct { type ClaudeUsage struct {
InputTokens int `json:"input_tokens"` InputTokens int `json:"input_tokens"`
...@@ -74,32 +80,32 @@ type ForwardResult struct { ...@@ -74,32 +80,32 @@ type ForwardResult struct {
// GatewayService handles API gateway operations // GatewayService handles API gateway operations
type GatewayService struct { type GatewayService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
userRepo ports.UserRepository userRepo UserRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.GatewayCache cache GatewayCache
cfg *config.Config cfg *config.Config
billingService *BillingService billingService *BillingService
rateLimitService *RateLimitService rateLimitService *RateLimitService
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
identityService *IdentityService identityService *IdentityService
httpUpstream ports.HTTPUpstream httpUpstream HTTPUpstream
} }
// NewGatewayService creates a new GatewayService // NewGatewayService creates a new GatewayService
func NewGatewayService( func NewGatewayService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
usageLogRepo ports.UsageLogRepository, usageLogRepo UsageLogRepository,
userRepo ports.UserRepository, userRepo UserRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.GatewayCache, cache GatewayCache,
cfg *config.Config, cfg *config.Config,
billingService *BillingService, billingService *BillingService,
rateLimitService *RateLimitService, rateLimitService *RateLimitService,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
identityService *IdentityService, identityService *IdentityService,
httpUpstream ports.HTTPUpstream, httpUpstream HTTPUpstream,
) *GatewayService { ) *GatewayService {
return &GatewayService{ return &GatewayService{
accountRepo: accountRepo, accountRepo: accountRepo,
...@@ -507,7 +513,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex ...@@ -507,7 +513,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
} }
// OAuth账号:应用统一指纹 // OAuth账号:应用统一指纹
var fingerprint *ports.Fingerprint var fingerprint *Fingerprint
if account.IsOAuth() && s.identityService != nil { if account.IsOAuth() && s.identityService != nil {
// 1. 获取或创建指纹(包含随机生成的ClientID) // 1. 获取或创建指纹(包含随机生成的ClientID)
fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header)
......
...@@ -4,10 +4,9 @@ import ( ...@@ -4,10 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
...@@ -16,6 +15,24 @@ var ( ...@@ -16,6 +15,24 @@ var (
ErrGroupExists = errors.New("group name already exists") ErrGroupExists = errors.New("group name already exists")
) )
type GroupRepository interface {
Create(ctx context.Context, group *model.Group) error
GetByID(ctx context.Context, id int64) (*model.Group, error)
Update(ctx context.Context, group *model.Group) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, params pagination.PaginationParams) ([]model.Group, *pagination.PaginationResult, error)
ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]model.Group, *pagination.PaginationResult, error)
ListActive(ctx context.Context) ([]model.Group, error)
ListActiveByPlatform(ctx context.Context, platform string) ([]model.Group, error)
ExistsByName(ctx context.Context, name string) (bool, error)
GetAccountCount(ctx context.Context, groupID int64) (int64, error)
DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error)
DB() *gorm.DB
}
// CreateGroupRequest 创建分组请求 // CreateGroupRequest 创建分组请求
type CreateGroupRequest struct { type CreateGroupRequest struct {
Name string `json:"name"` Name string `json:"name"`
...@@ -35,11 +52,11 @@ type UpdateGroupRequest struct { ...@@ -35,11 +52,11 @@ type UpdateGroupRequest struct {
// GroupService 分组管理服务 // GroupService 分组管理服务
type GroupService struct { type GroupService struct {
groupRepo ports.GroupRepository groupRepo GroupRepository
} }
// NewGroupService 创建分组服务实例 // NewGroupService 创建分组服务实例
func NewGroupService(groupRepo ports.GroupRepository) *GroupService { func NewGroupService(groupRepo GroupRepository) *GroupService {
return &GroupService{ return &GroupService{
groupRepo: groupRepo, groupRepo: groupRepo,
} }
......
...@@ -7,7 +7,6 @@ import ( ...@@ -7,7 +7,6 @@ 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"
...@@ -24,7 +23,7 @@ var ( ...@@ -24,7 +23,7 @@ var (
) )
// 默认指纹值(当客户端未提供时使用) // 默认指纹值(当客户端未提供时使用)
var defaultFingerprint = ports.Fingerprint{ var defaultFingerprint = Fingerprint{
UserAgent: "claude-cli/2.0.62 (external, cli)", UserAgent: "claude-cli/2.0.62 (external, cli)",
StainlessLang: "js", StainlessLang: "js",
StainlessPackageVersion: "0.52.0", StainlessPackageVersion: "0.52.0",
...@@ -34,20 +33,38 @@ var defaultFingerprint = ports.Fingerprint{ ...@@ -34,20 +33,38 @@ var defaultFingerprint = ports.Fingerprint{
StainlessRuntimeVersion: "v22.14.0", StainlessRuntimeVersion: "v22.14.0",
} }
// Fingerprint represents account fingerprint data
type Fingerprint struct {
ClientID string
UserAgent string
StainlessLang string
StainlessPackageVersion string
StainlessOS string
StainlessArch string
StainlessRuntime string
StainlessRuntimeVersion string
}
// IdentityCache defines cache operations for identity service
type IdentityCache interface {
GetFingerprint(ctx context.Context, accountID int64) (*Fingerprint, error)
SetFingerprint(ctx context.Context, accountID int64, fp *Fingerprint) error
}
// IdentityService 管理OAuth账号的请求身份指纹 // IdentityService 管理OAuth账号的请求身份指纹
type IdentityService struct { type IdentityService struct {
cache ports.IdentityCache cache IdentityCache
} }
// NewIdentityService 创建新的IdentityService // NewIdentityService 创建新的IdentityService
func NewIdentityService(cache ports.IdentityCache) *IdentityService { func NewIdentityService(cache IdentityCache) *IdentityService {
return &IdentityService{cache: cache} return &IdentityService{cache: cache}
} }
// GetOrCreateFingerprint 获取或创建账号的指纹 // GetOrCreateFingerprint 获取或创建账号的指纹
// 如果缓存存在,检测user-agent版本,新版本则更新 // 如果缓存存在,检测user-agent版本,新版本则更新
// 如果缓存不存在,生成随机ClientID并从请求头创建指纹,然后缓存 // 如果缓存不存在,生成随机ClientID并从请求头创建指纹,然后缓存
func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*ports.Fingerprint, error) { func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*Fingerprint, error) {
// 尝试从缓存获取指纹 // 尝试从缓存获取指纹
cached, err := s.cache.GetFingerprint(ctx, accountID) cached, err := s.cache.GetFingerprint(ctx, accountID)
if err == nil && cached != nil { if err == nil && cached != nil {
...@@ -79,8 +96,8 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID ...@@ -79,8 +96,8 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
} }
// createFingerprintFromHeaders 从请求头创建指纹 // createFingerprintFromHeaders 从请求头创建指纹
func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *ports.Fingerprint { func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fingerprint {
fp := &ports.Fingerprint{} fp := &Fingerprint{}
// 获取User-Agent // 获取User-Agent
if ua := headers.Get("User-Agent"); ua != "" { if ua := headers.Get("User-Agent"); ua != "" {
...@@ -109,7 +126,7 @@ func getHeaderOrDefault(headers http.Header, key, defaultValue string) string { ...@@ -109,7 +126,7 @@ func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
} }
// ApplyFingerprint 将指纹应用到请求头(覆盖原有的x-stainless-*头) // ApplyFingerprint 将指纹应用到请求头(覆盖原有的x-stainless-*头)
func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *ports.Fingerprint) { func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) {
if fp == nil { if fp == nil {
return return
} }
......
...@@ -8,9 +8,15 @@ import ( ...@@ -8,9 +8,15 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth" "github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
) )
// OpenAIOAuthClient interface for OpenAI OAuth operations
type OpenAIOAuthClient interface {
ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL string) (*openai.TokenResponse, error)
RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*openai.TokenResponse, error)
}
// ClaudeOAuthClient handles HTTP requests for Claude OAuth flows // ClaudeOAuthClient handles HTTP requests for Claude OAuth flows
type ClaudeOAuthClient interface { type ClaudeOAuthClient interface {
GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error) GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error)
...@@ -22,12 +28,12 @@ type ClaudeOAuthClient interface { ...@@ -22,12 +28,12 @@ type ClaudeOAuthClient interface {
// OAuthService handles OAuth authentication flows // OAuthService handles OAuth authentication flows
type OAuthService struct { type OAuthService struct {
sessionStore *oauth.SessionStore sessionStore *oauth.SessionStore
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthClient ClaudeOAuthClient oauthClient ClaudeOAuthClient
} }
// NewOAuthService creates a new OAuth service // NewOAuthService creates a new OAuth service
func NewOAuthService(proxyRepo ports.ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService { func NewOAuthService(proxyRepo ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService {
return &OAuthService{ return &OAuthService{
sessionStore: oauth.NewSessionStore(), sessionStore: oauth.NewSessionStore(),
proxyRepo: proxyRepo, proxyRepo: proxyRepo,
......
...@@ -17,8 +17,6 @@ import ( ...@@ -17,8 +17,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -71,30 +69,30 @@ type OpenAIForwardResult struct { ...@@ -71,30 +69,30 @@ type OpenAIForwardResult struct {
// OpenAIGatewayService handles OpenAI API gateway operations // OpenAIGatewayService handles OpenAI API gateway operations
type OpenAIGatewayService struct { type OpenAIGatewayService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
userRepo ports.UserRepository userRepo UserRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.GatewayCache cache GatewayCache
cfg *config.Config cfg *config.Config
billingService *BillingService billingService *BillingService
rateLimitService *RateLimitService rateLimitService *RateLimitService
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
httpUpstream ports.HTTPUpstream httpUpstream HTTPUpstream
} }
// NewOpenAIGatewayService creates a new OpenAIGatewayService // NewOpenAIGatewayService creates a new OpenAIGatewayService
func NewOpenAIGatewayService( func NewOpenAIGatewayService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
usageLogRepo ports.UsageLogRepository, usageLogRepo UsageLogRepository,
userRepo ports.UserRepository, userRepo UserRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.GatewayCache, cache GatewayCache,
cfg *config.Config, cfg *config.Config,
billingService *BillingService, billingService *BillingService,
rateLimitService *RateLimitService, rateLimitService *RateLimitService,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
httpUpstream ports.HTTPUpstream, httpUpstream HTTPUpstream,
) *OpenAIGatewayService { ) *OpenAIGatewayService {
return &OpenAIGatewayService{ return &OpenAIGatewayService{
accountRepo: accountRepo, accountRepo: accountRepo,
......
...@@ -7,18 +7,17 @@ import ( ...@@ -7,18 +7,17 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// OpenAIOAuthService handles OpenAI OAuth authentication flows // OpenAIOAuthService handles OpenAI OAuth authentication flows
type OpenAIOAuthService struct { type OpenAIOAuthService struct {
sessionStore *openai.SessionStore sessionStore *openai.SessionStore
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthClient ports.OpenAIOAuthClient oauthClient OpenAIOAuthClient
} }
// NewOpenAIOAuthService creates a new OpenAI OAuth service // NewOpenAIOAuthService creates a new OpenAI OAuth service
func NewOpenAIOAuthService(proxyRepo ports.ProxyRepository, oauthClient ports.OpenAIOAuthClient) *OpenAIOAuthService { func NewOpenAIOAuthService(proxyRepo ProxyRepository, oauthClient OpenAIOAuthClient) *OpenAIOAuthService {
return &OpenAIOAuthService{ return &OpenAIOAuthService{
sessionStore: openai.NewSessionStore(), sessionStore: openai.NewSessionStore(),
proxyRepo: proxyRepo, proxyRepo: proxyRepo,
......
package ports
import (
"context"
"time"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
)
type AccountRepository interface {
Create(ctx context.Context, account *model.Account) error
GetByID(ctx context.Context, id int64) (*model.Account, error)
// GetByCRSAccountID finds an account previously synced from CRS.
// Returns (nil, nil) if not found.
GetByCRSAccountID(ctx context.Context, crsAccountID string) (*model.Account, error)
Update(ctx context.Context, account *model.Account) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, params pagination.PaginationParams) ([]model.Account, *pagination.PaginationResult, error)
ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string) ([]model.Account, *pagination.PaginationResult, error)
ListByGroup(ctx context.Context, groupID int64) ([]model.Account, error)
ListActive(ctx context.Context) ([]model.Account, error)
ListByPlatform(ctx context.Context, platform string) ([]model.Account, error)
UpdateLastUsed(ctx context.Context, id int64) error
SetError(ctx context.Context, id int64, errorMsg string) error
SetSchedulable(ctx context.Context, id int64, schedulable bool) error
BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error
ListSchedulable(ctx context.Context) ([]model.Account, error)
ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]model.Account, error)
ListSchedulableByPlatform(ctx context.Context, platform string) ([]model.Account, error)
ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]model.Account, error)
SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error
SetOverloaded(ctx context.Context, id int64, until time.Time) error
ClearRateLimit(ctx context.Context, id int64) error
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
}
// AccountBulkUpdate describes the fields that can be updated in a bulk operation.
// Nil pointers mean "do not change".
type AccountBulkUpdate struct {
Name *string
ProxyID *int64
Concurrency *int
Priority *int
Status *string
Credentials map[string]any
Extra map[string]any
}
package ports
import (
"context"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
)
type ApiKeyRepository interface {
Create(ctx context.Context, key *model.ApiKey) error
GetByID(ctx context.Context, id int64) (*model.ApiKey, error)
GetByKey(ctx context.Context, key string) (*model.ApiKey, error)
Update(ctx context.Context, key *model.ApiKey) error
Delete(ctx context.Context, id int64) error
ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
CountByUserID(ctx context.Context, userID int64) (int64, error)
ExistsByKey(ctx context.Context, key string) (bool, error)
ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error)
ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error)
CountByGroupID(ctx context.Context, groupID int64) (int64, error)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment