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
66e15a54
Commit
66e15a54
authored
Apr 09, 2026
by
IanShaw027
Browse files
fix(export): 导出逻辑与当前筛选条件对齐
parent
ad80606a
Changes
7
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/account_data.go
View file @
66e15a54
...
...
@@ -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 @
66e15a54
...
...
@@ -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/proxy_data.go
View file @
66e15a54
...
...
@@ -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
...
...
backend/internal/handler/admin/proxy_data_handler_test.go
View file @
66e15a54
...
...
@@ -74,6 +74,10 @@ func TestProxyExportDataRespectsFilters(t *testing.T) {
require
.
Len
(
t
,
resp
.
Data
.
Proxies
,
1
)
require
.
Len
(
t
,
resp
.
Data
.
Accounts
,
0
)
require
.
Equal
(
t
,
"https"
,
resp
.
Data
.
Proxies
[
0
]
.
Protocol
)
require
.
Equal
(
t
,
1
,
adminSvc
.
lastListProxies
.
calls
)
require
.
Equal
(
t
,
"https"
,
adminSvc
.
lastListProxies
.
protocol
)
require
.
Equal
(
t
,
"id"
,
adminSvc
.
lastListProxies
.
sortBy
)
require
.
Equal
(
t
,
"desc"
,
adminSvc
.
lastListProxies
.
sortOrder
)
}
func
TestProxyExportDataWithSelectedIDs
(
t
*
testing
.
T
)
{
...
...
@@ -113,6 +117,96 @@ func TestProxyExportDataWithSelectedIDs(t *testing.T) {
require
.
Len
(
t
,
resp
.
Data
.
Proxies
,
1
)
require
.
Equal
(
t
,
"https"
,
resp
.
Data
.
Proxies
[
0
]
.
Protocol
)
require
.
Equal
(
t
,
"10.0.0.2"
,
resp
.
Data
.
Proxies
[
0
]
.
Host
)
require
.
Equal
(
t
,
0
,
adminSvc
.
lastListProxies
.
calls
)
}
func
TestProxyExportDataPassesSortParams
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupProxyDataRouter
()
adminSvc
.
proxies
=
[]
service
.
Proxy
{
{
ID
:
1
,
Name
:
"proxy-a"
,
Protocol
:
"http"
,
Host
:
"127.0.0.1"
,
Port
:
8080
,
Username
:
"user"
,
Password
:
"pass"
,
Status
:
service
.
StatusActive
,
},
}
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/data?protocol=http&status=active&search=proxy&sort_by=name&sort_order=asc"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
adminSvc
.
lastListProxies
.
calls
)
require
.
Equal
(
t
,
"http"
,
adminSvc
.
lastListProxies
.
protocol
)
require
.
Equal
(
t
,
"active"
,
adminSvc
.
lastListProxies
.
status
)
require
.
Equal
(
t
,
"proxy"
,
adminSvc
.
lastListProxies
.
search
)
require
.
Equal
(
t
,
"name"
,
adminSvc
.
lastListProxies
.
sortBy
)
require
.
Equal
(
t
,
"asc"
,
adminSvc
.
lastListProxies
.
sortOrder
)
}
func
TestProxyExportDataSortByAccountCountUsesAccountCountListing
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupProxyDataRouter
()
adminSvc
.
proxies
=
[]
service
.
Proxy
{
{
ID
:
1
,
Name
:
"proxy-id-1"
,
Protocol
:
"http"
,
Host
:
"127.0.0.1"
,
Port
:
8080
,
Status
:
service
.
StatusActive
,
},
{
ID
:
2
,
Name
:
"proxy-id-2"
,
Protocol
:
"http"
,
Host
:
"127.0.0.2"
,
Port
:
8081
,
Status
:
service
.
StatusActive
,
},
}
adminSvc
.
proxyCounts
=
[]
service
.
ProxyWithAccountCount
{
{
Proxy
:
service
.
Proxy
{
ID
:
2
,
Name
:
"proxy-count-high"
,
Protocol
:
"http"
,
Host
:
"127.0.0.2"
,
Port
:
8081
,
Status
:
service
.
StatusActive
,
},
AccountCount
:
9
,
},
{
Proxy
:
service
.
Proxy
{
ID
:
1
,
Name
:
"proxy-count-low"
,
Protocol
:
"http"
,
Host
:
"127.0.0.1"
,
Port
:
8080
,
Status
:
service
.
StatusActive
,
},
AccountCount
:
1
,
},
}
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/proxies/data?sort_by=account_count&sort_order=desc"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
var
resp
proxyDataResponse
require
.
NoError
(
t
,
json
.
Unmarshal
(
rec
.
Body
.
Bytes
(),
&
resp
))
require
.
Equal
(
t
,
0
,
resp
.
Code
)
require
.
Len
(
t
,
resp
.
Data
.
Proxies
,
2
)
require
.
Equal
(
t
,
"proxy-count-high"
,
resp
.
Data
.
Proxies
[
0
]
.
Name
)
require
.
Equal
(
t
,
"proxy-count-low"
,
resp
.
Data
.
Proxies
[
1
]
.
Name
)
require
.
Equal
(
t
,
0
,
adminSvc
.
lastListProxies
.
calls
)
}
func
TestProxyImportDataReusesAndTriggersLatencyProbe
(
t
*
testing
.
T
)
{
...
...
backend/internal/handler/admin/redeem_export_handler_test.go
0 → 100644
View file @
66e15a54
package
admin
import
(
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
func
setupRedeemExportRouter
()
(
*
gin
.
Engine
,
*
stubAdminService
)
{
gin
.
SetMode
(
gin
.
TestMode
)
router
:=
gin
.
New
()
adminSvc
:=
newStubAdminService
()
h
:=
NewRedeemHandler
(
adminSvc
,
nil
)
router
.
GET
(
"/api/v1/admin/redeem-codes/export"
,
h
.
Export
)
return
router
,
adminSvc
}
func
TestRedeemExportPassesSearchAndSort
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupRedeemExportRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/redeem-codes/export?type=balance&status=unused&search=ABC&sort_by=value&sort_order=asc"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
adminSvc
.
lastListRedeemCodes
.
calls
)
require
.
Equal
(
t
,
"balance"
,
adminSvc
.
lastListRedeemCodes
.
codeType
)
require
.
Equal
(
t
,
"unused"
,
adminSvc
.
lastListRedeemCodes
.
status
)
require
.
Equal
(
t
,
"ABC"
,
adminSvc
.
lastListRedeemCodes
.
search
)
require
.
Equal
(
t
,
"value"
,
adminSvc
.
lastListRedeemCodes
.
sortBy
)
require
.
Equal
(
t
,
"asc"
,
adminSvc
.
lastListRedeemCodes
.
sortOrder
)
}
func
TestRedeemExportSortDefaults
(
t
*
testing
.
T
)
{
router
,
adminSvc
:=
setupRedeemExportRouter
()
rec
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/api/v1/admin/redeem-codes/export"
,
nil
)
router
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
adminSvc
.
lastListRedeemCodes
.
calls
)
require
.
Equal
(
t
,
"id"
,
adminSvc
.
lastListRedeemCodes
.
sortBy
)
require
.
Equal
(
t
,
"desc"
,
adminSvc
.
lastListRedeemCodes
.
sortOrder
)
}
backend/internal/handler/admin/redeem_handler.go
View file @
66e15a54
...
...
@@ -59,13 +59,15 @@ func (h *RedeemHandler) List(c *gin.Context) {
codeType
:=
c
.
Query
(
"type"
)
status
:=
c
.
Query
(
"status"
)
search
:=
c
.
Query
(
"search"
)
sortBy
:=
c
.
DefaultQuery
(
"sort_by"
,
"id"
)
sortOrder
:=
c
.
DefaultQuery
(
"sort_order"
,
"desc"
)
// 标准化和验证 search 参数
search
=
strings
.
TrimSpace
(
search
)
if
len
(
search
)
>
100
{
search
=
search
[
:
100
]
}
codes
,
total
,
err
:=
h
.
adminService
.
ListRedeemCodes
(
c
.
Request
.
Context
(),
page
,
pageSize
,
codeType
,
status
,
search
)
codes
,
total
,
err
:=
h
.
adminService
.
ListRedeemCodes
(
c
.
Request
.
Context
(),
page
,
pageSize
,
codeType
,
status
,
search
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
@@ -300,9 +302,15 @@ func (h *RedeemHandler) GetStats(c *gin.Context) {
func
(
h
*
RedeemHandler
)
Export
(
c
*
gin
.
Context
)
{
codeType
:=
c
.
Query
(
"type"
)
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
]
}
// Get all codes without pagination (use large page size)
codes
,
_
,
err
:=
h
.
adminService
.
ListRedeemCodes
(
c
.
Request
.
Context
(),
1
,
10000
,
codeType
,
status
,
""
)
codes
,
_
,
err
:=
h
.
adminService
.
ListRedeemCodes
(
c
.
Request
.
Context
(),
1
,
10000
,
codeType
,
status
,
search
,
sortBy
,
sortOrder
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
...
...
frontend/src/api/admin/redeem.ts
View file @
66e15a54
...
...
@@ -25,6 +25,8 @@ export async function list(
type
?:
RedeemCodeType
status
?:
'
active
'
|
'
used
'
|
'
expired
'
|
'
unused
'
search
?:
string
sort_by
?:
string
sort_order
?:
'
asc
'
|
'
desc
'
},
options
?:
{
signal
?:
AbortSignal
...
...
@@ -151,7 +153,10 @@ export async function getStats(): Promise<{
*/
export
async
function
exportCodes
(
filters
?:
{
type
?:
RedeemCodeType
status
?:
'
active
'
|
'
used
'
|
'
expired
'
status
?:
'
used
'
|
'
expired
'
|
'
unused
'
search
?:
string
sort_by
?:
string
sort_order
?:
'
asc
'
|
'
desc
'
}):
Promise
<
Blob
>
{
const
response
=
await
apiClient
.
get
(
'
/admin/redeem-codes/export
'
,
{
params
:
filters
,
...
...
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