feat: 一键整理强制对齐分组逻辑,移除旧托管分组并优化统计消息区分

This commit is contained in:
SmartUp Developer
2026-07-02 10:42:18 +08:00
parent c797aff773
commit fe3f9f4a4c
2 changed files with 202 additions and 25 deletions
+38 -16
View File
@@ -1300,6 +1300,22 @@ def organize_website_groups(
.all() .all()
) )
smartup_managed_group_ids = {str(b.target_group_id) for b in bindings if b.target_group_id}
# 合并已导入 Key 上历史记录的 imported_target_group_id,以确保变更绑定后能清理旧分组
hist_group_ids = (
db.query(UpstreamGeneratedKey.imported_target_group_id)
.filter(
UpstreamGeneratedKey.imported_website_id == wid,
UpstreamGeneratedKey.imported_target_group_id.isnot(None),
UpstreamGeneratedKey.imported_target_group_id != "",
)
.distinct()
.all()
)
for row in hist_group_ids:
smartup_managed_group_ids.add(str(row[0]))
items: list[OrganizeGroupsItem] = [] items: list[OrganizeGroupsItem] = []
# 2. 收集所有的 upstream_id 并执行对账,以保证本地 Key 的状态最新 # 2. 收集所有的 upstream_id 并执行对账,以保证本地 Key 的状态最新
@@ -1474,28 +1490,30 @@ def organize_website_groups(
remote_acc = remote_account_map.get(str(old_account_id)) remote_acc = remote_account_map.get(str(old_account_id))
if remote_acc is not None: if remote_acc is not None:
# 目标账号存在:跳过或补齐目标分组记录 # 目标账号存在:检测并对齐目标分组,移除其余已启用的 SmartUp 托管分组,保留非 SmartUp 托管分组
try: try:
current_group_ids = remote_acc.get("group_ids") or [] current_group_ids = remote_acc.get("group_ids") or []
if not isinstance(current_group_ids, list): if not isinstance(current_group_ids, list):
current_group_ids = [current_group_ids] current_group_ids = [current_group_ids]
str_group_ids = [str(g) for g in current_group_ids]
if str(target_group_id) not in str_group_ids:
# 需要补齐绑定关系
numeric_target = _numeric_group_id(target_group_id) numeric_target = _numeric_group_id(target_group_id)
target_val = numeric_target if numeric_target is not None else target_group_id target_val = numeric_target if numeric_target is not None else target_group_id
new_group_ids = list(current_group_ids) # 过滤出非 SmartUp 托管的 group_ids
if target_val not in new_group_ids: new_group_ids = [g for g in current_group_ids if str(g) not in smartup_managed_group_ids]
# 追加当前目标分组
new_group_ids.append(target_val) new_group_ids.append(target_val)
old_set = {str(g) for g in current_group_ids}
new_set = {str(g) for g in new_group_ids}
if old_set != new_set:
c.update_account(old_account_id, {"group_ids": new_group_ids}) c.update_account(old_account_id, {"group_ids": new_group_ids})
# 顺便更新下 remote_account_map 中的数据,防止同个账号后续在其他绑定中重复补齐 # 顺便更新下 remote_account_map 中的数据,防止同个账号后续在其他绑定中重复处理
remote_acc["group_ids"] = new_group_ids remote_acc["group_ids"] = new_group_ids
msg = "账号已存在,已补齐目标分组绑定" msg = "已存在,已迁移到目标分组"
else: else:
msg = "已存在且已绑定,已跳过" msg = "已存在且已对齐,已跳过"
# 补齐本地 DB 的目标分组标记 # 补齐本地 DB 的目标分组标记
if row.imported_target_group_id != target_group_id: if row.imported_target_group_id != target_group_id:
@@ -1638,17 +1656,21 @@ def organize_website_groups(
) )
created_count = sum(1 for item in items if item.status == "created") created_count = sum(1 for item in items if item.status == "created")
exists_count = sum(1 for item in items if item.status == "exists")
missing_key_count = sum(1 for item in items if item.status == "missing_key")
recreated_count = sum(1 for item in items if item.status == "recreated") recreated_count = sum(1 for item in items if item.status == "recreated")
aligned_count = sum(1 for item in items if item.status == "exists" and "已对齐" in item.message)
migrated_count = sum(1 for item in items if item.status == "exists" and "已迁移" in item.message)
missing_key_count = sum(1 for item in items if item.status == "missing_key")
failed_count = sum(1 for item in items if item.status == "failed") failed_count = sum(1 for item in items if item.status == "failed")
parts = [] parts = []
total_created = created_count + recreated_count if created_count:
if total_created: parts.append(f"已创建 {created_count}")
parts.append(f"创建账号 {total_created}") if recreated_count:
if exists_count: parts.append(f"已重建 {recreated_count}")
parts.append(f"已存在 {exists_count}") if aligned_count:
parts.append(f"已存在且已对齐 {aligned_count}")
if migrated_count:
parts.append(f"已存在已迁移 {migrated_count}")
if missing_key_count: if missing_key_count:
parts.append(f"缺少 Key {missing_key_count}") parts.append(f"缺少 Key {missing_key_count}")
if failed_count: if failed_count:
+161 -6
View File
@@ -163,9 +163,10 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch):
# 6. 断言结果 # 6. 断言结果
assert response.success is True assert response.success is True
# 整理完成:创建账号 2 (k1创建 + k4重建) / 已存在 1 (k3已存在并补齐) / 缺少 Key 1 (G2缺少) # 整理完成:创建 1 / 已重建 1 / 已存在已迁移 1 / 缺少 Key 1
assert "创建账号 2" in response.message assert "创建 1" in response.message
assert "存在 1" in response.message assert "重建 1" in response.message
assert "已存在已迁移 1" in response.message
assert "缺少 Key 1" in response.message assert "缺少 Key 1" in response.message
items = response.items items = response.items
@@ -186,7 +187,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch):
# TG2 分组下的 G3 (已存在并补齐) # TG2 分组下的 G3 (已存在并补齐)
item_g3 = next(item for item in items if item.target_group_id == "TG2" and item.source_group_id == "G3") item_g3 = next(item for item in items if item.target_group_id == "TG2" and item.source_group_id == "G3")
assert item_g3.status == "exists" assert item_g3.status == "exists"
assert "补齐目标分组绑定" in item_g3.message assert "迁移到目标分组" in item_g3.message
# 校验是否触发了 update_account 补齐了 TG2 # 校验是否触发了 update_account 补齐了 TG2
assert len(updated_accounts) == 1 assert len(updated_accounts) == 1
assert updated_accounts[0][0] == "ACC-G3" assert updated_accounts[0][0] == "ACC-G3"
@@ -301,8 +302,8 @@ def test_organize_groups_list_accounts_none_conservatively_skips(db_session, mon
# 断言结果:由于其中有 1 个已导入账号校验失败,response.success 应为 False(有失败项) # 断言结果:由于其中有 1 个已导入账号校验失败,response.success 应为 False(有失败项)
assert response.success is False assert response.success is False
# 统计信息中应该有“创建账号 1”与“失败 1” # 统计信息中应该有“创建 1”与“失败 1”
assert "创建账号 1" in response.message assert "创建 1" in response.message
assert "失败 1" in response.message assert "失败 1" in response.message
items = response.items items = response.items
@@ -324,3 +325,157 @@ def test_organize_groups_list_accounts_none_conservatively_skips(db_session, mon
assert k3.imported_website_id == w.id assert k3.imported_website_id == w.id
assert k3.imported_account_id == "ACC-G3-EXISTING" assert k3.imported_account_id == "ACC-G3-EXISTING"
assert k3.status == "imported" assert k3.status == "imported"
def test_organize_groups_force_alignment(db_session, monkeypatch):
"""测试强对齐场景:
1. 已存在账号在旧 SmartUp 分组,整理后移除旧分组并加入新分组。
2. 已存在账号同时有非 SmartUp 分组,整理后保留非 SmartUp 分组。
3. 已存在账号已经完全一致,整理不重复调用更新接口。
4. 多目标分组、多上游来源时,每个账号按自己的当前绑定关系对齐。
"""
w = Website(
name="W1",
site_type="sub2api",
base_url="http://w1",
enabled=True,
auth_config_json="{}",
timeout_seconds=30
)
u1 = Upstream(name="U1", base_url="http://u1")
db_session.add_all([w, u1])
db_session.commit()
db_session.refresh(w)
db_session.refresh(u1)
# 两个绑定关系:
# 绑定 1:目标分组 TG1 ↔ G1
# 绑定 2:目标分组 TG2 ↔ G2
b1 = WebsiteGroupBinding(
website_id=w.id,
target_group_id="TG1",
target_group_name="TG1-Group",
source_groups_json=json.dumps([{"upstream_id": u1.id, "group_id": "G1"}]),
enabled=True
)
b2 = WebsiteGroupBinding(
website_id=w.id,
target_group_id="TG2",
target_group_name="TG2-Group",
source_groups_json=json.dumps([{"upstream_id": u1.id, "group_id": "G2"}]),
enabled=True
)
db_session.add_all([b1, b2])
db_session.commit()
# 3. 建立 3 个 Key,对应 3 个不同的远端账号状态
# k1: 原本在 TG_old (旧 SmartUp 分组),但根据绑定 1 它现在应该在 TG1
k1 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G1",
group_name="G1-Group",
key_name="Key-G1",
key_value="sk-g1-secret",
status="imported",
imported_website_id=w.id,
imported_account_id="ACC-G1",
imported_target_group_id="TG_old", # 旧 SmartUp 分组
)
# k2: 已经在 TG2 (正确),且同时有非 SmartUp 分组 (999) 和另一个旧 SmartUp 分组 (TG1 - 应该被移除)
k2 = UpstreamGeneratedKey(
upstream_id=u1.id,
group_id="G2",
group_name="G2-Group",
key_name="Key-G2",
key_value="sk-g2-secret",
status="imported",
imported_website_id=w.id,
imported_account_id="ACC-G2",
imported_target_group_id="TG2",
)
db_session.add_all([k1, k2])
db_session.commit()
updated_accounts = []
class MockClient:
def __init__(self, **kwargs): pass
def __enter__(self): return self
def __exit__(self, *a): pass
def get_groups(self, *a, **kw):
return [
{"id": "TG1", "name": "TG1-Group"},
{"id": "TG2", "name": "TG2-Group"},
]
def list_accounts(self):
return [
{
"id": "ACC-G1",
"name": "SmartUp-G1",
"group_ids": ["TG_old"], # 包含旧 SmartUp 分组 TG_old,没有 TG1
},
{
"id": "ACC-G2",
"name": "SmartUp-G2",
"group_ids": ["TG2", 999, "TG1"], # TG2 是当前正确分组,999 是非托管分组,TG1 是旧托管分组(应移除)
}
]
def extract_id(self, val):
return val.get("id") if isinstance(val, dict) else str(val)
def update_account(self, account_id, body):
updated_accounts.append((account_id, body))
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: [])
# 执行一键整理
response = organize_website_groups(wid=w.id, db=db_session)
assert response.success is True
# ACC-G1 (G1) 的 group_ids 应变更为 ["TG1"] (TG2 被移除了)
# ACC-G2 (G2) 的 group_ids 应变更为 [999, "TG2"] (TG1 被移除了,999 保留)
assert len(updated_accounts) == 2
# 验证 ACC-G1 变更
up_g1 = next(item for item in updated_accounts if item[0] == "ACC-G1")
assert set(str(g) for g in up_g1[1]["group_ids"]) == {"TG1"}
# 验证 ACC-G2 变更
up_g2 = next(item for item in updated_accounts if item[0] == "ACC-G2")
assert set(str(g) for g in up_g2[1]["group_ids"]) == {"999", "TG2"}
# 验证本地 DB 的标记已被同步更新为正确的 TG1 / TG2
db_session.refresh(k1)
db_session.refresh(k2)
assert k1.imported_target_group_id == "TG1"
assert k2.imported_target_group_id == "TG2"
# 验证结果消息汇总包含 aligned 0, migrated 2 (因为两个都发生了迁移/更新)
assert "已存在已迁移 2" in response.message
# 让我们跑一次完全一致的再次整理,验证不触发 update_account (已对齐)
updated_accounts.clear()
class MockClientAligned(MockClient):
def list_accounts(self):
return [
{
"id": "ACC-G1",
"name": "SmartUp-G1",
"group_ids": ["TG1"],
},
{
"id": "ACC-G2",
"name": "SmartUp-G2",
"group_ids": [999, "TG2"],
}
]
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClientAligned)
response_aligned = organize_website_groups(wid=w.id, db=db_session)
assert response_aligned.success is True
# 既然已经一致,不会触发 update_account
assert len(updated_accounts) == 0
assert "已存在且已对齐 2" in response_aligned.message