feat: 一键整理强制对齐分组逻辑,移除旧托管分组并优化统计消息区分
This commit is contained in:
@@ -1300,6 +1300,22 @@ def organize_website_groups(
|
||||
.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] = []
|
||||
|
||||
# 2. 收集所有的 upstream_id 并执行对账,以保证本地 Key 的状态最新
|
||||
@@ -1474,28 +1490,30 @@ def organize_website_groups(
|
||||
remote_acc = remote_account_map.get(str(old_account_id))
|
||||
|
||||
if remote_acc is not None:
|
||||
# 目标账号存在:跳过或补齐目标分组记录
|
||||
# 目标账号存在:检测并对齐目标分组,移除其余已启用的 SmartUp 托管分组,保留非 SmartUp 托管分组
|
||||
try:
|
||||
current_group_ids = remote_acc.get("group_ids") or []
|
||||
if not isinstance(current_group_ids, list):
|
||||
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)
|
||||
target_val = numeric_target if numeric_target is not None else target_group_id
|
||||
|
||||
new_group_ids = list(current_group_ids)
|
||||
if target_val not in new_group_ids:
|
||||
# 过滤出非 SmartUp 托管的 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)
|
||||
|
||||
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})
|
||||
# 顺便更新下 remote_account_map 中的数据,防止同个账号后续在其他绑定中重复补齐
|
||||
# 顺便更新下 remote_account_map 中的数据,防止同个账号后续在其他绑定中重复处理
|
||||
remote_acc["group_ids"] = new_group_ids
|
||||
msg = "账号已存在,已补齐目标分组绑定"
|
||||
msg = "已存在,已迁移到目标分组"
|
||||
else:
|
||||
msg = "已存在且已绑定,已跳过"
|
||||
msg = "已存在且已对齐,已跳过"
|
||||
|
||||
# 补齐本地 DB 的目标分组标记
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
|
||||
parts = []
|
||||
total_created = created_count + recreated_count
|
||||
if total_created:
|
||||
parts.append(f"创建账号 {total_created}")
|
||||
if exists_count:
|
||||
parts.append(f"已存在 {exists_count}")
|
||||
if created_count:
|
||||
parts.append(f"已创建 {created_count}")
|
||||
if recreated_count:
|
||||
parts.append(f"已重建 {recreated_count}")
|
||||
if aligned_count:
|
||||
parts.append(f"已存在且已对齐 {aligned_count}")
|
||||
if migrated_count:
|
||||
parts.append(f"已存在已迁移 {migrated_count}")
|
||||
if missing_key_count:
|
||||
parts.append(f"缺少 Key {missing_key_count}")
|
||||
if failed_count:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user