fix(export): 导出逻辑与当前筛选条件对齐
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,18 +222,33 @@ 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 err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, items...)
|
||||
if len(out) >= int(total) || len(items) == 0 {
|
||||
break
|
||||
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
|
||||
}
|
||||
out = append(out, items...)
|
||||
if len(out) >= int(total) || len(items) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
page++
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
49
backend/internal/handler/admin/redeem_export_handler_test.go
Normal file
49
backend/internal/handler/admin/redeem_export_handler_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
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) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user