Commit 66e15a54 authored by IanShaw027's avatar IanShaw027
Browse files

fix(export): 导出逻辑与当前筛选条件对齐

parent ad80606a
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"log/slog" "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/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/service"
...@@ -359,7 +360,7 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e ...@@ -359,7 +360,7 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e
pageSize := dataPageCap pageSize := dataPageCap
var out []service.Proxy var out []service.Proxy
for { 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 { if err != nil {
return nil, err return nil, err
} }
...@@ -372,12 +373,12 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e ...@@ -372,12 +373,12 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e
return out, nil 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 page := 1
pageSize := dataPageCap pageSize := dataPageCap
var out []service.Account var out []service.Account
for { 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 { if err != nil {
return nil, err return nil, err
} }
...@@ -409,11 +410,28 @@ func (h *AccountHandler) resolveExportAccounts(ctx context.Context, ids []int64, ...@@ -409,11 +410,28 @@ func (h *AccountHandler) resolveExportAccounts(ctx context.Context, ids []int64,
platform := c.Query("platform") platform := c.Query("platform")
accountType := c.Query("type") accountType := c.Query("type")
status := c.Query("status") status := c.Query("status")
privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
search := strings.TrimSpace(c.Query("search")) search := strings.TrimSpace(c.Query("search"))
sortBy := c.DefaultQuery("sort_by", "name")
sortOrder := c.DefaultQuery("sort_order", "asc")
if len(search) > 100 { if len(search) > 100 {
search = 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) { func (h *AccountHandler) resolveExportProxies(ctx context.Context, accounts []service.Account) ([]service.Proxy, error) {
......
...@@ -172,6 +172,51 @@ func TestExportDataWithoutProxies(t *testing.T) { ...@@ -172,6 +172,51 @@ func TestExportDataWithoutProxies(t *testing.T) {
require.Nil(t, resp.Data.Accounts[0].ProxyKey) 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) { func TestImportDataReusesProxyAndSkipsDefaultGroup(t *testing.T) {
router, adminSvc := setupAccountDataRouter() router, adminSvc := setupAccountDataRouter()
......
...@@ -33,11 +33,13 @@ func (h *ProxyHandler) ExportData(c *gin.Context) { ...@@ -33,11 +33,13 @@ func (h *ProxyHandler) ExportData(c *gin.Context) {
protocol := c.Query("protocol") protocol := c.Query("protocol")
status := c.Query("status") status := c.Query("status")
search := strings.TrimSpace(c.Query("search")) search := strings.TrimSpace(c.Query("search"))
sortBy := c.DefaultQuery("sort_by", "id")
sortOrder := c.DefaultQuery("sort_order", "desc")
if len(search) > 100 { if len(search) > 100 {
search = 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 { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -89,7 +91,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { ...@@ -89,7 +91,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
result := DataImportResult{} result := DataImportResult{}
existingProxies, err := h.listProxiesFiltered(ctx, "", "", "") existingProxies, err := h.listProxiesFiltered(ctx, "", "", "", "id", "desc")
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -220,18 +222,33 @@ func parseProxyIDs(c *gin.Context) ([]int64, error) { ...@@ -220,18 +222,33 @@ func parseProxyIDs(c *gin.Context) ([]int64, error) {
return ids, nil 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 page := 1
pageSize := dataPageCap pageSize := dataPageCap
var out []service.Proxy var out []service.Proxy
sortBy = strings.TrimSpace(sortBy)
useAccountCountSort := strings.EqualFold(sortBy, "account_count")
for { for {
items, total, err := h.adminService.ListProxies(ctx, page, pageSize, protocol, status, search) if useAccountCountSort {
if err != nil { items, total, err := h.adminService.ListProxiesWithAccountCount(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder)
return nil, err if err != nil {
} return nil, err
out = append(out, items...) }
if len(out) >= int(total) || len(items) == 0 { for i := range items {
break 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
}
out = append(out, items...)
if len(out) >= int(total) || len(items) == 0 {
break
}
} }
page++ page++
} }
......
...@@ -74,6 +74,10 @@ func TestProxyExportDataRespectsFilters(t *testing.T) { ...@@ -74,6 +74,10 @@ func TestProxyExportDataRespectsFilters(t *testing.T) {
require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Proxies, 1)
require.Len(t, resp.Data.Accounts, 0) require.Len(t, resp.Data.Accounts, 0)
require.Equal(t, "https", resp.Data.Proxies[0].Protocol) 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) { func TestProxyExportDataWithSelectedIDs(t *testing.T) {
...@@ -113,6 +117,96 @@ func TestProxyExportDataWithSelectedIDs(t *testing.T) { ...@@ -113,6 +117,96 @@ func TestProxyExportDataWithSelectedIDs(t *testing.T) {
require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Proxies, 1)
require.Equal(t, "https", resp.Data.Proxies[0].Protocol) require.Equal(t, "https", resp.Data.Proxies[0].Protocol)
require.Equal(t, "10.0.0.2", resp.Data.Proxies[0].Host) 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) { func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) {
......
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)
}
...@@ -59,13 +59,15 @@ func (h *RedeemHandler) List(c *gin.Context) { ...@@ -59,13 +59,15 @@ func (h *RedeemHandler) List(c *gin.Context) {
codeType := c.Query("type") codeType := c.Query("type")
status := c.Query("status") status := c.Query("status")
search := c.Query("search") search := c.Query("search")
sortBy := c.DefaultQuery("sort_by", "id")
sortOrder := c.DefaultQuery("sort_order", "desc")
// 标准化和验证 search 参数 // 标准化和验证 search 参数
search = strings.TrimSpace(search) search = strings.TrimSpace(search)
if len(search) > 100 { if len(search) > 100 {
search = 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 { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
...@@ -300,9 +302,15 @@ func (h *RedeemHandler) GetStats(c *gin.Context) { ...@@ -300,9 +302,15 @@ func (h *RedeemHandler) GetStats(c *gin.Context) {
func (h *RedeemHandler) Export(c *gin.Context) { func (h *RedeemHandler) Export(c *gin.Context) {
codeType := c.Query("type") codeType := c.Query("type")
status := c.Query("status") 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) // 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 { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
......
...@@ -25,6 +25,8 @@ export async function list( ...@@ -25,6 +25,8 @@ export async function list(
type?: RedeemCodeType type?: RedeemCodeType
status?: 'active' | 'used' | 'expired' | 'unused' status?: 'active' | 'used' | 'expired' | 'unused'
search?: string search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}, },
options?: { options?: {
signal?: AbortSignal signal?: AbortSignal
...@@ -151,7 +153,10 @@ export async function getStats(): Promise<{ ...@@ -151,7 +153,10 @@ export async function getStats(): Promise<{
*/ */
export async function exportCodes(filters?: { export async function exportCodes(filters?: {
type?: RedeemCodeType type?: RedeemCodeType
status?: 'active' | 'used' | 'expired' status?: 'used' | 'expired' | 'unused'
search?: string
sort_by?: string
sort_order?: 'asc' | 'desc'
}): Promise<Blob> { }): Promise<Blob> {
const response = await apiClient.get('/admin/redeem-codes/export', { const response = await apiClient.get('/admin/redeem-codes/export', {
params: filters, params: filters,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment