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
a04ae28a
Commit
a04ae28a
authored
Apr 13, 2026
by
陈曦
Browse files
merge v0.1.111
parents
68f67198
ad64190b
Changes
302
Show whitespace changes
Inline
Side-by-side
Too many changes to show.
To preserve performance only
302 of 302+
files are displayed.
Plain diff
Email patch
backend/ent/user/where.go
View file @
a04ae28a
...
...
@@ -1067,6 +1067,29 @@ func HasPromoCodeUsagesWith(preds ...predicate.PromoCodeUsage) predicate.User {
})
}
// HasPaymentOrders applies the HasEdge predicate on the "payment_orders" edge.
func
HasPaymentOrders
()
predicate
.
User
{
return
predicate
.
User
(
func
(
s
*
sql
.
Selector
)
{
step
:=
sqlgraph
.
NewStep
(
sqlgraph
.
From
(
Table
,
FieldID
),
sqlgraph
.
Edge
(
sqlgraph
.
O2M
,
false
,
PaymentOrdersTable
,
PaymentOrdersColumn
),
)
sqlgraph
.
HasNeighbors
(
s
,
step
)
})
}
// HasPaymentOrdersWith applies the HasEdge predicate on the "payment_orders" edge with a given conditions (other predicates).
func
HasPaymentOrdersWith
(
preds
...
predicate
.
PaymentOrder
)
predicate
.
User
{
return
predicate
.
User
(
func
(
s
*
sql
.
Selector
)
{
step
:=
newPaymentOrdersStep
()
sqlgraph
.
HasNeighborsWith
(
s
,
step
,
func
(
s
*
sql
.
Selector
)
{
for
_
,
p
:=
range
preds
{
p
(
s
)
}
})
})
}
// HasUserAllowedGroups applies the HasEdge predicate on the "user_allowed_groups" edge.
func
HasUserAllowedGroups
()
predicate
.
User
{
return
predicate
.
User
(
func
(
s
*
sql
.
Selector
)
{
...
...
backend/ent/user_create.go
View file @
a04ae28a
...
...
@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/announcementread"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/usagelog"
...
...
@@ -345,6 +346,21 @@ func (_c *UserCreate) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserCreate {
return
_c
.
AddPromoCodeUsageIDs
(
ids
...
)
}
// AddPaymentOrderIDs adds the "payment_orders" edge to the PaymentOrder entity by IDs.
func
(
_c
*
UserCreate
)
AddPaymentOrderIDs
(
ids
...
int64
)
*
UserCreate
{
_c
.
mutation
.
AddPaymentOrderIDs
(
ids
...
)
return
_c
}
// AddPaymentOrders adds the "payment_orders" edges to the PaymentOrder entity.
func
(
_c
*
UserCreate
)
AddPaymentOrders
(
v
...*
PaymentOrder
)
*
UserCreate
{
ids
:=
make
([]
int64
,
len
(
v
))
for
i
:=
range
v
{
ids
[
i
]
=
v
[
i
]
.
ID
}
return
_c
.
AddPaymentOrderIDs
(
ids
...
)
}
// Mutation returns the UserMutation object of the builder.
func
(
_c
*
UserCreate
)
Mutation
()
*
UserMutation
{
return
_c
.
mutation
...
...
@@ -718,6 +734,22 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) {
}
_spec
.
Edges
=
append
(
_spec
.
Edges
,
edge
)
}
if
nodes
:=
_c
.
mutation
.
PaymentOrdersIDs
();
len
(
nodes
)
>
0
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
for
_
,
k
:=
range
nodes
{
edge
.
Target
.
Nodes
=
append
(
edge
.
Target
.
Nodes
,
k
)
}
_spec
.
Edges
=
append
(
_spec
.
Edges
,
edge
)
}
return
_node
,
_spec
}
...
...
backend/ent/user_query.go
View file @
a04ae28a
...
...
@@ -16,6 +16,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/announcementread"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
...
...
@@ -42,6 +43,7 @@ type UserQuery struct {
withUsageLogs
*
UsageLogQuery
withAttributeValues
*
UserAttributeValueQuery
withPromoCodeUsages
*
PromoCodeUsageQuery
withPaymentOrders
*
PaymentOrderQuery
withUserAllowedGroups
*
UserAllowedGroupQuery
modifiers
[]
func
(
*
sql
.
Selector
)
// intermediate query (i.e. traversal path).
...
...
@@ -278,6 +280,28 @@ func (_q *UserQuery) QueryPromoCodeUsages() *PromoCodeUsageQuery {
return
query
}
// QueryPaymentOrders chains the current query on the "payment_orders" edge.
func
(
_q
*
UserQuery
)
QueryPaymentOrders
()
*
PaymentOrderQuery
{
query
:=
(
&
PaymentOrderClient
{
config
:
_q
.
config
})
.
Query
()
query
.
path
=
func
(
ctx
context
.
Context
)
(
fromU
*
sql
.
Selector
,
err
error
)
{
if
err
:=
_q
.
prepareQuery
(
ctx
);
err
!=
nil
{
return
nil
,
err
}
selector
:=
_q
.
sqlQuery
(
ctx
)
if
err
:=
selector
.
Err
();
err
!=
nil
{
return
nil
,
err
}
step
:=
sqlgraph
.
NewStep
(
sqlgraph
.
From
(
user
.
Table
,
user
.
FieldID
,
selector
),
sqlgraph
.
To
(
paymentorder
.
Table
,
paymentorder
.
FieldID
),
sqlgraph
.
Edge
(
sqlgraph
.
O2M
,
false
,
user
.
PaymentOrdersTable
,
user
.
PaymentOrdersColumn
),
)
fromU
=
sqlgraph
.
SetNeighbors
(
_q
.
driver
.
Dialect
(),
step
)
return
fromU
,
nil
}
return
query
}
// QueryUserAllowedGroups chains the current query on the "user_allowed_groups" edge.
func
(
_q
*
UserQuery
)
QueryUserAllowedGroups
()
*
UserAllowedGroupQuery
{
query
:=
(
&
UserAllowedGroupClient
{
config
:
_q
.
config
})
.
Query
()
...
...
@@ -501,6 +525,7 @@ func (_q *UserQuery) Clone() *UserQuery {
withUsageLogs
:
_q
.
withUsageLogs
.
Clone
(),
withAttributeValues
:
_q
.
withAttributeValues
.
Clone
(),
withPromoCodeUsages
:
_q
.
withPromoCodeUsages
.
Clone
(),
withPaymentOrders
:
_q
.
withPaymentOrders
.
Clone
(),
withUserAllowedGroups
:
_q
.
withUserAllowedGroups
.
Clone
(),
// clone intermediate query.
sql
:
_q
.
sql
.
Clone
(),
...
...
@@ -607,6 +632,17 @@ func (_q *UserQuery) WithPromoCodeUsages(opts ...func(*PromoCodeUsageQuery)) *Us
return
_q
}
// WithPaymentOrders tells the query-builder to eager-load the nodes that are connected to
// the "payment_orders" edge. The optional arguments are used to configure the query builder of the edge.
func
(
_q
*
UserQuery
)
WithPaymentOrders
(
opts
...
func
(
*
PaymentOrderQuery
))
*
UserQuery
{
query
:=
(
&
PaymentOrderClient
{
config
:
_q
.
config
})
.
Query
()
for
_
,
opt
:=
range
opts
{
opt
(
query
)
}
_q
.
withPaymentOrders
=
query
return
_q
}
// WithUserAllowedGroups tells the query-builder to eager-load the nodes that are connected to
// the "user_allowed_groups" edge. The optional arguments are used to configure the query builder of the edge.
func
(
_q
*
UserQuery
)
WithUserAllowedGroups
(
opts
...
func
(
*
UserAllowedGroupQuery
))
*
UserQuery
{
...
...
@@ -696,7 +732,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
var
(
nodes
=
[]
*
User
{}
_spec
=
_q
.
querySpec
()
loadedTypes
=
[
1
0
]
bool
{
loadedTypes
=
[
1
1
]
bool
{
_q
.
withAPIKeys
!=
nil
,
_q
.
withRedeemCodes
!=
nil
,
_q
.
withSubscriptions
!=
nil
,
...
...
@@ -706,6 +742,7 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
_q
.
withUsageLogs
!=
nil
,
_q
.
withAttributeValues
!=
nil
,
_q
.
withPromoCodeUsages
!=
nil
,
_q
.
withPaymentOrders
!=
nil
,
_q
.
withUserAllowedGroups
!=
nil
,
}
)
...
...
@@ -795,6 +832,13 @@ func (_q *UserQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*User, e
return
nil
,
err
}
}
if
query
:=
_q
.
withPaymentOrders
;
query
!=
nil
{
if
err
:=
_q
.
loadPaymentOrders
(
ctx
,
query
,
nodes
,
func
(
n
*
User
)
{
n
.
Edges
.
PaymentOrders
=
[]
*
PaymentOrder
{}
},
func
(
n
*
User
,
e
*
PaymentOrder
)
{
n
.
Edges
.
PaymentOrders
=
append
(
n
.
Edges
.
PaymentOrders
,
e
)
});
err
!=
nil
{
return
nil
,
err
}
}
if
query
:=
_q
.
withUserAllowedGroups
;
query
!=
nil
{
if
err
:=
_q
.
loadUserAllowedGroups
(
ctx
,
query
,
nodes
,
func
(
n
*
User
)
{
n
.
Edges
.
UserAllowedGroups
=
[]
*
UserAllowedGroup
{}
},
...
...
@@ -1112,6 +1156,36 @@ func (_q *UserQuery) loadPromoCodeUsages(ctx context.Context, query *PromoCodeUs
}
return
nil
}
func
(
_q
*
UserQuery
)
loadPaymentOrders
(
ctx
context
.
Context
,
query
*
PaymentOrderQuery
,
nodes
[]
*
User
,
init
func
(
*
User
),
assign
func
(
*
User
,
*
PaymentOrder
))
error
{
fks
:=
make
([]
driver
.
Value
,
0
,
len
(
nodes
))
nodeids
:=
make
(
map
[
int64
]
*
User
)
for
i
:=
range
nodes
{
fks
=
append
(
fks
,
nodes
[
i
]
.
ID
)
nodeids
[
nodes
[
i
]
.
ID
]
=
nodes
[
i
]
if
init
!=
nil
{
init
(
nodes
[
i
])
}
}
if
len
(
query
.
ctx
.
Fields
)
>
0
{
query
.
ctx
.
AppendFieldOnce
(
paymentorder
.
FieldUserID
)
}
query
.
Where
(
predicate
.
PaymentOrder
(
func
(
s
*
sql
.
Selector
)
{
s
.
Where
(
sql
.
InValues
(
s
.
C
(
user
.
PaymentOrdersColumn
),
fks
...
))
}))
neighbors
,
err
:=
query
.
All
(
ctx
)
if
err
!=
nil
{
return
err
}
for
_
,
n
:=
range
neighbors
{
fk
:=
n
.
UserID
node
,
ok
:=
nodeids
[
fk
]
if
!
ok
{
return
fmt
.
Errorf
(
`unexpected referenced foreign-key "user_id" returned %v for node %v`
,
fk
,
n
.
ID
)
}
assign
(
node
,
n
)
}
return
nil
}
func
(
_q
*
UserQuery
)
loadUserAllowedGroups
(
ctx
context
.
Context
,
query
*
UserAllowedGroupQuery
,
nodes
[]
*
User
,
init
func
(
*
User
),
assign
func
(
*
User
,
*
UserAllowedGroup
))
error
{
fks
:=
make
([]
driver
.
Value
,
0
,
len
(
nodes
))
nodeids
:=
make
(
map
[
int64
]
*
User
)
...
...
backend/ent/user_update.go
View file @
a04ae28a
...
...
@@ -14,6 +14,7 @@ import (
"github.com/Wei-Shaw/sub2api/ent/announcementread"
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
...
...
@@ -377,6 +378,21 @@ func (_u *UserUpdate) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserUpdate {
return
_u
.
AddPromoCodeUsageIDs
(
ids
...
)
}
// AddPaymentOrderIDs adds the "payment_orders" edge to the PaymentOrder entity by IDs.
func
(
_u
*
UserUpdate
)
AddPaymentOrderIDs
(
ids
...
int64
)
*
UserUpdate
{
_u
.
mutation
.
AddPaymentOrderIDs
(
ids
...
)
return
_u
}
// AddPaymentOrders adds the "payment_orders" edges to the PaymentOrder entity.
func
(
_u
*
UserUpdate
)
AddPaymentOrders
(
v
...*
PaymentOrder
)
*
UserUpdate
{
ids
:=
make
([]
int64
,
len
(
v
))
for
i
:=
range
v
{
ids
[
i
]
=
v
[
i
]
.
ID
}
return
_u
.
AddPaymentOrderIDs
(
ids
...
)
}
// Mutation returns the UserMutation object of the builder.
func
(
_u
*
UserUpdate
)
Mutation
()
*
UserMutation
{
return
_u
.
mutation
...
...
@@ -571,6 +587,27 @@ func (_u *UserUpdate) RemovePromoCodeUsages(v ...*PromoCodeUsage) *UserUpdate {
return
_u
.
RemovePromoCodeUsageIDs
(
ids
...
)
}
// ClearPaymentOrders clears all "payment_orders" edges to the PaymentOrder entity.
func
(
_u
*
UserUpdate
)
ClearPaymentOrders
()
*
UserUpdate
{
_u
.
mutation
.
ClearPaymentOrders
()
return
_u
}
// RemovePaymentOrderIDs removes the "payment_orders" edge to PaymentOrder entities by IDs.
func
(
_u
*
UserUpdate
)
RemovePaymentOrderIDs
(
ids
...
int64
)
*
UserUpdate
{
_u
.
mutation
.
RemovePaymentOrderIDs
(
ids
...
)
return
_u
}
// RemovePaymentOrders removes "payment_orders" edges to PaymentOrder entities.
func
(
_u
*
UserUpdate
)
RemovePaymentOrders
(
v
...*
PaymentOrder
)
*
UserUpdate
{
ids
:=
make
([]
int64
,
len
(
v
))
for
i
:=
range
v
{
ids
[
i
]
=
v
[
i
]
.
ID
}
return
_u
.
RemovePaymentOrderIDs
(
ids
...
)
}
// Save executes the query and returns the number of nodes affected by the update operation.
func
(
_u
*
UserUpdate
)
Save
(
ctx
context
.
Context
)
(
int
,
error
)
{
if
err
:=
_u
.
defaults
();
err
!=
nil
{
...
...
@@ -1126,6 +1163,51 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
}
_spec
.
Edges
.
Add
=
append
(
_spec
.
Edges
.
Add
,
edge
)
}
if
_u
.
mutation
.
PaymentOrdersCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
_spec
.
Edges
.
Clear
=
append
(
_spec
.
Edges
.
Clear
,
edge
)
}
if
nodes
:=
_u
.
mutation
.
RemovedPaymentOrdersIDs
();
len
(
nodes
)
>
0
&&
!
_u
.
mutation
.
PaymentOrdersCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
for
_
,
k
:=
range
nodes
{
edge
.
Target
.
Nodes
=
append
(
edge
.
Target
.
Nodes
,
k
)
}
_spec
.
Edges
.
Clear
=
append
(
_spec
.
Edges
.
Clear
,
edge
)
}
if
nodes
:=
_u
.
mutation
.
PaymentOrdersIDs
();
len
(
nodes
)
>
0
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
for
_
,
k
:=
range
nodes
{
edge
.
Target
.
Nodes
=
append
(
edge
.
Target
.
Nodes
,
k
)
}
_spec
.
Edges
.
Add
=
append
(
_spec
.
Edges
.
Add
,
edge
)
}
if
_node
,
err
=
sqlgraph
.
UpdateNodes
(
ctx
,
_u
.
driver
,
_spec
);
err
!=
nil
{
if
_
,
ok
:=
err
.
(
*
sqlgraph
.
NotFoundError
);
ok
{
err
=
&
NotFoundError
{
user
.
Label
}
...
...
@@ -1487,6 +1569,21 @@ func (_u *UserUpdateOne) AddPromoCodeUsages(v ...*PromoCodeUsage) *UserUpdateOne
return
_u
.
AddPromoCodeUsageIDs
(
ids
...
)
}
// AddPaymentOrderIDs adds the "payment_orders" edge to the PaymentOrder entity by IDs.
func
(
_u
*
UserUpdateOne
)
AddPaymentOrderIDs
(
ids
...
int64
)
*
UserUpdateOne
{
_u
.
mutation
.
AddPaymentOrderIDs
(
ids
...
)
return
_u
}
// AddPaymentOrders adds the "payment_orders" edges to the PaymentOrder entity.
func
(
_u
*
UserUpdateOne
)
AddPaymentOrders
(
v
...*
PaymentOrder
)
*
UserUpdateOne
{
ids
:=
make
([]
int64
,
len
(
v
))
for
i
:=
range
v
{
ids
[
i
]
=
v
[
i
]
.
ID
}
return
_u
.
AddPaymentOrderIDs
(
ids
...
)
}
// Mutation returns the UserMutation object of the builder.
func
(
_u
*
UserUpdateOne
)
Mutation
()
*
UserMutation
{
return
_u
.
mutation
...
...
@@ -1681,6 +1778,27 @@ func (_u *UserUpdateOne) RemovePromoCodeUsages(v ...*PromoCodeUsage) *UserUpdate
return
_u
.
RemovePromoCodeUsageIDs
(
ids
...
)
}
// ClearPaymentOrders clears all "payment_orders" edges to the PaymentOrder entity.
func
(
_u
*
UserUpdateOne
)
ClearPaymentOrders
()
*
UserUpdateOne
{
_u
.
mutation
.
ClearPaymentOrders
()
return
_u
}
// RemovePaymentOrderIDs removes the "payment_orders" edge to PaymentOrder entities by IDs.
func
(
_u
*
UserUpdateOne
)
RemovePaymentOrderIDs
(
ids
...
int64
)
*
UserUpdateOne
{
_u
.
mutation
.
RemovePaymentOrderIDs
(
ids
...
)
return
_u
}
// RemovePaymentOrders removes "payment_orders" edges to PaymentOrder entities.
func
(
_u
*
UserUpdateOne
)
RemovePaymentOrders
(
v
...*
PaymentOrder
)
*
UserUpdateOne
{
ids
:=
make
([]
int64
,
len
(
v
))
for
i
:=
range
v
{
ids
[
i
]
=
v
[
i
]
.
ID
}
return
_u
.
RemovePaymentOrderIDs
(
ids
...
)
}
// Where appends a list predicates to the UserUpdate builder.
func
(
_u
*
UserUpdateOne
)
Where
(
ps
...
predicate
.
User
)
*
UserUpdateOne
{
_u
.
mutation
.
Where
(
ps
...
)
...
...
@@ -2266,6 +2384,51 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
}
_spec
.
Edges
.
Add
=
append
(
_spec
.
Edges
.
Add
,
edge
)
}
if
_u
.
mutation
.
PaymentOrdersCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
_spec
.
Edges
.
Clear
=
append
(
_spec
.
Edges
.
Clear
,
edge
)
}
if
nodes
:=
_u
.
mutation
.
RemovedPaymentOrdersIDs
();
len
(
nodes
)
>
0
&&
!
_u
.
mutation
.
PaymentOrdersCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
for
_
,
k
:=
range
nodes
{
edge
.
Target
.
Nodes
=
append
(
edge
.
Target
.
Nodes
,
k
)
}
_spec
.
Edges
.
Clear
=
append
(
_spec
.
Edges
.
Clear
,
edge
)
}
if
nodes
:=
_u
.
mutation
.
PaymentOrdersIDs
();
len
(
nodes
)
>
0
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
Inverse
:
false
,
Table
:
user
.
PaymentOrdersTable
,
Columns
:
[]
string
{
user
.
PaymentOrdersColumn
},
Bidi
:
false
,
Target
:
&
sqlgraph
.
EdgeTarget
{
IDSpec
:
sqlgraph
.
NewFieldSpec
(
paymentorder
.
FieldID
,
field
.
TypeInt64
),
},
}
for
_
,
k
:=
range
nodes
{
edge
.
Target
.
Nodes
=
append
(
edge
.
Target
.
Nodes
,
k
)
}
_spec
.
Edges
.
Add
=
append
(
_spec
.
Edges
.
Add
,
edge
)
}
_node
=
&
User
{
config
:
_u
.
config
}
_spec
.
Assign
=
_node
.
assignValues
_spec
.
ScanValues
=
_node
.
scanValues
...
...
backend/go.mod
View file @
a04ae28a
...
...
@@ -27,12 +27,16 @@ require (
github.com/refraction-networking/utls v1.8.2
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.6
github.com/shopspring/decimal v1.4.0
github.com/smartwalle/alipay/v3 v3.2.29
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.11.1
github.com/stripe/stripe-go/v85 v85.0.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0
github.com/tidwall/gjson v1.18.0
github.com/tidwall/sjson v1.2.5
github.com/wechatpay-apiv3/wechatpay-go v0.2.21
github.com/zeromicro/go-zero v1.9.4
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.48.0
...
...
@@ -99,6 +103,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/subcommands v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
...
...
@@ -137,6 +142,9 @@ require (
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.1.0 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
...
...
@@ -167,6 +175,7 @@ require (
golang.org/x/mod v0.32.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
...
...
backend/go.sum
View file @
a04ae28a
...
...
@@ -14,6 +14,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/alitto/pond/v2 v2.6.2 h1:Sphe40g0ILeM1pA2c2K+Th0DGU+pt0A/Kprr+WB24Pw=
github.com/alitto/pond/v2 v2.6.2/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
...
...
@@ -160,6 +162,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
...
...
@@ -288,8 +292,18 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartwalle/alipay/v3 v3.2.29 h1:roGFqlml8hDa//0TpFmlyxZhndTYs7rbYLu/HlNFNJo=
github.com/smartwalle/alipay/v3 v3.2.29/go.mod h1:XarBLuAkwK3ah7mYjVtghRu+ysxzlex9sRkgqNMzMRU=
github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8=
github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
github.com/smartwalle/ngx v1.1.0 h1:q8nANgWSPRGeI/u+ixBoA4mf68DrUq6vZ+n9L5UKv9I=
github.com/smartwalle/ngx v1.1.0/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
...
...
@@ -317,6 +331,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stripe/stripe-go/v85 v85.0.0 h1:HMlFJXW6I/9WvkeSAtj8V7dI5pzeDu4gS1TaqR1ccI4=
github.com/stripe/stripe-go/v85 v85.0.0/go.mod h1:5P+HGFenpWgak27T5Is6JMsmDfUC1yJnjhhmquz7kXw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
...
...
@@ -342,6 +358,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs=
github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
...
...
backend/internal/config/config.go
View file @
a04ae28a
...
...
@@ -65,6 +65,7 @@ type Config struct {
JWT
JWTConfig
`mapstructure:"jwt"`
Totp
TotpConfig
`mapstructure:"totp"`
LinuxDo
LinuxDoConnectConfig
`mapstructure:"linuxdo_connect"`
OIDC
OIDCConnectConfig
`mapstructure:"oidc_connect"`
Default
DefaultConfig
`mapstructure:"default"`
RateLimit
RateLimitConfig
`mapstructure:"rate_limit"`
Pricing
PricingConfig
`mapstructure:"pricing"`
...
...
@@ -184,6 +185,34 @@ type LinuxDoConnectConfig struct {
UserInfoUsernamePath
string
`mapstructure:"userinfo_username_path"`
}
type
OIDCConnectConfig
struct
{
Enabled
bool
`mapstructure:"enabled"`
ProviderName
string
`mapstructure:"provider_name"`
// 显示名: "Keycloak" 等
ClientID
string
`mapstructure:"client_id"`
ClientSecret
string
`mapstructure:"client_secret"`
IssuerURL
string
`mapstructure:"issuer_url"`
DiscoveryURL
string
`mapstructure:"discovery_url"`
AuthorizeURL
string
`mapstructure:"authorize_url"`
TokenURL
string
`mapstructure:"token_url"`
UserInfoURL
string
`mapstructure:"userinfo_url"`
JWKSURL
string
`mapstructure:"jwks_url"`
Scopes
string
`mapstructure:"scopes"`
// 默认 "openid email profile"
RedirectURL
string
`mapstructure:"redirect_url"`
// 后端回调地址(需在提供方后台登记)
FrontendRedirectURL
string
`mapstructure:"frontend_redirect_url"`
// 前端接收 token 的路由(默认:/auth/oidc/callback)
TokenAuthMethod
string
`mapstructure:"token_auth_method"`
// client_secret_post / client_secret_basic / none
UsePKCE
bool
`mapstructure:"use_pkce"`
ValidateIDToken
bool
`mapstructure:"validate_id_token"`
AllowedSigningAlgs
string
`mapstructure:"allowed_signing_algs"`
// 默认 "RS256,ES256,PS256"
ClockSkewSeconds
int
`mapstructure:"clock_skew_seconds"`
// 默认 120
RequireEmailVerified
bool
`mapstructure:"require_email_verified"`
// 默认 false
// 可选:用于从 userinfo JSON 中提取字段的 gjson 路径。
// 为空时,服务端会尝试一组常见字段名。
UserInfoEmailPath
string
`mapstructure:"userinfo_email_path"`
UserInfoIDPath
string
`mapstructure:"userinfo_id_path"`
UserInfoUsernamePath
string
`mapstructure:"userinfo_username_path"`
}
// TokenRefreshConfig OAuth token自动刷新配置
type
TokenRefreshConfig
struct
{
// 是否启用自动刷新
...
...
@@ -318,6 +347,12 @@ type GatewayConfig struct {
// ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
// 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
ForceCodexCLI
bool
`mapstructure:"force_codex_cli"`
// ForcedCodexInstructionsTemplateFile: 服务端强制附加到 Codex 顶层 instructions 的模板文件路径。
// 模板渲染后会直接覆盖最终 instructions;若需要保留客户端 system 转换结果,请在模板中显式引用 {{ .ExistingInstructions }}。
ForcedCodexInstructionsTemplateFile
string
`mapstructure:"forced_codex_instructions_template_file"`
// ForcedCodexInstructionsTemplate: 启动时从模板文件读取并缓存的模板内容。
// 该字段不直接参与配置反序列化,仅用于请求热路径避免重复读盘。
ForcedCodexInstructionsTemplate
string
`mapstructure:"-"`
// OpenAIPassthroughAllowTimeoutHeaders: OpenAI 透传模式是否放行客户端超时头
// 关闭(默认)可避免 x-stainless-timeout 等头导致上游提前断流。
OpenAIPassthroughAllowTimeoutHeaders
bool
`mapstructure:"openai_passthrough_allow_timeout_headers"`
...
...
@@ -620,6 +655,10 @@ type GatewaySchedulingConfig struct {
// 负载计算
LoadBatchEnabled
bool
`mapstructure:"load_batch_enabled"`
// 快照桶读取时的 MGET 分块大小
SnapshotMGetChunkSize
int
`mapstructure:"snapshot_mget_chunk_size"`
// 快照重建时的缓存写入分块大小
SnapshotWriteChunkSize
int
`mapstructure:"snapshot_write_chunk_size"`
// 过期槽位清理周期(0 表示禁用)
SlotCleanupInterval
time
.
Duration
`mapstructure:"slot_cleanup_interval"`
...
...
@@ -968,6 +1007,23 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
cfg
.
LinuxDo
.
UserInfoEmailPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoEmailPath
)
cfg
.
LinuxDo
.
UserInfoIDPath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoIDPath
)
cfg
.
LinuxDo
.
UserInfoUsernamePath
=
strings
.
TrimSpace
(
cfg
.
LinuxDo
.
UserInfoUsernamePath
)
cfg
.
OIDC
.
ProviderName
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ProviderName
)
cfg
.
OIDC
.
ClientID
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ClientID
)
cfg
.
OIDC
.
ClientSecret
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
ClientSecret
)
cfg
.
OIDC
.
IssuerURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
IssuerURL
)
cfg
.
OIDC
.
DiscoveryURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
DiscoveryURL
)
cfg
.
OIDC
.
AuthorizeURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
AuthorizeURL
)
cfg
.
OIDC
.
TokenURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
TokenURL
)
cfg
.
OIDC
.
UserInfoURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoURL
)
cfg
.
OIDC
.
JWKSURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
JWKSURL
)
cfg
.
OIDC
.
Scopes
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
Scopes
)
cfg
.
OIDC
.
RedirectURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
RedirectURL
)
cfg
.
OIDC
.
FrontendRedirectURL
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
FrontendRedirectURL
)
cfg
.
OIDC
.
TokenAuthMethod
=
strings
.
ToLower
(
strings
.
TrimSpace
(
cfg
.
OIDC
.
TokenAuthMethod
))
cfg
.
OIDC
.
AllowedSigningAlgs
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
AllowedSigningAlgs
)
cfg
.
OIDC
.
UserInfoEmailPath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoEmailPath
)
cfg
.
OIDC
.
UserInfoIDPath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoIDPath
)
cfg
.
OIDC
.
UserInfoUsernamePath
=
strings
.
TrimSpace
(
cfg
.
OIDC
.
UserInfoUsernamePath
)
cfg
.
Dashboard
.
KeyPrefix
=
strings
.
TrimSpace
(
cfg
.
Dashboard
.
KeyPrefix
)
cfg
.
CORS
.
AllowedOrigins
=
normalizeStringSlice
(
cfg
.
CORS
.
AllowedOrigins
)
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
=
normalizeStringSlice
(
cfg
.
Security
.
ResponseHeaders
.
AdditionalAllowed
)
...
...
@@ -979,6 +1035,14 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
cfg
.
Log
.
Environment
=
strings
.
TrimSpace
(
cfg
.
Log
.
Environment
)
cfg
.
Log
.
StacktraceLevel
=
strings
.
ToLower
(
strings
.
TrimSpace
(
cfg
.
Log
.
StacktraceLevel
))
cfg
.
Log
.
Output
.
FilePath
=
strings
.
TrimSpace
(
cfg
.
Log
.
Output
.
FilePath
)
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
=
strings
.
TrimSpace
(
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
)
if
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
!=
""
{
content
,
err
:=
os
.
ReadFile
(
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"read forced codex instructions template %q: %w"
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
,
err
)
}
cfg
.
Gateway
.
ForcedCodexInstructionsTemplate
=
string
(
content
)
}
// 兼容旧键 gateway.openai_ws.sticky_previous_response_ttl_seconds。
// 新键未配置(<=0)时回退旧键;新键优先。
...
...
@@ -1138,6 +1202,30 @@ func setDefaults() {
viper
.
SetDefault
(
"linuxdo_connect.userinfo_id_path"
,
""
)
viper
.
SetDefault
(
"linuxdo_connect.userinfo_username_path"
,
""
)
// Generic OIDC OAuth 登录
viper
.
SetDefault
(
"oidc_connect.enabled"
,
false
)
viper
.
SetDefault
(
"oidc_connect.provider_name"
,
"OIDC"
)
viper
.
SetDefault
(
"oidc_connect.client_id"
,
""
)
viper
.
SetDefault
(
"oidc_connect.client_secret"
,
""
)
viper
.
SetDefault
(
"oidc_connect.issuer_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.discovery_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.authorize_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.token_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.jwks_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.scopes"
,
"openid email profile"
)
viper
.
SetDefault
(
"oidc_connect.redirect_url"
,
""
)
viper
.
SetDefault
(
"oidc_connect.frontend_redirect_url"
,
"/auth/oidc/callback"
)
viper
.
SetDefault
(
"oidc_connect.token_auth_method"
,
"client_secret_post"
)
viper
.
SetDefault
(
"oidc_connect.use_pkce"
,
false
)
viper
.
SetDefault
(
"oidc_connect.validate_id_token"
,
true
)
viper
.
SetDefault
(
"oidc_connect.allowed_signing_algs"
,
"RS256,ES256,PS256"
)
viper
.
SetDefault
(
"oidc_connect.clock_skew_seconds"
,
120
)
viper
.
SetDefault
(
"oidc_connect.require_email_verified"
,
false
)
viper
.
SetDefault
(
"oidc_connect.userinfo_email_path"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_id_path"
,
""
)
viper
.
SetDefault
(
"oidc_connect.userinfo_username_path"
,
""
)
// Database
viper
.
SetDefault
(
"database.host"
,
"localhost"
)
viper
.
SetDefault
(
"database.port"
,
5432
)
...
...
@@ -1340,6 +1428,8 @@ func setDefaults() {
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.snapshot_mget_chunk_size"
,
128
)
viper
.
SetDefault
(
"gateway.scheduling.snapshot_write_chunk_size"
,
256
)
viper
.
SetDefault
(
"gateway.scheduling.slot_cleanup_interval"
,
30
*
time
.
Second
)
viper
.
SetDefault
(
"gateway.scheduling.db_fallback_enabled"
,
true
)
viper
.
SetDefault
(
"gateway.scheduling.db_fallback_timeout_seconds"
,
0
)
...
...
@@ -1572,6 +1662,87 @@ func (c *Config) Validate() error {
warnIfInsecureURL
(
"linuxdo_connect.redirect_url"
,
c
.
LinuxDo
.
RedirectURL
)
warnIfInsecureURL
(
"linuxdo_connect.frontend_redirect_url"
,
c
.
LinuxDo
.
FrontendRedirectURL
)
}
if
c
.
OIDC
.
Enabled
{
if
strings
.
TrimSpace
(
c
.
OIDC
.
ClientID
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.client_id is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
IssuerURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.issuer_url is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
RedirectURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.redirect_url is required when oidc_connect.enabled=true"
)
}
if
strings
.
TrimSpace
(
c
.
OIDC
.
FrontendRedirectURL
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.frontend_redirect_url is required when oidc_connect.enabled=true"
)
}
if
!
scopeContainsOpenID
(
c
.
OIDC
.
Scopes
)
{
return
fmt
.
Errorf
(
"oidc_connect.scopes must contain openid"
)
}
method
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
c
.
OIDC
.
TokenAuthMethod
))
switch
method
{
case
""
,
"client_secret_post"
,
"client_secret_basic"
,
"none"
:
default
:
return
fmt
.
Errorf
(
"oidc_connect.token_auth_method must be one of: client_secret_post/client_secret_basic/none"
)
}
if
method
==
"none"
&&
!
c
.
OIDC
.
UsePKCE
{
return
fmt
.
Errorf
(
"oidc_connect.use_pkce must be true when oidc_connect.token_auth_method=none"
)
}
if
(
method
==
""
||
method
==
"client_secret_post"
||
method
==
"client_secret_basic"
)
&&
strings
.
TrimSpace
(
c
.
OIDC
.
ClientSecret
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.client_secret is required when oidc_connect.enabled=true and token_auth_method is client_secret_post/client_secret_basic"
)
}
if
c
.
OIDC
.
ClockSkewSeconds
<
0
||
c
.
OIDC
.
ClockSkewSeconds
>
600
{
return
fmt
.
Errorf
(
"oidc_connect.clock_skew_seconds must be between 0 and 600"
)
}
if
c
.
OIDC
.
ValidateIDToken
&&
strings
.
TrimSpace
(
c
.
OIDC
.
AllowedSigningAlgs
)
==
""
{
return
fmt
.
Errorf
(
"oidc_connect.allowed_signing_algs is required when oidc_connect.validate_id_token=true"
)
}
if
err
:=
ValidateAbsoluteHTTPURL
(
c
.
OIDC
.
IssuerURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.issuer_url invalid: %w"
,
err
)
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
DiscoveryURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.discovery_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
AuthorizeURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.authorize_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
TokenURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.token_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
UserInfoURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.userinfo_url invalid: %w"
,
err
)
}
}
if
v
:=
strings
.
TrimSpace
(
c
.
OIDC
.
JWKSURL
);
v
!=
""
{
if
err
:=
ValidateAbsoluteHTTPURL
(
v
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.jwks_url invalid: %w"
,
err
)
}
}
if
err
:=
ValidateAbsoluteHTTPURL
(
c
.
OIDC
.
RedirectURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.redirect_url invalid: %w"
,
err
)
}
if
err
:=
ValidateFrontendRedirectURL
(
c
.
OIDC
.
FrontendRedirectURL
);
err
!=
nil
{
return
fmt
.
Errorf
(
"oidc_connect.frontend_redirect_url invalid: %w"
,
err
)
}
warnIfInsecureURL
(
"oidc_connect.issuer_url"
,
c
.
OIDC
.
IssuerURL
)
warnIfInsecureURL
(
"oidc_connect.discovery_url"
,
c
.
OIDC
.
DiscoveryURL
)
warnIfInsecureURL
(
"oidc_connect.authorize_url"
,
c
.
OIDC
.
AuthorizeURL
)
warnIfInsecureURL
(
"oidc_connect.token_url"
,
c
.
OIDC
.
TokenURL
)
warnIfInsecureURL
(
"oidc_connect.userinfo_url"
,
c
.
OIDC
.
UserInfoURL
)
warnIfInsecureURL
(
"oidc_connect.jwks_url"
,
c
.
OIDC
.
JWKSURL
)
warnIfInsecureURL
(
"oidc_connect.redirect_url"
,
c
.
OIDC
.
RedirectURL
)
warnIfInsecureURL
(
"oidc_connect.frontend_redirect_url"
,
c
.
OIDC
.
FrontendRedirectURL
)
}
if
c
.
Billing
.
CircuitBreaker
.
Enabled
{
if
c
.
Billing
.
CircuitBreaker
.
FailureThreshold
<=
0
{
return
fmt
.
Errorf
(
"billing.circuit_breaker.failure_threshold must be positive"
)
...
...
@@ -2001,6 +2172,12 @@ func (c *Config) Validate() error {
if
c
.
Gateway
.
Scheduling
.
FallbackMaxWaiting
<=
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.fallback_max_waiting must be positive"
)
}
if
c
.
Gateway
.
Scheduling
.
SnapshotMGetChunkSize
<=
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.snapshot_mget_chunk_size must be positive"
)
}
if
c
.
Gateway
.
Scheduling
.
SnapshotWriteChunkSize
<=
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.snapshot_write_chunk_size must be positive"
)
}
if
c
.
Gateway
.
Scheduling
.
SlotCleanupInterval
<
0
{
return
fmt
.
Errorf
(
"gateway.scheduling.slot_cleanup_interval must be non-negative"
)
}
...
...
@@ -2184,6 +2361,15 @@ func ValidateFrontendRedirectURL(raw string) error {
return
nil
}
func
scopeContainsOpenID
(
scopes
string
)
bool
{
for
_
,
scope
:=
range
strings
.
Fields
(
strings
.
ToLower
(
strings
.
TrimSpace
(
scopes
)))
{
if
scope
==
"openid"
{
return
true
}
}
return
false
}
// isHTTPScheme 检查是否为 HTTP 或 HTTPS 协议
func
isHTTPScheme
(
scheme
string
)
bool
{
return
strings
.
EqualFold
(
scheme
,
"http"
)
||
strings
.
EqualFold
(
scheme
,
"https"
)
...
...
backend/internal/config/config_test.go
View file @
a04ae28a
package
config
import
(
"os"
"path/filepath"
"strings"
"testing"
"time"
...
...
@@ -223,6 +225,23 @@ func TestLoadSchedulingConfigFromEnv(t *testing.T) {
}
}
func
TestLoadForcedCodexInstructionsTemplate
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
tempDir
:=
t
.
TempDir
()
templatePath
:=
filepath
.
Join
(
tempDir
,
"codex-instructions.md.tmpl"
)
configPath
:=
filepath
.
Join
(
tempDir
,
"config.yaml"
)
require
.
NoError
(
t
,
os
.
WriteFile
(
templatePath
,
[]
byte
(
"server-prefix
\n\n
{{ .ExistingInstructions }}"
),
0
o644
))
require
.
NoError
(
t
,
os
.
WriteFile
(
configPath
,
[]
byte
(
"gateway:
\n
forced_codex_instructions_template_file:
\"
"
+
templatePath
+
"
\"\n
"
),
0
o644
))
t
.
Setenv
(
"DATA_DIR"
,
tempDir
)
cfg
,
err
:=
Load
()
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
templatePath
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplateFile
)
require
.
Equal
(
t
,
"server-prefix
\n\n
{{ .ExistingInstructions }}"
,
cfg
.
Gateway
.
ForcedCodexInstructionsTemplate
)
}
func
TestLoadDefaultSecurityToggles
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
...
...
@@ -351,6 +370,60 @@ func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
}
}
func
TestValidateOIDCScopesMustContainOpenID
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
OIDC
.
Enabled
=
true
cfg
.
OIDC
.
ClientID
=
"oidc-client"
cfg
.
OIDC
.
ClientSecret
=
"oidc-secret"
cfg
.
OIDC
.
IssuerURL
=
"https://issuer.example.com"
cfg
.
OIDC
.
AuthorizeURL
=
"https://issuer.example.com/auth"
cfg
.
OIDC
.
TokenURL
=
"https://issuer.example.com/token"
cfg
.
OIDC
.
JWKSURL
=
"https://issuer.example.com/jwks"
cfg
.
OIDC
.
RedirectURL
=
"https://example.com/api/v1/auth/oauth/oidc/callback"
cfg
.
OIDC
.
FrontendRedirectURL
=
"/auth/oidc/callback"
cfg
.
OIDC
.
Scopes
=
"profile email"
err
=
cfg
.
Validate
()
if
err
==
nil
{
t
.
Fatalf
(
"Validate() expected error when scopes do not include openid, got nil"
)
}
if
!
strings
.
Contains
(
err
.
Error
(),
"oidc_connect.scopes"
)
{
t
.
Fatalf
(
"Validate() expected oidc_connect.scopes error, got: %v"
,
err
)
}
}
func
TestValidateOIDCAllowsIssuerOnlyEndpointsWithDiscoveryFallback
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
cfg
,
err
:=
Load
()
if
err
!=
nil
{
t
.
Fatalf
(
"Load() error: %v"
,
err
)
}
cfg
.
OIDC
.
Enabled
=
true
cfg
.
OIDC
.
ClientID
=
"oidc-client"
cfg
.
OIDC
.
ClientSecret
=
"oidc-secret"
cfg
.
OIDC
.
IssuerURL
=
"https://issuer.example.com"
cfg
.
OIDC
.
AuthorizeURL
=
""
cfg
.
OIDC
.
TokenURL
=
""
cfg
.
OIDC
.
JWKSURL
=
""
cfg
.
OIDC
.
RedirectURL
=
"https://example.com/api/v1/auth/oauth/oidc/callback"
cfg
.
OIDC
.
FrontendRedirectURL
=
"/auth/oidc/callback"
cfg
.
OIDC
.
Scopes
=
"openid email profile"
cfg
.
OIDC
.
ValidateIDToken
=
true
err
=
cfg
.
Validate
()
if
err
!=
nil
{
t
.
Fatalf
(
"Validate() expected issuer-only OIDC config to pass with discovery fallback, got: %v"
,
err
)
}
}
func
TestLoadDefaultDashboardCacheConfig
(
t
*
testing
.
T
)
{
resetViperWithJWTSecret
(
t
)
...
...
backend/internal/domain/openai_messages_dispatch.go
0 → 100644
View file @
a04ae28a
package
domain
// OpenAIMessagesDispatchModelConfig controls how Anthropic /v1/messages
// requests are mapped onto OpenAI/Codex models.
type
OpenAIMessagesDispatchModelConfig
struct
{
OpusMappedModel
string
`json:"opus_mapped_model,omitempty"`
SonnetMappedModel
string
`json:"sonnet_mapped_model,omitempty"`
HaikuMappedModel
string
`json:"haiku_mapped_model,omitempty"`
ExactModelMappings
map
[
string
]
string
`json:"exact_model_mappings,omitempty"`
}
backend/internal/handler/admin/account_data.go
View file @
a04ae28a
...
...
@@ -10,6 +10,7 @@ import (
"log/slog"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
...
...
@@ -359,7 +360,7 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e
pageSize
:=
dataPageCap
var
out
[]
service
.
Proxy
for
{
items
,
total
,
err
:=
h
.
adminService
.
ListProxies
(
ctx
,
page
,
pageSize
,
""
,
""
,
""
)
items
,
total
,
err
:=
h
.
adminService
.
ListProxies
(
ctx
,
page
,
pageSize
,
""
,
""
,
""
,
"created_at"
,
"desc"
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -372,12 +373,12 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e
return
out
,
nil
}
func
(
h
*
AccountHandler
)
listAccountsFiltered
(
ctx
context
.
Context
,
platform
,
accountType
,
status
,
search
string
)
([]
service
.
Account
,
error
)
{
func
(
h
*
AccountHandler
)
listAccountsFiltered
(
ctx
context
.
Context
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
,
privacyMode
,
sortBy
,
sortOrder
string
)
([]
service
.
Account
,
error
)
{
page
:=
1
pageSize
:=
dataPageCap
var
out
[]
service
.
Account
for
{
items
,
total
,
err
:=
h
.
adminService
.
ListAccounts
(
ctx
,
page
,
pageSize
,
platform
,
accountType
,
status
,
search
,
0
,
""
)
items
,
total
,
err
:=
h
.
adminService
.
ListAccounts
(
ctx
,
page
,
pageSize
,
platform
,
accountType
,
status
,
search
,
groupID
,
privacyMode
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -409,11 +410,28 @@ func (h *AccountHandler) resolveExportAccounts(ctx context.Context, ids []int64,
platform
:=
c
.
Query
(
"platform"
)
accountType
:=
c
.
Query
(
"type"
)
status
:=
c
.
Query
(
"status"
)
privacyMode
:=
strings
.
TrimSpace
(
c
.
Query
(
"privacy_mode"
))
search
:=
strings
.
TrimSpace
(
c
.
Query
(
"search"
))
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"name"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"asc"
)
if
len
(
search
)
>
100
{
search
=
search
[
:
100
]
}
return
h
.
listAccountsFiltered
(
ctx
,
platform
,
accountType
,
status
,
search
)
groupID
:=
int64
(
0
)
if
groupIDStr
:=
c
.
Query
(
"group"
);
groupIDStr
!=
""
{
if
groupIDStr
==
accountListGroupUngroupedQueryValue
{
groupID
=
service
.
AccountListGroupUngrouped
}
else
{
parsedGroupID
,
parseErr
:=
strconv
.
ParseInt
(
groupIDStr
,
10
,
64
)
if
parseErr
!=
nil
||
parsedGroupID
<=
0
{
return
nil
,
infraerrors
.
BadRequest
(
"INVALID_GROUP_FILTER"
,
"invalid group filter"
)
}
groupID
=
parsedGroupID
}
}
return
h
.
listAccountsFiltered
(
ctx
,
platform
,
accountType
,
status
,
search
,
groupID
,
privacyMode
,
sortBy
,
sortOrder
)
}
func
(
h
*
AccountHandler
)
resolveExportProxies
(
ctx
context
.
Context
,
accounts
[]
service
.
Account
)
([]
service
.
Proxy
,
error
)
{
...
...
backend/internal/handler/admin/account_data_handler_test.go
View file @
a04ae28a
...
...
@@ -172,6 +172,51 @@ func TestExportDataWithoutProxies(t *testing.T) {
require
.
Nil
(
t
,
resp
.
Data
.
Accounts
[
0
]
.
ProxyKey
)
}
func
TestExportDataPassesAccountFiltersAndSort
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupAccountDataRouter
()
adminSvc
.
accounts
=
[]
service
.
Account
{
{
ID
:
1
,
Name
:
"acc-1"
,
Status
:
service
.
StatusActive
},
}
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&group=12&privacy_mode=blocked&search=keyword&sort_by=priority&sort_order=desc"
,
nil
,
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
adminSvc
.
lastListAccounts
.
calls
)
require
.
Equal
(
t
,
"openai"
,
adminSvc
.
lastListAccounts
.
platform
)
require
.
Equal
(
t
,
"oauth"
,
adminSvc
.
lastListAccounts
.
accountType
)
require
.
Equal
(
t
,
"active"
,
adminSvc
.
lastListAccounts
.
status
)
require
.
Equal
(
t
,
int64
(
12
),
adminSvc
.
lastListAccounts
.
groupID
)
require
.
Equal
(
t
,
"blocked"
,
adminSvc
.
lastListAccounts
.
privacyMode
)
require
.
Equal
(
t
,
"keyword"
,
adminSvc
.
lastListAccounts
.
search
)
require
.
Equal
(
t
,
"priority"
,
adminSvc
.
lastListAccounts
.
sortBy
)
require
.
Equal
(
t
,
"desc"
,
adminSvc
.
lastListAccounts
.
sortOrder
)
}
func
TestExportDataSelectedIDsOverrideFilters
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupAccountDataRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/accounts/data?ids=1,2&platform=openai&search=keyword&sort_by=priority&sort_order=desc"
,
nil
,
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
var
resp
dataResponse
require
.
NoError
(
t
,
json
.
Unmarshal
(
rec
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Len
(
t
,
resp
.
Data
.
Accounts
,
2
)
require
.
Equal
(
t
,
0
,
adminSvc
.
lastListAccounts
.
calls
)
}
func
TestImportDataReusesProxyAndSkipsDefaultGroup
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupAccountDataRouter
()
...
...
backend/internal/handler/admin/account_handler.go
View file @
a04ae28a
...
...
@@ -221,6 +221,8 @@ func (h *AccountHandler) List(c *gin.Context) {
status
:=
c
.
Query
(
"status"
)
search
:=
c
.
Query
(
"search"
)
privacyMode
:=
strings
.
TrimSpace
(
c
.
Query
(
"privacy_mode"
))
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"name"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"asc"
)
// 标准化和验证 search 参数
search
=
strings
.
TrimSpace
(
search
)
if
len
(
search
)
>
100
{
...
...
@@ -246,7 +248,7 @@ func (h *AccountHandler) List(c *gin.Context) {
}
}
accounts
,
total
,
err
:=
h
.
adminService
.
ListAccounts
(
c
.
Request
.
Context
(),
page
,
pageSize
,
platform
,
accountType
,
status
,
search
,
groupID
,
privacyMode
)
accounts
,
total
,
err
:=
h
.
adminService
.
ListAccounts
(
c
.
Request
.
Context
(),
page
,
pageSize
,
platform
,
accountType
,
status
,
search
,
groupID
,
privacyMode
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -2029,7 +2031,7 @@ func (h *AccountHandler) BatchRefreshTier(c *gin.Context) {
accounts
:=
make
([]
*
service
.
Account
,
0
)
if
len
(
req
.
AccountIDs
)
==
0
{
allAccounts
,
_
,
err
:=
h
.
adminService
.
ListAccounts
(
ctx
,
1
,
10000
,
"gemini"
,
"oauth"
,
""
,
""
,
0
,
""
)
allAccounts
,
_
,
err
:=
h
.
adminService
.
ListAccounts
(
ctx
,
1
,
10000
,
"gemini"
,
"oauth"
,
""
,
""
,
0
,
""
,
"name"
,
"asc"
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
a04ae28a
...
...
@@ -31,6 +31,33 @@ type stubAdminService struct {
platform
string
groupIDs
[]
int64
}
lastListAccounts
struct
{
platform
string
accountType
string
status
string
search
string
groupID
int64
privacyMode
string
sortBy
string
sortOrder
string
calls
int
}
lastListProxies
struct
{
protocol
string
status
string
search
string
sortBy
string
sortOrder
string
calls
int
}
lastListRedeemCodes
struct
{
codeType
string
status
string
search
string
sortBy
string
sortOrder
string
calls
int
}
mu
sync
.
Mutex
}
...
...
@@ -99,7 +126,7 @@ func newStubAdminService() *stubAdminService {
}
}
func
(
s
*
stubAdminService
)
ListUsers
(
ctx
context
.
Context
,
page
,
pageSize
int
,
filters
service
.
UserListFilters
)
([]
service
.
User
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
ListUsers
(
ctx
context
.
Context
,
page
,
pageSize
int
,
filters
service
.
UserListFilters
,
sortBy
,
sortOrder
string
)
([]
service
.
User
,
int64
,
error
)
{
return
s
.
users
,
int64
(
len
(
s
.
users
)),
nil
}
...
...
@@ -132,7 +159,7 @@ func (s *stubAdminService) UpdateUserBalance(ctx context.Context, userID int64,
return
&
user
,
nil
}
func
(
s
*
stubAdminService
)
GetUserAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
page
,
pageSize
int
)
([]
service
.
APIKey
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
GetUserAPIKeys
(
ctx
context
.
Context
,
userID
int64
,
page
,
pageSize
int
,
sortBy
,
sortOrder
string
)
([]
service
.
APIKey
,
int64
,
error
)
{
return
s
.
apiKeys
,
int64
(
len
(
s
.
apiKeys
)),
nil
}
...
...
@@ -140,7 +167,7 @@ func (s *stubAdminService) GetUserUsageStats(ctx context.Context, userID int64,
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
)
{
func
(
s
*
stubAdminService
)
ListGroups
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
status
,
search
string
,
isExclusive
*
bool
,
sortBy
,
sortOrder
string
)
([]
service
.
Group
,
int64
,
error
)
{
return
s
.
groups
,
int64
(
len
(
s
.
groups
)),
nil
}
...
...
@@ -187,7 +214,16 @@ func (s *stubAdminService) BatchSetGroupRateMultipliers(_ context.Context, _ int
return
nil
}
func
(
s
*
stubAdminService
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
,
privacyMode
string
)
([]
service
.
Account
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
,
groupID
int64
,
privacyMode
string
,
sortBy
,
sortOrder
string
)
([]
service
.
Account
,
int64
,
error
)
{
s
.
lastListAccounts
.
platform
=
platform
s
.
lastListAccounts
.
accountType
=
accountType
s
.
lastListAccounts
.
status
=
status
s
.
lastListAccounts
.
search
=
search
s
.
lastListAccounts
.
groupID
=
groupID
s
.
lastListAccounts
.
privacyMode
=
privacyMode
s
.
lastListAccounts
.
sortBy
=
sortBy
s
.
lastListAccounts
.
sortOrder
=
sortOrder
s
.
lastListAccounts
.
calls
++
return
s
.
accounts
,
int64
(
len
(
s
.
accounts
)),
nil
}
...
...
@@ -261,7 +297,13 @@ func (s *stubAdminService) CheckMixedChannelRisk(ctx context.Context, currentAcc
return
s
.
checkMixedErr
}
func
(
s
*
stubAdminService
)
ListProxies
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
)
([]
service
.
Proxy
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
ListProxies
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
,
sortBy
,
sortOrder
string
)
([]
service
.
Proxy
,
int64
,
error
)
{
s
.
lastListProxies
.
protocol
=
protocol
s
.
lastListProxies
.
status
=
status
s
.
lastListProxies
.
search
=
search
s
.
lastListProxies
.
sortBy
=
sortBy
s
.
lastListProxies
.
sortOrder
=
sortOrder
s
.
lastListProxies
.
calls
++
search
=
strings
.
TrimSpace
(
strings
.
ToLower
(
search
))
filtered
:=
make
([]
service
.
Proxy
,
0
,
len
(
s
.
proxies
))
for
_
,
proxy
:=
range
s
.
proxies
{
...
...
@@ -283,7 +325,7 @@ func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int,
return
filtered
,
int64
(
len
(
filtered
)),
nil
}
func
(
s
*
stubAdminService
)
ListProxiesWithAccountCount
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
)
([]
service
.
ProxyWithAccountCount
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
ListProxiesWithAccountCount
(
ctx
context
.
Context
,
page
,
pageSize
int
,
protocol
,
status
,
search
string
,
sortBy
,
sortOrder
string
)
([]
service
.
ProxyWithAccountCount
,
int64
,
error
)
{
return
s
.
proxyCounts
,
int64
(
len
(
s
.
proxyCounts
)),
nil
}
...
...
@@ -384,7 +426,13 @@ func (s *stubAdminService) CheckProxyQuality(ctx context.Context, id int64) (*se
},
nil
}
func
(
s
*
stubAdminService
)
ListRedeemCodes
(
ctx
context
.
Context
,
page
,
pageSize
int
,
codeType
,
status
,
search
string
)
([]
service
.
RedeemCode
,
int64
,
error
)
{
func
(
s
*
stubAdminService
)
ListRedeemCodes
(
ctx
context
.
Context
,
page
,
pageSize
int
,
codeType
,
status
,
search
string
,
sortBy
,
sortOrder
string
)
([]
service
.
RedeemCode
,
int64
,
error
)
{
s
.
lastListRedeemCodes
.
codeType
=
codeType
s
.
lastListRedeemCodes
.
status
=
status
s
.
lastListRedeemCodes
.
search
=
search
s
.
lastListRedeemCodes
.
sortBy
=
sortBy
s
.
lastListRedeemCodes
.
sortOrder
=
sortOrder
s
.
lastListRedeemCodes
.
calls
++
return
s
.
redeems
,
int64
(
len
(
s
.
redeems
)),
nil
}
...
...
backend/internal/handler/admin/announcement_handler.go
View file @
a04ae28a
...
...
@@ -52,6 +52,8 @@ func (h *AnnouncementHandler) List(c *gin.Context) {
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
status
:=
strings
.
TrimSpace
(
c
.
Query
(
"status"
))
search
:=
strings
.
TrimSpace
(
c
.
Query
(
"search"
))
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"created_at"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"desc"
)
if
len
(
search
)
>
200
{
search
=
search
[
:
200
]
}
...
...
@@ -59,6 +61,8 @@ func (h *AnnouncementHandler) List(c *gin.Context) {
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
sortBy
,
SortOrder
:
sortOrder
,
}
items
,
paginationResult
,
err
:=
h
.
announcementService
.
List
(
...
...
@@ -229,6 +233,8 @@ func (h *AnnouncementHandler) ListReadStatus(c *gin.Context) {
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
c
.
DefaultQuery
(
"sort_by"
,
"email"
),
SortOrder
:
c
.
DefaultQuery
(
"sort_order"
,
"asc"
),
}
search
:=
strings
.
TrimSpace
(
c
.
Query
(
"search"
))
if
len
(
search
)
>
200
{
...
...
backend/internal/handler/admin/announcement_handler_sort_test.go
0 → 100644
View file @
a04ae28a
package
admin
import
(
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type
announcementRepoCapture
struct
{
service
.
AnnouncementRepository
listParams
pagination
.
PaginationParams
}
func
(
r
*
announcementRepoCapture
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
filters
service
.
AnnouncementListFilters
)
([]
service
.
Announcement
,
*
pagination
.
PaginationResult
,
error
)
{
r
.
listParams
=
params
return
[]
service
.
Announcement
{},
&
pagination
.
PaginationResult
{
Total
:
0
,
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
Pages
:
0
,
},
nil
}
func
(
r
*
announcementRepoCapture
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
Announcement
,
error
)
{
return
&
service
.
Announcement
{
ID
:
id
,
Title
:
"announcement"
,
Content
:
"content"
,
Status
:
service
.
AnnouncementStatusActive
,
CreatedAt
:
time
.
Now
(),
UpdatedAt
:
time
.
Now
(),
},
nil
}
type
announcementUserRepoCapture
struct
{
service
.
UserRepository
listParams
pagination
.
PaginationParams
}
func
(
r
*
announcementUserRepoCapture
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
filters
service
.
UserListFilters
)
([]
service
.
User
,
*
pagination
.
PaginationResult
,
error
)
{
r
.
listParams
=
params
return
[]
service
.
User
{},
&
pagination
.
PaginationResult
{
Total
:
0
,
Page
:
params
.
Page
,
PageSize
:
params
.
PageSize
,
Pages
:
0
,
},
nil
}
type
announcementReadRepoCapture
struct
{
service
.
AnnouncementReadRepository
}
func
(
r
*
announcementReadRepoCapture
)
GetReadMapByUsers
(
ctx
context
.
Context
,
announcementID
int64
,
userIDs
[]
int64
)
(
map
[
int64
]
time
.
Time
,
error
)
{
return
map
[
int64
]
time
.
Time
{},
nil
}
type
announcementUserSubRepoCapture
struct
{
service
.
UserSubscriptionRepository
}
func
newAnnouncementSortTestRouter
(
announcementRepo
*
announcementRepoCapture
,
userRepo
*
announcementUserRepoCapture
)
*
gin
.
Engine
{
gin
.
SetMode
(
gin
.
TestMode
)
svc
:=
service
.
NewAnnouncementService
(
announcementRepo
,
&
announcementReadRepoCapture
{},
userRepo
,
&
announcementUserSubRepoCapture
{},
)
handler
:=
NewAnnouncementHandler
(
svc
)
router
:=
gin
.
New
()
router
.
GET
(
"/admin/announcements"
,
handler
.
List
)
router
.
GET
(
"/admin/announcements/:id/read-status"
,
handler
.
ListReadStatus
)
return
router
}
func
TestAdminAnnouncementListSortParams
(
t
*
testing
.
T
)
{
announcementRepo
:=
&
announcementRepoCapture
{}
userRepo
:=
&
announcementUserRepoCapture
{}
router
:=
newAnnouncementSortTestRouter
(
announcementRepo
,
userRepo
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/announcements?sort_by=title&sort_order=ASC"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
"title"
,
announcementRepo
.
listParams
.
SortBy
)
require
.
Equal
(
t
,
"ASC"
,
announcementRepo
.
listParams
.
SortOrder
)
}
func
TestAdminAnnouncementListSortDefaults
(
t
*
testing
.
T
)
{
announcementRepo
:=
&
announcementRepoCapture
{}
userRepo
:=
&
announcementUserRepoCapture
{}
router
:=
newAnnouncementSortTestRouter
(
announcementRepo
,
userRepo
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/announcements"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
"created_at"
,
announcementRepo
.
listParams
.
SortBy
)
require
.
Equal
(
t
,
"desc"
,
announcementRepo
.
listParams
.
SortOrder
)
}
func
TestAdminAnnouncementReadStatusSortParams
(
t
*
testing
.
T
)
{
announcementRepo
:=
&
announcementRepoCapture
{}
userRepo
:=
&
announcementUserRepoCapture
{}
router
:=
newAnnouncementSortTestRouter
(
announcementRepo
,
userRepo
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/announcements/1/read-status?sort_by=balance&sort_order=DESC"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
"balance"
,
userRepo
.
listParams
.
SortBy
)
require
.
Equal
(
t
,
"DESC"
,
userRepo
.
listParams
.
SortOrder
)
}
func
TestAdminAnnouncementReadStatusSortDefaults
(
t
*
testing
.
T
)
{
announcementRepo
:=
&
announcementRepoCapture
{}
userRepo
:=
&
announcementUserRepoCapture
{}
router
:=
newAnnouncementSortTestRouter
(
announcementRepo
,
userRepo
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/announcements/1/read-status"
,
nil
)
rec
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
"email"
,
userRepo
.
listParams
.
SortBy
)
require
.
Equal
(
t
,
"asc"
,
userRepo
.
listParams
.
SortOrder
)
}
backend/internal/handler/admin/channel_handler.go
View file @
a04ae28a
...
...
@@ -245,7 +245,12 @@ func (h *ChannelHandler) List(c *gin.Context) {
search
=
search
[
:
100
]
}
channels
,
pag
,
err
:=
h
.
channelService
.
List
(
c
.
Request
.
Context
(),
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
},
status
,
search
)
channels
,
pag
,
err
:=
h
.
channelService
.
List
(
c
.
Request
.
Context
(),
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
c
.
DefaultQuery
(
"sort_by"
,
"created_at"
),
SortOrder
:
c
.
DefaultQuery
(
"sort_order"
,
"desc"
),
},
status
,
search
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
backend/internal/handler/admin/group_handler.go
View file @
a04ae28a
...
...
@@ -109,6 +109,7 @@ type CreateGroupRequest struct {
RequireOAuthOnly
bool
`json:"require_oauth_only"`
RequirePrivacySet
bool
`json:"require_privacy_set"`
DefaultMappedModel
string
`json:"default_mapped_model"`
MessagesDispatchModelConfig
service
.
OpenAIMessagesDispatchModelConfig
`json:"messages_dispatch_model_config"`
// 从指定分组复制账号(创建后自动绑定)
CopyAccountsFromGroupIDs
[]
int64
`json:"copy_accounts_from_group_ids"`
}
...
...
@@ -143,6 +144,7 @@ type UpdateGroupRequest struct {
RequireOAuthOnly
*
bool
`json:"require_oauth_only"`
RequirePrivacySet
*
bool
`json:"require_privacy_set"`
DefaultMappedModel
*
string
`json:"default_mapped_model"`
MessagesDispatchModelConfig
*
service
.
OpenAIMessagesDispatchModelConfig
`json:"messages_dispatch_model_config"`
// 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
CopyAccountsFromGroupIDs
[]
int64
`json:"copy_accounts_from_group_ids"`
}
...
...
@@ -160,6 +162,8 @@ func (h *GroupHandler) List(c *gin.Context) {
search
=
search
[
:
100
]
}
isExclusiveStr
:=
c
.
Query
(
"is_exclusive"
)
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"sort_order"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"asc"
)
var
isExclusive
*
bool
if
isExclusiveStr
!=
""
{
...
...
@@ -167,7 +171,7 @@ func (h *GroupHandler) List(c *gin.Context) {
isExclusive
=
&
val
}
groups
,
total
,
err
:=
h
.
adminService
.
ListGroups
(
c
.
Request
.
Context
(),
page
,
pageSize
,
platform
,
status
,
search
,
isExclusive
)
groups
,
total
,
err
:=
h
.
adminService
.
ListGroups
(
c
.
Request
.
Context
(),
page
,
pageSize
,
platform
,
status
,
search
,
isExclusive
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -257,6 +261,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
RequireOAuthOnly
:
req
.
RequireOAuthOnly
,
RequirePrivacySet
:
req
.
RequirePrivacySet
,
DefaultMappedModel
:
req
.
DefaultMappedModel
,
MessagesDispatchModelConfig
:
req
.
MessagesDispatchModelConfig
,
CopyAccountsFromGroupIDs
:
req
.
CopyAccountsFromGroupIDs
,
})
if
err
!=
nil
{
...
...
@@ -307,6 +312,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
RequireOAuthOnly
:
req
.
RequireOAuthOnly
,
RequirePrivacySet
:
req
.
RequirePrivacySet
,
DefaultMappedModel
:
req
.
DefaultMappedModel
,
MessagesDispatchModelConfig
:
req
.
MessagesDispatchModelConfig
,
CopyAccountsFromGroupIDs
:
req
.
CopyAccountsFromGroupIDs
,
})
if
err
!=
nil
{
...
...
backend/internal/handler/admin/payment_handler.go
0 → 100644
View file @
a04ae28a
package
admin
import
(
"strconv"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// PaymentHandler handles admin payment management.
type
PaymentHandler
struct
{
paymentService
*
service
.
PaymentService
configService
*
service
.
PaymentConfigService
}
// NewPaymentHandler creates a new admin PaymentHandler.
func
NewPaymentHandler
(
paymentService
*
service
.
PaymentService
,
configService
*
service
.
PaymentConfigService
)
*
PaymentHandler
{
return
&
PaymentHandler
{
paymentService
:
paymentService
,
configService
:
configService
,
}
}
// --- Dashboard ---
// GetDashboard returns payment dashboard statistics.
// GET /api/v1/admin/payment/dashboard
func
(
h
*
PaymentHandler
)
GetDashboard
(
c
*
gin
.
Context
)
{
days
:=
30
if
d
:=
c
.
Query
(
"days"
);
d
!=
""
{
if
v
,
err
:=
strconv
.
Atoi
(
d
);
err
==
nil
&&
v
>
0
{
days
=
v
}
}
stats
,
err
:=
h
.
paymentService
.
GetDashboardStats
(
c
.
Request
.
Context
(),
days
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
stats
)
}
// --- Orders ---
// ListOrders returns a paginated list of all payment orders.
// GET /api/v1/admin/payment/orders
func
(
h
*
PaymentHandler
)
ListOrders
(
c
*
gin
.
Context
)
{
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
var
userID
int64
if
uid
:=
c
.
Query
(
"user_id"
);
uid
!=
""
{
if
v
,
err
:=
strconv
.
ParseInt
(
uid
,
10
,
64
);
err
==
nil
{
userID
=
v
}
}
orders
,
total
,
err
:=
h
.
paymentService
.
AdminListOrders
(
c
.
Request
.
Context
(),
userID
,
service
.
OrderListParams
{
Page
:
page
,
PageSize
:
pageSize
,
Status
:
c
.
Query
(
"status"
),
OrderType
:
c
.
Query
(
"order_type"
),
PaymentType
:
c
.
Query
(
"payment_type"
),
Keyword
:
c
.
Query
(
"keyword"
),
})
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Paginated
(
c
,
orders
,
int64
(
total
),
page
,
pageSize
)
}
// GetOrderDetail returns detailed information about a single order.
// GET /api/v1/admin/payment/orders/:id
func
(
h
*
PaymentHandler
)
GetOrderDetail
(
c
*
gin
.
Context
)
{
orderID
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
order
,
err
:=
h
.
paymentService
.
GetOrderByID
(
c
.
Request
.
Context
(),
orderID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
auditLogs
,
_
:=
h
.
paymentService
.
GetOrderAuditLogs
(
c
.
Request
.
Context
(),
orderID
)
response
.
Success
(
c
,
gin
.
H
{
"order"
:
order
,
"auditLogs"
:
auditLogs
})
}
// CancelOrder cancels a pending order (admin).
// POST /api/v1/admin/payment/orders/:id/cancel
func
(
h
*
PaymentHandler
)
CancelOrder
(
c
*
gin
.
Context
)
{
orderID
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
msg
,
err
:=
h
.
paymentService
.
AdminCancelOrder
(
c
.
Request
.
Context
(),
orderID
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
msg
})
}
// RetryFulfillment retries fulfillment for a paid order.
// POST /api/v1/admin/payment/orders/:id/retry
func
(
h
*
PaymentHandler
)
RetryFulfillment
(
c
*
gin
.
Context
)
{
orderID
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
if
err
:=
h
.
paymentService
.
RetryFulfillment
(
c
.
Request
.
Context
(),
orderID
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"fulfillment retried"
})
}
// AdminProcessRefundRequest is the request body for admin refund processing.
type
AdminProcessRefundRequest
struct
{
Amount
float64
`json:"amount"`
Reason
string
`json:"reason"`
Force
bool
`json:"force"`
DeductBalance
bool
`json:"deduct_balance"`
}
// ProcessRefund processes a refund for an order (admin).
// POST /api/v1/admin/payment/orders/:id/refund
func
(
h
*
PaymentHandler
)
ProcessRefund
(
c
*
gin
.
Context
)
{
orderID
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
var
req
AdminProcessRefundRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
plan
,
earlyResult
,
err
:=
h
.
paymentService
.
PrepareRefund
(
c
.
Request
.
Context
(),
orderID
,
req
.
Amount
,
req
.
Reason
,
req
.
Force
,
req
.
DeductBalance
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
earlyResult
!=
nil
{
response
.
Success
(
c
,
earlyResult
)
return
}
result
,
err
:=
h
.
paymentService
.
ExecuteRefund
(
c
.
Request
.
Context
(),
plan
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
result
)
}
// --- Subscription Plans ---
// ListPlans returns all subscription plans.
// GET /api/v1/admin/payment/plans
func
(
h
*
PaymentHandler
)
ListPlans
(
c
*
gin
.
Context
)
{
plans
,
err
:=
h
.
configService
.
ListPlans
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
plans
)
}
// CreatePlan creates a new subscription plan.
// POST /api/v1/admin/payment/plans
func
(
h
*
PaymentHandler
)
CreatePlan
(
c
*
gin
.
Context
)
{
var
req
service
.
CreatePlanRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
plan
,
err
:=
h
.
configService
.
CreatePlan
(
c
.
Request
.
Context
(),
req
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Created
(
c
,
plan
)
}
// UpdatePlan updates an existing subscription plan.
// PUT /api/v1/admin/payment/plans/:id
func
(
h
*
PaymentHandler
)
UpdatePlan
(
c
*
gin
.
Context
)
{
id
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
var
req
service
.
UpdatePlanRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
plan
,
err
:=
h
.
configService
.
UpdatePlan
(
c
.
Request
.
Context
(),
id
,
req
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
plan
)
}
// DeletePlan deletes a subscription plan.
// DELETE /api/v1/admin/payment/plans/:id
func
(
h
*
PaymentHandler
)
DeletePlan
(
c
*
gin
.
Context
)
{
id
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
if
err
:=
h
.
configService
.
DeletePlan
(
c
.
Request
.
Context
(),
id
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"deleted"
})
}
// --- Provider Instances ---
// ListProviders returns all payment provider instances.
// GET /api/v1/admin/payment/providers
func
(
h
*
PaymentHandler
)
ListProviders
(
c
*
gin
.
Context
)
{
providers
,
err
:=
h
.
configService
.
ListProviderInstancesWithConfig
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
providers
)
}
// CreateProvider creates a new payment provider instance.
// POST /api/v1/admin/payment/providers
func
(
h
*
PaymentHandler
)
CreateProvider
(
c
*
gin
.
Context
)
{
var
req
service
.
CreateProviderInstanceRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
inst
,
err
:=
h
.
configService
.
CreateProviderInstance
(
c
.
Request
.
Context
(),
req
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
h
.
paymentService
.
RefreshProviders
(
c
.
Request
.
Context
())
response
.
Created
(
c
,
inst
)
}
// UpdateProvider updates an existing payment provider instance.
// PUT /api/v1/admin/payment/providers/:id
func
(
h
*
PaymentHandler
)
UpdateProvider
(
c
*
gin
.
Context
)
{
id
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
var
req
service
.
UpdateProviderInstanceRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
inst
,
err
:=
h
.
configService
.
UpdateProviderInstance
(
c
.
Request
.
Context
(),
id
,
req
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
h
.
paymentService
.
RefreshProviders
(
c
.
Request
.
Context
())
response
.
Success
(
c
,
inst
)
}
// DeleteProvider deletes a payment provider instance.
// DELETE /api/v1/admin/payment/providers/:id
func
(
h
*
PaymentHandler
)
DeleteProvider
(
c
*
gin
.
Context
)
{
id
,
ok
:=
parseIDParam
(
c
,
"id"
)
if
!
ok
{
return
}
if
err
:=
h
.
configService
.
DeleteProviderInstance
(
c
.
Request
.
Context
(),
id
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
h
.
paymentService
.
RefreshProviders
(
c
.
Request
.
Context
())
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"deleted"
})
}
// parseIDParam parses an int64 path parameter.
// Returns the parsed ID and true on success; on failure it writes a BadRequest response and returns false.
func
parseIDParam
(
c
*
gin
.
Context
,
paramName
string
)
(
int64
,
bool
)
{
id
,
err
:=
strconv
.
ParseInt
(
c
.
Param
(
paramName
),
10
,
64
)
if
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid "
+
paramName
)
return
0
,
false
}
return
id
,
true
}
// --- Config ---
// GetConfig returns the payment configuration (admin view).
// GET /api/v1/admin/payment/config
func
(
h
*
PaymentHandler
)
GetConfig
(
c
*
gin
.
Context
)
{
cfg
,
err
:=
h
.
configService
.
GetPaymentConfig
(
c
.
Request
.
Context
())
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
cfg
)
}
// UpdateConfig updates the payment configuration.
// PUT /api/v1/admin/payment/config
func
(
h
*
PaymentHandler
)
UpdateConfig
(
c
*
gin
.
Context
)
{
var
req
service
.
UpdatePaymentConfigRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
err
:=
h
.
configService
.
UpdatePaymentConfig
(
c
.
Request
.
Context
(),
req
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"updated"
})
}
backend/internal/handler/admin/promo_handler.go
View file @
a04ae28a
...
...
@@ -57,6 +57,8 @@ func (h *PromoHandler) List(c *gin.Context) {
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
,
SortBy
:
c
.
DefaultQuery
(
"sort_by"
,
"created_at"
),
SortOrder
:
c
.
DefaultQuery
(
"sort_order"
,
"desc"
),
}
codes
,
paginationResult
,
err
:=
h
.
promoService
.
List
(
c
.
Request
.
Context
(),
params
,
status
,
search
)
...
...
backend/internal/handler/admin/proxy_data.go
View file @
a04ae28a
...
...
@@ -33,11 +33,13 @@ func (h *ProxyHandler) ExportData(c *gin.Context) {
protocol
:=
c
.
Query
(
"protocol"
)
status
:=
c
.
Query
(
"status"
)
search
:=
strings
.
TrimSpace
(
c
.
Query
(
"search"
))
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"id"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"desc"
)
if
len
(
search
)
>
100
{
search
=
search
[
:
100
]
}
proxies
,
err
=
h
.
listProxiesFiltered
(
ctx
,
protocol
,
status
,
search
)
proxies
,
err
=
h
.
listProxiesFiltered
(
ctx
,
protocol
,
status
,
search
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -89,7 +91,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
ctx
:=
c
.
Request
.
Context
()
result
:=
DataImportResult
{}
existingProxies
,
err
:=
h
.
listProxiesFiltered
(
ctx
,
""
,
""
,
""
)
existingProxies
,
err
:=
h
.
listProxiesFiltered
(
ctx
,
""
,
""
,
""
,
"id"
,
"desc"
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -220,12 +222,26 @@ func parseProxyIDs(c *gin.Context) ([]int64, error) {
return
ids
,
nil
}
func
(
h
*
ProxyHandler
)
listProxiesFiltered
(
ctx
context
.
Context
,
protocol
,
status
,
search
string
)
([]
service
.
Proxy
,
error
)
{
func
(
h
*
ProxyHandler
)
listProxiesFiltered
(
ctx
context
.
Context
,
protocol
,
status
,
search
,
sortBy
,
sortOrder
string
)
([]
service
.
Proxy
,
error
)
{
page
:=
1
pageSize
:=
dataPageCap
var
out
[]
service
.
Proxy
sortBy
=
strings
.
TrimSpace
(
sortBy
)
useAccountCountSort
:=
strings
.
EqualFold
(
sortBy
,
"account_count"
)
for
{
items
,
total
,
err
:=
h
.
adminService
.
ListProxies
(
ctx
,
page
,
pageSize
,
protocol
,
status
,
search
)
if
useAccountCountSort
{
items
,
total
,
err
:=
h
.
adminService
.
ListProxiesWithAccountCount
(
ctx
,
page
,
pageSize
,
protocol
,
status
,
search
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
return
nil
,
err
}
for
i
:=
range
items
{
out
=
append
(
out
,
items
[
i
]
.
Proxy
)
}
if
len
(
out
)
>=
int
(
total
)
||
len
(
items
)
==
0
{
break
}
}
else
{
items
,
total
,
err
:=
h
.
adminService
.
ListProxies
(
ctx
,
page
,
pageSize
,
protocol
,
status
,
search
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
return
nil
,
err
}
...
...
@@ -233,6 +249,7 @@ func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status
if
len
(
out
)
>=
int
(
total
)
||
len
(
items
)
==
0
{
break
}
}
page
++
}
return
out
,
nil
...
...
Prev
1
2
3
4
5
6
7
8
…
16
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