Commit c86d445c authored by IanShaw027's avatar IanShaw027
Browse files

fix(frontend): sync with main and finalize i18n & component optimizations

parents 6c036d7b e78c8646
...@@ -166,14 +166,14 @@ func (_c *UserCreate) SetNillableNotes(v *string) *UserCreate { ...@@ -166,14 +166,14 @@ func (_c *UserCreate) SetNillableNotes(v *string) *UserCreate {
return _c return _c
} }
// AddAPIKeyIDs adds the "api_keys" edge to the ApiKey entity by IDs. // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_c *UserCreate) AddAPIKeyIDs(ids ...int64) *UserCreate { func (_c *UserCreate) AddAPIKeyIDs(ids ...int64) *UserCreate {
_c.mutation.AddAPIKeyIDs(ids...) _c.mutation.AddAPIKeyIDs(ids...)
return _c return _c
} }
// AddAPIKeys adds the "api_keys" edges to the ApiKey entity. // AddAPIKeys adds the "api_keys" edges to the APIKey entity.
func (_c *UserCreate) AddAPIKeys(v ...*ApiKey) *UserCreate { func (_c *UserCreate) AddAPIKeys(v ...*APIKey) *UserCreate {
ids := make([]int64, len(v)) ids := make([]int64, len(v))
for i := range v { for i := range v {
ids[i] = v[i].ID ids[i] = v[i].ID
......
...@@ -30,7 +30,7 @@ type UserQuery struct { ...@@ -30,7 +30,7 @@ type UserQuery struct {
order []user.OrderOption order []user.OrderOption
inters []Interceptor inters []Interceptor
predicates []predicate.User predicates []predicate.User
withAPIKeys *ApiKeyQuery withAPIKeys *APIKeyQuery
withRedeemCodes *RedeemCodeQuery withRedeemCodes *RedeemCodeQuery
withSubscriptions *UserSubscriptionQuery withSubscriptions *UserSubscriptionQuery
withAssignedSubscriptions *UserSubscriptionQuery withAssignedSubscriptions *UserSubscriptionQuery
...@@ -75,8 +75,8 @@ func (_q *UserQuery) Order(o ...user.OrderOption) *UserQuery { ...@@ -75,8 +75,8 @@ func (_q *UserQuery) Order(o ...user.OrderOption) *UserQuery {
} }
// QueryAPIKeys chains the current query on the "api_keys" edge. // QueryAPIKeys chains the current query on the "api_keys" edge.
func (_q *UserQuery) QueryAPIKeys() *ApiKeyQuery { func (_q *UserQuery) QueryAPIKeys() *APIKeyQuery {
query := (&ApiKeyClient{config: _q.config}).Query() query := (&APIKeyClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil { if err := _q.prepareQuery(ctx); err != nil {
return nil, err return nil, err
...@@ -458,8 +458,8 @@ func (_q *UserQuery) Clone() *UserQuery { ...@@ -458,8 +458,8 @@ func (_q *UserQuery) Clone() *UserQuery {
// WithAPIKeys tells the query-builder to eager-load the nodes that are connected to // WithAPIKeys tells the query-builder to eager-load the nodes that are connected to
// the "api_keys" edge. The optional arguments are used to configure the query builder of the edge. // the "api_keys" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *UserQuery) WithAPIKeys(opts ...func(*ApiKeyQuery)) *UserQuery { func (_q *UserQuery) WithAPIKeys(opts ...func(*APIKeyQuery)) *UserQuery {
query := (&ApiKeyClient{config: _q.config}).Query() query := (&APIKeyClient{config: _q.config}).Query()
for _, opt := range opts { for _, opt := range opts {
opt(query) opt(query)
} }
...@@ -653,8 +653,8 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e ...@@ -653,8 +653,8 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
} }
if query := _q.withAPIKeys; query != nil { if query := _q.withAPIKeys; query != nil {
if err := _q.loadAPIKeys(ctx, query, nodes, if err := _q.loadAPIKeys(ctx, query, nodes,
func(n *User) { n.Edges.APIKeys = []*ApiKey{} }, func(n *User) { n.Edges.APIKeys = []*APIKey{} },
func(n *User, e *ApiKey) { n.Edges.APIKeys = append(n.Edges.APIKeys, e) }); err != nil { func(n *User, e *APIKey) { n.Edges.APIKeys = append(n.Edges.APIKeys, e) }); err != nil {
return nil, err return nil, err
} }
} }
...@@ -712,7 +712,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e ...@@ -712,7 +712,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
return nodes, nil return nodes, nil
} }
func (_q *UserQuery) loadAPIKeys(ctx context.Context, query *ApiKeyQuery, nodes []*User, init func(*User), assign func(*User, *ApiKey)) error { func (_q *UserQuery) loadAPIKeys(ctx context.Context, query *APIKeyQuery, nodes []*User, init func(*User), assign func(*User, *APIKey)) error {
fks := make([]driver.Value, 0, len(nodes)) fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[int64]*User) nodeids := make(map[int64]*User)
for i := range nodes { for i := range nodes {
...@@ -725,7 +725,7 @@ func (_q *UserQuery) loadAPIKeys(ctx context.Context, query *ApiKeyQuery, nodes ...@@ -725,7 +725,7 @@ func (_q *UserQuery) loadAPIKeys(ctx context.Context, query *ApiKeyQuery, nodes
if len(query.ctx.Fields) > 0 { if len(query.ctx.Fields) > 0 {
query.ctx.AppendFieldOnce(apikey.FieldUserID) query.ctx.AppendFieldOnce(apikey.FieldUserID)
} }
query.Where(predicate.ApiKey(func(s *sql.Selector) { query.Where(predicate.APIKey(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(user.APIKeysColumn), fks...)) s.Where(sql.InValues(s.C(user.APIKeysColumn), fks...))
})) }))
neighbors, err := query.All(ctx) neighbors, err := query.All(ctx)
......
...@@ -186,14 +186,14 @@ func (_u *UserUpdate) SetNillableNotes(v *string) *UserUpdate { ...@@ -186,14 +186,14 @@ func (_u *UserUpdate) SetNillableNotes(v *string) *UserUpdate {
return _u return _u
} }
// AddAPIKeyIDs adds the "api_keys" edge to the ApiKey entity by IDs. // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate { func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate {
_u.mutation.AddAPIKeyIDs(ids...) _u.mutation.AddAPIKeyIDs(ids...)
return _u return _u
} }
// AddAPIKeys adds the "api_keys" edges to the ApiKey entity. // AddAPIKeys adds the "api_keys" edges to the APIKey entity.
func (_u *UserUpdate) AddAPIKeys(v ...*ApiKey) *UserUpdate { func (_u *UserUpdate) AddAPIKeys(v ...*APIKey) *UserUpdate {
ids := make([]int64, len(v)) ids := make([]int64, len(v))
for i := range v { for i := range v {
ids[i] = v[i].ID ids[i] = v[i].ID
...@@ -296,20 +296,20 @@ func (_u *UserUpdate) Mutation() *UserMutation { ...@@ -296,20 +296,20 @@ func (_u *UserUpdate) Mutation() *UserMutation {
return _u.mutation return _u.mutation
} }
// ClearAPIKeys clears all "api_keys" edges to the ApiKey entity. // ClearAPIKeys clears all "api_keys" edges to the APIKey entity.
func (_u *UserUpdate) ClearAPIKeys() *UserUpdate { func (_u *UserUpdate) ClearAPIKeys() *UserUpdate {
_u.mutation.ClearAPIKeys() _u.mutation.ClearAPIKeys()
return _u return _u
} }
// RemoveAPIKeyIDs removes the "api_keys" edge to ApiKey entities by IDs. // RemoveAPIKeyIDs removes the "api_keys" edge to APIKey entities by IDs.
func (_u *UserUpdate) RemoveAPIKeyIDs(ids ...int64) *UserUpdate { func (_u *UserUpdate) RemoveAPIKeyIDs(ids ...int64) *UserUpdate {
_u.mutation.RemoveAPIKeyIDs(ids...) _u.mutation.RemoveAPIKeyIDs(ids...)
return _u return _u
} }
// RemoveAPIKeys removes "api_keys" edges to ApiKey entities. // RemoveAPIKeys removes "api_keys" edges to APIKey entities.
func (_u *UserUpdate) RemoveAPIKeys(v ...*ApiKey) *UserUpdate { func (_u *UserUpdate) RemoveAPIKeys(v ...*APIKey) *UserUpdate {
ids := make([]int64, len(v)) ids := make([]int64, len(v))
for i := range v { for i := range v {
ids[i] = v[i].ID ids[i] = v[i].ID
...@@ -1065,14 +1065,14 @@ func (_u *UserUpdateOne) SetNillableNotes(v *string) *UserUpdateOne { ...@@ -1065,14 +1065,14 @@ func (_u *UserUpdateOne) SetNillableNotes(v *string) *UserUpdateOne {
return _u return _u
} }
// AddAPIKeyIDs adds the "api_keys" edge to the ApiKey entity by IDs. // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne { func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne {
_u.mutation.AddAPIKeyIDs(ids...) _u.mutation.AddAPIKeyIDs(ids...)
return _u return _u
} }
// AddAPIKeys adds the "api_keys" edges to the ApiKey entity. // AddAPIKeys adds the "api_keys" edges to the APIKey entity.
func (_u *UserUpdateOne) AddAPIKeys(v ...*ApiKey) *UserUpdateOne { func (_u *UserUpdateOne) AddAPIKeys(v ...*APIKey) *UserUpdateOne {
ids := make([]int64, len(v)) ids := make([]int64, len(v))
for i := range v { for i := range v {
ids[i] = v[i].ID ids[i] = v[i].ID
...@@ -1175,20 +1175,20 @@ func (_u *UserUpdateOne) Mutation() *UserMutation { ...@@ -1175,20 +1175,20 @@ func (_u *UserUpdateOne) Mutation() *UserMutation {
return _u.mutation return _u.mutation
} }
// ClearAPIKeys clears all "api_keys" edges to the ApiKey entity. // ClearAPIKeys clears all "api_keys" edges to the APIKey entity.
func (_u *UserUpdateOne) ClearAPIKeys() *UserUpdateOne { func (_u *UserUpdateOne) ClearAPIKeys() *UserUpdateOne {
_u.mutation.ClearAPIKeys() _u.mutation.ClearAPIKeys()
return _u return _u
} }
// RemoveAPIKeyIDs removes the "api_keys" edge to ApiKey entities by IDs. // RemoveAPIKeyIDs removes the "api_keys" edge to APIKey entities by IDs.
func (_u *UserUpdateOne) RemoveAPIKeyIDs(ids ...int64) *UserUpdateOne { func (_u *UserUpdateOne) RemoveAPIKeyIDs(ids ...int64) *UserUpdateOne {
_u.mutation.RemoveAPIKeyIDs(ids...) _u.mutation.RemoveAPIKeyIDs(ids...)
return _u return _u
} }
// RemoveAPIKeys removes "api_keys" edges to ApiKey entities. // RemoveAPIKeys removes "api_keys" edges to APIKey entities.
func (_u *UserUpdateOne) RemoveAPIKeys(v ...*ApiKey) *UserUpdateOne { func (_u *UserUpdateOne) RemoveAPIKeys(v ...*APIKey) *UserUpdateOne {
ids := make([]int64, len(v)) ids := make([]int64, len(v))
for i := range v { for i := range v {
ids[i] = v[i].ID ids[i] = v[i].ID
......
// Package config provides configuration loading, defaults, and validation.
package config package config
import ( import (
...@@ -139,7 +140,7 @@ type GatewayConfig struct { ...@@ -139,7 +140,7 @@ type GatewayConfig struct {
LogUpstreamErrorBodyMaxBytes int `mapstructure:"log_upstream_error_body_max_bytes"` LogUpstreamErrorBodyMaxBytes int `mapstructure:"log_upstream_error_body_max_bytes"`
// API-key 账号在客户端未提供 anthropic-beta 时,是否按需自动补齐(默认关闭以保持兼容) // API-key 账号在客户端未提供 anthropic-beta 时,是否按需自动补齐(默认关闭以保持兼容)
InjectBetaForApiKey bool `mapstructure:"inject_beta_for_apikey"` InjectBetaForAPIKey bool `mapstructure:"inject_beta_for_apikey"`
// 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义) // 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
FailoverOn400 bool `mapstructure:"failover_on_400"` FailoverOn400 bool `mapstructure:"failover_on_400"`
...@@ -241,7 +242,7 @@ type DefaultConfig struct { ...@@ -241,7 +242,7 @@ type DefaultConfig struct {
AdminPassword string `mapstructure:"admin_password"` AdminPassword string `mapstructure:"admin_password"`
UserConcurrency int `mapstructure:"user_concurrency"` UserConcurrency int `mapstructure:"user_concurrency"`
UserBalance float64 `mapstructure:"user_balance"` UserBalance float64 `mapstructure:"user_balance"`
ApiKeyPrefix string `mapstructure:"api_key_prefix"` APIKeyPrefix string `mapstructure:"api_key_prefix"`
RateMultiplier float64 `mapstructure:"rate_multiplier"` RateMultiplier float64 `mapstructure:"rate_multiplier"`
} }
......
// Package admin provides HTTP handlers for administrative operations.
package admin package admin
import ( import (
"errors"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
...@@ -69,42 +72,45 @@ func NewAccountHandler( ...@@ -69,42 +72,45 @@ func NewAccountHandler(
// CreateAccountRequest represents create account request // CreateAccountRequest represents create account request
type CreateAccountRequest struct { type CreateAccountRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Platform string `json:"platform" binding:"required"` Platform string `json:"platform" binding:"required"`
Type string `json:"type" binding:"required,oneof=oauth setup-token apikey"` Type string `json:"type" binding:"required,oneof=oauth setup-token apikey"`
Credentials map[string]any `json:"credentials" binding:"required"` Credentials map[string]any `json:"credentials" binding:"required"`
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
Concurrency int `json:"concurrency"` Concurrency int `json:"concurrency"`
Priority int `json:"priority"` Priority int `json:"priority"`
GroupIDs []int64 `json:"group_ids"` GroupIDs []int64 `json:"group_ids"`
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
} }
// UpdateAccountRequest represents update account request // UpdateAccountRequest represents update account request
// 使用指针类型来区分"未提供"和"设置为0" // 使用指针类型来区分"未提供"和"设置为0"
type UpdateAccountRequest struct { type UpdateAccountRequest struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey"` Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey"`
Credentials map[string]any `json:"credentials"` Credentials map[string]any `json:"credentials"`
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
Concurrency *int `json:"concurrency"` Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"` Priority *int `json:"priority"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"` Status string `json:"status" binding:"omitempty,oneof=active inactive"`
GroupIDs *[]int64 `json:"group_ids"` GroupIDs *[]int64 `json:"group_ids"`
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
} }
// BulkUpdateAccountsRequest represents the payload for bulk editing accounts // BulkUpdateAccountsRequest represents the payload for bulk editing accounts
type BulkUpdateAccountsRequest struct { type BulkUpdateAccountsRequest struct {
AccountIDs []int64 `json:"account_ids" binding:"required,min=1"` AccountIDs []int64 `json:"account_ids" binding:"required,min=1"`
Name string `json:"name"` Name string `json:"name"`
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
Concurrency *int `json:"concurrency"` Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"` Priority *int `json:"priority"`
Status string `json:"status" binding:"omitempty,oneof=active inactive error"` Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
GroupIDs *[]int64 `json:"group_ids"` GroupIDs *[]int64 `json:"group_ids"`
Credentials map[string]any `json:"credentials"` Credentials map[string]any `json:"credentials"`
Extra map[string]any `json:"extra"` Extra map[string]any `json:"extra"`
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
} }
// AccountWithConcurrency extends Account with real-time concurrency info // AccountWithConcurrency extends Account with real-time concurrency info
...@@ -179,18 +185,40 @@ func (h *AccountHandler) Create(c *gin.Context) { ...@@ -179,18 +185,40 @@ func (h *AccountHandler) Create(c *gin.Context) {
return return
} }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{ account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{
Name: req.Name, Name: req.Name,
Platform: req.Platform, Platform: req.Platform,
Type: req.Type, Type: req.Type,
Credentials: req.Credentials, Credentials: req.Credentials,
Extra: req.Extra, Extra: req.Extra,
ProxyID: req.ProxyID, ProxyID: req.ProxyID,
Concurrency: req.Concurrency, Concurrency: req.Concurrency,
Priority: req.Priority, Priority: req.Priority,
GroupIDs: req.GroupIDs, GroupIDs: req.GroupIDs,
SkipMixedChannelCheck: skipCheck,
}) })
if err != nil { if err != nil {
// 检查是否为混合渠道错误
var mixedErr *service.MixedChannelError
if errors.As(err, &mixedErr) {
// 返回特殊错误码要求确认
c.JSON(409, gin.H{
"error": "mixed_channel_warning",
"message": mixedErr.Error(),
"details": gin.H{
"group_id": mixedErr.GroupID,
"group_name": mixedErr.GroupName,
"current_platform": mixedErr.CurrentPlatform,
"other_platform": mixedErr.OtherPlatform,
},
"require_confirmation": true,
})
return
}
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
...@@ -213,18 +241,40 @@ func (h *AccountHandler) Update(c *gin.Context) { ...@@ -213,18 +241,40 @@ func (h *AccountHandler) Update(c *gin.Context) {
return return
} }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
account, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{ account, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{
Name: req.Name, Name: req.Name,
Type: req.Type, Type: req.Type,
Credentials: req.Credentials, Credentials: req.Credentials,
Extra: req.Extra, Extra: req.Extra,
ProxyID: req.ProxyID, ProxyID: req.ProxyID,
Concurrency: req.Concurrency, // 指针类型,nil 表示未提供 Concurrency: req.Concurrency, // 指针类型,nil 表示未提供
Priority: req.Priority, // 指针类型,nil 表示未提供 Priority: req.Priority, // 指针类型,nil 表示未提供
Status: req.Status, Status: req.Status,
GroupIDs: req.GroupIDs, GroupIDs: req.GroupIDs,
SkipMixedChannelCheck: skipCheck,
}) })
if err != nil { if err != nil {
// 检查是否为混合渠道错误
var mixedErr *service.MixedChannelError
if errors.As(err, &mixedErr) {
// 返回特殊错误码要求确认
c.JSON(409, gin.H{
"error": "mixed_channel_warning",
"message": mixedErr.Error(),
"details": gin.H{
"group_id": mixedErr.GroupID,
"group_name": mixedErr.GroupName,
"current_platform": mixedErr.CurrentPlatform,
"other_platform": mixedErr.OtherPlatform,
},
"require_confirmation": true,
})
return
}
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
...@@ -568,6 +618,9 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) { ...@@ -568,6 +618,9 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
return return
} }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
hasUpdates := req.Name != "" || hasUpdates := req.Name != "" ||
req.ProxyID != nil || req.ProxyID != nil ||
req.Concurrency != nil || req.Concurrency != nil ||
...@@ -583,15 +636,16 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) { ...@@ -583,15 +636,16 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
} }
result, err := h.adminService.BulkUpdateAccounts(c.Request.Context(), &service.BulkUpdateAccountsInput{ result, err := h.adminService.BulkUpdateAccounts(c.Request.Context(), &service.BulkUpdateAccountsInput{
AccountIDs: req.AccountIDs, AccountIDs: req.AccountIDs,
Name: req.Name, Name: req.Name,
ProxyID: req.ProxyID, ProxyID: req.ProxyID,
Concurrency: req.Concurrency, Concurrency: req.Concurrency,
Priority: req.Priority, Priority: req.Priority,
Status: req.Status, Status: req.Status,
GroupIDs: req.GroupIDs, GroupIDs: req.GroupIDs,
Credentials: req.Credentials, Credentials: req.Credentials,
Extra: req.Extra, Extra: req.Extra,
SkipMixedChannelCheck: skipCheck,
}) })
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
...@@ -781,6 +835,49 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) { ...@@ -781,6 +835,49 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) {
response.Success(c, gin.H{"message": "Rate limit cleared successfully"}) response.Success(c, gin.H{"message": "Rate limit cleared successfully"})
} }
// GetTempUnschedulable handles getting temporary unschedulable status
// GET /api/v1/admin/accounts/:id/temp-unschedulable
func (h *AccountHandler) GetTempUnschedulable(c *gin.Context) {
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid account ID")
return
}
state, err := h.rateLimitService.GetTempUnschedStatus(c.Request.Context(), accountID)
if err != nil {
response.ErrorFrom(c, err)
return
}
if state == nil || state.UntilUnix <= time.Now().Unix() {
response.Success(c, gin.H{"active": false})
return
}
response.Success(c, gin.H{
"active": true,
"state": state,
})
}
// ClearTempUnschedulable handles clearing temporary unschedulable status
// DELETE /api/v1/admin/accounts/:id/temp-unschedulable
func (h *AccountHandler) ClearTempUnschedulable(c *gin.Context) {
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid account ID")
return
}
if err := h.rateLimitService.ClearTempUnschedulable(c.Request.Context(), accountID); err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, gin.H{"message": "Temp unschedulable cleared successfully"})
}
// GetTodayStats handles getting account today statistics // GetTodayStats handles getting account today statistics
// GET /api/v1/admin/accounts/:id/today-stats // GET /api/v1/admin/accounts/:id/today-stats
func (h *AccountHandler) GetTodayStats(c *gin.Context) { func (h *AccountHandler) GetTodayStats(c *gin.Context) {
......
...@@ -75,8 +75,8 @@ func (h *DashboardHandler) GetStats(c *gin.Context) { ...@@ -75,8 +75,8 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
"active_users": stats.ActiveUsers, "active_users": stats.ActiveUsers,
// API Key 统计 // API Key 统计
"total_api_keys": stats.TotalApiKeys, "total_api_keys": stats.TotalAPIKeys,
"active_api_keys": stats.ActiveApiKeys, "active_api_keys": stats.ActiveAPIKeys,
// 账户统计 // 账户统计
"total_accounts": stats.TotalAccounts, "total_accounts": stats.TotalAccounts,
...@@ -193,10 +193,10 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) { ...@@ -193,10 +193,10 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
}) })
} }
// GetApiKeyUsageTrend handles getting API key usage trend data // GetAPIKeyUsageTrend handles getting API key usage trend data
// GET /api/v1/admin/dashboard/api-keys-trend // GET /api/v1/admin/dashboard/api-keys-trend
// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 5) // Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 5)
func (h *DashboardHandler) GetApiKeyUsageTrend(c *gin.Context) { func (h *DashboardHandler) GetAPIKeyUsageTrend(c *gin.Context) {
startTime, endTime := parseTimeRange(c) startTime, endTime := parseTimeRange(c)
granularity := c.DefaultQuery("granularity", "day") granularity := c.DefaultQuery("granularity", "day")
limitStr := c.DefaultQuery("limit", "5") limitStr := c.DefaultQuery("limit", "5")
...@@ -205,7 +205,7 @@ func (h *DashboardHandler) GetApiKeyUsageTrend(c *gin.Context) { ...@@ -205,7 +205,7 @@ func (h *DashboardHandler) GetApiKeyUsageTrend(c *gin.Context) {
limit = 5 limit = 5
} }
trend, err := h.dashboardService.GetApiKeyUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) trend, err := h.dashboardService.GetAPIKeyUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit)
if err != nil { if err != nil {
response.Error(c, 500, "Failed to get API key usage trend") response.Error(c, 500, "Failed to get API key usage trend")
return return
...@@ -273,26 +273,26 @@ func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) { ...@@ -273,26 +273,26 @@ func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) {
response.Success(c, gin.H{"stats": stats}) response.Success(c, gin.H{"stats": stats})
} }
// BatchApiKeysUsageRequest represents the request body for batch api key usage stats // BatchAPIKeysUsageRequest represents the request body for batch api key usage stats
type BatchApiKeysUsageRequest struct { type BatchAPIKeysUsageRequest struct {
ApiKeyIDs []int64 `json:"api_key_ids" binding:"required"` APIKeyIDs []int64 `json:"api_key_ids" binding:"required"`
} }
// GetBatchApiKeysUsage handles getting usage stats for multiple API keys // GetBatchAPIKeysUsage handles getting usage stats for multiple API keys
// POST /api/v1/admin/dashboard/api-keys-usage // POST /api/v1/admin/dashboard/api-keys-usage
func (h *DashboardHandler) GetBatchApiKeysUsage(c *gin.Context) { func (h *DashboardHandler) GetBatchAPIKeysUsage(c *gin.Context) {
var req BatchApiKeysUsageRequest var req BatchAPIKeysUsageRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error()) response.BadRequest(c, "Invalid request: "+err.Error())
return return
} }
if len(req.ApiKeyIDs) == 0 { if len(req.APIKeyIDs) == 0 {
response.Success(c, gin.H{"stats": map[string]any{}}) response.Success(c, gin.H{"stats": map[string]any{}})
return return
} }
stats, err := h.dashboardService.GetBatchApiKeyUsageStats(c.Request.Context(), req.ApiKeyIDs) stats, err := h.dashboardService.GetBatchAPIKeyUsageStats(c.Request.Context(), req.APIKeyIDs)
if err != nil { if err != nil {
response.Error(c, 500, "Failed to get API key usage stats") response.Error(c, 500, "Failed to get API key usage stats")
return return
......
...@@ -18,6 +18,7 @@ func NewGeminiOAuthHandler(geminiOAuthService *service.GeminiOAuthService) *Gemi ...@@ -18,6 +18,7 @@ func NewGeminiOAuthHandler(geminiOAuthService *service.GeminiOAuthService) *Gemi
return &GeminiOAuthHandler{geminiOAuthService: geminiOAuthService} return &GeminiOAuthHandler{geminiOAuthService: geminiOAuthService}
} }
// GetCapabilities returns the Gemini OAuth configuration capabilities.
// GET /api/v1/admin/gemini/oauth/capabilities // GET /api/v1/admin/gemini/oauth/capabilities
func (h *GeminiOAuthHandler) GetCapabilities(c *gin.Context) { func (h *GeminiOAuthHandler) GetCapabilities(c *gin.Context) {
cfg := h.geminiOAuthService.GetOAuthConfig() cfg := h.geminiOAuthService.GetOAuthConfig()
...@@ -30,6 +31,8 @@ type GeminiGenerateAuthURLRequest struct { ...@@ -30,6 +31,8 @@ type GeminiGenerateAuthURLRequest struct {
// OAuth 类型: "code_assist" (需要 project_id) 或 "ai_studio" (不需要 project_id) // OAuth 类型: "code_assist" (需要 project_id) 或 "ai_studio" (不需要 project_id)
// 默认为 "code_assist" 以保持向后兼容 // 默认为 "code_assist" 以保持向后兼容
OAuthType string `json:"oauth_type"` OAuthType string `json:"oauth_type"`
// TierID is a user-selected tier to be used when auto detection is unavailable or fails.
TierID string `json:"tier_id"`
} }
// GenerateAuthURL generates Google OAuth authorization URL for Gemini. // GenerateAuthURL generates Google OAuth authorization URL for Gemini.
...@@ -54,7 +57,7 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) { ...@@ -54,7 +57,7 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
// Always pass the "hosted" callback URI; the OAuth service may override it depending on // Always pass the "hosted" callback URI; the OAuth service may override it depending on
// oauth_type and whether the built-in Gemini CLI OAuth client is used. // oauth_type and whether the built-in Gemini CLI OAuth client is used.
redirectURI := deriveGeminiRedirectURI(c) redirectURI := deriveGeminiRedirectURI(c)
result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, redirectURI, req.ProjectID, oauthType) result, err := h.geminiOAuthService.GenerateAuthURL(c.Request.Context(), req.ProxyID, redirectURI, req.ProjectID, oauthType, req.TierID)
if err != nil { if err != nil {
msg := err.Error() msg := err.Error()
// Treat missing/invalid OAuth client configuration as a user/config error. // Treat missing/invalid OAuth client configuration as a user/config error.
...@@ -76,6 +79,9 @@ type GeminiExchangeCodeRequest struct { ...@@ -76,6 +79,9 @@ type GeminiExchangeCodeRequest struct {
ProxyID *int64 `json:"proxy_id"` ProxyID *int64 `json:"proxy_id"`
// OAuth 类型: "code_assist" 或 "ai_studio",需要与 GenerateAuthURL 时的类型一致 // OAuth 类型: "code_assist" 或 "ai_studio",需要与 GenerateAuthURL 时的类型一致
OAuthType string `json:"oauth_type"` OAuthType string `json:"oauth_type"`
// TierID is a user-selected tier to be used when auto detection is unavailable or fails.
// This field is optional; when omitted, the server uses the tier stored in the OAuth session.
TierID string `json:"tier_id"`
} }
// ExchangeCode exchanges authorization code for tokens. // ExchangeCode exchanges authorization code for tokens.
...@@ -103,6 +109,7 @@ func (h *GeminiOAuthHandler) ExchangeCode(c *gin.Context) { ...@@ -103,6 +109,7 @@ func (h *GeminiOAuthHandler) ExchangeCode(c *gin.Context) {
Code: req.Code, Code: req.Code,
ProxyID: req.ProxyID, ProxyID: req.ProxyID,
OAuthType: oauthType, OAuthType: oauthType,
TierID: req.TierID,
}) })
if err != nil { if err != nil {
response.BadRequest(c, "Failed to exchange code: "+err.Error()) response.BadRequest(c, "Failed to exchange code: "+err.Error())
......
...@@ -237,9 +237,9 @@ func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) { ...@@ -237,9 +237,9 @@ func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {
return return
} }
outKeys := make([]dto.ApiKey, 0, len(keys)) outKeys := make([]dto.APIKey, 0, len(keys))
for i := range keys { for i := range keys {
outKeys = append(outKeys, *dto.ApiKeyFromService(&keys[i])) outKeys = append(outKeys, *dto.APIKeyFromService(&keys[i]))
} }
response.Paginated(c, outKeys, total, page, pageSize) response.Paginated(c, outKeys, total, page, pageSize)
} }
...@@ -34,26 +34,31 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ...@@ -34,26 +34,31 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
} }
response.Success(c, dto.SystemSettings{ response.Success(c, dto.SystemSettings{
RegistrationEnabled: settings.RegistrationEnabled, RegistrationEnabled: settings.RegistrationEnabled,
EmailVerifyEnabled: settings.EmailVerifyEnabled, EmailVerifyEnabled: settings.EmailVerifyEnabled,
SmtpHost: settings.SmtpHost, SMTPHost: settings.SMTPHost,
SmtpPort: settings.SmtpPort, SMTPPort: settings.SMTPPort,
SmtpUsername: settings.SmtpUsername, SMTPUsername: settings.SMTPUsername,
SmtpPassword: settings.SmtpPassword, SMTPPassword: settings.SMTPPassword,
SmtpFrom: settings.SmtpFrom, SMTPFrom: settings.SMTPFrom,
SmtpFromName: settings.SmtpFromName, SMTPFromName: settings.SMTPFromName,
SmtpUseTLS: settings.SmtpUseTLS, SMTPUseTLS: settings.SMTPUseTLS,
TurnstileEnabled: settings.TurnstileEnabled, TurnstileEnabled: settings.TurnstileEnabled,
TurnstileSiteKey: settings.TurnstileSiteKey, TurnstileSiteKey: settings.TurnstileSiteKey,
TurnstileSecretKey: settings.TurnstileSecretKey, TurnstileSecretKey: settings.TurnstileSecretKey,
SiteName: settings.SiteName, SiteName: settings.SiteName,
SiteLogo: settings.SiteLogo, SiteLogo: settings.SiteLogo,
SiteSubtitle: settings.SiteSubtitle, SiteSubtitle: settings.SiteSubtitle,
ApiBaseUrl: settings.ApiBaseUrl, APIBaseURL: settings.APIBaseURL,
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocUrl: settings.DocUrl, DocURL: settings.DocURL,
DefaultConcurrency: settings.DefaultConcurrency, DefaultConcurrency: settings.DefaultConcurrency,
DefaultBalance: settings.DefaultBalance, DefaultBalance: settings.DefaultBalance,
EnableModelFallback: settings.EnableModelFallback,
FallbackModelAnthropic: settings.FallbackModelAnthropic,
FallbackModelOpenAI: settings.FallbackModelOpenAI,
FallbackModelGemini: settings.FallbackModelGemini,
FallbackModelAntigravity: settings.FallbackModelAntigravity,
}) })
} }
...@@ -64,13 +69,13 @@ type UpdateSettingsRequest struct { ...@@ -64,13 +69,13 @@ type UpdateSettingsRequest struct {
EmailVerifyEnabled bool `json:"email_verify_enabled"` EmailVerifyEnabled bool `json:"email_verify_enabled"`
// 邮件服务设置 // 邮件服务设置
SmtpHost string `json:"smtp_host"` SMTPHost string `json:"smtp_host"`
SmtpPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SmtpUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SmtpPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
SmtpFrom string `json:"smtp_from_email"` SMTPFrom string `json:"smtp_from_email"`
SmtpFromName string `json:"smtp_from_name"` SMTPFromName string `json:"smtp_from_name"`
SmtpUseTLS bool `json:"smtp_use_tls"` SMTPUseTLS bool `json:"smtp_use_tls"`
// Cloudflare Turnstile 设置 // Cloudflare Turnstile 设置
TurnstileEnabled bool `json:"turnstile_enabled"` TurnstileEnabled bool `json:"turnstile_enabled"`
...@@ -81,13 +86,20 @@ type UpdateSettingsRequest struct { ...@@ -81,13 +86,20 @@ type UpdateSettingsRequest struct {
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
ApiBaseUrl string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocUrl string `json:"doc_url"` DocURL string `json:"doc_url"`
// 默认配置 // 默认配置
DefaultConcurrency int `json:"default_concurrency"` DefaultConcurrency int `json:"default_concurrency"`
DefaultBalance float64 `json:"default_balance"` DefaultBalance float64 `json:"default_balance"`
// Model fallback configuration
EnableModelFallback bool `json:"enable_model_fallback"`
FallbackModelAnthropic string `json:"fallback_model_anthropic"`
FallbackModelOpenAI string `json:"fallback_model_openai"`
FallbackModelGemini string `json:"fallback_model_gemini"`
FallbackModelAntigravity string `json:"fallback_model_antigravity"`
} }
// UpdateSettings 更新系统设置 // UpdateSettings 更新系统设置
...@@ -106,8 +118,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -106,8 +118,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
if req.DefaultBalance < 0 { if req.DefaultBalance < 0 {
req.DefaultBalance = 0 req.DefaultBalance = 0
} }
if req.SmtpPort <= 0 { if req.SMTPPort <= 0 {
req.SmtpPort = 587 req.SMTPPort = 587
} }
// Turnstile 参数验证 // Turnstile 参数验证
...@@ -141,26 +153,31 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -141,26 +153,31 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
settings := &service.SystemSettings{ settings := &service.SystemSettings{
RegistrationEnabled: req.RegistrationEnabled, RegistrationEnabled: req.RegistrationEnabled,
EmailVerifyEnabled: req.EmailVerifyEnabled, EmailVerifyEnabled: req.EmailVerifyEnabled,
SmtpHost: req.SmtpHost, SMTPHost: req.SMTPHost,
SmtpPort: req.SmtpPort, SMTPPort: req.SMTPPort,
SmtpUsername: req.SmtpUsername, SMTPUsername: req.SMTPUsername,
SmtpPassword: req.SmtpPassword, SMTPPassword: req.SMTPPassword,
SmtpFrom: req.SmtpFrom, SMTPFrom: req.SMTPFrom,
SmtpFromName: req.SmtpFromName, SMTPFromName: req.SMTPFromName,
SmtpUseTLS: req.SmtpUseTLS, SMTPUseTLS: req.SMTPUseTLS,
TurnstileEnabled: req.TurnstileEnabled, TurnstileEnabled: req.TurnstileEnabled,
TurnstileSiteKey: req.TurnstileSiteKey, TurnstileSiteKey: req.TurnstileSiteKey,
TurnstileSecretKey: req.TurnstileSecretKey, TurnstileSecretKey: req.TurnstileSecretKey,
SiteName: req.SiteName, SiteName: req.SiteName,
SiteLogo: req.SiteLogo, SiteLogo: req.SiteLogo,
SiteSubtitle: req.SiteSubtitle, SiteSubtitle: req.SiteSubtitle,
ApiBaseUrl: req.ApiBaseUrl, APIBaseURL: req.APIBaseURL,
ContactInfo: req.ContactInfo, ContactInfo: req.ContactInfo,
DocUrl: req.DocUrl, DocURL: req.DocURL,
DefaultConcurrency: req.DefaultConcurrency, DefaultConcurrency: req.DefaultConcurrency,
DefaultBalance: req.DefaultBalance, DefaultBalance: req.DefaultBalance,
EnableModelFallback: req.EnableModelFallback,
FallbackModelAnthropic: req.FallbackModelAnthropic,
FallbackModelOpenAI: req.FallbackModelOpenAI,
FallbackModelGemini: req.FallbackModelGemini,
FallbackModelAntigravity: req.FallbackModelAntigravity,
} }
if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil { if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil {
...@@ -176,69 +193,74 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ...@@ -176,69 +193,74 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
} }
response.Success(c, dto.SystemSettings{ response.Success(c, dto.SystemSettings{
RegistrationEnabled: updatedSettings.RegistrationEnabled, RegistrationEnabled: updatedSettings.RegistrationEnabled,
EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled, EmailVerifyEnabled: updatedSettings.EmailVerifyEnabled,
SmtpHost: updatedSettings.SmtpHost, SMTPHost: updatedSettings.SMTPHost,
SmtpPort: updatedSettings.SmtpPort, SMTPPort: updatedSettings.SMTPPort,
SmtpUsername: updatedSettings.SmtpUsername, SMTPUsername: updatedSettings.SMTPUsername,
SmtpPassword: updatedSettings.SmtpPassword, SMTPPassword: updatedSettings.SMTPPassword,
SmtpFrom: updatedSettings.SmtpFrom, SMTPFrom: updatedSettings.SMTPFrom,
SmtpFromName: updatedSettings.SmtpFromName, SMTPFromName: updatedSettings.SMTPFromName,
SmtpUseTLS: updatedSettings.SmtpUseTLS, SMTPUseTLS: updatedSettings.SMTPUseTLS,
TurnstileEnabled: updatedSettings.TurnstileEnabled, TurnstileEnabled: updatedSettings.TurnstileEnabled,
TurnstileSiteKey: updatedSettings.TurnstileSiteKey, TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
TurnstileSecretKey: updatedSettings.TurnstileSecretKey, TurnstileSecretKey: updatedSettings.TurnstileSecretKey,
SiteName: updatedSettings.SiteName, SiteName: updatedSettings.SiteName,
SiteLogo: updatedSettings.SiteLogo, SiteLogo: updatedSettings.SiteLogo,
SiteSubtitle: updatedSettings.SiteSubtitle, SiteSubtitle: updatedSettings.SiteSubtitle,
ApiBaseUrl: updatedSettings.ApiBaseUrl, APIBaseURL: updatedSettings.APIBaseURL,
ContactInfo: updatedSettings.ContactInfo, ContactInfo: updatedSettings.ContactInfo,
DocUrl: updatedSettings.DocUrl, DocURL: updatedSettings.DocURL,
DefaultConcurrency: updatedSettings.DefaultConcurrency, DefaultConcurrency: updatedSettings.DefaultConcurrency,
DefaultBalance: updatedSettings.DefaultBalance, DefaultBalance: updatedSettings.DefaultBalance,
EnableModelFallback: updatedSettings.EnableModelFallback,
FallbackModelAnthropic: updatedSettings.FallbackModelAnthropic,
FallbackModelOpenAI: updatedSettings.FallbackModelOpenAI,
FallbackModelGemini: updatedSettings.FallbackModelGemini,
FallbackModelAntigravity: updatedSettings.FallbackModelAntigravity,
}) })
} }
// TestSmtpRequest 测试SMTP连接请求 // TestSMTPRequest 测试SMTP连接请求
type TestSmtpRequest struct { type TestSMTPRequest struct {
SmtpHost string `json:"smtp_host" binding:"required"` SMTPHost string `json:"smtp_host" binding:"required"`
SmtpPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SmtpUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SmtpPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
SmtpUseTLS bool `json:"smtp_use_tls"` SMTPUseTLS bool `json:"smtp_use_tls"`
} }
// TestSmtpConnection 测试SMTP连接 // TestSMTPConnection 测试SMTP连接
// POST /api/v1/admin/settings/test-smtp // POST /api/v1/admin/settings/test-smtp
func (h *SettingHandler) TestSmtpConnection(c *gin.Context) { func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
var req TestSmtpRequest var req TestSMTPRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error()) response.BadRequest(c, "Invalid request: "+err.Error())
return return
} }
if req.SmtpPort <= 0 { if req.SMTPPort <= 0 {
req.SmtpPort = 587 req.SMTPPort = 587
} }
// 如果未提供密码,从数据库获取已保存的密码 // 如果未提供密码,从数据库获取已保存的密码
password := req.SmtpPassword password := req.SMTPPassword
if password == "" { if password == "" {
savedConfig, err := h.emailService.GetSmtpConfig(c.Request.Context()) savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
if err == nil && savedConfig != nil { if err == nil && savedConfig != nil {
password = savedConfig.Password password = savedConfig.Password
} }
} }
config := &service.SmtpConfig{ config := &service.SMTPConfig{
Host: req.SmtpHost, Host: req.SMTPHost,
Port: req.SmtpPort, Port: req.SMTPPort,
Username: req.SmtpUsername, Username: req.SMTPUsername,
Password: password, Password: password,
UseTLS: req.SmtpUseTLS, UseTLS: req.SMTPUseTLS,
} }
err := h.emailService.TestSmtpConnectionWithConfig(config) err := h.emailService.TestSMTPConnectionWithConfig(config)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -250,13 +272,13 @@ func (h *SettingHandler) TestSmtpConnection(c *gin.Context) { ...@@ -250,13 +272,13 @@ func (h *SettingHandler) TestSmtpConnection(c *gin.Context) {
// SendTestEmailRequest 发送测试邮件请求 // SendTestEmailRequest 发送测试邮件请求
type SendTestEmailRequest struct { type SendTestEmailRequest struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`
SmtpHost string `json:"smtp_host" binding:"required"` SMTPHost string `json:"smtp_host" binding:"required"`
SmtpPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SmtpUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SmtpPassword string `json:"smtp_password"` SMTPPassword string `json:"smtp_password"`
SmtpFrom string `json:"smtp_from_email"` SMTPFrom string `json:"smtp_from_email"`
SmtpFromName string `json:"smtp_from_name"` SMTPFromName string `json:"smtp_from_name"`
SmtpUseTLS bool `json:"smtp_use_tls"` SMTPUseTLS bool `json:"smtp_use_tls"`
} }
// SendTestEmail 发送测试邮件 // SendTestEmail 发送测试邮件
...@@ -268,27 +290,27 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) { ...@@ -268,27 +290,27 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
return return
} }
if req.SmtpPort <= 0 { if req.SMTPPort <= 0 {
req.SmtpPort = 587 req.SMTPPort = 587
} }
// 如果未提供密码,从数据库获取已保存的密码 // 如果未提供密码,从数据库获取已保存的密码
password := req.SmtpPassword password := req.SMTPPassword
if password == "" { if password == "" {
savedConfig, err := h.emailService.GetSmtpConfig(c.Request.Context()) savedConfig, err := h.emailService.GetSMTPConfig(c.Request.Context())
if err == nil && savedConfig != nil { if err == nil && savedConfig != nil {
password = savedConfig.Password password = savedConfig.Password
} }
} }
config := &service.SmtpConfig{ config := &service.SMTPConfig{
Host: req.SmtpHost, Host: req.SMTPHost,
Port: req.SmtpPort, Port: req.SMTPPort,
Username: req.SmtpUsername, Username: req.SMTPUsername,
Password: password, Password: password,
From: req.SmtpFrom, From: req.SMTPFrom,
FromName: req.SmtpFromName, FromName: req.SMTPFromName,
UseTLS: req.SmtpUseTLS, UseTLS: req.SMTPUseTLS,
} }
siteName := h.settingService.GetSiteName(c.Request.Context()) siteName := h.settingService.GetSiteName(c.Request.Context())
...@@ -333,10 +355,10 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) { ...@@ -333,10 +355,10 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
response.Success(c, gin.H{"message": "Test email sent successfully"}) response.Success(c, gin.H{"message": "Test email sent successfully"})
} }
// GetAdminApiKey 获取管理员 API Key 状态 // GetAdminAPIKey 获取管理员 API Key 状态
// GET /api/v1/admin/settings/admin-api-key // GET /api/v1/admin/settings/admin-api-key
func (h *SettingHandler) GetAdminApiKey(c *gin.Context) { func (h *SettingHandler) GetAdminAPIKey(c *gin.Context) {
maskedKey, exists, err := h.settingService.GetAdminApiKeyStatus(c.Request.Context()) maskedKey, exists, err := h.settingService.GetAdminAPIKeyStatus(c.Request.Context())
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -348,10 +370,10 @@ func (h *SettingHandler) GetAdminApiKey(c *gin.Context) { ...@@ -348,10 +370,10 @@ func (h *SettingHandler) GetAdminApiKey(c *gin.Context) {
}) })
} }
// RegenerateAdminApiKey 生成/重新生成管理员 API Key // RegenerateAdminAPIKey 生成/重新生成管理员 API Key
// POST /api/v1/admin/settings/admin-api-key/regenerate // POST /api/v1/admin/settings/admin-api-key/regenerate
func (h *SettingHandler) RegenerateAdminApiKey(c *gin.Context) { func (h *SettingHandler) RegenerateAdminAPIKey(c *gin.Context) {
key, err := h.settingService.GenerateAdminApiKey(c.Request.Context()) key, err := h.settingService.GenerateAdminAPIKey(c.Request.Context())
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -362,10 +384,10 @@ func (h *SettingHandler) RegenerateAdminApiKey(c *gin.Context) { ...@@ -362,10 +384,10 @@ func (h *SettingHandler) RegenerateAdminApiKey(c *gin.Context) {
}) })
} }
// DeleteAdminApiKey 删除管理员 API Key // DeleteAdminAPIKey 删除管理员 API Key
// DELETE /api/v1/admin/settings/admin-api-key // DELETE /api/v1/admin/settings/admin-api-key
func (h *SettingHandler) DeleteAdminApiKey(c *gin.Context) { func (h *SettingHandler) DeleteAdminAPIKey(c *gin.Context) {
if err := h.settingService.DeleteAdminApiKey(c.Request.Context()); err != nil { if err := h.settingService.DeleteAdminAPIKey(c.Request.Context()); err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
......
...@@ -17,14 +17,14 @@ import ( ...@@ -17,14 +17,14 @@ import (
// UsageHandler handles admin usage-related requests // UsageHandler handles admin usage-related requests
type UsageHandler struct { type UsageHandler struct {
usageService *service.UsageService usageService *service.UsageService
apiKeyService *service.ApiKeyService apiKeyService *service.APIKeyService
adminService service.AdminService adminService service.AdminService
} }
// NewUsageHandler creates a new admin usage handler // NewUsageHandler creates a new admin usage handler
func NewUsageHandler( func NewUsageHandler(
usageService *service.UsageService, usageService *service.UsageService,
apiKeyService *service.ApiKeyService, apiKeyService *service.APIKeyService,
adminService service.AdminService, adminService service.AdminService,
) *UsageHandler { ) *UsageHandler {
return &UsageHandler{ return &UsageHandler{
...@@ -125,7 +125,7 @@ func (h *UsageHandler) List(c *gin.Context) { ...@@ -125,7 +125,7 @@ func (h *UsageHandler) List(c *gin.Context) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize}
filters := usagestats.UsageLogFilters{ filters := usagestats.UsageLogFilters{
UserID: userID, UserID: userID,
ApiKeyID: apiKeyID, APIKeyID: apiKeyID,
AccountID: accountID, AccountID: accountID,
GroupID: groupID, GroupID: groupID,
Model: model, Model: model,
...@@ -207,7 +207,7 @@ func (h *UsageHandler) Stats(c *gin.Context) { ...@@ -207,7 +207,7 @@ func (h *UsageHandler) Stats(c *gin.Context) {
} }
if apiKeyID > 0 { if apiKeyID > 0 {
stats, err := h.usageService.GetStatsByApiKey(c.Request.Context(), apiKeyID, startTime, endTime) stats, err := h.usageService.GetStatsByAPIKey(c.Request.Context(), apiKeyID, startTime, endTime)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -269,9 +269,9 @@ func (h *UsageHandler) SearchUsers(c *gin.Context) { ...@@ -269,9 +269,9 @@ func (h *UsageHandler) SearchUsers(c *gin.Context) {
response.Success(c, result) response.Success(c, result)
} }
// SearchApiKeys handles searching API keys by user // SearchAPIKeys handles searching API keys by user
// GET /api/v1/admin/usage/search-api-keys // GET /api/v1/admin/usage/search-api-keys
func (h *UsageHandler) SearchApiKeys(c *gin.Context) { func (h *UsageHandler) SearchAPIKeys(c *gin.Context) {
userIDStr := c.Query("user_id") userIDStr := c.Query("user_id")
keyword := c.Query("q") keyword := c.Query("q")
...@@ -285,22 +285,22 @@ func (h *UsageHandler) SearchApiKeys(c *gin.Context) { ...@@ -285,22 +285,22 @@ func (h *UsageHandler) SearchApiKeys(c *gin.Context) {
userID = id userID = id
} }
keys, err := h.apiKeyService.SearchApiKeys(c.Request.Context(), userID, keyword, 30) keys, err := h.apiKeyService.SearchAPIKeys(c.Request.Context(), userID, keyword, 30)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
// Return simplified API key list (only id and name) // Return simplified API key list (only id and name)
type SimpleApiKey struct { type SimpleAPIKey struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
} }
result := make([]SimpleApiKey, len(keys)) result := make([]SimpleAPIKey, len(keys))
for i, k := range keys { for i, k := range keys {
result[i] = SimpleApiKey{ result[i] = SimpleAPIKey{
ID: k.ID, ID: k.ID,
Name: k.Name, Name: k.Name,
UserID: k.UserID, UserID: k.UserID,
......
...@@ -243,9 +243,9 @@ func (h *UserHandler) GetUserAPIKeys(c *gin.Context) { ...@@ -243,9 +243,9 @@ func (h *UserHandler) GetUserAPIKeys(c *gin.Context) {
return return
} }
out := make([]dto.ApiKey, 0, len(keys)) out := make([]dto.APIKey, 0, len(keys))
for i := range keys { for i := range keys {
out = append(out, *dto.ApiKeyFromService(&keys[i])) out = append(out, *dto.APIKeyFromService(&keys[i]))
} }
response.Paginated(c, out, total, page, pageSize) response.Paginated(c, out, total, page, pageSize)
} }
......
// Package handler provides HTTP request handlers for the application.
package handler package handler
import ( import (
...@@ -14,11 +15,11 @@ import ( ...@@ -14,11 +15,11 @@ import (
// APIKeyHandler handles API key-related requests // APIKeyHandler handles API key-related requests
type APIKeyHandler struct { type APIKeyHandler struct {
apiKeyService *service.ApiKeyService apiKeyService *service.APIKeyService
} }
// NewAPIKeyHandler creates a new APIKeyHandler // NewAPIKeyHandler creates a new APIKeyHandler
func NewAPIKeyHandler(apiKeyService *service.ApiKeyService) *APIKeyHandler { func NewAPIKeyHandler(apiKeyService *service.APIKeyService) *APIKeyHandler {
return &APIKeyHandler{ return &APIKeyHandler{
apiKeyService: apiKeyService, apiKeyService: apiKeyService,
} }
...@@ -56,9 +57,9 @@ func (h *APIKeyHandler) List(c *gin.Context) { ...@@ -56,9 +57,9 @@ func (h *APIKeyHandler) List(c *gin.Context) {
return return
} }
out := make([]dto.ApiKey, 0, len(keys)) out := make([]dto.APIKey, 0, len(keys))
for i := range keys { for i := range keys {
out = append(out, *dto.ApiKeyFromService(&keys[i])) out = append(out, *dto.APIKeyFromService(&keys[i]))
} }
response.Paginated(c, out, result.Total, page, pageSize) response.Paginated(c, out, result.Total, page, pageSize)
} }
...@@ -90,7 +91,7 @@ func (h *APIKeyHandler) GetByID(c *gin.Context) { ...@@ -90,7 +91,7 @@ func (h *APIKeyHandler) GetByID(c *gin.Context) {
return return
} }
response.Success(c, dto.ApiKeyFromService(key)) response.Success(c, dto.APIKeyFromService(key))
} }
// Create handles creating a new API key // Create handles creating a new API key
...@@ -108,7 +109,7 @@ func (h *APIKeyHandler) Create(c *gin.Context) { ...@@ -108,7 +109,7 @@ func (h *APIKeyHandler) Create(c *gin.Context) {
return return
} }
svcReq := service.CreateApiKeyRequest{ svcReq := service.CreateAPIKeyRequest{
Name: req.Name, Name: req.Name,
GroupID: req.GroupID, GroupID: req.GroupID,
CustomKey: req.CustomKey, CustomKey: req.CustomKey,
...@@ -119,7 +120,7 @@ func (h *APIKeyHandler) Create(c *gin.Context) { ...@@ -119,7 +120,7 @@ func (h *APIKeyHandler) Create(c *gin.Context) {
return return
} }
response.Success(c, dto.ApiKeyFromService(key)) response.Success(c, dto.APIKeyFromService(key))
} }
// Update handles updating an API key // Update handles updating an API key
...@@ -143,7 +144,7 @@ func (h *APIKeyHandler) Update(c *gin.Context) { ...@@ -143,7 +144,7 @@ func (h *APIKeyHandler) Update(c *gin.Context) {
return return
} }
svcReq := service.UpdateApiKeyRequest{} svcReq := service.UpdateAPIKeyRequest{}
if req.Name != "" { if req.Name != "" {
svcReq.Name = &req.Name svcReq.Name = &req.Name
} }
...@@ -158,7 +159,7 @@ func (h *APIKeyHandler) Update(c *gin.Context) { ...@@ -158,7 +159,7 @@ func (h *APIKeyHandler) Update(c *gin.Context) {
return return
} }
response.Success(c, dto.ApiKeyFromService(key)) response.Success(c, dto.APIKeyFromService(key))
} }
// Delete handles deleting an API key // Delete handles deleting an API key
......
// Package dto provides data transfer objects for HTTP handlers.
package dto package dto
import "github.com/Wei-Shaw/sub2api/internal/service" import "github.com/Wei-Shaw/sub2api/internal/service"
...@@ -26,11 +27,11 @@ func UserFromService(u *service.User) *User { ...@@ -26,11 +27,11 @@ func UserFromService(u *service.User) *User {
return nil return nil
} }
out := UserFromServiceShallow(u) out := UserFromServiceShallow(u)
if len(u.ApiKeys) > 0 { if len(u.APIKeys) > 0 {
out.ApiKeys = make([]ApiKey, 0, len(u.ApiKeys)) out.APIKeys = make([]APIKey, 0, len(u.APIKeys))
for i := range u.ApiKeys { for i := range u.APIKeys {
k := u.ApiKeys[i] k := u.APIKeys[i]
out.ApiKeys = append(out.ApiKeys, *ApiKeyFromService(&k)) out.APIKeys = append(out.APIKeys, *APIKeyFromService(&k))
} }
} }
if len(u.Subscriptions) > 0 { if len(u.Subscriptions) > 0 {
...@@ -43,11 +44,11 @@ func UserFromService(u *service.User) *User { ...@@ -43,11 +44,11 @@ func UserFromService(u *service.User) *User {
return out return out
} }
func ApiKeyFromService(k *service.ApiKey) *ApiKey { func APIKeyFromService(k *service.APIKey) *APIKey {
if k == nil { if k == nil {
return nil return nil
} }
return &ApiKey{ return &APIKey{
ID: k.ID, ID: k.ID,
UserID: k.UserID, UserID: k.UserID,
Key: k.Key, Key: k.Key,
...@@ -103,28 +104,30 @@ func AccountFromServiceShallow(a *service.Account) *Account { ...@@ -103,28 +104,30 @@ func AccountFromServiceShallow(a *service.Account) *Account {
return nil return nil
} }
return &Account{ return &Account{
ID: a.ID, ID: a.ID,
Name: a.Name, Name: a.Name,
Platform: a.Platform, Platform: a.Platform,
Type: a.Type, Type: a.Type,
Credentials: a.Credentials, Credentials: a.Credentials,
Extra: a.Extra, Extra: a.Extra,
ProxyID: a.ProxyID, ProxyID: a.ProxyID,
Concurrency: a.Concurrency, Concurrency: a.Concurrency,
Priority: a.Priority, Priority: a.Priority,
Status: a.Status, Status: a.Status,
ErrorMessage: a.ErrorMessage, ErrorMessage: a.ErrorMessage,
LastUsedAt: a.LastUsedAt, LastUsedAt: a.LastUsedAt,
CreatedAt: a.CreatedAt, CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt, UpdatedAt: a.UpdatedAt,
Schedulable: a.Schedulable, Schedulable: a.Schedulable,
RateLimitedAt: a.RateLimitedAt, RateLimitedAt: a.RateLimitedAt,
RateLimitResetAt: a.RateLimitResetAt, RateLimitResetAt: a.RateLimitResetAt,
OverloadUntil: a.OverloadUntil, OverloadUntil: a.OverloadUntil,
SessionWindowStart: a.SessionWindowStart, TempUnschedulableUntil: a.TempUnschedulableUntil,
SessionWindowEnd: a.SessionWindowEnd, TempUnschedulableReason: a.TempUnschedulableReason,
SessionWindowStatus: a.SessionWindowStatus, SessionWindowStart: a.SessionWindowStart,
GroupIDs: a.GroupIDs, SessionWindowEnd: a.SessionWindowEnd,
SessionWindowStatus: a.SessionWindowStatus,
GroupIDs: a.GroupIDs,
} }
} }
...@@ -220,7 +223,7 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog { ...@@ -220,7 +223,7 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog {
return &UsageLog{ return &UsageLog{
ID: l.ID, ID: l.ID,
UserID: l.UserID, UserID: l.UserID,
ApiKeyID: l.ApiKeyID, APIKeyID: l.APIKeyID,
AccountID: l.AccountID, AccountID: l.AccountID,
RequestID: l.RequestID, RequestID: l.RequestID,
Model: l.Model, Model: l.Model,
...@@ -245,7 +248,7 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog { ...@@ -245,7 +248,7 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog {
FirstTokenMs: l.FirstTokenMs, FirstTokenMs: l.FirstTokenMs,
CreatedAt: l.CreatedAt, CreatedAt: l.CreatedAt,
User: UserFromServiceShallow(l.User), User: UserFromServiceShallow(l.User),
ApiKey: ApiKeyFromService(l.ApiKey), APIKey: APIKeyFromService(l.APIKey),
Account: AccountFromService(l.Account), Account: AccountFromService(l.Account),
Group: GroupFromServiceShallow(l.Group), Group: GroupFromServiceShallow(l.Group),
Subscription: UserSubscriptionFromService(l.Subscription), Subscription: UserSubscriptionFromService(l.Subscription),
......
...@@ -5,13 +5,13 @@ type SystemSettings struct { ...@@ -5,13 +5,13 @@ type SystemSettings struct {
RegistrationEnabled bool `json:"registration_enabled"` RegistrationEnabled bool `json:"registration_enabled"`
EmailVerifyEnabled bool `json:"email_verify_enabled"` EmailVerifyEnabled bool `json:"email_verify_enabled"`
SmtpHost string `json:"smtp_host"` SMTPHost string `json:"smtp_host"`
SmtpPort int `json:"smtp_port"` SMTPPort int `json:"smtp_port"`
SmtpUsername string `json:"smtp_username"` SMTPUsername string `json:"smtp_username"`
SmtpPassword string `json:"smtp_password,omitempty"` SMTPPassword string `json:"smtp_password,omitempty"`
SmtpFrom string `json:"smtp_from_email"` SMTPFrom string `json:"smtp_from_email"`
SmtpFromName string `json:"smtp_from_name"` SMTPFromName string `json:"smtp_from_name"`
SmtpUseTLS bool `json:"smtp_use_tls"` SMTPUseTLS bool `json:"smtp_use_tls"`
TurnstileEnabled bool `json:"turnstile_enabled"` TurnstileEnabled bool `json:"turnstile_enabled"`
TurnstileSiteKey string `json:"turnstile_site_key"` TurnstileSiteKey string `json:"turnstile_site_key"`
...@@ -20,12 +20,19 @@ type SystemSettings struct { ...@@ -20,12 +20,19 @@ type SystemSettings struct {
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
ApiBaseUrl string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocUrl string `json:"doc_url"` DocURL string `json:"doc_url"`
DefaultConcurrency int `json:"default_concurrency"` DefaultConcurrency int `json:"default_concurrency"`
DefaultBalance float64 `json:"default_balance"` DefaultBalance float64 `json:"default_balance"`
// Model fallback configuration
EnableModelFallback bool `json:"enable_model_fallback"`
FallbackModelAnthropic string `json:"fallback_model_anthropic"`
FallbackModelOpenAI string `json:"fallback_model_openai"`
FallbackModelGemini string `json:"fallback_model_gemini"`
FallbackModelAntigravity string `json:"fallback_model_antigravity"`
} }
type PublicSettings struct { type PublicSettings struct {
...@@ -36,8 +43,8 @@ type PublicSettings struct { ...@@ -36,8 +43,8 @@ type PublicSettings struct {
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
SiteLogo string `json:"site_logo"` SiteLogo string `json:"site_logo"`
SiteSubtitle string `json:"site_subtitle"` SiteSubtitle string `json:"site_subtitle"`
ApiBaseUrl string `json:"api_base_url"` APIBaseURL string `json:"api_base_url"`
ContactInfo string `json:"contact_info"` ContactInfo string `json:"contact_info"`
DocUrl string `json:"doc_url"` DocURL string `json:"doc_url"`
Version string `json:"version"` Version string `json:"version"`
} }
...@@ -15,11 +15,11 @@ type User struct { ...@@ -15,11 +15,11 @@ type User struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
ApiKeys []ApiKey `json:"api_keys,omitempty"` APIKeys []APIKey `json:"api_keys,omitempty"`
Subscriptions []UserSubscription `json:"subscriptions,omitempty"` Subscriptions []UserSubscription `json:"subscriptions,omitempty"`
} }
type ApiKey struct { type APIKey struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Key string `json:"key"` Key string `json:"key"`
...@@ -76,6 +76,9 @@ type Account struct { ...@@ -76,6 +76,9 @@ type Account struct {
RateLimitResetAt *time.Time `json:"rate_limit_reset_at"` RateLimitResetAt *time.Time `json:"rate_limit_reset_at"`
OverloadUntil *time.Time `json:"overload_until"` OverloadUntil *time.Time `json:"overload_until"`
TempUnschedulableUntil *time.Time `json:"temp_unschedulable_until"`
TempUnschedulableReason string `json:"temp_unschedulable_reason"`
SessionWindowStart *time.Time `json:"session_window_start"` SessionWindowStart *time.Time `json:"session_window_start"`
SessionWindowEnd *time.Time `json:"session_window_end"` SessionWindowEnd *time.Time `json:"session_window_end"`
SessionWindowStatus string `json:"session_window_status"` SessionWindowStatus string `json:"session_window_status"`
...@@ -136,7 +139,7 @@ type RedeemCode struct { ...@@ -136,7 +139,7 @@ type RedeemCode struct {
type UsageLog struct { type UsageLog struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
ApiKeyID int64 `json:"api_key_id"` APIKeyID int64 `json:"api_key_id"`
AccountID int64 `json:"account_id"` AccountID int64 `json:"account_id"`
RequestID string `json:"request_id"` RequestID string `json:"request_id"`
Model string `json:"model"` Model string `json:"model"`
...@@ -168,7 +171,7 @@ type UsageLog struct { ...@@ -168,7 +171,7 @@ type UsageLog struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
ApiKey *ApiKey `json:"api_key,omitempty"` APIKey *APIKey `json:"api_key,omitempty"`
Account *Account `json:"account,omitempty"` Account *Account `json:"account,omitempty"`
Group *Group `json:"group,omitempty"` Group *Group `json:"group,omitempty"`
Subscription *UserSubscription `json:"subscription,omitempty"` Subscription *UserSubscription `json:"subscription,omitempty"`
......
...@@ -53,7 +53,7 @@ func NewGatewayHandler( ...@@ -53,7 +53,7 @@ func NewGatewayHandler(
// POST /v1/messages // POST /v1/messages
func (h *GatewayHandler) Messages(c *gin.Context) { func (h *GatewayHandler) Messages(c *gin.Context) {
// 从context获取apiKey和user(ApiKeyAuth中间件已设置) // 从context获取apiKey和user(ApiKeyAuth中间件已设置)
apiKey, ok := middleware2.GetApiKeyFromContext(c) apiKey, ok := middleware2.GetAPIKeyFromContext(c)
if !ok { if !ok {
h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
return return
...@@ -259,7 +259,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { ...@@ -259,7 +259,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
defer cancel() defer cancel()
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
Result: result, Result: result,
ApiKey: apiKey, APIKey: apiKey,
User: apiKey.User, User: apiKey.User,
Account: usedAccount, Account: usedAccount,
Subscription: subscription, Subscription: subscription,
...@@ -383,7 +383,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { ...@@ -383,7 +383,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
defer cancel() defer cancel()
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
Result: result, Result: result,
ApiKey: apiKey, APIKey: apiKey,
User: apiKey.User, User: apiKey.User,
Account: usedAccount, Account: usedAccount,
Subscription: subscription, Subscription: subscription,
...@@ -400,7 +400,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { ...@@ -400,7 +400,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// Returns models based on account configurations (model_mapping whitelist) // Returns models based on account configurations (model_mapping whitelist)
// Falls back to default models if no whitelist is configured // Falls back to default models if no whitelist is configured
func (h *GatewayHandler) Models(c *gin.Context) { func (h *GatewayHandler) Models(c *gin.Context) {
apiKey, _ := middleware2.GetApiKeyFromContext(c) apiKey, _ := middleware2.GetAPIKeyFromContext(c)
var groupID *int64 var groupID *int64
var platform string var platform string
...@@ -458,7 +458,7 @@ func (h *GatewayHandler) AntigravityModels(c *gin.Context) { ...@@ -458,7 +458,7 @@ func (h *GatewayHandler) AntigravityModels(c *gin.Context) {
// Usage handles getting account balance for CC Switch integration // Usage handles getting account balance for CC Switch integration
// GET /v1/usage // GET /v1/usage
func (h *GatewayHandler) Usage(c *gin.Context) { func (h *GatewayHandler) Usage(c *gin.Context) {
apiKey, ok := middleware2.GetApiKeyFromContext(c) apiKey, ok := middleware2.GetAPIKeyFromContext(c)
if !ok { if !ok {
h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
return return
...@@ -628,7 +628,7 @@ func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, mess ...@@ -628,7 +628,7 @@ func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, mess
// 特点:校验订阅/余额,但不计算并发、不记录使用量 // 特点:校验订阅/余额,但不计算并发、不记录使用量
func (h *GatewayHandler) CountTokens(c *gin.Context) { func (h *GatewayHandler) CountTokens(c *gin.Context) {
// 从context获取apiKey和user(ApiKeyAuth中间件已设置) // 从context获取apiKey和user(ApiKeyAuth中间件已设置)
apiKey, ok := middleware2.GetApiKeyFromContext(c) apiKey, ok := middleware2.GetAPIKeyFromContext(c)
if !ok { if !ok {
h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
return return
......
...@@ -21,7 +21,7 @@ import ( ...@@ -21,7 +21,7 @@ import (
// GeminiV1BetaListModels proxies: // GeminiV1BetaListModels proxies:
// GET /v1beta/models // GET /v1beta/models
func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) { func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) {
apiKey, ok := middleware.GetApiKeyFromContext(c) apiKey, ok := middleware.GetAPIKeyFromContext(c)
if !ok || apiKey == nil { if !ok || apiKey == nil {
googleError(c, http.StatusUnauthorized, "Invalid API key") googleError(c, http.StatusUnauthorized, "Invalid API key")
return return
...@@ -67,7 +67,7 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) { ...@@ -67,7 +67,7 @@ func (h *GatewayHandler) GeminiV1BetaListModels(c *gin.Context) {
// GeminiV1BetaGetModel proxies: // GeminiV1BetaGetModel proxies:
// GET /v1beta/models/{model} // GET /v1beta/models/{model}
func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) { func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
apiKey, ok := middleware.GetApiKeyFromContext(c) apiKey, ok := middleware.GetAPIKeyFromContext(c)
if !ok || apiKey == nil { if !ok || apiKey == nil {
googleError(c, http.StatusUnauthorized, "Invalid API key") googleError(c, http.StatusUnauthorized, "Invalid API key")
return return
...@@ -120,7 +120,7 @@ func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) { ...@@ -120,7 +120,7 @@ func (h *GatewayHandler) GeminiV1BetaGetModel(c *gin.Context) {
// POST /v1beta/models/{model}:generateContent // POST /v1beta/models/{model}:generateContent
// POST /v1beta/models/{model}:streamGenerateContent?alt=sse // POST /v1beta/models/{model}:streamGenerateContent?alt=sse
func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
apiKey, ok := middleware.GetApiKeyFromContext(c) apiKey, ok := middleware.GetAPIKeyFromContext(c)
if !ok || apiKey == nil { if !ok || apiKey == nil {
googleError(c, http.StatusUnauthorized, "Invalid API key") googleError(c, http.StatusUnauthorized, "Invalid API key")
return return
...@@ -299,7 +299,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { ...@@ -299,7 +299,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
defer cancel() defer cancel()
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
Result: result, Result: result,
ApiKey: apiKey, APIKey: apiKey,
User: apiKey.User, User: apiKey.User,
Account: usedAccount, Account: usedAccount,
Subscription: subscription, Subscription: subscription,
......
...@@ -41,7 +41,7 @@ func NewOpenAIGatewayHandler( ...@@ -41,7 +41,7 @@ func NewOpenAIGatewayHandler(
// POST /openai/v1/responses // POST /openai/v1/responses
func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
// Get apiKey and user from context (set by ApiKeyAuth middleware) // Get apiKey and user from context (set by ApiKeyAuth middleware)
apiKey, ok := middleware2.GetApiKeyFromContext(c) apiKey, ok := middleware2.GetAPIKeyFromContext(c)
if !ok { if !ok {
h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key")
return return
...@@ -235,7 +235,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { ...@@ -235,7 +235,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
defer cancel() defer cancel()
if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{ if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{
Result: result, Result: result,
ApiKey: apiKey, APIKey: apiKey,
User: apiKey.User, User: apiKey.User,
Account: usedAccount, Account: usedAccount,
Subscription: subscription, Subscription: subscription,
......
...@@ -39,9 +39,9 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { ...@@ -39,9 +39,9 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
SiteName: settings.SiteName, SiteName: settings.SiteName,
SiteLogo: settings.SiteLogo, SiteLogo: settings.SiteLogo,
SiteSubtitle: settings.SiteSubtitle, SiteSubtitle: settings.SiteSubtitle,
ApiBaseUrl: settings.ApiBaseUrl, APIBaseURL: settings.APIBaseURL,
ContactInfo: settings.ContactInfo, ContactInfo: settings.ContactInfo,
DocUrl: settings.DocUrl, DocURL: settings.DocURL,
Version: h.version, Version: h.version,
}) })
} }
...@@ -18,11 +18,11 @@ import ( ...@@ -18,11 +18,11 @@ import (
// UsageHandler handles usage-related requests // UsageHandler handles usage-related requests
type UsageHandler struct { type UsageHandler struct {
usageService *service.UsageService usageService *service.UsageService
apiKeyService *service.ApiKeyService apiKeyService *service.APIKeyService
} }
// NewUsageHandler creates a new UsageHandler // NewUsageHandler creates a new UsageHandler
func NewUsageHandler(usageService *service.UsageService, apiKeyService *service.ApiKeyService) *UsageHandler { func NewUsageHandler(usageService *service.UsageService, apiKeyService *service.APIKeyService) *UsageHandler {
return &UsageHandler{ return &UsageHandler{
usageService: usageService, usageService: usageService,
apiKeyService: apiKeyService, apiKeyService: apiKeyService,
...@@ -111,7 +111,7 @@ func (h *UsageHandler) List(c *gin.Context) { ...@@ -111,7 +111,7 @@ func (h *UsageHandler) List(c *gin.Context) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize}
filters := usagestats.UsageLogFilters{ filters := usagestats.UsageLogFilters{
UserID: subject.UserID, // Always filter by current user for security UserID: subject.UserID, // Always filter by current user for security
ApiKeyID: apiKeyID, APIKeyID: apiKeyID,
Model: model, Model: model,
Stream: stream, Stream: stream,
BillingType: billingType, BillingType: billingType,
...@@ -235,7 +235,7 @@ func (h *UsageHandler) Stats(c *gin.Context) { ...@@ -235,7 +235,7 @@ func (h *UsageHandler) Stats(c *gin.Context) {
var stats *service.UsageStats var stats *service.UsageStats
var err error var err error
if apiKeyID > 0 { if apiKeyID > 0 {
stats, err = h.usageService.GetStatsByApiKey(c.Request.Context(), apiKeyID, startTime, endTime) stats, err = h.usageService.GetStatsByAPIKey(c.Request.Context(), apiKeyID, startTime, endTime)
} else { } else {
stats, err = h.usageService.GetStatsByUser(c.Request.Context(), subject.UserID, startTime, endTime) stats, err = h.usageService.GetStatsByUser(c.Request.Context(), subject.UserID, startTime, endTime)
} }
...@@ -346,49 +346,49 @@ func (h *UsageHandler) DashboardModels(c *gin.Context) { ...@@ -346,49 +346,49 @@ func (h *UsageHandler) DashboardModels(c *gin.Context) {
}) })
} }
// BatchApiKeysUsageRequest represents the request for batch API keys usage // BatchAPIKeysUsageRequest represents the request for batch API keys usage
type BatchApiKeysUsageRequest struct { type BatchAPIKeysUsageRequest struct {
ApiKeyIDs []int64 `json:"api_key_ids" binding:"required"` APIKeyIDs []int64 `json:"api_key_ids" binding:"required"`
} }
// DashboardApiKeysUsage handles getting usage stats for user's own API keys // DashboardAPIKeysUsage handles getting usage stats for user's own API keys
// POST /api/v1/usage/dashboard/api-keys-usage // POST /api/v1/usage/dashboard/api-keys-usage
func (h *UsageHandler) DashboardApiKeysUsage(c *gin.Context) { func (h *UsageHandler) DashboardAPIKeysUsage(c *gin.Context) {
subject, ok := middleware2.GetAuthSubjectFromContext(c) subject, ok := middleware2.GetAuthSubjectFromContext(c)
if !ok { if !ok {
response.Unauthorized(c, "User not authenticated") response.Unauthorized(c, "User not authenticated")
return return
} }
var req BatchApiKeysUsageRequest var req BatchAPIKeysUsageRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error()) response.BadRequest(c, "Invalid request: "+err.Error())
return return
} }
if len(req.ApiKeyIDs) == 0 { if len(req.APIKeyIDs) == 0 {
response.Success(c, gin.H{"stats": map[string]any{}}) response.Success(c, gin.H{"stats": map[string]any{}})
return return
} }
// Limit the number of API key IDs to prevent SQL parameter overflow // Limit the number of API key IDs to prevent SQL parameter overflow
if len(req.ApiKeyIDs) > 100 { if len(req.APIKeyIDs) > 100 {
response.BadRequest(c, "Too many API key IDs (maximum 100 allowed)") response.BadRequest(c, "Too many API key IDs (maximum 100 allowed)")
return return
} }
validApiKeyIDs, err := h.apiKeyService.VerifyOwnership(c.Request.Context(), subject.UserID, req.ApiKeyIDs) validAPIKeyIDs, err := h.apiKeyService.VerifyOwnership(c.Request.Context(), subject.UserID, req.APIKeyIDs)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
if len(validApiKeyIDs) == 0 { if len(validAPIKeyIDs) == 0 {
response.Success(c, gin.H{"stats": map[string]any{}}) response.Success(c, gin.H{"stats": map[string]any{}})
return return
} }
stats, err := h.usageService.GetBatchApiKeyUsageStats(c.Request.Context(), validApiKeyIDs) stats, err := h.usageService.GetBatchAPIKeyUsageStats(c.Request.Context(), validAPIKeyIDs)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment