feat: 调整新建/重建账号命名格式为 {上游网站名}-{上游分组名/ID}-{KeyID}

This commit is contained in:
liumangmang
2026-07-01 21:11:37 +08:00
parent ebf7def177
commit 94d9550f77
4 changed files with 130 additions and 22 deletions
+60 -17
View File
@@ -49,6 +49,7 @@ from app.services.website_sync import (
reconcile_upstream_keys_full, reconcile_upstream_keys_full,
sync_account_priorities_for_upstream, sync_account_priorities_for_upstream,
latest_rate_map, latest_rate_map,
build_imported_account_name,
) )
from app.utils.auth import get_current_user from app.utils.auth import get_current_user
from app.utils.number import fixed_decimal_number from app.utils.number import fixed_decimal_number
@@ -630,15 +631,11 @@ def import_upstream_keys_as_accounts(
) )
for kid in missing_ids for kid in missing_ids
] ]
# 查出来源上游的 Base URL # 预载上游以方便获取名称和 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 from app.models.upstream import Upstream as _Up
_u = db.query(_Up).filter(_Up.id == first_row.upstream_id).first() upstream_ids = {row.upstream_id for row in rows}
if _u: upstreams_db = db.query(_Up).filter(_Up.id.in_(upstream_ids)).all()
upstream_base_url = _u.base_url upstreams_map = {u.id: u for u in upstreams_db}
# 按倍率自动分配优先级 # 按倍率自动分配优先级
rate_priority_map: dict[tuple[str, int, str], int] = {} rate_priority_map: dict[tuple[str, int, str], int] = {}
@@ -665,6 +662,21 @@ def import_upstream_keys_as_accounts(
except Exception as e: except Exception as e:
logger.warning("failed to fetch groups for website %s at import: %s", wid, 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: for row in rows:
# 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed # 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed
if row.status in ("orphaned", "failed", "import_failed"): if row.status in ("orphaned", "failed", "import_failed"):
@@ -687,10 +699,34 @@ def import_upstream_keys_as_accounts(
else: else:
platform = body.default_platform 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: if row.imported_website_id == wid and row.imported_account_id:
old_account_id = row.imported_account_id old_account_id = 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) exists = c.account_exists(row.imported_account_id)
except Exception:
exists = None
if exists is True: if exists is True:
# 顺手回填 imported_target_group_id(老数据升级后可通过重导自动补齐) # 顺手回填 imported_target_group_id(老数据升级后可通过重导自动补齐)
new_tgid = body.target_group_map.get(row.group_id) or None 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_id = new_tgid
row.imported_target_group_name = tg_name row.imported_target_group_name = tg_name
db.commit() 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( items.append(ImportAccountItem(
upstream_key_id=row.id, upstream_key_id=row.id,
source_group_id=row.group_id, source_group_id=row.group_id,
source_group_name=row.group_name, source_group_name=row.group_name,
target_group_id=body.target_group_map.get(row.group_id), target_group_id=body.target_group_map.get(row.group_id),
account_id=old_account_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, platform=platform,
upstream_base_url=upstream_base_url, upstream_base_url=upstream_base_url,
status="exists", status="exists",
@@ -750,7 +790,7 @@ def import_upstream_keys_as_accounts(
numeric_target = _numeric_group_id(target_group_id) numeric_target = _numeric_group_id(target_group_id)
if numeric_target is not None: if numeric_target is not None:
group_ids.append(numeric_target) 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 = { account_body = {
"name": account_name, "name": account_name,
"platform": platform, "platform": platform,
@@ -921,7 +961,8 @@ def organize_website_groups(
if not uid or not gid: if not uid or not gid:
continue 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 # 来源名称优先取 src.group_name,再 fallback 快照,再 fallback ID
source_group_name = src.get("group_name") or "" source_group_name = src.get("group_name") or ""
@@ -966,11 +1007,8 @@ def organize_website_groups(
) )
continue continue
# 查询来源上游的 Base URL 用于账号创建 # 获取来源上游的 Base URL 用于账号创建
upstream_base_url = "" upstream_base_url = _u.base_url if _u else ""
_u = db.query(Upstream).filter(Upstream.id == uid).first()
if _u:
upstream_base_url = _u.base_url
for row in keys: for row in keys:
# 跳过状态为 failed 的 Key # 跳过状态为 failed 的 Key
@@ -1115,7 +1153,12 @@ def organize_website_groups(
else: else:
group_ids.append(target_group_id) 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) priority_val = rate_priority_map.get((str(target_group_id), row.upstream_id, row.group_id), 1)
account_body = { account_body = {
"name": account_name, "name": account_name,
+6
View File
@@ -22,6 +22,12 @@ from app.utils.number import fixed_decimal_string
logger = logging.getLogger(__name__) 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_BASE = 1
PRIORITY_STEP = 10 PRIORITY_STEP = 10
+5 -3
View File
@@ -156,6 +156,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch):
return {"id": account_id, "group_ids": body.get("group_ids")} return {"id": account_id, "group_ids": body.get("group_ids")}
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient) monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient)
monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: [])
# 5. 执行一键整理 # 5. 执行一键整理
response = organize_website_groups(wid=w.id, db=db_session) 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") 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.status == "created"
assert item_g1.key_name == "Key-G1" 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) # TG1 分组下的 G2 (缺少 Key)
item_g2 = next(item for item in items if item.target_group_id == "TG1" and item.source_group_id == "G2") 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") 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.status == "recreated"
assert item_g4.message == "清理后重建账号" assert item_g4.message == "清理后重建账号"
assert item_g4.account_id.startswith("NEW-SmartUp-") assert item_g4.account_id.startswith("NEW-U1-")
# 7. 检查数据库中 Key 记录的状态变化 # 7. 检查数据库中 Key 记录的状态变化
db_session.refresh(k1) db_session.refresh(k1)
@@ -293,6 +294,7 @@ def test_organize_groups_list_accounts_none_conservatively_skips(db_session, mon
return new_acc return new_acc
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient) 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) 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 (未导入) 仍应该成功创建账号 # G1 (未导入) 仍应该成功创建账号
item_g1 = next(item for item in items if item.source_group_id == "G1") item_g1 = next(item for item in items if item.source_group_id == "G1")
assert item_g1.status == "created" 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 优先从绑定中获取! assert item_g1.source_group_name == "G1-SourceGroup" # 校验 group_name 优先从绑定中获取!
# G3 (已导入) 应该保守跳过 # G3 (已导入) 应该保守跳过
@@ -171,6 +171,63 @@ def test_organize_website_groups_priority(db_session, monkeypatch):
assert acc1["priority"] == 1 assert acc1["priority"] == 1
assert acc2["priority"] == 11 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 website level priority sync was triggered once at the end
assert len(sync_called) == 1 assert len(sync_called) == 1
assert sync_called[0][0] == w.id 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}"