perf(import): 优化账号导入弹窗为秒开体验并支持批量回查校验

This commit is contained in:
liumangmang
2026-07-01 10:00:40 +08:00
parent 88c783b922
commit efd0a88249
3 changed files with 103 additions and 46 deletions
+26 -11
View File
@@ -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"])
+54 -25
View File
@@ -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):
+23 -10
View File
@@ -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) {