diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 424dcfd5..0b9c6bf8 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -3,10 +3,11 @@ package repository import ( "context" "errors" + "time" + "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "time" + "github.com/Wei-Shaw/sub2api/internal/service" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -357,7 +358,7 @@ func (r *AccountRepository) UpdateExtra(ctx context.Context, id int64, updates m // BulkUpdate updates multiple accounts with the provided fields. // It merges credentials/extra JSONB fields instead of overwriting them. -func (r *AccountRepository) BulkUpdate(ctx context.Context, ids []int64, updates ports.AccountBulkUpdate) (int64, error) { +func (r *AccountRepository) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) { if len(ids) == 0 { return 0, nil } diff --git a/backend/internal/repository/account_repo_integration_test.go b/backend/internal/repository/account_repo_integration_test.go index ca6decb8..76353d72 100644 --- a/backend/internal/repository/account_repo_integration_test.go +++ b/backend/internal/repository/account_repo_integration_test.go @@ -9,7 +9,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/stretchr/testify/suite" "gorm.io/gorm" ) @@ -513,7 +513,7 @@ func (s *AccountRepoSuite) TestBulkUpdate() { a2 := mustCreateAccount(s.T(), s.db, &model.Account{Name: "bulk2", Priority: 1}) newPriority := 99 - affected, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID, a2.ID}, ports.AccountBulkUpdate{ + affected, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID, a2.ID}, service.AccountBulkUpdate{ Priority: &newPriority, }) s.Require().NoError(err) @@ -531,7 +531,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeCredentials() { Credentials: model.JSONB{"existing": "value"}, }) - _, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, ports.AccountBulkUpdate{ + _, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, service.AccountBulkUpdate{ Credentials: model.JSONB{"new_key": "new_value"}, }) s.Require().NoError(err) @@ -547,7 +547,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeExtra() { Extra: model.JSONB{"existing": "val"}, }) - _, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, ports.AccountBulkUpdate{ + _, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, service.AccountBulkUpdate{ Extra: model.JSONB{"new_key": "new_val"}, }) s.Require().NoError(err) @@ -558,7 +558,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeExtra() { } func (s *AccountRepoSuite) TestBulkUpdate_EmptyIDs() { - affected, err := s.repo.BulkUpdate(s.ctx, []int64{}, ports.AccountBulkUpdate{}) + affected, err := s.repo.BulkUpdate(s.ctx, []int64{}, service.AccountBulkUpdate{}) s.Require().NoError(err) s.Require().Zero(affected) } @@ -566,7 +566,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_EmptyIDs() { func (s *AccountRepoSuite) TestBulkUpdate_EmptyUpdates() { a1 := mustCreateAccount(s.T(), s.db, &model.Account{Name: "bulk-empty"}) - affected, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, ports.AccountBulkUpdate{}) + affected, err := s.repo.BulkUpdate(s.ctx, []int64{a1.ID}, service.AccountBulkUpdate{}) s.Require().NoError(err) s.Require().Zero(affected) } diff --git a/backend/internal/repository/api_key_cache.go b/backend/internal/repository/api_key_cache.go index 000b3971..a33382ec 100644 --- a/backend/internal/repository/api_key_cache.go +++ b/backend/internal/repository/api_key_cache.go @@ -5,8 +5,7 @@ import ( "fmt" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -19,7 +18,7 @@ type apiKeyCache struct { rdb *redis.Client } -func NewApiKeyCache(rdb *redis.Client) ports.ApiKeyCache { +func NewApiKeyCache(rdb *redis.Client) service.ApiKeyCache { return &apiKeyCache{rdb: rdb} } diff --git a/backend/internal/repository/billing_cache.go b/backend/internal/repository/billing_cache.go index 5bf6e962..26d789d1 100644 --- a/backend/internal/repository/billing_cache.go +++ b/backend/internal/repository/billing_cache.go @@ -8,8 +8,7 @@ import ( "strconv" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -58,7 +57,7 @@ type billingCache struct { rdb *redis.Client } -func NewBillingCache(rdb *redis.Client) ports.BillingCache { +func NewBillingCache(rdb *redis.Client) service.BillingCache { return &billingCache{rdb: rdb} } @@ -90,7 +89,7 @@ func (c *billingCache) InvalidateUserBalance(ctx context.Context, userID int64) return c.rdb.Del(ctx, key).Err() } -func (c *billingCache) GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*ports.SubscriptionCacheData, error) { +func (c *billingCache) GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*service.SubscriptionCacheData, error) { key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) result, err := c.rdb.HGetAll(ctx, key).Result() if err != nil { @@ -102,8 +101,8 @@ func (c *billingCache) GetSubscriptionCache(ctx context.Context, userID, groupID return c.parseSubscriptionCache(result) } -func (c *billingCache) parseSubscriptionCache(data map[string]string) (*ports.SubscriptionCacheData, error) { - result := &ports.SubscriptionCacheData{} +func (c *billingCache) parseSubscriptionCache(data map[string]string) (*service.SubscriptionCacheData, error) { + result := &service.SubscriptionCacheData{} result.Status = data[subFieldStatus] if result.Status == "" { @@ -136,7 +135,7 @@ func (c *billingCache) parseSubscriptionCache(data map[string]string) (*ports.Su return result, nil } -func (c *billingCache) SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *ports.SubscriptionCacheData) error { +func (c *billingCache) SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *service.SubscriptionCacheData) error { if data == nil { return nil } diff --git a/backend/internal/repository/billing_cache_integration_test.go b/backend/internal/repository/billing_cache_integration_test.go index 893ae8d7..2f7c69a7 100644 --- a/backend/internal/repository/billing_cache_integration_test.go +++ b/backend/internal/repository/billing_cache_integration_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -21,18 +21,18 @@ type BillingCacheSuite struct { func (s *BillingCacheSuite) TestUserBalance() { tests := []struct { name string - fn func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) + fn func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) }{ { name: "missing_key_returns_redis_nil", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { _, err := cache.GetUserBalance(ctx, 1) require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil for missing balance key") }, }, { name: "deduct_on_nonexistent_is_noop", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(1) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) @@ -44,7 +44,7 @@ func (s *BillingCacheSuite) TestUserBalance() { }, { name: "set_and_get_with_ttl", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(2) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) @@ -61,7 +61,7 @@ func (s *BillingCacheSuite) TestUserBalance() { }, { name: "deduct_reduces_balance", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(3) require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 10.5), "SetUserBalance") @@ -74,7 +74,7 @@ func (s *BillingCacheSuite) TestUserBalance() { }, { name: "invalidate_removes_key", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(100) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) @@ -96,7 +96,7 @@ func (s *BillingCacheSuite) TestUserBalance() { }, { name: "deduct_refreshes_ttl", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(103) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) @@ -133,11 +133,11 @@ func (s *BillingCacheSuite) TestUserBalance() { func (s *BillingCacheSuite) TestSubscriptionCache() { tests := []struct { name string - fn func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) + fn func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) }{ { name: "missing_key_returns_redis_nil", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(10) groupID := int64(20) @@ -147,7 +147,7 @@ func (s *BillingCacheSuite) TestSubscriptionCache() { }, { name: "update_usage_on_nonexistent_is_noop", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(11) groupID := int64(21) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) @@ -161,12 +161,12 @@ func (s *BillingCacheSuite) TestSubscriptionCache() { }, { name: "set_and_get_with_ttl", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(12) groupID := int64(22) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) - data := &ports.SubscriptionCacheData{ + data := &service.SubscriptionCacheData{ Status: "active", ExpiresAt: time.Now().Add(1 * time.Hour), DailyUsage: 1.0, @@ -189,11 +189,11 @@ func (s *BillingCacheSuite) TestSubscriptionCache() { }, { name: "update_usage_increments_all_fields", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(13) groupID := int64(23) - data := &ports.SubscriptionCacheData{ + data := &service.SubscriptionCacheData{ Status: "active", ExpiresAt: time.Now().Add(1 * time.Hour), DailyUsage: 1.0, @@ -214,12 +214,12 @@ func (s *BillingCacheSuite) TestSubscriptionCache() { }, { name: "invalidate_removes_key", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(101) groupID := int64(10) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) - data := &ports.SubscriptionCacheData{ + data := &service.SubscriptionCacheData{ Status: "active", ExpiresAt: time.Now().Add(1 * time.Hour), DailyUsage: 1.0, @@ -245,7 +245,7 @@ func (s *BillingCacheSuite) TestSubscriptionCache() { }, { name: "missing_status_returns_parsing_error", - fn: func(ctx context.Context, rdb *redis.Client, cache ports.BillingCache) { + fn: func(ctx context.Context, rdb *redis.Client, cache service.BillingCache) { userID := int64(102) groupID := int64(11) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) diff --git a/backend/internal/repository/concurrency_cache.go b/backend/internal/repository/concurrency_cache.go index baefb385..2946f691 100644 --- a/backend/internal/repository/concurrency_cache.go +++ b/backend/internal/repository/concurrency_cache.go @@ -5,8 +5,7 @@ import ( "fmt" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -107,7 +106,7 @@ type concurrencyCache struct { rdb *redis.Client } -func NewConcurrencyCache(rdb *redis.Client) ports.ConcurrencyCache { +func NewConcurrencyCache(rdb *redis.Client) service.ConcurrencyCache { return &concurrencyCache{rdb: rdb} } diff --git a/backend/internal/repository/concurrency_cache_integration_test.go b/backend/internal/repository/concurrency_cache_integration_test.go index dc27dc9c..c1feaf85 100644 --- a/backend/internal/repository/concurrency_cache_integration_test.go +++ b/backend/internal/repository/concurrency_cache_integration_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -16,7 +16,7 @@ import ( type ConcurrencyCacheSuite struct { IntegrationRedisSuite - cache ports.ConcurrencyCache + cache service.ConcurrencyCache } func (s *ConcurrencyCacheSuite) SetupTest() { diff --git a/backend/internal/repository/email_cache.go b/backend/internal/repository/email_cache.go index dff7a97c..d6cb5c01 100644 --- a/backend/internal/repository/email_cache.go +++ b/backend/internal/repository/email_cache.go @@ -5,8 +5,7 @@ import ( "encoding/json" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -16,24 +15,24 @@ type emailCache struct { rdb *redis.Client } -func NewEmailCache(rdb *redis.Client) ports.EmailCache { +func NewEmailCache(rdb *redis.Client) service.EmailCache { return &emailCache{rdb: rdb} } -func (c *emailCache) GetVerificationCode(ctx context.Context, email string) (*ports.VerificationCodeData, error) { +func (c *emailCache) GetVerificationCode(ctx context.Context, email string) (*service.VerificationCodeData, error) { key := verifyCodeKeyPrefix + email val, err := c.rdb.Get(ctx, key).Result() if err != nil { return nil, err } - var data ports.VerificationCodeData + var data service.VerificationCodeData if err := json.Unmarshal([]byte(val), &data); err != nil { return nil, err } return &data, nil } -func (c *emailCache) SetVerificationCode(ctx context.Context, email string, data *ports.VerificationCodeData, ttl time.Duration) error { +func (c *emailCache) SetVerificationCode(ctx context.Context, email string, data *service.VerificationCodeData, ttl time.Duration) error { key := verifyCodeKeyPrefix + email val, err := json.Marshal(data) if err != nil { diff --git a/backend/internal/repository/email_cache_integration_test.go b/backend/internal/repository/email_cache_integration_test.go index 22ce3f5e..40ec677b 100644 --- a/backend/internal/repository/email_cache_integration_test.go +++ b/backend/internal/repository/email_cache_integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -15,7 +15,7 @@ import ( type EmailCacheSuite struct { IntegrationRedisSuite - cache ports.EmailCache + cache service.EmailCache } func (s *EmailCacheSuite) SetupTest() { @@ -31,7 +31,7 @@ func (s *EmailCacheSuite) TestGetVerificationCode_Missing() { func (s *EmailCacheSuite) TestSetAndGetVerificationCode() { email := "a@example.com" emailTTL := 2 * time.Minute - data := &ports.VerificationCodeData{Code: "123456", Attempts: 1, CreatedAt: time.Now()} + data := &service.VerificationCodeData{Code: "123456", Attempts: 1, CreatedAt: time.Now()} require.NoError(s.T(), s.cache.SetVerificationCode(s.ctx, email, data, emailTTL), "SetVerificationCode") @@ -44,7 +44,7 @@ func (s *EmailCacheSuite) TestSetAndGetVerificationCode() { func (s *EmailCacheSuite) TestVerificationCode_TTL() { email := "ttl@example.com" emailTTL := 2 * time.Minute - data := &ports.VerificationCodeData{Code: "654321", Attempts: 0, CreatedAt: time.Now()} + data := &service.VerificationCodeData{Code: "654321", Attempts: 0, CreatedAt: time.Now()} require.NoError(s.T(), s.cache.SetVerificationCode(s.ctx, email, data, emailTTL), "SetVerificationCode") @@ -56,7 +56,7 @@ func (s *EmailCacheSuite) TestVerificationCode_TTL() { func (s *EmailCacheSuite) TestDeleteVerificationCode() { email := "delete@example.com" - data := &ports.VerificationCodeData{Code: "999999", Attempts: 0, CreatedAt: time.Now()} + data := &service.VerificationCodeData{Code: "999999", Attempts: 0, CreatedAt: time.Now()} require.NoError(s.T(), s.cache.SetVerificationCode(s.ctx, email, data, 2*time.Minute), "SetVerificationCode") diff --git a/backend/internal/repository/gateway_cache.go b/backend/internal/repository/gateway_cache.go index 8a738760..4ed47e9b 100644 --- a/backend/internal/repository/gateway_cache.go +++ b/backend/internal/repository/gateway_cache.go @@ -4,8 +4,7 @@ import ( "context" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -15,7 +14,7 @@ type gatewayCache struct { rdb *redis.Client } -func NewGatewayCache(rdb *redis.Client) ports.GatewayCache { +func NewGatewayCache(rdb *redis.Client) service.GatewayCache { return &gatewayCache{rdb: rdb} } diff --git a/backend/internal/repository/gateway_cache_integration_test.go b/backend/internal/repository/gateway_cache_integration_test.go index 5afe30fa..170f4074 100644 --- a/backend/internal/repository/gateway_cache_integration_test.go +++ b/backend/internal/repository/gateway_cache_integration_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -15,7 +15,7 @@ import ( type GatewayCacheSuite struct { IntegrationRedisSuite - cache ports.GatewayCache + cache service.GatewayCache } func (s *GatewayCacheSuite) SetupTest() { diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index 2dfd8675..26befb25 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -6,7 +6,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" ) // httpUpstreamService is a generic HTTP upstream service that can be used for @@ -17,7 +17,7 @@ type httpUpstreamService struct { } // NewHTTPUpstream creates a new generic HTTP upstream service -func NewHTTPUpstream(cfg *config.Config) ports.HTTPUpstream { +func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream { responseHeaderTimeout := time.Duration(cfg.Gateway.ResponseHeaderTimeout) * time.Second if responseHeaderTimeout == 0 { responseHeaderTimeout = 300 * time.Second diff --git a/backend/internal/repository/identity_cache.go b/backend/internal/repository/identity_cache.go index 0bdfc3ed..9c776d9c 100644 --- a/backend/internal/repository/identity_cache.go +++ b/backend/internal/repository/identity_cache.go @@ -6,8 +6,7 @@ import ( "fmt" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -20,24 +19,24 @@ type identityCache struct { rdb *redis.Client } -func NewIdentityCache(rdb *redis.Client) ports.IdentityCache { +func NewIdentityCache(rdb *redis.Client) service.IdentityCache { return &identityCache{rdb: rdb} } -func (c *identityCache) GetFingerprint(ctx context.Context, accountID int64) (*ports.Fingerprint, error) { +func (c *identityCache) GetFingerprint(ctx context.Context, accountID int64) (*service.Fingerprint, error) { key := fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID) val, err := c.rdb.Get(ctx, key).Result() if err != nil { return nil, err } - var fp ports.Fingerprint + var fp service.Fingerprint if err := json.Unmarshal([]byte(val), &fp); err != nil { return nil, err } return &fp, nil } -func (c *identityCache) SetFingerprint(ctx context.Context, accountID int64, fp *ports.Fingerprint) error { +func (c *identityCache) SetFingerprint(ctx context.Context, accountID int64, fp *service.Fingerprint) error { key := fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID) val, err := json.Marshal(fp) if err != nil { diff --git a/backend/internal/repository/identity_cache_integration_test.go b/backend/internal/repository/identity_cache_integration_test.go index 9452cb48..48f59c13 100644 --- a/backend/internal/repository/identity_cache_integration_test.go +++ b/backend/internal/repository/identity_cache_integration_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -30,7 +30,7 @@ func (s *IdentityCacheSuite) TestGetFingerprint_Missing() { } func (s *IdentityCacheSuite) TestSetAndGetFingerprint() { - fp := &ports.Fingerprint{ClientID: "c1", UserAgent: "ua"} + fp := &service.Fingerprint{ClientID: "c1", UserAgent: "ua"} require.NoError(s.T(), s.cache.SetFingerprint(s.ctx, 1, fp), "SetFingerprint") gotFP, err := s.cache.GetFingerprint(s.ctx, 1) require.NoError(s.T(), err, "GetFingerprint") @@ -39,7 +39,7 @@ func (s *IdentityCacheSuite) TestSetAndGetFingerprint() { } func (s *IdentityCacheSuite) TestFingerprint_TTL() { - fp := &ports.Fingerprint{ClientID: "c1", UserAgent: "ua"} + fp := &service.Fingerprint{ClientID: "c1", UserAgent: "ua"} require.NoError(s.T(), s.cache.SetFingerprint(s.ctx, 2, fp)) fpKey := fmt.Sprintf("%s%d", fingerprintKeyPrefix, 2) diff --git a/backend/internal/repository/openai_oauth_service.go b/backend/internal/repository/openai_oauth_service.go index 846b7b93..da14a338 100644 --- a/backend/internal/repository/openai_oauth_service.go +++ b/backend/internal/repository/openai_oauth_service.go @@ -7,13 +7,12 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/imroc/req/v3" ) // NewOpenAIOAuthClient creates a new OpenAI OAuth client -func NewOpenAIOAuthClient() ports.OpenAIOAuthClient { +func NewOpenAIOAuthClient() service.OpenAIOAuthClient { return &openaiOAuthService{tokenURL: openai.TokenURL} } diff --git a/backend/internal/repository/redeem_cache.go b/backend/internal/repository/redeem_cache.go index 0a13a49c..e0330b58 100644 --- a/backend/internal/repository/redeem_cache.go +++ b/backend/internal/repository/redeem_cache.go @@ -5,8 +5,7 @@ import ( "fmt" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -20,7 +19,7 @@ type redeemCache struct { rdb *redis.Client } -func NewRedeemCache(rdb *redis.Client) ports.RedeemCache { +func NewRedeemCache(rdb *redis.Client) service.RedeemCache { return &redeemCache{rdb: rdb} } diff --git a/backend/internal/repository/update_cache.go b/backend/internal/repository/update_cache.go index 4b1f2892..86a8f14a 100644 --- a/backend/internal/repository/update_cache.go +++ b/backend/internal/repository/update_cache.go @@ -4,8 +4,7 @@ import ( "context" "time" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/redis/go-redis/v9" ) @@ -15,7 +14,7 @@ type updateCache struct { rdb *redis.Client } -func NewUpdateCache(rdb *redis.Client) ports.UpdateCache { +func NewUpdateCache(rdb *redis.Client) service.UpdateCache { return &updateCache{rdb: rdb} } diff --git a/backend/internal/repository/wire.go b/backend/internal/repository/wire.go index 5e188ab8..b4aff0c0 100644 --- a/backend/internal/repository/wire.go +++ b/backend/internal/repository/wire.go @@ -1,8 +1,7 @@ package repository import ( - "github.com/Wei-Shaw/sub2api/internal/service/ports" - + "github.com/Wei-Shaw/sub2api/internal/service" "github.com/google/wire" ) @@ -40,13 +39,13 @@ var ProviderSet = wire.NewSet( NewOpenAIOAuthClient, // Bind concrete repositories to service port interfaces - wire.Bind(new(ports.UserRepository), new(*UserRepository)), - wire.Bind(new(ports.ApiKeyRepository), new(*ApiKeyRepository)), - wire.Bind(new(ports.GroupRepository), new(*GroupRepository)), - wire.Bind(new(ports.AccountRepository), new(*AccountRepository)), - wire.Bind(new(ports.ProxyRepository), new(*ProxyRepository)), - wire.Bind(new(ports.RedeemCodeRepository), new(*RedeemCodeRepository)), - wire.Bind(new(ports.UsageLogRepository), new(*UsageLogRepository)), - wire.Bind(new(ports.SettingRepository), new(*SettingRepository)), - wire.Bind(new(ports.UserSubscriptionRepository), new(*UserSubscriptionRepository)), + wire.Bind(new(service.UserRepository), new(*UserRepository)), + wire.Bind(new(service.ApiKeyRepository), new(*ApiKeyRepository)), + wire.Bind(new(service.GroupRepository), new(*GroupRepository)), + wire.Bind(new(service.AccountRepository), new(*AccountRepository)), + wire.Bind(new(service.ProxyRepository), new(*ProxyRepository)), + wire.Bind(new(service.RedeemCodeRepository), new(*RedeemCodeRepository)), + wire.Bind(new(service.UsageLogRepository), new(*UsageLogRepository)), + wire.Bind(new(service.SettingRepository), new(*SettingRepository)), + wire.Bind(new(service.UserSubscriptionRepository), new(*UserSubscriptionRepository)), ) diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go index ea2c4c85..705f8a6d 100644 --- a/backend/internal/service/account_service.go +++ b/backend/internal/service/account_service.go @@ -4,10 +4,10 @@ import ( "context" "errors" "fmt" + "time" + "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "gorm.io/gorm" ) @@ -15,6 +15,51 @@ var ( ErrAccountNotFound = errors.New("account not found") ) +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 +} + // CreateAccountRequest 创建账号请求 type CreateAccountRequest struct { Name string `json:"name"` @@ -42,12 +87,12 @@ type UpdateAccountRequest struct { // AccountService 账号管理服务 type AccountService struct { - accountRepo ports.AccountRepository - groupRepo ports.GroupRepository + accountRepo AccountRepository + groupRepo GroupRepository } // NewAccountService 创建账号服务实例 -func NewAccountService(accountRepo ports.AccountRepository, groupRepo ports.GroupRepository) *AccountService { +func NewAccountService(accountRepo AccountRepository, groupRepo GroupRepository) *AccountService { return &AccountService{ accountRepo: accountRepo, groupRepo: groupRepo, diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index d85adc98..782aa95c 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -17,8 +17,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "github.com/gin-gonic/gin" "github.com/google/uuid" ) @@ -40,14 +38,14 @@ type TestEvent struct { // AccountTestService handles account testing operations type AccountTestService struct { - accountRepo ports.AccountRepository + accountRepo AccountRepository oauthService *OAuthService openaiOAuthService *OpenAIOAuthService - httpUpstream ports.HTTPUpstream + httpUpstream HTTPUpstream } // NewAccountTestService creates a new AccountTestService -func NewAccountTestService(accountRepo ports.AccountRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, httpUpstream ports.HTTPUpstream) *AccountTestService { +func NewAccountTestService(accountRepo AccountRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, httpUpstream HTTPUpstream) *AccountTestService { return &AccountTestService{ accountRepo: accountRepo, oauthService: oauthService, diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 63325856..6f89b600 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -8,10 +8,49 @@ import ( "time" "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/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数据 type usageCache struct { data *UsageInfo @@ -69,13 +108,13 @@ type ClaudeUsageFetcher interface { // AccountUsageService 账号使用量查询服务 type AccountUsageService struct { - accountRepo ports.AccountRepository - usageLogRepo ports.UsageLogRepository + accountRepo AccountRepository + usageLogRepo UsageLogRepository usageFetcher ClaudeUsageFetcher } // NewAccountUsageService 创建AccountUsageService实例 -func NewAccountUsageService(accountRepo ports.AccountRepository, usageLogRepo ports.UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService { +func NewAccountUsageService(accountRepo AccountRepository, usageLogRepo UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService { return &AccountUsageService{ accountRepo: accountRepo, usageLogRepo: usageLogRepo, diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 737c6a50..3000d87a 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -9,8 +9,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "gorm.io/gorm" ) @@ -221,24 +219,24 @@ type ProxyExitInfoProber interface { // adminServiceImpl implements AdminService type adminServiceImpl struct { - userRepo ports.UserRepository - groupRepo ports.GroupRepository - accountRepo ports.AccountRepository - proxyRepo ports.ProxyRepository - apiKeyRepo ports.ApiKeyRepository - redeemCodeRepo ports.RedeemCodeRepository + userRepo UserRepository + groupRepo GroupRepository + accountRepo AccountRepository + proxyRepo ProxyRepository + apiKeyRepo ApiKeyRepository + redeemCodeRepo RedeemCodeRepository billingCacheService *BillingCacheService proxyProber ProxyExitInfoProber } // NewAdminService creates a new AdminService func NewAdminService( - userRepo ports.UserRepository, - groupRepo ports.GroupRepository, - accountRepo ports.AccountRepository, - proxyRepo ports.ProxyRepository, - apiKeyRepo ports.ApiKeyRepository, - redeemCodeRepo ports.RedeemCodeRepository, + userRepo UserRepository, + groupRepo GroupRepository, + accountRepo AccountRepository, + proxyRepo ProxyRepository, + apiKeyRepo ApiKeyRepository, + redeemCodeRepo RedeemCodeRepository, billingCacheService *BillingCacheService, proxyProber ProxyExitInfoProber, ) AdminService { @@ -734,7 +732,7 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp } // Prepare bulk updates for columns and JSONB fields. - repoUpdates := ports.AccountBulkUpdate{ + repoUpdates := AccountBulkUpdate{ Credentials: input.Credentials, Extra: input.Extra, } diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 557b925b..f1d7c9f3 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -6,13 +6,12 @@ import ( "encoding/hex" "errors" "fmt" + "time" + "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "time" - "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -30,6 +29,32 @@ const ( 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请求 type CreateApiKeyRequest struct { Name string `json:"name"` @@ -46,21 +71,21 @@ type UpdateApiKeyRequest struct { // ApiKeyService API Key服务 type ApiKeyService struct { - apiKeyRepo ports.ApiKeyRepository - userRepo ports.UserRepository - groupRepo ports.GroupRepository - userSubRepo ports.UserSubscriptionRepository - cache ports.ApiKeyCache + apiKeyRepo ApiKeyRepository + userRepo UserRepository + groupRepo GroupRepository + userSubRepo UserSubscriptionRepository + cache ApiKeyCache cfg *config.Config } // NewApiKeyService 创建API Key服务实例 func NewApiKeyService( - apiKeyRepo ports.ApiKeyRepository, - userRepo ports.UserRepository, - groupRepo ports.GroupRepository, - userSubRepo ports.UserSubscriptionRepository, - cache ports.ApiKeyCache, + apiKeyRepo ApiKeyRepository, + userRepo UserRepository, + groupRepo GroupRepository, + userSubRepo UserSubscriptionRepository, + cache ApiKeyCache, cfg *config.Config, ) *ApiKeyService { return &ApiKeyService{ diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go index ce788f55..8c6d78d5 100644 --- a/backend/internal/service/auth_service.go +++ b/backend/internal/service/auth_service.go @@ -4,12 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" "log" "time" + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -36,7 +36,7 @@ type JWTClaims struct { // AuthService 认证服务 type AuthService struct { - userRepo ports.UserRepository + userRepo UserRepository cfg *config.Config settingService *SettingService emailService *EmailService @@ -46,7 +46,7 @@ type AuthService struct { // NewAuthService 创建认证服务实例 func NewAuthService( - userRepo ports.UserRepository, + userRepo UserRepository, cfg *config.Config, settingService *SettingService, emailService *EmailService, diff --git a/backend/internal/service/billing_cache_port.go b/backend/internal/service/billing_cache_port.go new file mode 100644 index 00000000..00bb43da --- /dev/null +++ b/backend/internal/service/billing_cache_port.go @@ -0,0 +1,15 @@ +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 +} diff --git a/backend/internal/service/billing_cache_service.go b/backend/internal/service/billing_cache_service.go index 4271d751..1a18ff12 100644 --- a/backend/internal/service/billing_cache_service.go +++ b/backend/internal/service/billing_cache_service.go @@ -8,7 +8,6 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) // 错误定义 @@ -31,13 +30,13 @@ type subscriptionCacheData struct { // BillingCacheService 计费缓存服务 // 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查 type BillingCacheService struct { - cache ports.BillingCache - userRepo ports.UserRepository - subRepo ports.UserSubscriptionRepository + cache BillingCache + userRepo UserRepository + subRepo UserSubscriptionRepository } // NewBillingCacheService 创建计费缓存服务 -func NewBillingCacheService(cache ports.BillingCache, userRepo ports.UserRepository, subRepo ports.UserSubscriptionRepository) *BillingCacheService { +func NewBillingCacheService(cache BillingCache, userRepo UserRepository, subRepo UserSubscriptionRepository) *BillingCacheService { return &BillingCacheService{ cache: cache, userRepo: userRepo, @@ -149,7 +148,7 @@ func (s *BillingCacheService) GetSubscriptionStatus(ctx context.Context, userID, return data, nil } -func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCacheData) *subscriptionCacheData { +func (s *BillingCacheService) convertFromPortsData(data *SubscriptionCacheData) *subscriptionCacheData { return &subscriptionCacheData{ Status: data.Status, ExpiresAt: data.ExpiresAt, @@ -160,8 +159,8 @@ func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCache } } -func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *ports.SubscriptionCacheData { - return &ports.SubscriptionCacheData{ +func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *SubscriptionCacheData { + return &SubscriptionCacheData{ Status: data.Status, ExpiresAt: data.ExpiresAt, DailyUsage: data.DailyUsage, diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index cd275133..a2254744 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -1,12 +1,30 @@ package service import ( + "context" "fmt" - "github.com/Wei-Shaw/sub2api/internal/config" + "log" "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格式一致) type ModelPricing struct { InputPricePerToken float64 // 每token输入价格 (USD) diff --git a/backend/internal/service/concurrency_service.go b/backend/internal/service/concurrency_service.go index f70fd2cf..a6cff234 100644 --- a/backend/internal/service/concurrency_service.go +++ b/backend/internal/service/concurrency_service.go @@ -7,10 +7,28 @@ import ( "fmt" "log" "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 // Uses 8 random bytes (16 hex chars) for uniqueness func generateRequestID() string { @@ -29,11 +47,11 @@ const ( // ConcurrencyService manages concurrent request limiting for accounts and users type ConcurrencyService struct { - cache ports.ConcurrencyCache + cache ConcurrencyCache } // NewConcurrencyService creates a new ConcurrencyService -func NewConcurrencyService(cache ports.ConcurrencyCache) *ConcurrencyService { +func NewConcurrencyService(cache ConcurrencyCache) *ConcurrencyService { return &ConcurrencyService{cache: cache} } diff --git a/backend/internal/service/crs_sync_service.go b/backend/internal/service/crs_sync_service.go index 09bf2660..e1f9d252 100644 --- a/backend/internal/service/crs_sync_service.go +++ b/backend/internal/service/crs_sync_service.go @@ -13,19 +13,18 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) type CRSSyncService struct { - accountRepo ports.AccountRepository - proxyRepo ports.ProxyRepository + accountRepo AccountRepository + proxyRepo ProxyRepository oauthService *OAuthService openaiOAuthService *OpenAIOAuthService } func NewCRSSyncService( - accountRepo ports.AccountRepository, - proxyRepo ports.ProxyRepository, + accountRepo AccountRepository, + proxyRepo ProxyRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, ) *CRSSyncService { diff --git a/backend/internal/service/dashboard_service.go b/backend/internal/service/dashboard_service.go index f23a1e01..4de4a751 100644 --- a/backend/internal/service/dashboard_service.go +++ b/backend/internal/service/dashboard_service.go @@ -6,15 +6,14 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) // DashboardService provides aggregated statistics for admin dashboard. type DashboardService struct { - usageRepo ports.UsageLogRepository + usageRepo UsageLogRepository } -func NewDashboardService(usageRepo ports.UsageLogRepository) *DashboardService { +func NewDashboardService(usageRepo UsageLogRepository) *DashboardService { return &DashboardService{ usageRepo: usageRepo, } diff --git a/backend/internal/service/email_service.go b/backend/internal/service/email_service.go index 5a956a32..38da392e 100644 --- a/backend/internal/service/email_service.go +++ b/backend/internal/service/email_service.go @@ -6,12 +6,12 @@ import ( "crypto/tls" "errors" "fmt" - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" "math/big" "net/smtp" "strconv" "time" + + "github.com/Wei-Shaw/sub2api/internal/model" ) var ( @@ -21,6 +21,20 @@ var ( 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 ( verifyCodeTTL = 15 * time.Minute verifyCodeCooldown = 1 * time.Minute @@ -40,12 +54,12 @@ type SmtpConfig struct { // EmailService 邮件服务 type EmailService struct { - settingRepo ports.SettingRepository - cache ports.EmailCache + settingRepo SettingRepository + cache EmailCache } // NewEmailService 创建邮件服务实例 -func NewEmailService(settingRepo ports.SettingRepository, cache ports.EmailCache) *EmailService { +func NewEmailService(settingRepo SettingRepository, cache EmailCache) *EmailService { return &EmailService{ settingRepo: settingRepo, cache: cache, @@ -205,7 +219,7 @@ func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName strin } // 保存验证码到 Redis - data := &ports.VerificationCodeData{ + data := &VerificationCodeData{ Code: code, Attempts: 0, CreatedAt: time.Now(), diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index c58f5ab7..9b8def88 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -19,7 +19,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" - "github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -54,6 +53,13 @@ var allowedHeaders = map[string]bool{ "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信息 type ClaudeUsage struct { InputTokens int `json:"input_tokens"` @@ -74,32 +80,32 @@ type ForwardResult struct { // GatewayService handles API gateway operations type GatewayService struct { - accountRepo ports.AccountRepository - usageLogRepo ports.UsageLogRepository - userRepo ports.UserRepository - userSubRepo ports.UserSubscriptionRepository - cache ports.GatewayCache + accountRepo AccountRepository + usageLogRepo UsageLogRepository + userRepo UserRepository + userSubRepo UserSubscriptionRepository + cache GatewayCache cfg *config.Config billingService *BillingService rateLimitService *RateLimitService billingCacheService *BillingCacheService identityService *IdentityService - httpUpstream ports.HTTPUpstream + httpUpstream HTTPUpstream } // NewGatewayService creates a new GatewayService func NewGatewayService( - accountRepo ports.AccountRepository, - usageLogRepo ports.UsageLogRepository, - userRepo ports.UserRepository, - userSubRepo ports.UserSubscriptionRepository, - cache ports.GatewayCache, + accountRepo AccountRepository, + usageLogRepo UsageLogRepository, + userRepo UserRepository, + userSubRepo UserSubscriptionRepository, + cache GatewayCache, cfg *config.Config, billingService *BillingService, rateLimitService *RateLimitService, billingCacheService *BillingCacheService, identityService *IdentityService, - httpUpstream ports.HTTPUpstream, + httpUpstream HTTPUpstream, ) *GatewayService { return &GatewayService{ accountRepo: accountRepo, @@ -507,7 +513,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex } // OAuth账号:应用统一指纹 - var fingerprint *ports.Fingerprint + var fingerprint *Fingerprint if account.IsOAuth() && s.identityService != nil { // 1. 获取或创建指纹(包含随机生成的ClientID) fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) diff --git a/backend/internal/service/group_service.go b/backend/internal/service/group_service.go index d7b76969..de351a0e 100644 --- a/backend/internal/service/group_service.go +++ b/backend/internal/service/group_service.go @@ -4,10 +4,9 @@ import ( "context" "errors" "fmt" + "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "gorm.io/gorm" ) @@ -16,6 +15,24 @@ var ( 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 创建分组请求 type CreateGroupRequest struct { Name string `json:"name"` @@ -35,11 +52,11 @@ type UpdateGroupRequest struct { // GroupService 分组管理服务 type GroupService struct { - groupRepo ports.GroupRepository + groupRepo GroupRepository } // NewGroupService 创建分组服务实例 -func NewGroupService(groupRepo ports.GroupRepository) *GroupService { +func NewGroupService(groupRepo GroupRepository) *GroupService { return &GroupService{ groupRepo: groupRepo, } diff --git a/backend/internal/service/ports/http_upstream.go b/backend/internal/service/http_upstream_port.go similarity index 94% rename from backend/internal/service/ports/http_upstream.go rename to backend/internal/service/http_upstream_port.go index f3c4b369..7fb9407f 100644 --- a/backend/internal/service/ports/http_upstream.go +++ b/backend/internal/service/http_upstream_port.go @@ -1,4 +1,4 @@ -package ports +package service import "net/http" diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go index 6eec69ae..1ffa8057 100644 --- a/backend/internal/service/identity_service.go +++ b/backend/internal/service/identity_service.go @@ -7,7 +7,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/Wei-Shaw/sub2api/internal/service/ports" "log" "net/http" "regexp" @@ -24,7 +23,7 @@ var ( ) // 默认指纹值(当客户端未提供时使用) -var defaultFingerprint = ports.Fingerprint{ +var defaultFingerprint = Fingerprint{ UserAgent: "claude-cli/2.0.62 (external, cli)", StainlessLang: "js", StainlessPackageVersion: "0.52.0", @@ -34,20 +33,38 @@ var defaultFingerprint = ports.Fingerprint{ 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账号的请求身份指纹 type IdentityService struct { - cache ports.IdentityCache + cache IdentityCache } // NewIdentityService 创建新的IdentityService -func NewIdentityService(cache ports.IdentityCache) *IdentityService { +func NewIdentityService(cache IdentityCache) *IdentityService { return &IdentityService{cache: cache} } // GetOrCreateFingerprint 获取或创建账号的指纹 // 如果缓存存在,检测user-agent版本,新版本则更新 // 如果缓存不存在,生成随机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) if err == nil && cached != nil { @@ -79,8 +96,8 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID } // createFingerprintFromHeaders 从请求头创建指纹 -func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *ports.Fingerprint { - fp := &ports.Fingerprint{} +func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fingerprint { + fp := &Fingerprint{} // 获取User-Agent if ua := headers.Get("User-Agent"); ua != "" { @@ -109,7 +126,7 @@ func getHeaderOrDefault(headers http.Header, key, defaultValue string) string { } // 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 { return } diff --git a/backend/internal/service/oauth_service.go b/backend/internal/service/oauth_service.go index 67c1326c..8eb476c2 100644 --- a/backend/internal/service/oauth_service.go +++ b/backend/internal/service/oauth_service.go @@ -8,9 +8,15 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "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 type ClaudeOAuthClient interface { GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error) @@ -22,12 +28,12 @@ type ClaudeOAuthClient interface { // OAuthService handles OAuth authentication flows type OAuthService struct { sessionStore *oauth.SessionStore - proxyRepo ports.ProxyRepository + proxyRepo ProxyRepository oauthClient ClaudeOAuthClient } // NewOAuthService creates a new OAuth service -func NewOAuthService(proxyRepo ports.ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService { +func NewOAuthService(proxyRepo ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService { return &OAuthService{ sessionStore: oauth.NewSessionStore(), proxyRepo: proxyRepo, diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index ffd61a92..ca3c2c36 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -17,8 +17,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "github.com/gin-gonic/gin" ) @@ -71,30 +69,30 @@ type OpenAIForwardResult struct { // OpenAIGatewayService handles OpenAI API gateway operations type OpenAIGatewayService struct { - accountRepo ports.AccountRepository - usageLogRepo ports.UsageLogRepository - userRepo ports.UserRepository - userSubRepo ports.UserSubscriptionRepository - cache ports.GatewayCache + accountRepo AccountRepository + usageLogRepo UsageLogRepository + userRepo UserRepository + userSubRepo UserSubscriptionRepository + cache GatewayCache cfg *config.Config billingService *BillingService rateLimitService *RateLimitService billingCacheService *BillingCacheService - httpUpstream ports.HTTPUpstream + httpUpstream HTTPUpstream } // NewOpenAIGatewayService creates a new OpenAIGatewayService func NewOpenAIGatewayService( - accountRepo ports.AccountRepository, - usageLogRepo ports.UsageLogRepository, - userRepo ports.UserRepository, - userSubRepo ports.UserSubscriptionRepository, - cache ports.GatewayCache, + accountRepo AccountRepository, + usageLogRepo UsageLogRepository, + userRepo UserRepository, + userSubRepo UserSubscriptionRepository, + cache GatewayCache, cfg *config.Config, billingService *BillingService, rateLimitService *RateLimitService, billingCacheService *BillingCacheService, - httpUpstream ports.HTTPUpstream, + httpUpstream HTTPUpstream, ) *OpenAIGatewayService { return &OpenAIGatewayService{ accountRepo: accountRepo, diff --git a/backend/internal/service/openai_oauth_service.go b/backend/internal/service/openai_oauth_service.go index 716e1b93..420c755c 100644 --- a/backend/internal/service/openai_oauth_service.go +++ b/backend/internal/service/openai_oauth_service.go @@ -7,18 +7,17 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) // OpenAIOAuthService handles OpenAI OAuth authentication flows type OpenAIOAuthService struct { sessionStore *openai.SessionStore - proxyRepo ports.ProxyRepository - oauthClient ports.OpenAIOAuthClient + proxyRepo ProxyRepository + oauthClient OpenAIOAuthClient } // 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{ sessionStore: openai.NewSessionStore(), proxyRepo: proxyRepo, diff --git a/backend/internal/service/ports/account.go b/backend/internal/service/ports/account.go deleted file mode 100644 index 2d0e979d..00000000 --- a/backend/internal/service/ports/account.go +++ /dev/null @@ -1,54 +0,0 @@ -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 -} diff --git a/backend/internal/service/ports/api_key.go b/backend/internal/service/ports/api_key.go deleted file mode 100644 index fc39f3f2..00000000 --- a/backend/internal/service/ports/api_key.go +++ /dev/null @@ -1,24 +0,0 @@ -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) -} diff --git a/backend/internal/service/ports/api_key_cache.go b/backend/internal/service/ports/api_key_cache.go deleted file mode 100644 index 0b9efb24..00000000 --- a/backend/internal/service/ports/api_key_cache.go +++ /dev/null @@ -1,16 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// 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 -} diff --git a/backend/internal/service/ports/billing_cache.go b/backend/internal/service/ports/billing_cache.go deleted file mode 100644 index b357006a..00000000 --- a/backend/internal/service/ports/billing_cache.go +++ /dev/null @@ -1,31 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// SubscriptionCacheData represents cached subscription data -type SubscriptionCacheData struct { - Status string - ExpiresAt time.Time - DailyUsage float64 - WeeklyUsage float64 - MonthlyUsage float64 - Version int64 -} - -// 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 -} diff --git a/backend/internal/service/ports/concurrency_cache.go b/backend/internal/service/ports/concurrency_cache.go deleted file mode 100644 index 2344fe62..00000000 --- a/backend/internal/service/ports/concurrency_cache.go +++ /dev/null @@ -1,23 +0,0 @@ -package ports - -import "context" - -// 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 -} diff --git a/backend/internal/service/ports/email_cache.go b/backend/internal/service/ports/email_cache.go deleted file mode 100644 index a48a3761..00000000 --- a/backend/internal/service/ports/email_cache.go +++ /dev/null @@ -1,20 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// VerificationCodeData represents verification code data -type VerificationCodeData struct { - Code string - Attempts int - CreatedAt time.Time -} - -// 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 -} diff --git a/backend/internal/service/ports/gateway_cache.go b/backend/internal/service/ports/gateway_cache.go deleted file mode 100644 index 9df3aa40..00000000 --- a/backend/internal/service/ports/gateway_cache.go +++ /dev/null @@ -1,13 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// 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 -} diff --git a/backend/internal/service/ports/group.go b/backend/internal/service/ports/group.go deleted file mode 100644 index 5dda2933..00000000 --- a/backend/internal/service/ports/group.go +++ /dev/null @@ -1,28 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - - "gorm.io/gorm" -) - -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 -} diff --git a/backend/internal/service/ports/identity_cache.go b/backend/internal/service/ports/identity_cache.go deleted file mode 100644 index a8fbc611..00000000 --- a/backend/internal/service/ports/identity_cache.go +++ /dev/null @@ -1,21 +0,0 @@ -package ports - -import "context" - -// 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 -} diff --git a/backend/internal/service/ports/openai_oauth.go b/backend/internal/service/ports/openai_oauth.go deleted file mode 100644 index 06be867d..00000000 --- a/backend/internal/service/ports/openai_oauth.go +++ /dev/null @@ -1,13 +0,0 @@ -package ports - -import ( - "context" - - "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) -} diff --git a/backend/internal/service/ports/proxy.go b/backend/internal/service/ports/proxy.go deleted file mode 100644 index 65585fac..00000000 --- a/backend/internal/service/ports/proxy.go +++ /dev/null @@ -1,23 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" -) - -type ProxyRepository interface { - Create(ctx context.Context, proxy *model.Proxy) error - GetByID(ctx context.Context, id int64) (*model.Proxy, error) - Update(ctx context.Context, proxy *model.Proxy) error - Delete(ctx context.Context, id int64) error - - List(ctx context.Context, params pagination.PaginationParams) ([]model.Proxy, *pagination.PaginationResult, error) - ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]model.Proxy, *pagination.PaginationResult, error) - ListActive(ctx context.Context) ([]model.Proxy, error) - ListActiveWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) - - ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) - CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) -} diff --git a/backend/internal/service/ports/redeem_cache.go b/backend/internal/service/ports/redeem_cache.go deleted file mode 100644 index a90ad1de..00000000 --- a/backend/internal/service/ports/redeem_cache.go +++ /dev/null @@ -1,15 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// RedeemCache defines cache operations for redeem service -type RedeemCache interface { - GetRedeemAttemptCount(ctx context.Context, userID int64) (int, error) - IncrementRedeemAttemptCount(ctx context.Context, userID int64) error - - AcquireRedeemLock(ctx context.Context, code string, ttl time.Duration) (bool, error) - ReleaseRedeemLock(ctx context.Context, code string) error -} diff --git a/backend/internal/service/ports/redeem_code.go b/backend/internal/service/ports/redeem_code.go deleted file mode 100644 index 6b513b89..00000000 --- a/backend/internal/service/ports/redeem_code.go +++ /dev/null @@ -1,22 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" -) - -type RedeemCodeRepository interface { - Create(ctx context.Context, code *model.RedeemCode) error - CreateBatch(ctx context.Context, codes []model.RedeemCode) error - GetByID(ctx context.Context, id int64) (*model.RedeemCode, error) - GetByCode(ctx context.Context, code string) (*model.RedeemCode, error) - Update(ctx context.Context, code *model.RedeemCode) error - Delete(ctx context.Context, id int64) error - Use(ctx context.Context, id, userID int64) error - - List(ctx context.Context, params pagination.PaginationParams) ([]model.RedeemCode, *pagination.PaginationResult, error) - ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]model.RedeemCode, *pagination.PaginationResult, error) - ListByUser(ctx context.Context, userID int64, limit int) ([]model.RedeemCode, error) -} diff --git a/backend/internal/service/ports/setting.go b/backend/internal/service/ports/setting.go deleted file mode 100644 index c928b720..00000000 --- a/backend/internal/service/ports/setting.go +++ /dev/null @@ -1,17 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Wei-Shaw/sub2api/internal/model" -) - -type SettingRepository interface { - Get(ctx context.Context, key string) (*model.Setting, error) - GetValue(ctx context.Context, key string) (string, error) - Set(ctx context.Context, key, value string) error - GetMultiple(ctx context.Context, keys []string) (map[string]string, error) - SetMultiple(ctx context.Context, settings map[string]string) error - GetAll(ctx context.Context) (map[string]string, error) - Delete(ctx context.Context, key string) error -} diff --git a/backend/internal/service/ports/update_cache.go b/backend/internal/service/ports/update_cache.go deleted file mode 100644 index 125bbc62..00000000 --- a/backend/internal/service/ports/update_cache.go +++ /dev/null @@ -1,12 +0,0 @@ -package ports - -import ( - "context" - "time" -) - -// UpdateCache defines cache operations for update service -type UpdateCache interface { - GetUpdateInfo(ctx context.Context) (string, error) - SetUpdateInfo(ctx context.Context, data string, ttl time.Duration) error -} diff --git a/backend/internal/service/ports/usage_log.go b/backend/internal/service/ports/usage_log.go deleted file mode 100644 index 6f3f9370..00000000 --- a/backend/internal/service/ports/usage_log.go +++ /dev/null @@ -1,49 +0,0 @@ -package ports - -import ( - "context" - "time" - - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" -) - -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) -} diff --git a/backend/internal/service/ports/user.go b/backend/internal/service/ports/user.go deleted file mode 100644 index bd27bc2b..00000000 --- a/backend/internal/service/ports/user.go +++ /dev/null @@ -1,26 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" -) - -type UserRepository interface { - Create(ctx context.Context, user *model.User) error - GetByID(ctx context.Context, id int64) (*model.User, error) - GetByEmail(ctx context.Context, email string) (*model.User, error) - GetFirstAdmin(ctx context.Context) (*model.User, error) - Update(ctx context.Context, user *model.User) error - Delete(ctx context.Context, id int64) error - - List(ctx context.Context, params pagination.PaginationParams) ([]model.User, *pagination.PaginationResult, error) - ListWithFilters(ctx context.Context, params pagination.PaginationParams, status, role, search string) ([]model.User, *pagination.PaginationResult, error) - - UpdateBalance(ctx context.Context, id int64, amount float64) error - DeductBalance(ctx context.Context, id int64, amount float64) error - UpdateConcurrency(ctx context.Context, id int64, amount int) error - ExistsByEmail(ctx context.Context, email string) (bool, error) - RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) -} diff --git a/backend/internal/service/proxy_service.go b/backend/internal/service/proxy_service.go index 36fdd0ce..2c16a045 100644 --- a/backend/internal/service/proxy_service.go +++ b/backend/internal/service/proxy_service.go @@ -4,10 +4,9 @@ import ( "context" "errors" "fmt" + "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "gorm.io/gorm" ) @@ -15,6 +14,21 @@ var ( ErrProxyNotFound = errors.New("proxy not found") ) +type ProxyRepository interface { + Create(ctx context.Context, proxy *model.Proxy) error + GetByID(ctx context.Context, id int64) (*model.Proxy, error) + Update(ctx context.Context, proxy *model.Proxy) error + Delete(ctx context.Context, id int64) error + + List(ctx context.Context, params pagination.PaginationParams) ([]model.Proxy, *pagination.PaginationResult, error) + ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]model.Proxy, *pagination.PaginationResult, error) + ListActive(ctx context.Context) ([]model.Proxy, error) + ListActiveWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) + + ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) + CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) +} + // CreateProxyRequest 创建代理请求 type CreateProxyRequest struct { Name string `json:"name"` @@ -38,11 +52,11 @@ type UpdateProxyRequest struct { // ProxyService 代理管理服务 type ProxyService struct { - proxyRepo ports.ProxyRepository + proxyRepo ProxyRepository } // NewProxyService 创建代理服务实例 -func NewProxyService(proxyRepo ports.ProxyRepository) *ProxyService { +func NewProxyService(proxyRepo ProxyRepository) *ProxyService { return &ProxyService{ proxyRepo: proxyRepo, } diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go index ad2c0248..7bee7907 100644 --- a/backend/internal/service/ratelimit_service.go +++ b/backend/internal/service/ratelimit_service.go @@ -9,17 +9,16 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) // RateLimitService 处理限流和过载状态管理 type RateLimitService struct { - accountRepo ports.AccountRepository + accountRepo AccountRepository cfg *config.Config } // NewRateLimitService 创建RateLimitService实例 -func NewRateLimitService(accountRepo ports.AccountRepository, cfg *config.Config) *RateLimitService { +func NewRateLimitService(accountRepo AccountRepository, cfg *config.Config) *RateLimitService { return &RateLimitService{ accountRepo: accountRepo, cfg: cfg, diff --git a/backend/internal/service/redeem_service.go b/backend/internal/service/redeem_service.go index a0b63515..578fb3dd 100644 --- a/backend/internal/service/redeem_service.go +++ b/backend/internal/service/redeem_service.go @@ -6,12 +6,11 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" "strings" "time" + "github.com/Wei-Shaw/sub2api/internal/model" + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -31,6 +30,29 @@ const ( redeemLockDuration = 10 * time.Second // 锁超时时间,防止死锁 ) +// RedeemCache defines cache operations for redeem service +type RedeemCache interface { + GetRedeemAttemptCount(ctx context.Context, userID int64) (int, error) + IncrementRedeemAttemptCount(ctx context.Context, userID int64) error + + AcquireRedeemLock(ctx context.Context, code string, ttl time.Duration) (bool, error) + ReleaseRedeemLock(ctx context.Context, code string) error +} + +type RedeemCodeRepository interface { + Create(ctx context.Context, code *model.RedeemCode) error + CreateBatch(ctx context.Context, codes []model.RedeemCode) error + GetByID(ctx context.Context, id int64) (*model.RedeemCode, error) + GetByCode(ctx context.Context, code string) (*model.RedeemCode, error) + Update(ctx context.Context, code *model.RedeemCode) error + Delete(ctx context.Context, id int64) error + Use(ctx context.Context, id, userID int64) error + + List(ctx context.Context, params pagination.PaginationParams) ([]model.RedeemCode, *pagination.PaginationResult, error) + ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]model.RedeemCode, *pagination.PaginationResult, error) + ListByUser(ctx context.Context, userID int64, limit int) ([]model.RedeemCode, error) +} + // GenerateCodesRequest 生成兑换码请求 type GenerateCodesRequest struct { Count int `json:"count"` @@ -48,19 +70,19 @@ type RedeemCodeResponse struct { // RedeemService 兑换码服务 type RedeemService struct { - redeemRepo ports.RedeemCodeRepository - userRepo ports.UserRepository + redeemRepo RedeemCodeRepository + userRepo UserRepository subscriptionService *SubscriptionService - cache ports.RedeemCache + cache RedeemCache billingCacheService *BillingCacheService } // NewRedeemService 创建兑换码服务实例 func NewRedeemService( - redeemRepo ports.RedeemCodeRepository, - userRepo ports.UserRepository, + redeemRepo RedeemCodeRepository, + userRepo UserRepository, subscriptionService *SubscriptionService, - cache ports.RedeemCache, + cache RedeemCache, billingCacheService *BillingCacheService, ) *RedeemService { return &RedeemService{ diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index c914efd6..fcbb4035 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -6,10 +6,10 @@ import ( "encoding/hex" "errors" "fmt" + "strconv" + "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "strconv" "gorm.io/gorm" ) @@ -18,14 +18,24 @@ var ( ErrRegistrationDisabled = errors.New("registration is currently disabled") ) +type SettingRepository interface { + Get(ctx context.Context, key string) (*model.Setting, error) + GetValue(ctx context.Context, key string) (string, error) + Set(ctx context.Context, key, value string) error + GetMultiple(ctx context.Context, keys []string) (map[string]string, error) + SetMultiple(ctx context.Context, settings map[string]string) error + GetAll(ctx context.Context) (map[string]string, error) + Delete(ctx context.Context, key string) error +} + // SettingService 系统设置服务 type SettingService struct { - settingRepo ports.SettingRepository + settingRepo SettingRepository cfg *config.Config } // NewSettingService 创建系统设置服务实例 -func NewSettingService(settingRepo ports.SettingRepository, cfg *config.Config) *SettingService { +func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService { return &SettingService{ settingRepo: settingRepo, cfg: cfg, diff --git a/backend/internal/service/subscription_service.go b/backend/internal/service/subscription_service.go index e08d0cf3..8d7a1b3b 100644 --- a/backend/internal/service/subscription_service.go +++ b/backend/internal/service/subscription_service.go @@ -9,7 +9,6 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) var ( @@ -25,13 +24,13 @@ var ( // SubscriptionService 订阅服务 type SubscriptionService struct { - groupRepo ports.GroupRepository - userSubRepo ports.UserSubscriptionRepository + groupRepo GroupRepository + userSubRepo UserSubscriptionRepository billingCacheService *BillingCacheService } // NewSubscriptionService 创建订阅服务 -func NewSubscriptionService(groupRepo ports.GroupRepository, userSubRepo ports.UserSubscriptionRepository, billingCacheService *BillingCacheService) *SubscriptionService { +func NewSubscriptionService(groupRepo GroupRepository, userSubRepo UserSubscriptionRepository, billingCacheService *BillingCacheService) *SubscriptionService { return &SubscriptionService{ groupRepo: groupRepo, userSubRepo: userSubRepo, diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index 955f588f..24ef7b8e 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -9,13 +9,12 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/model" - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) // TokenRefreshService OAuth token自动刷新服务 // 定期检查并刷新即将过期的token type TokenRefreshService struct { - accountRepo ports.AccountRepository + accountRepo AccountRepository refreshers []TokenRefresher cfg *config.TokenRefreshConfig @@ -25,7 +24,7 @@ type TokenRefreshService struct { // NewTokenRefreshService 创建token刷新服务 func NewTokenRefreshService( - accountRepo ports.AccountRepository, + accountRepo AccountRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, cfg *config.Config, diff --git a/backend/internal/service/update_service.go b/backend/internal/service/update_service.go index a98f07e3..0c7e5a20 100644 --- a/backend/internal/service/update_service.go +++ b/backend/internal/service/update_service.go @@ -17,8 +17,6 @@ import ( "strconv" "strings" "time" - - "github.com/Wei-Shaw/sub2api/internal/service/ports" ) const ( @@ -34,6 +32,12 @@ const ( maxDownloadSize = 500 * 1024 * 1024 ) +// UpdateCache defines cache operations for update service +type UpdateCache interface { + GetUpdateInfo(ctx context.Context) (string, error) + SetUpdateInfo(ctx context.Context, data string, ttl time.Duration) error +} + // GitHubReleaseClient 获取 GitHub release 信息的接口 type GitHubReleaseClient interface { FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error) @@ -43,14 +47,14 @@ type GitHubReleaseClient interface { // UpdateService handles software updates type UpdateService struct { - cache ports.UpdateCache + cache UpdateCache githubClient GitHubReleaseClient currentVersion string buildType string // "source" for manual builds, "release" for CI builds } // NewUpdateService creates a new UpdateService -func NewUpdateService(cache ports.UpdateCache, githubClient GitHubReleaseClient, version, buildType string) *UpdateService { +func NewUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, version, buildType string) *UpdateService { return &UpdateService{ cache: cache, githubClient: githubClient, diff --git a/backend/internal/service/usage_service.go b/backend/internal/service/usage_service.go index a953c22a..33e41ea3 100644 --- a/backend/internal/service/usage_service.go +++ b/backend/internal/service/usage_service.go @@ -4,12 +4,11 @@ import ( "context" "errors" "fmt" + "time" + "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/service/ports" - "time" - "gorm.io/gorm" ) @@ -55,12 +54,12 @@ type UsageStats struct { // UsageService 使用统计服务 type UsageService struct { - usageRepo ports.UsageLogRepository - userRepo ports.UserRepository + usageRepo UsageLogRepository + userRepo UserRepository } // NewUsageService 创建使用统计服务实例 -func NewUsageService(usageRepo ports.UsageLogRepository, userRepo ports.UserRepository) *UsageService { +func NewUsageService(usageRepo UsageLogRepository, userRepo UserRepository) *UsageService { return &UsageService{ usageRepo: usageRepo, userRepo: userRepo, diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index f51630e1..14830b57 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -4,10 +4,9 @@ import ( "context" "errors" "fmt" + "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) @@ -18,6 +17,24 @@ var ( ErrInsufficientPerms = errors.New("insufficient permissions") ) +type UserRepository interface { + Create(ctx context.Context, user *model.User) error + GetByID(ctx context.Context, id int64) (*model.User, error) + GetByEmail(ctx context.Context, email string) (*model.User, error) + GetFirstAdmin(ctx context.Context) (*model.User, error) + Update(ctx context.Context, user *model.User) error + Delete(ctx context.Context, id int64) error + + List(ctx context.Context, params pagination.PaginationParams) ([]model.User, *pagination.PaginationResult, error) + ListWithFilters(ctx context.Context, params pagination.PaginationParams, status, role, search string) ([]model.User, *pagination.PaginationResult, error) + + UpdateBalance(ctx context.Context, id int64, amount float64) error + DeductBalance(ctx context.Context, id int64, amount float64) error + UpdateConcurrency(ctx context.Context, id int64, amount int) error + ExistsByEmail(ctx context.Context, email string) (bool, error) + RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) +} + // UpdateProfileRequest 更新用户资料请求 type UpdateProfileRequest struct { Email *string `json:"email"` @@ -34,11 +51,11 @@ type ChangePasswordRequest struct { // UserService 用户服务 type UserService struct { - userRepo ports.UserRepository + userRepo UserRepository } // NewUserService 创建用户服务实例 -func NewUserService(userRepo ports.UserRepository) *UserService { +func NewUserService(userRepo UserRepository) *UserService { return &UserService{ userRepo: userRepo, } diff --git a/backend/internal/service/ports/user_subscription.go b/backend/internal/service/user_subscription_port.go similarity index 99% rename from backend/internal/service/ports/user_subscription.go rename to backend/internal/service/user_subscription_port.go index 4181dbf8..615a4501 100644 --- a/backend/internal/service/ports/user_subscription.go +++ b/backend/internal/service/user_subscription_port.go @@ -1,4 +1,4 @@ -package ports +package service import ( "context" diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 998eaee6..050563a6 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -2,8 +2,6 @@ package service import ( "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/service/ports" - "github.com/google/wire" ) @@ -24,7 +22,7 @@ func ProvidePricingService(cfg *config.Config, remoteClient PricingRemoteClient) } // ProvideUpdateService creates UpdateService with BuildInfo -func ProvideUpdateService(cache ports.UpdateCache, githubClient GitHubReleaseClient, buildInfo BuildInfo) *UpdateService { +func ProvideUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, buildInfo BuildInfo) *UpdateService { return NewUpdateService(cache, githubClient, buildInfo.Version, buildInfo.BuildType) } @@ -35,7 +33,7 @@ func ProvideEmailQueueService(emailService *EmailService) *EmailQueueService { // ProvideTokenRefreshService creates and starts TokenRefreshService func ProvideTokenRefreshService( - accountRepo ports.AccountRepository, + accountRepo AccountRepository, oauthService *OAuthService, openaiOAuthService *OpenAIOAuthService, cfg *config.Config,