refactor: 删除 ports 目录
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
15
backend/internal/service/billing_cache_port.go
Normal file
15
backend/internal/service/billing_cache_port.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,18 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/model"
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CRSSyncService struct {
|
type CRSSyncService struct {
|
||||||
accountRepo ports.AccountRepository
|
accountRepo AccountRepository
|
||||||
proxyRepo ports.ProxyRepository
|
proxyRepo ProxyRepository
|
||||||
oauthService *OAuthService
|
oauthService *OAuthService
|
||||||
openaiOAuthService *OpenAIOAuthService
|
openaiOAuthService *OpenAIOAuthService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCRSSyncService(
|
func NewCRSSyncService(
|
||||||
accountRepo ports.AccountRepository,
|
accountRepo AccountRepository,
|
||||||
proxyRepo ports.ProxyRepository,
|
proxyRepo ProxyRepository,
|
||||||
oauthService *OAuthService,
|
oauthService *OAuthService,
|
||||||
openaiOAuthService *OpenAIOAuthService,
|
openaiOAuthService *OpenAIOAuthService,
|
||||||
) *CRSSyncService {
|
) *CRSSyncService {
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service/ports"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DashboardService provides aggregated statistics for admin dashboard.
|
// DashboardService provides aggregated statistics for admin dashboard.
|
||||||
type DashboardService struct {
|
type DashboardService struct {
|
||||||
usageRepo ports.UsageLogRepository
|
usageRepo UsageLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardService(usageRepo ports.UsageLogRepository) *DashboardService {
|
func NewDashboardService(usageRepo UsageLogRepository) *DashboardService {
|
||||||
return &DashboardService{
|
return &DashboardService{
|
||||||
usageRepo: usageRepo,
|
usageRepo: usageRepo,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package ports
|
package service
|
||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package ports
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user