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
+161 -6
View File
@@ -163,9 +163,10 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch):
# 6. 断言结果
assert response.success is True
# 整理完成:创建账号 2 (k1创建 + k4重建) / 已存在 1 (k3已存在并补齐) / 缺少 Key 1 (G2缺少)
assert "创建账号 2" in response.message
assert "存在 1" in response.message
# 整理完成:创建 1 / 已重建 1 / 已存在已迁移 1 / 缺少 Key 1
assert "创建 1" in response.message
assert "重建 1" in response.message
assert "已存在已迁移 1" in response.message
assert "缺少 Key 1" in response.message
items = response.items
@@ -186,7 +187,7 @@ def test_organize_groups_full_scenarios(db_session, monkeypatch):
# TG2 分组下的 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 "补齐目标分组绑定" in item_g3.message
assert "迁移到目标分组" in item_g3.message
# 校验是否触发了 update_account 补齐了 TG2
assert len(updated_accounts) == 1
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(有失败项)
assert response.success is False
# 统计信息中应该有“创建账号 1”与“失败 1”
assert "创建账号 1" in response.message
# 统计信息中应该有“创建 1”与“失败 1”
assert "创建 1" in response.message
assert "失败 1" in response.message
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_account_id == "ACC-G3-EXISTING"
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