| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792 |
- package service
- import (
- "context"
- "net/http"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/internal/config"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- // AdminService interface defines admin management operations
- type AdminService interface {
- // User management
- ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters, sortBy, sortOrder string) ([]User, int64, error)
- GetUser(ctx context.Context, id int64) (*User, error)
- GetUserIncludeDeleted(ctx context.Context, id int64) (*User, error)
- CreateUser(ctx context.Context, input *CreateUserInput) (*User, error)
- UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*User, error)
- DeleteUser(ctx context.Context, id int64) error
- UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*User, error)
- BatchUpdateConcurrency(ctx context.Context, userIDs []int64, value int, mode string) (int, error)
- BatchUpdateLimits(ctx context.Context, userIDs []int64, concurrency, rpmLimit *int) (int, error)
- GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error)
- GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error)
- GetUserRPMStatus(ctx context.Context, userID int64) (*UserRPMStatus, error)
- // GetUserBalanceHistory returns paginated balance/concurrency change records for a user.
- // codeType is optional - pass empty string to return all types.
- // Also returns totalRecharged (sum of all positive balance top-ups).
- GetUserBalanceHistory(ctx context.Context, userID int64, page, pageSize int, codeType string) ([]RedeemCode, int64, float64, error)
- BindUserAuthIdentity(ctx context.Context, userID int64, input AdminBindAuthIdentityInput) (*AdminBoundAuthIdentity, error)
- // Group management
- ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error)
- GetAllGroups(ctx context.Context) ([]Group, error)
- GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error)
- // GetAllGroupsIncludingInactive returns all groups regardless of status (active + disabled),
- // ordered by sort_order then id. Used by the API Key group filter dropdown.
- GetAllGroupsIncludingInactive(ctx context.Context) ([]Group, error)
- GetGroup(ctx context.Context, id int64) (*Group, error)
- GetGroupModelsListCandidates(ctx context.Context, id int64, platform string) ([]string, error)
- CreateGroup(ctx context.Context, input *CreateGroupInput) (*Group, error)
- // DuplicateGroup creates an inactive independent copy of a group's configuration
- // and account bindings while preserving each binding's priority.
- DuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*Group, error)
- // RecoverDuplicateGroup returns a previously committed copy for an ambiguous retry.
- // It never creates a group.
- RecoverDuplicateGroup(ctx context.Context, id int64, actorScope, operationKey string) (*Group, error)
- UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error)
- DeleteGroup(ctx context.Context, id int64) error
- DeleteGroupIfEmpty(ctx context.Context, id int64) error
- ListCompositeRoutes(ctx context.Context, groupID int64) ([]CompositeModelRoute, error)
- CreateCompositeRoute(ctx context.Context, groupID int64, input CompositeRouteInput) (*CompositeModelRoute, error)
- UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input CompositeRouteInput) (*CompositeModelRoute, error)
- DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error
- PreviewCompositeRoute(ctx context.Context, groupID int64, input CompositeRoutePreviewRequest) (*CompositeRouteDecision, error)
- GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]APIKey, int64, error)
- GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error)
- ClearGroupRateMultipliers(ctx context.Context, groupID int64) error
- BatchSetGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error
- ClearGroupRPMOverrides(ctx context.Context, groupID int64) error
- BatchSetGroupRPMOverrides(ctx context.Context, groupID int64, entries []GroupRPMOverrideInput) error
- UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error
- // API Key management (admin)
- AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*AdminUpdateAPIKeyGroupIDResult, error)
- AdminResetAPIKeyRateLimitUsage(ctx context.Context, keyID int64) (*APIKey, error)
- // ReplaceUserGroup 替换用户的专属分组:授予新分组权限、迁移 Key、移除旧分组权限
- ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error)
- // Account management
- ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]Account, int64, error)
- // ListAccountsForSchedulerScoreFilter 返回符合过滤条件的全部账号(不分页),
- // 作为账号列表页计算 OpenAI 调度分数的过滤范围池。
- ListAccountsForSchedulerScoreFilter(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, error)
- // ListOpenAISchedulableAccountsForSchedulerScore 返回指定分组(nil 为未分组)内
- // 可调度的 OpenAI 账号,用于按组计算调度分数。
- ListOpenAISchedulableAccountsForSchedulerScore(ctx context.Context, groupID *int64) ([]Account, error)
- GetAccount(ctx context.Context, id int64) (*Account, error)
- GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error)
- CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error)
- ValidateAccountGroupBindings(ctx context.Context, groupIDs []int64) error
- // DuplicateAccount creates an independent account from an existing account's configuration.
- // First-class runtime columns are intentionally reset by the normal account creation path.
- DuplicateAccount(ctx context.Context, id int64, actorScope, operationKey string) (*Account, error)
- // RecoverDuplicateAccount returns a previously committed duplicate for an ambiguous retry.
- // It never creates an account.
- RecoverDuplicateAccount(ctx context.Context, id int64, actorScope, operationKey string) (*Account, error)
- UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*Account, error)
- // UpdateAccountExtra 仅对 Extra 做 JSONB 增量合并(key 级覆盖),不会影响其它字段或运行态键。
- // 用于刷新流程持久化 account_uuid / org_uuid 等少量键,避免被全量快照覆盖。
- UpdateAccountExtra(ctx context.Context, id int64, updates map[string]any) error
- DeleteAccount(ctx context.Context, id int64) error
- RefreshAccountCredentials(ctx context.Context, id int64) (*Account, error)
- ClearAccountError(ctx context.Context, id int64) (*Account, error)
- SetAccountError(ctx context.Context, id int64, errorMsg string) error
- // EnsureOpenAIPrivacy 检查 OpenAI OAuth 账号 privacy_mode,未设置则尝试关闭训练数据共享并持久化。
- EnsureOpenAIPrivacy(ctx context.Context, account *Account) string
- // EnsureAntigravityPrivacy 检查 Antigravity OAuth 账号 privacy_mode,未设置则调用 setUserSettings 并持久化。
- EnsureAntigravityPrivacy(ctx context.Context, account *Account) string
- // ForceOpenAIPrivacy 强制重新设置 OpenAI OAuth 账号隐私,无论当前状态。
- ForceOpenAIPrivacy(ctx context.Context, account *Account) string
- // ForceAntigravityPrivacy 强制重新设置 Antigravity OAuth 账号隐私,无论当前状态。
- ForceAntigravityPrivacy(ctx context.Context, account *Account) string
- SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error)
- BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error)
- CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error
- // RevertAccountProxyFallback 将账号的 proxy_id 切回 proxy_fallback_origin_id,并清空 origin 字段。
- // 若账号不存在返回 ErrAccountNotFound;若账号存在但不在 fallback 状态,返回 ErrAccountNotInFallback。
- RevertAccountProxyFallback(ctx context.Context, id int64) error
- // CreateShadow 为指定 OpenAI OAuth 母账号创建 spark 维度影子账号(一母一影)。
- // 影子账号不持凭据(Credentials 恒为空),透传母账号凭据;继承母账号的 ProxyID。
- CreateShadow(ctx context.Context, parentID int64, opts ShadowOptions) (*Account, error)
- // Proxy management
- ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error)
- ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error)
- GetAllProxies(ctx context.Context) ([]Proxy, error)
- GetAllProxiesWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error)
- GetProxy(ctx context.Context, id int64) (*Proxy, error)
- GetProxiesByIDs(ctx context.Context, ids []int64) ([]Proxy, error)
- CreateProxy(ctx context.Context, input *CreateProxyInput) (*Proxy, error)
- UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*Proxy, error)
- DeleteProxy(ctx context.Context, id int64) error
- BatchDeleteProxies(ctx context.Context, ids []int64) (*ProxyBatchDeleteResult, error)
- GetProxyAccounts(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error)
- CheckProxyExists(ctx context.Context, host string, port int, username, password string) (bool, error)
- TestProxy(ctx context.Context, id int64) (*ProxyTestResult, error)
- CheckProxyQuality(ctx context.Context, id int64) (*ProxyQualityCheckResult, error)
- // Redeem code management
- ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]RedeemCode, int64, error)
- GetRedeemCode(ctx context.Context, id int64) (*RedeemCode, error)
- GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]RedeemCode, error)
- DeleteRedeemCode(ctx context.Context, id int64) error
- BatchDeleteRedeemCodes(ctx context.Context, ids []int64) (int64, error)
- ExpireRedeemCode(ctx context.Context, id int64) (*RedeemCode, error)
- ResetAccountQuota(ctx context.Context, id int64) error
- }
- type AdminGroupOperation string
- const (
- AdminGroupOperationBasic AdminGroupOperation = "basic"
- AdminGroupOperationDuplicate AdminGroupOperation = "duplicate"
- AdminGroupOperationCompositeRoute AdminGroupOperation = "composite_route"
- AdminGroupOperationMultiplier AdminGroupOperation = "multiplier"
- AdminGroupOperationRPMOverride AdminGroupOperation = "rpm_override"
- AdminGroupOperationSort AdminGroupOperation = "sort"
- )
- func ValidateSimpleModeGroupOperation(cfg *config.Config, operation AdminGroupOperation) error {
- if cfg != nil && cfg.RunMode == config.RunModeSimple && operation != AdminGroupOperationBasic {
- return infraerrors.New(http.StatusForbidden, "SIMPLE_MODE_OPERATION_UNSUPPORTED", "This operation is not supported in simple mode")
- }
- return nil
- }
- // CreateUserInput represents input for creating a new user via admin operations.
- type CreateUserInput struct {
- Email string
- Password string
- Username string
- Notes string
- Role string // 空字符串表示使用默认角色(user);合法值 admin/user
- Balance *float64
- Concurrency int
- RPMLimit int
- AllowedGroups []int64
- RestrictPublicGroups bool
- // ActorAdminID 执行本次操作的管理员ID(来自JWT),仅用于权限敏感操作的审计日志。
- ActorAdminID int64
- }
- type UpdateUserInput struct {
- Email string
- Password string
- Username *string
- Notes *string
- Role string // 空字符串表示"未提供"(不修改);合法值 admin/user
- Balance *float64 // 使用指针区分"未提供"和"设置为0"
- Concurrency *int // 使用指针区分"未提供"和"设置为0"
- RPMLimit *int // 使用指针区分"未提供"和"设置为0"
- Status string
- AllowedGroups *[]int64 // 使用指针区分"未提供"和"设置为空数组"
- // RestrictPublicGroups 指针区分"未提供"和"显式开关"。
- RestrictPublicGroups *bool
- // GroupRates 用户专属分组倍率配置
- // map[groupID]*rate,nil 表示删除该分组的专属倍率
- GroupRates map[int64]*float64
- // ActorAdminID 执行本次操作的管理员ID(来自JWT),仅用于权限敏感操作的审计日志。
- ActorAdminID int64
- }
- type AdminBindAuthIdentityInput struct {
- ProviderType string
- ProviderKey string
- ProviderSubject string
- Issuer *string
- Metadata map[string]any
- Channel *AdminBindAuthIdentityChannelInput
- }
- type AdminBindAuthIdentityChannelInput struct {
- Channel string
- ChannelAppID string
- ChannelSubject string
- Metadata map[string]any
- }
- type AdminBoundAuthIdentity struct {
- UserID int64 `json:"user_id"`
- ProviderType string `json:"provider_type"`
- ProviderKey string `json:"provider_key"`
- ProviderSubject string `json:"provider_subject"`
- VerifiedAt *time.Time `json:"verified_at,omitempty"`
- Issuer *string `json:"issuer,omitempty"`
- Metadata map[string]any `json:"metadata"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
- Channel *AdminBoundAuthIdentityChannel `json:"channel,omitempty"`
- }
- type AdminBoundAuthIdentityChannel struct {
- Channel string `json:"channel"`
- ChannelAppID string `json:"channel_app_id"`
- ChannelSubject string `json:"channel_subject"`
- Metadata map[string]any `json:"metadata"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
- }
- type CreateGroupInput struct {
- SecurityPolicyEnabled bool
- SecurityPolicyMode string
- SecurityPolicyEmailEnabled *bool
- Name string
- Description string
- Platform string
- RateMultiplier float64
- IsExclusive bool
- SubscriptionType string // standard/subscription
- DailyLimitUSD *float64 // 日限额 (USD)
- WeeklyLimitUSD *float64 // 周限额 (USD)
- MonthlyLimitUSD *float64 // 月限额 (USD)
- LongContextPricingEnabled bool
- ModelPricing []ChannelModelPricing
- // 图片生成计费配置(仅 antigravity 平台使用)
- AllowImageGeneration bool
- AllowBatchImageGeneration bool
- ImageRateIndependent bool
- ImageRateMultiplier *float64
- BatchImageDiscountMultiplier *float64
- BatchImageHoldMultiplier *float64
- VideoRateIndependent bool
- VideoRateMultiplier *float64
- // 高峰时段倍率配置(PeakRateMultiplier 为 nil 时按 1.0 处理)
- PeakRateEnabled bool
- PeakStart string
- PeakEnd string
- PeakRateMultiplier *float64
- ImagePrice1K *float64
- ImagePrice2K *float64
- ImagePrice4K *float64
- VideoPrice480P *float64
- VideoPrice720P *float64
- VideoPrice1080P *float64
- // VideoModelPrices 可选按模型族×分辨率覆盖视频每秒单价。
- VideoModelPrices map[string]map[string]float64
- // Codex alpha/search 网页搜索单次价格(USD/次,仅 openai 平台使用);nil/负数按默认价 0.01 处理
- WebSearchPricePerCall *float64
- // 搜索工具单价 per 1k
- SearchPricePer1k *float64
- // Grok Voice 显式定价(分组级)
- AudioRealtimePricePerMin *float64
- AudioTTSPricePerMillionChars *float64
- AudioSTTPricePerHour *float64
- ClaudeCodeOnly bool // 仅允许 Claude Code 客户端
- FallbackGroupID *int64 // 降级分组 ID
- // 无效请求兜底分组 ID(仅 anthropic 平台使用)
- FallbackGroupIDOnInvalidRequest *int64
- // 模型路由配置(仅 anthropic 平台使用)
- ModelRouting map[string][]int64
- ModelRoutingEnabled bool // 是否启用模型路由
- MCPXMLInject *bool
- // 支持的模型系列(仅 antigravity 平台使用)
- SupportedModelScopes []string
- // OpenAI Messages 调度配置(仅 openai 平台使用)
- AllowMessagesDispatch bool
- AllowLive bool
- ForceOpenAIFast bool
- FreeOpenAIFast bool
- DefaultMappedModel string
- RequireOAuthOnly bool
- RequirePrivacySet bool
- MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig
- ModelAllowlist GroupModelAllowlist
- // CodexModelsManifestConfig 固定账号 manifest 配置;创建路径禁止开启,仅编辑可配置。
- CodexModelsManifestConfig GroupCodexModelsManifestConfig
- // RPMLimit 分组 RPM 上限(0 = 不限制)
- RPMLimit int
- // MaxReasoningEffort Anthropic/OpenAI 请求的推理强度上限,空字符串表示不限制。
- MaxReasoningEffort string
- // MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
- MaxReasoningEffortOverLimit string
- // ReasoningEffortMappings Anthropic/OpenAI 推理强度映射,可按模型精确名、前缀或后缀限定。
- ReasoningEffortMappings []ReasoningEffortMapping
- // 分组利润控制(五个 token 平台分组可启用;margin/buffer 为小数,nil 按 0 处理)
- ProfitControlEnabled bool
- ProfitMinMargin *float64
- ProfitSafetyBuffer *float64
- // 从指定分组复制账号(创建分组后在同一事务内绑定)
- CopyAccountsFromGroupIDs []int64
- }
- type UpdateGroupInput struct {
- SecurityPolicyEnabled *bool
- SecurityPolicyMode *string
- SecurityPolicyEmailEnabled *bool
- Name string
- Description *string
- Platform string
- RateMultiplier *float64 // 使用指针以支持设置为0
- IsExclusive *bool
- Status string
- SubscriptionType string // standard/subscription
- DailyLimitUSD *float64 // 日限额 (USD)
- WeeklyLimitUSD *float64 // 周限额 (USD)
- MonthlyLimitUSD *float64 // 月限额 (USD)
- LongContextPricingEnabled *bool
- ModelPricing *[]ChannelModelPricing
- // 图片生成计费配置(仅 antigravity 平台使用)
- AllowImageGeneration *bool
- AllowBatchImageGeneration *bool
- ImageRateIndependent *bool
- ImageRateMultiplier *float64
- BatchImageDiscountMultiplier *float64
- BatchImageHoldMultiplier *float64
- VideoRateIndependent *bool
- VideoRateMultiplier *float64
- // 高峰时段倍率配置(nil 表示不修改)
- PeakRateEnabled *bool
- PeakStart *string
- PeakEnd *string
- PeakRateMultiplier *float64
- ImagePrice1K *float64
- ImagePrice2K *float64
- ImagePrice4K *float64
- VideoPrice480P *float64
- VideoPrice720P *float64
- VideoPrice1080P *float64
- // VideoModelPrices 可选按模型族×分辨率覆盖;nil 表示不修改,空 map 表示清除。
- VideoModelPrices map[string]map[string]float64
- // Codex alpha/search 网页搜索单次价格(USD/次);nil 表示不修改,负数表示清除回默认价 0.01
- WebSearchPricePerCall *float64
- // 搜索工具单价;nil 不修改,负数清除
- SearchPricePer1k *float64
- // Grok Voice 显式定价;nil 表示不修改,负数表示清除
- AudioRealtimePricePerMin *float64
- AudioTTSPricePerMillionChars *float64
- AudioSTTPricePerHour *float64
- ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端
- FallbackGroupID *int64 // 降级分组 ID
- // 无效请求兜底分组 ID(仅 anthropic 平台使用)
- FallbackGroupIDOnInvalidRequest *int64
- // 模型路由配置(仅 anthropic 平台使用)
- ModelRouting map[string][]int64
- ModelRoutingEnabled *bool // 是否启用模型路由
- MCPXMLInject *bool
- // 支持的模型系列(仅 antigravity 平台使用)
- SupportedModelScopes *[]string
- // OpenAI Messages 调度配置(仅 openai 平台使用)
- AllowMessagesDispatch *bool
- AllowLive *bool
- ForceOpenAIFast *bool
- FreeOpenAIFast *bool
- DefaultMappedModel *string
- RequireOAuthOnly *bool
- RequirePrivacySet *bool
- MessagesDispatchModelConfig *OpenAIMessagesDispatchModelConfig
- ModelAllowlist *GroupModelAllowlist
- // CodexModelsManifestConfig nil 表示不修改;非 openai 平台会被归一化为关闭。
- CodexModelsManifestConfig *GroupCodexModelsManifestConfig
- // RPMLimit 分组 RPM 上限(0 = 不限制),nil 表示未提供不改动。
- RPMLimit *int
- // MaxReasoningEffort 空字符串表示清除上限;nil 表示未提供不改动。
- MaxReasoningEffort *string
- // MaxReasoningEffortOverLimit 空字符串视为 downgrade;nil 表示未提供不改动。
- MaxReasoningEffortOverLimit *string
- // ReasoningEffortMappings nil 表示不修改,空数组表示清空,非空数组表示替换。
- ReasoningEffortMappings *[]ReasoningEffortMapping
- // 分组利润控制(nil 表示不修改;margin/buffer 为小数)
- ProfitControlEnabled *bool
- ProfitMinMargin *float64
- ProfitSafetyBuffer *float64
- // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
- CopyAccountsFromGroupIDs []int64
- }
- type CreateAccountInput struct {
- Name string
- Notes *string
- Platform string
- Type string
- Credentials map[string]any
- Extra map[string]any
- ProxyID *int64
- Concurrency int
- Priority int
- RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
- LoadFactor *int
- GroupIDs []int64
- ExpiresAt *int64
- AutoPauseOnExpired *bool
- ProbeEnabled *bool
- // SkipDefaultGroupBind prevents auto-binding to platform default group when GroupIDs is empty.
- SkipDefaultGroupBind bool
- // SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
- // This should only be set when the caller has explicitly confirmed the risk.
- SkipMixedChannelCheck bool
- }
- // ShadowOptions is the input for CreateShadow.
- // The shadow holds no credentials — the scheduler transparently delegates to the parent account's tokens.
- type ShadowOptions struct {
- Name string
- Priority int
- Concurrency int
- GroupIDs []int64
- }
- type UpdateAccountInput struct {
- Name string
- Notes *string
- Type string // Account type: oauth, setup-token, apikey
- Credentials map[string]any
- Extra map[string]any
- ProxyID *int64
- Concurrency *int // 使用指针区分"未提供"和"设置为0"
- Priority *int // 使用指针区分"未提供"和"设置为0"
- RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
- LoadFactor *int
- Status string
- GroupIDs *[]int64
- ExpiresAt *int64
- AutoPauseOnExpired *bool
- ProbeEnabled *bool
- RateSyncEnabled *bool
- SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
- }
- // BulkUpdateAccountsInput describes the payload for bulk updating accounts.
- type BulkUpdateAccountsInput struct {
- AccountIDs []int64
- Filters *BulkUpdateAccountFilters
- Name string
- ProxyID *int64
- Concurrency *int
- Priority *int
- RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
- LoadFactor *int
- Status string
- Schedulable *bool
- GroupIDs *[]int64
- Credentials map[string]any
- Extra map[string]any
- ProbeEnabled *bool
- // SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
- // This should only be set when the caller has explicitly confirmed the risk.
- SkipMixedChannelCheck bool
- }
- type BulkUpdateAccountFilters struct {
- Platform string
- Type string
- Status string
- Group string
- Search string
- PrivacyMode string
- }
- // BulkUpdateAccountResult captures the result for a single account update.
- type BulkUpdateAccountResult struct {
- AccountID int64 `json:"account_id"`
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
- }
- // AdminUpdateAPIKeyGroupIDResult is the result of AdminUpdateAPIKeyGroupID.
- type AdminUpdateAPIKeyGroupIDResult struct {
- APIKey *APIKey
- AutoGrantedGroupAccess bool // true if a new exclusive group permission was auto-added
- GrantedGroupID *int64 // the group ID that was auto-granted
- GrantedGroupName string // the group name that was auto-granted
- }
- // ReplaceUserGroupResult 分组替换操作的结果
- type ReplaceUserGroupResult struct {
- MigratedKeys int64 // 迁移的 Key 数量
- }
- // UserRPMStatus describes a user's current per-minute RPM usage.
- type UserRPMStatus struct {
- UserRPMUsed int `json:"user_rpm_used"`
- UserRPMLimit int `json:"user_rpm_limit"`
- PerGroup []UserGroupRPMStatus `json:"per_group"`
- }
- // UserGroupRPMStatus describes current per-minute RPM usage for one user/group pair.
- type UserGroupRPMStatus struct {
- GroupID int64 `json:"group_id"`
- GroupName string `json:"group_name"`
- Used int `json:"used"`
- Limit int `json:"limit"`
- Source string `json:"source"` // "group" | "override"
- }
- // BulkUpdateAccountsResult is the aggregated response for bulk updates.
- type BulkUpdateAccountsResult struct {
- Success int `json:"success"`
- Failed int `json:"failed"`
- SuccessIDs []int64 `json:"success_ids"`
- FailedIDs []int64 `json:"failed_ids"`
- Results []BulkUpdateAccountResult `json:"results"`
- LongContextInheritedCount int `json:"long_context_inherited_count,omitempty"`
- }
- type CreateProxyInput struct {
- Name string
- Protocol string
- Host string
- Port int
- Username string
- Password string
- ExpiresAt *time.Time
- FallbackMode string
- BackupProxyID *int64
- ExpiryWarnDays int
- }
- // UpdateProxyInput preserves omitted expiry/backup values; Clear flags explicitly
- // remove them. A nil ExpiryWarnDays preserves the current warning period.
- type UpdateProxyInput struct {
- Name string
- Protocol string
- Host string
- Port int
- Username string
- Password string
- Status string
- ExpiresAt *time.Time
- ClearExpiresAt bool
- FallbackMode string
- BackupProxyID *int64
- ClearBackupID bool
- ExpiryWarnDays *int
- }
- type GenerateRedeemCodesInput struct {
- Count int
- Type string
- Value float64
- GroupID *int64 // 订阅类型专用:关联的分组ID
- ValidityDays int // 订阅类型专用:有效天数
- ExpiresAt *time.Time
- }
- type ProxyBatchDeleteResult struct {
- DeletedIDs []int64 `json:"deleted_ids"`
- Skipped []ProxyBatchDeleteSkipped `json:"skipped"`
- }
- type ProxyBatchDeleteSkipped struct {
- ID int64 `json:"id"`
- Reason string `json:"reason"`
- }
- // ProxyTestResult represents the result of testing a proxy
- type ProxyTestResult struct {
- Success bool `json:"success"`
- Message string `json:"message"`
- LatencyMs int64 `json:"latency_ms,omitempty"`
- IPAddress string `json:"ip_address,omitempty"`
- City string `json:"city,omitempty"`
- Region string `json:"region,omitempty"`
- Country string `json:"country,omitempty"`
- CountryCode string `json:"country_code,omitempty"`
- }
- type ProxyQualityCheckResult struct {
- ProxyID int64 `json:"proxy_id"`
- Score int `json:"score"`
- Grade string `json:"grade"`
- Summary string `json:"summary"`
- ExitIP string `json:"exit_ip,omitempty"`
- Country string `json:"country,omitempty"`
- CountryCode string `json:"country_code,omitempty"`
- BaseLatencyMs int64 `json:"base_latency_ms,omitempty"`
- PassedCount int `json:"passed_count"`
- WarnCount int `json:"warn_count"`
- FailedCount int `json:"failed_count"`
- ChallengeCount int `json:"challenge_count"`
- CheckedAt int64 `json:"checked_at"`
- Items []ProxyQualityCheckItem `json:"items"`
- }
- type ProxyQualityCheckItem struct {
- Target string `json:"target"`
- Status string `json:"status"` // pass/warn/fail/challenge
- HTTPStatus int `json:"http_status,omitempty"`
- LatencyMs int64 `json:"latency_ms,omitempty"`
- Message string `json:"message,omitempty"`
- CFRay string `json:"cf_ray,omitempty"`
- }
- // ProxyExitInfo represents proxy exit information from ip-api.com
- type ProxyExitInfo struct {
- IP string
- City string
- Region string
- Country string
- CountryCode string
- }
- // ProxyExitInfoProber tests proxy connectivity and retrieves exit information
- type ProxyExitInfoProber interface {
- ProbeProxy(ctx context.Context, proxyURL string) (*ProxyExitInfo, int64, error)
- }
- type groupExistenceBatchReader interface {
- ExistsByIDs(ctx context.Context, ids []int64) (map[int64]bool, error)
- }
- type proxyQualityTarget struct {
- Target string
- URL string
- Method string
- AllowedStatuses map[int]struct{}
- }
- var proxyQualityTargets = []proxyQualityTarget{
- {
- Target: "openai",
- URL: "https://api.openai.com/v1/models",
- Method: http.MethodGet,
- AllowedStatuses: map[int]struct{}{
- http.StatusUnauthorized: {},
- },
- },
- {
- Target: "anthropic",
- URL: "https://api.anthropic.com/v1/messages",
- Method: http.MethodGet,
- AllowedStatuses: map[int]struct{}{
- http.StatusUnauthorized: {},
- http.StatusMethodNotAllowed: {},
- http.StatusNotFound: {},
- http.StatusBadRequest: {},
- },
- },
- {
- Target: "gemini",
- URL: "https://generativelanguage.googleapis.com/$discovery/rest?version=v1beta",
- Method: http.MethodGet,
- AllowedStatuses: map[int]struct{}{
- http.StatusOK: {},
- },
- },
- {
- Target: "grok",
- URL: "https://api.x.ai/v1/models",
- Method: http.MethodGet,
- AllowedStatuses: map[int]struct{}{
- http.StatusUnauthorized: {},
- },
- },
- }
- const (
- proxyQualityRequestTimeout = 15 * time.Second
- proxyQualityResponseHeaderTimeout = 10 * time.Second
- proxyQualityMaxBodyBytes = int64(8 * 1024)
- proxyQualityClientUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
- )
- var ErrRPMStatusUnavailable = infraerrors.New(http.StatusNotImplemented, "RPM_STATUS_UNAVAILABLE", "RPM cache not available")
- // adminServiceImpl implements AdminService
- type adminServiceImpl struct {
- cfg *config.Config
- userRepo UserRepository
- groupRepo GroupRepository
- groupDuplicateRepo GroupDuplicateRepository
- emptyGroupDeleteRepo EmptyGroupDeleteRepository
- accountRepo AccountRepository
- accountDuplicateRepo AccountDuplicateRepository
- accountBillingRepo AccountBillingSettingsRepository
- proxyRepo ProxyRepository
- apiKeyRepo APIKeyRepository
- redeemCodeRepo RedeemCodeRepository
- userGroupRateRepo UserGroupRateRepository
- userRPMCache UserRPMCache
- billingCacheService *BillingCacheService
- proxyProber ProxyExitInfoProber
- proxyLatencyCache ProxyLatencyCache
- authCacheInvalidator APIKeyAuthCacheInvalidator
- entClient *dbent.Client // 用于开启数据库事务
- settingService *SettingService
- defaultSubAssigner DefaultSubscriptionAssigner
- userSubRepo UserSubscriptionRepository
- privacyClientFactory PrivacyClientFactory
- runtimeBlocker AccountRuntimeBlocker
- affiliateService adminRechargeAffiliateAccruer
- compositeRouteRepo CompositeModelRouteRepository
- compositeResolver *CompositeRouteResolver
- // 分组平台变更后用来失效渠道缓存;可为 nil(缓存会在 TTL 到期后自然重建)
- channelCacheInvalidator ChannelCacheInvalidator
- }
- // ChannelCacheInvalidator 失效渠道缓存。
- // 窄接口,避免 admin 服务依赖整个 ChannelService——与 APIKeyAuthCacheInvalidator 同一思路。
- type ChannelCacheInvalidator interface {
- InvalidateCache()
- }
- type adminRechargeAffiliateAccruer interface {
- AccrueInviteRebate(ctx context.Context, inviteeUserID int64, baseRechargeAmount float64) (float64, error)
- }
- type userGroupRateBatchReader interface {
- GetByUserIDs(ctx context.Context, userIDs []int64) (map[int64]map[int64]float64, error)
- }
- // NewAdminService creates a new AdminService
- func NewAdminService(
- cfg *config.Config,
- userRepo UserRepository,
- groupRepo AdminGroupRepository,
- accountRepo AdminAccountRepository,
- proxyRepo ProxyRepository,
- apiKeyRepo APIKeyRepository,
- redeemCodeRepo RedeemCodeRepository,
- userGroupRateRepo UserGroupRateRepository,
- userRPMCache UserRPMCache,
- billingCacheService *BillingCacheService,
- proxyProber ProxyExitInfoProber,
- proxyLatencyCache ProxyLatencyCache,
- authCacheInvalidator APIKeyAuthCacheInvalidator,
- entClient *dbent.Client,
- settingService *SettingService,
- defaultSubAssigner DefaultSubscriptionAssigner,
- userSubRepo UserSubscriptionRepository,
- privacyClientFactory PrivacyClientFactory,
- runtimeBlocker AccountRuntimeBlocker,
- affiliateService *AffiliateService,
- compositeRouteRepo CompositeModelRouteRepository,
- compositeResolver *CompositeRouteResolver,
- channelCacheInvalidator ChannelCacheInvalidator,
- ) AdminService {
- return &adminServiceImpl{
- cfg: cfg,
- userRepo: userRepo,
- groupRepo: groupRepo,
- groupDuplicateRepo: groupRepo,
- emptyGroupDeleteRepo: groupRepo,
- accountRepo: accountRepo,
- accountDuplicateRepo: accountRepo,
- accountBillingRepo: accountRepo,
- proxyRepo: proxyRepo,
- apiKeyRepo: apiKeyRepo,
- redeemCodeRepo: redeemCodeRepo,
- userGroupRateRepo: userGroupRateRepo,
- userRPMCache: userRPMCache,
- billingCacheService: billingCacheService,
- proxyProber: proxyProber,
- proxyLatencyCache: proxyLatencyCache,
- authCacheInvalidator: authCacheInvalidator,
- entClient: entClient,
- settingService: settingService,
- defaultSubAssigner: defaultSubAssigner,
- userSubRepo: userSubRepo,
- privacyClientFactory: privacyClientFactory,
- runtimeBlocker: runtimeBlocker,
- affiliateService: affiliateService,
- compositeRouteRepo: compositeRouteRepo,
- compositeResolver: compositeResolver,
- channelCacheInvalidator: channelCacheInvalidator,
- }
- }
|