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
c8e2f614
Commit
c8e2f614
authored
Jan 20, 2026
by
cyhhao
Browse files
Merge branch 'main' of github.com:Wei-Shaw/sub2api
parents
c0347cde
c95a8649
Changes
167
Hide whitespace changes
Inline
Side-by-side
backend/ent/usagecleanuptask_update.go
0 → 100644
View file @
c8e2f614
// Code generated by ent, DO NOT EDIT.
package
ent
import
(
"context"
"encoding/json"
"errors"
"fmt"
"time"
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/dialect/sql/sqljson"
"entgo.io/ent/schema/field"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
)
// UsageCleanupTaskUpdate is the builder for updating UsageCleanupTask entities.
type
UsageCleanupTaskUpdate
struct
{
config
hooks
[]
Hook
mutation
*
UsageCleanupTaskMutation
}
// Where appends a list predicates to the UsageCleanupTaskUpdate builder.
func
(
_u
*
UsageCleanupTaskUpdate
)
Where
(
ps
...
predicate
.
UsageCleanupTask
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
Where
(
ps
...
)
return
_u
}
// SetUpdatedAt sets the "updated_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetUpdatedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetUpdatedAt
(
v
)
return
_u
}
// SetStatus sets the "status" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetStatus
(
v
string
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetStatus
(
v
)
return
_u
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableStatus
(
v
*
string
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetStatus
(
*
v
)
}
return
_u
}
// SetFilters sets the "filters" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetFilters
(
v
json
.
RawMessage
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetFilters
(
v
)
return
_u
}
// AppendFilters appends value to the "filters" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
AppendFilters
(
v
json
.
RawMessage
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
AppendFilters
(
v
)
return
_u
}
// SetCreatedBy sets the "created_by" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetCreatedBy
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ResetCreatedBy
()
_u
.
mutation
.
SetCreatedBy
(
v
)
return
_u
}
// SetNillableCreatedBy sets the "created_by" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableCreatedBy
(
v
*
int64
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetCreatedBy
(
*
v
)
}
return
_u
}
// AddCreatedBy adds value to the "created_by" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
AddCreatedBy
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
AddCreatedBy
(
v
)
return
_u
}
// SetDeletedRows sets the "deleted_rows" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetDeletedRows
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ResetDeletedRows
()
_u
.
mutation
.
SetDeletedRows
(
v
)
return
_u
}
// SetNillableDeletedRows sets the "deleted_rows" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableDeletedRows
(
v
*
int64
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetDeletedRows
(
*
v
)
}
return
_u
}
// AddDeletedRows adds value to the "deleted_rows" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
AddDeletedRows
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
AddDeletedRows
(
v
)
return
_u
}
// SetErrorMessage sets the "error_message" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetErrorMessage
(
v
string
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetErrorMessage
(
v
)
return
_u
}
// SetNillableErrorMessage sets the "error_message" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableErrorMessage
(
v
*
string
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetErrorMessage
(
*
v
)
}
return
_u
}
// ClearErrorMessage clears the value of the "error_message" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
ClearErrorMessage
()
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ClearErrorMessage
()
return
_u
}
// SetCanceledBy sets the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetCanceledBy
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ResetCanceledBy
()
_u
.
mutation
.
SetCanceledBy
(
v
)
return
_u
}
// SetNillableCanceledBy sets the "canceled_by" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableCanceledBy
(
v
*
int64
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetCanceledBy
(
*
v
)
}
return
_u
}
// AddCanceledBy adds value to the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
AddCanceledBy
(
v
int64
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
AddCanceledBy
(
v
)
return
_u
}
// ClearCanceledBy clears the value of the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
ClearCanceledBy
()
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ClearCanceledBy
()
return
_u
}
// SetCanceledAt sets the "canceled_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetCanceledAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetCanceledAt
(
v
)
return
_u
}
// SetNillableCanceledAt sets the "canceled_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableCanceledAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetCanceledAt
(
*
v
)
}
return
_u
}
// ClearCanceledAt clears the value of the "canceled_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
ClearCanceledAt
()
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ClearCanceledAt
()
return
_u
}
// SetStartedAt sets the "started_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetStartedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetStartedAt
(
v
)
return
_u
}
// SetNillableStartedAt sets the "started_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableStartedAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetStartedAt
(
*
v
)
}
return
_u
}
// ClearStartedAt clears the value of the "started_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
ClearStartedAt
()
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ClearStartedAt
()
return
_u
}
// SetFinishedAt sets the "finished_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetFinishedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
SetFinishedAt
(
v
)
return
_u
}
// SetNillableFinishedAt sets the "finished_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdate
)
SetNillableFinishedAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdate
{
if
v
!=
nil
{
_u
.
SetFinishedAt
(
*
v
)
}
return
_u
}
// ClearFinishedAt clears the value of the "finished_at" field.
func
(
_u
*
UsageCleanupTaskUpdate
)
ClearFinishedAt
()
*
UsageCleanupTaskUpdate
{
_u
.
mutation
.
ClearFinishedAt
()
return
_u
}
// Mutation returns the UsageCleanupTaskMutation object of the builder.
func
(
_u
*
UsageCleanupTaskUpdate
)
Mutation
()
*
UsageCleanupTaskMutation
{
return
_u
.
mutation
}
// Save executes the query and returns the number of nodes affected by the update operation.
func
(
_u
*
UsageCleanupTaskUpdate
)
Save
(
ctx
context
.
Context
)
(
int
,
error
)
{
_u
.
defaults
()
return
withHooks
(
ctx
,
_u
.
sqlSave
,
_u
.
mutation
,
_u
.
hooks
)
}
// SaveX is like Save, but panics if an error occurs.
func
(
_u
*
UsageCleanupTaskUpdate
)
SaveX
(
ctx
context
.
Context
)
int
{
affected
,
err
:=
_u
.
Save
(
ctx
)
if
err
!=
nil
{
panic
(
err
)
}
return
affected
}
// Exec executes the query.
func
(
_u
*
UsageCleanupTaskUpdate
)
Exec
(
ctx
context
.
Context
)
error
{
_
,
err
:=
_u
.
Save
(
ctx
)
return
err
}
// ExecX is like Exec, but panics if an error occurs.
func
(
_u
*
UsageCleanupTaskUpdate
)
ExecX
(
ctx
context
.
Context
)
{
if
err
:=
_u
.
Exec
(
ctx
);
err
!=
nil
{
panic
(
err
)
}
}
// defaults sets the default values of the builder before save.
func
(
_u
*
UsageCleanupTaskUpdate
)
defaults
()
{
if
_
,
ok
:=
_u
.
mutation
.
UpdatedAt
();
!
ok
{
v
:=
usagecleanuptask
.
UpdateDefaultUpdatedAt
()
_u
.
mutation
.
SetUpdatedAt
(
v
)
}
}
// check runs all checks and user-defined validators on the builder.
func
(
_u
*
UsageCleanupTaskUpdate
)
check
()
error
{
if
v
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
if
err
:=
usagecleanuptask
.
StatusValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageCleanupTask.status": %w`
,
err
)}
}
}
return
nil
}
func
(
_u
*
UsageCleanupTaskUpdate
)
sqlSave
(
ctx
context
.
Context
)
(
_node
int
,
err
error
)
{
if
err
:=
_u
.
check
();
err
!=
nil
{
return
_node
,
err
}
_spec
:=
sqlgraph
.
NewUpdateSpec
(
usagecleanuptask
.
Table
,
usagecleanuptask
.
Columns
,
sqlgraph
.
NewFieldSpec
(
usagecleanuptask
.
FieldID
,
field
.
TypeInt64
))
if
ps
:=
_u
.
mutation
.
predicates
;
len
(
ps
)
>
0
{
_spec
.
Predicate
=
func
(
selector
*
sql
.
Selector
)
{
for
i
:=
range
ps
{
ps
[
i
](
selector
)
}
}
}
if
value
,
ok
:=
_u
.
mutation
.
UpdatedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldUpdatedAt
,
field
.
TypeTime
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldStatus
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Filters
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldFilters
,
field
.
TypeJSON
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AppendedFilters
();
ok
{
_spec
.
AddModifier
(
func
(
u
*
sql
.
UpdateBuilder
)
{
sqljson
.
Append
(
u
,
usagecleanuptask
.
FieldFilters
,
value
)
})
}
if
value
,
ok
:=
_u
.
mutation
.
CreatedBy
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCreatedBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedCreatedBy
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldCreatedBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
DeletedRows
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldDeletedRows
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedDeletedRows
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldDeletedRows
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
ErrorMessage
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldErrorMessage
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
ErrorMessageCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldErrorMessage
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
CanceledBy
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedCanceledBy
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
,
value
)
}
if
_u
.
mutation
.
CanceledByCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
)
}
if
value
,
ok
:=
_u
.
mutation
.
CanceledAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCanceledAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
CanceledAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldCanceledAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
StartedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldStartedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
StartedAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldStartedAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
FinishedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldFinishedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
FinishedAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldFinishedAt
,
field
.
TypeTime
)
}
if
_node
,
err
=
sqlgraph
.
UpdateNodes
(
ctx
,
_u
.
driver
,
_spec
);
err
!=
nil
{
if
_
,
ok
:=
err
.
(
*
sqlgraph
.
NotFoundError
);
ok
{
err
=
&
NotFoundError
{
usagecleanuptask
.
Label
}
}
else
if
sqlgraph
.
IsConstraintError
(
err
)
{
err
=
&
ConstraintError
{
msg
:
err
.
Error
(),
wrap
:
err
}
}
return
0
,
err
}
_u
.
mutation
.
done
=
true
return
_node
,
nil
}
// UsageCleanupTaskUpdateOne is the builder for updating a single UsageCleanupTask entity.
type
UsageCleanupTaskUpdateOne
struct
{
config
fields
[]
string
hooks
[]
Hook
mutation
*
UsageCleanupTaskMutation
}
// SetUpdatedAt sets the "updated_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetUpdatedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetUpdatedAt
(
v
)
return
_u
}
// SetStatus sets the "status" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetStatus
(
v
string
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetStatus
(
v
)
return
_u
}
// SetNillableStatus sets the "status" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableStatus
(
v
*
string
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetStatus
(
*
v
)
}
return
_u
}
// SetFilters sets the "filters" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetFilters
(
v
json
.
RawMessage
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetFilters
(
v
)
return
_u
}
// AppendFilters appends value to the "filters" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
AppendFilters
(
v
json
.
RawMessage
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
AppendFilters
(
v
)
return
_u
}
// SetCreatedBy sets the "created_by" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetCreatedBy
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ResetCreatedBy
()
_u
.
mutation
.
SetCreatedBy
(
v
)
return
_u
}
// SetNillableCreatedBy sets the "created_by" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableCreatedBy
(
v
*
int64
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetCreatedBy
(
*
v
)
}
return
_u
}
// AddCreatedBy adds value to the "created_by" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
AddCreatedBy
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
AddCreatedBy
(
v
)
return
_u
}
// SetDeletedRows sets the "deleted_rows" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetDeletedRows
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ResetDeletedRows
()
_u
.
mutation
.
SetDeletedRows
(
v
)
return
_u
}
// SetNillableDeletedRows sets the "deleted_rows" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableDeletedRows
(
v
*
int64
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetDeletedRows
(
*
v
)
}
return
_u
}
// AddDeletedRows adds value to the "deleted_rows" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
AddDeletedRows
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
AddDeletedRows
(
v
)
return
_u
}
// SetErrorMessage sets the "error_message" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetErrorMessage
(
v
string
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetErrorMessage
(
v
)
return
_u
}
// SetNillableErrorMessage sets the "error_message" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableErrorMessage
(
v
*
string
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetErrorMessage
(
*
v
)
}
return
_u
}
// ClearErrorMessage clears the value of the "error_message" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ClearErrorMessage
()
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ClearErrorMessage
()
return
_u
}
// SetCanceledBy sets the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetCanceledBy
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ResetCanceledBy
()
_u
.
mutation
.
SetCanceledBy
(
v
)
return
_u
}
// SetNillableCanceledBy sets the "canceled_by" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableCanceledBy
(
v
*
int64
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetCanceledBy
(
*
v
)
}
return
_u
}
// AddCanceledBy adds value to the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
AddCanceledBy
(
v
int64
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
AddCanceledBy
(
v
)
return
_u
}
// ClearCanceledBy clears the value of the "canceled_by" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ClearCanceledBy
()
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ClearCanceledBy
()
return
_u
}
// SetCanceledAt sets the "canceled_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetCanceledAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetCanceledAt
(
v
)
return
_u
}
// SetNillableCanceledAt sets the "canceled_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableCanceledAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetCanceledAt
(
*
v
)
}
return
_u
}
// ClearCanceledAt clears the value of the "canceled_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ClearCanceledAt
()
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ClearCanceledAt
()
return
_u
}
// SetStartedAt sets the "started_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetStartedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetStartedAt
(
v
)
return
_u
}
// SetNillableStartedAt sets the "started_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableStartedAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetStartedAt
(
*
v
)
}
return
_u
}
// ClearStartedAt clears the value of the "started_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ClearStartedAt
()
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ClearStartedAt
()
return
_u
}
// SetFinishedAt sets the "finished_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetFinishedAt
(
v
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
SetFinishedAt
(
v
)
return
_u
}
// SetNillableFinishedAt sets the "finished_at" field if the given value is not nil.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SetNillableFinishedAt
(
v
*
time
.
Time
)
*
UsageCleanupTaskUpdateOne
{
if
v
!=
nil
{
_u
.
SetFinishedAt
(
*
v
)
}
return
_u
}
// ClearFinishedAt clears the value of the "finished_at" field.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ClearFinishedAt
()
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
ClearFinishedAt
()
return
_u
}
// Mutation returns the UsageCleanupTaskMutation object of the builder.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
Mutation
()
*
UsageCleanupTaskMutation
{
return
_u
.
mutation
}
// Where appends a list predicates to the UsageCleanupTaskUpdate builder.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
Where
(
ps
...
predicate
.
UsageCleanupTask
)
*
UsageCleanupTaskUpdateOne
{
_u
.
mutation
.
Where
(
ps
...
)
return
_u
}
// Select allows selecting one or more fields (columns) of the returned entity.
// The default is selecting all fields defined in the entity schema.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
Select
(
field
string
,
fields
...
string
)
*
UsageCleanupTaskUpdateOne
{
_u
.
fields
=
append
([]
string
{
field
},
fields
...
)
return
_u
}
// Save executes the query and returns the updated UsageCleanupTask entity.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
Save
(
ctx
context
.
Context
)
(
*
UsageCleanupTask
,
error
)
{
_u
.
defaults
()
return
withHooks
(
ctx
,
_u
.
sqlSave
,
_u
.
mutation
,
_u
.
hooks
)
}
// SaveX is like Save, but panics if an error occurs.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
SaveX
(
ctx
context
.
Context
)
*
UsageCleanupTask
{
node
,
err
:=
_u
.
Save
(
ctx
)
if
err
!=
nil
{
panic
(
err
)
}
return
node
}
// Exec executes the query on the entity.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
Exec
(
ctx
context
.
Context
)
error
{
_
,
err
:=
_u
.
Save
(
ctx
)
return
err
}
// ExecX is like Exec, but panics if an error occurs.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
ExecX
(
ctx
context
.
Context
)
{
if
err
:=
_u
.
Exec
(
ctx
);
err
!=
nil
{
panic
(
err
)
}
}
// defaults sets the default values of the builder before save.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
defaults
()
{
if
_
,
ok
:=
_u
.
mutation
.
UpdatedAt
();
!
ok
{
v
:=
usagecleanuptask
.
UpdateDefaultUpdatedAt
()
_u
.
mutation
.
SetUpdatedAt
(
v
)
}
}
// check runs all checks and user-defined validators on the builder.
func
(
_u
*
UsageCleanupTaskUpdateOne
)
check
()
error
{
if
v
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
if
err
:=
usagecleanuptask
.
StatusValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "UsageCleanupTask.status": %w`
,
err
)}
}
}
return
nil
}
func
(
_u
*
UsageCleanupTaskUpdateOne
)
sqlSave
(
ctx
context
.
Context
)
(
_node
*
UsageCleanupTask
,
err
error
)
{
if
err
:=
_u
.
check
();
err
!=
nil
{
return
_node
,
err
}
_spec
:=
sqlgraph
.
NewUpdateSpec
(
usagecleanuptask
.
Table
,
usagecleanuptask
.
Columns
,
sqlgraph
.
NewFieldSpec
(
usagecleanuptask
.
FieldID
,
field
.
TypeInt64
))
id
,
ok
:=
_u
.
mutation
.
ID
()
if
!
ok
{
return
nil
,
&
ValidationError
{
Name
:
"id"
,
err
:
errors
.
New
(
`ent: missing "UsageCleanupTask.id" for update`
)}
}
_spec
.
Node
.
ID
.
Value
=
id
if
fields
:=
_u
.
fields
;
len
(
fields
)
>
0
{
_spec
.
Node
.
Columns
=
make
([]
string
,
0
,
len
(
fields
))
_spec
.
Node
.
Columns
=
append
(
_spec
.
Node
.
Columns
,
usagecleanuptask
.
FieldID
)
for
_
,
f
:=
range
fields
{
if
!
usagecleanuptask
.
ValidColumn
(
f
)
{
return
nil
,
&
ValidationError
{
Name
:
f
,
err
:
fmt
.
Errorf
(
"ent: invalid field %q for query"
,
f
)}
}
if
f
!=
usagecleanuptask
.
FieldID
{
_spec
.
Node
.
Columns
=
append
(
_spec
.
Node
.
Columns
,
f
)
}
}
}
if
ps
:=
_u
.
mutation
.
predicates
;
len
(
ps
)
>
0
{
_spec
.
Predicate
=
func
(
selector
*
sql
.
Selector
)
{
for
i
:=
range
ps
{
ps
[
i
](
selector
)
}
}
}
if
value
,
ok
:=
_u
.
mutation
.
UpdatedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldUpdatedAt
,
field
.
TypeTime
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldStatus
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Filters
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldFilters
,
field
.
TypeJSON
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AppendedFilters
();
ok
{
_spec
.
AddModifier
(
func
(
u
*
sql
.
UpdateBuilder
)
{
sqljson
.
Append
(
u
,
usagecleanuptask
.
FieldFilters
,
value
)
})
}
if
value
,
ok
:=
_u
.
mutation
.
CreatedBy
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCreatedBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedCreatedBy
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldCreatedBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
DeletedRows
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldDeletedRows
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedDeletedRows
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldDeletedRows
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
ErrorMessage
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldErrorMessage
,
field
.
TypeString
,
value
)
}
if
_u
.
mutation
.
ErrorMessageCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldErrorMessage
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
CanceledBy
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedCanceledBy
();
ok
{
_spec
.
AddField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
,
value
)
}
if
_u
.
mutation
.
CanceledByCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldCanceledBy
,
field
.
TypeInt64
)
}
if
value
,
ok
:=
_u
.
mutation
.
CanceledAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldCanceledAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
CanceledAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldCanceledAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
StartedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldStartedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
StartedAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldStartedAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
FinishedAt
();
ok
{
_spec
.
SetField
(
usagecleanuptask
.
FieldFinishedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
FinishedAtCleared
()
{
_spec
.
ClearField
(
usagecleanuptask
.
FieldFinishedAt
,
field
.
TypeTime
)
}
_node
=
&
UsageCleanupTask
{
config
:
_u
.
config
}
_spec
.
Assign
=
_node
.
assignValues
_spec
.
ScanValues
=
_node
.
scanValues
if
err
=
sqlgraph
.
UpdateNode
(
ctx
,
_u
.
driver
,
_spec
);
err
!=
nil
{
if
_
,
ok
:=
err
.
(
*
sqlgraph
.
NotFoundError
);
ok
{
err
=
&
NotFoundError
{
usagecleanuptask
.
Label
}
}
else
if
sqlgraph
.
IsConstraintError
(
err
)
{
err
=
&
ConstraintError
{
msg
:
err
.
Error
(),
wrap
:
err
}
}
return
nil
,
err
}
_u
.
mutation
.
done
=
true
return
_node
,
nil
}
backend/go.mod
View file @
c8e2f614
...
...
@@ -31,6 +31,7 @@ require (
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
...
...
@@ -97,6 +98,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
...
...
@@ -107,6 +109,7 @@ require (
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
...
...
@@ -139,7 +142,7 @@ require (
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-202
30905200255-921286631fa9
// indirect
golang.org/x/exp v0.0.0-202
51023183803-a4bb9ffd2546
// 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
...
...
@@ -148,4 +151,8 @@ require (
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.44.1 // indirect
)
backend/go.sum
View file @
c8e2f614
...
...
@@ -141,6 +141,7 @@ 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
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=
...
...
@@ -199,6 +200,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
...
...
@@ -224,6 +227,8 @@ github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4Vi
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=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
...
...
@@ -338,6 +343,8 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.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/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
...
...
@@ -365,6 +372,7 @@ golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.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/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
...
...
@@ -387,4 +395,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.44.1 h1:qybx/rNpfQipX/t47OxbHmkkJuv2JWifCMH8SVUiDas=
modernc.org/sqlite v1.44.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
backend/internal/config/config.go
View file @
c8e2f614
...
...
@@ -55,6 +55,7 @@ type Config struct {
APIKeyAuth
APIKeyAuthCacheConfig
`mapstructure:"api_key_auth_cache"`
Dashboard
DashboardCacheConfig
`mapstructure:"dashboard_cache"`
DashboardAgg
DashboardAggregationConfig
`mapstructure:"dashboard_aggregation"`
UsageCleanup
UsageCleanupConfig
`mapstructure:"usage_cleanup"`
Concurrency
ConcurrencyConfig
`mapstructure:"concurrency"`
TokenRefresh
TokenRefreshConfig
`mapstructure:"token_refresh"`
RunMode
string
`mapstructure:"run_mode" yaml:"run_mode"`
...
...
@@ -257,8 +258,43 @@ type GatewayConfig struct {
// 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
FailoverOn400
bool
`mapstructure:"failover_on_400"`
// 账户切换最大次数(遇到上游错误时切换到其他账户的次数上限)
MaxAccountSwitches
int
`mapstructure:"max_account_switches"`
// Gemini 账户切换最大次数(Gemini 平台单独配置,因 API 限制更严格)
MaxAccountSwitchesGemini
int
`mapstructure:"max_account_switches_gemini"`
// Antigravity 429 fallback 限流时间(分钟),解析重置时间失败时使用
AntigravityFallbackCooldownMinutes
int
`mapstructure:"antigravity_fallback_cooldown_minutes"`
// Scheduling: 账号调度相关配置
Scheduling
GatewaySchedulingConfig
`mapstructure:"scheduling"`
// TLSFingerprint: TLS指纹伪装配置
TLSFingerprint
TLSFingerprintConfig
`mapstructure:"tls_fingerprint"`
}
// TLSFingerprintConfig TLS指纹伪装配置
// 用于模拟 Claude CLI (Node.js) 的 TLS 握手特征,避免被识别为非官方客户端
type
TLSFingerprintConfig
struct
{
// Enabled: 是否全局启用TLS指纹功能
Enabled
bool
`mapstructure:"enabled"`
// Profiles: 预定义的TLS指纹配置模板
// key 为模板名称,如 "claude_cli_v2", "chrome_120" 等
Profiles
map
[
string
]
TLSProfileConfig
`mapstructure:"profiles"`
}
// TLSProfileConfig 单个TLS指纹模板的配置
type
TLSProfileConfig
struct
{
// Name: 模板显示名称
Name
string
`mapstructure:"name"`
// EnableGREASE: 是否启用GREASE扩展(Chrome使用,Node.js不使用)
EnableGREASE
bool
`mapstructure:"enable_grease"`
// CipherSuites: TLS加密套件列表(空则使用内置默认值)
CipherSuites
[]
uint16
`mapstructure:"cipher_suites"`
// Curves: 椭圆曲线列表(空则使用内置默认值)
Curves
[]
uint16
`mapstructure:"curves"`
// PointFormats: 点格式列表(空则使用内置默认值)
PointFormats
[]
uint8
`mapstructure:"point_formats"`
}
// GatewaySchedulingConfig accounts scheduling configuration.
...
...
@@ -271,6 +307,9 @@ type GatewaySchedulingConfig struct {
FallbackWaitTimeout
time
.
Duration
`mapstructure:"fallback_wait_timeout"`
FallbackMaxWaiting
int
`mapstructure:"fallback_max_waiting"`
// 兜底层账户选择策略: "last_used"(按最后使用时间排序,默认) 或 "random"(随机)
FallbackSelectionMode
string
`mapstructure:"fallback_selection_mode"`
// 负载计算
LoadBatchEnabled
bool
`mapstructure:"load_batch_enabled"`
...
...
@@ -493,6 +532,20 @@ type DashboardAggregationRetentionConfig struct {
DailyDays
int
`mapstructure:"daily_days"`
}
// UsageCleanupConfig 使用记录清理任务配置
type
UsageCleanupConfig
struct
{
// Enabled: 是否启用清理任务执行器
Enabled
bool
`mapstructure:"enabled"`
// MaxRangeDays: 单次任务允许的最大时间跨度(天)
MaxRangeDays
int
`mapstructure:"max_range_days"`
// BatchSize: 单批删除数量
BatchSize
int
`mapstructure:"batch_size"`
// WorkerIntervalSeconds: 后台任务轮询间隔(秒)
WorkerIntervalSeconds
int
`mapstructure:"worker_interval_seconds"`
// TaskTimeoutSeconds: 单次任务最大执行时长(秒)
TaskTimeoutSeconds
int
`mapstructure:"task_timeout_seconds"`
}
func
NormalizeRunMode
(
value
string
)
string
{
normalized
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
value
))
switch
normalized
{
...
...
@@ -753,12 +806,22 @@ func setDefaults() {
viper
.
SetDefault
(
"dashboard_aggregation.retention.daily_days"
,
730
)
viper
.
SetDefault
(
"dashboard_aggregation.recompute_days"
,
2
)
// Usage cleanup task
viper
.
SetDefault
(
"usage_cleanup.enabled"
,
true
)
viper
.
SetDefault
(
"usage_cleanup.max_range_days"
,
31
)
viper
.
SetDefault
(
"usage_cleanup.batch_size"
,
5000
)
viper
.
SetDefault
(
"usage_cleanup.worker_interval_seconds"
,
10
)
viper
.
SetDefault
(
"usage_cleanup.task_timeout_seconds"
,
1800
)
// Gateway
viper
.
SetDefault
(
"gateway.response_header_timeout"
,
600
)
// 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
viper
.
SetDefault
(
"gateway.log_upstream_error_body"
,
true
)
viper
.
SetDefault
(
"gateway.log_upstream_error_body_max_bytes"
,
2048
)
viper
.
SetDefault
(
"gateway.inject_beta_for_apikey"
,
false
)
viper
.
SetDefault
(
"gateway.failover_on_400"
,
false
)
viper
.
SetDefault
(
"gateway.max_account_switches"
,
10
)
viper
.
SetDefault
(
"gateway.max_account_switches_gemini"
,
3
)
viper
.
SetDefault
(
"gateway.antigravity_fallback_cooldown_minutes"
,
1
)
viper
.
SetDefault
(
"gateway.max_body_size"
,
int64
(
100
*
1024
*
1024
))
viper
.
SetDefault
(
"gateway.connection_pool_isolation"
,
ConnectionPoolIsolationAccountProxy
)
// HTTP 上游连接池配置(针对 5000+ 并发用户优化)
...
...
@@ -771,11 +834,12 @@ func setDefaults() {
viper
.
SetDefault
(
"gateway.concurrency_slot_ttl_minutes"
,
30
)
// 并发槽位过期时间(支持超长请求)
viper
.
SetDefault
(
"gateway.stream_data_interval_timeout"
,
180
)
viper
.
SetDefault
(
"gateway.stream_keepalive_interval"
,
10
)
viper
.
SetDefault
(
"gateway.max_line_size"
,
1
0
*
1024
*
1024
)
viper
.
SetDefault
(
"gateway.max_line_size"
,
4
0
*
1024
*
1024
)
viper
.
SetDefault
(
"gateway.scheduling.sticky_session_max_waiting"
,
3
)
viper
.
SetDefault
(
"gateway.scheduling.sticky_session_wait_timeout"
,
120
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.fallback_wait_timeout"
,
30
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.fallback_max_waiting"
,
100
)
viper
.
SetDefault
(
"gateway.scheduling.fallback_selection_mode"
,
"last_used"
)
viper
.
SetDefault
(
"gateway.scheduling.load_batch_enabled"
,
true
)
viper
.
SetDefault
(
"gateway.scheduling.slot_cleanup_interval"
,
30
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.db_fallback_enabled"
,
true
)
...
...
@@ -787,6 +851,8 @@ func setDefaults() {
viper
.
SetDefault
(
"gateway.scheduling.outbox_lag_rebuild_failures"
,
3
)
viper
.
SetDefault
(
"gateway.scheduling.outbox_backlog_rebuild_rows"
,
10000
)
viper
.
SetDefault
(
"gateway.scheduling.full_rebuild_interval_seconds"
,
300
)
// TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
viper
.
SetDefault
(
"gateway.tls_fingerprint.enabled"
,
true
)
viper
.
SetDefault
(
"concurrency.ping_interval"
,
10
)
// TokenRefresh
...
...
@@ -989,6 +1055,33 @@ func (c *Config) Validate() error {
return
fmt
.
Errorf
(
"dashboard_aggregation.recompute_days must be non-negative"
)
}
}
if
c
.
UsageCleanup
.
Enabled
{
if
c
.
UsageCleanup
.
MaxRangeDays
<=
0
{
return
fmt
.
Errorf
(
"usage_cleanup.max_range_days must be positive"
)
}
if
c
.
UsageCleanup
.
BatchSize
<=
0
{
return
fmt
.
Errorf
(
"usage_cleanup.batch_size must be positive"
)
}
if
c
.
UsageCleanup
.
WorkerIntervalSeconds
<=
0
{
return
fmt
.
Errorf
(
"usage_cleanup.worker_interval_seconds must be positive"
)
}
if
c
.
UsageCleanup
.
TaskTimeoutSeconds
<=
0
{
return
fmt
.
Errorf
(
"usage_cleanup.task_timeout_seconds must be positive"
)
}
}
else
{
if
c
.
UsageCleanup
.
MaxRangeDays
<
0
{
return
fmt
.
Errorf
(
"usage_cleanup.max_range_days must be non-negative"
)
}
if
c
.
UsageCleanup
.
BatchSize
<
0
{
return
fmt
.
Errorf
(
"usage_cleanup.batch_size must be non-negative"
)
}
if
c
.
UsageCleanup
.
WorkerIntervalSeconds
<
0
{
return
fmt
.
Errorf
(
"usage_cleanup.worker_interval_seconds must be non-negative"
)
}
if
c
.
UsageCleanup
.
TaskTimeoutSeconds
<
0
{
return
fmt
.
Errorf
(
"usage_cleanup.task_timeout_seconds must be non-negative"
)
}
}
if
c
.
Gateway
.
MaxBodySize
<=
0
{
return
fmt
.
Errorf
(
"gateway.max_body_size must be positive"
)
}
...
...
backend/internal/config/config_test.go
View file @
c8e2f614
...
...
@@ -280,3 +280,573 @@ func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
t
.
Fatalf
(
"Validate() expected backfill_max_days error, got: %v"
,
err
)
}
}
func
TestLoadDefaultUsageCleanupConfig
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
if
!
cfg
.
UsageCleanup
.
Enabled
{
t
.
Fatalf
(
"UsageCleanup.Enabled = false, want true"
)
}
if
cfg
.
UsageCleanup
.
MaxRangeDays
!=
31
{
t
.
Fatalf
(
"UsageCleanup.MaxRangeDays = %d, want 31"
,
cfg
.
UsageCleanup
.
MaxRangeDays
)
}
if
cfg
.
UsageCleanup
.
BatchSize
!=
5000
{
t
.
Fatalf
(
"UsageCleanup.BatchSize = %d, want 5000"
,
cfg
.
UsageCleanup
.
BatchSize
)
}
if
cfg
.
UsageCleanup
.
WorkerIntervalSeconds
!=
10
{
t
.
Fatalf
(
"UsageCleanup.WorkerIntervalSeconds = %d, want 10"
,
cfg
.
UsageCleanup
.
WorkerIntervalSeconds
)
}
if
cfg
.
UsageCleanup
.
TaskTimeoutSeconds
!=
1800
{
t
.
Fatalf
(
"UsageCleanup.TaskTimeoutSeconds = %d, want 1800"
,
cfg
.
UsageCleanup
.
TaskTimeoutSeconds
)
}
}
func
TestValidateUsageCleanupConfigEnabled
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
UsageCleanup
.
Enabled
=
true
cfg
.
UsageCleanup
.
MaxRangeDays
=
0
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error for usage_cleanup.max_range_days, got nil"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"usage_cleanup.max_range_days"
)
{
t
.
Fatalf
(
"Validate() expected max_range_days error, got: %v"
,
err
)
}
}
func
TestValidateUsageCleanupConfigDisabled
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
UsageCleanup
.
Enabled
=
false
cfg
.
UsageCleanup
.
BatchSize
=
-
1
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error for usage_cleanup.batch_size, got nil"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"usage_cleanup.batch_size"
)
{
t
.
Fatalf
(
"Validate() expected batch_size error, got: %v"
,
err
)
}
}
func
TestConfigAddressHelpers
(
t
*
testing
.
T
)
{
server
:=
ServerConfig
{
Host
:
"127.0.0.1"
,
Port
:
9000
}
if
server
.
Address
()
!=
"127.0.0.1:9000"
{
t
.
Fatalf
(
"ServerConfig.Address() = %q"
,
server
.
Address
())
}
dbCfg
:=
DatabaseConfig
{
Host
:
"localhost"
,
Port
:
5432
,
User
:
"postgres"
,
Password
:
""
,
DBName
:
"sub2api"
,
SSLMode
:
"disable"
,
}
if
!
strings
.
Contains
(
dbCfg
.
DSN
(),
"password="
)
{
}
else
{
t
.
Fatalf
(
"DatabaseConfig.DSN() should not include password when empty"
)
}
dbCfg
.
Password
=
"secret"
if
!
strings
.
Contains
(
dbCfg
.
DSN
(),
"password=secret"
)
{
t
.
Fatalf
(
"DatabaseConfig.DSN() missing password"
)
}
dbCfg
.
Password
=
""
if
strings
.
Contains
(
dbCfg
.
DSNWithTimezone
(
"UTC"
),
"password="
)
{
t
.
Fatalf
(
"DatabaseConfig.DSNWithTimezone() should omit password when empty"
)
}
if
!
strings
.
Contains
(
dbCfg
.
DSNWithTimezone
(
""
),
"TimeZone=Asia/Shanghai"
)
{
t
.
Fatalf
(
"DatabaseConfig.DSNWithTimezone() should use default timezone"
)
}
if
!
strings
.
Contains
(
dbCfg
.
DSNWithTimezone
(
"UTC"
),
"TimeZone=UTC"
)
{
t
.
Fatalf
(
"DatabaseConfig.DSNWithTimezone() should use provided timezone"
)
}
redis
:=
RedisConfig
{
Host
:
"redis"
,
Port
:
6379
}
if
redis
.
Address
()
!=
"redis:6379"
{
t
.
Fatalf
(
"RedisConfig.Address() = %q"
,
redis
.
Address
())
}
}
func
TestNormalizeStringSlice
(
t
*
testing
.
T
)
{
values
:=
normalizeStringSlice
([]
string
{
" a "
,
""
,
"b"
,
" "
,
"c"
})
if
len
(
values
)
!=
3
||
values
[
0
]
!=
"a"
||
values
[
1
]
!=
"b"
||
values
[
2
]
!=
"c"
{
t
.
Fatalf
(
"normalizeStringSlice() unexpected result: %#v"
,
values
)
}
if
normalizeStringSlice
(
nil
)
!=
nil
{
t
.
Fatalf
(
"normalizeStringSlice(nil) expected nil slice"
)
}
}
func
TestGetServerAddressFromEnv
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"SERVER_HOST"
,
"127.0.0.1"
)
t
.
Setenv
(
"SERVER_PORT"
,
"9090"
)
address
:=
GetServerAddress
()
if
address
!=
"127.0.0.1:9090"
{
t
.
Fatalf
(
"GetServerAddress() = %q"
,
address
)
}
}
func
TestValidateAbsoluteHTTPURL
(
t
*
testing
.
T
)
{
if
err
:=
ValidateAbsoluteHTTPURL
(
"https://example.com/path"
);
err
!=
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL valid url error: %v"
,
err
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
""
);
err
==
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL should reject empty url"
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
"/relative"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL should reject relative url"
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
"ftp://example.com"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL should reject ftp scheme"
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
"https://example.com/#frag"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL should reject fragment"
)
}
}
func
TestValidateFrontendRedirectURL
(
t
*
testing
.
T
)
{
if
err
:=
ValidateFrontendRedirectURL
(
"/auth/callback"
);
err
!=
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL relative error: %v"
,
err
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"https://example.com/auth"
);
err
!=
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL absolute error: %v"
,
err
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"example.com/path"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject non-absolute url"
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"//evil.com"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject // prefix"
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"javascript:alert(1)"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject javascript scheme"
)
}
}
func
TestWarnIfInsecureURL
(
t
*
testing
.
T
)
{
warnIfInsecureURL
(
"test"
,
"http://example.com"
)
warnIfInsecureURL
(
"test"
,
"bad://url"
)
}
func
TestGenerateJWTSecretDefaultLength
(
t
*
testing
.
T
)
{
secret
,
err
:=
generateJWTSecret
(
0
)
if
err
!=
nil
{
t
.
Fatalf
(
"generateJWTSecret error: %v"
,
err
)
}
if
len
(
secret
)
==
0
{
t
.
Fatalf
(
"generateJWTSecret returned empty string"
)
}
}
func
TestValidateOpsCleanupScheduleRequired
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
Ops
.
Cleanup
.
Enabled
=
true
cfg
.
Ops
.
Cleanup
.
Schedule
=
""
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error for ops.cleanup.schedule"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"ops.cleanup.schedule"
)
{
t
.
Fatalf
(
"Validate() expected ops.cleanup.schedule error, got: %v"
,
err
)
}
}
func
TestValidateConcurrencyPingInterval
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
Concurrency
.
PingInterval
=
3
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error for concurrency.ping_interval"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"concurrency.ping_interval"
)
{
t
.
Fatalf
(
"Validate() expected concurrency.ping_interval error, got: %v"
,
err
)
}
}
func
TestProvideConfig
(
t
*
testing
.
T
)
{
viper
.
Reset
()
if
_
,
err
:=
ProvideConfig
();
err
!=
nil
{
t
.
Fatalf
(
"ProvideConfig() error: %v"
,
err
)
}
}
func
TestValidateConfigWithLinuxDoEnabled
(
t
*
testing
.
T
)
{
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
Security
.
CSP
.
Enabled
=
true
cfg
.
Security
.
CSP
.
Policy
=
"default-src 'self'"
cfg
.
LinuxDo
.
Enabled
=
true
cfg
.
LinuxDo
.
ClientID
=
"client"
cfg
.
LinuxDo
.
ClientSecret
=
"secret"
cfg
.
LinuxDo
.
AuthorizeURL
=
"https://example.com/oauth2/authorize"
cfg
.
LinuxDo
.
TokenURL
=
"https://example.com/oauth2/token"
cfg
.
LinuxDo
.
UserInfoURL
=
"https://example.com/oauth2/userinfo"
cfg
.
LinuxDo
.
RedirectURL
=
"https://example.com/api/v1/auth/oauth/linuxdo/callback"
cfg
.
LinuxDo
.
FrontendRedirectURL
=
"/auth/linuxdo/callback"
cfg
.
LinuxDo
.
TokenAuthMethod
=
"client_secret_post"
if
err
:=
cfg
.
Validate
();
err
!=
nil
{
t
.
Fatalf
(
"Validate() unexpected error: %v"
,
err
)
}
}
func
TestValidateJWTSecretStrength
(
t
*
testing
.
T
)
{
if
!
isWeakJWTSecret
(
"change-me-in-production"
)
{
t
.
Fatalf
(
"isWeakJWTSecret should detect weak secret"
)
}
if
isWeakJWTSecret
(
"StrongSecretValue"
)
{
t
.
Fatalf
(
"isWeakJWTSecret should accept strong secret"
)
}
}
func
TestGenerateJWTSecretWithLength
(
t
*
testing
.
T
)
{
secret
,
err
:=
generateJWTSecret
(
16
)
if
err
!=
nil
{
t
.
Fatalf
(
"generateJWTSecret error: %v"
,
err
)
}
if
len
(
secret
)
==
0
{
t
.
Fatalf
(
"generateJWTSecret returned empty string"
)
}
}
func
TestValidateAbsoluteHTTPURLMissingHost
(
t
*
testing
.
T
)
{
if
err
:=
ValidateAbsoluteHTTPURL
(
"https://"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateAbsoluteHTTPURL should reject missing host"
)
}
}
func
TestValidateFrontendRedirectURLInvalidChars
(
t
*
testing
.
T
)
{
if
err
:=
ValidateFrontendRedirectURL
(
"/auth/
\n
callback"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject invalid chars"
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"http://"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject missing host"
)
}
if
err
:=
ValidateFrontendRedirectURL
(
"mailto:user@example.com"
);
err
==
nil
{
t
.
Fatalf
(
"ValidateFrontendRedirectURL should reject mailto"
)
}
}
func
TestWarnIfInsecureURLHTTPS
(
t
*
testing
.
T
)
{
warnIfInsecureURL
(
"secure"
,
"https://example.com"
)
}
func
TestValidateConfigErrors
(
t
*
testing
.
T
)
{
buildValid
:=
func
(
t
*
testing
.
T
)
*
Config
{
t
.
Helper
()
viper
.
Reset
()
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
return
cfg
}
cases
:=
[]
struct
{
name
string
mutate
func
(
*
Config
)
wantErr
string
}{
{
name
:
"jwt expire hour positive"
,
mutate
:
func
(
c
*
Config
)
{
c
.
JWT
.
ExpireHour
=
0
},
wantErr
:
"jwt.expire_hour must be positive"
,
},
{
name
:
"jwt expire hour max"
,
mutate
:
func
(
c
*
Config
)
{
c
.
JWT
.
ExpireHour
=
200
},
wantErr
:
"jwt.expire_hour must be <= 168"
,
},
{
name
:
"csp policy required"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Security
.
CSP
.
Enabled
=
true
;
c
.
Security
.
CSP
.
Policy
=
""
},
wantErr
:
"security.csp.policy"
,
},
{
name
:
"linuxdo client id required"
,
mutate
:
func
(
c
*
Config
)
{
c
.
LinuxDo
.
Enabled
=
true
c
.
LinuxDo
.
ClientID
=
""
},
wantErr
:
"linuxdo_connect.client_id"
,
},
{
name
:
"linuxdo token auth method"
,
mutate
:
func
(
c
*
Config
)
{
c
.
LinuxDo
.
Enabled
=
true
c
.
LinuxDo
.
ClientID
=
"client"
c
.
LinuxDo
.
ClientSecret
=
"secret"
c
.
LinuxDo
.
AuthorizeURL
=
"https://example.com/authorize"
c
.
LinuxDo
.
TokenURL
=
"https://example.com/token"
c
.
LinuxDo
.
UserInfoURL
=
"https://example.com/userinfo"
c
.
LinuxDo
.
RedirectURL
=
"https://example.com/callback"
c
.
LinuxDo
.
FrontendRedirectURL
=
"/auth/callback"
c
.
LinuxDo
.
TokenAuthMethod
=
"invalid"
},
wantErr
:
"linuxdo_connect.token_auth_method"
,
},
{
name
:
"billing circuit breaker threshold"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Billing
.
CircuitBreaker
.
FailureThreshold
=
0
},
wantErr
:
"billing.circuit_breaker.failure_threshold"
,
},
{
name
:
"billing circuit breaker reset"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Billing
.
CircuitBreaker
.
ResetTimeoutSeconds
=
0
},
wantErr
:
"billing.circuit_breaker.reset_timeout_seconds"
,
},
{
name
:
"billing circuit breaker half open"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Billing
.
CircuitBreaker
.
HalfOpenRequests
=
0
},
wantErr
:
"billing.circuit_breaker.half_open_requests"
,
},
{
name
:
"database max open conns"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Database
.
MaxOpenConns
=
0
},
wantErr
:
"database.max_open_conns"
,
},
{
name
:
"database max lifetime"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Database
.
ConnMaxLifetimeMinutes
=
-
1
},
wantErr
:
"database.conn_max_lifetime_minutes"
,
},
{
name
:
"database idle exceeds open"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Database
.
MaxIdleConns
=
c
.
Database
.
MaxOpenConns
+
1
},
wantErr
:
"database.max_idle_conns cannot exceed"
,
},
{
name
:
"redis dial timeout"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Redis
.
DialTimeoutSeconds
=
0
},
wantErr
:
"redis.dial_timeout_seconds"
,
},
{
name
:
"redis read timeout"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Redis
.
ReadTimeoutSeconds
=
0
},
wantErr
:
"redis.read_timeout_seconds"
,
},
{
name
:
"redis write timeout"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Redis
.
WriteTimeoutSeconds
=
0
},
wantErr
:
"redis.write_timeout_seconds"
,
},
{
name
:
"redis pool size"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Redis
.
PoolSize
=
0
},
wantErr
:
"redis.pool_size"
,
},
{
name
:
"redis idle exceeds pool"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Redis
.
MinIdleConns
=
c
.
Redis
.
PoolSize
+
1
},
wantErr
:
"redis.min_idle_conns cannot exceed"
,
},
{
name
:
"dashboard cache disabled negative"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Dashboard
.
Enabled
=
false
;
c
.
Dashboard
.
StatsTTLSeconds
=
-
1
},
wantErr
:
"dashboard_cache.stats_ttl_seconds"
,
},
{
name
:
"dashboard cache fresh ttl positive"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Dashboard
.
Enabled
=
true
;
c
.
Dashboard
.
StatsFreshTTLSeconds
=
0
},
wantErr
:
"dashboard_cache.stats_fresh_ttl_seconds"
,
},
{
name
:
"dashboard aggregation enabled interval"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
;
c
.
DashboardAgg
.
IntervalSeconds
=
0
},
wantErr
:
"dashboard_aggregation.interval_seconds"
,
},
{
name
:
"dashboard aggregation backfill positive"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
c
.
DashboardAgg
.
BackfillEnabled
=
true
c
.
DashboardAgg
.
BackfillMaxDays
=
0
},
wantErr
:
"dashboard_aggregation.backfill_max_days"
,
},
{
name
:
"dashboard aggregation retention"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
true
;
c
.
DashboardAgg
.
Retention
.
UsageLogsDays
=
0
},
wantErr
:
"dashboard_aggregation.retention.usage_logs_days"
,
},
{
name
:
"dashboard aggregation disabled interval"
,
mutate
:
func
(
c
*
Config
)
{
c
.
DashboardAgg
.
Enabled
=
false
;
c
.
DashboardAgg
.
IntervalSeconds
=
-
1
},
wantErr
:
"dashboard_aggregation.interval_seconds"
,
},
{
name
:
"usage cleanup max range"
,
mutate
:
func
(
c
*
Config
)
{
c
.
UsageCleanup
.
Enabled
=
true
;
c
.
UsageCleanup
.
MaxRangeDays
=
0
},
wantErr
:
"usage_cleanup.max_range_days"
,
},
{
name
:
"usage cleanup worker interval"
,
mutate
:
func
(
c
*
Config
)
{
c
.
UsageCleanup
.
Enabled
=
true
;
c
.
UsageCleanup
.
WorkerIntervalSeconds
=
0
},
wantErr
:
"usage_cleanup.worker_interval_seconds"
,
},
{
name
:
"usage cleanup batch size"
,
mutate
:
func
(
c
*
Config
)
{
c
.
UsageCleanup
.
Enabled
=
true
;
c
.
UsageCleanup
.
BatchSize
=
0
},
wantErr
:
"usage_cleanup.batch_size"
,
},
{
name
:
"usage cleanup disabled negative"
,
mutate
:
func
(
c
*
Config
)
{
c
.
UsageCleanup
.
Enabled
=
false
;
c
.
UsageCleanup
.
BatchSize
=
-
1
},
wantErr
:
"usage_cleanup.batch_size"
,
},
{
name
:
"gateway max body size"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxBodySize
=
0
},
wantErr
:
"gateway.max_body_size"
,
},
{
name
:
"gateway max idle conns"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxIdleConns
=
0
},
wantErr
:
"gateway.max_idle_conns"
,
},
{
name
:
"gateway max idle conns per host"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxIdleConnsPerHost
=
0
},
wantErr
:
"gateway.max_idle_conns_per_host"
,
},
{
name
:
"gateway idle timeout"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
IdleConnTimeoutSeconds
=
0
},
wantErr
:
"gateway.idle_conn_timeout_seconds"
,
},
{
name
:
"gateway max upstream clients"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxUpstreamClients
=
0
},
wantErr
:
"gateway.max_upstream_clients"
,
},
{
name
:
"gateway client idle ttl"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
ClientIdleTTLSeconds
=
0
},
wantErr
:
"gateway.client_idle_ttl_seconds"
,
},
{
name
:
"gateway concurrency slot ttl"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
ConcurrencySlotTTLMinutes
=
0
},
wantErr
:
"gateway.concurrency_slot_ttl_minutes"
,
},
{
name
:
"gateway max conns per host"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxConnsPerHost
=
-
1
},
wantErr
:
"gateway.max_conns_per_host"
,
},
{
name
:
"gateway connection isolation"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
ConnectionPoolIsolation
=
"invalid"
},
wantErr
:
"gateway.connection_pool_isolation"
,
},
{
name
:
"gateway stream keepalive range"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
StreamKeepaliveInterval
=
4
},
wantErr
:
"gateway.stream_keepalive_interval"
,
},
{
name
:
"gateway stream data interval range"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
StreamDataIntervalTimeout
=
5
},
wantErr
:
"gateway.stream_data_interval_timeout"
,
},
{
name
:
"gateway stream data interval negative"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
StreamDataIntervalTimeout
=
-
1
},
wantErr
:
"gateway.stream_data_interval_timeout must be non-negative"
,
},
{
name
:
"gateway max line size"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxLineSize
=
1024
},
wantErr
:
"gateway.max_line_size must be at least"
,
},
{
name
:
"gateway max line size negative"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
MaxLineSize
=
-
1
},
wantErr
:
"gateway.max_line_size must be non-negative"
,
},
{
name
:
"gateway scheduling sticky waiting"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
Scheduling
.
StickySessionMaxWaiting
=
0
},
wantErr
:
"gateway.scheduling.sticky_session_max_waiting"
,
},
{
name
:
"gateway scheduling outbox poll"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
Scheduling
.
OutboxPollIntervalSeconds
=
0
},
wantErr
:
"gateway.scheduling.outbox_poll_interval_seconds"
,
},
{
name
:
"gateway scheduling outbox failures"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
Scheduling
.
OutboxLagRebuildFailures
=
0
},
wantErr
:
"gateway.scheduling.outbox_lag_rebuild_failures"
,
},
{
name
:
"gateway outbox lag rebuild"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Gateway
.
Scheduling
.
OutboxLagWarnSeconds
=
10
c
.
Gateway
.
Scheduling
.
OutboxLagRebuildSeconds
=
5
},
wantErr
:
"gateway.scheduling.outbox_lag_rebuild_seconds"
,
},
{
name
:
"ops metrics collector ttl"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Ops
.
MetricsCollectorCache
.
TTL
=
-
1
},
wantErr
:
"ops.metrics_collector_cache.ttl"
,
},
{
name
:
"ops cleanup retention"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Ops
.
Cleanup
.
ErrorLogRetentionDays
=
-
1
},
wantErr
:
"ops.cleanup.error_log_retention_days"
,
},
{
name
:
"ops cleanup minute retention"
,
mutate
:
func
(
c
*
Config
)
{
c
.
Ops
.
Cleanup
.
MinuteMetricsRetentionDays
=
-
1
},
wantErr
:
"ops.cleanup.minute_metrics_retention_days"
,
},
}
for
_
,
tt
:=
range
cases
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
cfg
:=
buildValid
(
t
)
tt
.
mutate
(
cfg
)
err
:=
cfg
.
Validate
()
if
err
==
nil
||
!
strings
.
Contains
(
err
.
Error
(),
tt
.
wantErr
)
{
t
.
Fatalf
(
"Validate() error = %v, want %q"
,
err
,
tt
.
wantErr
)
}
})
}
}
backend/internal/handler/admin/account_handler.go
View file @
c8e2f614
...
...
@@ -45,6 +45,7 @@ type AccountHandler struct {
concurrencyService
*
service
.
ConcurrencyService
crsSyncService
*
service
.
CRSSyncService
sessionLimitCache
service
.
SessionLimitCache
tokenCacheInvalidator
service
.
TokenCacheInvalidator
}
// NewAccountHandler creates a new admin account handler
...
...
@@ -60,6 +61,7 @@ func NewAccountHandler(
concurrencyService
*
service
.
ConcurrencyService
,
crsSyncService
*
service
.
CRSSyncService
,
sessionLimitCache
service
.
SessionLimitCache
,
tokenCacheInvalidator
service
.
TokenCacheInvalidator
,
)
*
AccountHandler
{
return
&
AccountHandler
{
adminService
:
adminService
,
...
...
@@ -73,6 +75,7 @@ func NewAccountHandler(
concurrencyService
:
concurrencyService
,
crsSyncService
:
crsSyncService
,
sessionLimitCache
:
sessionLimitCache
,
tokenCacheInvalidator
:
tokenCacheInvalidator
,
}
}
...
...
@@ -173,6 +176,7 @@ func (h *AccountHandler) List(c *gin.Context) {
// 识别需要查询窗口费用和会话数的账号(Anthropic OAuth/SetupToken 且启用了相应功能)
windowCostAccountIDs
:=
make
([]
int64
,
0
)
sessionLimitAccountIDs
:=
make
([]
int64
,
0
)
sessionIdleTimeouts
:=
make
(
map
[
int64
]
time
.
Duration
)
// 各账号的会话空闲超时配置
for
i
:=
range
accounts
{
acc
:=
&
accounts
[
i
]
if
acc
.
IsAnthropicOAuthOrSetupToken
()
{
...
...
@@ -181,6 +185,7 @@ func (h *AccountHandler) List(c *gin.Context) {
}
if
acc
.
GetMaxSessions
()
>
0
{
sessionLimitAccountIDs
=
append
(
sessionLimitAccountIDs
,
acc
.
ID
)
sessionIdleTimeouts
[
acc
.
ID
]
=
time
.
Duration
(
acc
.
GetSessionIdleTimeoutMinutes
())
*
time
.
Minute
}
}
}
...
...
@@ -189,9 +194,9 @@ func (h *AccountHandler) List(c *gin.Context) {
var
windowCosts
map
[
int64
]
float64
var
activeSessions
map
[
int64
]
int
// 获取活跃会话数(批量查询)
// 获取活跃会话数(批量查询
,传入各账号的 idleTimeout 配置
)
if
len
(
sessionLimitAccountIDs
)
>
0
&&
h
.
sessionLimitCache
!=
nil
{
activeSessions
,
_
=
h
.
sessionLimitCache
.
GetActiveSessionCountBatch
(
c
.
Request
.
Context
(),
sessionLimitAccountIDs
)
activeSessions
,
_
=
h
.
sessionLimitCache
.
GetActiveSessionCountBatch
(
c
.
Request
.
Context
(),
sessionLimitAccountIDs
,
sessionIdleTimeouts
)
if
activeSessions
==
nil
{
activeSessions
=
make
(
map
[
int64
]
int
)
}
...
...
@@ -211,12 +216,8 @@ func (h *AccountHandler) List(c *gin.Context) {
}
accCopy
:=
acc
// 闭包捕获
g
.
Go
(
func
()
error
{
var
startTime
time
.
Time
if
accCopy
.
SessionWindowStart
!=
nil
{
startTime
=
*
accCopy
.
SessionWindowStart
}
else
{
startTime
=
time
.
Now
()
.
Add
(
-
5
*
time
.
Hour
)
}
// 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况)
startTime
:=
accCopy
.
GetCurrentWindowStartTime
()
stats
,
err
:=
h
.
accountUsageService
.
GetAccountWindowStats
(
gctx
,
accCopy
.
ID
,
startTime
)
if
err
==
nil
&&
stats
!=
nil
{
mu
.
Lock
()
...
...
@@ -545,6 +546,36 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
newCredentials
[
k
]
=
v
}
}
// 如果 project_id 获取失败,先更新凭证,再标记账户为 error
if
tokenInfo
.
ProjectIDMissing
{
// 先更新凭证
_
,
updateErr
:=
h
.
adminService
.
UpdateAccount
(
c
.
Request
.
Context
(),
accountID
,
&
service
.
UpdateAccountInput
{
Credentials
:
newCredentials
,
})
if
updateErr
!=
nil
{
response
.
InternalError
(
c
,
"Failed to update credentials: "
+
updateErr
.
Error
())
return
}
// 标记账户为 error
if
setErr
:=
h
.
adminService
.
SetAccountError
(
c
.
Request
.
Context
(),
accountID
,
"missing_project_id: 账户缺少project id,可能无法使用Antigravity"
);
setErr
!=
nil
{
response
.
InternalError
(
c
,
"Failed to set account error: "
+
setErr
.
Error
())
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Token refreshed but project_id is missing, account marked as error"
,
"warning"
:
"missing_project_id"
,
})
return
}
// 成功获取到 project_id,如果之前是 missing_project_id 错误则清除
if
account
.
Status
==
service
.
StatusError
&&
strings
.
Contains
(
account
.
ErrorMessage
,
"missing_project_id:"
)
{
if
_
,
clearErr
:=
h
.
adminService
.
ClearAccountError
(
c
.
Request
.
Context
(),
accountID
);
clearErr
!=
nil
{
response
.
InternalError
(
c
,
"Failed to clear account error: "
+
clearErr
.
Error
())
return
}
}
}
else
{
// Use Anthropic/Claude OAuth service to refresh token
tokenInfo
,
err
:=
h
.
oauthService
.
RefreshAccountToken
(
c
.
Request
.
Context
(),
account
)
...
...
@@ -580,6 +611,14 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
return
}
// 刷新成功后,清除 token 缓存,确保下次请求使用新 token
if
h
.
tokenCacheInvalidator
!=
nil
{
if
invalidateErr
:=
h
.
tokenCacheInvalidator
.
InvalidateToken
(
c
.
Request
.
Context
(),
updatedAccount
);
invalidateErr
!=
nil
{
// 缓存失效失败只记录日志,不影响主流程
_
=
c
.
Error
(
invalidateErr
)
}
}
response
.
Success
(
c
,
dto
.
AccountFromService
(
updatedAccount
))
}
...
...
backend/internal/handler/admin/admin_basic_handlers_test.go
0 → 100644
View file @
c8e2f614
package
admin
import
(
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
setupAdminRouter
()
(
*
gin
.
Engine
,
*
stubAdminService
)
{
gin
.
SetMode
(
gin
.
TestMode
)
router
:=
gin
.
New
()
adminSvc
:=
newStubAdminService
()
userHandler
:=
NewUserHandler
(
adminSvc
)
groupHandler
:=
NewGroupHandler
(
adminSvc
)
proxyHandler
:=
NewProxyHandler
(
adminSvc
)
redeemHandler
:=
NewRedeemHandler
(
adminSvc
)
router
.
GET
(
"/api/v1/admin/users"
,
userHandler
.
List
)
router
.
GET
(
"/api/v1/admin/users/:id"
,
userHandler
.
GetByID
)
router
.
POST
(
"/api/v1/admin/users"
,
userHandler
.
Create
)
router
.
PUT
(
"/api/v1/admin/users/:id"
,
userHandler
.
Update
)
router
.
DELETE
(
"/api/v1/admin/users/:id"
,
userHandler
.
Delete
)
router
.
POST
(
"/api/v1/admin/users/:id/balance"
,
userHandler
.
UpdateBalance
)
router
.
GET
(
"/api/v1/admin/users/:id/api-keys"
,
userHandler
.
GetUserAPIKeys
)
router
.
GET
(
"/api/v1/admin/users/:id/usage"
,
userHandler
.
GetUserUsage
)
router
.
GET
(
"/api/v1/admin/groups"
,
groupHandler
.
List
)
router
.
GET
(
"/api/v1/admin/groups/all"
,
groupHandler
.
GetAll
)
router
.
GET
(
"/api/v1/admin/groups/:id"
,
groupHandler
.
GetByID
)
router
.
POST
(
"/api/v1/admin/groups"
,
groupHandler
.
Create
)
router
.
PUT
(
"/api/v1/admin/groups/:id"
,
groupHandler
.
Update
)
router
.
DELETE
(
"/api/v1/admin/groups/:id"
,
groupHandler
.
Delete
)
router
.
GET
(
"/api/v1/admin/groups/:id/stats"
,
groupHandler
.
GetStats
)
router
.
GET
(
"/api/v1/admin/groups/:id/api-keys"
,
groupHandler
.
GetGroupAPIKeys
)
router
.
GET
(
"/api/v1/admin/proxies"
,
proxyHandler
.
List
)
router
.
GET
(
"/api/v1/admin/proxies/all"
,
proxyHandler
.
GetAll
)
router
.
GET
(
"/api/v1/admin/proxies/:id"
,
proxyHandler
.
GetByID
)
router
.
POST
(
"/api/v1/admin/proxies"
,
proxyHandler
.
Create
)
router
.
PUT
(
"/api/v1/admin/proxies/:id"
,
proxyHandler
.
Update
)
router
.
DELETE
(
"/api/v1/admin/proxies/:id"
,
proxyHandler
.
Delete
)
router
.
POST
(
"/api/v1/admin/proxies/batch-delete"
,
proxyHandler
.
BatchDelete
)
router
.
POST
(
"/api/v1/admin/proxies/:id/test"
,
proxyHandler
.
Test
)
router
.
GET
(
"/api/v1/admin/proxies/:id/stats"
,
proxyHandler
.
GetStats
)
router
.
GET
(
"/api/v1/admin/proxies/:id/accounts"
,
proxyHandler
.
GetProxyAccounts
)
router
.
GET
(
"/api/v1/admin/redeem-codes"
,
redeemHandler
.
List
)
router
.
GET
(
"/api/v1/admin/redeem-codes/:id"
,
redeemHandler
.
GetByID
)
router
.
POST
(
"/api/v1/admin/redeem-codes"
,
redeemHandler
.
Generate
)
router
.
DELETE
(
"/api/v1/admin/redeem-codes/:id"
,
redeemHandler
.
Delete
)
router
.
POST
(
"/api/v1/admin/redeem-codes/batch-delete"
,
redeemHandler
.
BatchDelete
)
router
.
POST
(
"/api/v1/admin/redeem-codes/:id/expire"
,
redeemHandler
.
Expire
)
router
.
GET
(
"/api/v1/admin/redeem-codes/:id/stats"
,
redeemHandler
.
GetStats
)
return
router
,
adminSvc
}
func
TestUserHandlerEndpoints
(
t
*
testing
.
T
)
{
router
,
_
:=
setupAdminRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users?page=1&page_size=20"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/1"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
createBody
:=
map
[
string
]
any
{
"email"
:
"new@example.com"
,
"password"
:
"pass123"
,
"balance"
:
1
,
"concurrency"
:
2
}
body
,
_
:=
json
.
Marshal
(
createBody
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/users"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
updateBody
:=
map
[
string
]
any
{
"email"
:
"updated@example.com"
}
body
,
_
=
json
.
Marshal
(
updateBody
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPut
,
"/api/v1/admin/users/1"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodDelete
,
"/api/v1/admin/users/1"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/users/1/balance"
,
bytes
.
NewBufferString
(
`{"balance":1,"operation":"add"}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/1/api-keys"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/users/1/usage?period=today"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
}
func
TestGroupHandlerEndpoints
(
t
*
testing
.
T
)
{
router
,
_
:=
setupAdminRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/groups"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/groups/all"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/groups/2"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
body
,
_
:=
json
.
Marshal
(
map
[
string
]
any
{
"name"
:
"new"
,
"platform"
:
"anthropic"
,
"subscription_type"
:
"standard"
})
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/groups"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
body
,
_
=
json
.
Marshal
(
map
[
string
]
any
{
"name"
:
"update"
})
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPut
,
"/api/v1/admin/groups/2"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodDelete
,
"/api/v1/admin/groups/2"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/groups/2/stats"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/groups/2/api-keys"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
}
func
TestProxyHandlerEndpoints
(
t
*
testing
.
T
)
{
router
,
_
:=
setupAdminRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/all"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/4"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
body
,
_
:=
json
.
Marshal
(
map
[
string
]
any
{
"name"
:
"proxy"
,
"protocol"
:
"http"
,
"host"
:
"localhost"
,
"port"
:
8080
})
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/proxies"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
body
,
_
=
json
.
Marshal
(
map
[
string
]
any
{
"name"
:
"proxy2"
})
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPut
,
"/api/v1/admin/proxies/4"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodDelete
,
"/api/v1/admin/proxies/4"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/proxies/batch-delete"
,
bytes
.
NewBufferString
(
`{"ids":[1,2]}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/proxies/4/test"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/4/stats"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/4/accounts"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
}
func
TestRedeemHandlerEndpoints
(
t
*
testing
.
T
)
{
router
,
_
:=
setupAdminRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/redeem-codes"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/redeem-codes/5"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
body
,
_
:=
json
.
Marshal
(
map
[
string
]
any
{
"count"
:
1
,
"type"
:
"balance"
,
"value"
:
10
})
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/redeem-codes"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodDelete
,
"/api/v1/admin/redeem-codes/5"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/redeem-codes/batch-delete"
,
bytes
.
NewBufferString
(
`{"ids":[1,2]}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/redeem-codes/5/expire"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
rec
=
httptest
.
NewRecorder
()
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/redeem-codes/5/stats"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
}
backend/internal/handler/admin/admin_helpers_test.go
0 → 100644
View file @
c8e2f614
package
admin
import
(
"encoding/json"
"net/http"
"net/http/httptest"
"net/netip"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
TestParseTimeRange
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
w
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
w
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?start_date=2024-01-01&end_date=2024-01-02&timezone=UTC"
,
nil
)
c
.
Request
=
req
start
,
end
:=
parseTimeRange
(
c
)
require
.
Equal
(
t
,
time
.
Date
(
2024
,
1
,
1
,
0
,
0
,
0
,
0
,
time
.
UTC
),
start
)
require
.
Equal
(
t
,
time
.
Date
(
2024
,
1
,
3
,
0
,
0
,
0
,
0
,
time
.
UTC
),
end
)
req
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?start_date=bad&timezone=UTC"
,
nil
)
c
.
Request
=
req
start
,
end
=
parseTimeRange
(
c
)
require
.
False
(
t
,
start
.
IsZero
())
require
.
False
(
t
,
end
.
IsZero
())
}
func
TestParseOpsViewParam
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
w
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
w
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?view=excluded"
,
nil
)
require
.
Equal
(
t
,
opsListViewExcluded
,
parseOpsViewParam
(
c
))
c2
,
_
:=
gin
.
CreateTestContext
(
w
)
c2
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?view=all"
,
nil
)
require
.
Equal
(
t
,
opsListViewAll
,
parseOpsViewParam
(
c2
))
c3
,
_
:=
gin
.
CreateTestContext
(
w
)
c3
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?view=unknown"
,
nil
)
require
.
Equal
(
t
,
opsListViewErrors
,
parseOpsViewParam
(
c3
))
require
.
Equal
(
t
,
""
,
parseOpsViewParam
(
nil
))
}
func
TestParseOpsDuration
(
t
*
testing
.
T
)
{
dur
,
ok
:=
parseOpsDuration
(
"1h"
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
time
.
Hour
,
dur
)
_
,
ok
=
parseOpsDuration
(
"invalid"
)
require
.
False
(
t
,
ok
)
}
func
TestParseOpsTimeRange
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
w
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
w
)
now
:=
time
.
Now
()
.
UTC
()
startStr
:=
now
.
Add
(
-
time
.
Hour
)
.
Format
(
time
.
RFC3339
)
endStr
:=
now
.
Format
(
time
.
RFC3339
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?start_time="
+
startStr
+
"&end_time="
+
endStr
,
nil
)
start
,
end
,
err
:=
parseOpsTimeRange
(
c
,
"1h"
)
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
start
.
Before
(
end
))
c2
,
_
:=
gin
.
CreateTestContext
(
w
)
c2
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?start_time=bad"
,
nil
)
_
,
_
,
err
=
parseOpsTimeRange
(
c2
,
"1h"
)
require
.
Error
(
t
,
err
)
}
func
TestParseOpsRealtimeWindow
(
t
*
testing
.
T
)
{
dur
,
label
,
ok
:=
parseOpsRealtimeWindow
(
"5m"
)
require
.
True
(
t
,
ok
)
require
.
Equal
(
t
,
5
*
time
.
Minute
,
dur
)
require
.
Equal
(
t
,
"5min"
,
label
)
_
,
_
,
ok
=
parseOpsRealtimeWindow
(
"invalid"
)
require
.
False
(
t
,
ok
)
}
func
TestPickThroughputBucketSeconds
(
t
*
testing
.
T
)
{
require
.
Equal
(
t
,
60
,
pickThroughputBucketSeconds
(
30
*
time
.
Minute
))
require
.
Equal
(
t
,
300
,
pickThroughputBucketSeconds
(
6
*
time
.
Hour
))
require
.
Equal
(
t
,
3600
,
pickThroughputBucketSeconds
(
48
*
time
.
Hour
))
}
func
TestParseOpsQueryMode
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
w
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
w
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/?mode=raw"
,
nil
)
require
.
Equal
(
t
,
service
.
ParseOpsQueryMode
(
"raw"
),
parseOpsQueryMode
(
c
))
require
.
Equal
(
t
,
service
.
OpsQueryMode
(
""
),
parseOpsQueryMode
(
nil
))
}
func
TestOpsAlertRuleValidation
(
t
*
testing
.
T
)
{
raw
:=
map
[
string
]
json
.
RawMessage
{
"name"
:
json
.
RawMessage
(
`"High error rate"`
),
"metric_type"
:
json
.
RawMessage
(
`"error_rate"`
),
"operator"
:
json
.
RawMessage
(
`">"`
),
"threshold"
:
json
.
RawMessage
(
`90`
),
}
validated
,
err
:=
validateOpsAlertRulePayload
(
raw
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
"High error rate"
,
validated
.
Name
)
_
,
err
=
validateOpsAlertRulePayload
(
map
[
string
]
json
.
RawMessage
{})
require
.
Error
(
t
,
err
)
require
.
True
(
t
,
isPercentOrRateMetric
(
"error_rate"
))
require
.
False
(
t
,
isPercentOrRateMetric
(
"concurrency_queue_depth"
))
}
func
TestOpsWSHelpers
(
t
*
testing
.
T
)
{
prefixes
,
invalid
:=
parseTrustedProxyList
(
"10.0.0.0/8,invalid"
)
require
.
Len
(
t
,
prefixes
,
1
)
require
.
Len
(
t
,
invalid
,
1
)
host
:=
hostWithoutPort
(
"example.com:443"
)
require
.
Equal
(
t
,
"example.com"
,
host
)
addr
:=
netip
.
MustParseAddr
(
"10.0.0.1"
)
require
.
True
(
t
,
isAddrInTrustedProxies
(
addr
,
prefixes
))
require
.
False
(
t
,
isAddrInTrustedProxies
(
netip
.
MustParseAddr
(
"192.168.0.1"
),
prefixes
))
}
backend/internal/handler/admin/admin_service_stub_test.go
0 → 100644
View file @
c8e2f614
package
admin
import
(
"context"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
)
type
stubAdminService
struct
{
users
[]
service
.
User
apiKeys
[]
service
.
APIKey
groups
[]
service
.
Group
accounts
[]
service
.
Account
proxies
[]
service
.
Proxy
proxyCounts
[]
service
.
ProxyWithAccountCount
redeems
[]
service
.
RedeemCode
}
func
newStubAdminService
()
*
stubAdminService
{
now
:=
time
.
Now
()
.
UTC
()
user
:=
service
.
User
{
ID
:
1
,
Email
:
"user@example.com"
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
CreatedAt
:
now
,
UpdatedAt
:
now
,
}
apiKey
:=
service
.
APIKey
{
ID
:
10
,
UserID
:
user
.
ID
,
Key
:
"sk-test"
,
Name
:
"test"
,
Status
:
service
.
StatusActive
,
CreatedAt
:
now
,
UpdatedAt
:
now
,
}
group
:=
service
.
Group
{
ID
:
2
,
Name
:
"group"
,
Platform
:
service
.
PlatformAnthropic
,
Status
:
service
.
StatusActive
,
CreatedAt
:
now
,
UpdatedAt
:
now
,
}
account
:=
service
.
Account
{
ID
:
3
,
Name
:
"account"
,
Platform
:
service
.
PlatformAnthropic
,
Type
:
service
.
AccountTypeOAuth
,
Status
:
service
.
StatusActive
,
CreatedAt
:
now
,
UpdatedAt
:
now
,
}
proxy
:=
service
.
Proxy
{
ID
:
4
,
Name
:
"proxy"
,
Protocol
:
"http"
,
Host
:
"127.0.0.1"
,
Port
:
8080
,
Status
:
service
.
StatusActive
,
CreatedAt
:
now
,
UpdatedAt
:
now
,
}
redeem
:=
service
.
RedeemCode
{
ID
:
5
,
Code
:
"R-TEST"
,
Type
:
service
.
RedeemTypeBalance
,
Value
:
10
,
Status
:
service
.
StatusUnused
,
CreatedAt
:
now
,
}
return
&
stubAdminService
{
users
:
[]
service
.
User
{
user
},
apiKeys
:
[]
service
.
APIKey
{
apiKey
},
groups
:
[]
service
.
Group
{
group
},
accounts
:
[]
service
.
Account
{
account
},
proxies
:
[]
service
.
Proxy
{
proxy
},
proxyCounts
:
[]
service
.
ProxyWithAccountCount
{{
Proxy
:
proxy
,
AccountCount
:
1
}},
redeems
:
[]
service
.
RedeemCode
{
redeem
},
}
}
func
(
s
*
stubAdminService
)
ListUsers
(
ctx
context
.
Context
,
page
,
pageSize
int
,
filters
service
.
UserListFilters
)
([]
service
.
User
,
int64
,
error
)
{
return
s
.
users
,
int64
(
len
(
s
.
users
)),
nil
}
func
(
s
*
stubAdminService
)
GetUser
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
User
,
error
)
{
for
i
:=
range
s
.
users
{
if
s
.
users
[
i
]
.
ID
==
id
{
return
&
s
.
users
[
i
],
nil
}
}
user
:=
service
.
User
{
ID
:
id
,
Email
:
"user@example.com"
,
Status
:
service
.
StatusActive
}
return
&
user
,
nil
}
func
(
s
*
stubAdminService
)
CreateUser
(
ctx
context
.
Context
,
input
*
service
.
CreateUserInput
)
(
*
service
.
User
,
error
)
{
user
:=
service
.
User
{
ID
:
100
,
Email
:
input
.
Email
,
Status
:
service
.
StatusActive
}
return
&
user
,
nil
}
func
(
s
*
stubAdminService
)
UpdateUser
(
ctx
context
.
Context
,
id
int64
,
input
*
service
.
UpdateUserInput
)
(
*
service
.
User
,
error
)
{
user
:=
service
.
User
{
ID
:
id
,
Email
:
"updated@example.com"
,
Status
:
service
.
StatusActive
}
return
&
user
,
nil
}
func
(
s
*
stubAdminService
)
DeleteUser
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
UpdateUserBalance
(
ctx
context
.
Context
,
userID
int64
,
balance
float64
,
operation
string
,
notes
string
)
(
*
service
.
User
,
error
)
{
user
:=
service
.
User
{
ID
:
userID
,
Balance
:
balance
,
Status
:
service
.
StatusActive
}
return
&
user
,
nil
}
func
(
s
*
stubAdminService
)
GetUserAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
page
,
pageSize
int
)
([]
service
.
APIKey
,
int64
,
error
)
{
return
s
.
apiKeys
,
int64
(
len
(
s
.
apiKeys
)),
nil
}
func
(
s
*
stubAdminService
)
GetUserUsageStats
(
ctx
context
.
Context
,
userID
int64
,
period
string
)
(
any
,
error
)
{
return
map
[
string
]
any
{
"user_id"
:
userID
},
nil
}
func
(
s
*
stubAdminService
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
)
([]
service
.
Group
,
int64
,
error
)
{
return
s
.
groups
,
int64
(
len
(
s
.
groups
)),
nil
}
func
(
s
*
stubAdminService
)
GetAllGroups
(
ctx
context
.
Context
)
([]
service
.
Group
,
error
)
{
return
s
.
groups
,
nil
}
func
(
s
*
stubAdminService
)
GetAllGroupsByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
service
.
Group
,
error
)
{
return
s
.
groups
,
nil
}
func
(
s
*
stubAdminService
)
GetGroup
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Group
,
error
)
{
group
:=
service
.
Group
{
ID
:
id
,
Name
:
"group"
,
Status
:
service
.
StatusActive
}
return
&
group
,
nil
}
func
(
s
*
stubAdminService
)
CreateGroup
(
ctx
context
.
Context
,
input
*
service
.
CreateGroupInput
)
(
*
service
.
Group
,
error
)
{
group
:=
service
.
Group
{
ID
:
200
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
group
,
nil
}
func
(
s
*
stubAdminService
)
UpdateGroup
(
ctx
context
.
Context
,
id
int64
,
input
*
service
.
UpdateGroupInput
)
(
*
service
.
Group
,
error
)
{
group
:=
service
.
Group
{
ID
:
id
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
group
,
nil
}
func
(
s
*
stubAdminService
)
DeleteGroup
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
GetGroupAPIKeys
(
ctx
context
.
Context
,
groupID
int64
,
page
,
pageSize
int
)
([]
service
.
APIKey
,
int64
,
error
)
{
return
s
.
apiKeys
,
int64
(
len
(
s
.
apiKeys
)),
nil
}
func
(
s
*
stubAdminService
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
)
([]
service
.
Account
,
int64
,
error
)
{
return
s
.
accounts
,
int64
(
len
(
s
.
accounts
)),
nil
}
func
(
s
*
stubAdminService
)
GetAccount
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
"account"
,
Status
:
service
.
StatusActive
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
GetAccountsByIDs
(
ctx
context
.
Context
,
ids
[]
int64
)
([]
*
service
.
Account
,
error
)
{
out
:=
make
([]
*
service
.
Account
,
0
,
len
(
ids
))
for
_
,
id
:=
range
ids
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
"account"
,
Status
:
service
.
StatusActive
}
out
=
append
(
out
,
&
account
)
}
return
out
,
nil
}
func
(
s
*
stubAdminService
)
CreateAccount
(
ctx
context
.
Context
,
input
*
service
.
CreateAccountInput
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
300
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
UpdateAccount
(
ctx
context
.
Context
,
id
int64
,
input
*
service
.
UpdateAccountInput
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
DeleteAccount
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
RefreshAccountCredentials
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
"account"
,
Status
:
service
.
StatusActive
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
ClearAccountError
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
"account"
,
Status
:
service
.
StatusActive
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
SetAccountError
(
ctx
context
.
Context
,
id
int64
,
errorMsg
string
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
SetAccountSchedulable
(
ctx
context
.
Context
,
id
int64
,
schedulable
bool
)
(
*
service
.
Account
,
error
)
{
account
:=
service
.
Account
{
ID
:
id
,
Name
:
"account"
,
Status
:
service
.
StatusActive
,
Schedulable
:
schedulable
}
return
&
account
,
nil
}
func
(
s
*
stubAdminService
)
BulkUpdateAccounts
(
ctx
context
.
Context
,
input
*
service
.
BulkUpdateAccountsInput
)
(
*
service
.
BulkUpdateAccountsResult
,
error
)
{
return
&
service
.
BulkUpdateAccountsResult
{
Success
:
1
,
Failed
:
0
,
SuccessIDs
:
[]
int64
{
1
}},
nil
}
func
(
s
*
stubAdminService
)
ListProxies
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
)
([]
service
.
Proxy
,
int64
,
error
)
{
return
s
.
proxies
,
int64
(
len
(
s
.
proxies
)),
nil
}
func
(
s
*
stubAdminService
)
ListProxiesWithAccountCount
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
)
([]
service
.
ProxyWithAccountCount
,
int64
,
error
)
{
return
s
.
proxyCounts
,
int64
(
len
(
s
.
proxyCounts
)),
nil
}
func
(
s
*
stubAdminService
)
GetAllProxies
(
ctx
context
.
Context
)
([]
service
.
Proxy
,
error
)
{
return
s
.
proxies
,
nil
}
func
(
s
*
stubAdminService
)
GetAllProxiesWithAccountCount
(
ctx
context
.
Context
)
([]
service
.
ProxyWithAccountCount
,
error
)
{
return
s
.
proxyCounts
,
nil
}
func
(
s
*
stubAdminService
)
GetProxy
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Proxy
,
error
)
{
proxy
:=
service
.
Proxy
{
ID
:
id
,
Name
:
"proxy"
,
Status
:
service
.
StatusActive
}
return
&
proxy
,
nil
}
func
(
s
*
stubAdminService
)
CreateProxy
(
ctx
context
.
Context
,
input
*
service
.
CreateProxyInput
)
(
*
service
.
Proxy
,
error
)
{
proxy
:=
service
.
Proxy
{
ID
:
400
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
proxy
,
nil
}
func
(
s
*
stubAdminService
)
UpdateProxy
(
ctx
context
.
Context
,
id
int64
,
input
*
service
.
UpdateProxyInput
)
(
*
service
.
Proxy
,
error
)
{
proxy
:=
service
.
Proxy
{
ID
:
id
,
Name
:
input
.
Name
,
Status
:
service
.
StatusActive
}
return
&
proxy
,
nil
}
func
(
s
*
stubAdminService
)
DeleteProxy
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
BatchDeleteProxies
(
ctx
context
.
Context
,
ids
[]
int64
)
(
*
service
.
ProxyBatchDeleteResult
,
error
)
{
return
&
service
.
ProxyBatchDeleteResult
{
DeletedIDs
:
ids
},
nil
}
func
(
s
*
stubAdminService
)
GetProxyAccounts
(
ctx
context
.
Context
,
proxyID
int64
)
([]
service
.
ProxyAccountSummary
,
error
)
{
return
[]
service
.
ProxyAccountSummary
{{
ID
:
1
,
Name
:
"account"
}},
nil
}
func
(
s
*
stubAdminService
)
CheckProxyExists
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
{
return
false
,
nil
}
func
(
s
*
stubAdminService
)
TestProxy
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
ProxyTestResult
,
error
)
{
return
&
service
.
ProxyTestResult
{
Success
:
true
,
Message
:
"ok"
},
nil
}
func
(
s
*
stubAdminService
)
ListRedeemCodes
(
ctx
context
.
Context
,
page
,
pageSize
int
,
codeType
,
status
,
search
string
)
([]
service
.
RedeemCode
,
int64
,
error
)
{
return
s
.
redeems
,
int64
(
len
(
s
.
redeems
)),
nil
}
func
(
s
*
stubAdminService
)
GetRedeemCode
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
RedeemCode
,
error
)
{
code
:=
service
.
RedeemCode
{
ID
:
id
,
Code
:
"R-TEST"
,
Status
:
service
.
StatusUnused
}
return
&
code
,
nil
}
func
(
s
*
stubAdminService
)
GenerateRedeemCodes
(
ctx
context
.
Context
,
input
*
service
.
GenerateRedeemCodesInput
)
([]
service
.
RedeemCode
,
error
)
{
return
s
.
redeems
,
nil
}
func
(
s
*
stubAdminService
)
DeleteRedeemCode
(
ctx
context
.
Context
,
id
int64
)
error
{
return
nil
}
func
(
s
*
stubAdminService
)
BatchDeleteRedeemCodes
(
ctx
context
.
Context
,
ids
[]
int64
)
(
int64
,
error
)
{
return
int64
(
len
(
ids
)),
nil
}
func
(
s
*
stubAdminService
)
ExpireRedeemCode
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
RedeemCode
,
error
)
{
code
:=
service
.
RedeemCode
{
ID
:
id
,
Code
:
"R-TEST"
,
Status
:
service
.
StatusUsed
}
return
&
code
,
nil
}
// Ensure stub implements interface.
var
_
service
.
AdminService
=
(
*
stubAdminService
)(
nil
)
backend/internal/handler/admin/dashboard_handler.go
View file @
c8e2f614
...
...
@@ -186,7 +186,7 @@ func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) {
// GetUsageTrend handles getting usage trend data
// GET /api/v1/admin/dashboard/trend
// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), user_id, api_key_id, model, account_id, group_id, stream
// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), user_id, api_key_id, model, account_id, group_id, stream
, billing_type
func
(
h
*
DashboardHandler
)
GetUsageTrend
(
c
*
gin
.
Context
)
{
startTime
,
endTime
:=
parseTimeRange
(
c
)
granularity
:=
c
.
DefaultQuery
(
"granularity"
,
"day"
)
...
...
@@ -195,6 +195,7 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
var
userID
,
apiKeyID
,
accountID
,
groupID
int64
var
model
string
var
stream
*
bool
var
billingType
*
int8
if
userIDStr
:=
c
.
Query
(
"user_id"
);
userIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
userIDStr
,
10
,
64
);
err
==
nil
{
...
...
@@ -224,8 +225,17 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
stream
=
&
streamVal
}
}
if
billingTypeStr
:=
c
.
Query
(
"billing_type"
);
billingTypeStr
!=
""
{
if
v
,
err
:=
strconv
.
ParseInt
(
billingTypeStr
,
10
,
8
);
err
==
nil
{
bt
:=
int8
(
v
)
billingType
=
&
bt
}
else
{
response
.
BadRequest
(
c
,
"Invalid billing_type"
)
return
}
}
trend
,
err
:=
h
.
dashboardService
.
GetUsageTrendWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
stream
)
trend
,
err
:=
h
.
dashboardService
.
GetUsageTrendWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
stream
,
billingType
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get usage trend"
)
return
...
...
@@ -241,13 +251,14 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
// GetModelStats handles getting model usage statistics
// GET /api/v1/admin/dashboard/models
// Query params: start_date, end_date (YYYY-MM-DD), user_id, api_key_id, account_id, group_id, stream
// Query params: start_date, end_date (YYYY-MM-DD), user_id, api_key_id, account_id, group_id, stream
, billing_type
func
(
h
*
DashboardHandler
)
GetModelStats
(
c
*
gin
.
Context
)
{
startTime
,
endTime
:=
parseTimeRange
(
c
)
// Parse optional filter params
var
userID
,
apiKeyID
,
accountID
,
groupID
int64
var
stream
*
bool
var
billingType
*
int8
if
userIDStr
:=
c
.
Query
(
"user_id"
);
userIDStr
!=
""
{
if
id
,
err
:=
strconv
.
ParseInt
(
userIDStr
,
10
,
64
);
err
==
nil
{
...
...
@@ -274,8 +285,17 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
stream
=
&
streamVal
}
}
if
billingTypeStr
:=
c
.
Query
(
"billing_type"
);
billingTypeStr
!=
""
{
if
v
,
err
:=
strconv
.
ParseInt
(
billingTypeStr
,
10
,
8
);
err
==
nil
{
bt
:=
int8
(
v
)
billingType
=
&
bt
}
else
{
response
.
BadRequest
(
c
,
"Invalid billing_type"
)
return
}
}
stats
,
err
:=
h
.
dashboardService
.
GetModelStatsWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
stream
)
stats
,
err
:=
h
.
dashboardService
.
GetModelStatsWithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
stream
,
billingType
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get model statistics"
)
return
...
...
backend/internal/handler/admin/group_handler.go
View file @
c8e2f614
...
...
@@ -94,9 +94,9 @@ func (h *GroupHandler) List(c *gin.Context) {
return
}
outGroups
:=
make
([]
dto
.
Group
,
0
,
len
(
groups
))
outGroups
:=
make
([]
dto
.
Admin
Group
,
0
,
len
(
groups
))
for
i
:=
range
groups
{
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
(
&
groups
[
i
]))
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
Admin
(
&
groups
[
i
]))
}
response
.
Paginated
(
c
,
outGroups
,
total
,
page
,
pageSize
)
}
...
...
@@ -120,9 +120,9 @@ func (h *GroupHandler) GetAll(c *gin.Context) {
return
}
outGroups
:=
make
([]
dto
.
Group
,
0
,
len
(
groups
))
outGroups
:=
make
([]
dto
.
Admin
Group
,
0
,
len
(
groups
))
for
i
:=
range
groups
{
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
(
&
groups
[
i
]))
outGroups
=
append
(
outGroups
,
*
dto
.
GroupFromService
Admin
(
&
groups
[
i
]))
}
response
.
Success
(
c
,
outGroups
)
}
...
...
@@ -142,7 +142,7 @@ func (h *GroupHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Create handles creating a new group
...
...
@@ -177,7 +177,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Update handles updating a group
...
...
@@ -219,7 +219,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
GroupFromService
(
group
))
response
.
Success
(
c
,
dto
.
GroupFromService
Admin
(
group
))
}
// Delete handles deleting a group
...
...
backend/internal/handler/admin/redeem_handler.go
View file @
c8e2f614
...
...
@@ -54,9 +54,9 @@ func (h *RedeemHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
RedeemCode
,
0
,
len
(
codes
))
out
:=
make
([]
dto
.
Admin
RedeemCode
,
0
,
len
(
codes
))
for
i
:=
range
codes
{
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
(
&
codes
[
i
]))
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
Admin
(
&
codes
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
}
...
...
@@ -76,7 +76,7 @@ func (h *RedeemHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
(
code
))
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
Admin
(
code
))
}
// Generate handles generating new redeem codes
...
...
@@ -100,9 +100,9 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
RedeemCode
,
0
,
len
(
codes
))
out
:=
make
([]
dto
.
Admin
RedeemCode
,
0
,
len
(
codes
))
for
i
:=
range
codes
{
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
(
&
codes
[
i
]))
out
=
append
(
out
,
*
dto
.
RedeemCodeFromService
Admin
(
&
codes
[
i
]))
}
response
.
Success
(
c
,
out
)
}
...
...
@@ -163,7 +163,7 @@ func (h *RedeemHandler) Expire(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
(
code
))
response
.
Success
(
c
,
dto
.
RedeemCodeFromService
Admin
(
code
))
}
// GetStats handles getting redeem code statistics
...
...
backend/internal/handler/admin/setting_handler.go
View file @
c8e2f614
...
...
@@ -68,6 +68,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
ContactInfo
:
settings
.
ContactInfo
,
DocURL
:
settings
.
DocURL
,
HomeContent
:
settings
.
HomeContent
,
HideCcsImportButton
:
settings
.
HideCcsImportButton
,
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultBalance
:
settings
.
DefaultBalance
,
EnableModelFallback
:
settings
.
EnableModelFallback
,
...
...
@@ -111,13 +112,14 @@ type UpdateSettingsRequest struct {
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
// OEM设置
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
// 默认配置
DefaultConcurrency
int
`json:"default_concurrency"`
...
...
@@ -259,6 +261,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ContactInfo
:
req
.
ContactInfo
,
DocURL
:
req
.
DocURL
,
HomeContent
:
req
.
HomeContent
,
HideCcsImportButton
:
req
.
HideCcsImportButton
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
EnableModelFallback
:
req
.
EnableModelFallback
,
...
...
@@ -332,6 +335,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ContactInfo
:
updatedSettings
.
ContactInfo
,
DocURL
:
updatedSettings
.
DocURL
,
HomeContent
:
updatedSettings
.
HomeContent
,
HideCcsImportButton
:
updatedSettings
.
HideCcsImportButton
,
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
EnableModelFallback
:
updatedSettings
.
EnableModelFallback
,
...
...
@@ -439,6 +443,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
HomeContent
!=
after
.
HomeContent
{
changed
=
append
(
changed
,
"home_content"
)
}
if
before
.
HideCcsImportButton
!=
after
.
HideCcsImportButton
{
changed
=
append
(
changed
,
"hide_ccs_import_button"
)
}
if
before
.
DefaultConcurrency
!=
after
.
DefaultConcurrency
{
changed
=
append
(
changed
,
"default_concurrency"
)
}
...
...
backend/internal/handler/admin/subscription_handler.go
View file @
c8e2f614
...
...
@@ -53,9 +53,9 @@ type BulkAssignSubscriptionRequest struct {
Notes
string
`json:"notes"`
}
//
Extend
SubscriptionRequest represents
extend
subscription request
type
Extend
SubscriptionRequest
struct
{
Days
int
`json:"days" binding:"required,min=
1
,max=36500"`
//
max 100 years
//
Adjust
SubscriptionRequest represents
adjust
subscription request
(extend or shorten)
type
Adjust
SubscriptionRequest
struct
{
Days
int
`json:"days" binding:"required,min=
-36500
,max=36500"`
//
negative to shorten, positive to extend
}
// List handles listing all subscriptions with pagination and filters
...
...
@@ -83,9 +83,9 @@ func (h *SubscriptionHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
PaginatedWithResult
(
c
,
out
,
toResponsePagination
(
pagination
))
}
...
...
@@ -105,7 +105,7 @@ func (h *SubscriptionHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// GetProgress handles getting subscription usage progress
...
...
@@ -150,7 +150,7 @@ func (h *SubscriptionHandler) Assign(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// BulkAssign handles bulk assigning subscriptions to multiple users
...
...
@@ -180,7 +180,7 @@ func (h *SubscriptionHandler) BulkAssign(c *gin.Context) {
response
.
Success
(
c
,
dto
.
BulkAssignResultFromService
(
result
))
}
// Extend handles
extend
ing a subscription
// Extend handles
adjust
ing a subscription
(extend or shorten)
// POST /api/v1/admin/subscriptions/:id/extend
func
(
h
*
SubscriptionHandler
)
Extend
(
c
*
gin
.
Context
)
{
subscriptionID
,
err
:=
strconv
.
ParseInt
(
c
.
Param
(
"id"
),
10
,
64
)
...
...
@@ -189,7 +189,7 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
return
}
var
req
Extend
SubscriptionRequest
var
req
Adjust
SubscriptionRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
...
...
@@ -201,7 +201,7 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
(
subscription
))
response
.
Success
(
c
,
dto
.
UserSubscriptionFromService
Admin
(
subscription
))
}
// Revoke handles revoking a subscription
...
...
@@ -239,9 +239,9 @@ func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
PaginatedWithResult
(
c
,
out
,
toResponsePagination
(
pagination
))
}
...
...
@@ -261,9 +261,9 @@ func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UserSubscription
,
0
,
len
(
subscriptions
))
out
:=
make
([]
dto
.
Admin
UserSubscription
,
0
,
len
(
subscriptions
))
for
i
:=
range
subscriptions
{
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
(
&
subscriptions
[
i
]))
out
=
append
(
out
,
*
dto
.
UserSubscriptionFromService
Admin
(
&
subscriptions
[
i
]))
}
response
.
Success
(
c
,
out
)
}
...
...
backend/internal/handler/admin/usage_cleanup_handler_test.go
0 → 100644
View file @
c8e2f614
package
admin
import
(
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type
cleanupRepoStub
struct
{
mu
sync
.
Mutex
created
[]
*
service
.
UsageCleanupTask
listTasks
[]
service
.
UsageCleanupTask
listResult
*
pagination
.
PaginationResult
listErr
error
statusByID
map
[
int64
]
string
}
func
(
s
*
cleanupRepoStub
)
CreateTask
(
ctx
context
.
Context
,
task
*
service
.
UsageCleanupTask
)
error
{
if
task
==
nil
{
return
nil
}
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
if
task
.
ID
==
0
{
task
.
ID
=
int64
(
len
(
s
.
created
)
+
1
)
}
if
task
.
CreatedAt
.
IsZero
()
{
task
.
CreatedAt
=
time
.
Now
()
.
UTC
()
}
task
.
UpdatedAt
=
task
.
CreatedAt
clone
:=
*
task
s
.
created
=
append
(
s
.
created
,
&
clone
)
return
nil
}
func
(
s
*
cleanupRepoStub
)
ListTasks
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
service
.
UsageCleanupTask
,
*
pagination
.
PaginationResult
,
error
)
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
return
s
.
listTasks
,
s
.
listResult
,
s
.
listErr
}
func
(
s
*
cleanupRepoStub
)
ClaimNextPendingTask
(
ctx
context
.
Context
,
staleRunningAfterSeconds
int64
)
(
*
service
.
UsageCleanupTask
,
error
)
{
return
nil
,
nil
}
func
(
s
*
cleanupRepoStub
)
GetTaskStatus
(
ctx
context
.
Context
,
taskID
int64
)
(
string
,
error
)
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
if
s
.
statusByID
==
nil
{
return
""
,
sql
.
ErrNoRows
}
status
,
ok
:=
s
.
statusByID
[
taskID
]
if
!
ok
{
return
""
,
sql
.
ErrNoRows
}
return
status
,
nil
}
func
(
s
*
cleanupRepoStub
)
UpdateTaskProgress
(
ctx
context
.
Context
,
taskID
int64
,
deletedRows
int64
)
error
{
return
nil
}
func
(
s
*
cleanupRepoStub
)
CancelTask
(
ctx
context
.
Context
,
taskID
int64
,
canceledBy
int64
)
(
bool
,
error
)
{
s
.
mu
.
Lock
()
defer
s
.
mu
.
Unlock
()
if
s
.
statusByID
==
nil
{
s
.
statusByID
=
map
[
int64
]
string
{}
}
status
:=
s
.
statusByID
[
taskID
]
if
status
!=
service
.
UsageCleanupStatusPending
&&
status
!=
service
.
UsageCleanupStatusRunning
{
return
false
,
nil
}
s
.
statusByID
[
taskID
]
=
service
.
UsageCleanupStatusCanceled
return
true
,
nil
}
func
(
s
*
cleanupRepoStub
)
MarkTaskSucceeded
(
ctx
context
.
Context
,
taskID
int64
,
deletedRows
int64
)
error
{
return
nil
}
func
(
s
*
cleanupRepoStub
)
MarkTaskFailed
(
ctx
context
.
Context
,
taskID
int64
,
deletedRows
int64
,
errorMsg
string
)
error
{
return
nil
}
func
(
s
*
cleanupRepoStub
)
DeleteUsageLogsBatch
(
ctx
context
.
Context
,
filters
service
.
UsageCleanupFilters
,
limit
int
)
(
int64
,
error
)
{
return
0
,
nil
}
var
_
service
.
UsageCleanupRepository
=
(
*
cleanupRepoStub
)(
nil
)
func
setupCleanupRouter
(
cleanupService
*
service
.
UsageCleanupService
,
userID
int64
)
*
gin
.
Engine
{
gin
.
SetMode
(
gin
.
TestMode
)
router
:=
gin
.
New
()
if
userID
>
0
{
router
.
Use
(
func
(
c
*
gin
.
Context
)
{
c
.
Set
(
string
(
middleware
.
ContextKeyUser
),
middleware
.
AuthSubject
{
UserID
:
userID
})
c
.
Next
()
})
}
handler
:=
NewUsageHandler
(
nil
,
nil
,
nil
,
cleanupService
)
router
.
POST
(
"/api/v1/admin/usage/cleanup-tasks"
,
handler
.
CreateCleanupTask
)
router
.
GET
(
"/api/v1/admin/usage/cleanup-tasks"
,
handler
.
ListCleanupTasks
)
router
.
POST
(
"/api/v1/admin/usage/cleanup-tasks/:id/cancel"
,
handler
.
CancelCleanupTask
)
return
router
}
func
TestUsageHandlerCreateCleanupTaskUnauthorized
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
0
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewBufferString
(
`{}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskUnavailable
(
t
*
testing
.
T
)
{
router
:=
setupCleanupRouter
(
nil
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewBufferString
(
`{}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusServiceUnavailable
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskBindError
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
88
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewBufferString
(
"{bad-json"
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskMissingRange
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
88
)
payload
:=
map
[
string
]
any
{
"start_date"
:
"2024-01-01"
,
"timezone"
:
"UTC"
,
}
body
,
err
:=
json
.
Marshal
(
payload
)
require
.
NoError
(
t
,
err
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskInvalidDate
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
88
)
payload
:=
map
[
string
]
any
{
"start_date"
:
"2024-13-01"
,
"end_date"
:
"2024-01-02"
,
"timezone"
:
"UTC"
,
}
body
,
err
:=
json
.
Marshal
(
payload
)
require
.
NoError
(
t
,
err
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskInvalidEndDate
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
88
)
payload
:=
map
[
string
]
any
{
"start_date"
:
"2024-01-01"
,
"end_date"
:
"2024-02-40"
,
"timezone"
:
"UTC"
,
}
body
,
err
:=
json
.
Marshal
(
payload
)
require
.
NoError
(
t
,
err
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
}
func
TestUsageHandlerCreateCleanupTaskSuccess
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
99
)
payload
:=
map
[
string
]
any
{
"start_date"
:
" 2024-01-01 "
,
"end_date"
:
"2024-01-02"
,
"timezone"
:
"UTC"
,
"model"
:
"gpt-4"
,
}
body
,
err
:=
json
.
Marshal
(
payload
)
require
.
NoError
(
t
,
err
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks"
,
bytes
.
NewReader
(
body
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
var
resp
response
.
Response
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
repo
.
mu
.
Lock
()
defer
repo
.
mu
.
Unlock
()
require
.
Len
(
t
,
repo
.
created
,
1
)
created
:=
repo
.
created
[
0
]
require
.
Equal
(
t
,
int64
(
99
),
created
.
CreatedBy
)
require
.
NotNil
(
t
,
created
.
Filters
.
Model
)
require
.
Equal
(
t
,
"gpt-4"
,
*
created
.
Filters
.
Model
)
start
:=
time
.
Date
(
2024
,
1
,
1
,
0
,
0
,
0
,
0
,
time
.
UTC
)
end
:=
time
.
Date
(
2024
,
1
,
2
,
0
,
0
,
0
,
0
,
time
.
UTC
)
.
Add
(
24
*
time
.
Hour
-
time
.
Nanosecond
)
require
.
True
(
t
,
created
.
Filters
.
StartTime
.
Equal
(
start
))
require
.
True
(
t
,
created
.
Filters
.
EndTime
.
Equal
(
end
))
}
func
TestUsageHandlerListCleanupTasksUnavailable
(
t
*
testing
.
T
)
{
router
:=
setupCleanupRouter
(
nil
,
0
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/usage/cleanup-tasks"
,
nil
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusServiceUnavailable
,
recorder
.
Code
)
}
func
TestUsageHandlerListCleanupTasksSuccess
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
repo
.
listTasks
=
[]
service
.
UsageCleanupTask
{
{
ID
:
7
,
Status
:
service
.
UsageCleanupStatusSucceeded
,
CreatedBy
:
4
,
},
}
repo
.
listResult
=
&
pagination
.
PaginationResult
{
Total
:
1
,
Page
:
1
,
PageSize
:
20
,
Pages
:
1
}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/usage/cleanup-tasks"
,
nil
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
var
resp
struct
{
Code
int
`json:"code"`
Data
struct
{
Items
[]
dto
.
UsageCleanupTask
`json:"items"`
Total
int64
`json:"total"`
Page
int
`json:"page"`
}
`json:"data"`
}
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Len
(
t
,
resp
.
Data
.
Items
,
1
)
require
.
Equal
(
t
,
int64
(
7
),
resp
.
Data
.
Items
[
0
]
.
ID
)
require
.
Equal
(
t
,
int64
(
1
),
resp
.
Data
.
Total
)
require
.
Equal
(
t
,
1
,
resp
.
Data
.
Page
)
}
func
TestUsageHandlerListCleanupTasksError
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{
listErr
:
errors
.
New
(
"boom"
)}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
,
MaxRangeDays
:
31
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/usage/cleanup-tasks"
,
nil
)
recorder
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
recorder
,
req
)
require
.
Equal
(
t
,
http
.
StatusInternalServerError
,
recorder
.
Code
)
}
func
TestUsageHandlerCancelCleanupTaskUnauthorized
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
0
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks/1/cancel"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
rec
.
Code
)
}
func
TestUsageHandlerCancelCleanupTaskNotFound
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks/999/cancel"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusNotFound
,
rec
.
Code
)
}
func
TestUsageHandlerCancelCleanupTaskConflict
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{
statusByID
:
map
[
int64
]
string
{
2
:
service
.
UsageCleanupStatusSucceeded
}}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks/2/cancel"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusConflict
,
rec
.
Code
)
}
func
TestUsageHandlerCancelCleanupTaskSuccess
(
t
*
testing
.
T
)
{
repo
:=
&
cleanupRepoStub
{
statusByID
:
map
[
int64
]
string
{
3
:
service
.
UsageCleanupStatusPending
}}
cfg
:=
&
config
.
Config
{
UsageCleanup
:
config
.
UsageCleanupConfig
{
Enabled
:
true
}}
cleanupService
:=
service
.
NewUsageCleanupService
(
repo
,
nil
,
nil
,
cfg
)
router
:=
setupCleanupRouter
(
cleanupService
,
1
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/usage/cleanup-tasks/3/cancel"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
}
backend/internal/handler/admin/usage_handler.go
View file @
c8e2f614
package
admin
import
(
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
...
...
@@ -9,6 +12,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
...
...
@@ -16,9 +20,10 @@ import (
// UsageHandler handles admin usage-related requests
type
UsageHandler
struct
{
usageService
*
service
.
UsageService
apiKeyService
*
service
.
APIKeyService
adminService
service
.
AdminService
usageService
*
service
.
UsageService
apiKeyService
*
service
.
APIKeyService
adminService
service
.
AdminService
cleanupService
*
service
.
UsageCleanupService
}
// NewUsageHandler creates a new admin usage handler
...
...
@@ -26,14 +31,30 @@ func NewUsageHandler(
usageService
*
service
.
UsageService
,
apiKeyService
*
service
.
APIKeyService
,
adminService
service
.
AdminService
,
cleanupService
*
service
.
UsageCleanupService
,
)
*
UsageHandler
{
return
&
UsageHandler
{
usageService
:
usageService
,
apiKeyService
:
apiKeyService
,
adminService
:
adminService
,
usageService
:
usageService
,
apiKeyService
:
apiKeyService
,
adminService
:
adminService
,
cleanupService
:
cleanupService
,
}
}
// CreateUsageCleanupTaskRequest represents cleanup task creation request
type
CreateUsageCleanupTaskRequest
struct
{
StartDate
string
`json:"start_date"`
EndDate
string
`json:"end_date"`
UserID
*
int64
`json:"user_id"`
APIKeyID
*
int64
`json:"api_key_id"`
AccountID
*
int64
`json:"account_id"`
GroupID
*
int64
`json:"group_id"`
Model
*
string
`json:"model"`
Stream
*
bool
`json:"stream"`
BillingType
*
int8
`json:"billing_type"`
Timezone
string
`json:"timezone"`
}
// List handles listing all usage records with filters
// GET /api/v1/admin/usage
func
(
h
*
UsageHandler
)
List
(
c
*
gin
.
Context
)
{
...
...
@@ -142,7 +163,7 @@ func (h *UsageHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
UsageLog
,
0
,
len
(
records
))
out
:=
make
([]
dto
.
Admin
UsageLog
,
0
,
len
(
records
))
for
i
:=
range
records
{
out
=
append
(
out
,
*
dto
.
UsageLogFromServiceAdmin
(
&
records
[
i
]))
}
...
...
@@ -344,3 +365,162 @@ func (h *UsageHandler) SearchAPIKeys(c *gin.Context) {
response
.
Success
(
c
,
result
)
}
// ListCleanupTasks handles listing usage cleanup tasks
// GET /api/v1/admin/usage/cleanup-tasks
func
(
h
*
UsageHandler
)
ListCleanupTasks
(
c
*
gin
.
Context
)
{
if
h
.
cleanupService
==
nil
{
response
.
Error
(
c
,
http
.
StatusServiceUnavailable
,
"Usage cleanup service unavailable"
)
return
}
operator
:=
int64
(
0
)
if
subject
,
ok
:=
middleware
.
GetAuthSubjectFromContext
(
c
);
ok
{
operator
=
subject
.
UserID
}
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
log
.
Printf
(
"[UsageCleanup] 请求清理任务列表: operator=%d page=%d page_size=%d"
,
operator
,
page
,
pageSize
)
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
tasks
,
result
,
err
:=
h
.
cleanupService
.
ListTasks
(
c
.
Request
.
Context
(),
params
)
if
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] 查询清理任务列表失败: operator=%d page=%d page_size=%d err=%v"
,
operator
,
page
,
pageSize
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
}
out
:=
make
([]
dto
.
UsageCleanupTask
,
0
,
len
(
tasks
))
for
i
:=
range
tasks
{
out
=
append
(
out
,
*
dto
.
UsageCleanupTaskFromService
(
&
tasks
[
i
]))
}
log
.
Printf
(
"[UsageCleanup] 返回清理任务列表: operator=%d total=%d items=%d page=%d page_size=%d"
,
operator
,
result
.
Total
,
len
(
out
),
page
,
pageSize
)
response
.
Paginated
(
c
,
out
,
result
.
Total
,
page
,
pageSize
)
}
// CreateCleanupTask handles creating a usage cleanup task
// POST /api/v1/admin/usage/cleanup-tasks
func
(
h
*
UsageHandler
)
CreateCleanupTask
(
c
*
gin
.
Context
)
{
if
h
.
cleanupService
==
nil
{
response
.
Error
(
c
,
http
.
StatusServiceUnavailable
,
"Usage cleanup service unavailable"
)
return
}
subject
,
ok
:=
middleware
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
||
subject
.
UserID
<=
0
{
response
.
Unauthorized
(
c
,
"Unauthorized"
)
return
}
var
req
CreateUsageCleanupTaskRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
req
.
StartDate
=
strings
.
TrimSpace
(
req
.
StartDate
)
req
.
EndDate
=
strings
.
TrimSpace
(
req
.
EndDate
)
if
req
.
StartDate
==
""
||
req
.
EndDate
==
""
{
response
.
BadRequest
(
c
,
"start_date and end_date are required"
)
return
}
startTime
,
err
:=
timezone
.
ParseInUserLocation
(
"2006-01-02"
,
req
.
StartDate
,
req
.
Timezone
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid start_date format, use YYYY-MM-DD"
)
return
}
endTime
,
err
:=
timezone
.
ParseInUserLocation
(
"2006-01-02"
,
req
.
EndDate
,
req
.
Timezone
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid end_date format, use YYYY-MM-DD"
)
return
}
endTime
=
endTime
.
Add
(
24
*
time
.
Hour
-
time
.
Nanosecond
)
filters
:=
service
.
UsageCleanupFilters
{
StartTime
:
startTime
,
EndTime
:
endTime
,
UserID
:
req
.
UserID
,
APIKeyID
:
req
.
APIKeyID
,
AccountID
:
req
.
AccountID
,
GroupID
:
req
.
GroupID
,
Model
:
req
.
Model
,
Stream
:
req
.
Stream
,
BillingType
:
req
.
BillingType
,
}
var
userID
any
if
filters
.
UserID
!=
nil
{
userID
=
*
filters
.
UserID
}
var
apiKeyID
any
if
filters
.
APIKeyID
!=
nil
{
apiKeyID
=
*
filters
.
APIKeyID
}
var
accountID
any
if
filters
.
AccountID
!=
nil
{
accountID
=
*
filters
.
AccountID
}
var
groupID
any
if
filters
.
GroupID
!=
nil
{
groupID
=
*
filters
.
GroupID
}
var
model
any
if
filters
.
Model
!=
nil
{
model
=
*
filters
.
Model
}
var
stream
any
if
filters
.
Stream
!=
nil
{
stream
=
*
filters
.
Stream
}
var
billingType
any
if
filters
.
BillingType
!=
nil
{
billingType
=
*
filters
.
BillingType
}
log
.
Printf
(
"[UsageCleanup] 请求创建清理任务: operator=%d start=%s end=%s user_id=%v api_key_id=%v account_id=%v group_id=%v model=%v stream=%v billing_type=%v tz=%q"
,
subject
.
UserID
,
filters
.
StartTime
.
Format
(
time
.
RFC3339
),
filters
.
EndTime
.
Format
(
time
.
RFC3339
),
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
stream
,
billingType
,
req
.
Timezone
,
)
task
,
err
:=
h
.
cleanupService
.
CreateTask
(
c
.
Request
.
Context
(),
filters
,
subject
.
UserID
)
if
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] 创建清理任务失败: operator=%d err=%v"
,
subject
.
UserID
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
}
log
.
Printf
(
"[UsageCleanup] 清理任务已创建: task=%d operator=%d status=%s"
,
task
.
ID
,
subject
.
UserID
,
task
.
Status
)
response
.
Success
(
c
,
dto
.
UsageCleanupTaskFromService
(
task
))
}
// CancelCleanupTask handles canceling a usage cleanup task
// POST /api/v1/admin/usage/cleanup-tasks/:id/cancel
func
(
h
*
UsageHandler
)
CancelCleanupTask
(
c
*
gin
.
Context
)
{
if
h
.
cleanupService
==
nil
{
response
.
Error
(
c
,
http
.
StatusServiceUnavailable
,
"Usage cleanup service unavailable"
)
return
}
subject
,
ok
:=
middleware
.
GetAuthSubjectFromContext
(
c
)
if
!
ok
||
subject
.
UserID
<=
0
{
response
.
Unauthorized
(
c
,
"Unauthorized"
)
return
}
idStr
:=
strings
.
TrimSpace
(
c
.
Param
(
"id"
))
taskID
,
err
:=
strconv
.
ParseInt
(
idStr
,
10
,
64
)
if
err
!=
nil
||
taskID
<=
0
{
response
.
BadRequest
(
c
,
"Invalid task id"
)
return
}
log
.
Printf
(
"[UsageCleanup] 请求取消清理任务: task=%d operator=%d"
,
taskID
,
subject
.
UserID
)
if
err
:=
h
.
cleanupService
.
CancelTask
(
c
.
Request
.
Context
(),
taskID
,
subject
.
UserID
);
err
!=
nil
{
log
.
Printf
(
"[UsageCleanup] 取消清理任务失败: task=%d operator=%d err=%v"
,
taskID
,
subject
.
UserID
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
}
log
.
Printf
(
"[UsageCleanup] 清理任务已取消: task=%d operator=%d"
,
taskID
,
subject
.
UserID
)
response
.
Success
(
c
,
gin
.
H
{
"id"
:
taskID
,
"status"
:
service
.
UsageCleanupStatusCanceled
})
}
backend/internal/handler/admin/user_handler.go
View file @
c8e2f614
...
...
@@ -84,9 +84,9 @@ func (h *UserHandler) List(c *gin.Context) {
return
}
out
:=
make
([]
dto
.
User
,
0
,
len
(
users
))
out
:=
make
([]
dto
.
Admin
User
,
0
,
len
(
users
))
for
i
:=
range
users
{
out
=
append
(
out
,
*
dto
.
UserFromService
(
&
users
[
i
]))
out
=
append
(
out
,
*
dto
.
UserFromService
Admin
(
&
users
[
i
]))
}
response
.
Paginated
(
c
,
out
,
total
,
page
,
pageSize
)
}
...
...
@@ -129,7 +129,7 @@ func (h *UserHandler) GetByID(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Create handles creating a new user
...
...
@@ -155,7 +155,7 @@ func (h *UserHandler) Create(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Update handles updating a user
...
...
@@ -189,7 +189,7 @@ func (h *UserHandler) Update(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// Delete handles deleting a user
...
...
@@ -231,7 +231,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
return
}
response
.
Success
(
c
,
dto
.
UserFromService
(
user
))
response
.
Success
(
c
,
dto
.
UserFromService
Admin
(
user
))
}
// GetUserAPIKeys handles getting user's API keys
...
...
backend/internal/handler/dto/mappers.go
View file @
c8e2f614
...
...
@@ -15,7 +15,6 @@ func UserFromServiceShallow(u *service.User) *User {
ID
:
u
.
ID
,
Email
:
u
.
Email
,
Username
:
u
.
Username
,
Notes
:
u
.
Notes
,
Role
:
u
.
Role
,
Balance
:
u
.
Balance
,
Concurrency
:
u
.
Concurrency
,
...
...
@@ -48,6 +47,22 @@ func UserFromService(u *service.User) *User {
return
out
}
// UserFromServiceAdmin converts a service User to DTO for admin users.
// It includes notes - user-facing endpoints must not use this.
func
UserFromServiceAdmin
(
u
*
service
.
User
)
*
AdminUser
{
if
u
==
nil
{
return
nil
}
base
:=
UserFromService
(
u
)
if
base
==
nil
{
return
nil
}
return
&
AdminUser
{
User
:
*
base
,
Notes
:
u
.
Notes
,
}
}
func
APIKeyFromService
(
k
*
service
.
APIKey
)
*
APIKey
{
if
k
==
nil
{
return
nil
...
...
@@ -72,36 +87,29 @@ func GroupFromServiceShallow(g *service.Group) *Group {
if
g
==
nil
{
return
nil
}
return
&
Group
{
ID
:
g
.
ID
,
Name
:
g
.
Name
,
Description
:
g
.
Description
,
Platform
:
g
.
Platform
,
RateMultiplier
:
g
.
RateMultiplier
,
IsExclusive
:
g
.
IsExclusive
,
Status
:
g
.
Status
,
SubscriptionType
:
g
.
SubscriptionType
,
DailyLimitUSD
:
g
.
DailyLimitUSD
,
WeeklyLimitUSD
:
g
.
WeeklyLimitUSD
,
MonthlyLimitUSD
:
g
.
MonthlyLimitUSD
,
ImagePrice1K
:
g
.
ImagePrice1K
,
ImagePrice2K
:
g
.
ImagePrice2K
,
ImagePrice4K
:
g
.
ImagePrice4K
,
ClaudeCodeOnly
:
g
.
ClaudeCodeOnly
,
FallbackGroupID
:
g
.
FallbackGroupID
,
ModelRouting
:
g
.
ModelRouting
,
ModelRoutingEnabled
:
g
.
ModelRoutingEnabled
,
CreatedAt
:
g
.
CreatedAt
,
UpdatedAt
:
g
.
UpdatedAt
,
AccountCount
:
g
.
AccountCount
,
}
out
:=
groupFromServiceBase
(
g
)
return
&
out
}
func
GroupFromService
(
g
*
service
.
Group
)
*
Group
{
if
g
==
nil
{
return
nil
}
out
:=
GroupFromServiceShallow
(
g
)
return
GroupFromServiceShallow
(
g
)
}
// GroupFromServiceAdmin converts a service Group to DTO for admin users.
// It includes internal fields like model_routing and account_count.
func
GroupFromServiceAdmin
(
g
*
service
.
Group
)
*
AdminGroup
{
if
g
==
nil
{
return
nil
}
out
:=
&
AdminGroup
{
Group
:
groupFromServiceBase
(
g
),
ModelRouting
:
g
.
ModelRouting
,
ModelRoutingEnabled
:
g
.
ModelRoutingEnabled
,
AccountCount
:
g
.
AccountCount
,
}
if
len
(
g
.
AccountGroups
)
>
0
{
out
.
AccountGroups
=
make
([]
AccountGroup
,
0
,
len
(
g
.
AccountGroups
))
for
i
:=
range
g
.
AccountGroups
{
...
...
@@ -112,6 +120,29 @@ func GroupFromService(g *service.Group) *Group {
return
out
}
func
groupFromServiceBase
(
g
*
service
.
Group
)
Group
{
return
Group
{
ID
:
g
.
ID
,
Name
:
g
.
Name
,
Description
:
g
.
Description
,
Platform
:
g
.
Platform
,
RateMultiplier
:
g
.
RateMultiplier
,
IsExclusive
:
g
.
IsExclusive
,
Status
:
g
.
Status
,
SubscriptionType
:
g
.
SubscriptionType
,
DailyLimitUSD
:
g
.
DailyLimitUSD
,
WeeklyLimitUSD
:
g
.
WeeklyLimitUSD
,
MonthlyLimitUSD
:
g
.
MonthlyLimitUSD
,
ImagePrice1K
:
g
.
ImagePrice1K
,
ImagePrice2K
:
g
.
ImagePrice2K
,
ImagePrice4K
:
g
.
ImagePrice4K
,
ClaudeCodeOnly
:
g
.
ClaudeCodeOnly
,
FallbackGroupID
:
g
.
FallbackGroupID
,
CreatedAt
:
g
.
CreatedAt
,
UpdatedAt
:
g
.
UpdatedAt
,
}
}
func
AccountFromServiceShallow
(
a
*
service
.
Account
)
*
Account
{
if
a
==
nil
{
return
nil
...
...
@@ -161,6 +192,16 @@ func AccountFromServiceShallow(a *service.Account) *Account {
if
idleTimeout
:=
a
.
GetSessionIdleTimeoutMinutes
();
idleTimeout
>
0
{
out
.
SessionIdleTimeoutMin
=
&
idleTimeout
}
// TLS指纹伪装开关
if
a
.
IsTLSFingerprintEnabled
()
{
enabled
:=
true
out
.
EnableTLSFingerprint
=
&
enabled
}
// 会话ID伪装开关
if
a
.
IsSessionIDMaskingEnabled
()
{
enabled
:=
true
out
.
EnableSessionIDMasking
=
&
enabled
}
}
return
out
...
...
@@ -263,7 +304,24 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
if
rc
==
nil
{
return
nil
}
return
&
RedeemCode
{
out
:=
redeemCodeFromServiceBase
(
rc
)
return
&
out
}
// RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
// It includes notes - user-facing endpoints must not use this.
func
RedeemCodeFromServiceAdmin
(
rc
*
service
.
RedeemCode
)
*
AdminRedeemCode
{
if
rc
==
nil
{
return
nil
}
return
&
AdminRedeemCode
{
RedeemCode
:
redeemCodeFromServiceBase
(
rc
),
Notes
:
rc
.
Notes
,
}
}
func
redeemCodeFromServiceBase
(
rc
*
service
.
RedeemCode
)
RedeemCode
{
return
RedeemCode
{
ID
:
rc
.
ID
,
Code
:
rc
.
Code
,
Type
:
rc
.
Type
,
...
...
@@ -271,7 +329,6 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
Status
:
rc
.
Status
,
UsedBy
:
rc
.
UsedBy
,
UsedAt
:
rc
.
UsedAt
,
Notes
:
rc
.
Notes
,
CreatedAt
:
rc
.
CreatedAt
,
GroupID
:
rc
.
GroupID
,
ValidityDays
:
rc
.
ValidityDays
,
...
...
@@ -292,14 +349,9 @@ func AccountSummaryFromService(a *service.Account) *AccountSummary {
}
}
// usageLogFromServiceBase is a helper that converts service UsageLog to DTO.
// The account parameter allows caller to control what Account info is included.
// The includeIPAddress parameter controls whether to include the IP address (admin-only).
func
usageLogFromServiceBase
(
l
*
service
.
UsageLog
,
account
*
AccountSummary
,
includeIPAddress
bool
)
*
UsageLog
{
if
l
==
nil
{
return
nil
}
result
:=
&
UsageLog
{
func
usageLogFromServiceUser
(
l
*
service
.
UsageLog
)
UsageLog
{
// 普通用户 DTO:严禁包含管理员字段(例如 account_rate_multiplier、ip_address、account)。
return
UsageLog
{
ID
:
l
.
ID
,
UserID
:
l
.
UserID
,
APIKeyID
:
l
.
APIKeyID
,
...
...
@@ -321,7 +373,6 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
TotalCost
:
l
.
TotalCost
,
ActualCost
:
l
.
ActualCost
,
RateMultiplier
:
l
.
RateMultiplier
,
AccountRateMultiplier
:
l
.
AccountRateMultiplier
,
BillingType
:
l
.
BillingType
,
Stream
:
l
.
Stream
,
DurationMs
:
l
.
DurationMs
,
...
...
@@ -332,30 +383,63 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
CreatedAt
:
l
.
CreatedAt
,
User
:
UserFromServiceShallow
(
l
.
User
),
APIKey
:
APIKeyFromService
(
l
.
APIKey
),
Account
:
account
,
Group
:
GroupFromServiceShallow
(
l
.
Group
),
Subscription
:
UserSubscriptionFromService
(
l
.
Subscription
),
}
// IP 地址仅对管理员可见
if
includeIPAddress
{
result
.
IPAddress
=
l
.
IPAddress
}
return
result
}
// UsageLogFromService converts a service UsageLog to DTO for regular users.
// It excludes Account details and IP address - users should not see these.
func
UsageLogFromService
(
l
*
service
.
UsageLog
)
*
UsageLog
{
return
usageLogFromServiceBase
(
l
,
nil
,
false
)
if
l
==
nil
{
return
nil
}
u
:=
usageLogFromServiceUser
(
l
)
return
&
u
}
// UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
// It includes minimal Account info (ID, Name only) and IP address.
func
UsageLogFromServiceAdmin
(
l
*
service
.
UsageLog
)
*
UsageLog
{
func
UsageLogFromServiceAdmin
(
l
*
service
.
UsageLog
)
*
Admin
UsageLog
{
if
l
==
nil
{
return
nil
}
return
usageLogFromServiceBase
(
l
,
AccountSummaryFromService
(
l
.
Account
),
true
)
return
&
AdminUsageLog
{
UsageLog
:
usageLogFromServiceUser
(
l
),
AccountRateMultiplier
:
l
.
AccountRateMultiplier
,
IPAddress
:
l
.
IPAddress
,
Account
:
AccountSummaryFromService
(
l
.
Account
),
}
}
func
UsageCleanupTaskFromService
(
task
*
service
.
UsageCleanupTask
)
*
UsageCleanupTask
{
if
task
==
nil
{
return
nil
}
return
&
UsageCleanupTask
{
ID
:
task
.
ID
,
Status
:
task
.
Status
,
Filters
:
UsageCleanupFilters
{
StartTime
:
task
.
Filters
.
StartTime
,
EndTime
:
task
.
Filters
.
EndTime
,
UserID
:
task
.
Filters
.
UserID
,
APIKeyID
:
task
.
Filters
.
APIKeyID
,
AccountID
:
task
.
Filters
.
AccountID
,
GroupID
:
task
.
Filters
.
GroupID
,
Model
:
task
.
Filters
.
Model
,
Stream
:
task
.
Filters
.
Stream
,
BillingType
:
task
.
Filters
.
BillingType
,
},
CreatedBy
:
task
.
CreatedBy
,
DeletedRows
:
task
.
DeletedRows
,
ErrorMessage
:
task
.
ErrorMsg
,
CanceledBy
:
task
.
CanceledBy
,
CanceledAt
:
task
.
CanceledAt
,
StartedAt
:
task
.
StartedAt
,
FinishedAt
:
task
.
FinishedAt
,
CreatedAt
:
task
.
CreatedAt
,
UpdatedAt
:
task
.
UpdatedAt
,
}
}
func
SettingFromService
(
s
*
service
.
Setting
)
*
Setting
{
...
...
@@ -374,7 +458,27 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
if
sub
==
nil
{
return
nil
}
return
&
UserSubscription
{
out
:=
userSubscriptionFromServiceBase
(
sub
)
return
&
out
}
// UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
// It includes assignment metadata and notes.
func
UserSubscriptionFromServiceAdmin
(
sub
*
service
.
UserSubscription
)
*
AdminUserSubscription
{
if
sub
==
nil
{
return
nil
}
return
&
AdminUserSubscription
{
UserSubscription
:
userSubscriptionFromServiceBase
(
sub
),
AssignedBy
:
sub
.
AssignedBy
,
AssignedAt
:
sub
.
AssignedAt
,
Notes
:
sub
.
Notes
,
AssignedByUser
:
UserFromServiceShallow
(
sub
.
AssignedByUser
),
}
}
func
userSubscriptionFromServiceBase
(
sub
*
service
.
UserSubscription
)
UserSubscription
{
return
UserSubscription
{
ID
:
sub
.
ID
,
UserID
:
sub
.
UserID
,
GroupID
:
sub
.
GroupID
,
...
...
@@ -387,14 +491,10 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
DailyUsageUSD
:
sub
.
DailyUsageUSD
,
WeeklyUsageUSD
:
sub
.
WeeklyUsageUSD
,
MonthlyUsageUSD
:
sub
.
MonthlyUsageUSD
,
AssignedBy
:
sub
.
AssignedBy
,
AssignedAt
:
sub
.
AssignedAt
,
Notes
:
sub
.
Notes
,
CreatedAt
:
sub
.
CreatedAt
,
UpdatedAt
:
sub
.
UpdatedAt
,
User
:
UserFromServiceShallow
(
sub
.
User
),
Group
:
GroupFromServiceShallow
(
sub
.
Group
),
AssignedByUser
:
UserFromServiceShallow
(
sub
.
AssignedByUser
),
}
}
...
...
@@ -402,9 +502,9 @@ func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult
if
r
==
nil
{
return
nil
}
subs
:=
make
([]
UserSubscription
,
0
,
len
(
r
.
Subscriptions
))
subs
:=
make
([]
Admin
UserSubscription
,
0
,
len
(
r
.
Subscriptions
))
for
i
:=
range
r
.
Subscriptions
{
subs
=
append
(
subs
,
*
UserSubscriptionFromService
(
&
r
.
Subscriptions
[
i
]))
subs
=
append
(
subs
,
*
UserSubscriptionFromService
Admin
(
&
r
.
Subscriptions
[
i
]))
}
return
&
BulkAssignResult
{
SuccessCount
:
r
.
SuccessCount
,
...
...
backend/internal/handler/dto/settings.go
View file @
c8e2f614
...
...
@@ -22,13 +22,14 @@ type SystemSettings struct {
LinuxDoConnectClientSecretConfigured
bool
`json:"linuxdo_connect_client_secret_configured"`
LinuxDoConnectRedirectURL
string
`json:"linuxdo_connect_redirect_url"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
SiteName
string
`json:"site_name"`
SiteLogo
string
`json:"site_logo"`
SiteSubtitle
string
`json:"site_subtitle"`
APIBaseURL
string
`json:"api_base_url"`
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
...
...
@@ -63,6 +64,7 @@ type PublicSettings struct {
ContactInfo
string
`json:"contact_info"`
DocURL
string
`json:"doc_url"`
HomeContent
string
`json:"home_content"`
HideCcsImportButton
bool
`json:"hide_ccs_import_button"`
LinuxDoOAuthEnabled
bool
`json:"linuxdo_oauth_enabled"`
Version
string
`json:"version"`
}
...
...
backend/internal/handler/dto/types.go
View file @
c8e2f614
...
...
@@ -6,7 +6,6 @@ type User struct {
ID
int64
`json:"id"`
Email
string
`json:"email"`
Username
string
`json:"username"`
Notes
string
`json:"notes"`
Role
string
`json:"role"`
Balance
float64
`json:"balance"`
Concurrency
int
`json:"concurrency"`
...
...
@@ -19,6 +18,14 @@ type User struct {
Subscriptions
[]
UserSubscription
`json:"subscriptions,omitempty"`
}
// AdminUser 是管理员接口使用的 user DTO(包含敏感/内部字段)。
// 注意:普通用户接口不得返回 notes 等管理员备注信息。
type
AdminUser
struct
{
User
Notes
string
`json:"notes"`
}
type
APIKey
struct
{
ID
int64
`json:"id"`
UserID
int64
`json:"user_id"`
...
...
@@ -58,13 +65,19 @@ type Group struct {
ClaudeCodeOnly
bool
`json:"claude_code_only"`
FallbackGroupID
*
int64
`json:"fallback_group_id"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
// AdminGroup 是管理员接口使用的 group DTO(包含敏感/内部字段)。
// 注意:普通用户接口不得返回 model_routing/account_count/account_groups 等内部信息。
type
AdminGroup
struct
{
Group
// 模型路由配置(仅 anthropic 平台使用)
ModelRouting
map
[
string
][]
int64
`json:"model_routing"`
ModelRoutingEnabled
bool
`json:"model_routing_enabled"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
AccountCount
int64
`json:"account_count,omitempty"`
}
...
...
@@ -112,6 +125,15 @@ type Account struct {
MaxSessions
*
int
`json:"max_sessions,omitempty"`
SessionIdleTimeoutMin
*
int
`json:"session_idle_timeout_minutes,omitempty"`
// TLS指纹伪装(仅 Anthropic OAuth/SetupToken 账号有效)
// 从 extra 字段提取,方便前端显示和编辑
EnableTLSFingerprint
*
bool
`json:"enable_tls_fingerprint,omitempty"`
// 会话ID伪装(仅 Anthropic OAuth/SetupToken 账号有效)
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
// 从 extra 字段提取,方便前端显示和编辑
EnableSessionIDMasking
*
bool
`json:"session_id_masking_enabled,omitempty"`
Proxy
*
Proxy
`json:"proxy,omitempty"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
...
...
@@ -171,7 +193,6 @@ type RedeemCode struct {
Status
string
`json:"status"`
UsedBy
*
int64
`json:"used_by"`
UsedAt
*
time
.
Time
`json:"used_at"`
Notes
string
`json:"notes"`
CreatedAt
time
.
Time
`json:"created_at"`
GroupID
*
int64
`json:"group_id"`
...
...
@@ -181,6 +202,15 @@ type RedeemCode struct {
Group
*
Group
`json:"group,omitempty"`
}
// AdminRedeemCode 是管理员接口使用的 redeem code DTO(包含 notes 等字段)。
// 注意:普通用户接口不得返回 notes 等内部信息。
type
AdminRedeemCode
struct
{
RedeemCode
Notes
string
`json:"notes"`
}
// UsageLog 是普通用户接口使用的 usage log DTO(不包含管理员字段)。
type
UsageLog
struct
{
ID
int64
`json:"id"`
UserID
int64
`json:"user_id"`
...
...
@@ -200,14 +230,13 @@ type UsageLog struct {
CacheCreation5mTokens
int
`json:"cache_creation_5m_tokens"`
CacheCreation1hTokens
int
`json:"cache_creation_1h_tokens"`
InputCost
float64
`json:"input_cost"`
OutputCost
float64
`json:"output_cost"`
CacheCreationCost
float64
`json:"cache_creation_cost"`
CacheReadCost
float64
`json:"cache_read_cost"`
TotalCost
float64
`json:"total_cost"`
ActualCost
float64
`json:"actual_cost"`
RateMultiplier
float64
`json:"rate_multiplier"`
AccountRateMultiplier
*
float64
`json:"account_rate_multiplier"`
InputCost
float64
`json:"input_cost"`
OutputCost
float64
`json:"output_cost"`
CacheCreationCost
float64
`json:"cache_creation_cost"`
CacheReadCost
float64
`json:"cache_read_cost"`
TotalCost
float64
`json:"total_cost"`
ActualCost
float64
`json:"actual_cost"`
RateMultiplier
float64
`json:"rate_multiplier"`
BillingType
int8
`json:"billing_type"`
Stream
bool
`json:"stream"`
...
...
@@ -221,18 +250,55 @@ type UsageLog struct {
// User-Agent
UserAgent
*
string
`json:"user_agent"`
// IP 地址(仅管理员可见)
IPAddress
*
string
`json:"ip_address,omitempty"`
CreatedAt
time
.
Time
`json:"created_at"`
User
*
User
`json:"user,omitempty"`
APIKey
*
APIKey
`json:"api_key,omitempty"`
Account
*
AccountSummary
`json:"account,omitempty"`
// Use minimal AccountSummary to prevent data leakage
Group
*
Group
`json:"group,omitempty"`
Subscription
*
UserSubscription
`json:"subscription,omitempty"`
}
// AdminUsageLog 是管理员接口使用的 usage log DTO(包含管理员字段)。
type
AdminUsageLog
struct
{
UsageLog
// AccountRateMultiplier 账号计费倍率快照(nil 表示按 1.0 处理)
AccountRateMultiplier
*
float64
`json:"account_rate_multiplier"`
// IPAddress 用户请求 IP(仅管理员可见)
IPAddress
*
string
`json:"ip_address,omitempty"`
// Account 最小账号信息(避免泄露敏感字段)
Account
*
AccountSummary
`json:"account,omitempty"`
}
type
UsageCleanupFilters
struct
{
StartTime
time
.
Time
`json:"start_time"`
EndTime
time
.
Time
`json:"end_time"`
UserID
*
int64
`json:"user_id,omitempty"`
APIKeyID
*
int64
`json:"api_key_id,omitempty"`
AccountID
*
int64
`json:"account_id,omitempty"`
GroupID
*
int64
`json:"group_id,omitempty"`
Model
*
string
`json:"model,omitempty"`
Stream
*
bool
`json:"stream,omitempty"`
BillingType
*
int8
`json:"billing_type,omitempty"`
}
type
UsageCleanupTask
struct
{
ID
int64
`json:"id"`
Status
string
`json:"status"`
Filters
UsageCleanupFilters
`json:"filters"`
CreatedBy
int64
`json:"created_by"`
DeletedRows
int64
`json:"deleted_rows"`
ErrorMessage
*
string
`json:"error_message,omitempty"`
CanceledBy
*
int64
`json:"canceled_by,omitempty"`
CanceledAt
*
time
.
Time
`json:"canceled_at,omitempty"`
StartedAt
*
time
.
Time
`json:"started_at,omitempty"`
FinishedAt
*
time
.
Time
`json:"finished_at,omitempty"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
}
// AccountSummary is a minimal account info for usage log display.
// It intentionally excludes sensitive fields like Credentials, Proxy, etc.
type
AccountSummary
struct
{
...
...
@@ -264,23 +330,30 @@ type UserSubscription struct {
WeeklyUsageUSD
float64
`json:"weekly_usage_usd"`
MonthlyUsageUSD
float64
`json:"monthly_usage_usd"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
User
*
User
`json:"user,omitempty"`
Group
*
Group
`json:"group,omitempty"`
}
// AdminUserSubscription 是管理员接口使用的订阅 DTO(包含分配信息/备注等字段)。
// 注意:普通用户接口不得返回 assigned_by/assigned_at/notes/assigned_by_user 等管理员字段。
type
AdminUserSubscription
struct
{
UserSubscription
AssignedBy
*
int64
`json:"assigned_by"`
AssignedAt
time
.
Time
`json:"assigned_at"`
Notes
string
`json:"notes"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
User
*
User
`json:"user,omitempty"`
Group
*
Group
`json:"group,omitempty"`
AssignedByUser
*
User
`json:"assigned_by_user,omitempty"`
AssignedByUser
*
User
`json:"assigned_by_user,omitempty"`
}
type
BulkAssignResult
struct
{
SuccessCount
int
`json:"success_count"`
FailedCount
int
`json:"failed_count"`
Subscriptions
[]
UserSubscription
`json:"subscriptions"`
Errors
[]
string
`json:"errors"`
SuccessCount
int
`json:"success_count"`
FailedCount
int
`json:"failed_count"`
Subscriptions
[]
Admin
UserSubscription
`json:"subscriptions"`
Errors
[]
string
`json:"errors"`
}
// PromoCode 注册优惠码
...
...
Prev
1
2
3
4
5
6
…
9
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