From 94d9550f777d4c5b93a2a0db26033aacf0f7b34a Mon Sep 17 00:00:00 2001 From: liumangmang Date: Wed, 1 Jul 2026 21:11:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E6=96=B0=E5=BB=BA/?= =?UTF-8?q?=E9=87=8D=E5=BB=BA=E8=B4=A6=E5=8F=B7=E5=91=BD=E5=90=8D=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E4=B8=BA=20{=E4=B8=8A=E6=B8=B8=E7=BD=91=E7=AB=99?= =?UTF-8?q?=E5=90=8D}-{=E4=B8=8A=E6=B8=B8=E5=88=86=E7=BB=84=E5=90=8D/ID}-{?= =?UTF-8?q?KeyID}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/websites.py | 81 ++++++++++++++----- backend/app/services/website_sync.py | 6 ++ backend/test_organize_groups.py | 8 +- backend/test_priority_algorithm_correction.py | 57 +++++++++++++ 4 files changed, 130 insertions(+), 22 deletions(-) diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index 64913ce..7891244 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -49,6 +49,7 @@ from app.services.website_sync import ( reconcile_upstream_keys_full, sync_account_priorities_for_upstream, latest_rate_map, + build_imported_account_name, ) from app.utils.auth import get_current_user from app.utils.number import fixed_decimal_number @@ -630,15 +631,11 @@ def import_upstream_keys_as_accounts( ) for kid in missing_ids ] - # 查出来源上游的 Base URL - upstream_base_url = "" - if body.upstream_key_ids: - first_row = rows[0] if rows else None - if first_row: - from app.models.upstream import Upstream as _Up - _u = db.query(_Up).filter(_Up.id == first_row.upstream_id).first() - if _u: - upstream_base_url = _u.base_url + # 预载上游以方便获取名称和 base_url + from app.models.upstream import Upstream as _Up + upstream_ids = {row.upstream_id for row in rows} + upstreams_db = db.query(_Up).filter(_Up.id.in_(upstream_ids)).all() + upstreams_map = {u.id: u for u in upstreams_db} # 按倍率自动分配优先级 rate_priority_map: dict[tuple[str, int, str], int] = {} @@ -665,6 +662,21 @@ def import_upstream_keys_as_accounts( except Exception as e: logger.warning("failed to fetch groups for website %s at import: %s", wid, e) + # 预先拉取远端账号列表以避免循环请求,并能拿到已存在账号 of 远端名称 + remote_accounts_map: dict[str, dict[str, Any]] = {} + remote_accounts_list = None + has_list_accounts = hasattr(c, "list_accounts") + if has_list_accounts: + try: + remote_accounts_list = c.list_accounts(endpoint=website.accounts_endpoint) + if remote_accounts_list is not None: + for acc in remote_accounts_list: + aid = c.extract_id(acc) + if aid: + remote_accounts_map[aid] = acc + except Exception as e: + logger.warning("failed to fetch remote accounts at import: %s", e) + for row in rows: # 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed) if row.status in ("orphaned", "failed", "import_failed"): @@ -687,10 +699,34 @@ def import_upstream_keys_as_accounts( else: platform = body.default_platform + upstream = upstreams_map.get(row.upstream_id) + upstream_name = upstream.name if upstream else "Unknown" + upstream_base_url = upstream.base_url if upstream else "" + + # 构建新格式的预期账号名称 + expected_new_name = build_imported_account_name( + upstream_name=upstream_name, + group_name=row.group_name, + group_id=row.group_id, + key_id=row.id, + ) + # 幂等校验:已导入过则检查远端账号是否仍存在 if row.imported_website_id == wid and row.imported_account_id: old_account_id = row.imported_account_id - exists = c.account_exists(row.imported_account_id) + if has_list_accounts: + if remote_accounts_list is None: + # 接口获取失败,保守判定为校验失败 + exists = None + else: + exists = old_account_id in remote_accounts_map + else: + # 如果 client 没有 list_accounts,则 fallback 回原先的 account_exists 检测以兼容旧 Mock 测试 + try: + exists = c.account_exists(row.imported_account_id) + except Exception: + exists = None + if exists is True: # 顺手回填 imported_target_group_id(老数据升级后可通过重导自动补齐) new_tgid = body.target_group_map.get(row.group_id) or None @@ -700,13 +736,17 @@ def import_upstream_keys_as_accounts( row.imported_target_group_id = new_tgid row.imported_target_group_name = tg_name db.commit() + + remote_name = "" + if old_account_id in remote_accounts_map: + remote_name = remote_accounts_map[old_account_id].get("name") or "" items.append(ImportAccountItem( upstream_key_id=row.id, source_group_id=row.group_id, source_group_name=row.group_name, target_group_id=body.target_group_map.get(row.group_id), account_id=old_account_id, - account_name=f"{body.account_name_prefix}-{row.group_name or row.group_id}-{row.id}", + account_name=remote_name or expected_new_name, platform=platform, upstream_base_url=upstream_base_url, status="exists", @@ -750,7 +790,7 @@ def import_upstream_keys_as_accounts( numeric_target = _numeric_group_id(target_group_id) if numeric_target is not None: group_ids.append(numeric_target) - account_name = f"{body.account_name_prefix}-{row.group_name or row.group_id}-{row.id}" + account_name = expected_new_name account_body = { "name": account_name, "platform": platform, @@ -921,7 +961,8 @@ def organize_website_groups( if not uid or not gid: continue - upstream_name = db.query(Upstream.name).filter(Upstream.id == uid).scalar() or f"#{uid}" + _u = db.query(Upstream).filter(Upstream.id == uid).first() + upstream_name = _u.name if _u else f"#{uid}" # 来源名称优先取 src.group_name,再 fallback 快照,再 fallback ID source_group_name = src.get("group_name") or "" @@ -966,11 +1007,8 @@ def organize_website_groups( ) continue - # 查询来源上游的 Base URL 用于账号创建 - upstream_base_url = "" - _u = db.query(Upstream).filter(Upstream.id == uid).first() - if _u: - upstream_base_url = _u.base_url + # 获取来源上游的 Base URL 用于账号创建 + upstream_base_url = _u.base_url if _u else "" for row in keys: # 跳过状态为 failed 的 Key @@ -1115,7 +1153,12 @@ def organize_website_groups( else: group_ids.append(target_group_id) - account_name = f"SmartUp-{row.group_name or row.group_id}-{row.id}" + account_name = build_imported_account_name( + upstream_name=upstream_name, + group_name=row.group_name, + group_id=row.group_id, + key_id=row.id, + ) priority_val = rate_priority_map.get((str(target_group_id), row.upstream_id, row.group_id), 1) account_body = { "name": account_name, diff --git a/backend/app/services/website_sync.py b/backend/app/services/website_sync.py index fef13de..3ab4e10 100644 --- a/backend/app/services/website_sync.py +++ b/backend/app/services/website_sync.py @@ -22,6 +22,12 @@ from app.utils.number import fixed_decimal_string logger = logging.getLogger(__name__) +def build_imported_account_name(upstream_name: str, group_name: str | None, group_id: str, key_id: int) -> str: + """构建导入的账号名称:{上游网站名}-{上游分组名或分组ID}-{KeyID}""" + grp = group_name if group_name else group_id + return f"{upstream_name}-{grp}-{key_id}" + + PRIORITY_BASE = 1 PRIORITY_STEP = 10 diff --git a/backend/test_organize_groups.py b/backend/test_organize_groups.py index 3e703aa..39790e1 100644 --- a/backend/test_organize_groups.py +++ b/backend/test_organize_groups.py @@ -156,6 +156,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch): return {"id": account_id, "group_ids": body.get("group_ids")} monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient) + monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: []) # 5. 执行一键整理 response = organize_website_groups(wid=w.id, db=db_session) @@ -175,7 +176,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch): item_g1 = next(item for item in items if item.target_group_id == "TG1" and item.source_group_id == "G1") assert item_g1.status == "created" assert item_g1.key_name == "Key-G1" - assert item_g1.account_id.startswith("NEW-SmartUp-") + assert item_g1.account_id.startswith("NEW-U1-") # TG1 分组下的 G2 (缺少 Key) item_g2 = next(item for item in items if item.target_group_id == "TG1" and item.source_group_id == "G2") @@ -196,7 +197,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch): item_g4 = next(item for item in items if item.target_group_id == "TG2" and item.source_group_id == "G4") assert item_g4.status == "recreated" assert item_g4.message == "清理后重建账号" - assert item_g4.account_id.startswith("NEW-SmartUp-") + assert item_g4.account_id.startswith("NEW-U1-") # 7. 检查数据库中 Key 记录的状态变化 db_session.refresh(k1) @@ -293,6 +294,7 @@ def test_organize_groups_list_accounts_none_conservatively_skips(db_session, mon return new_acc monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient) + monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: []) # 执行一键整理 response = organize_website_groups(wid=w.id, db=db_session) @@ -309,7 +311,7 @@ def test_organize_groups_list_accounts_none_conservatively_skips(db_session, mon # G1 (未导入) 仍应该成功创建账号 item_g1 = next(item for item in items if item.source_group_id == "G1") assert item_g1.status == "created" - assert item_g1.account_id.startswith("NEW-SmartUp-") + assert item_g1.account_id.startswith("NEW-U1-") assert item_g1.source_group_name == "G1-SourceGroup" # 校验 group_name 优先从绑定中获取! # G3 (已导入) 应该保守跳过 diff --git a/backend/test_priority_algorithm_correction.py b/backend/test_priority_algorithm_correction.py index e2076d0..e54fb74 100644 --- a/backend/test_priority_algorithm_correction.py +++ b/backend/test_priority_algorithm_correction.py @@ -171,6 +171,63 @@ def test_organize_website_groups_priority(db_session, monkeypatch): assert acc1["priority"] == 1 assert acc2["priority"] == 11 + # Assert new naming format: {upstream_name}-{group_name/id}-{key_id} + assert acc1["name"] == f"{u.name}-G1-{k1.id}" + assert acc2["name"] == f"{u.name}-G2-{k2.id}" + # Assert website level priority sync was triggered once at the end assert len(sync_called) == 1 assert sync_called[0][0] == w.id + + +def test_naming_format_on_manual_import(db_session, monkeypatch): + """验证手动导入创建账号名称为 {upstream.name}-{group_name/id}-{key_id},且 account_name_prefix 传入其他值也不影响新建。""" + w = Website(name="W1", base_url="http://w1", enabled=True, site_type="sub2api", auth_config_json="{}", timeout_seconds=30) + u = Upstream(name="U1", base_url="http://u1") + db_session.add_all([w, u]) + db_session.commit() + db_session.refresh(w); db_session.refresh(u) + + k1 = UpstreamGeneratedKey(upstream_id=u.id, group_id="G1", group_name="G1-Name", key_name="K1", key_value="V1") + k2 = UpstreamGeneratedKey(upstream_id=u.id, group_id="G2", group_name=None, key_name="K2", key_value="V2") + db_session.add_all([k1, k2]) + db_session.commit() + + created_accounts = [] + + class FakeClient: + def __init__(self, *args, **kwargs): pass + def __enter__(self): return self + def __exit__(self, *args): pass + def get_groups(self, *args, **kwargs): + return [] + def list_accounts(self): + return [] + def create_account(self, body): + created_accounts.append(body) + return {"id": f"remote-{len(created_accounts)}", "name": body["name"]} + def extract_id(self, data): + return data.get("id") + + monkeypatch.setattr("app.routers.websites._client", lambda website: FakeClient()) + monkeypatch.setattr("app.services.website_client.Sub2ApiWebsiteClient", FakeClient) + + req = ImportAccountsRequest( + upstream_key_ids=[k1.id, k2.id], + target_group_map={}, + auto_priority_by_rate=False, + priority=10, + account_name_prefix="deprecated-prefix-that-should-be-ignored", + default_platform="openai", + ) + + import_upstream_keys_as_accounts(w.id, req, db_session) + + # 验证新创建的账号名字 + assert len(created_accounts) == 2 + acc1 = next(a for a in created_accounts if "G1" in a["name"]) + acc2 = next(a for a in created_accounts if "G2" in a["name"]) + + # 优先使用 group_name,若为空使用 group_id + assert acc1["name"] == f"{u.name}-G1-Name-{k1.id}" + assert acc2["name"] == f"{u.name}-G2-{k2.id}"