refactor: 删除 ports 目录

This commit is contained in:
Forest
2025-12-25 17:15:01 +08:00
parent f57f12c6cc
commit f51ad2e126
66 changed files with 519 additions and 664 deletions

View File

@@ -3,10 +3,11 @@ package repository
import ( import (
"context" "context"
"errors" "errors"
"time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"time"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "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. // BulkUpdate updates multiple accounts with the provided fields.
// It merges credentials/extra JSONB fields instead of overwriting them. // 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 { if len(ids) == 0 {
return 0, nil return 0, nil
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -513,7 +513,7 @@ func (s *AccountRepoSuite) TestBulkUpdate() {
a2 := mustCreateAccount(s.T(), s.db, &model.Account{Name: "bulk2", Priority: 1}) a2 := mustCreateAccount(s.T(), s.db, &model.Account{Name: "bulk2", Priority: 1})
newPriority := 99 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, Priority: &newPriority,
}) })
s.Require().NoError(err) s.Require().NoError(err)
@@ -531,7 +531,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeCredentials() {
Credentials: model.JSONB{"existing": "value"}, 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"}, Credentials: model.JSONB{"new_key": "new_value"},
}) })
s.Require().NoError(err) s.Require().NoError(err)
@@ -547,7 +547,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeExtra() {
Extra: model.JSONB{"existing": "val"}, 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"}, Extra: model.JSONB{"new_key": "new_val"},
}) })
s.Require().NoError(err) s.Require().NoError(err)
@@ -558,7 +558,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_MergeExtra() {
} }
func (s *AccountRepoSuite) TestBulkUpdate_EmptyIDs() { 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().NoError(err)
s.Require().Zero(affected) s.Require().Zero(affected)
} }
@@ -566,7 +566,7 @@ func (s *AccountRepoSuite) TestBulkUpdate_EmptyIDs() {
func (s *AccountRepoSuite) TestBulkUpdate_EmptyUpdates() { func (s *AccountRepoSuite) TestBulkUpdate_EmptyUpdates() {
a1 := mustCreateAccount(s.T(), s.db, &model.Account{Name: "bulk-empty"}) 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().NoError(err)
s.Require().Zero(affected) s.Require().Zero(affected)
} }

View File

@@ -5,8 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -19,7 +18,7 @@ type apiKeyCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewApiKeyCache(rdb *redis.Client) ports.ApiKeyCache { func NewApiKeyCache(rdb *redis.Client) service.ApiKeyCache {
return &apiKeyCache{rdb: rdb} return &apiKeyCache{rdb: rdb}
} }

View File

@@ -8,8 +8,7 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -58,7 +57,7 @@ type billingCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewBillingCache(rdb *redis.Client) ports.BillingCache { func NewBillingCache(rdb *redis.Client) service.BillingCache {
return &billingCache{rdb: rdb} return &billingCache{rdb: rdb}
} }
@@ -90,7 +89,7 @@ func (c *billingCache) InvalidateUserBalance(ctx context.Context, userID int64)
return c.rdb.Del(ctx, key).Err() 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) key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
result, err := c.rdb.HGetAll(ctx, key).Result() result, err := c.rdb.HGetAll(ctx, key).Result()
if err != nil { if err != nil {
@@ -102,8 +101,8 @@ func (c *billingCache) GetSubscriptionCache(ctx context.Context, userID, groupID
return c.parseSubscriptionCache(result) return c.parseSubscriptionCache(result)
} }
func (c *billingCache) parseSubscriptionCache(data map[string]string) (*ports.SubscriptionCacheData, error) { func (c *billingCache) parseSubscriptionCache(data map[string]string) (*service.SubscriptionCacheData, error) {
result := &ports.SubscriptionCacheData{} result := &service.SubscriptionCacheData{}
result.Status = data[subFieldStatus] result.Status = data[subFieldStatus]
if result.Status == "" { if result.Status == "" {
@@ -136,7 +135,7 @@ func (c *billingCache) parseSubscriptionCache(data map[string]string) (*ports.Su
return result, nil 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 { if data == nil {
return nil return nil
} }

View File

@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -21,18 +21,18 @@ type BillingCacheSuite struct {
func (s *BillingCacheSuite) TestUserBalance() { func (s *BillingCacheSuite) TestUserBalance() {
tests := []struct { tests := []struct {
name string 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", 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) _, err := cache.GetUserBalance(ctx, 1)
require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil for missing balance key") require.ErrorIs(s.T(), err, redis.Nil, "expected redis.Nil for missing balance key")
}, },
}, },
{ {
name: "deduct_on_nonexistent_is_noop", 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) userID := int64(1)
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
@@ -44,7 +44,7 @@ func (s *BillingCacheSuite) TestUserBalance() {
}, },
{ {
name: "set_and_get_with_ttl", 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) userID := int64(2)
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
@@ -61,7 +61,7 @@ func (s *BillingCacheSuite) TestUserBalance() {
}, },
{ {
name: "deduct_reduces_balance", 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) userID := int64(3)
require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 10.5), "SetUserBalance") require.NoError(s.T(), cache.SetUserBalance(ctx, userID, 10.5), "SetUserBalance")
@@ -74,7 +74,7 @@ func (s *BillingCacheSuite) TestUserBalance() {
}, },
{ {
name: "invalidate_removes_key", 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) userID := int64(100)
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
@@ -96,7 +96,7 @@ func (s *BillingCacheSuite) TestUserBalance() {
}, },
{ {
name: "deduct_refreshes_ttl", 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) userID := int64(103)
balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) balanceKey := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID)
@@ -133,11 +133,11 @@ func (s *BillingCacheSuite) TestUserBalance() {
func (s *BillingCacheSuite) TestSubscriptionCache() { func (s *BillingCacheSuite) TestSubscriptionCache() {
tests := []struct { tests := []struct {
name string 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", 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) userID := int64(10)
groupID := int64(20) groupID := int64(20)
@@ -147,7 +147,7 @@ func (s *BillingCacheSuite) TestSubscriptionCache() {
}, },
{ {
name: "update_usage_on_nonexistent_is_noop", 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) userID := int64(11)
groupID := int64(21) groupID := int64(21)
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
@@ -161,12 +161,12 @@ func (s *BillingCacheSuite) TestSubscriptionCache() {
}, },
{ {
name: "set_and_get_with_ttl", 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) userID := int64(12)
groupID := int64(22) groupID := int64(22)
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
data := &ports.SubscriptionCacheData{ data := &service.SubscriptionCacheData{
Status: "active", Status: "active",
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour),
DailyUsage: 1.0, DailyUsage: 1.0,
@@ -189,11 +189,11 @@ func (s *BillingCacheSuite) TestSubscriptionCache() {
}, },
{ {
name: "update_usage_increments_all_fields", 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) userID := int64(13)
groupID := int64(23) groupID := int64(23)
data := &ports.SubscriptionCacheData{ data := &service.SubscriptionCacheData{
Status: "active", Status: "active",
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour),
DailyUsage: 1.0, DailyUsage: 1.0,
@@ -214,12 +214,12 @@ func (s *BillingCacheSuite) TestSubscriptionCache() {
}, },
{ {
name: "invalidate_removes_key", 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) userID := int64(101)
groupID := int64(10) groupID := int64(10)
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)
data := &ports.SubscriptionCacheData{ data := &service.SubscriptionCacheData{
Status: "active", Status: "active",
ExpiresAt: time.Now().Add(1 * time.Hour), ExpiresAt: time.Now().Add(1 * time.Hour),
DailyUsage: 1.0, DailyUsage: 1.0,
@@ -245,7 +245,7 @@ func (s *BillingCacheSuite) TestSubscriptionCache() {
}, },
{ {
name: "missing_status_returns_parsing_error", 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) userID := int64(102)
groupID := int64(11) groupID := int64(11)
subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) subKey := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID)

View File

@@ -5,8 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -107,7 +106,7 @@ type concurrencyCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewConcurrencyCache(rdb *redis.Client) ports.ConcurrencyCache { func NewConcurrencyCache(rdb *redis.Client) service.ConcurrencyCache {
return &concurrencyCache{rdb: rdb} return &concurrencyCache{rdb: rdb}
} }

View File

@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -16,7 +16,7 @@ import (
type ConcurrencyCacheSuite struct { type ConcurrencyCacheSuite struct {
IntegrationRedisSuite IntegrationRedisSuite
cache ports.ConcurrencyCache cache service.ConcurrencyCache
} }
func (s *ConcurrencyCacheSuite) SetupTest() { func (s *ConcurrencyCacheSuite) SetupTest() {

View File

@@ -5,8 +5,7 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -16,24 +15,24 @@ type emailCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewEmailCache(rdb *redis.Client) ports.EmailCache { func NewEmailCache(rdb *redis.Client) service.EmailCache {
return &emailCache{rdb: rdb} 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 key := verifyCodeKeyPrefix + email
val, err := c.rdb.Get(ctx, key).Result() val, err := c.rdb.Get(ctx, key).Result()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var data ports.VerificationCodeData var data service.VerificationCodeData
if err := json.Unmarshal([]byte(val), &data); err != nil { if err := json.Unmarshal([]byte(val), &data); err != nil {
return nil, err return nil, err
} }
return &data, nil 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 key := verifyCodeKeyPrefix + email
val, err := json.Marshal(data) val, err := json.Marshal(data)
if err != nil { if err != nil {

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -15,7 +15,7 @@ import (
type EmailCacheSuite struct { type EmailCacheSuite struct {
IntegrationRedisSuite IntegrationRedisSuite
cache ports.EmailCache cache service.EmailCache
} }
func (s *EmailCacheSuite) SetupTest() { func (s *EmailCacheSuite) SetupTest() {
@@ -31,7 +31,7 @@ func (s *EmailCacheSuite) TestGetVerificationCode_Missing() {
func (s *EmailCacheSuite) TestSetAndGetVerificationCode() { func (s *EmailCacheSuite) TestSetAndGetVerificationCode() {
email := "a@example.com" email := "a@example.com"
emailTTL := 2 * time.Minute 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") 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() { func (s *EmailCacheSuite) TestVerificationCode_TTL() {
email := "ttl@example.com" email := "ttl@example.com"
emailTTL := 2 * time.Minute 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") 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() { func (s *EmailCacheSuite) TestDeleteVerificationCode() {
email := "delete@example.com" 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") require.NoError(s.T(), s.cache.SetVerificationCode(s.ctx, email, data, 2*time.Minute), "SetVerificationCode")

View File

@@ -4,8 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -15,7 +14,7 @@ type gatewayCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewGatewayCache(rdb *redis.Client) ports.GatewayCache { func NewGatewayCache(rdb *redis.Client) service.GatewayCache {
return &gatewayCache{rdb: rdb} return &gatewayCache{rdb: rdb}
} }

View File

@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -15,7 +15,7 @@ import (
type GatewayCacheSuite struct { type GatewayCacheSuite struct {
IntegrationRedisSuite IntegrationRedisSuite
cache ports.GatewayCache cache service.GatewayCache
} }
func (s *GatewayCacheSuite) SetupTest() { func (s *GatewayCacheSuite) SetupTest() {

View File

@@ -6,7 +6,7 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/config" "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 // 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 // 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 responseHeaderTimeout := time.Duration(cfg.Gateway.ResponseHeaderTimeout) * time.Second
if responseHeaderTimeout == 0 { if responseHeaderTimeout == 0 {
responseHeaderTimeout = 300 * time.Second responseHeaderTimeout = 300 * time.Second

View File

@@ -6,8 +6,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -20,24 +19,24 @@ type identityCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewIdentityCache(rdb *redis.Client) ports.IdentityCache { func NewIdentityCache(rdb *redis.Client) service.IdentityCache {
return &identityCache{rdb: rdb} 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) key := fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID)
val, err := c.rdb.Get(ctx, key).Result() val, err := c.rdb.Get(ctx, key).Result()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var fp ports.Fingerprint var fp service.Fingerprint
if err := json.Unmarshal([]byte(val), &fp); err != nil { if err := json.Unmarshal([]byte(val), &fp); err != nil {
return nil, err return nil, err
} }
return &fp, nil 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) key := fmt.Sprintf("%s%d", fingerprintKeyPrefix, accountID)
val, err := json.Marshal(fp) val, err := json.Marshal(fp)
if err != nil { if err != nil {

View File

@@ -8,7 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -30,7 +30,7 @@ func (s *IdentityCacheSuite) TestGetFingerprint_Missing() {
} }
func (s *IdentityCacheSuite) TestSetAndGetFingerprint() { 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") require.NoError(s.T(), s.cache.SetFingerprint(s.ctx, 1, fp), "SetFingerprint")
gotFP, err := s.cache.GetFingerprint(s.ctx, 1) gotFP, err := s.cache.GetFingerprint(s.ctx, 1)
require.NoError(s.T(), err, "GetFingerprint") require.NoError(s.T(), err, "GetFingerprint")
@@ -39,7 +39,7 @@ func (s *IdentityCacheSuite) TestSetAndGetFingerprint() {
} }
func (s *IdentityCacheSuite) TestFingerprint_TTL() { 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)) require.NoError(s.T(), s.cache.SetFingerprint(s.ctx, 2, fp))
fpKey := fmt.Sprintf("%s%d", fingerprintKeyPrefix, 2) fpKey := fmt.Sprintf("%s%d", fingerprintKeyPrefix, 2)

View File

@@ -7,13 +7,12 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "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" "github.com/imroc/req/v3"
) )
// NewOpenAIOAuthClient creates a new OpenAI OAuth client // NewOpenAIOAuthClient creates a new OpenAI OAuth client
func NewOpenAIOAuthClient() ports.OpenAIOAuthClient { func NewOpenAIOAuthClient() service.OpenAIOAuthClient {
return &openaiOAuthService{tokenURL: openai.TokenURL} return &openaiOAuthService{tokenURL: openai.TokenURL}
} }

View File

@@ -5,8 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -20,7 +19,7 @@ type redeemCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewRedeemCache(rdb *redis.Client) ports.RedeemCache { func NewRedeemCache(rdb *redis.Client) service.RedeemCache {
return &redeemCache{rdb: rdb} return &redeemCache{rdb: rdb}
} }

View File

@@ -4,8 +4,7 @@ import (
"context" "context"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
@@ -15,7 +14,7 @@ type updateCache struct {
rdb *redis.Client rdb *redis.Client
} }
func NewUpdateCache(rdb *redis.Client) ports.UpdateCache { func NewUpdateCache(rdb *redis.Client) service.UpdateCache {
return &updateCache{rdb: rdb} return &updateCache{rdb: rdb}
} }

View File

@@ -1,8 +1,7 @@
package repository package repository
import ( import (
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/google/wire" "github.com/google/wire"
) )
@@ -40,13 +39,13 @@ var ProviderSet = wire.NewSet(
NewOpenAIOAuthClient, NewOpenAIOAuthClient,
// Bind concrete repositories to service port interfaces // Bind concrete repositories to service port interfaces
wire.Bind(new(ports.UserRepository), new(*UserRepository)), wire.Bind(new(service.UserRepository), new(*UserRepository)),
wire.Bind(new(ports.ApiKeyRepository), new(*ApiKeyRepository)), wire.Bind(new(service.ApiKeyRepository), new(*ApiKeyRepository)),
wire.Bind(new(ports.GroupRepository), new(*GroupRepository)), wire.Bind(new(service.GroupRepository), new(*GroupRepository)),
wire.Bind(new(ports.AccountRepository), new(*AccountRepository)), wire.Bind(new(service.AccountRepository), new(*AccountRepository)),
wire.Bind(new(ports.ProxyRepository), new(*ProxyRepository)), wire.Bind(new(service.ProxyRepository), new(*ProxyRepository)),
wire.Bind(new(ports.RedeemCodeRepository), new(*RedeemCodeRepository)), wire.Bind(new(service.RedeemCodeRepository), new(*RedeemCodeRepository)),
wire.Bind(new(ports.UsageLogRepository), new(*UsageLogRepository)), wire.Bind(new(service.UsageLogRepository), new(*UsageLogRepository)),
wire.Bind(new(ports.SettingRepository), new(*SettingRepository)), wire.Bind(new(service.SettingRepository), new(*SettingRepository)),
wire.Bind(new(ports.UserSubscriptionRepository), new(*UserSubscriptionRepository)), wire.Bind(new(service.UserSubscriptionRepository), new(*UserSubscriptionRepository)),
) )

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -15,6 +15,51 @@ var (
ErrAccountNotFound = errors.New("account not found") 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 创建账号请求 // CreateAccountRequest 创建账号请求
type CreateAccountRequest struct { type CreateAccountRequest struct {
Name string `json:"name"` Name string `json:"name"`
@@ -42,12 +87,12 @@ type UpdateAccountRequest struct {
// AccountService 账号管理服务 // AccountService 账号管理服务
type AccountService struct { type AccountService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
groupRepo ports.GroupRepository groupRepo GroupRepository
} }
// NewAccountService 创建账号服务实例 // NewAccountService 创建账号服务实例
func NewAccountService(accountRepo ports.AccountRepository, groupRepo ports.GroupRepository) *AccountService { func NewAccountService(accountRepo AccountRepository, groupRepo GroupRepository) *AccountService {
return &AccountService{ return &AccountService{
accountRepo: accountRepo, accountRepo: accountRepo,
groupRepo: groupRepo, groupRepo: groupRepo,

View File

@@ -17,8 +17,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -40,14 +38,14 @@ type TestEvent struct {
// AccountTestService handles account testing operations // AccountTestService handles account testing operations
type AccountTestService struct { type AccountTestService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
oauthService *OAuthService oauthService *OAuthService
openaiOAuthService *OpenAIOAuthService openaiOAuthService *OpenAIOAuthService
httpUpstream ports.HTTPUpstream httpUpstream HTTPUpstream
} }
// NewAccountTestService creates a new AccountTestService // 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{ return &AccountTestService{
accountRepo: accountRepo, accountRepo: accountRepo,
oauthService: oauthService, oauthService: oauthService,

View File

@@ -8,10 +8,49 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
type UsageLogRepository interface {
Create(ctx context.Context, log *model.UsageLog) error
GetByID(ctx context.Context, id int64) (*model.UsageLog, error)
Delete(ctx context.Context, id int64) error
ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByApiKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]model.UsageLog, *pagination.PaginationResult, error)
GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error)
GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error)
// Admin dashboard stats
GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error)
GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID int64) ([]usagestats.TrendDataPoint, error)
GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) ([]usagestats.ModelStat, error)
GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.ApiKeyUsageTrendPoint, error)
GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error)
GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*usagestats.BatchUserUsageStats, error)
GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*usagestats.BatchApiKeyUsageStats, error)
// User dashboard stats
GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error)
GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error)
GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error)
// Admin usage listing/stats
ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]model.UsageLog, *pagination.PaginationResult, error)
GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error)
// Account stats
GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error)
}
// usageCache 用于缓存usage数据 // usageCache 用于缓存usage数据
type usageCache struct { type usageCache struct {
data *UsageInfo data *UsageInfo
@@ -69,13 +108,13 @@ type ClaudeUsageFetcher interface {
// AccountUsageService 账号使用量查询服务 // AccountUsageService 账号使用量查询服务
type AccountUsageService struct { type AccountUsageService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
usageFetcher ClaudeUsageFetcher usageFetcher ClaudeUsageFetcher
} }
// NewAccountUsageService 创建AccountUsageService实例 // NewAccountUsageService 创建AccountUsageService实例
func NewAccountUsageService(accountRepo ports.AccountRepository, usageLogRepo ports.UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService { func NewAccountUsageService(accountRepo AccountRepository, usageLogRepo UsageLogRepository, usageFetcher ClaudeUsageFetcher) *AccountUsageService {
return &AccountUsageService{ return &AccountUsageService{
accountRepo: accountRepo, accountRepo: accountRepo,
usageLogRepo: usageLogRepo, usageLogRepo: usageLogRepo,

View File

@@ -9,8 +9,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -221,24 +219,24 @@ type ProxyExitInfoProber interface {
// adminServiceImpl implements AdminService // adminServiceImpl implements AdminService
type adminServiceImpl struct { type adminServiceImpl struct {
userRepo ports.UserRepository userRepo UserRepository
groupRepo ports.GroupRepository groupRepo GroupRepository
accountRepo ports.AccountRepository accountRepo AccountRepository
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
apiKeyRepo ports.ApiKeyRepository apiKeyRepo ApiKeyRepository
redeemCodeRepo ports.RedeemCodeRepository redeemCodeRepo RedeemCodeRepository
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
proxyProber ProxyExitInfoProber proxyProber ProxyExitInfoProber
} }
// NewAdminService creates a new AdminService // NewAdminService creates a new AdminService
func NewAdminService( func NewAdminService(
userRepo ports.UserRepository, userRepo UserRepository,
groupRepo ports.GroupRepository, groupRepo GroupRepository,
accountRepo ports.AccountRepository, accountRepo AccountRepository,
proxyRepo ports.ProxyRepository, proxyRepo ProxyRepository,
apiKeyRepo ports.ApiKeyRepository, apiKeyRepo ApiKeyRepository,
redeemCodeRepo ports.RedeemCodeRepository, redeemCodeRepo RedeemCodeRepository,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
proxyProber ProxyExitInfoProber, proxyProber ProxyExitInfoProber,
) AdminService { ) AdminService {
@@ -734,7 +732,7 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
} }
// Prepare bulk updates for columns and JSONB fields. // Prepare bulk updates for columns and JSONB fields.
repoUpdates := ports.AccountBulkUpdate{ repoUpdates := AccountBulkUpdate{
Credentials: input.Credentials, Credentials: input.Credentials,
Extra: input.Extra, Extra: input.Extra,
} }

View File

@@ -6,13 +6,12 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -30,6 +29,32 @@ const (
apiKeyMaxErrorsPerHour = 20 apiKeyMaxErrorsPerHour = 20
) )
type ApiKeyRepository interface {
Create(ctx context.Context, key *model.ApiKey) error
GetByID(ctx context.Context, id int64) (*model.ApiKey, error)
GetByKey(ctx context.Context, key string) (*model.ApiKey, error)
Update(ctx context.Context, key *model.ApiKey) error
Delete(ctx context.Context, id int64) error
ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
CountByUserID(ctx context.Context, userID int64) (int64, error)
ExistsByKey(ctx context.Context, key string) (bool, error)
ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]model.ApiKey, *pagination.PaginationResult, error)
SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error)
ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error)
CountByGroupID(ctx context.Context, groupID int64) (int64, error)
}
// ApiKeyCache defines cache operations for API key service
type ApiKeyCache interface {
GetCreateAttemptCount(ctx context.Context, userID int64) (int, error)
IncrementCreateAttemptCount(ctx context.Context, userID int64) error
DeleteCreateAttemptCount(ctx context.Context, userID int64) error
IncrementDailyUsage(ctx context.Context, apiKey string) error
SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error
}
// CreateApiKeyRequest 创建API Key请求 // CreateApiKeyRequest 创建API Key请求
type CreateApiKeyRequest struct { type CreateApiKeyRequest struct {
Name string `json:"name"` Name string `json:"name"`
@@ -46,21 +71,21 @@ type UpdateApiKeyRequest struct {
// ApiKeyService API Key服务 // ApiKeyService API Key服务
type ApiKeyService struct { type ApiKeyService struct {
apiKeyRepo ports.ApiKeyRepository apiKeyRepo ApiKeyRepository
userRepo ports.UserRepository userRepo UserRepository
groupRepo ports.GroupRepository groupRepo GroupRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.ApiKeyCache cache ApiKeyCache
cfg *config.Config cfg *config.Config
} }
// NewApiKeyService 创建API Key服务实例 // NewApiKeyService 创建API Key服务实例
func NewApiKeyService( func NewApiKeyService(
apiKeyRepo ports.ApiKeyRepository, apiKeyRepo ApiKeyRepository,
userRepo ports.UserRepository, userRepo UserRepository,
groupRepo ports.GroupRepository, groupRepo GroupRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.ApiKeyCache, cache ApiKeyCache,
cfg *config.Config, cfg *config.Config,
) *ApiKeyService { ) *ApiKeyService {
return &ApiKeyService{ return &ApiKeyService{

View File

@@ -4,12 +4,12 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"log" "log"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -36,7 +36,7 @@ type JWTClaims struct {
// AuthService 认证服务 // AuthService 认证服务
type AuthService struct { type AuthService struct {
userRepo ports.UserRepository userRepo UserRepository
cfg *config.Config cfg *config.Config
settingService *SettingService settingService *SettingService
emailService *EmailService emailService *EmailService
@@ -46,7 +46,7 @@ type AuthService struct {
// NewAuthService 创建认证服务实例 // NewAuthService 创建认证服务实例
func NewAuthService( func NewAuthService(
userRepo ports.UserRepository, userRepo UserRepository,
cfg *config.Config, cfg *config.Config,
settingService *SettingService, settingService *SettingService,
emailService *EmailService, emailService *EmailService,

View File

@@ -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
}

View File

@@ -8,7 +8,6 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// 错误定义 // 错误定义
@@ -31,13 +30,13 @@ type subscriptionCacheData struct {
// BillingCacheService 计费缓存服务 // BillingCacheService 计费缓存服务
// 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查 // 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查
type BillingCacheService struct { type BillingCacheService struct {
cache ports.BillingCache cache BillingCache
userRepo ports.UserRepository userRepo UserRepository
subRepo ports.UserSubscriptionRepository subRepo UserSubscriptionRepository
} }
// NewBillingCacheService 创建计费缓存服务 // NewBillingCacheService 创建计费缓存服务
func NewBillingCacheService(cache ports.BillingCache, userRepo ports.UserRepository, subRepo ports.UserSubscriptionRepository) *BillingCacheService { func NewBillingCacheService(cache BillingCache, userRepo UserRepository, subRepo UserSubscriptionRepository) *BillingCacheService {
return &BillingCacheService{ return &BillingCacheService{
cache: cache, cache: cache,
userRepo: userRepo, userRepo: userRepo,
@@ -149,7 +148,7 @@ func (s *BillingCacheService) GetSubscriptionStatus(ctx context.Context, userID,
return data, nil return data, nil
} }
func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCacheData) *subscriptionCacheData { func (s *BillingCacheService) convertFromPortsData(data *SubscriptionCacheData) *subscriptionCacheData {
return &subscriptionCacheData{ return &subscriptionCacheData{
Status: data.Status, Status: data.Status,
ExpiresAt: data.ExpiresAt, ExpiresAt: data.ExpiresAt,
@@ -160,8 +159,8 @@ func (s *BillingCacheService) convertFromPortsData(data *ports.SubscriptionCache
} }
} }
func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *ports.SubscriptionCacheData { func (s *BillingCacheService) convertToPortsData(data *subscriptionCacheData) *SubscriptionCacheData {
return &ports.SubscriptionCacheData{ return &SubscriptionCacheData{
Status: data.Status, Status: data.Status,
ExpiresAt: data.ExpiresAt, ExpiresAt: data.ExpiresAt,
DailyUsage: data.DailyUsage, DailyUsage: data.DailyUsage,

View File

@@ -1,12 +1,30 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/config"
"log" "log"
"strings" "strings"
"github.com/Wei-Shaw/sub2api/internal/config"
) )
// BillingCache defines cache operations for billing service
type BillingCache interface {
// Balance operations
GetUserBalance(ctx context.Context, userID int64) (float64, error)
SetUserBalance(ctx context.Context, userID int64, balance float64) error
DeductUserBalance(ctx context.Context, userID int64, amount float64) error
InvalidateUserBalance(ctx context.Context, userID int64) error
// Subscription operations
GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error)
SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error
UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error
InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error
}
// ModelPricing 模型价格配置per-token价格与LiteLLM格式一致 // ModelPricing 模型价格配置per-token价格与LiteLLM格式一致
type ModelPricing struct { type ModelPricing struct {
InputPricePerToken float64 // 每token输入价格 (USD) InputPricePerToken float64 // 每token输入价格 (USD)

View File

@@ -7,10 +7,28 @@ import (
"fmt" "fmt"
"log" "log"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// ConcurrencyCache defines cache operations for concurrency service
// Uses independent keys per request slot with native Redis TTL for automatic cleanup
type ConcurrencyCache interface {
// Account slot management - each slot is a separate key with independent TTL
// Key format: concurrency:account:{accountID}:{requestID}
AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int, requestID string) (bool, error)
ReleaseAccountSlot(ctx context.Context, accountID int64, requestID string) error
GetAccountConcurrency(ctx context.Context, accountID int64) (int, error)
// User slot management - each slot is a separate key with independent TTL
// Key format: concurrency:user:{userID}:{requestID}
AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int, requestID string) (bool, error)
ReleaseUserSlot(ctx context.Context, userID int64, requestID string) error
GetUserConcurrency(ctx context.Context, userID int64) (int, error)
// Wait queue - uses counter with TTL set only on creation
IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error)
DecrementWaitCount(ctx context.Context, userID int64) error
}
// generateRequestID generates a unique request ID for concurrency slot tracking // generateRequestID generates a unique request ID for concurrency slot tracking
// Uses 8 random bytes (16 hex chars) for uniqueness // Uses 8 random bytes (16 hex chars) for uniqueness
func generateRequestID() string { func generateRequestID() string {
@@ -29,11 +47,11 @@ const (
// ConcurrencyService manages concurrent request limiting for accounts and users // ConcurrencyService manages concurrent request limiting for accounts and users
type ConcurrencyService struct { type ConcurrencyService struct {
cache ports.ConcurrencyCache cache ConcurrencyCache
} }
// NewConcurrencyService creates a new ConcurrencyService // NewConcurrencyService creates a new ConcurrencyService
func NewConcurrencyService(cache ports.ConcurrencyCache) *ConcurrencyService { func NewConcurrencyService(cache ConcurrencyCache) *ConcurrencyService {
return &ConcurrencyService{cache: cache} return &ConcurrencyService{cache: cache}
} }

View File

@@ -13,19 +13,18 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
type CRSSyncService struct { type CRSSyncService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthService *OAuthService oauthService *OAuthService
openaiOAuthService *OpenAIOAuthService openaiOAuthService *OpenAIOAuthService
} }
func NewCRSSyncService( func NewCRSSyncService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
proxyRepo ports.ProxyRepository, proxyRepo ProxyRepository,
oauthService *OAuthService, oauthService *OAuthService,
openaiOAuthService *OpenAIOAuthService, openaiOAuthService *OpenAIOAuthService,
) *CRSSyncService { ) *CRSSyncService {

View File

@@ -6,15 +6,14 @@ import (
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// DashboardService provides aggregated statistics for admin dashboard. // DashboardService provides aggregated statistics for admin dashboard.
type DashboardService struct { type DashboardService struct {
usageRepo ports.UsageLogRepository usageRepo UsageLogRepository
} }
func NewDashboardService(usageRepo ports.UsageLogRepository) *DashboardService { func NewDashboardService(usageRepo UsageLogRepository) *DashboardService {
return &DashboardService{ return &DashboardService{
usageRepo: usageRepo, usageRepo: usageRepo,
} }

View File

@@ -6,12 +6,12 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"math/big" "math/big"
"net/smtp" "net/smtp"
"strconv" "strconv"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model"
) )
var ( var (
@@ -21,6 +21,20 @@ var (
ErrVerifyCodeMaxAttempts = errors.New("too many failed attempts, please request a new code") ErrVerifyCodeMaxAttempts = errors.New("too many failed attempts, please request a new code")
) )
// EmailCache defines cache operations for email service
type EmailCache interface {
GetVerificationCode(ctx context.Context, email string) (*VerificationCodeData, error)
SetVerificationCode(ctx context.Context, email string, data *VerificationCodeData, ttl time.Duration) error
DeleteVerificationCode(ctx context.Context, email string) error
}
// VerificationCodeData represents verification code data
type VerificationCodeData struct {
Code string
Attempts int
CreatedAt time.Time
}
const ( const (
verifyCodeTTL = 15 * time.Minute verifyCodeTTL = 15 * time.Minute
verifyCodeCooldown = 1 * time.Minute verifyCodeCooldown = 1 * time.Minute
@@ -40,12 +54,12 @@ type SmtpConfig struct {
// EmailService 邮件服务 // EmailService 邮件服务
type EmailService struct { type EmailService struct {
settingRepo ports.SettingRepository settingRepo SettingRepository
cache ports.EmailCache cache EmailCache
} }
// NewEmailService 创建邮件服务实例 // NewEmailService 创建邮件服务实例
func NewEmailService(settingRepo ports.SettingRepository, cache ports.EmailCache) *EmailService { func NewEmailService(settingRepo SettingRepository, cache EmailCache) *EmailService {
return &EmailService{ return &EmailService{
settingRepo: settingRepo, settingRepo: settingRepo,
cache: cache, cache: cache,
@@ -205,7 +219,7 @@ func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName strin
} }
// 保存验证码到 Redis // 保存验证码到 Redis
data := &ports.VerificationCodeData{ data := &VerificationCodeData{
Code: code, Code: code,
Attempts: 0, Attempts: 0,
CreatedAt: time.Now(), CreatedAt: time.Now(),

View File

@@ -19,7 +19,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
@@ -54,6 +53,13 @@ var allowedHeaders = map[string]bool{
"content-type": true, "content-type": true,
} }
// GatewayCache defines cache operations for gateway service
type GatewayCache interface {
GetSessionAccountID(ctx context.Context, sessionHash string) (int64, error)
SetSessionAccountID(ctx context.Context, sessionHash string, accountID int64, ttl time.Duration) error
RefreshSessionTTL(ctx context.Context, sessionHash string, ttl time.Duration) error
}
// ClaudeUsage 表示Claude API返回的usage信息 // ClaudeUsage 表示Claude API返回的usage信息
type ClaudeUsage struct { type ClaudeUsage struct {
InputTokens int `json:"input_tokens"` InputTokens int `json:"input_tokens"`
@@ -74,32 +80,32 @@ type ForwardResult struct {
// GatewayService handles API gateway operations // GatewayService handles API gateway operations
type GatewayService struct { type GatewayService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
userRepo ports.UserRepository userRepo UserRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.GatewayCache cache GatewayCache
cfg *config.Config cfg *config.Config
billingService *BillingService billingService *BillingService
rateLimitService *RateLimitService rateLimitService *RateLimitService
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
identityService *IdentityService identityService *IdentityService
httpUpstream ports.HTTPUpstream httpUpstream HTTPUpstream
} }
// NewGatewayService creates a new GatewayService // NewGatewayService creates a new GatewayService
func NewGatewayService( func NewGatewayService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
usageLogRepo ports.UsageLogRepository, usageLogRepo UsageLogRepository,
userRepo ports.UserRepository, userRepo UserRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.GatewayCache, cache GatewayCache,
cfg *config.Config, cfg *config.Config,
billingService *BillingService, billingService *BillingService,
rateLimitService *RateLimitService, rateLimitService *RateLimitService,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
identityService *IdentityService, identityService *IdentityService,
httpUpstream ports.HTTPUpstream, httpUpstream HTTPUpstream,
) *GatewayService { ) *GatewayService {
return &GatewayService{ return &GatewayService{
accountRepo: accountRepo, accountRepo: accountRepo,
@@ -507,7 +513,7 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex
} }
// OAuth账号应用统一指纹 // OAuth账号应用统一指纹
var fingerprint *ports.Fingerprint var fingerprint *Fingerprint
if account.IsOAuth() && s.identityService != nil { if account.IsOAuth() && s.identityService != nil {
// 1. 获取或创建指纹包含随机生成的ClientID // 1. 获取或创建指纹包含随机生成的ClientID
fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header)

View File

@@ -4,10 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -16,6 +15,24 @@ var (
ErrGroupExists = errors.New("group name already exists") ErrGroupExists = errors.New("group name already exists")
) )
type GroupRepository interface {
Create(ctx context.Context, group *model.Group) error
GetByID(ctx context.Context, id int64) (*model.Group, error)
Update(ctx context.Context, group *model.Group) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, params pagination.PaginationParams) ([]model.Group, *pagination.PaginationResult, error)
ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]model.Group, *pagination.PaginationResult, error)
ListActive(ctx context.Context) ([]model.Group, error)
ListActiveByPlatform(ctx context.Context, platform string) ([]model.Group, error)
ExistsByName(ctx context.Context, name string) (bool, error)
GetAccountCount(ctx context.Context, groupID int64) (int64, error)
DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error)
DB() *gorm.DB
}
// CreateGroupRequest 创建分组请求 // CreateGroupRequest 创建分组请求
type CreateGroupRequest struct { type CreateGroupRequest struct {
Name string `json:"name"` Name string `json:"name"`
@@ -35,11 +52,11 @@ type UpdateGroupRequest struct {
// GroupService 分组管理服务 // GroupService 分组管理服务
type GroupService struct { type GroupService struct {
groupRepo ports.GroupRepository groupRepo GroupRepository
} }
// NewGroupService 创建分组服务实例 // NewGroupService 创建分组服务实例
func NewGroupService(groupRepo ports.GroupRepository) *GroupService { func NewGroupService(groupRepo GroupRepository) *GroupService {
return &GroupService{ return &GroupService{
groupRepo: groupRepo, groupRepo: groupRepo,
} }

View File

@@ -1,4 +1,4 @@
package ports package service
import "net/http" import "net/http"

View File

@@ -7,7 +7,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"log" "log"
"net/http" "net/http"
"regexp" "regexp"
@@ -24,7 +23,7 @@ var (
) )
// 默认指纹值(当客户端未提供时使用) // 默认指纹值(当客户端未提供时使用)
var defaultFingerprint = ports.Fingerprint{ var defaultFingerprint = Fingerprint{
UserAgent: "claude-cli/2.0.62 (external, cli)", UserAgent: "claude-cli/2.0.62 (external, cli)",
StainlessLang: "js", StainlessLang: "js",
StainlessPackageVersion: "0.52.0", StainlessPackageVersion: "0.52.0",
@@ -34,20 +33,38 @@ var defaultFingerprint = ports.Fingerprint{
StainlessRuntimeVersion: "v22.14.0", StainlessRuntimeVersion: "v22.14.0",
} }
// Fingerprint represents account fingerprint data
type Fingerprint struct {
ClientID string
UserAgent string
StainlessLang string
StainlessPackageVersion string
StainlessOS string
StainlessArch string
StainlessRuntime string
StainlessRuntimeVersion string
}
// IdentityCache defines cache operations for identity service
type IdentityCache interface {
GetFingerprint(ctx context.Context, accountID int64) (*Fingerprint, error)
SetFingerprint(ctx context.Context, accountID int64, fp *Fingerprint) error
}
// IdentityService 管理OAuth账号的请求身份指纹 // IdentityService 管理OAuth账号的请求身份指纹
type IdentityService struct { type IdentityService struct {
cache ports.IdentityCache cache IdentityCache
} }
// NewIdentityService 创建新的IdentityService // NewIdentityService 创建新的IdentityService
func NewIdentityService(cache ports.IdentityCache) *IdentityService { func NewIdentityService(cache IdentityCache) *IdentityService {
return &IdentityService{cache: cache} return &IdentityService{cache: cache}
} }
// GetOrCreateFingerprint 获取或创建账号的指纹 // GetOrCreateFingerprint 获取或创建账号的指纹
// 如果缓存存在检测user-agent版本新版本则更新 // 如果缓存存在检测user-agent版本新版本则更新
// 如果缓存不存在生成随机ClientID并从请求头创建指纹然后缓存 // 如果缓存不存在生成随机ClientID并从请求头创建指纹然后缓存
func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*ports.Fingerprint, error) { func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*Fingerprint, error) {
// 尝试从缓存获取指纹 // 尝试从缓存获取指纹
cached, err := s.cache.GetFingerprint(ctx, accountID) cached, err := s.cache.GetFingerprint(ctx, accountID)
if err == nil && cached != nil { if err == nil && cached != nil {
@@ -79,8 +96,8 @@ func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID
} }
// createFingerprintFromHeaders 从请求头创建指纹 // createFingerprintFromHeaders 从请求头创建指纹
func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *ports.Fingerprint { func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fingerprint {
fp := &ports.Fingerprint{} fp := &Fingerprint{}
// 获取User-Agent // 获取User-Agent
if ua := headers.Get("User-Agent"); ua != "" { if ua := headers.Get("User-Agent"); ua != "" {
@@ -109,7 +126,7 @@ func getHeaderOrDefault(headers http.Header, key, defaultValue string) string {
} }
// ApplyFingerprint 将指纹应用到请求头覆盖原有的x-stainless-*头) // ApplyFingerprint 将指纹应用到请求头覆盖原有的x-stainless-*头)
func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *ports.Fingerprint) { func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) {
if fp == nil { if fp == nil {
return return
} }

View File

@@ -8,9 +8,15 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth" "github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/Wei-Shaw/sub2api/internal/service/ports" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
) )
// OpenAIOAuthClient interface for OpenAI OAuth operations
type OpenAIOAuthClient interface {
ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI, proxyURL string) (*openai.TokenResponse, error)
RefreshToken(ctx context.Context, refreshToken, proxyURL string) (*openai.TokenResponse, error)
}
// ClaudeOAuthClient handles HTTP requests for Claude OAuth flows // ClaudeOAuthClient handles HTTP requests for Claude OAuth flows
type ClaudeOAuthClient interface { type ClaudeOAuthClient interface {
GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error) GetOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error)
@@ -22,12 +28,12 @@ type ClaudeOAuthClient interface {
// OAuthService handles OAuth authentication flows // OAuthService handles OAuth authentication flows
type OAuthService struct { type OAuthService struct {
sessionStore *oauth.SessionStore sessionStore *oauth.SessionStore
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthClient ClaudeOAuthClient oauthClient ClaudeOAuthClient
} }
// NewOAuthService creates a new OAuth service // NewOAuthService creates a new OAuth service
func NewOAuthService(proxyRepo ports.ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService { func NewOAuthService(proxyRepo ProxyRepository, oauthClient ClaudeOAuthClient) *OAuthService {
return &OAuthService{ return &OAuthService{
sessionStore: oauth.NewSessionStore(), sessionStore: oauth.NewSessionStore(),
proxyRepo: proxyRepo, proxyRepo: proxyRepo,

View File

@@ -17,8 +17,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -71,30 +69,30 @@ type OpenAIForwardResult struct {
// OpenAIGatewayService handles OpenAI API gateway operations // OpenAIGatewayService handles OpenAI API gateway operations
type OpenAIGatewayService struct { type OpenAIGatewayService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
usageLogRepo ports.UsageLogRepository usageLogRepo UsageLogRepository
userRepo ports.UserRepository userRepo UserRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
cache ports.GatewayCache cache GatewayCache
cfg *config.Config cfg *config.Config
billingService *BillingService billingService *BillingService
rateLimitService *RateLimitService rateLimitService *RateLimitService
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
httpUpstream ports.HTTPUpstream httpUpstream HTTPUpstream
} }
// NewOpenAIGatewayService creates a new OpenAIGatewayService // NewOpenAIGatewayService creates a new OpenAIGatewayService
func NewOpenAIGatewayService( func NewOpenAIGatewayService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
usageLogRepo ports.UsageLogRepository, usageLogRepo UsageLogRepository,
userRepo ports.UserRepository, userRepo UserRepository,
userSubRepo ports.UserSubscriptionRepository, userSubRepo UserSubscriptionRepository,
cache ports.GatewayCache, cache GatewayCache,
cfg *config.Config, cfg *config.Config,
billingService *BillingService, billingService *BillingService,
rateLimitService *RateLimitService, rateLimitService *RateLimitService,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
httpUpstream ports.HTTPUpstream, httpUpstream HTTPUpstream,
) *OpenAIGatewayService { ) *OpenAIGatewayService {
return &OpenAIGatewayService{ return &OpenAIGatewayService{
accountRepo: accountRepo, accountRepo: accountRepo,

View File

@@ -7,18 +7,17 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// OpenAIOAuthService handles OpenAI OAuth authentication flows // OpenAIOAuthService handles OpenAI OAuth authentication flows
type OpenAIOAuthService struct { type OpenAIOAuthService struct {
sessionStore *openai.SessionStore sessionStore *openai.SessionStore
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
oauthClient ports.OpenAIOAuthClient oauthClient OpenAIOAuthClient
} }
// NewOpenAIOAuthService creates a new OpenAI OAuth service // NewOpenAIOAuthService creates a new OpenAI OAuth service
func NewOpenAIOAuthService(proxyRepo ports.ProxyRepository, oauthClient ports.OpenAIOAuthClient) *OpenAIOAuthService { func NewOpenAIOAuthService(proxyRepo ProxyRepository, oauthClient OpenAIOAuthClient) *OpenAIOAuthService {
return &OpenAIOAuthService{ return &OpenAIOAuthService{
sessionStore: openai.NewSessionStore(), sessionStore: openai.NewSessionStore(),
proxyRepo: proxyRepo, proxyRepo: proxyRepo,

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -4,10 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -15,6 +14,21 @@ var (
ErrProxyNotFound = errors.New("proxy not found") 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 创建代理请求 // CreateProxyRequest 创建代理请求
type CreateProxyRequest struct { type CreateProxyRequest struct {
Name string `json:"name"` Name string `json:"name"`
@@ -38,11 +52,11 @@ type UpdateProxyRequest struct {
// ProxyService 代理管理服务 // ProxyService 代理管理服务
type ProxyService struct { type ProxyService struct {
proxyRepo ports.ProxyRepository proxyRepo ProxyRepository
} }
// NewProxyService 创建代理服务实例 // NewProxyService 创建代理服务实例
func NewProxyService(proxyRepo ports.ProxyRepository) *ProxyService { func NewProxyService(proxyRepo ProxyRepository) *ProxyService {
return &ProxyService{ return &ProxyService{
proxyRepo: proxyRepo, proxyRepo: proxyRepo,
} }

View File

@@ -9,17 +9,16 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// RateLimitService 处理限流和过载状态管理 // RateLimitService 处理限流和过载状态管理
type RateLimitService struct { type RateLimitService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
cfg *config.Config cfg *config.Config
} }
// NewRateLimitService 创建RateLimitService实例 // NewRateLimitService 创建RateLimitService实例
func NewRateLimitService(accountRepo ports.AccountRepository, cfg *config.Config) *RateLimitService { func NewRateLimitService(accountRepo AccountRepository, cfg *config.Config) *RateLimitService {
return &RateLimitService{ return &RateLimitService{
accountRepo: accountRepo, accountRepo: accountRepo,
cfg: cfg, cfg: cfg,

View File

@@ -6,12 +6,11 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"strings" "strings"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -31,6 +30,29 @@ const (
redeemLockDuration = 10 * time.Second // 锁超时时间,防止死锁 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 生成兑换码请求 // GenerateCodesRequest 生成兑换码请求
type GenerateCodesRequest struct { type GenerateCodesRequest struct {
Count int `json:"count"` Count int `json:"count"`
@@ -48,19 +70,19 @@ type RedeemCodeResponse struct {
// RedeemService 兑换码服务 // RedeemService 兑换码服务
type RedeemService struct { type RedeemService struct {
redeemRepo ports.RedeemCodeRepository redeemRepo RedeemCodeRepository
userRepo ports.UserRepository userRepo UserRepository
subscriptionService *SubscriptionService subscriptionService *SubscriptionService
cache ports.RedeemCache cache RedeemCache
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
} }
// NewRedeemService 创建兑换码服务实例 // NewRedeemService 创建兑换码服务实例
func NewRedeemService( func NewRedeemService(
redeemRepo ports.RedeemCodeRepository, redeemRepo RedeemCodeRepository,
userRepo ports.UserRepository, userRepo UserRepository,
subscriptionService *SubscriptionService, subscriptionService *SubscriptionService,
cache ports.RedeemCache, cache RedeemCache,
billingCacheService *BillingCacheService, billingCacheService *BillingCacheService,
) *RedeemService { ) *RedeemService {
return &RedeemService{ return &RedeemService{

View File

@@ -6,10 +6,10 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"strconv"
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"strconv"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -18,14 +18,24 @@ var (
ErrRegistrationDisabled = errors.New("registration is currently disabled") 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 系统设置服务 // SettingService 系统设置服务
type SettingService struct { type SettingService struct {
settingRepo ports.SettingRepository settingRepo SettingRepository
cfg *config.Config cfg *config.Config
} }
// NewSettingService 创建系统设置服务实例 // NewSettingService 创建系统设置服务实例
func NewSettingService(settingRepo ports.SettingRepository, cfg *config.Config) *SettingService { func NewSettingService(settingRepo SettingRepository, cfg *config.Config) *SettingService {
return &SettingService{ return &SettingService{
settingRepo: settingRepo, settingRepo: settingRepo,
cfg: cfg, cfg: cfg,

View File

@@ -9,7 +9,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
var ( var (
@@ -25,13 +24,13 @@ var (
// SubscriptionService 订阅服务 // SubscriptionService 订阅服务
type SubscriptionService struct { type SubscriptionService struct {
groupRepo ports.GroupRepository groupRepo GroupRepository
userSubRepo ports.UserSubscriptionRepository userSubRepo UserSubscriptionRepository
billingCacheService *BillingCacheService billingCacheService *BillingCacheService
} }
// NewSubscriptionService 创建订阅服务 // NewSubscriptionService 创建订阅服务
func NewSubscriptionService(groupRepo ports.GroupRepository, userSubRepo ports.UserSubscriptionRepository, billingCacheService *BillingCacheService) *SubscriptionService { func NewSubscriptionService(groupRepo GroupRepository, userSubRepo UserSubscriptionRepository, billingCacheService *BillingCacheService) *SubscriptionService {
return &SubscriptionService{ return &SubscriptionService{
groupRepo: groupRepo, groupRepo: groupRepo,
userSubRepo: userSubRepo, userSubRepo: userSubRepo,

View File

@@ -9,13 +9,12 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
// TokenRefreshService OAuth token自动刷新服务 // TokenRefreshService OAuth token自动刷新服务
// 定期检查并刷新即将过期的token // 定期检查并刷新即将过期的token
type TokenRefreshService struct { type TokenRefreshService struct {
accountRepo ports.AccountRepository accountRepo AccountRepository
refreshers []TokenRefresher refreshers []TokenRefresher
cfg *config.TokenRefreshConfig cfg *config.TokenRefreshConfig
@@ -25,7 +24,7 @@ type TokenRefreshService struct {
// NewTokenRefreshService 创建token刷新服务 // NewTokenRefreshService 创建token刷新服务
func NewTokenRefreshService( func NewTokenRefreshService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
oauthService *OAuthService, oauthService *OAuthService,
openaiOAuthService *OpenAIOAuthService, openaiOAuthService *OpenAIOAuthService,
cfg *config.Config, cfg *config.Config,

View File

@@ -17,8 +17,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
) )
const ( const (
@@ -34,6 +32,12 @@ const (
maxDownloadSize = 500 * 1024 * 1024 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 信息的接口 // GitHubReleaseClient 获取 GitHub release 信息的接口
type GitHubReleaseClient interface { type GitHubReleaseClient interface {
FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error) FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error)
@@ -43,14 +47,14 @@ type GitHubReleaseClient interface {
// UpdateService handles software updates // UpdateService handles software updates
type UpdateService struct { type UpdateService struct {
cache ports.UpdateCache cache UpdateCache
githubClient GitHubReleaseClient githubClient GitHubReleaseClient
currentVersion string currentVersion string
buildType string // "source" for manual builds, "release" for CI builds buildType string // "source" for manual builds, "release" for CI builds
} }
// NewUpdateService creates a new UpdateService // 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{ return &UpdateService{
cache: cache, cache: cache,
githubClient: githubClient, githubClient: githubClient,

View File

@@ -4,12 +4,11 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"time"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -55,12 +54,12 @@ type UsageStats struct {
// UsageService 使用统计服务 // UsageService 使用统计服务
type UsageService struct { type UsageService struct {
usageRepo ports.UsageLogRepository usageRepo UsageLogRepository
userRepo ports.UserRepository userRepo UserRepository
} }
// NewUsageService 创建使用统计服务实例 // NewUsageService 创建使用统计服务实例
func NewUsageService(usageRepo ports.UsageLogRepository, userRepo ports.UserRepository) *UsageService { func NewUsageService(usageRepo UsageLogRepository, userRepo UserRepository) *UsageService {
return &UsageService{ return &UsageService{
usageRepo: usageRepo, usageRepo: usageRepo,
userRepo: userRepo, userRepo: userRepo,

View File

@@ -4,10 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/model"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -18,6 +17,24 @@ var (
ErrInsufficientPerms = errors.New("insufficient permissions") 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 更新用户资料请求 // UpdateProfileRequest 更新用户资料请求
type UpdateProfileRequest struct { type UpdateProfileRequest struct {
Email *string `json:"email"` Email *string `json:"email"`
@@ -34,11 +51,11 @@ type ChangePasswordRequest struct {
// UserService 用户服务 // UserService 用户服务
type UserService struct { type UserService struct {
userRepo ports.UserRepository userRepo UserRepository
} }
// NewUserService 创建用户服务实例 // NewUserService 创建用户服务实例
func NewUserService(userRepo ports.UserRepository) *UserService { func NewUserService(userRepo UserRepository) *UserService {
return &UserService{ return &UserService{
userRepo: userRepo, userRepo: userRepo,
} }

View File

@@ -1,4 +1,4 @@
package ports package service
import ( import (
"context" "context"

View File

@@ -2,8 +2,6 @@ package service
import ( import (
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/service/ports"
"github.com/google/wire" "github.com/google/wire"
) )
@@ -24,7 +22,7 @@ func ProvidePricingService(cfg *config.Config, remoteClient PricingRemoteClient)
} }
// ProvideUpdateService creates UpdateService with BuildInfo // 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) return NewUpdateService(cache, githubClient, buildInfo.Version, buildInfo.BuildType)
} }
@@ -35,7 +33,7 @@ func ProvideEmailQueueService(emailService *EmailService) *EmailQueueService {
// ProvideTokenRefreshService creates and starts TokenRefreshService // ProvideTokenRefreshService creates and starts TokenRefreshService
func ProvideTokenRefreshService( func ProvideTokenRefreshService(
accountRepo ports.AccountRepository, accountRepo AccountRepository,
oauthService *OAuthService, oauthService *OAuthService,
openaiOAuthService *OpenAIOAuthService, openaiOAuthService *OpenAIOAuthService,
cfg *config.Config, cfg *config.Config,