fix: Sub2ApiWebsiteClient.list_accounts 支持分页拉取,避免只拿到默认第一页 20 条导致误建重复账号
This commit is contained in:
@@ -334,17 +334,83 @@ class Sub2ApiWebsiteClient:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def list_accounts(self, endpoint: str = "/accounts") -> list[dict[str, Any]] | None:
|
def list_accounts(self, endpoint: str = "/accounts") -> list[dict[str, Any]] | None:
|
||||||
"""拉取远端账号列表。成功返回账号 dict 列表,失败返回 None。"""
|
"""拉取远端账号列表。支持分页拉取,成功返回账号 dict 列表,失败返回 None。"""
|
||||||
|
base_path = endpoint
|
||||||
|
query_params = {}
|
||||||
|
if "?" in endpoint:
|
||||||
|
base_path, query_str = endpoint.split("?", 1)
|
||||||
|
for part in query_str.split("&"):
|
||||||
|
if "=" in part:
|
||||||
|
k, v = part.split("=", 1)
|
||||||
|
query_params[k] = v
|
||||||
|
|
||||||
|
page_size = 100
|
||||||
|
if "page_size" in query_params:
|
||||||
try:
|
try:
|
||||||
resp = self._request("GET", endpoint)
|
page_size = int(query_params["page_size"])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
page = 1
|
||||||
|
all_items = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
params = dict(query_params)
|
||||||
|
params["page"] = str(page)
|
||||||
|
params["page_size"] = str(page_size)
|
||||||
|
|
||||||
|
qp_str = "&".join(f"{k}={v}" for k, v in params.items())
|
||||||
|
current_path = f"{base_path}?{qp_str}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = self._request("GET", current_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("account list fetch failed for %s", endpoint, exc_info=True)
|
logger.warning("account list fetch failed for %s", current_path, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
items = self._unwrap_list(resp)
|
items = self._unwrap_list(resp)
|
||||||
if items is None:
|
if items is None:
|
||||||
logger.warning("account list unexpected format for %s", endpoint)
|
logger.warning("account list unexpected format for %s", current_path)
|
||||||
return None
|
return None
|
||||||
return [item for item in items if isinstance(item, dict)]
|
|
||||||
|
all_items.extend([item for item in items if isinstance(item, dict)])
|
||||||
|
|
||||||
|
if isinstance(resp, dict):
|
||||||
|
# 尝试从顶层或嵌套的 data 字典中提取分页字段
|
||||||
|
meta = resp
|
||||||
|
data_val = resp.get("data")
|
||||||
|
if isinstance(data_val, dict) and any(k in data_val for k in ("pages", "total", "page_size")):
|
||||||
|
meta = data_val
|
||||||
|
|
||||||
|
pages = meta.get("pages")
|
||||||
|
if pages is not None:
|
||||||
|
try:
|
||||||
|
pages_val = int(pages)
|
||||||
|
if page >= pages_val:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
continue
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
total = meta.get("total")
|
||||||
|
current_page_size = meta.get("page_size")
|
||||||
|
if total is not None and current_page_size is not None:
|
||||||
|
try:
|
||||||
|
total_val = int(total)
|
||||||
|
page_size_val = int(current_page_size)
|
||||||
|
import math
|
||||||
|
calculated_pages = math.ceil(total_val / page_size_val)
|
||||||
|
if page >= calculated_pages:
|
||||||
|
break
|
||||||
|
page += 1
|
||||||
|
continue
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
return all_items
|
||||||
|
|
||||||
def _get_account_ids(self, endpoint: str = "/accounts") -> set[str] | None:
|
def _get_account_ids(self, endpoint: str = "/accounts") -> set[str] | None:
|
||||||
"""拉取远端账号列表。成功返回 ID 集合(可能为空),解析失败返回 None。"""
|
"""拉取远端账号列表。成功返回 ID 集合(可能为空),解析失败返回 None。"""
|
||||||
|
|||||||
@@ -272,3 +272,57 @@ def test_get_groups_all_404_no_raw_url(monkeypatch):
|
|||||||
assert "接口不存在" in msg
|
assert "接口不存在" in msg
|
||||||
assert "http://" not in msg
|
assert "http://" not in msg
|
||||||
assert "MDN" not in msg
|
assert "MDN" not in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_accounts_pagination():
|
||||||
|
from app.services.website_client import Sub2ApiWebsiteClient
|
||||||
|
|
||||||
|
requests_called = []
|
||||||
|
|
||||||
|
def handler(req: httpx.Request) -> httpx.Response:
|
||||||
|
requests_called.append(req)
|
||||||
|
url_str = str(req.url)
|
||||||
|
if "page=1" in url_str:
|
||||||
|
return httpx.Response(200, json={
|
||||||
|
"total": 3,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 2,
|
||||||
|
"pages": 2,
|
||||||
|
"data": [
|
||||||
|
{"id": "a1", "name": "acc1"},
|
||||||
|
{"id": "a2", "name": "acc2"},
|
||||||
|
]
|
||||||
|
}, request=req)
|
||||||
|
elif "page=2" in url_str:
|
||||||
|
return httpx.Response(200, json={
|
||||||
|
"total": 3,
|
||||||
|
"page": 2,
|
||||||
|
"page_size": 2,
|
||||||
|
"pages": 2,
|
||||||
|
"data": [
|
||||||
|
{"id": "a3", "name": "acc3"},
|
||||||
|
]
|
||||||
|
}, request=req)
|
||||||
|
return httpx.Response(404, json={"error": "Not Found"}, request=req)
|
||||||
|
|
||||||
|
client = Sub2ApiWebsiteClient(
|
||||||
|
base_url="https://target.example",
|
||||||
|
api_prefix="/api/v1/admin",
|
||||||
|
auth_type="api_key",
|
||||||
|
auth_config={"key": "admin-key"},
|
||||||
|
)
|
||||||
|
client._client = httpx.Client(transport=httpx.MockTransport(handler))
|
||||||
|
|
||||||
|
try:
|
||||||
|
accounts = client.list_accounts("/accounts")
|
||||||
|
assert accounts == [
|
||||||
|
{"id": "a1", "name": "acc1"},
|
||||||
|
{"id": "a2", "name": "acc2"},
|
||||||
|
{"id": "a3", "name": "acc3"},
|
||||||
|
]
|
||||||
|
assert len(requests_called) == 2
|
||||||
|
assert "page_size=100" in str(requests_called[0].url)
|
||||||
|
assert "page=1" in str(requests_called[0].url)
|
||||||
|
assert "page=2" in str(requests_called[1].url)
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user