diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index a0f2c68..25df558 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -579,6 +579,16 @@ def import_upstream_keys_as_accounts( pass with _client(website) as c: + target_group_names: dict[str, str] = {} + try: + grps = c.get_groups(endpoint=website.groups_endpoint) or [] + for g in grps: + g_id = g.get("id") + if g_id is not None: + target_group_names[str(g_id)] = g.get("name") or str(g_id) + except Exception as e: + logger.warning("failed to fetch groups for website %s at import: %s", wid, e) + for row in rows: # 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed) if row.status in ("orphaned", "failed", "import_failed"): @@ -608,9 +618,12 @@ def import_upstream_keys_as_accounts( if exists is True: # 顺手回填 imported_target_group_id(老数据升级后可通过重导自动补齐) new_tgid = body.target_group_map.get(row.group_id) or None - if new_tgid and row.imported_target_group_id != new_tgid: - row.imported_target_group_id = new_tgid - db.commit() + if new_tgid: + tg_name = target_group_names.get(str(new_tgid)) + if row.imported_target_group_id != new_tgid or row.imported_target_group_name != tg_name: + row.imported_target_group_id = new_tgid + row.imported_target_group_name = tg_name + db.commit() items.append(ImportAccountItem( upstream_key_id=row.id, source_group_id=row.group_id, @@ -683,7 +696,7 @@ def import_upstream_keys_as_accounts( row.imported_account_id = account_id or None row.imported_at = datetime.now(timezone.utc) row.imported_target_group_id = target_group_id or None - row.imported_target_group_name = None # target_group_map 只存 ID,name 展示用可留 NULL + row.imported_target_group_name = target_group_names.get(str(target_group_id)) if target_group_id else None row.status = "imported" row.error = None db.commit() diff --git a/backend/app/schemas/website.py b/backend/app/schemas/website.py index f2a3a02..7d47c33 100644 --- a/backend/app/schemas/website.py +++ b/backend/app/schemas/website.py @@ -192,6 +192,12 @@ class ReorderPriorityItem(BaseModel): new_priority: Optional[int] = None status: str message: str = "" + target_group_id: Optional[str] = None + target_group_name: Optional[str] = None + account_name: Optional[str] = None + source_group_id: Optional[str] = None + source_group_name: Optional[str] = None + source_rate: Optional[float] = None class ReorderPriorityResponse(BaseModel): diff --git a/backend/app/services/website_sync.py b/backend/app/services/website_sync.py index 41cba74..936df62 100644 --- a/backend/app/services/website_sync.py +++ b/backend/app/services/website_sync.py @@ -247,10 +247,11 @@ def _priority_result( account_name: str | None = None, old_priority: Any = None, source_rate: float | None = None, + target_group_name: str | None = None, ) -> dict: """构建统一的优先级同步结果 dict,支持丰富的展示和后向兼容性。""" tg_id = row.imported_target_group_id or row.group_id - tg_name = row.imported_target_group_name or tg_id + tg_name = target_group_name or row.imported_target_group_name or tg_id return { # 兼容旧字段 "account_id": row.imported_account_id, @@ -547,8 +548,9 @@ def sync_account_priorities_for_upstream( auth_config=json.loads(website.auth_config_json or "{}"), timeout=float(website.timeout_seconds), ) as client: - # 尽量拉取目标网站账号列表以获取账号名称和旧 priority,失败不阻断 + # 尽量拉取目标网站账号列表以获取账号名称和旧 priority,并拉取分组以获取分组名称,失败不阻断 account_info_map: dict[str, dict[str, Any]] = {} + target_group_names: dict[str, str] = {} try: remote_accounts = client.list_accounts() or [] for acc in remote_accounts: @@ -561,18 +563,33 @@ def sync_account_priorities_for_upstream( except Exception as e: logger.warning("failed to fetch remote accounts for website %s: %s", wid, e) + try: + grps = client.get_groups(endpoint=website.groups_endpoint) or [] + for g in grps: + g_id = g.get("id") + if g_id is not None: + target_group_names[str(g_id)] = g.get("name") or str(g_id) + except Exception as e: + logger.warning("failed to fetch groups for website %s at sync: %s", wid, e) + for comp_rows in competitive_buckets.values(): for row in comp_rows: account_id = row.imported_account_id new_priority = priority_assignment.get(account_id) if new_priority is None: continue - + acc_info = account_info_map.get(str(account_id)) or {} acc_name = acc_info.get("name") or str(account_id) old_prio = acc_info.get("priority") src_rate = raw_rate_map.get(f"{row.upstream_id}:{row.group_id}") + tg_id_str = str(row.imported_target_group_id or row.group_id) + tg_name = target_group_names.get(tg_id_str) + if tg_name and row.imported_target_group_name != tg_name: + row.imported_target_group_name = tg_name + db.commit() + try: client.update_account(account_id, {"priority": new_priority}) logger.info( @@ -590,6 +607,7 @@ def sync_account_priorities_for_upstream( account_name=acc_name, old_priority=old_prio, source_rate=src_rate, + target_group_name=tg_name or row.imported_target_group_name, ) ) except Exception as exc: @@ -606,6 +624,7 @@ def sync_account_priorities_for_upstream( account_name=acc_name, old_priority=old_prio, source_rate=src_rate, + target_group_name=tg_name or row.imported_target_group_name, ) ) except Exception as exc: diff --git a/backend/app/utils/dingtalk.py b/backend/app/utils/dingtalk.py index 9af6416..f2c62f5 100644 --- a/backend/app/utils/dingtalk.py +++ b/backend/app/utils/dingtalk.py @@ -93,18 +93,18 @@ def format_dingtalk_priority_changed( emoji = {"success": "✅", "failed": "❌", "skipped": "⏭️"}.get(u.get("status", ""), "➖") acc_name = u.get("account_name") or u.get("account_id") or "未知账号" acc_id = u.get("account_id") or "未知ID" - + old_p = u.get("old_priority") old_p_str = "未知" if old_p in (None, "", "未知") else str(old_p) - + new_p = u.get("new_priority") new_p_str = "未知" if new_p in (None, "", "未知") else str(new_p) - + src_group = u.get("source_group_name") or u.get("source_group_id") or "未知来源" - + rate = u.get("source_rate") rate_str = str(rate) if rate is not None else "—" - + line = f"- {emoji} {acc_name}(账号ID {acc_id}):old={old_p_str} → new={new_p_str},来源分组={src_group},倍率={rate_str}" if u.get("status") == "failed" and u.get("message"): line += f",原因={u.get('message')}" diff --git a/backend/test_priority_sync.py b/backend/test_priority_sync.py index e82e5d7..e4a08c2 100644 --- a/backend/test_priority_sync.py +++ b/backend/test_priority_sync.py @@ -619,6 +619,10 @@ def test_priority_sync_fetches_remote_accounts_and_backfills_info(db_session, mo {"id": "A1", "name": "SmartUp-A1-Name", "priority": 15}, {"id": "A2", "name": "SmartUp-A2-Name", "priority": 25}, ] + def get_groups(self, *args, **kwargs): + return [ + {"id": "TG1", "name": "TG1-Readable-Group-Name"} + ] def update_account(self, account_id, data): update_calls.append((account_id, data)) @@ -634,7 +638,7 @@ def test_priority_sync_fetches_remote_accounts_and_backfills_info(db_session, mo assert res_a1["new_priority"] == 1 assert res_a1["source_rate"] == 1.0 assert res_a1["target_group_id"] == "TG1" - assert res_a1["target_group_name"] == "TG1" # fallback to tg_id since target_group_name is not set + assert res_a1["target_group_name"] == "TG1-Readable-Group-Name" res_a2 = next(r for r in results if r["account_id"] == "A2") assert res_a2["account_name"] == "SmartUp-A2-Name" @@ -642,6 +646,10 @@ def test_priority_sync_fetches_remote_accounts_and_backfills_info(db_session, mo assert res_a2["new_priority"] == 11 assert res_a2["source_rate"] == 2.0 + # 验证本地 DB 中行的 imported_target_group_name 被正确更新写入 + row1 = db_session.query(UpstreamGeneratedKey).filter_by(imported_account_id="A1").first() + assert row1.imported_target_group_name == "TG1-Readable-Group-Name" + def test_priority_sync_graceful_on_remote_list_failure(db_session, monkeypatch): """验证 list_accounts 抛异常时,优先级同步依然成功更新,且各项降级为账号 ID 和未知。""" @@ -677,4 +685,3 @@ def test_priority_sync_graceful_on_remote_list_failure(db_session, monkeypatch): assert res_a1["old_priority"] is None assert res_a1["new_priority"] == 1 assert res_a1["status"] == "success" - diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f9c8091..4705520 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -299,6 +299,12 @@ export interface ReorderPriorityItem { new_priority: number | null status: string message: string + target_group_id?: string | null + target_group_name?: string | null + account_name?: string | null + source_group_id?: string | null + source_group_name?: string | null + source_rate?: number | null } export interface WebsiteBatchSyncResponse {