feat: 调整新建/重建账号命名格式为 {上游网站名}-{上游分组名/ID}-{KeyID}
This commit is contained in:
@@ -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:
|
||||
# 预载上游以方便获取名称和 base_url
|
||||
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
|
||||
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
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (已导入) 应该保守跳过
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user