fix: 优化并发数批量设置接口,引入 c.close() 资源管理并增加对 list_accounts() 返回 None 的拦截保护
This commit is contained in:
+130
-125
@@ -1123,156 +1123,161 @@ def set_website_accounts_concurrency(
|
|||||||
if website.site_type != "sub2api":
|
if website.site_type != "sub2api":
|
||||||
raise HTTPException(400, "only sub2api site supports account concurrency settings")
|
raise HTTPException(400, "only sub2api site supports account concurrency settings")
|
||||||
|
|
||||||
c = _client(website)
|
with _client(website) as c:
|
||||||
|
try:
|
||||||
|
remote_accounts = c.list_accounts()
|
||||||
|
except Exception as e:
|
||||||
|
return SetConcurrencyResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"拉取远端账号列表失败: {e}",
|
||||||
|
items=[]
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
if remote_accounts is None:
|
||||||
remote_accounts = c.list_accounts()
|
return SetConcurrencyResponse(
|
||||||
except Exception as e:
|
success=False,
|
||||||
return SetConcurrencyResponse(
|
message="拉取远端账号列表失败,无法设置并发",
|
||||||
success=False,
|
items=[]
|
||||||
message=f"拉取远端账号列表失败: {e}",
|
)
|
||||||
items=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
remote_map = {}
|
remote_map = {}
|
||||||
if remote_accounts:
|
|
||||||
for acc in remote_accounts:
|
for acc in remote_accounts:
|
||||||
aid = c.extract_id(acc)
|
aid = c.extract_id(acc)
|
||||||
if aid:
|
if aid:
|
||||||
remote_map[aid] = acc
|
remote_map[aid] = acc
|
||||||
|
|
||||||
keys = db.query(UpstreamGeneratedKey).filter(
|
keys = db.query(UpstreamGeneratedKey).filter(
|
||||||
UpstreamGeneratedKey.imported_website_id == wid,
|
UpstreamGeneratedKey.imported_website_id == wid,
|
||||||
UpstreamGeneratedKey.imported_account_id.isnot(None)
|
UpstreamGeneratedKey.imported_account_id.isnot(None)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
candidates = {}
|
candidates = {}
|
||||||
for key in keys:
|
for key in keys:
|
||||||
aid = key.imported_account_id
|
aid = key.imported_account_id
|
||||||
if aid not in candidates:
|
if aid not in candidates:
|
||||||
candidates[aid] = {
|
candidates[aid] = {
|
||||||
"account_id": aid,
|
"account_id": aid,
|
||||||
"db_key": key
|
"db_key": key
|
||||||
}
|
}
|
||||||
|
|
||||||
if not candidates:
|
if not candidates:
|
||||||
return SetConcurrencyResponse(
|
return SetConcurrencyResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message="没有找到 SmartUp 导入的有效账号",
|
message="没有找到 SmartUp 导入的有效账号",
|
||||||
items=[]
|
items=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
bulk_candidates = []
|
bulk_candidates = []
|
||||||
skipped_items = []
|
skipped_items = []
|
||||||
|
|
||||||
for aid, cand in candidates.items():
|
for aid, cand in candidates.items():
|
||||||
remote_acc = remote_map.get(aid)
|
remote_acc = remote_map.get(aid)
|
||||||
if not remote_acc:
|
if not remote_acc:
|
||||||
skipped_items.append(SetConcurrencyItem(
|
skipped_items.append(SetConcurrencyItem(
|
||||||
account_id=aid,
|
account_id=aid,
|
||||||
account_name=None,
|
account_name=None,
|
||||||
old_concurrency=None,
|
old_concurrency=None,
|
||||||
target_concurrency=body.concurrency,
|
target_concurrency=body.concurrency,
|
||||||
status="skipped",
|
status="skipped",
|
||||||
message="账号在远端已被删除或不存在"
|
message="账号在远端已被删除或不存在"
|
||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
acc_name = remote_acc.get("name")
|
acc_name = remote_acc.get("name")
|
||||||
old_con = remote_acc.get("concurrency")
|
old_con = remote_acc.get("concurrency")
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(aid)
|
||||||
|
bulk_candidates.append({
|
||||||
|
"account_id": aid,
|
||||||
|
"account_name": acc_name,
|
||||||
|
"old_concurrency": old_con
|
||||||
|
})
|
||||||
|
except ValueError:
|
||||||
|
skipped_items.append(SetConcurrencyItem(
|
||||||
|
account_id=aid,
|
||||||
|
account_name=acc_name,
|
||||||
|
old_concurrency=old_con,
|
||||||
|
target_concurrency=body.concurrency,
|
||||||
|
status="skipped",
|
||||||
|
message="账号 ID 无法转换为数字,跳过批量更新"
|
||||||
|
))
|
||||||
|
|
||||||
|
items = []
|
||||||
|
items.extend(skipped_items)
|
||||||
|
|
||||||
|
if not bulk_candidates:
|
||||||
|
success = all(item.status == "skipped" for item in items)
|
||||||
|
return SetConcurrencyResponse(
|
||||||
|
success=success,
|
||||||
|
message=f"无可执行批量更新 of 账号。已跳过 {len(skipped_items)} 个",
|
||||||
|
items=items
|
||||||
|
)
|
||||||
|
|
||||||
|
bulk_ids = [item["account_id"] for item in bulk_candidates]
|
||||||
|
bulk_success = False
|
||||||
|
bulk_error_msg = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
int(aid)
|
c.bulk_update_accounts(bulk_ids, {"concurrency": body.concurrency})
|
||||||
bulk_candidates.append({
|
bulk_success = True
|
||||||
"account_id": aid,
|
except Exception as e:
|
||||||
"account_name": acc_name,
|
bulk_error_msg = str(e)
|
||||||
"old_concurrency": old_con
|
logger.warning("bulk_update_accounts failed, falling back to individual updates: %s", e)
|
||||||
})
|
|
||||||
except ValueError:
|
|
||||||
skipped_items.append(SetConcurrencyItem(
|
|
||||||
account_id=aid,
|
|
||||||
account_name=acc_name,
|
|
||||||
old_concurrency=old_con,
|
|
||||||
target_concurrency=body.concurrency,
|
|
||||||
status="skipped",
|
|
||||||
message="账号 ID 无法转换为数字,跳过批量更新"
|
|
||||||
))
|
|
||||||
|
|
||||||
items = []
|
if bulk_success:
|
||||||
items.extend(skipped_items)
|
for item in bulk_candidates:
|
||||||
|
|
||||||
if not bulk_candidates:
|
|
||||||
success = all(item.status == "skipped" for item in items)
|
|
||||||
return SetConcurrencyResponse(
|
|
||||||
success=success,
|
|
||||||
message=f"无可执行批量更新的账号。已跳过 {len(skipped_items)} 个",
|
|
||||||
items=items
|
|
||||||
)
|
|
||||||
|
|
||||||
bulk_ids = [item["account_id"] for item in bulk_candidates]
|
|
||||||
bulk_success = False
|
|
||||||
bulk_error_msg = ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
c.bulk_update_accounts(bulk_ids, {"concurrency": body.concurrency})
|
|
||||||
bulk_success = True
|
|
||||||
except Exception as e:
|
|
||||||
bulk_error_msg = str(e)
|
|
||||||
logger.warning("bulk_update_accounts failed, falling back to individual updates: %s", e)
|
|
||||||
|
|
||||||
if bulk_success:
|
|
||||||
for item in bulk_candidates:
|
|
||||||
items.append(SetConcurrencyItem(
|
|
||||||
account_id=item["account_id"],
|
|
||||||
account_name=item["account_name"],
|
|
||||||
old_concurrency=item["old_concurrency"],
|
|
||||||
target_concurrency=body.concurrency,
|
|
||||||
status="success",
|
|
||||||
message="成功"
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
for item in bulk_candidates:
|
|
||||||
aid = item["account_id"]
|
|
||||||
try:
|
|
||||||
c.update_account(aid, {"concurrency": body.concurrency})
|
|
||||||
items.append(SetConcurrencyItem(
|
items.append(SetConcurrencyItem(
|
||||||
account_id=aid,
|
account_id=item["account_id"],
|
||||||
account_name=item["account_name"],
|
account_name=item["account_name"],
|
||||||
old_concurrency=item["old_concurrency"],
|
old_concurrency=item["old_concurrency"],
|
||||||
target_concurrency=body.concurrency,
|
target_concurrency=body.concurrency,
|
||||||
status="success",
|
status="success",
|
||||||
message="成功 (逐个更新)"
|
message="成功"
|
||||||
))
|
|
||||||
except Exception as e:
|
|
||||||
items.append(SetConcurrencyItem(
|
|
||||||
account_id=aid,
|
|
||||||
account_name=item["account_name"],
|
|
||||||
old_concurrency=item["old_concurrency"],
|
|
||||||
target_concurrency=body.concurrency,
|
|
||||||
status="failed",
|
|
||||||
message=f"更新失败: {e}"
|
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
for item in bulk_candidates:
|
||||||
|
aid = item["account_id"]
|
||||||
|
try:
|
||||||
|
c.update_account(aid, {"concurrency": body.concurrency})
|
||||||
|
items.append(SetConcurrencyItem(
|
||||||
|
account_id=aid,
|
||||||
|
account_name=item["account_name"],
|
||||||
|
old_concurrency=item["old_concurrency"],
|
||||||
|
target_concurrency=body.concurrency,
|
||||||
|
status="success",
|
||||||
|
message="成功 (逐个更新)"
|
||||||
|
))
|
||||||
|
except Exception as e:
|
||||||
|
items.append(SetConcurrencyItem(
|
||||||
|
account_id=aid,
|
||||||
|
account_name=item["account_name"],
|
||||||
|
old_concurrency=item["old_concurrency"],
|
||||||
|
target_concurrency=body.concurrency,
|
||||||
|
status="failed",
|
||||||
|
message=f"更新失败: {e}"
|
||||||
|
))
|
||||||
|
|
||||||
success_count = sum(1 for item in items if item.status == "success")
|
success_count = sum(1 for item in items if item.status == "success")
|
||||||
failed_count = sum(1 for item in items if item.status == "failed")
|
failed_count = sum(1 for item in items if item.status == "failed")
|
||||||
skip_count = sum(1 for item in items if item.status == "skipped")
|
skip_count = sum(1 for item in items if item.status == "skipped")
|
||||||
|
|
||||||
success = (failed_count == 0)
|
success = (failed_count == 0)
|
||||||
msg_parts = []
|
msg_parts = []
|
||||||
if success_count:
|
if success_count:
|
||||||
msg_parts.append(f"成功 {success_count} 个")
|
msg_parts.append(f"成功 {success_count} 个")
|
||||||
if failed_count:
|
if failed_count:
|
||||||
msg_parts.append(f"失败 {failed_count} 个")
|
msg_parts.append(f"失败 {failed_count} 个")
|
||||||
if skip_count:
|
if skip_count:
|
||||||
msg_parts.append(f"跳过 {skip_count} 个")
|
msg_parts.append(f"跳过 {skip_count} 个")
|
||||||
|
|
||||||
message = "设置账号并发执行完毕:" + ",".join(msg_parts)
|
message = "设置账号并发执行完毕:" + ",".join(msg_parts)
|
||||||
|
|
||||||
return SetConcurrencyResponse(
|
return SetConcurrencyResponse(
|
||||||
success=success,
|
success=success,
|
||||||
message=message,
|
message=message,
|
||||||
items=items
|
items=items
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/websites/{wid}/groups/organize", response_model=OrganizeGroupsResponse)
|
@router.post("/api/websites/{wid}/groups/organize", response_model=OrganizeGroupsResponse)
|
||||||
|
|||||||
@@ -98,8 +98,17 @@ def test_set_website_accounts_concurrency_workflow(db_session, monkeypatch):
|
|||||||
bulk_calls = []
|
bulk_calls = []
|
||||||
update_calls = []
|
update_calls = []
|
||||||
list_calls_count = 0
|
list_calls_count = 0
|
||||||
|
closed_count = 0
|
||||||
|
|
||||||
class MockClient:
|
class MockClient:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
nonlocal closed_count
|
||||||
|
closed_count += 1
|
||||||
|
return False
|
||||||
|
|
||||||
def list_accounts(self):
|
def list_accounts(self):
|
||||||
nonlocal list_calls_count
|
nonlocal list_calls_count
|
||||||
list_calls_count += 1
|
list_calls_count += 1
|
||||||
@@ -146,6 +155,8 @@ def test_set_website_accounts_concurrency_workflow(db_session, monkeypatch):
|
|||||||
assert len(bulk_calls) == 1
|
assert len(bulk_calls) == 1
|
||||||
assert bulk_calls[0] == (["1001"], {"concurrency": 200})
|
assert bulk_calls[0] == (["1001"], {"concurrency": 200})
|
||||||
assert len(update_calls) == 0
|
assert len(update_calls) == 0
|
||||||
|
# 验证是否调用了 __exit__ 关闭连接
|
||||||
|
assert closed_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_set_website_accounts_concurrency_fallback(db_session, monkeypatch):
|
def test_set_website_accounts_concurrency_fallback(db_session, monkeypatch):
|
||||||
@@ -179,8 +190,17 @@ def test_set_website_accounts_concurrency_fallback(db_session, monkeypatch):
|
|||||||
|
|
||||||
bulk_calls = []
|
bulk_calls = []
|
||||||
update_calls = []
|
update_calls = []
|
||||||
|
closed_count = 0
|
||||||
|
|
||||||
class MockClientError:
|
class MockClientError:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
nonlocal closed_count
|
||||||
|
closed_count += 1
|
||||||
|
return False
|
||||||
|
|
||||||
def list_accounts(self):
|
def list_accounts(self):
|
||||||
return [{"id": 1001, "name": "acc-1001", "concurrency": 10}]
|
return [{"id": 1001, "name": "acc-1001", "concurrency": 10}]
|
||||||
|
|
||||||
@@ -210,3 +230,42 @@ def test_set_website_accounts_concurrency_fallback(db_session, monkeypatch):
|
|||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0].status == "success"
|
assert items[0].status == "success"
|
||||||
assert "逐个更新" in items[0].message
|
assert "逐个更新" in items[0].message
|
||||||
|
assert closed_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_website_accounts_concurrency_list_accounts_none(db_session, monkeypatch):
|
||||||
|
w = Website(
|
||||||
|
id=1,
|
||||||
|
name="Sub2Api site",
|
||||||
|
site_type="sub2api",
|
||||||
|
base_url="http://sub2api",
|
||||||
|
api_prefix="api/v1",
|
||||||
|
auth_type="bearer",
|
||||||
|
auth_config_json='{"token": "tok1"}'
|
||||||
|
)
|
||||||
|
db_session.add(w)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
closed_count = 0
|
||||||
|
|
||||||
|
class MockClientNone:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc, tb):
|
||||||
|
nonlocal closed_count
|
||||||
|
closed_count += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_accounts(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.routers.websites._client", lambda row: MockClientNone())
|
||||||
|
|
||||||
|
req = SetConcurrencyRequest(concurrency=150)
|
||||||
|
res = set_website_accounts_concurrency(wid=w.id, body=req, db=db_session)
|
||||||
|
|
||||||
|
assert res.success is False
|
||||||
|
assert "拉取远端账号列表失败" in res.message
|
||||||
|
assert len(res.items) == 0
|
||||||
|
assert closed_count == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user