package service import ( "context" "net/http" "time" dbent "github.com/Wei-Shaw/sub2api/ent" 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 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) // 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 } // 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 // 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 // 使用指针区分"未提供"和"设置为空数组" // 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 { 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 DefaultMappedModel string RequireOAuthOnly bool RequirePrivacySet bool MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig ModelsListConfig GroupModelsListConfig // RPMLimit 分组 RPM 上限(0 = 不限制) RPMLimit int // MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。 MaxReasoningEffort string // ReasoningEffortMappings OpenAI/Codex 推理强度精确映射。 ReasoningEffortMappings []ReasoningEffortMapping // 分组利润控制(五个 token 平台分组可启用;margin/buffer 为小数,nil 按 0 处理) ProfitControlEnabled bool ProfitMinMargin *float64 ProfitSafetyBuffer *float64 // 从指定分组复制账号(创建分组后在同一事务内绑定) CopyAccountsFromGroupIDs []int64 } type UpdateGroupInput struct { 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 DefaultMappedModel *string RequireOAuthOnly *bool RequirePrivacySet *bool MessagesDispatchModelConfig *OpenAIMessagesDispatchModelConfig ModelsListConfig *GroupModelsListConfig // RPMLimit 分组 RPM 上限(0 = 不限制),nil 表示未提供不改动。 RPMLimit *int // MaxReasoningEffort 空字符串表示清除上限;nil 表示未提供不改动。 MaxReasoningEffort *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 } type UpdateProxyInput struct { Name string Protocol string Host string Port int Username string Password string Status string ExpiresAt *time.Time FallbackMode string BackupProxyID *int64 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 { userRepo UserRepository groupRepo GroupRepository groupDuplicateRepo GroupDuplicateRepository 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( 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{ userRepo: userRepo, groupRepo: groupRepo, groupDuplicateRepo: 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, } }