From efd0a8824938eabae10b1e4405b0d2c1218a1a0a Mon Sep 17 00:00:00 2001 From: liumangmang Date: Wed, 1 Jul 2026 10:00:40 +0800 Subject: [PATCH] =?UTF-8?q?perf(import):=20=E4=BC=98=E5=8C=96=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=AF=BC=E5=85=A5=E5=BC=B9=E7=AA=97=E4=B8=BA=E7=A7=92?= =?UTF-8?q?=E5=BC=80=E4=BD=93=E9=AA=8C=E5=B9=B6=E6=94=AF=E6=8C=81=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E5=9B=9E=E6=9F=A5=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/websites.py | 37 +++++++--- backend/test_upstream_key_account_import.py | 79 ++++++++++++++------- frontend/src/views/Websites.vue | 33 ++++++--- 3 files changed, 103 insertions(+), 46 deletions(-) diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index 25df558..de6d954 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -36,7 +36,7 @@ from app.schemas.website import ( WebsiteBatchSyncResponse, ) -from app.services.website_client import Sub2ApiWebsiteClient +from app.services.website_client import Sub2ApiWebsiteClient, _extract_id from app.services.website_sync import ( binding_sources, sync_binding, @@ -428,13 +428,35 @@ def sync_imported_upstream_keys( ) items: list[ImportAccountItem] = [] with _client(website) as c: + remote_accounts = None + try: + remote_accounts = c.list_accounts() + except Exception as e: + logger.warning("failed to list accounts for website %s at sync: %s", wid, e) + + if remote_accounts is not None: + remote_ids = set() + for acc in remote_accounts: + acc_id = _extract_id(acc) + if acc_id: + remote_ids.add(str(acc_id)) + else: + remote_ids = None + for row in rows: platform = _detect_platform(f"{row.group_name} {row.group_id} {row.key_name}", "openai") if not row.imported_account_id: continue old_account_id = row.imported_account_id - exists = c.account_exists(row.imported_account_id) - if exists is False: + + if remote_ids is None: + items.append(ImportAccountItem( + upstream_key_id=row.id, source_group_id=row.group_id, + source_group_name=row.group_name, account_id=old_account_id, + platform=platform, status="check_failed", + message="无法校验目标账号存在性(拉取账号列表失败)", + )) + elif str(old_account_id) not in remote_ids: row.imported_website_id = None row.imported_account_id = None row.imported_at = None @@ -445,18 +467,11 @@ def sync_imported_upstream_keys( platform=platform, status="stale_cleared", message="目标账号已删除,已清除导入标记", )) - elif exists is True: - items.append(ImportAccountItem( - upstream_key_id=row.id, source_group_id=row.group_id, - source_group_name=row.group_name, account_id=old_account_id, - platform=platform, status="exists", message="目标账号仍存在", - )) else: items.append(ImportAccountItem( upstream_key_id=row.id, source_group_id=row.group_id, source_group_name=row.group_name, account_id=old_account_id, - platform=platform, status="check_failed", - message="无法校验目标账号存在性(目标网站认证/网络问题)", + platform=platform, status="exists", message="目标账号仍存在", )) db.commit() cleared_count = len([i for i in items if i.status == "stale_cleared"]) diff --git a/backend/test_upstream_key_account_import.py b/backend/test_upstream_key_account_import.py index e62d54f..7abb896 100644 --- a/backend/test_upstream_key_account_import.py +++ b/backend/test_upstream_key_account_import.py @@ -182,27 +182,45 @@ def test_import_upstream_key_idempotent_skips_already_imported(monkeypatch, db_s assert response.success -def test_sync_clears_stale_import_mark(monkeypatch, db_session): - """同步接口:远端账号已删除时清除本地导入标记。""" +def test_sync_imported_upstream_keys_efficient_and_correct(monkeypatch, db_session): + """验证 sync_imported_upstream_keys 优化:只拉一次账号列表,处理 exists 和 stale_cleared。""" from app.routers import websites as websites_router - from app.schemas.website import SyncImportStatusRequest, ImportAccountsRequest + from app.schemas.website import SyncImportStatusRequest - website, generated = seed_account_import_rows(db_session) - generated.imported_website_id = website.id - generated.imported_account_id = "101" + website, generated1 = seed_account_import_rows(db_session) + generated1.imported_website_id = website.id + generated1.imported_account_id = "101" + + # 增加另一个已导入行以测试单次请求 & 多个状态 + generated2 = UpstreamGeneratedKey( + upstream_id=generated1.upstream_id, + group_id="normal", + group_name="normal", + key_name="normal-key", + key_value="sk-normal", + imported_website_id=website.id, + imported_account_id="102", + status="imported", + ) + db_session.add(generated2) db_session.commit() + list_accounts_calls = 0 + class FakeClient: def __init__(self, **kwargs): - self.kwargs = kwargs + pass def __enter__(self): return self def __exit__(self, *a): return False - def account_exists(self, account_id): - return False # 远端返回不存在 - def _get_account_ids(self, endpoint="/accounts"): - return set() + def list_accounts(self): + nonlocal list_accounts_calls + list_accounts_calls += 1 + # 远端只有 A1 (id: 101),没有 A2 (id: 102) + return [ + {"id": "101", "name": "SmartUp-A1"} + ] @staticmethod def extract_id(data): return str(data.get("id")) @@ -210,19 +228,32 @@ def test_sync_clears_stale_import_mark(monkeypatch, db_session): monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", lambda **kw: FakeClient(**kw)) response = websites_router.sync_imported_upstream_keys( - website.id, SyncImportStatusRequest(upstream_id=generated.upstream_id), + website.id, SyncImportStatusRequest(upstream_id=generated1.upstream_id), db_session, object(), ) - assert len(response.items) == 1 - assert response.items[0].status == "stale_cleared" - assert response.items[0].account_id == "101" - db_session.refresh(generated) - assert generated.imported_account_id is None + # 1. 验证 list_accounts() 只被调用了 1 次 + assert list_accounts_calls == 1 + + # 2. 检查结果 + assert len(response.items) == 2 + item_101 = next(item for item in response.items if item.account_id == "101") + item_102 = next(item for item in response.items if item.account_id == "102") + + # 101 仍存在 + assert item_101.status == "exists" + # 102 已被清除 + assert item_102.status == "stale_cleared" + + # 3. 校验 DB 反映 + db_session.refresh(generated1) + db_session.refresh(generated2) + assert generated1.imported_account_id == "101" + assert generated2.imported_account_id is None -def test_sync_preserves_mark_on_check_failed(monkeypatch, db_session): - """同步接口:校验失败时不清除本地标记。""" +def test_sync_imported_upstream_keys_check_failed_on_exception(monkeypatch, db_session): + """验证 list_accounts 报错或返回 None 时返回 check_failed,且不清空本地导入标记。""" from app.routers import websites as websites_router from app.schemas.website import SyncImportStatusRequest @@ -233,15 +264,13 @@ def test_sync_preserves_mark_on_check_failed(monkeypatch, db_session): class FakeClient: def __init__(self, **kwargs): - self.kwargs = kwargs + pass def __enter__(self): return self def __exit__(self, *a): return False - def account_exists(self, account_id): - return None # 校验失败 - def _get_account_ids(self, endpoint="/accounts"): - return set() + def list_accounts(self): + raise Exception("List accounts failure") @staticmethod def extract_id(data): return str(data.get("id")) @@ -256,7 +285,7 @@ def test_sync_preserves_mark_on_check_failed(monkeypatch, db_session): assert len(response.items) == 1 assert response.items[0].status == "check_failed" db_session.refresh(generated) - assert generated.imported_account_id == "101" # 未被清除 + assert generated.imported_account_id == "101" def test_import_rebuilds_when_remote_deleted(monkeypatch, db_session): diff --git a/frontend/src/views/Websites.vue b/frontend/src/views/Websites.vue index 38ba4c6..5e903f4 100644 --- a/frontend/src/views/Websites.vue +++ b/frontend/src/views/Websites.vue @@ -704,10 +704,13 @@ async function loadImportTargetGroups(websiteId: number) { importTargetGroups.value = [] return } + const frozenId = websiteId try { - const res = await websitesApi.groups(websiteId) + const res = await websitesApi.groups(frozenId) + if (importAccountsForm.value.website_id !== frozenId) return importTargetGroups.value = res.data } catch { + if (importAccountsForm.value.website_id !== frozenId) return importTargetGroups.value = [] } } @@ -1073,26 +1076,36 @@ async function openImportAccounts(site?: WebsiteData | null) { } importAccountResults.value = [] importSyncStatus.value = null - await loadImportTargetGroups(importAccountsForm.value.website_id) - // 打开弹窗后自动同步导入状态(校验远端账号是否仍存在) - const reloaded = await syncImportStatus() - if (!reloaded) await loadImportGeneratedKeys(importAccountsForm.value.upstream_id) + + // 1. 立即打开弹窗,实现秒开体验 importAccountsDialog.value = true + + // 2. 在后台并发加载分组、Keys,并在加载完成后自动执行同步 + void Promise.all([ + loadImportTargetGroups(importAccountsForm.value.website_id), + loadImportGeneratedKeys(importAccountsForm.value.upstream_id) + ]).then(() => { + void syncImportStatus() + }) } async function onImportAccountWebsiteChange(value: number) { importAccountsForm.value.target_group_map = {} - await loadImportTargetGroups(value) - const reloaded = await syncImportStatus() - if (!reloaded) await loadImportGeneratedKeys(importAccountsForm.value.upstream_id) + void Promise.all([ + loadImportTargetGroups(value), + loadImportGeneratedKeys(importAccountsForm.value.upstream_id) + ]).then(() => { + void syncImportStatus() + }) } async function onImportAccountUpstreamChange(value: number) { importAccountsForm.value.upstream_key_ids = [] importAccountsForm.value.target_group_map = {} importAccountResults.value = [] - const reloaded = await syncImportStatus() - if (!reloaded) await loadImportGeneratedKeys(value) + void loadImportGeneratedKeys(value).then(() => { + void syncImportStatus() + }) } function onPlatformModeChange(value: string) {