2025-12-18 13:50:39 +08:00
package service
import (
"context"
"errors"
"fmt"
2025-12-20 15:29:52 +08:00
"log"
2026-01-03 06:34:00 -08:00
"strings"
2025-12-18 13:50:39 +08:00
"time"
2025-12-24 21:07:21 +08:00
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
2025-12-18 13:50:39 +08:00
)
// AdminService interface defines admin management operations
type AdminService interface {
// User management
2026-01-01 18:59:38 +08:00
ListUsers ( ctx context . Context , page , pageSize int , filters UserListFilters ) ( [ ] User , int64 , error )
2025-12-26 15:40:24 +08:00
GetUser ( 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 )
2025-12-18 13:50:39 +08:00
DeleteUser ( ctx context . Context , id int64 ) error
2025-12-26 15:40:24 +08:00
UpdateUserBalance ( ctx context . Context , userID int64 , balance float64 , operation string , notes string ) ( * User , error )
2026-01-04 19:27:53 +08:00
GetUserAPIKeys ( ctx context . Context , userID int64 , page , pageSize int ) ( [ ] APIKey , int64 , error )
2025-12-20 16:19:40 +08:00
GetUserUsageStats ( ctx context . Context , userID int64 , period string ) ( any , error )
2025-12-18 13:50:39 +08:00
// Group management
2026-01-09 18:58:06 +08:00
ListGroups ( ctx context . Context , page , pageSize int , platform , status , search string , isExclusive * bool ) ( [ ] Group , int64 , error )
2025-12-26 15:40:24 +08:00
GetAllGroups ( ctx context . Context ) ( [ ] Group , error )
GetAllGroupsByPlatform ( ctx context . Context , platform string ) ( [ ] Group , error )
GetGroup ( ctx context . Context , id int64 ) ( * Group , error )
CreateGroup ( ctx context . Context , input * CreateGroupInput ) ( * Group , error )
UpdateGroup ( ctx context . Context , id int64 , input * UpdateGroupInput ) ( * Group , error )
2025-12-18 13:50:39 +08:00
DeleteGroup ( ctx context . Context , id int64 ) error
2026-01-04 19:27:53 +08:00
GetGroupAPIKeys ( ctx context . Context , groupID int64 , page , pageSize int ) ( [ ] APIKey , int64 , error )
2025-12-18 13:50:39 +08:00
// Account management
2025-12-26 15:40:24 +08:00
ListAccounts ( ctx context . Context , page , pageSize int , platform , accountType , status , search string ) ( [ ] Account , int64 , error )
GetAccount ( ctx context . Context , id int64 ) ( * Account , error )
2026-01-01 15:07:16 +08:00
GetAccountsByIDs ( ctx context . Context , ids [ ] int64 ) ( [ ] * Account , error )
2025-12-26 15:40:24 +08:00
CreateAccount ( ctx context . Context , input * CreateAccountInput ) ( * Account , error )
UpdateAccount ( ctx context . Context , id int64 , input * UpdateAccountInput ) ( * Account , error )
2025-12-18 13:50:39 +08:00
DeleteAccount ( ctx context . Context , id int64 ) error
2025-12-26 15:40:24 +08:00
RefreshAccountCredentials ( ctx context . Context , id int64 ) ( * Account , error )
ClearAccountError ( ctx context . Context , id int64 ) ( * Account , error )
2026-01-16 12:13:54 +08:00
SetAccountError ( ctx context . Context , id int64 , errorMsg string ) error
2025-12-26 15:40:24 +08:00
SetAccountSchedulable ( ctx context . Context , id int64 , schedulable bool ) ( * Account , error )
2025-12-24 17:16:19 -08:00
BulkUpdateAccounts ( ctx context . Context , input * BulkUpdateAccountsInput ) ( * BulkUpdateAccountsResult , error )
2025-12-18 13:50:39 +08:00
// Proxy management
2025-12-26 15:40:24 +08:00
ListProxies ( ctx context . Context , page , pageSize int , protocol , status , search string ) ( [ ] Proxy , int64 , error )
2026-01-08 21:20:12 +08:00
ListProxiesWithAccountCount ( ctx context . Context , page , pageSize int , protocol , status , search string ) ( [ ] ProxyWithAccountCount , int64 , error )
2025-12-26 15:40:24 +08:00
GetAllProxies ( ctx context . Context ) ( [ ] Proxy , error )
GetAllProxiesWithAccountCount ( ctx context . Context ) ( [ ] ProxyWithAccountCount , error )
GetProxy ( ctx context . Context , id int64 ) ( * Proxy , error )
CreateProxy ( ctx context . Context , input * CreateProxyInput ) ( * Proxy , error )
UpdateProxy ( ctx context . Context , id int64 , input * UpdateProxyInput ) ( * Proxy , error )
2025-12-18 13:50:39 +08:00
DeleteProxy ( ctx context . Context , id int64 ) error
2026-01-14 19:45:29 +08:00
BatchDeleteProxies ( ctx context . Context , ids [ ] int64 ) ( * ProxyBatchDeleteResult , error )
GetProxyAccounts ( ctx context . Context , proxyID int64 ) ( [ ] ProxyAccountSummary , error )
2025-12-18 13:50:39 +08:00
CheckProxyExists ( ctx context . Context , host string , port int , username , password string ) ( bool , error )
TestProxy ( ctx context . Context , id int64 ) ( * ProxyTestResult , error )
// Redeem code management
2025-12-26 15:40:24 +08:00
ListRedeemCodes ( ctx context . Context , page , pageSize int , codeType , status , search string ) ( [ ] RedeemCode , int64 , error )
GetRedeemCode ( ctx context . Context , id int64 ) ( * RedeemCode , error )
GenerateRedeemCodes ( ctx context . Context , input * GenerateRedeemCodesInput ) ( [ ] RedeemCode , error )
2025-12-18 13:50:39 +08:00
DeleteRedeemCode ( ctx context . Context , id int64 ) error
BatchDeleteRedeemCodes ( ctx context . Context , ids [ ] int64 ) ( int64 , error )
2025-12-26 15:40:24 +08:00
ExpireRedeemCode ( ctx context . Context , id int64 ) ( * RedeemCode , error )
2025-12-18 13:50:39 +08:00
}
2026-01-04 19:27:53 +08:00
// CreateUserInput represents input for creating a new user via admin operations.
2025-12-18 13:50:39 +08:00
type CreateUserInput struct {
Email string
Password string
2025-12-23 11:26:22 +08:00
Username string
Notes string
2025-12-18 13:50:39 +08:00
Balance float64
Concurrency int
AllowedGroups [ ] int64
}
type UpdateUserInput struct {
Email string
Password string
2025-12-23 11:26:22 +08:00
Username * string
Notes * string
2025-12-18 13:50:39 +08:00
Balance * float64 // 使用指针区分"未提供"和"设置为0"
Concurrency * int // 使用指针区分"未提供"和"设置为0"
Status string
AllowedGroups * [ ] int64 // 使用指针区分"未提供"和"设置为空数组"
}
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)
2026-01-05 17:07:29 +08:00
// 图片生成计费配置(仅 antigravity 平台使用)
2026-01-08 23:13:57 +08:00
ImagePrice1K * float64
ImagePrice2K * float64
ImagePrice4K * float64
ClaudeCodeOnly bool // 仅允许 Claude Code 客户端
FallbackGroupID * int64 // 降级分组 ID
2026-01-16 17:26:05 +08:00
// 模型路由配置(仅 anthropic 平台使用)
ModelRouting map [ string ] [ ] int64
ModelRoutingEnabled bool // 是否启用模型路由
2025-12-18 13:50:39 +08:00
}
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)
2026-01-05 17:07:29 +08:00
// 图片生成计费配置(仅 antigravity 平台使用)
2026-01-08 23:13:57 +08:00
ImagePrice1K * float64
ImagePrice2K * float64
ImagePrice4K * float64
ClaudeCodeOnly * bool // 仅允许 Claude Code 客户端
FallbackGroupID * int64 // 降级分组 ID
2026-01-16 17:26:05 +08:00
// 模型路由配置(仅 anthropic 平台使用)
ModelRouting map [ string ] [ ] int64
ModelRoutingEnabled * bool // 是否启用模型路由
2025-12-18 13:50:39 +08:00
}
type CreateAccountInput struct {
2026-01-07 16:59:35 +08:00
Name string
Notes * string
Platform string
Type string
Credentials map [ string ] any
Extra map [ string ] any
ProxyID * int64
Concurrency int
Priority int
2026-01-14 16:12:08 +08:00
RateMultiplier * float64 // 账号计费倍率(>=0, 允许 0)
2026-01-07 16:59:35 +08:00
GroupIDs [ ] int64
ExpiresAt * int64
AutoPauseOnExpired * bool
2026-01-03 06:34:00 -08:00
// 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
2025-12-18 13:50:39 +08:00
}
type UpdateAccountInput struct {
2026-01-03 06:34:00 -08:00
Name string
2026-01-05 14:07:33 +08:00
Notes * string
2026-01-03 06:34:00 -08:00
Type string // Account type: oauth, setup-token, apikey
Credentials map [ string ] any
Extra map [ string ] any
ProxyID * int64
2026-01-14 16:12:08 +08:00
Concurrency * int // 使用指针区分"未提供"和"设置为0"
Priority * int // 使用指针区分"未提供"和"设置为0"
RateMultiplier * float64 // 账号计费倍率(>=0, 允许 0)
2026-01-03 06:34:00 -08:00
Status string
GroupIDs * [ ] int64
2026-01-07 16:59:35 +08:00
ExpiresAt * int64
AutoPauseOnExpired * bool
2026-01-03 06:34:00 -08:00
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
2025-12-18 13:50:39 +08:00
}
2025-12-24 17:16:19 -08:00
// BulkUpdateAccountsInput describes the payload for bulk updating accounts.
type BulkUpdateAccountsInput struct {
2026-01-14 16:12:08 +08:00
AccountIDs [ ] int64
Name string
ProxyID * int64
Concurrency * int
Priority * int
RateMultiplier * float64 // 账号计费倍率(>=0, 允许 0)
Status string
Schedulable * bool
GroupIDs * [ ] int64
Credentials map [ string ] any
Extra map [ string ] any
2026-01-03 06:34:00 -08:00
// 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
2025-12-24 17:16:19 -08:00
}
// 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" `
}
// BulkUpdateAccountsResult is the aggregated response for bulk updates.
type BulkUpdateAccountsResult struct {
2026-01-11 20:22:17 +08:00
Success int ` json:"success" `
Failed int ` json:"failed" `
SuccessIDs [ ] int64 ` json:"success_ids" `
FailedIDs [ ] int64 ` json:"failed_ids" `
Results [ ] BulkUpdateAccountResult ` json:"results" `
2025-12-24 17:16:19 -08:00
}
2025-12-18 13:50:39 +08:00
type CreateProxyInput struct {
Name string
Protocol string
Host string
Port int
Username string
Password string
}
type UpdateProxyInput struct {
Name string
Protocol string
Host string
Port int
Username string
Password string
Status string
}
type GenerateRedeemCodesInput struct {
Count int
Type string
Value float64
GroupID * int64 // 订阅类型专用: 关联的分组ID
ValidityDays int // 订阅类型专用:有效天数
}
2026-01-14 19:45:29 +08:00
type ProxyBatchDeleteResult struct {
DeletedIDs [ ] int64 ` json:"deleted_ids" `
Skipped [ ] ProxyBatchDeleteSkipped ` json:"skipped" `
}
type ProxyBatchDeleteSkipped struct {
ID int64 ` json:"id" `
Reason string ` json:"reason" `
}
2025-12-18 13:50:39 +08:00
// ProxyTestResult represents the result of testing a proxy
type ProxyTestResult struct {
2026-01-15 15:15:20 +08:00
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" `
2025-12-18 13:50:39 +08:00
}
2026-01-15 15:15:20 +08:00
// ProxyExitInfo represents proxy exit information from ip-api.com
2025-12-20 11:56:11 +08:00
type ProxyExitInfo struct {
2026-01-15 15:15:20 +08:00
IP string
City string
Region string
Country string
CountryCode string
2025-12-20 11:56:11 +08:00
}
// ProxyExitInfoProber tests proxy connectivity and retrieves exit information
type ProxyExitInfoProber interface {
ProbeProxy ( ctx context . Context , proxyURL string ) ( * ProxyExitInfo , int64 , error )
}
2025-12-18 13:50:39 +08:00
// adminServiceImpl implements AdminService
type adminServiceImpl struct {
2026-01-10 22:23:51 +08:00
userRepo UserRepository
groupRepo GroupRepository
accountRepo AccountRepository
proxyRepo ProxyRepository
apiKeyRepo APIKeyRepository
redeemCodeRepo RedeemCodeRepository
billingCacheService * BillingCacheService
proxyProber ProxyExitInfoProber
2026-01-14 19:45:29 +08:00
proxyLatencyCache ProxyLatencyCache
2026-01-10 22:23:51 +08:00
authCacheInvalidator APIKeyAuthCacheInvalidator
2025-12-18 13:50:39 +08:00
}
// NewAdminService creates a new AdminService
2025-12-19 21:26:19 +08:00
func NewAdminService (
2025-12-25 17:15:01 +08:00
userRepo UserRepository ,
groupRepo GroupRepository ,
accountRepo AccountRepository ,
proxyRepo ProxyRepository ,
2026-01-04 19:27:53 +08:00
apiKeyRepo APIKeyRepository ,
2025-12-25 17:15:01 +08:00
redeemCodeRepo RedeemCodeRepository ,
2025-12-19 21:26:19 +08:00
billingCacheService * BillingCacheService ,
2025-12-20 11:56:11 +08:00
proxyProber ProxyExitInfoProber ,
2026-01-14 19:45:29 +08:00
proxyLatencyCache ProxyLatencyCache ,
2026-01-10 22:23:51 +08:00
authCacheInvalidator APIKeyAuthCacheInvalidator ,
2025-12-19 21:26:19 +08:00
) AdminService {
2025-12-18 13:50:39 +08:00
return & adminServiceImpl {
2026-01-10 22:23:51 +08:00
userRepo : userRepo ,
groupRepo : groupRepo ,
accountRepo : accountRepo ,
proxyRepo : proxyRepo ,
apiKeyRepo : apiKeyRepo ,
redeemCodeRepo : redeemCodeRepo ,
billingCacheService : billingCacheService ,
proxyProber : proxyProber ,
2026-01-14 19:45:29 +08:00
proxyLatencyCache : proxyLatencyCache ,
2026-01-10 22:23:51 +08:00
authCacheInvalidator : authCacheInvalidator ,
2025-12-18 13:50:39 +08:00
}
}
// User management implementations
2026-01-01 18:59:38 +08:00
func ( s * adminServiceImpl ) ListUsers ( ctx context . Context , page , pageSize int , filters UserListFilters ) ( [ ] User , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2026-01-01 18:59:38 +08:00
users , result , err := s . userRepo . ListWithFilters ( ctx , params , filters )
2025-12-18 13:50:39 +08:00
if err != nil {
return nil , 0 , err
}
return users , result . Total , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetUser ( ctx context . Context , id int64 ) ( * User , error ) {
2025-12-18 13:50:39 +08:00
return s . userRepo . GetByID ( ctx , id )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) CreateUser ( ctx context . Context , input * CreateUserInput ) ( * User , error ) {
user := & User {
Email : input . Email ,
Username : input . Username ,
Notes : input . Notes ,
Role : RoleUser , // Always create as regular user, never admin
Balance : input . Balance ,
Concurrency : input . Concurrency ,
Status : StatusActive ,
AllowedGroups : input . AllowedGroups ,
2025-12-18 13:50:39 +08:00
}
if err := user . SetPassword ( input . Password ) ; err != nil {
return nil , err
}
if err := s . userRepo . Create ( ctx , user ) ; err != nil {
return nil , err
}
return user , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) UpdateUser ( ctx context . Context , id int64 , input * UpdateUserInput ) ( * User , error ) {
2025-12-18 13:50:39 +08:00
user , err := s . userRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
// Protect admin users: cannot disable admin accounts
if user . Role == "admin" && input . Status == "disabled" {
return nil , errors . New ( "cannot disable admin user" )
}
oldConcurrency := user . Concurrency
2026-01-10 22:23:51 +08:00
oldStatus := user . Status
oldRole := user . Role
2025-12-18 13:50:39 +08:00
if input . Email != "" {
user . Email = input . Email
}
if input . Password != "" {
if err := user . SetPassword ( input . Password ) ; err != nil {
return nil , err
}
}
2025-12-23 11:26:22 +08:00
if input . Username != nil {
user . Username = * input . Username
}
if input . Notes != nil {
user . Notes = * input . Notes
}
2025-12-18 13:50:39 +08:00
if input . Status != "" {
user . Status = input . Status
}
if input . Concurrency != nil {
user . Concurrency = * input . Concurrency
}
if input . AllowedGroups != nil {
user . AllowedGroups = * input . AllowedGroups
}
if err := s . userRepo . Update ( ctx , user ) ; err != nil {
return nil , err
}
2026-01-10 22:23:51 +08:00
if s . authCacheInvalidator != nil {
if user . Concurrency != oldConcurrency || user . Status != oldStatus || user . Role != oldRole {
s . authCacheInvalidator . InvalidateAuthCacheByUserID ( ctx , user . ID )
}
}
2025-12-18 13:50:39 +08:00
concurrencyDiff := user . Concurrency - oldConcurrency
if concurrencyDiff != 0 {
2025-12-26 15:40:24 +08:00
code , err := GenerateRedeemCode ( )
2025-12-20 15:29:52 +08:00
if err != nil {
log . Printf ( "failed to generate adjustment redeem code: %v" , err )
return user , nil
}
2025-12-26 15:40:24 +08:00
adjustmentRecord := & RedeemCode {
2025-12-20 15:29:52 +08:00
Code : code ,
2025-12-26 15:40:24 +08:00
Type : AdjustmentTypeAdminConcurrency ,
2025-12-18 13:50:39 +08:00
Value : float64 ( concurrencyDiff ) ,
2025-12-26 15:40:24 +08:00
Status : StatusUsed ,
2025-12-18 13:50:39 +08:00
UsedBy : & user . ID ,
}
now := time . Now ( )
adjustmentRecord . UsedAt = & now
if err := s . redeemCodeRepo . Create ( ctx , adjustmentRecord ) ; err != nil {
2025-12-20 15:29:52 +08:00
log . Printf ( "failed to create concurrency adjustment redeem code: %v" , err )
2025-12-18 13:50:39 +08:00
}
}
return user , nil
}
func ( s * adminServiceImpl ) DeleteUser ( ctx context . Context , id int64 ) error {
// Protect admin users: cannot delete admin accounts
user , err := s . userRepo . GetByID ( ctx , id )
if err != nil {
return err
}
if user . Role == "admin" {
return errors . New ( "cannot delete admin user" )
}
2025-12-29 16:57:50 +08:00
if err := s . userRepo . Delete ( ctx , id ) ; err != nil {
log . Printf ( "delete user failed: user_id=%d err=%v" , id , err )
return err
}
2026-01-10 22:23:51 +08:00
if s . authCacheInvalidator != nil {
s . authCacheInvalidator . InvalidateAuthCacheByUserID ( ctx , id )
}
2025-12-29 16:57:50 +08:00
return nil
2025-12-18 13:50:39 +08:00
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) UpdateUserBalance ( ctx context . Context , userID int64 , balance float64 , operation string , notes string ) ( * User , error ) {
2025-12-18 13:50:39 +08:00
user , err := s . userRepo . GetByID ( ctx , userID )
if err != nil {
return nil , err
}
2025-12-23 16:29:57 +08:00
oldBalance := user . Balance
2025-12-18 13:50:39 +08:00
switch operation {
case "set" :
user . Balance = balance
case "add" :
user . Balance += balance
case "subtract" :
user . Balance -= balance
}
2025-12-23 16:29:57 +08:00
if user . Balance < 0 {
return nil , fmt . Errorf ( "balance cannot be negative, current balance: %.2f, requested operation would result in: %.2f" , oldBalance , user . Balance )
}
2025-12-18 13:50:39 +08:00
if err := s . userRepo . Update ( ctx , user ) ; err != nil {
return nil , err
}
2026-01-10 22:23:51 +08:00
balanceDiff := user . Balance - oldBalance
if s . authCacheInvalidator != nil && balanceDiff != 0 {
s . authCacheInvalidator . InvalidateAuthCacheByUserID ( ctx , userID )
}
2025-12-18 13:50:39 +08:00
if s . billingCacheService != nil {
go func ( ) {
cacheCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
defer cancel ( )
2025-12-20 15:29:52 +08:00
if err := s . billingCacheService . InvalidateUserBalance ( cacheCtx , userID ) ; err != nil {
log . Printf ( "invalidate user balance cache failed: user_id=%d err=%v" , userID , err )
}
2025-12-18 13:50:39 +08:00
} ( )
}
2025-12-23 16:29:57 +08:00
if balanceDiff != 0 {
2025-12-26 15:40:24 +08:00
code , err := GenerateRedeemCode ( )
2025-12-23 16:29:57 +08:00
if err != nil {
log . Printf ( "failed to generate adjustment redeem code: %v" , err )
return user , nil
}
2025-12-26 15:40:24 +08:00
adjustmentRecord := & RedeemCode {
2025-12-23 16:29:57 +08:00
Code : code ,
2025-12-26 15:40:24 +08:00
Type : AdjustmentTypeAdminBalance ,
2025-12-23 16:29:57 +08:00
Value : balanceDiff ,
2025-12-26 15:40:24 +08:00
Status : StatusUsed ,
2025-12-23 16:29:57 +08:00
UsedBy : & user . ID ,
Notes : notes ,
}
now := time . Now ( )
adjustmentRecord . UsedAt = & now
if err := s . redeemCodeRepo . Create ( ctx , adjustmentRecord ) ; err != nil {
log . Printf ( "failed to create balance adjustment redeem code: %v" , err )
}
}
2025-12-18 13:50:39 +08:00
return user , nil
}
2026-01-04 19:27:53 +08:00
func ( s * adminServiceImpl ) GetUserAPIKeys ( ctx context . Context , userID int64 , page , pageSize int ) ( [ ] APIKey , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2025-12-18 13:50:39 +08:00
keys , result , err := s . apiKeyRepo . ListByUserID ( ctx , userID , params )
if err != nil {
return nil , 0 , err
}
return keys , result . Total , nil
}
2025-12-20 16:19:40 +08:00
func ( s * adminServiceImpl ) GetUserUsageStats ( ctx context . Context , userID int64 , period string ) ( any , error ) {
2025-12-18 13:50:39 +08:00
// Return mock data for now
2025-12-20 16:19:40 +08:00
return map [ string ] any {
2025-12-18 13:50:39 +08:00
"period" : period ,
"total_requests" : 0 ,
"total_cost" : 0.0 ,
"total_tokens" : 0 ,
"avg_duration_ms" : 0 ,
} , nil
}
// Group management implementations
2026-01-09 18:58:06 +08:00
func ( s * adminServiceImpl ) ListGroups ( ctx context . Context , page , pageSize int , platform , status , search string , isExclusive * bool ) ( [ ] Group , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2026-01-09 18:58:06 +08:00
groups , result , err := s . groupRepo . ListWithFilters ( ctx , params , platform , status , search , isExclusive )
2025-12-18 13:50:39 +08:00
if err != nil {
return nil , 0 , err
}
return groups , result . Total , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetAllGroups ( ctx context . Context ) ( [ ] Group , error ) {
2025-12-18 13:50:39 +08:00
return s . groupRepo . ListActive ( ctx )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetAllGroupsByPlatform ( ctx context . Context , platform string ) ( [ ] Group , error ) {
2025-12-18 13:50:39 +08:00
return s . groupRepo . ListActiveByPlatform ( ctx , platform )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetGroup ( ctx context . Context , id int64 ) ( * Group , error ) {
2025-12-18 13:50:39 +08:00
return s . groupRepo . GetByID ( ctx , id )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) CreateGroup ( ctx context . Context , input * CreateGroupInput ) ( * Group , error ) {
2025-12-18 13:50:39 +08:00
platform := input . Platform
if platform == "" {
2025-12-26 15:40:24 +08:00
platform = PlatformAnthropic
2025-12-18 13:50:39 +08:00
}
subscriptionType := input . SubscriptionType
if subscriptionType == "" {
2025-12-26 15:40:24 +08:00
subscriptionType = SubscriptionTypeStandard
2025-12-18 13:50:39 +08:00
}
2025-12-31 22:48:35 +08:00
// 限额字段: 0 和 nil 都表示"无限制"
dailyLimit := normalizeLimit ( input . DailyLimitUSD )
weeklyLimit := normalizeLimit ( input . WeeklyLimitUSD )
monthlyLimit := normalizeLimit ( input . MonthlyLimitUSD )
2026-01-05 17:14:06 +08:00
// 图片价格: 负数表示清除( 使用默认价格) , 0 保留(表示免费)
imagePrice1K := normalizePrice ( input . ImagePrice1K )
imagePrice2K := normalizePrice ( input . ImagePrice2K )
imagePrice4K := normalizePrice ( input . ImagePrice4K )
2026-01-08 23:07:00 +08:00
// 校验降级分组
if input . FallbackGroupID != nil {
if err := s . validateFallbackGroup ( ctx , 0 , * input . FallbackGroupID ) ; err != nil {
return nil , err
}
}
2025-12-26 15:40:24 +08:00
group := & Group {
2025-12-18 13:50:39 +08:00
Name : input . Name ,
Description : input . Description ,
Platform : platform ,
RateMultiplier : input . RateMultiplier ,
IsExclusive : input . IsExclusive ,
2025-12-26 15:40:24 +08:00
Status : StatusActive ,
2025-12-18 13:50:39 +08:00
SubscriptionType : subscriptionType ,
2025-12-31 22:48:35 +08:00
DailyLimitUSD : dailyLimit ,
WeeklyLimitUSD : weeklyLimit ,
MonthlyLimitUSD : monthlyLimit ,
2026-01-05 17:14:06 +08:00
ImagePrice1K : imagePrice1K ,
ImagePrice2K : imagePrice2K ,
ImagePrice4K : imagePrice4K ,
2026-01-08 23:07:00 +08:00
ClaudeCodeOnly : input . ClaudeCodeOnly ,
FallbackGroupID : input . FallbackGroupID ,
2026-01-16 17:26:05 +08:00
ModelRouting : input . ModelRouting ,
2025-12-18 13:50:39 +08:00
}
if err := s . groupRepo . Create ( ctx , group ) ; err != nil {
return nil , err
}
return group , nil
}
2025-12-31 22:48:35 +08:00
// normalizeLimit 将 0 或负数转换为 nil( 表示无限制)
func normalizeLimit ( limit * float64 ) * float64 {
if limit == nil || * limit <= 0 {
return nil
}
return limit
}
2026-01-05 17:14:06 +08:00
// normalizePrice 将负数转换为 nil( 表示使用默认价格) , 0 保留(表示免费)
func normalizePrice ( price * float64 ) * float64 {
if price == nil || * price < 0 {
return nil
}
return price
}
2026-01-08 23:07:00 +08:00
// validateFallbackGroup 校验降级分组的有效性
// currentGroupID: 当前分组 ID( 新建时为 0)
// fallbackGroupID: 降级分组 ID
func ( s * adminServiceImpl ) validateFallbackGroup ( ctx context . Context , currentGroupID , fallbackGroupID int64 ) error {
// 不能将自己设置为降级分组
if currentGroupID > 0 && currentGroupID == fallbackGroupID {
return fmt . Errorf ( "cannot set self as fallback group" )
}
2026-01-10 07:56:50 +08:00
visited := map [ int64 ] struct { } { }
nextID := fallbackGroupID
for {
if _ , seen := visited [ nextID ] ; seen {
return fmt . Errorf ( "fallback group cycle detected" )
}
visited [ nextID ] = struct { } { }
if currentGroupID > 0 && nextID == currentGroupID {
return fmt . Errorf ( "fallback group cycle detected" )
}
2026-01-08 23:07:00 +08:00
2026-01-10 07:56:50 +08:00
// 检查降级分组是否存在
fallbackGroup , err := s . groupRepo . GetByIDLite ( ctx , nextID )
if err != nil {
return fmt . Errorf ( "fallback group not found: %w" , err )
}
2026-01-08 23:07:00 +08:00
2026-01-10 07:56:50 +08:00
// 降级分组不能启用 claude_code_only, 否则会造成死循环
if nextID == fallbackGroupID && fallbackGroup . ClaudeCodeOnly {
return fmt . Errorf ( "fallback group cannot have claude_code_only enabled" )
}
if fallbackGroup . FallbackGroupID == nil {
return nil
}
nextID = * fallbackGroup . FallbackGroupID
}
2026-01-08 23:07:00 +08:00
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) UpdateGroup ( ctx context . Context , id int64 , input * UpdateGroupInput ) ( * Group , error ) {
2025-12-18 13:50:39 +08:00
group , err := s . groupRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
if input . Name != "" {
group . Name = input . Name
}
if input . Description != "" {
group . Description = input . Description
}
if input . Platform != "" {
group . Platform = input . Platform
}
if input . RateMultiplier != nil {
group . RateMultiplier = * input . RateMultiplier
}
if input . IsExclusive != nil {
group . IsExclusive = * input . IsExclusive
}
if input . Status != "" {
group . Status = input . Status
}
// 订阅相关字段
if input . SubscriptionType != "" {
group . SubscriptionType = input . SubscriptionType
}
2025-12-31 22:48:35 +08:00
// 限额字段: 0 和 nil 都表示"无限制",正数表示具体限额
2025-12-18 13:50:39 +08:00
if input . DailyLimitUSD != nil {
2025-12-31 22:48:35 +08:00
group . DailyLimitUSD = normalizeLimit ( input . DailyLimitUSD )
2025-12-18 13:50:39 +08:00
}
if input . WeeklyLimitUSD != nil {
2025-12-31 22:48:35 +08:00
group . WeeklyLimitUSD = normalizeLimit ( input . WeeklyLimitUSD )
2025-12-18 13:50:39 +08:00
}
if input . MonthlyLimitUSD != nil {
2025-12-31 22:48:35 +08:00
group . MonthlyLimitUSD = normalizeLimit ( input . MonthlyLimitUSD )
2025-12-18 13:50:39 +08:00
}
2026-01-05 17:14:06 +08:00
// 图片生成计费配置:负数表示清除(使用默认价格)
2026-01-05 17:07:29 +08:00
if input . ImagePrice1K != nil {
2026-01-05 17:14:06 +08:00
group . ImagePrice1K = normalizePrice ( input . ImagePrice1K )
2026-01-05 17:07:29 +08:00
}
if input . ImagePrice2K != nil {
2026-01-05 17:14:06 +08:00
group . ImagePrice2K = normalizePrice ( input . ImagePrice2K )
2026-01-05 17:07:29 +08:00
}
if input . ImagePrice4K != nil {
2026-01-05 17:14:06 +08:00
group . ImagePrice4K = normalizePrice ( input . ImagePrice4K )
2026-01-05 17:07:29 +08:00
}
2025-12-18 13:50:39 +08:00
2026-01-08 23:07:00 +08:00
// Claude Code 客户端限制
if input . ClaudeCodeOnly != nil {
group . ClaudeCodeOnly = * input . ClaudeCodeOnly
}
if input . FallbackGroupID != nil {
// 校验降级分组
if * input . FallbackGroupID > 0 {
if err := s . validateFallbackGroup ( ctx , id , * input . FallbackGroupID ) ; err != nil {
return nil , err
}
group . FallbackGroupID = input . FallbackGroupID
} else {
// 传入 0 或负数表示清除降级分组
group . FallbackGroupID = nil
}
}
2026-01-16 17:26:05 +08:00
// 模型路由配置
if input . ModelRouting != nil {
group . ModelRouting = input . ModelRouting
}
if input . ModelRoutingEnabled != nil {
group . ModelRoutingEnabled = * input . ModelRoutingEnabled
}
2025-12-18 13:50:39 +08:00
if err := s . groupRepo . Update ( ctx , group ) ; err != nil {
return nil , err
}
2026-01-10 22:23:51 +08:00
if s . authCacheInvalidator != nil {
s . authCacheInvalidator . InvalidateAuthCacheByGroupID ( ctx , id )
}
2025-12-18 13:50:39 +08:00
return group , nil
}
func ( s * adminServiceImpl ) DeleteGroup ( ctx context . Context , id int64 ) error {
2026-01-10 22:23:51 +08:00
var groupKeys [ ] string
if s . authCacheInvalidator != nil {
keys , err := s . apiKeyRepo . ListKeysByGroupID ( ctx , id )
if err == nil {
groupKeys = keys
}
}
2025-12-25 20:52:47 +08:00
affectedUserIDs , err := s . groupRepo . DeleteCascade ( ctx , id )
2025-12-18 13:50:39 +08:00
if err != nil {
return err
}
// 事务成功后,异步失效受影响用户的订阅缓存
if len ( affectedUserIDs ) > 0 && s . billingCacheService != nil {
groupID := id
go func ( ) {
cacheCtx , cancel := context . WithTimeout ( context . Background ( ) , 30 * time . Second )
defer cancel ( )
for _ , userID := range affectedUserIDs {
2025-12-20 15:29:52 +08:00
if err := s . billingCacheService . InvalidateSubscription ( cacheCtx , userID , groupID ) ; err != nil {
log . Printf ( "invalidate subscription cache failed: user_id=%d group_id=%d err=%v" , userID , groupID , err )
}
2025-12-18 13:50:39 +08:00
}
} ( )
}
2026-01-10 22:23:51 +08:00
if s . authCacheInvalidator != nil {
for _ , key := range groupKeys {
s . authCacheInvalidator . InvalidateAuthCacheByKey ( ctx , key )
}
}
2025-12-18 13:50:39 +08:00
return nil
}
2026-01-04 19:27:53 +08:00
func ( s * adminServiceImpl ) GetGroupAPIKeys ( ctx context . Context , groupID int64 , page , pageSize int ) ( [ ] APIKey , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2025-12-18 13:50:39 +08:00
keys , result , err := s . apiKeyRepo . ListByGroupID ( ctx , groupID , params )
if err != nil {
return nil , 0 , err
}
return keys , result . Total , nil
}
// Account management implementations
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) ListAccounts ( ctx context . Context , page , pageSize int , platform , accountType , status , search string ) ( [ ] Account , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2025-12-18 13:50:39 +08:00
accounts , result , err := s . accountRepo . ListWithFilters ( ctx , params , platform , accountType , status , search )
if err != nil {
return nil , 0 , err
}
return accounts , result . Total , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetAccount ( ctx context . Context , id int64 ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
return s . accountRepo . GetByID ( ctx , id )
}
2026-01-01 15:07:16 +08:00
func ( s * adminServiceImpl ) GetAccountsByIDs ( ctx context . Context , ids [ ] int64 ) ( [ ] * Account , error ) {
if len ( ids ) == 0 {
return [ ] * Account { } , nil
}
accounts , err := s . accountRepo . GetByIDs ( ctx , ids )
if err != nil {
return nil , fmt . Errorf ( "failed to get accounts by IDs: %w" , err )
}
return accounts , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) CreateAccount ( ctx context . Context , input * CreateAccountInput ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
// 绑定分组
2025-12-29 03:17:25 +08:00
groupIDs := input . GroupIDs
// 如果没有指定分组,自动绑定对应平台的默认分组
if len ( groupIDs ) == 0 {
defaultGroupName := input . Platform + "-default"
groups , err := s . groupRepo . ListActiveByPlatform ( ctx , input . Platform )
if err == nil {
for _ , g := range groups {
if g . Name == defaultGroupName {
groupIDs = [ ] int64 { g . ID }
break
}
}
}
}
2026-01-03 06:34:00 -08:00
// 检查混合渠道风险(除非用户已确认)
if len ( groupIDs ) > 0 && ! input . SkipMixedChannelCheck {
if err := s . checkMixedChannelRisk ( ctx , 0 , input . Platform , groupIDs ) ; err != nil {
return nil , err
}
}
account := & Account {
Name : input . Name ,
2026-01-05 14:07:33 +08:00
Notes : normalizeAccountNotes ( input . Notes ) ,
2026-01-03 06:34:00 -08:00
Platform : input . Platform ,
Type : input . Type ,
Credentials : input . Credentials ,
Extra : input . Extra ,
ProxyID : input . ProxyID ,
Concurrency : input . Concurrency ,
Priority : input . Priority ,
Status : StatusActive ,
2026-01-04 10:45:18 +08:00
Schedulable : true ,
2026-01-03 06:34:00 -08:00
}
2026-01-07 16:59:35 +08:00
if input . ExpiresAt != nil && * input . ExpiresAt > 0 {
expiresAt := time . Unix ( * input . ExpiresAt , 0 )
account . ExpiresAt = & expiresAt
}
if input . AutoPauseOnExpired != nil {
account . AutoPauseOnExpired = * input . AutoPauseOnExpired
} else {
account . AutoPauseOnExpired = true
}
2026-01-14 16:12:08 +08:00
if input . RateMultiplier != nil {
if * input . RateMultiplier < 0 {
return nil , errors . New ( "rate_multiplier must be >= 0" )
}
account . RateMultiplier = input . RateMultiplier
}
2026-01-03 06:34:00 -08:00
if err := s . accountRepo . Create ( ctx , account ) ; err != nil {
return nil , err
}
// 绑定分组
2025-12-29 03:17:25 +08:00
if len ( groupIDs ) > 0 {
if err := s . accountRepo . BindGroups ( ctx , account . ID , groupIDs ) ; err != nil {
2025-12-18 13:50:39 +08:00
return nil , err
}
}
2025-12-29 03:17:25 +08:00
2025-12-18 13:50:39 +08:00
return account , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) UpdateAccount ( ctx context . Context , id int64 , input * UpdateAccountInput ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
account , err := s . accountRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
if input . Name != "" {
account . Name = input . Name
}
if input . Type != "" {
account . Type = input . Type
}
2026-01-05 14:07:33 +08:00
if input . Notes != nil {
account . Notes = normalizeAccountNotes ( input . Notes )
}
2025-12-20 15:29:52 +08:00
if len ( input . Credentials ) > 0 {
2025-12-26 15:40:24 +08:00
account . Credentials = input . Credentials
2025-12-18 13:50:39 +08:00
}
2025-12-20 15:29:52 +08:00
if len ( input . Extra ) > 0 {
2025-12-26 15:40:24 +08:00
account . Extra = input . Extra
2025-12-18 13:50:39 +08:00
}
if input . ProxyID != nil {
2026-01-05 20:53:38 +08:00
// 0 表示清除代理(前端发送 0 而不是 null 来表达清除意图)
if * input . ProxyID == 0 {
account . ProxyID = nil
} else {
account . ProxyID = input . ProxyID
}
2025-12-25 21:58:09 +08:00
account . Proxy = nil // 清除关联对象,防止 GORM Save 时根据 Proxy.ID 覆盖 ProxyID
2025-12-18 13:50:39 +08:00
}
// 只在指针非 nil 时更新 Concurrency( 支持设置为 0)
if input . Concurrency != nil {
account . Concurrency = * input . Concurrency
}
// 只在指针非 nil 时更新 Priority( 支持设置为 0)
if input . Priority != nil {
account . Priority = * input . Priority
}
2026-01-14 16:12:08 +08:00
if input . RateMultiplier != nil {
if * input . RateMultiplier < 0 {
return nil , errors . New ( "rate_multiplier must be >= 0" )
}
account . RateMultiplier = input . RateMultiplier
}
2025-12-18 13:50:39 +08:00
if input . Status != "" {
account . Status = input . Status
}
2026-01-07 16:59:35 +08:00
if input . ExpiresAt != nil {
if * input . ExpiresAt <= 0 {
account . ExpiresAt = nil
} else {
expiresAt := time . Unix ( * input . ExpiresAt , 0 )
account . ExpiresAt = & expiresAt
}
}
if input . AutoPauseOnExpired != nil {
account . AutoPauseOnExpired = * input . AutoPauseOnExpired
}
2025-12-18 13:50:39 +08:00
2025-12-27 14:57:43 +08:00
// 先验证分组是否存在(在任何写操作之前)
if input . GroupIDs != nil {
for _ , groupID := range * input . GroupIDs {
if _ , err := s . groupRepo . GetByID ( ctx , groupID ) ; err != nil {
return nil , fmt . Errorf ( "get group: %w" , err )
}
}
2026-01-03 06:34:00 -08:00
// 检查混合渠道风险(除非用户已确认)
if ! input . SkipMixedChannelCheck {
if err := s . checkMixedChannelRisk ( ctx , account . ID , account . Platform , * input . GroupIDs ) ; err != nil {
return nil , err
}
}
2025-12-27 14:57:43 +08:00
}
2025-12-18 13:50:39 +08:00
if err := s . accountRepo . Update ( ctx , account ) ; err != nil {
return nil , err
}
2025-12-27 14:57:43 +08:00
// 绑定分组
2025-12-18 13:50:39 +08:00
if input . GroupIDs != nil {
if err := s . accountRepo . BindGroups ( ctx , account . ID , * input . GroupIDs ) ; err != nil {
return nil , err
}
}
2025-12-25 21:58:09 +08:00
// 重新查询以确保返回完整数据(包括正确的 Proxy 关联对象)
return s . accountRepo . GetByID ( ctx , id )
2025-12-18 13:50:39 +08:00
}
2025-12-24 17:16:19 -08:00
// BulkUpdateAccounts updates multiple accounts in one request.
// It merges credentials/extra keys instead of overwriting the whole object.
func ( s * adminServiceImpl ) BulkUpdateAccounts ( ctx context . Context , input * BulkUpdateAccountsInput ) ( * BulkUpdateAccountsResult , error ) {
result := & BulkUpdateAccountsResult {
2026-01-11 20:22:17 +08:00
SuccessIDs : make ( [ ] int64 , 0 , len ( input . AccountIDs ) ) ,
FailedIDs : make ( [ ] int64 , 0 , len ( input . AccountIDs ) ) ,
Results : make ( [ ] BulkUpdateAccountResult , 0 , len ( input . AccountIDs ) ) ,
2025-12-24 17:16:19 -08:00
}
if len ( input . AccountIDs ) == 0 {
return result , nil
}
2026-01-03 06:34:00 -08:00
// Preload account platforms for mixed channel risk checks if group bindings are requested.
platformByID := map [ int64 ] string { }
if input . GroupIDs != nil && ! input . SkipMixedChannelCheck {
accounts , err := s . accountRepo . GetByIDs ( ctx , input . AccountIDs )
if err != nil {
return nil , err
}
for _ , account := range accounts {
if account != nil {
platformByID [ account . ID ] = account . Platform
}
}
}
2026-01-14 16:12:08 +08:00
if input . RateMultiplier != nil {
if * input . RateMultiplier < 0 {
return nil , errors . New ( "rate_multiplier must be >= 0" )
}
}
2025-12-24 17:16:19 -08:00
// Prepare bulk updates for columns and JSONB fields.
2025-12-25 17:15:01 +08:00
repoUpdates := AccountBulkUpdate {
2025-12-24 17:16:19 -08:00
Credentials : input . Credentials ,
Extra : input . Extra ,
}
if input . Name != "" {
repoUpdates . Name = & input . Name
}
if input . ProxyID != nil {
repoUpdates . ProxyID = input . ProxyID
}
if input . Concurrency != nil {
repoUpdates . Concurrency = input . Concurrency
}
if input . Priority != nil {
repoUpdates . Priority = input . Priority
}
2026-01-14 16:12:08 +08:00
if input . RateMultiplier != nil {
repoUpdates . RateMultiplier = input . RateMultiplier
}
2025-12-24 17:16:19 -08:00
if input . Status != "" {
repoUpdates . Status = & input . Status
}
2026-01-09 19:26:32 +08:00
if input . Schedulable != nil {
repoUpdates . Schedulable = input . Schedulable
}
2025-12-24 17:16:19 -08:00
// Run bulk update for column/jsonb fields first.
if _ , err := s . accountRepo . BulkUpdate ( ctx , input . AccountIDs , repoUpdates ) ; err != nil {
return nil , err
}
// Handle group bindings per account (requires individual operations).
for _ , accountID := range input . AccountIDs {
entry := BulkUpdateAccountResult { AccountID : accountID }
if input . GroupIDs != nil {
2026-01-03 06:34:00 -08:00
// 检查混合渠道风险(除非用户已确认)
if ! input . SkipMixedChannelCheck {
platform := platformByID [ accountID ]
if platform == "" {
account , err := s . accountRepo . GetByID ( ctx , accountID )
if err != nil {
entry . Success = false
entry . Error = err . Error ( )
result . Failed ++
2026-01-11 20:22:17 +08:00
result . FailedIDs = append ( result . FailedIDs , accountID )
2026-01-03 06:34:00 -08:00
result . Results = append ( result . Results , entry )
continue
}
platform = account . Platform
}
if err := s . checkMixedChannelRisk ( ctx , accountID , platform , * input . GroupIDs ) ; err != nil {
entry . Success = false
entry . Error = err . Error ( )
result . Failed ++
2026-01-11 20:39:15 +08:00
result . FailedIDs = append ( result . FailedIDs , accountID )
2026-01-03 06:34:00 -08:00
result . Results = append ( result . Results , entry )
continue
}
}
2025-12-24 17:16:19 -08:00
if err := s . accountRepo . BindGroups ( ctx , accountID , * input . GroupIDs ) ; err != nil {
entry . Success = false
entry . Error = err . Error ( )
result . Failed ++
2026-01-11 20:22:17 +08:00
result . FailedIDs = append ( result . FailedIDs , accountID )
2025-12-24 17:16:19 -08:00
result . Results = append ( result . Results , entry )
continue
}
}
entry . Success = true
result . Success ++
2026-01-11 20:22:17 +08:00
result . SuccessIDs = append ( result . SuccessIDs , accountID )
2025-12-24 17:16:19 -08:00
result . Results = append ( result . Results , entry )
}
return result , nil
}
2025-12-18 13:50:39 +08:00
func ( s * adminServiceImpl ) DeleteAccount ( ctx context . Context , id int64 ) error {
return s . accountRepo . Delete ( ctx , id )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) RefreshAccountCredentials ( ctx context . Context , id int64 ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
account , err := s . accountRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
// TODO: Implement refresh logic
return account , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) ClearAccountError ( ctx context . Context , id int64 ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
account , err := s . accountRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
2025-12-26 15:40:24 +08:00
account . Status = StatusActive
2025-12-18 13:50:39 +08:00
account . ErrorMessage = ""
if err := s . accountRepo . Update ( ctx , account ) ; err != nil {
return nil , err
}
return account , nil
}
2026-01-16 12:13:54 +08:00
func ( s * adminServiceImpl ) SetAccountError ( ctx context . Context , id int64 , errorMsg string ) error {
return s . accountRepo . SetError ( ctx , id , errorMsg )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) SetAccountSchedulable ( ctx context . Context , id int64 , schedulable bool ) ( * Account , error ) {
2025-12-18 13:50:39 +08:00
if err := s . accountRepo . SetSchedulable ( ctx , id , schedulable ) ; err != nil {
return nil , err
}
return s . accountRepo . GetByID ( ctx , id )
}
// Proxy management implementations
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) ListProxies ( ctx context . Context , page , pageSize int , protocol , status , search string ) ( [ ] Proxy , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2025-12-18 13:50:39 +08:00
proxies , result , err := s . proxyRepo . ListWithFilters ( ctx , params , protocol , status , search )
if err != nil {
return nil , 0 , err
}
return proxies , result . Total , nil
}
2026-01-08 21:20:12 +08:00
func ( s * adminServiceImpl ) ListProxiesWithAccountCount ( ctx context . Context , page , pageSize int , protocol , status , search string ) ( [ ] ProxyWithAccountCount , int64 , error ) {
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
proxies , result , err := s . proxyRepo . ListWithFiltersAndAccountCount ( ctx , params , protocol , status , search )
if err != nil {
return nil , 0 , err
}
2026-01-14 19:45:29 +08:00
s . attachProxyLatency ( ctx , proxies )
2026-01-08 21:20:12 +08:00
return proxies , result . Total , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetAllProxies ( ctx context . Context ) ( [ ] Proxy , error ) {
2025-12-18 13:50:39 +08:00
return s . proxyRepo . ListActive ( ctx )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetAllProxiesWithAccountCount ( ctx context . Context ) ( [ ] ProxyWithAccountCount , error ) {
2026-01-14 19:45:29 +08:00
proxies , err := s . proxyRepo . ListActiveWithAccountCount ( ctx )
if err != nil {
return nil , err
}
s . attachProxyLatency ( ctx , proxies )
return proxies , nil
2025-12-18 13:50:39 +08:00
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetProxy ( ctx context . Context , id int64 ) ( * Proxy , error ) {
2025-12-18 13:50:39 +08:00
return s . proxyRepo . GetByID ( ctx , id )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) CreateProxy ( ctx context . Context , input * CreateProxyInput ) ( * Proxy , error ) {
proxy := & Proxy {
2025-12-18 13:50:39 +08:00
Name : input . Name ,
Protocol : input . Protocol ,
Host : input . Host ,
Port : input . Port ,
Username : input . Username ,
Password : input . Password ,
2025-12-26 15:40:24 +08:00
Status : StatusActive ,
2025-12-18 13:50:39 +08:00
}
if err := s . proxyRepo . Create ( ctx , proxy ) ; err != nil {
return nil , err
}
2026-01-14 19:45:29 +08:00
// Probe latency asynchronously so creation isn't blocked by network timeout.
go s . probeProxyLatency ( context . Background ( ) , proxy )
2025-12-18 13:50:39 +08:00
return proxy , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) UpdateProxy ( ctx context . Context , id int64 , input * UpdateProxyInput ) ( * Proxy , error ) {
2025-12-18 13:50:39 +08:00
proxy , err := s . proxyRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
if input . Name != "" {
proxy . Name = input . Name
}
if input . Protocol != "" {
proxy . Protocol = input . Protocol
}
if input . Host != "" {
proxy . Host = input . Host
}
if input . Port != 0 {
proxy . Port = input . Port
}
if input . Username != "" {
proxy . Username = input . Username
}
if input . Password != "" {
proxy . Password = input . Password
}
if input . Status != "" {
proxy . Status = input . Status
}
if err := s . proxyRepo . Update ( ctx , proxy ) ; err != nil {
return nil , err
}
return proxy , nil
}
func ( s * adminServiceImpl ) DeleteProxy ( ctx context . Context , id int64 ) error {
2026-01-14 19:45:29 +08:00
count , err := s . proxyRepo . CountAccountsByProxyID ( ctx , id )
if err != nil {
return err
}
if count > 0 {
return ErrProxyInUse
}
2025-12-18 13:50:39 +08:00
return s . proxyRepo . Delete ( ctx , id )
}
2026-01-14 19:45:29 +08:00
func ( s * adminServiceImpl ) BatchDeleteProxies ( ctx context . Context , ids [ ] int64 ) ( * ProxyBatchDeleteResult , error ) {
result := & ProxyBatchDeleteResult { }
if len ( ids ) == 0 {
return result , nil
}
for _ , id := range ids {
count , err := s . proxyRepo . CountAccountsByProxyID ( ctx , id )
if err != nil {
result . Skipped = append ( result . Skipped , ProxyBatchDeleteSkipped {
ID : id ,
Reason : err . Error ( ) ,
} )
continue
}
if count > 0 {
result . Skipped = append ( result . Skipped , ProxyBatchDeleteSkipped {
ID : id ,
Reason : ErrProxyInUse . Error ( ) ,
} )
continue
}
if err := s . proxyRepo . Delete ( ctx , id ) ; err != nil {
result . Skipped = append ( result . Skipped , ProxyBatchDeleteSkipped {
ID : id ,
Reason : err . Error ( ) ,
} )
continue
}
result . DeletedIDs = append ( result . DeletedIDs , id )
}
return result , nil
}
func ( s * adminServiceImpl ) GetProxyAccounts ( ctx context . Context , proxyID int64 ) ( [ ] ProxyAccountSummary , error ) {
return s . proxyRepo . ListAccountSummariesByProxyID ( ctx , proxyID )
2025-12-18 13:50:39 +08:00
}
func ( s * adminServiceImpl ) CheckProxyExists ( ctx context . Context , host string , port int , username , password string ) ( bool , error ) {
return s . proxyRepo . ExistsByHostPortAuth ( ctx , host , port , username , password )
}
// Redeem code management implementations
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) ListRedeemCodes ( ctx context . Context , page , pageSize int , codeType , status , search string ) ( [ ] RedeemCode , int64 , error ) {
2025-12-19 21:26:19 +08:00
params := pagination . PaginationParams { Page : page , PageSize : pageSize }
2025-12-18 13:50:39 +08:00
codes , result , err := s . redeemCodeRepo . ListWithFilters ( ctx , params , codeType , status , search )
if err != nil {
return nil , 0 , err
}
return codes , result . Total , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GetRedeemCode ( ctx context . Context , id int64 ) ( * RedeemCode , error ) {
2025-12-18 13:50:39 +08:00
return s . redeemCodeRepo . GetByID ( ctx , id )
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) GenerateRedeemCodes ( ctx context . Context , input * GenerateRedeemCodesInput ) ( [ ] RedeemCode , error ) {
2025-12-18 13:50:39 +08:00
// 如果是订阅类型,验证必须有 GroupID
2025-12-26 15:40:24 +08:00
if input . Type == RedeemTypeSubscription {
2025-12-18 13:50:39 +08:00
if input . GroupID == nil {
return nil , errors . New ( "group_id is required for subscription type" )
}
// 验证分组存在且为订阅类型
group , err := s . groupRepo . GetByID ( ctx , * input . GroupID )
if err != nil {
return nil , fmt . Errorf ( "group not found: %w" , err )
}
if ! group . IsSubscriptionType ( ) {
return nil , errors . New ( "group must be subscription type" )
}
}
2025-12-26 15:40:24 +08:00
codes := make ( [ ] RedeemCode , 0 , input . Count )
2025-12-18 13:50:39 +08:00
for i := 0 ; i < input . Count ; i ++ {
2025-12-26 15:40:24 +08:00
codeValue , err := GenerateRedeemCode ( )
2025-12-20 15:29:52 +08:00
if err != nil {
return nil , err
}
2025-12-26 15:40:24 +08:00
code := RedeemCode {
2025-12-20 15:29:52 +08:00
Code : codeValue ,
2025-12-18 13:50:39 +08:00
Type : input . Type ,
Value : input . Value ,
2025-12-26 15:40:24 +08:00
Status : StatusUnused ,
2025-12-18 13:50:39 +08:00
}
// 订阅类型专用字段
2025-12-26 15:40:24 +08:00
if input . Type == RedeemTypeSubscription {
2025-12-18 13:50:39 +08:00
code . GroupID = input . GroupID
code . ValidityDays = input . ValidityDays
if code . ValidityDays <= 0 {
code . ValidityDays = 30 // 默认30天
}
}
if err := s . redeemCodeRepo . Create ( ctx , & code ) ; err != nil {
return nil , err
}
codes = append ( codes , code )
}
return codes , nil
}
func ( s * adminServiceImpl ) DeleteRedeemCode ( ctx context . Context , id int64 ) error {
return s . redeemCodeRepo . Delete ( ctx , id )
}
func ( s * adminServiceImpl ) BatchDeleteRedeemCodes ( ctx context . Context , ids [ ] int64 ) ( int64 , error ) {
var deleted int64
for _ , id := range ids {
if err := s . redeemCodeRepo . Delete ( ctx , id ) ; err == nil {
deleted ++
}
}
return deleted , nil
}
2025-12-26 15:40:24 +08:00
func ( s * adminServiceImpl ) ExpireRedeemCode ( ctx context . Context , id int64 ) ( * RedeemCode , error ) {
2025-12-18 13:50:39 +08:00
code , err := s . redeemCodeRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
2025-12-26 15:40:24 +08:00
code . Status = StatusExpired
2025-12-18 13:50:39 +08:00
if err := s . redeemCodeRepo . Update ( ctx , code ) ; err != nil {
return nil , err
}
return code , nil
}
func ( s * adminServiceImpl ) TestProxy ( ctx context . Context , id int64 ) ( * ProxyTestResult , error ) {
proxy , err := s . proxyRepo . GetByID ( ctx , id )
if err != nil {
return nil , err
}
proxyURL := proxy . URL ( )
2025-12-20 11:56:11 +08:00
exitInfo , latencyMs , err := s . proxyProber . ProbeProxy ( ctx , proxyURL )
2025-12-18 13:50:39 +08:00
if err != nil {
2026-01-14 19:45:29 +08:00
s . saveProxyLatency ( ctx , id , & ProxyLatencyInfo {
Success : false ,
Message : err . Error ( ) ,
UpdatedAt : time . Now ( ) ,
} )
2025-12-18 13:50:39 +08:00
return & ProxyTestResult {
Success : false ,
2025-12-20 11:56:11 +08:00
Message : err . Error ( ) ,
2025-12-18 13:50:39 +08:00
} , nil
}
2026-01-14 19:45:29 +08:00
latency := latencyMs
s . saveProxyLatency ( ctx , id , & ProxyLatencyInfo {
2026-01-15 15:15:20 +08:00
Success : true ,
LatencyMs : & latency ,
Message : "Proxy is accessible" ,
IPAddress : exitInfo . IP ,
Country : exitInfo . Country ,
CountryCode : exitInfo . CountryCode ,
Region : exitInfo . Region ,
City : exitInfo . City ,
UpdatedAt : time . Now ( ) ,
2026-01-14 19:45:29 +08:00
} )
2025-12-18 13:50:39 +08:00
return & ProxyTestResult {
2026-01-15 15:15:20 +08:00
Success : true ,
Message : "Proxy is accessible" ,
LatencyMs : latencyMs ,
IPAddress : exitInfo . IP ,
City : exitInfo . City ,
Region : exitInfo . Region ,
Country : exitInfo . Country ,
CountryCode : exitInfo . CountryCode ,
2025-12-18 13:50:39 +08:00
} , nil
}
2026-01-03 06:34:00 -08:00
2026-01-14 19:45:29 +08:00
func ( s * adminServiceImpl ) probeProxyLatency ( ctx context . Context , proxy * Proxy ) {
if s . proxyProber == nil || proxy == nil {
return
}
2026-01-15 15:15:20 +08:00
exitInfo , latencyMs , err := s . proxyProber . ProbeProxy ( ctx , proxy . URL ( ) )
2026-01-14 19:45:29 +08:00
if err != nil {
s . saveProxyLatency ( ctx , proxy . ID , & ProxyLatencyInfo {
Success : false ,
Message : err . Error ( ) ,
UpdatedAt : time . Now ( ) ,
} )
return
}
latency := latencyMs
s . saveProxyLatency ( ctx , proxy . ID , & ProxyLatencyInfo {
2026-01-15 15:15:20 +08:00
Success : true ,
LatencyMs : & latency ,
Message : "Proxy is accessible" ,
IPAddress : exitInfo . IP ,
Country : exitInfo . Country ,
CountryCode : exitInfo . CountryCode ,
Region : exitInfo . Region ,
City : exitInfo . City ,
UpdatedAt : time . Now ( ) ,
2026-01-14 19:45:29 +08:00
} )
}
2026-01-03 06:34:00 -08:00
// checkMixedChannelRisk 检查分组中是否存在混合渠道( Antigravity + Anthropic)
// 如果存在混合,返回错误提示用户确认
func ( s * adminServiceImpl ) checkMixedChannelRisk ( ctx context . Context , currentAccountID int64 , currentAccountPlatform string , groupIDs [ ] int64 ) error {
// 判断当前账号的渠道类型(基于 platform 字段,而不是 type 字段)
currentPlatform := getAccountPlatform ( currentAccountPlatform )
if currentPlatform == "" {
// 不是 Antigravity 或 Anthropic, 无需检查
return nil
}
// 检查每个分组中的其他账号
for _ , groupID := range groupIDs {
accounts , err := s . accountRepo . ListByGroup ( ctx , groupID )
if err != nil {
return fmt . Errorf ( "get accounts in group %d: %w" , groupID , err )
}
// 检查是否存在不同渠道的账号
for _ , account := range accounts {
if currentAccountID > 0 && account . ID == currentAccountID {
continue // 跳过当前账号
}
otherPlatform := getAccountPlatform ( account . Platform )
if otherPlatform == "" {
continue // 不是 Antigravity 或 Anthropic, 跳过
}
// 检测混合渠道
if currentPlatform != otherPlatform {
group , _ := s . groupRepo . GetByID ( ctx , groupID )
groupName := fmt . Sprintf ( "Group %d" , groupID )
if group != nil {
groupName = group . Name
}
return & MixedChannelError {
GroupID : groupID ,
GroupName : groupName ,
CurrentPlatform : currentPlatform ,
OtherPlatform : otherPlatform ,
}
}
}
}
return nil
}
2026-01-14 19:45:29 +08:00
func ( s * adminServiceImpl ) attachProxyLatency ( ctx context . Context , proxies [ ] ProxyWithAccountCount ) {
if s . proxyLatencyCache == nil || len ( proxies ) == 0 {
return
}
ids := make ( [ ] int64 , 0 , len ( proxies ) )
for i := range proxies {
ids = append ( ids , proxies [ i ] . ID )
}
latencies , err := s . proxyLatencyCache . GetProxyLatencies ( ctx , ids )
if err != nil {
log . Printf ( "Warning: load proxy latency cache failed: %v" , err )
return
}
for i := range proxies {
info := latencies [ proxies [ i ] . ID ]
if info == nil {
continue
}
if info . Success {
proxies [ i ] . LatencyStatus = "success"
proxies [ i ] . LatencyMs = info . LatencyMs
} else {
proxies [ i ] . LatencyStatus = "failed"
}
proxies [ i ] . LatencyMessage = info . Message
2026-01-15 15:15:20 +08:00
proxies [ i ] . IPAddress = info . IPAddress
proxies [ i ] . Country = info . Country
proxies [ i ] . CountryCode = info . CountryCode
proxies [ i ] . Region = info . Region
proxies [ i ] . City = info . City
2026-01-14 19:45:29 +08:00
}
}
func ( s * adminServiceImpl ) saveProxyLatency ( ctx context . Context , proxyID int64 , info * ProxyLatencyInfo ) {
if s . proxyLatencyCache == nil || info == nil {
return
}
if err := s . proxyLatencyCache . SetProxyLatency ( ctx , proxyID , info ) ; err != nil {
log . Printf ( "Warning: store proxy latency cache failed: %v" , err )
}
}
2026-01-03 06:34:00 -08:00
// getAccountPlatform 根据账号 platform 判断混合渠道检查用的平台标识
func getAccountPlatform ( accountPlatform string ) string {
switch strings . ToLower ( strings . TrimSpace ( accountPlatform ) ) {
case PlatformAntigravity :
return "Antigravity"
case PlatformAnthropic , "claude" :
return "Anthropic"
default :
return ""
}
}
// MixedChannelError 混合渠道错误
type MixedChannelError struct {
GroupID int64
GroupName string
CurrentPlatform string
OtherPlatform string
}
func ( e * MixedChannelError ) Error ( ) string {
return fmt . Sprintf ( "mixed_channel_warning: Group '%s' contains both %s and %s accounts. Using mixed channels in the same context may cause thinking block signature validation issues, which will fallback to non-thinking mode for historical messages." ,
e . GroupName , e . CurrentPlatform , e . OtherPlatform )
}