perf(import): 优化账号导入弹窗为秒开体验并支持批量回查校验
This commit is contained in:
@@ -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"])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user