diff --git a/backend/app/services/website_client.py b/backend/app/services/website_client.py index 3f3be06..bfa4229 100644 --- a/backend/app/services/website_client.py +++ b/backend/app/services/website_client.py @@ -334,17 +334,83 @@ class Sub2ApiWebsiteClient: return None def list_accounts(self, endpoint: str = "/accounts") -> list[dict[str, Any]] | None: - """拉取远端账号列表。成功返回账号 dict 列表,失败返回 None。""" - try: - resp = self._request("GET", endpoint) - except Exception: - logger.warning("account list fetch failed for %s", endpoint, exc_info=True) - return None - items = self._unwrap_list(resp) - if items is None: - logger.warning("account list unexpected format for %s", endpoint) - return None - return [item for item in items if isinstance(item, dict)] + """拉取远端账号列表。支持分页拉取,成功返回账号 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: + 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: + logger.warning("account list fetch failed for %s", current_path, exc_info=True) + return None + + items = self._unwrap_list(resp) + if items is None: + logger.warning("account list unexpected format for %s", current_path) + return None + + 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: """拉取远端账号列表。成功返回 ID 集合(可能为空),解析失败返回 None。""" diff --git a/backend/test_website_client.py b/backend/test_website_client.py index a84bbdd..4ef7fae 100644 --- a/backend/test_website_client.py +++ b/backend/test_website_client.py @@ -272,3 +272,57 @@ def test_get_groups_all_404_no_raw_url(monkeypatch): assert "接口不存在" in msg assert "http://" 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()