Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
c7abfe67
Commit
c7abfe67
authored
Jan 08, 2026
by
song
Browse files
Merge remote-tracking branch 'upstream/main'
parents
4e3476a6
db6f53e2
Changes
99
Hide whitespace changes
Inline
Side-by-side
backend/ent/schema/account.go
View file @
c7abfe67
...
...
@@ -118,6 +118,16 @@ func (Account) Fields() []ent.Field {
Optional
()
.
Nillable
()
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"timestamptz"
}),
// expires_at: 账户过期时间(可为空)
field
.
Time
(
"expires_at"
)
.
Optional
()
.
Nillable
()
.
Comment
(
"Account expiration time (NULL means no expiration)."
)
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"timestamptz"
}),
// auto_pause_on_expired: 过期后自动暂停调度
field
.
Bool
(
"auto_pause_on_expired"
)
.
Default
(
true
)
.
Comment
(
"Auto pause scheduling when account expires."
),
// ========== 调度和速率限制相关字段 ==========
// 这些字段在 migrations/005_schema_parity.sql 中添加
...
...
backend/ent/schema/usage_log.go
View file @
c7abfe67
...
...
@@ -96,6 +96,10 @@ func (UsageLog) Fields() []ent.Field {
field
.
Int
(
"first_token_ms"
)
.
Optional
()
.
Nillable
(),
field
.
String
(
"user_agent"
)
.
MaxLen
(
512
)
.
Optional
()
.
Nillable
(),
// 图片生成字段(仅 gemini-3-pro-image 等图片模型使用)
field
.
Int
(
"image_count"
)
.
...
...
backend/ent/usagelog.go
View file @
c7abfe67
...
...
@@ -70,6 +70,8 @@ type UsageLog struct {
DurationMs
*
int
`json:"duration_ms,omitempty"`
// FirstTokenMs holds the value of the "first_token_ms" field.
FirstTokenMs
*
int
`json:"first_token_ms,omitempty"`
// UserAgent holds the value of the "user_agent" field.
UserAgent
*
string
`json:"user_agent,omitempty"`
// ImageCount holds the value of the "image_count" field.
ImageCount
int
`json:"image_count,omitempty"`
// ImageSize holds the value of the "image_size" field.
...
...
@@ -165,7 +167,7 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
(
sql
.
NullFloat64
)
case
usagelog
.
FieldID
,
usagelog
.
FieldUserID
,
usagelog
.
FieldAPIKeyID
,
usagelog
.
FieldAccountID
,
usagelog
.
FieldGroupID
,
usagelog
.
FieldSubscriptionID
,
usagelog
.
FieldInputTokens
,
usagelog
.
FieldOutputTokens
,
usagelog
.
FieldCacheCreationTokens
,
usagelog
.
FieldCacheReadTokens
,
usagelog
.
FieldCacheCreation5mTokens
,
usagelog
.
FieldCacheCreation1hTokens
,
usagelog
.
FieldBillingType
,
usagelog
.
FieldDurationMs
,
usagelog
.
FieldFirstTokenMs
,
usagelog
.
FieldImageCount
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
usagelog
.
FieldRequestID
,
usagelog
.
FieldModel
,
usagelog
.
FieldImageSize
:
case
usagelog
.
FieldRequestID
,
usagelog
.
FieldModel
,
usagelog
.
FieldUserAgent
,
usagelog
.
FieldImageSize
:
values
[
i
]
=
new
(
sql
.
NullString
)
case
usagelog
.
FieldCreatedAt
:
values
[
i
]
=
new
(
sql
.
NullTime
)
...
...
@@ -338,6 +340,13 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
_m
.
FirstTokenMs
=
new
(
int
)
*
_m
.
FirstTokenMs
=
int
(
value
.
Int64
)
}
case
usagelog
.
FieldUserAgent
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field user_agent"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
UserAgent
=
new
(
string
)
*
_m
.
UserAgent
=
value
.
String
}
case
usagelog
.
FieldImageCount
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullInt64
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field image_count"
,
values
[
i
])
...
...
@@ -498,6 +507,11 @@ func (_m *UsageLog) String() string {
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
*
v
))
}
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
UserAgent
;
v
!=
nil
{
builder
.
WriteString
(
"user_agent="
)
builder
.
WriteString
(
*
v
)
}
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"image_count="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
ImageCount
))
builder
.
WriteString
(
", "
)
...
...
backend/ent/usagelog/usagelog.go
View file @
c7abfe67
...
...
@@ -62,6 +62,8 @@ const (
FieldDurationMs
=
"duration_ms"
// FieldFirstTokenMs holds the string denoting the first_token_ms field in the database.
FieldFirstTokenMs
=
"first_token_ms"
// FieldUserAgent holds the string denoting the user_agent field in the database.
FieldUserAgent
=
"user_agent"
// FieldImageCount holds the string denoting the image_count field in the database.
FieldImageCount
=
"image_count"
// FieldImageSize holds the string denoting the image_size field in the database.
...
...
@@ -144,6 +146,7 @@ var Columns = []string{
FieldStream
,
FieldDurationMs
,
FieldFirstTokenMs
,
FieldUserAgent
,
FieldImageCount
,
FieldImageSize
,
FieldCreatedAt
,
...
...
@@ -194,6 +197,8 @@ var (
DefaultBillingType
int8
// DefaultStream holds the default value on creation for the "stream" field.
DefaultStream
bool
// UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
UserAgentValidator
func
(
string
)
error
// DefaultImageCount holds the default value on creation for the "image_count" field.
DefaultImageCount
int
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
...
...
@@ -330,6 +335,11 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldFirstTokenMs
,
opts
...
)
.
ToFunc
()
}
// ByUserAgent orders the results by the user_agent field.
func
ByUserAgent
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldUserAgent
,
opts
...
)
.
ToFunc
()
}
// ByImageCount orders the results by the image_count field.
func
ByImageCount
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldImageCount
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/usagelog/where.go
View file @
c7abfe67
...
...
@@ -175,6 +175,11 @@ func FirstTokenMs(v int) predicate.UsageLog {
return
predicate
.
UsageLog
(
sql
.
FieldEQ
(
FieldFirstTokenMs
,
v
))
}
// UserAgent applies equality check predicate on the "user_agent" field. It's identical to UserAgentEQ.
func
UserAgent
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldEQ
(
FieldUserAgent
,
v
))
}
// ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ.
func
ImageCount
(
v
int
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldEQ
(
FieldImageCount
,
v
))
...
...
@@ -1110,6 +1115,81 @@ func FirstTokenMsNotNil() predicate.UsageLog {
return
predicate
.
UsageLog
(
sql
.
FieldNotNull
(
FieldFirstTokenMs
))
}
// UserAgentEQ applies the EQ predicate on the "user_agent" field.
func
UserAgentEQ
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldEQ
(
FieldUserAgent
,
v
))
}
// UserAgentNEQ applies the NEQ predicate on the "user_agent" field.
func
UserAgentNEQ
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldNEQ
(
FieldUserAgent
,
v
))
}
// UserAgentIn applies the In predicate on the "user_agent" field.
func
UserAgentIn
(
vs
...
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldIn
(
FieldUserAgent
,
vs
...
))
}
// UserAgentNotIn applies the NotIn predicate on the "user_agent" field.
func
UserAgentNotIn
(
vs
...
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldNotIn
(
FieldUserAgent
,
vs
...
))
}
// UserAgentGT applies the GT predicate on the "user_agent" field.
func
UserAgentGT
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldGT
(
FieldUserAgent
,
v
))
}
// UserAgentGTE applies the GTE predicate on the "user_agent" field.
func
UserAgentGTE
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldGTE
(
FieldUserAgent
,
v
))
}
// UserAgentLT applies the LT predicate on the "user_agent" field.
func
UserAgentLT
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldLT
(
FieldUserAgent
,
v
))
}
// UserAgentLTE applies the LTE predicate on the "user_agent" field.
func
UserAgentLTE
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldLTE
(
FieldUserAgent
,
v
))
}
// UserAgentContains applies the Contains predicate on the "user_agent" field.
func
UserAgentContains
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldContains
(
FieldUserAgent
,
v
))
}
// UserAgentHasPrefix applies the HasPrefix predicate on the "user_agent" field.
func
UserAgentHasPrefix
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldHasPrefix
(
FieldUserAgent
,
v
))
}
// UserAgentHasSuffix applies the HasSuffix predicate on the "user_agent" field.
func
UserAgentHasSuffix
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldHasSuffix
(
FieldUserAgent
,
v
))
}
// UserAgentIsNil applies the IsNil predicate on the "user_agent" field.
func
UserAgentIsNil
()
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldIsNull
(
FieldUserAgent
))
}
// UserAgentNotNil applies the NotNil predicate on the "user_agent" field.
func
UserAgentNotNil
()
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldNotNull
(
FieldUserAgent
))
}
// UserAgentEqualFold applies the EqualFold predicate on the "user_agent" field.
func
UserAgentEqualFold
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldEqualFold
(
FieldUserAgent
,
v
))
}
// UserAgentContainsFold applies the ContainsFold predicate on the "user_agent" field.
func
UserAgentContainsFold
(
v
string
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldContainsFold
(
FieldUserAgent
,
v
))
}
// ImageCountEQ applies the EQ predicate on the "image_count" field.
func
ImageCountEQ
(
v
int
)
predicate
.
UsageLog
{
return
predicate
.
UsageLog
(
sql
.
FieldEQ
(
FieldImageCount
,
v
))
...
...
backend/ent/usagelog_create.go
View file @
c7abfe67
...
...
@@ -323,6 +323,20 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate {
return
_c
}
// SetUserAgent sets the "user_agent" field.
func
(
_c
*
UsageLogCreate
)
SetUserAgent
(
v
string
)
*
UsageLogCreate
{
_c
.
mutation
.
SetUserAgent
(
v
)
return
_c
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func
(
_c
*
UsageLogCreate
)
SetNillableUserAgent
(
v
*
string
)
*
UsageLogCreate
{
if
v
!=
nil
{
_c
.
SetUserAgent
(
*
v
)
}
return
_c
}
// SetImageCount sets the "image_count" field.
func
(
_c
*
UsageLogCreate
)
SetImageCount
(
v
int
)
*
UsageLogCreate
{
_c
.
mutation
.
SetImageCount
(
v
)
...
...
@@ -567,6 +581,11 @@ func (_c *UsageLogCreate) check() error {
if
_
,
ok
:=
_c
.
mutation
.
Stream
();
!
ok
{
return
&
ValidationError
{
Name
:
"stream"
,
err
:
errors
.
New
(
`ent: missing required field "UsageLog.stream"`
)}
}
if
v
,
ok
:=
_c
.
mutation
.
UserAgent
();
ok
{
if
err
:=
usagelog
.
UserAgentValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"user_agent"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.user_agent": %w`
,
err
)}
}
}
if
_
,
ok
:=
_c
.
mutation
.
ImageCount
();
!
ok
{
return
&
ValidationError
{
Name
:
"image_count"
,
err
:
errors
.
New
(
`ent: missing required field "UsageLog.image_count"`
)}
}
...
...
@@ -690,6 +709,10 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
_spec
.
SetField
(
usagelog
.
FieldFirstTokenMs
,
field
.
TypeInt
,
value
)
_node
.
FirstTokenMs
=
&
value
}
if
value
,
ok
:=
_c
.
mutation
.
UserAgent
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldUserAgent
,
field
.
TypeString
,
value
)
_node
.
UserAgent
=
&
value
}
if
value
,
ok
:=
_c
.
mutation
.
ImageCount
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldImageCount
,
field
.
TypeInt
,
value
)
_node
.
ImageCount
=
value
...
...
@@ -1247,6 +1270,24 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert {
return
u
}
// SetUserAgent sets the "user_agent" field.
func
(
u
*
UsageLogUpsert
)
SetUserAgent
(
v
string
)
*
UsageLogUpsert
{
u
.
Set
(
usagelog
.
FieldUserAgent
,
v
)
return
u
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func
(
u
*
UsageLogUpsert
)
UpdateUserAgent
()
*
UsageLogUpsert
{
u
.
SetExcluded
(
usagelog
.
FieldUserAgent
)
return
u
}
// ClearUserAgent clears the value of the "user_agent" field.
func
(
u
*
UsageLogUpsert
)
ClearUserAgent
()
*
UsageLogUpsert
{
u
.
SetNull
(
usagelog
.
FieldUserAgent
)
return
u
}
// SetImageCount sets the "image_count" field.
func
(
u
*
UsageLogUpsert
)
SetImageCount
(
v
int
)
*
UsageLogUpsert
{
u
.
Set
(
usagelog
.
FieldImageCount
,
v
)
...
...
@@ -1804,6 +1845,27 @@ func (u *UsageLogUpsertOne) ClearFirstTokenMs() *UsageLogUpsertOne {
})
}
// SetUserAgent sets the "user_agent" field.
func
(
u
*
UsageLogUpsertOne
)
SetUserAgent
(
v
string
)
*
UsageLogUpsertOne
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
SetUserAgent
(
v
)
})
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func
(
u
*
UsageLogUpsertOne
)
UpdateUserAgent
()
*
UsageLogUpsertOne
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
UpdateUserAgent
()
})
}
// ClearUserAgent clears the value of the "user_agent" field.
func
(
u
*
UsageLogUpsertOne
)
ClearUserAgent
()
*
UsageLogUpsertOne
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
ClearUserAgent
()
})
}
// SetImageCount sets the "image_count" field.
func
(
u
*
UsageLogUpsertOne
)
SetImageCount
(
v
int
)
*
UsageLogUpsertOne
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
...
...
@@ -2533,6 +2595,27 @@ func (u *UsageLogUpsertBulk) ClearFirstTokenMs() *UsageLogUpsertBulk {
})
}
// SetUserAgent sets the "user_agent" field.
func
(
u
*
UsageLogUpsertBulk
)
SetUserAgent
(
v
string
)
*
UsageLogUpsertBulk
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
SetUserAgent
(
v
)
})
}
// UpdateUserAgent sets the "user_agent" field to the value that was provided on create.
func
(
u
*
UsageLogUpsertBulk
)
UpdateUserAgent
()
*
UsageLogUpsertBulk
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
UpdateUserAgent
()
})
}
// ClearUserAgent clears the value of the "user_agent" field.
func
(
u
*
UsageLogUpsertBulk
)
ClearUserAgent
()
*
UsageLogUpsertBulk
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
s
.
ClearUserAgent
()
})
}
// SetImageCount sets the "image_count" field.
func
(
u
*
UsageLogUpsertBulk
)
SetImageCount
(
v
int
)
*
UsageLogUpsertBulk
{
return
u
.
Update
(
func
(
s
*
UsageLogUpsert
)
{
...
...
backend/ent/usagelog_update.go
View file @
c7abfe67
...
...
@@ -504,6 +504,26 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate {
return
_u
}
// SetUserAgent sets the "user_agent" field.
func
(
_u
*
UsageLogUpdate
)
SetUserAgent
(
v
string
)
*
UsageLogUpdate
{
_u
.
mutation
.
SetUserAgent
(
v
)
return
_u
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func
(
_u
*
UsageLogUpdate
)
SetNillableUserAgent
(
v
*
string
)
*
UsageLogUpdate
{
if
v
!=
nil
{
_u
.
SetUserAgent
(
*
v
)
}
return
_u
}
// ClearUserAgent clears the value of the "user_agent" field.
func
(
_u
*
UsageLogUpdate
)
ClearUserAgent
()
*
UsageLogUpdate
{
_u
.
mutation
.
ClearUserAgent
()
return
_u
}
// SetImageCount sets the "image_count" field.
func
(
_u
*
UsageLogUpdate
)
SetImageCount
(
v
int
)
*
UsageLogUpdate
{
_u
.
mutation
.
ResetImageCount
()
...
...
@@ -644,6 +664,11 @@ func (_u *UsageLogUpdate) check() error {
return
&
ValidationError
{
Name
:
"model"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.model": %w`
,
err
)}
}
}
if
v
,
ok
:=
_u
.
mutation
.
UserAgent
();
ok
{
if
err
:=
usagelog
.
UserAgentValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"user_agent"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.user_agent": %w`
,
err
)}
}
}
if
v
,
ok
:=
_u
.
mutation
.
ImageSize
();
ok
{
if
err
:=
usagelog
.
ImageSizeValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"image_size"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.image_size": %w`
,
err
)}
...
...
@@ -784,6 +809,12 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if
_u
.
mutation
.
FirstTokenMsCleared
()
{
_spec
.
ClearField
(
usagelog
.
FieldFirstTokenMs
,
field
.
TypeInt
)
}
if
value
,
ok
:=
_u
.
mutation
.
UserAgent
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldUserAgent
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
UserAgentCleared
()
{
_spec
.
ClearField
(
usagelog
.
FieldUserAgent
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
ImageCount
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldImageCount
,
field
.
TypeInt
,
value
)
}
...
...
@@ -1433,6 +1464,26 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne {
return
_u
}
// SetUserAgent sets the "user_agent" field.
func
(
_u
*
UsageLogUpdateOne
)
SetUserAgent
(
v
string
)
*
UsageLogUpdateOne
{
_u
.
mutation
.
SetUserAgent
(
v
)
return
_u
}
// SetNillableUserAgent sets the "user_agent" field if the given value is not nil.
func
(
_u
*
UsageLogUpdateOne
)
SetNillableUserAgent
(
v
*
string
)
*
UsageLogUpdateOne
{
if
v
!=
nil
{
_u
.
SetUserAgent
(
*
v
)
}
return
_u
}
// ClearUserAgent clears the value of the "user_agent" field.
func
(
_u
*
UsageLogUpdateOne
)
ClearUserAgent
()
*
UsageLogUpdateOne
{
_u
.
mutation
.
ClearUserAgent
()
return
_u
}
// SetImageCount sets the "image_count" field.
func
(
_u
*
UsageLogUpdateOne
)
SetImageCount
(
v
int
)
*
UsageLogUpdateOne
{
_u
.
mutation
.
ResetImageCount
()
...
...
@@ -1586,6 +1637,11 @@ func (_u *UsageLogUpdateOne) check() error {
return
&
ValidationError
{
Name
:
"model"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.model": %w`
,
err
)}
}
}
if
v
,
ok
:=
_u
.
mutation
.
UserAgent
();
ok
{
if
err
:=
usagelog
.
UserAgentValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"user_agent"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.user_agent": %w`
,
err
)}
}
}
if
v
,
ok
:=
_u
.
mutation
.
ImageSize
();
ok
{
if
err
:=
usagelog
.
ImageSizeValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"image_size"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageLog.image_size": %w`
,
err
)}
...
...
@@ -1743,6 +1799,12 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
if
_u
.
mutation
.
FirstTokenMsCleared
()
{
_spec
.
ClearField
(
usagelog
.
FieldFirstTokenMs
,
field
.
TypeInt
)
}
if
value
,
ok
:=
_u
.
mutation
.
UserAgent
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldUserAgent
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
UserAgentCleared
()
{
_spec
.
ClearField
(
usagelog
.
FieldUserAgent
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
ImageCount
();
ok
{
_spec
.
SetField
(
usagelog
.
FieldImageCount
,
field
.
TypeInt
,
value
)
}
...
...
backend/go.mod
View file @
c7abfe67
module github.com/Wei-Shaw/sub2api
go 1.24.0
toolchain go1.24.11
go 1.25.5
require (
entgo.io/ent v0.14.5
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.
0
github.com/golang-jwt/jwt/v5 v5.2.
2
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/imroc/req/v3 v3.5
6
.0
github.com/imroc/req/v3 v3.5
7
.0
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.17.2
github.com/spf13/viper v1.18.2
...
...
@@ -20,16 +18,16 @@ require (
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/zeromicro/go-zero v1.9.4
golang.org/x/crypto v0.44.0
golang.org/x/net v0.47.0
golang.org/x/term v0.37.0
golang.org/x/crypto v0.46.0
golang.org/x/net v0.48.0
golang.org/x/sync v0.19.0
golang.org/x/term v0.38.0
gopkg.in/yaml.v3 v3.0.1
)
require (
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
...
...
@@ -64,7 +62,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-sql-driver/mysql v1.9.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
...
...
@@ -74,10 +71,8 @@ require (
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.
1
// indirect
github.com/klauspost/compress v1.18.
2
// indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
...
...
@@ -105,8 +100,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.
5.1
// indirect
github.com/quic-go/quic-go v0.5
6.0
// indirect
github.com/quic-go/qpack v0.
6.0
// indirect
github.com/quic-go/quic-go v0.5
7.1
// indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
...
...
@@ -141,16 +136,12 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/datatypes v1.2.7 // indirect
gorm.io/driver/mysql v1.5.6 // indirect
gorm.io/gorm v1.30.0 // indirect
)
backend/go.sum
View file @
c7abfe67
...
...
@@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
...
...
@@ -96,15 +94,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.
0
h1:
d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw
=
github.com/golang-jwt/jwt/v5 v5.2.
0
/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.
2
h1:
Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8
=
github.com/golang-jwt/jwt/v5 v5.2.
2
/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
...
...
@@ -126,8 +121,8 @@ github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZY
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.5
6
.0 h1:
t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo
=
github.com/imroc/req/v3 v3.5
6
.0/go.mod h1:
cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk
=
github.com/imroc/req/v3 v3.5
7
.0 h1:
LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI
=
github.com/imroc/req/v3 v3.5
7
.0/go.mod h1:
JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00
=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
...
...
@@ -138,14 +133,10 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.
1
h1:
bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co
=
github.com/klauspost/compress v1.18.
1
/go.mod h1:
ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0
=
github.com/klauspost/compress v1.18.
2
h1:
iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk
=
github.com/klauspost/compress v1.18.
2
/go.mod h1:
R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4
=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
...
...
@@ -219,10 +210,10 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.
5.1
h1:g
iqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI
=
github.com/quic-go/qpack v0.
5.1
/go.mod h1:
+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg
=
github.com/quic-go/quic-go v0.5
6.0
h1:
q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY
=
github.com/quic-go/quic-go v0.5
6.0
/go.mod h1:
9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c
=
github.com/quic-go/qpack v0.
6.0
h1:g
7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8
=
github.com/quic-go/qpack v0.
6.0
/go.mod h1:
lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII
=
github.com/quic-go/quic-go v0.5
7.1
h1:
25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10
=
github.com/quic-go/quic-go v0.5
7.1
/go.mod h1:
ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s
=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
...
...
@@ -335,16 +326,16 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.4
4
.0 h1:
A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoA
U=
golang.org/x/crypto v0.4
4
.0/go.mod h1:
013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc
=
golang.org/x/crypto v0.4
6
.0 h1:
cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVB
U=
golang.org/x/crypto v0.4
6
.0/go.mod h1:
Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0
=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.
29
.0 h1:
HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA
=
golang.org/x/mod v0.
29
.0/go.mod h1:
NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w
=
golang.org/x/net v0.4
7
.0 h1:
Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY
=
golang.org/x/net v0.4
7
.0/go.mod h1:
/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU
=
golang.org/x/sync v0.1
8
.0 h1:
kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I
=
golang.org/x/sync v0.1
8
.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/mod v0.
30
.0 h1:
fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk
=
golang.org/x/mod v0.
30
.0/go.mod h1:
lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc
=
golang.org/x/net v0.4
8
.0 h1:
zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU
=
golang.org/x/net v0.4
8
.0/go.mod h1:
+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY
=
golang.org/x/sync v0.1
9
.0 h1:
vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4
=
golang.org/x/sync v0.1
9
.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
...
...
@@ -354,16 +345,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3
8
.0 h1:
3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc
=
golang.org/x/sys v0.3
8
.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.3
7
.0 h1:
8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU
=
golang.org/x/term v0.3
7
.0/go.mod h1:
5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254
=
golang.org/x/text v0.3
1
.0 h1:
aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM
=
golang.org/x/text v0.3
1
.0/go.mod h1:
tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM
=
golang.org/x/sys v0.3
9
.0 h1:
CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk
=
golang.org/x/sys v0.3
9
.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.3
8
.0 h1:
PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q
=
golang.org/x/term v0.3
8
.0/go.mod h1:
bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg
=
golang.org/x/text v0.3
2
.0 h1:
ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU
=
golang.org/x/text v0.3
2
.0/go.mod h1:
o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY
=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.3
8
.0 h1:
Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/I
Q=
golang.org/x/tools v0.3
8
.0/go.mod h1:
yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs
=
golang.org/x/tools v0.3
9
.0 h1:
ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQ
Q=
golang.org/x/tools v0.3
9
.0/go.mod h1:
JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ
=
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
...
...
@@ -386,13 +377,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
backend/internal/config/config.go
View file @
c7abfe67
...
...
@@ -52,6 +52,15 @@ type Config struct {
RunMode
string
`mapstructure:"run_mode" yaml:"run_mode"`
Timezone
string
`mapstructure:"timezone"`
// e.g. "Asia/Shanghai", "UTC"
Gemini
GeminiConfig
`mapstructure:"gemini"`
Update
UpdateConfig
`mapstructure:"update"`
}
// UpdateConfig 在线更新相关配置
type
UpdateConfig
struct
{
// ProxyURL 用于访问 GitHub 的代理地址
// 支持 http/https/socks5/socks5h 协议
// 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080"
ProxyURL
string
`mapstructure:"proxy_url"`
}
type
GeminiConfig
struct
{
...
...
@@ -148,7 +157,7 @@ type CSPConfig struct {
}
type
ProxyProbeConfig
struct
{
InsecureSkipVerify
bool
`mapstructure:"insecure_skip_verify"`
InsecureSkipVerify
bool
`mapstructure:"insecure_skip_verify"`
// 已禁用:禁止跳过 TLS 证书验证
}
type
BillingConfig
struct
{
...
...
@@ -448,8 +457,8 @@ func setDefaults() {
"raw.githubusercontent.com"
,
})
viper
.
SetDefault
(
"security.url_allowlist.crs_hosts"
,
[]
string
{})
viper
.
SetDefault
(
"security.url_allowlist.allow_private_hosts"
,
fals
e
)
viper
.
SetDefault
(
"security.url_allowlist.allow_insecure_http"
,
fals
e
)
viper
.
SetDefault
(
"security.url_allowlist.allow_private_hosts"
,
tru
e
)
viper
.
SetDefault
(
"security.url_allowlist.allow_insecure_http"
,
tru
e
)
viper
.
SetDefault
(
"security.response_headers.enabled"
,
false
)
viper
.
SetDefault
(
"security.response_headers.additional_allowed"
,
[]
string
{})
viper
.
SetDefault
(
"security.response_headers.force_remove"
,
[]
string
{})
...
...
@@ -558,6 +567,10 @@ func setDefaults() {
viper
.
SetDefault
(
"gemini.oauth.client_secret"
,
""
)
viper
.
SetDefault
(
"gemini.oauth.scopes"
,
""
)
viper
.
SetDefault
(
"gemini.quota.policy"
,
""
)
// Update - 在线更新配置
// 代理地址为空表示直连 GitHub(适用于海外服务器)
viper
.
SetDefault
(
"update.proxy_url"
,
""
)
}
func
(
c
*
Config
)
Validate
()
error
{
...
...
backend/internal/config/config_test.go
View file @
c7abfe67
...
...
@@ -80,8 +80,11 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
if
cfg
.
Security
.
URLAllowlist
.
Enabled
{
t
.
Fatalf
(
"URLAllowlist.Enabled = true, want false"
)
}
if
cfg
.
Security
.
URLAllowlist
.
AllowInsecureHTTP
{
t
.
Fatalf
(
"URLAllowlist.AllowInsecureHTTP = true, want false"
)
if
!
cfg
.
Security
.
URLAllowlist
.
AllowInsecureHTTP
{
t
.
Fatalf
(
"URLAllowlist.AllowInsecureHTTP = false, want true"
)
}
if
!
cfg
.
Security
.
URLAllowlist
.
AllowPrivateHosts
{
t
.
Fatalf
(
"URLAllowlist.AllowPrivateHosts = false, want true"
)
}
if
cfg
.
Security
.
ResponseHeaders
.
Enabled
{
t
.
Fatalf
(
"ResponseHeaders.Enabled = true, want false"
)
...
...
backend/internal/handler/admin/account_handler.go
View file @
c7abfe67
...
...
@@ -85,6 +85,8 @@ type CreateAccountRequest struct {
Concurrency
int
`json:"concurrency"`
Priority
int
`json:"priority"`
GroupIDs
[]
int64
`json:"group_ids"`
ExpiresAt
*
int64
`json:"expires_at"`
AutoPauseOnExpired
*
bool
`json:"auto_pause_on_expired"`
ConfirmMixedChannelRisk
*
bool
`json:"confirm_mixed_channel_risk"`
// 用户确认混合渠道风险
}
...
...
@@ -101,6 +103,8 @@ type UpdateAccountRequest struct {
Priority
*
int
`json:"priority"`
Status
string
`json:"status" binding:"omitempty,oneof=active inactive"`
GroupIDs
*
[]
int64
`json:"group_ids"`
ExpiresAt
*
int64
`json:"expires_at"`
AutoPauseOnExpired
*
bool
`json:"auto_pause_on_expired"`
ConfirmMixedChannelRisk
*
bool
`json:"confirm_mixed_channel_risk"`
// 用户确认混合渠道风险
}
...
...
@@ -204,6 +208,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
Concurrency
:
req
.
Concurrency
,
Priority
:
req
.
Priority
,
GroupIDs
:
req
.
GroupIDs
,
ExpiresAt
:
req
.
ExpiresAt
,
AutoPauseOnExpired
:
req
.
AutoPauseOnExpired
,
SkipMixedChannelCheck
:
skipCheck
,
})
if
err
!=
nil
{
...
...
@@ -261,6 +267,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
Priority
:
req
.
Priority
,
// 指针类型,nil 表示未提供
Status
:
req
.
Status
,
GroupIDs
:
req
.
GroupIDs
,
ExpiresAt
:
req
.
ExpiresAt
,
AutoPauseOnExpired
:
req
.
AutoPauseOnExpired
,
SkipMixedChannelCheck
:
skipCheck
,
})
if
err
!=
nil
{
...
...
backend/internal/handler/admin/dashboard_handler.go
View file @
c7abfe67
...
...
@@ -26,31 +26,33 @@ func NewDashboardHandler(dashboardService *service.DashboardService) *DashboardH
}
// parseTimeRange parses start_date, end_date query parameters
// Uses user's timezone if provided, otherwise falls back to server timezone
func
parseTimeRange
(
c
*
gin
.
Context
)
(
time
.
Time
,
time
.
Time
)
{
now
:=
timezone
.
Now
()
userTZ
:=
c
.
Query
(
"timezone"
)
// Get user's timezone from request
now
:=
timezone
.
NowInUserLocation
(
userTZ
)
startDate
:=
c
.
Query
(
"start_date"
)
endDate
:=
c
.
Query
(
"end_date"
)
var
startTime
,
endTime
time
.
Time
if
startDate
!=
""
{
if
t
,
err
:=
timezone
.
ParseInLocation
(
"2006-01-02"
,
startDate
);
err
==
nil
{
if
t
,
err
:=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
startDate
,
userTZ
);
err
==
nil
{
startTime
=
t
}
else
{
startTime
=
timezone
.
StartOfDay
(
now
.
AddDate
(
0
,
0
,
-
7
))
startTime
=
timezone
.
StartOfDay
InUserLocation
(
now
.
AddDate
(
0
,
0
,
-
7
)
,
userTZ
)
}
}
else
{
startTime
=
timezone
.
StartOfDay
(
now
.
AddDate
(
0
,
0
,
-
7
))
startTime
=
timezone
.
StartOfDay
InUserLocation
(
now
.
AddDate
(
0
,
0
,
-
7
)
,
userTZ
)
}
if
endDate
!=
""
{
if
t
,
err
:=
timezone
.
ParseInLocation
(
"2006-01-02"
,
endDate
);
err
==
nil
{
if
t
,
err
:=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
endDate
,
userTZ
);
err
==
nil
{
endTime
=
t
.
Add
(
24
*
time
.
Hour
)
// Include the end date
}
else
{
endTime
=
timezone
.
StartOfDay
(
now
.
AddDate
(
0
,
0
,
1
))
endTime
=
timezone
.
StartOfDay
InUserLocation
(
now
.
AddDate
(
0
,
0
,
1
)
,
userTZ
)
}
}
else
{
endTime
=
timezone
.
StartOfDay
(
now
.
AddDate
(
0
,
0
,
1
))
endTime
=
timezone
.
StartOfDay
InUserLocation
(
now
.
AddDate
(
0
,
0
,
1
)
,
userTZ
)
}
return
startTime
,
endTime
...
...
backend/internal/handler/admin/usage_handler.go
View file @
c7abfe67
...
...
@@ -102,8 +102,9 @@ func (h *UsageHandler) List(c *gin.Context) {
// Parse date range
var
startTime
,
endTime
*
time
.
Time
userTZ
:=
c
.
Query
(
"timezone"
)
// Get user's timezone from request
if
startDateStr
:=
c
.
Query
(
"start_date"
);
startDateStr
!=
""
{
t
,
err
:=
timezone
.
ParseInLocation
(
"2006-01-02"
,
startDateStr
)
t
,
err
:=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
startDateStr
,
userTZ
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid start_date format, use YYYY-MM-DD"
)
return
...
...
@@ -112,7 +113,7 @@ func (h *UsageHandler) List(c *gin.Context) {
}
if
endDateStr
:=
c
.
Query
(
"end_date"
);
endDateStr
!=
""
{
t
,
err
:=
timezone
.
ParseInLocation
(
"2006-01-02"
,
endDateStr
)
t
,
err
:=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
endDateStr
,
userTZ
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid end_date format, use YYYY-MM-DD"
)
return
...
...
@@ -151,8 +152,8 @@ func (h *UsageHandler) List(c *gin.Context) {
// Stats handles getting usage statistics with filters
// GET /api/v1/admin/usage/stats
func
(
h
*
UsageHandler
)
Stats
(
c
*
gin
.
Context
)
{
// Parse filters
var
userID
,
apiKeyID
int64
// Parse filters
- same as List endpoint
var
userID
,
apiKeyID
,
accountID
,
groupID
int64
if
userIDStr
:=
c
.
Query
(
"user_id"
);
userIDStr
!=
""
{
id
,
err
:=
strconv
.
ParseInt
(
userIDStr
,
10
,
64
)
if
err
!=
nil
{
...
...
@@ -171,8 +172,50 @@ func (h *UsageHandler) Stats(c *gin.Context) {
apiKeyID
=
id
}
if
accountIDStr
:=
c
.
Query
(
"account_id"
);
accountIDStr
!=
""
{
id
,
err
:=
strconv
.
ParseInt
(
accountIDStr
,
10
,
64
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid account_id"
)
return
}
accountID
=
id
}
if
groupIDStr
:=
c
.
Query
(
"group_id"
);
groupIDStr
!=
""
{
id
,
err
:=
strconv
.
ParseInt
(
groupIDStr
,
10
,
64
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid group_id"
)
return
}
groupID
=
id
}
model
:=
c
.
Query
(
"model"
)
var
stream
*
bool
if
streamStr
:=
c
.
Query
(
"stream"
);
streamStr
!=
""
{
val
,
err
:=
strconv
.
ParseBool
(
streamStr
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid stream value, use true or false"
)
return
}
stream
=
&
val
}
var
billingType
*
int8
if
billingTypeStr
:=
c
.
Query
(
"billing_type"
);
billingTypeStr
!=
""
{
val
,
err
:=
strconv
.
ParseInt
(
billingTypeStr
,
10
,
8
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid billing_type"
)
return
}
bt
:=
int8
(
val
)
billingType
=
&
bt
}
// Parse date range
now
:=
timezone
.
Now
()
userTZ
:=
c
.
Query
(
"timezone"
)
now
:=
timezone
.
NowInUserLocation
(
userTZ
)
var
startTime
,
endTime
time
.
Time
startDateStr
:=
c
.
Query
(
"start_date"
)
...
...
@@ -180,12 +223,12 @@ func (h *UsageHandler) Stats(c *gin.Context) {
if
startDateStr
!=
""
&&
endDateStr
!=
""
{
var
err
error
startTime
,
err
=
timezone
.
ParseInLocation
(
"2006-01-02"
,
startDateStr
)
startTime
,
err
=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
startDateStr
,
userTZ
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid start_date format, use YYYY-MM-DD"
)
return
}
endTime
,
err
=
timezone
.
ParseInLocation
(
"2006-01-02"
,
endDateStr
)
endTime
,
err
=
timezone
.
ParseIn
User
Location
(
"2006-01-02"
,
endDateStr
,
userTZ
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid end_date format, use YYYY-MM-DD"
)
return
...
...
@@ -195,39 +238,31 @@ func (h *UsageHandler) Stats(c *gin.Context) {
period
:=
c
.
DefaultQuery
(
"period"
,
"today"
)
switch
period
{
case
"today"
:
startTime
=
timezone
.
StartOfDay
(
now
)
startTime
=
timezone
.
StartOfDay
InUserLocation
(
now
,
userTZ
)
case
"week"
:
startTime
=
now
.
AddDate
(
0
,
0
,
-
7
)
case
"month"
:
startTime
=
now
.
AddDate
(
0
,
-
1
,
0
)
default
:
startTime
=
timezone
.
StartOfDay
(
now
)
startTime
=
timezone
.
StartOfDay
InUserLocation
(
now
,
userTZ
)
}
endTime
=
now
}
if
apiKeyID
>
0
{
stats
,
err
:=
h
.
usageService
.
GetStatsByAPIKey
(
c
.
Request
.
Context
(),
apiKeyID
,
startTime
,
endTime
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
stats
)
return
}
if
userID
>
0
{
stats
,
err
:=
h
.
usageService
.
GetStatsByUser
(
c
.
Request
.
Context
(),
userID
,
startTime
,
endTime
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
stats
)
return
// Build filters and call GetStatsWithFilters
filters
:=
usagestats
.
UsageLogFilters
{
UserID
:
userID
,
APIKeyID
:
apiKeyID
,
AccountID
:
accountID
,
GroupID
:
groupID
,
Model
:
model
,
Stream
:
stream
,
BillingType
:
billingType
,
StartTime
:
&
startTime
,
EndTime
:
&
endTime
,
}
// Get global stats
stats
,
err
:=
h
.
usageService
.
GetGlobalStats
(
c
.
Request
.
Context
(),
startTime
,
endTime
)
stats
,
err
:=
h
.
usageService
.
GetStatsWithFilters
(
c
.
Request
.
Context
(),
filters
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
backend/internal/handler/dto/mappers.go
View file @
c7abfe67
// Package dto provides data transfer objects for HTTP handlers.
package
dto
import
"github.com/Wei-Shaw/sub2api/internal/service"
import
(
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
)
func
UserFromServiceShallow
(
u
*
service
.
User
)
*
User
{
if
u
==
nil
{
...
...
@@ -120,6 +124,8 @@ func AccountFromServiceShallow(a *service.Account) *Account {
Status
:
a
.
Status
,
ErrorMessage
:
a
.
ErrorMessage
,
LastUsedAt
:
a
.
LastUsedAt
,
ExpiresAt
:
timeToUnixSeconds
(
a
.
ExpiresAt
),
AutoPauseOnExpired
:
a
.
AutoPauseOnExpired
,
CreatedAt
:
a
.
CreatedAt
,
UpdatedAt
:
a
.
UpdatedAt
,
Schedulable
:
a
.
Schedulable
,
...
...
@@ -157,6 +163,14 @@ func AccountFromService(a *service.Account) *Account {
return
out
}
func
timeToUnixSeconds
(
value
*
time
.
Time
)
*
int64
{
if
value
==
nil
{
return
nil
}
ts
:=
value
.
Unix
()
return
&
ts
}
func
AccountGroupFromService
(
ag
*
service
.
AccountGroup
)
*
AccountGroup
{
if
ag
==
nil
{
return
nil
...
...
backend/internal/handler/dto/types.go
View file @
c7abfe67
...
...
@@ -60,21 +60,23 @@ type Group struct {
}
type
Account
struct
{
ID
int64
`json:"id"`
Name
string
`json:"name"`
Notes
*
string
`json:"notes"`
Platform
string
`json:"platform"`
Type
string
`json:"type"`
Credentials
map
[
string
]
any
`json:"credentials"`
Extra
map
[
string
]
any
`json:"extra"`
ProxyID
*
int64
`json:"proxy_id"`
Concurrency
int
`json:"concurrency"`
Priority
int
`json:"priority"`
Status
string
`json:"status"`
ErrorMessage
string
`json:"error_message"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
ID
int64
`json:"id"`
Name
string
`json:"name"`
Notes
*
string
`json:"notes"`
Platform
string
`json:"platform"`
Type
string
`json:"type"`
Credentials
map
[
string
]
any
`json:"credentials"`
Extra
map
[
string
]
any
`json:"extra"`
ProxyID
*
int64
`json:"proxy_id"`
Concurrency
int
`json:"concurrency"`
Priority
int
`json:"priority"`
Status
string
`json:"status"`
ErrorMessage
string
`json:"error_message"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
ExpiresAt
*
int64
`json:"expires_at"`
AutoPauseOnExpired
bool
`json:"auto_pause_on_expired"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
Schedulable
bool
`json:"schedulable"`
...
...
backend/internal/handler/gateway_handler.go
View file @
c7abfe67
...
...
@@ -108,6 +108,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
// 获取订阅信息(可能为nil)- 提前获取用于后续检查
subscription
,
_
:=
middleware2
.
GetSubscriptionFromContext
(
c
)
// 获取 User-Agent
userAgent
:=
c
.
Request
.
UserAgent
()
// 0. 检查wait队列是否已满
maxWait
:=
service
.
CalculateMaxWait
(
subject
.
Concurrency
)
canWait
,
err
:=
h
.
concurrencyHelper
.
IncrementWaitCount
(
c
.
Request
.
Context
(),
subject
.
UserID
,
maxWait
)
...
...
@@ -267,7 +270,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
}
// 异步记录使用量(subscription已在函数开头获取)
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
)
{
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
,
ua
string
)
{
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
10
*
time
.
Second
)
defer
cancel
()
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
...
@@ -276,10 +279,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
User
:
apiKey
.
User
,
Account
:
usedAccount
,
Subscription
:
subscription
,
UserAgent
:
ua
,
});
err
!=
nil
{
log
.
Printf
(
"Record usage failed: %v"
,
err
)
}
}(
result
,
account
)
}(
result
,
account
,
userAgent
)
return
}
}
...
...
@@ -394,7 +398,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
}
// 异步记录使用量(subscription已在函数开头获取)
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
)
{
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
,
ua
string
)
{
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
10
*
time
.
Second
)
defer
cancel
()
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
...
@@ -403,10 +407,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
User
:
apiKey
.
User
,
Account
:
usedAccount
,
Subscription
:
subscription
,
UserAgent
:
ua
,
});
err
!=
nil
{
log
.
Printf
(
"Record usage failed: %v"
,
err
)
}
}(
result
,
account
)
}(
result
,
account
,
userAgent
)
return
}
}
...
...
backend/internal/handler/gateway_helper.go
View file @
c7abfe67
...
...
@@ -83,19 +83,33 @@ func NewConcurrencyHelper(concurrencyService *service.ConcurrencyService, pingFo
// wrapReleaseOnDone ensures release runs at most once and still triggers on context cancellation.
// 用于避免客户端断开或上游超时导致的并发槽位泄漏。
// 修复:添加 quit channel 确保 goroutine 及时退出,避免泄露
func
wrapReleaseOnDone
(
ctx
context
.
Context
,
releaseFunc
func
())
func
()
{
if
releaseFunc
==
nil
{
return
nil
}
var
once
sync
.
Once
wrapped
:=
func
()
{
once
.
Do
(
releaseFunc
)
quit
:=
make
(
chan
struct
{})
release
:=
func
()
{
once
.
Do
(
func
()
{
releaseFunc
()
close
(
quit
)
// 通知监听 goroutine 退出
})
}
go
func
()
{
<-
ctx
.
Done
()
wrapped
()
select
{
case
<-
ctx
.
Done
()
:
// Context 取消时释放资源
release
()
case
<-
quit
:
// 正常释放已完成,goroutine 退出
return
}
}()
return
wrapped
return
release
}
// IncrementWaitCount increments the wait count for a user
...
...
backend/internal/handler/gateway_helper_test.go
0 → 100644
View file @
c7abfe67
package
handler
import
(
"context"
"runtime"
"sync/atomic"
"testing"
"time"
)
// TestWrapReleaseOnDone_NoGoroutineLeak 验证 wrapReleaseOnDone 修复后不会泄露 goroutine
func
TestWrapReleaseOnDone_NoGoroutineLeak
(
t
*
testing
.
T
)
{
// 记录测试开始时的 goroutine 数量
runtime
.
GC
()
time
.
Sleep
(
100
*
time
.
Millisecond
)
initialGoroutines
:=
runtime
.
NumGoroutine
()
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
defer
cancel
()
var
releaseCount
int32
release
:=
wrapReleaseOnDone
(
ctx
,
func
()
{
atomic
.
AddInt32
(
&
releaseCount
,
1
)
})
// 正常释放
release
()
// 等待足够时间确保 goroutine 退出
time
.
Sleep
(
200
*
time
.
Millisecond
)
// 验证只释放一次
if
count
:=
atomic
.
LoadInt32
(
&
releaseCount
);
count
!=
1
{
t
.
Errorf
(
"expected release count to be 1, got %d"
,
count
)
}
// 强制 GC,清理已退出的 goroutine
runtime
.
GC
()
time
.
Sleep
(
100
*
time
.
Millisecond
)
// 验证 goroutine 数量没有增加(允许±2的误差,考虑到测试框架本身可能创建的 goroutine)
finalGoroutines
:=
runtime
.
NumGoroutine
()
if
finalGoroutines
>
initialGoroutines
+
2
{
t
.
Errorf
(
"goroutine leak detected: initial=%d, final=%d, leaked=%d"
,
initialGoroutines
,
finalGoroutines
,
finalGoroutines
-
initialGoroutines
)
}
}
// TestWrapReleaseOnDone_ContextCancellation 验证 context 取消时也能正确释放
func
TestWrapReleaseOnDone_ContextCancellation
(
t
*
testing
.
T
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
var
releaseCount
int32
_
=
wrapReleaseOnDone
(
ctx
,
func
()
{
atomic
.
AddInt32
(
&
releaseCount
,
1
)
})
// 取消 context,应该触发释放
cancel
()
// 等待释放完成
time
.
Sleep
(
100
*
time
.
Millisecond
)
// 验证释放被调用
if
count
:=
atomic
.
LoadInt32
(
&
releaseCount
);
count
!=
1
{
t
.
Errorf
(
"expected release count to be 1, got %d"
,
count
)
}
}
// TestWrapReleaseOnDone_MultipleCallsOnlyReleaseOnce 验证多次调用 release 只释放一次
func
TestWrapReleaseOnDone_MultipleCallsOnlyReleaseOnce
(
t
*
testing
.
T
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
defer
cancel
()
var
releaseCount
int32
release
:=
wrapReleaseOnDone
(
ctx
,
func
()
{
atomic
.
AddInt32
(
&
releaseCount
,
1
)
})
// 调用多次
release
()
release
()
release
()
// 等待执行完成
time
.
Sleep
(
100
*
time
.
Millisecond
)
// 验证只释放一次
if
count
:=
atomic
.
LoadInt32
(
&
releaseCount
);
count
!=
1
{
t
.
Errorf
(
"expected release count to be 1, got %d"
,
count
)
}
}
// TestWrapReleaseOnDone_NilReleaseFunc 验证 nil releaseFunc 不会 panic
func
TestWrapReleaseOnDone_NilReleaseFunc
(
t
*
testing
.
T
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
defer
cancel
()
release
:=
wrapReleaseOnDone
(
ctx
,
nil
)
if
release
!=
nil
{
t
.
Error
(
"expected nil release function when releaseFunc is nil"
)
}
}
// TestWrapReleaseOnDone_ConcurrentCalls 验证并发调用的安全性
func
TestWrapReleaseOnDone_ConcurrentCalls
(
t
*
testing
.
T
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
defer
cancel
()
var
releaseCount
int32
release
:=
wrapReleaseOnDone
(
ctx
,
func
()
{
atomic
.
AddInt32
(
&
releaseCount
,
1
)
})
// 并发调用 release
const
numGoroutines
=
10
for
i
:=
0
;
i
<
numGoroutines
;
i
++
{
go
release
()
}
// 等待所有 goroutine 完成
time
.
Sleep
(
200
*
time
.
Millisecond
)
// 验证只释放一次
if
count
:=
atomic
.
LoadInt32
(
&
releaseCount
);
count
!=
1
{
t
.
Errorf
(
"expected release count to be 1, got %d"
,
count
)
}
}
// BenchmarkWrapReleaseOnDone 性能基准测试
func
BenchmarkWrapReleaseOnDone
(
b
*
testing
.
B
)
{
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
defer
cancel
()
b
.
ResetTimer
()
for
i
:=
0
;
i
<
b
.
N
;
i
++
{
release
:=
wrapReleaseOnDone
(
ctx
,
func
()
{})
release
()
}
}
backend/internal/handler/gemini_v1beta_handler.go
View file @
c7abfe67
...
...
@@ -164,6 +164,9 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
// Get subscription (may be nil)
subscription
,
_
:=
middleware
.
GetSubscriptionFromContext
(
c
)
// 获取 User-Agent
userAgent
:=
c
.
Request
.
UserAgent
()
// For Gemini native API, do not send Claude-style ping frames.
geminiConcurrency
:=
NewConcurrencyHelper
(
h
.
concurrencyHelper
.
concurrencyService
,
SSEPingFormatNone
,
0
)
...
...
@@ -300,7 +303,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
}
// 6) record usage async
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
)
{
go
func
(
result
*
service
.
ForwardResult
,
usedAccount
*
service
.
Account
,
ua
string
)
{
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
10
*
time
.
Second
)
defer
cancel
()
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
...
@@ -309,10 +312,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) {
User
:
apiKey
.
User
,
Account
:
usedAccount
,
Subscription
:
subscription
,
UserAgent
:
ua
,
});
err
!=
nil
{
log
.
Printf
(
"Record usage failed: %v"
,
err
)
}
}(
result
,
account
)
}(
result
,
account
,
userAgent
)
return
}
}
...
...
Prev
1
2
3
4
5
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment