feat: track imported accounts per upstream key across platforms
Add upstream_key_account_links table mapping generated keys to remote accounts per website/platform, surface imported_accounts on key responses, and update website sync/routers to manage the links.
This commit is contained in:
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session
|
||||
from app.database import SessionLocal, get_db
|
||||
from app.models.admin_user import AdminUser
|
||||
from app.models.upstream import Upstream
|
||||
from app.models.upstream_key import UpstreamGeneratedKey
|
||||
from app.models.upstream_key import UpstreamGeneratedKey, UpstreamKeyAccountLink
|
||||
from app.models.upstream_recharge_event import UpstreamRechargeEvent
|
||||
from app.models.snapshot import UpstreamRateSnapshot
|
||||
from app.schemas.upstream import (
|
||||
@@ -25,6 +25,7 @@ from app.schemas.upstream import (
|
||||
UpstreamCreate, UpstreamUpdate, UpstreamResponse, SnapshotResponse, TestResult,
|
||||
UpstreamBatchActionItem, UpstreamBatchActionSummary, UpstreamBatchActionResponse,
|
||||
UpstreamRechargeCreate, UpstreamRechargeUpdate, UpstreamRechargeResponse,
|
||||
UpstreamKeyAccountLinkResponse,
|
||||
)
|
||||
from app.services.upstream_client import UpstreamClient, UpstreamError, _PendingKeyError, build_snapshot, build_new_api_token_name, mask_secret, _extract_key_value
|
||||
from app.services.auth_config import MASK, mask_auth_config, normalize_auth_config
|
||||
@@ -49,7 +50,13 @@ def _group_name(group: dict, gid: str) -> str:
|
||||
return str(group.get("name") or group.get("group_name") or gid)
|
||||
|
||||
|
||||
def _key_response(row: UpstreamGeneratedKey, include_value: bool = False) -> GeneratedUpstreamKeyResponse:
|
||||
def _key_response(row: UpstreamGeneratedKey, include_value: bool = False, db: Session | None = None) -> GeneratedUpstreamKeyResponse:
|
||||
sess = db or Session.object_session(row)
|
||||
imported_accounts = []
|
||||
if sess and row.id is not None:
|
||||
links = sess.query(UpstreamKeyAccountLink).filter(UpstreamKeyAccountLink.upstream_key_id == row.id).all()
|
||||
imported_accounts = [UpstreamKeyAccountLinkResponse.model_validate(l) for l in links]
|
||||
|
||||
return GeneratedUpstreamKeyResponse(
|
||||
id=row.id,
|
||||
upstream_id=row.upstream_id,
|
||||
@@ -67,6 +74,7 @@ def _key_response(row: UpstreamGeneratedKey, include_value: bool = False) -> Gen
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
has_key_value=bool(row.key_value),
|
||||
imported_accounts=imported_accounts,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+386
-240
@@ -16,7 +16,7 @@ from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models.snapshot import UpstreamRateSnapshot
|
||||
from app.models.upstream import Upstream
|
||||
from app.models.upstream_key import UpstreamGeneratedKey
|
||||
from app.models.upstream_key import UpstreamGeneratedKey, UpstreamKeyAccountLink
|
||||
from app.models.website import Website, WebsiteGroupBinding, WebsiteSyncLog
|
||||
from app.schemas.website import (
|
||||
BindingCreate,
|
||||
@@ -63,6 +63,10 @@ from app.services.website_sync import (
|
||||
sync_account_priorities_for_upstream,
|
||||
latest_rate_map,
|
||||
build_imported_account_name,
|
||||
sync_key_legacy_fields,
|
||||
get_or_backfill_account_links,
|
||||
get_account_mapping_pairs,
|
||||
deactivate_account_mapping,
|
||||
)
|
||||
from app.utils.auth import get_current_user
|
||||
from app.utils.number import fixed_decimal_number
|
||||
@@ -248,6 +252,7 @@ def _binding_response(db: Session, row: WebsiteGroupBinding) -> BindingResponse:
|
||||
percent=float(row.percent or 0),
|
||||
algorithm=row.algorithm,
|
||||
enabled=row.enabled,
|
||||
platform=row.platform,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
@@ -539,9 +544,10 @@ def import_groups_from_upstream(
|
||||
create_body = {
|
||||
"name": target_name,
|
||||
"description": group.get("description") or f"Imported from {upstream.name} / {source_name}",
|
||||
"platform": group.get("platform") or "openai",
|
||||
"rate_multiplier": fixed_decimal_number(_source_group_rate(group), 2),
|
||||
}
|
||||
if group.get("platform"):
|
||||
create_body["platform"] = group["platform"]
|
||||
if group.get("rpm_limit") is not None:
|
||||
create_body["rpm_limit"] = group.get("rpm_limit")
|
||||
try:
|
||||
@@ -593,8 +599,10 @@ def import_groups_from_upstream(
|
||||
)
|
||||
|
||||
|
||||
def _normalize_platform(platform: str) -> str:
|
||||
def _normalize_platform(platform: str | None) -> str:
|
||||
"""统一平台值:小写、xai → grok。"""
|
||||
if not platform:
|
||||
return ""
|
||||
p = platform.lower().strip()
|
||||
if p == "xai":
|
||||
return "grok"
|
||||
@@ -604,28 +612,22 @@ def _normalize_platform(platform: str) -> str:
|
||||
SUPPORTED_PLATFORMS = {"openai", "anthropic", "gemini", "grok", "antigravity"}
|
||||
|
||||
|
||||
def _resolve_platform(text: str, group_snapshot: dict | None, fallback: str = "openai") -> str:
|
||||
if group_snapshot and isinstance(group_snapshot, dict):
|
||||
raw = group_snapshot.get("platform")
|
||||
if raw:
|
||||
platform = _normalize_platform(str(raw))
|
||||
if platform in SUPPORTED_PLATFORMS:
|
||||
return platform
|
||||
return _detect_platform(text, fallback)
|
||||
def _target_group_platform(group: dict[str, Any] | None) -> str | None:
|
||||
platform = _normalize_platform(group.get("platform") if group else None)
|
||||
return platform if platform in SUPPORTED_PLATFORMS else None
|
||||
|
||||
|
||||
def _detect_platform(text: str, fallback: str = "openai") -> str:
|
||||
"""根据 Key 名或分组名关键词判断平台类型。"""
|
||||
lower = text.lower()
|
||||
if "claude" in lower or "anthropic" in lower:
|
||||
return "anthropic"
|
||||
if "gemini" in lower:
|
||||
return "gemini"
|
||||
if "grok" in lower or "xai" in lower:
|
||||
return "grok"
|
||||
if "antigravity" in lower:
|
||||
return "antigravity"
|
||||
return _normalize_platform(fallback)
|
||||
def _fetch_target_group(website: Website, target_group_id: str) -> tuple[dict[str, Any], str | None]:
|
||||
try:
|
||||
with _client(website) as client:
|
||||
groups = client.get_groups(website.groups_endpoint) or []
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, f"读取目标网站分组失败: {exc}") from exc
|
||||
target = next((group for group in groups if str(group.get("id")) == str(target_group_id)), None)
|
||||
if target is None:
|
||||
raise HTTPException(400, "目标分组不存在")
|
||||
platform = _target_group_platform(target)
|
||||
return target, platform
|
||||
|
||||
|
||||
@router.post("/api/websites/{wid}/accounts/sync-imported-upstream-keys", response_model=ImportAccountsResponse)
|
||||
@@ -640,21 +642,7 @@ def sync_imported_upstream_keys(
|
||||
if not website:
|
||||
raise HTTPException(404, "website not found")
|
||||
|
||||
rows = (
|
||||
db.query(UpstreamGeneratedKey)
|
||||
.filter(
|
||||
UpstreamGeneratedKey.upstream_id == body.upstream_id,
|
||||
UpstreamGeneratedKey.imported_website_id == wid,
|
||||
UpstreamGeneratedKey.imported_account_id.isnot(None),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
items: list[ImportAccountItem] = []
|
||||
# 预载最新快照映射,用于结构化 platform 解析
|
||||
upstream_ids = {row.upstream_id for row in rows}
|
||||
latest_snapshots = {}
|
||||
for uid in upstream_ids:
|
||||
latest_snapshots[uid] = latest_rate_map(db, uid)
|
||||
|
||||
with _client(website) as c:
|
||||
remote_accounts = None
|
||||
@@ -665,20 +653,34 @@ def sync_imported_upstream_keys(
|
||||
|
||||
if remote_accounts is not None:
|
||||
remote_ids = set()
|
||||
remote_map = {}
|
||||
for acc in remote_accounts:
|
||||
acc_id = _extract_id(acc)
|
||||
if acc_id:
|
||||
remote_ids.add(str(acc_id))
|
||||
remote_map[str(acc_id)] = acc
|
||||
else:
|
||||
remote_ids = None
|
||||
remote_map = {}
|
||||
|
||||
for row in rows:
|
||||
snap_groups = latest_snapshots.get(row.upstream_id) or {}
|
||||
group_snap = snap_groups.get(row.group_id) if isinstance(snap_groups, dict) else None
|
||||
platform = _resolve_platform(f"{row.group_name} {row.group_id} {row.key_name}", group_snap, "openai")
|
||||
if not row.imported_account_id:
|
||||
continue
|
||||
old_account_id = row.imported_account_id
|
||||
target_groups: dict[str, dict[str, Any]] = {}
|
||||
try:
|
||||
for group in c.get_groups(website.groups_endpoint) or []:
|
||||
if group.get("id") is not None:
|
||||
target_groups[str(group["id"])] = group
|
||||
except Exception as exc:
|
||||
logger.warning("failed to fetch groups during import status sync for website %s: %s", wid, exc)
|
||||
get_or_backfill_account_links(db, wid, remote_map, target_groups)
|
||||
|
||||
rows = [
|
||||
pair
|
||||
for pair in get_account_mapping_pairs(db, wid, include_orphaned=True)
|
||||
if pair[0].upstream_id == body.upstream_id
|
||||
]
|
||||
|
||||
for row, link in rows:
|
||||
old_account_id = str(link.remote_account_id)
|
||||
platform = link.platform
|
||||
|
||||
if remote_ids is None:
|
||||
items.append(ImportAccountItem(
|
||||
@@ -688,16 +690,13 @@ def sync_imported_upstream_keys(
|
||||
message="无法校验目标账号存在性(拉取账号列表失败)",
|
||||
))
|
||||
elif str(old_account_id) not in remote_ids:
|
||||
row.imported_website_id = None
|
||||
row.imported_account_id = None
|
||||
row.imported_at = None
|
||||
row.status = "created"
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id, source_group_id=row.group_id,
|
||||
source_group_name=row.group_name, account_id=old_account_id,
|
||||
platform=platform, status="stale_cleared",
|
||||
message="目标账号已删除,已清除导入标记",
|
||||
))
|
||||
deactivate_account_mapping(db, row, link)
|
||||
else:
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id, source_group_id=row.group_id,
|
||||
@@ -801,7 +800,7 @@ def import_upstream_keys_as_accounts(
|
||||
upstream_key_id=kid,
|
||||
source_group_id="",
|
||||
source_group_name="",
|
||||
platform=body.default_platform,
|
||||
platform="",
|
||||
status="failed",
|
||||
message="key not found",
|
||||
)
|
||||
@@ -813,11 +812,6 @@ def import_upstream_keys_as_accounts(
|
||||
upstreams_db = db.query(_Up).filter(_Up.id.in_(upstream_ids)).all()
|
||||
upstreams_map = {u.id: u for u in upstreams_db}
|
||||
|
||||
# 预载最新快照映射,用于结构化 platform 解析
|
||||
latest_snapshots: dict[int, dict[str, Any]] = {}
|
||||
for uid in upstream_ids:
|
||||
latest_snapshots[uid] = latest_rate_map(db, uid)
|
||||
|
||||
# 按倍率自动分配优先级
|
||||
rate_priority_map: dict[tuple[str, int, str], int] = {}
|
||||
if body.auto_priority_by_rate:
|
||||
@@ -834,12 +828,15 @@ def import_upstream_keys_as_accounts(
|
||||
|
||||
with _client(website) as c:
|
||||
target_group_names: dict[str, str] = {}
|
||||
target_groups: dict[str, dict[str, Any]] = {}
|
||||
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)
|
||||
group_id = str(g_id)
|
||||
target_group_names[group_id] = g.get("name") or group_id
|
||||
target_groups[group_id] = g
|
||||
except Exception as e:
|
||||
logger.warning("failed to fetch groups for website %s at import: %s", wid, e)
|
||||
|
||||
@@ -854,34 +851,40 @@ def import_upstream_keys_as_accounts(
|
||||
for acc in remote_accounts_list:
|
||||
aid = c.extract_id(acc)
|
||||
if aid:
|
||||
remote_accounts_map[aid] = acc
|
||||
remote_accounts_map[str(aid)] = acc
|
||||
except Exception as e:
|
||||
logger.warning("failed to fetch remote accounts at import: %s", e)
|
||||
|
||||
get_or_backfill_account_links(db, wid, remote_accounts_map, target_groups)
|
||||
|
||||
for row in rows:
|
||||
# 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed)
|
||||
if row.status in ("orphaned", "failed", "import_failed"):
|
||||
target_group_id = body.target_group_map.get(row.group_id)
|
||||
target_group = target_groups.get(str(target_group_id)) if target_group_id else None
|
||||
platform = _target_group_platform(target_group)
|
||||
if not target_group_id or target_group is None or platform is None:
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=body.target_group_map.get(row.group_id),
|
||||
platform=body.default_platform,
|
||||
target_group_id=target_group_id,
|
||||
platform=platform or "",
|
||||
status="failed",
|
||||
message="必须选择存在且包含合法 platform 的目标分组",
|
||||
))
|
||||
continue
|
||||
|
||||
# 跳过远端已不存在或导入失败的 Key(对账后标记为 orphaned / import_failed)
|
||||
if row.status in ("orphaned", "failed"):
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=target_group_id,
|
||||
platform=platform,
|
||||
status="failed",
|
||||
message="远端 Key 已不存在,请重新生成",
|
||||
))
|
||||
continue
|
||||
# 先确定平台(失败项也需要记录)
|
||||
if body.platform_mode == "auto":
|
||||
snap_groups = latest_snapshots.get(row.upstream_id) or {}
|
||||
group_snap = snap_groups.get(row.group_id) if isinstance(snap_groups, dict) else None
|
||||
platform = _resolve_platform(
|
||||
f"{row.group_name} {row.group_id} {row.key_name}",
|
||||
group_snap,
|
||||
body.default_platform,
|
||||
)
|
||||
else:
|
||||
platform = _normalize_platform(body.default_platform)
|
||||
|
||||
upstream = upstreams_map.get(row.upstream_id)
|
||||
upstream_name = upstream.name if upstream else "Unknown"
|
||||
@@ -893,87 +896,114 @@ def import_upstream_keys_as_accounts(
|
||||
group_name=row.group_name,
|
||||
group_id=row.group_id,
|
||||
key_id=row.id,
|
||||
platform=platform,
|
||||
)
|
||||
|
||||
# 幂等校验:已导入过则检查远端账号是否仍存在
|
||||
if row.imported_website_id == wid and row.imported_account_id:
|
||||
old_account_id = row.imported_account_id
|
||||
if has_list_accounts:
|
||||
if remote_accounts_list is None:
|
||||
# 接口获取失败,保守判定为校验失败
|
||||
exists = None
|
||||
else:
|
||||
exists = old_account_id in remote_accounts_map
|
||||
else:
|
||||
# 如果 client 没有 list_accounts,则 fallback 回原先的 account_exists 检测以兼容旧 Mock 测试
|
||||
try:
|
||||
exists = c.account_exists(row.imported_account_id)
|
||||
except Exception:
|
||||
exists = None
|
||||
|
||||
if exists is True:
|
||||
# 顺手回填 imported_target_group_id(老数据升级后可通过重导自动补齐)
|
||||
new_tgid = body.target_group_map.get(row.group_id) or None
|
||||
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()
|
||||
|
||||
remote_name = ""
|
||||
if old_account_id in remote_accounts_map:
|
||||
remote_name = remote_accounts_map[old_account_id].get("name") or ""
|
||||
link = db.query(UpstreamKeyAccountLink).filter(
|
||||
UpstreamKeyAccountLink.upstream_key_id == row.id,
|
||||
UpstreamKeyAccountLink.website_id == wid,
|
||||
UpstreamKeyAccountLink.platform == platform,
|
||||
).first()
|
||||
if link is None and row.imported_website_id == wid and row.imported_account_id:
|
||||
legacy_account = remote_accounts_map.get(str(row.imported_account_id))
|
||||
legacy_platform = _normalize_platform(legacy_account.get("platform")) if legacy_account else ""
|
||||
if legacy_account is not None and legacy_platform == platform:
|
||||
link = UpstreamKeyAccountLink(
|
||||
upstream_key_id=row.id,
|
||||
website_id=wid,
|
||||
target_group=str(target_group_id),
|
||||
platform=platform,
|
||||
remote_account_id=str(row.imported_account_id),
|
||||
status="active",
|
||||
)
|
||||
db.add(link)
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
has_any_link = db.query(UpstreamKeyAccountLink.id).filter(
|
||||
UpstreamKeyAccountLink.upstream_key_id == row.id,
|
||||
).first()
|
||||
if (
|
||||
link is None
|
||||
and has_any_link is None
|
||||
and row.imported_website_id == wid
|
||||
and row.imported_account_id
|
||||
and remote_accounts_list is None
|
||||
):
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=target_group_id,
|
||||
account_id=str(row.imported_account_id),
|
||||
platform=platform,
|
||||
upstream_base_url=upstream_base_url,
|
||||
status="check_failed",
|
||||
message="无法校验旧账号状态,已保守跳过",
|
||||
))
|
||||
continue
|
||||
if link and link.status == "active":
|
||||
old_account_id = str(link.remote_account_id)
|
||||
if remote_accounts_list is None:
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=body.target_group_map.get(row.group_id),
|
||||
target_group_id=target_group_id,
|
||||
account_id=old_account_id,
|
||||
account_name=remote_name or expected_new_name,
|
||||
platform=platform,
|
||||
upstream_base_url=upstream_base_url,
|
||||
status="exists",
|
||||
message="已导入过,已跳过",
|
||||
))
|
||||
continue
|
||||
elif exists is False:
|
||||
# 远端已删除,清空标记后继续创建
|
||||
row.imported_website_id = None
|
||||
row.imported_account_id = None
|
||||
row.imported_at = None
|
||||
row.status = "created"
|
||||
# 继续往下走(不 continue)
|
||||
else:
|
||||
# 校验失败,保守跳过
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=body.target_group_map.get(row.group_id),
|
||||
account_id=old_account_id,
|
||||
platform=platform,
|
||||
status="check_failed",
|
||||
message="无法校验目标账号状态,已保守跳过",
|
||||
))
|
||||
continue
|
||||
remote_account = remote_accounts_map.get(old_account_id)
|
||||
remote_platform = _normalize_platform(remote_account.get("platform")) if remote_account else ""
|
||||
if remote_account is not None and remote_platform == platform:
|
||||
current_group_ids = remote_account.get("group_ids") or []
|
||||
if not isinstance(current_group_ids, list):
|
||||
current_group_ids = [current_group_ids]
|
||||
target_value = _numeric_group_id(target_group_id)
|
||||
target_value = target_value if target_value is not None else target_group_id
|
||||
new_group_ids = [g for g in current_group_ids if str(g) != str(link.target_group)]
|
||||
if str(target_group_id) not in {str(g) for g in new_group_ids}:
|
||||
new_group_ids.append(target_value)
|
||||
if {str(g) for g in current_group_ids} != {str(g) for g in new_group_ids}:
|
||||
c.update_account(old_account_id, {"group_ids": new_group_ids})
|
||||
link.target_group = str(target_group_id)
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=target_group_id,
|
||||
account_id=old_account_id,
|
||||
account_name=str(remote_account.get("name") or expected_new_name),
|
||||
platform=platform,
|
||||
upstream_base_url=upstream_base_url,
|
||||
status="exists",
|
||||
message="已存在并已对齐目标分组",
|
||||
raw=remote_account,
|
||||
))
|
||||
continue
|
||||
link.status = "stale"
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
|
||||
if not row.key_value:
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
source_group_name=row.group_name,
|
||||
target_group_id=target_group_id,
|
||||
platform=platform,
|
||||
status="failed",
|
||||
status="check_failed",
|
||||
message="该 Key 无明文值,无法导入(远端已存在 Key 不会保留明文,请重新创建或手动填入)",
|
||||
))
|
||||
continue
|
||||
|
||||
target_group_id = body.target_group_map.get(row.group_id)
|
||||
group_ids = []
|
||||
numeric_target = _numeric_group_id(target_group_id)
|
||||
if numeric_target is not None:
|
||||
group_ids.append(numeric_target)
|
||||
group_ids = [numeric_target if numeric_target is not None else target_group_id]
|
||||
account_name = expected_new_name
|
||||
account_body = {
|
||||
"name": account_name,
|
||||
@@ -992,14 +1022,25 @@ def import_upstream_keys_as_accounts(
|
||||
try:
|
||||
created = c.create_account(account_body)
|
||||
account_id = c.extract_id(created)
|
||||
row.imported_website_id = wid
|
||||
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 = target_group_names.get(str(target_group_id)) if target_group_id else None
|
||||
row.status = "imported"
|
||||
if not account_id:
|
||||
raise WebsiteError("创建账号成功但响应缺少账号 ID")
|
||||
if link:
|
||||
link.target_group = str(target_group_id)
|
||||
link.remote_account_id = str(account_id)
|
||||
link.status = "active"
|
||||
else:
|
||||
db.add(UpstreamKeyAccountLink(
|
||||
upstream_key_id=row.id,
|
||||
website_id=wid,
|
||||
target_group=str(target_group_id),
|
||||
platform=platform,
|
||||
remote_account_id=str(account_id),
|
||||
status="active",
|
||||
))
|
||||
row.error = None
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
remote_accounts_map[str(account_id)] = created
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
source_group_id=row.group_id,
|
||||
@@ -1015,8 +1056,13 @@ def import_upstream_keys_as_accounts(
|
||||
))
|
||||
except Exception as exc:
|
||||
logger.exception("import upstream key as account failed website=%s key=%s", wid, row.id)
|
||||
row.status = "import_failed"
|
||||
row.error = str(exc)
|
||||
has_active_link = db.query(UpstreamKeyAccountLink.id).filter(
|
||||
UpstreamKeyAccountLink.upstream_key_id == row.id,
|
||||
UpstreamKeyAccountLink.status == "active",
|
||||
).first()
|
||||
if not has_active_link:
|
||||
row.status = "import_failed"
|
||||
db.commit()
|
||||
items.append(ImportAccountItem(
|
||||
upstream_key_id=row.id,
|
||||
@@ -1060,20 +1106,31 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
for acc in remote_accounts:
|
||||
aid = c.extract_id(acc)
|
||||
if aid:
|
||||
remote_accounts_map[aid] = acc
|
||||
remote_accounts_map[str(aid)] = acc
|
||||
|
||||
target_groups: dict[str, dict[str, Any]] = {}
|
||||
try:
|
||||
for group in c.get_groups(website.groups_endpoint) or []:
|
||||
if group.get("id") is not None:
|
||||
target_groups[str(group["id"])] = group
|
||||
except Exception as exc:
|
||||
logger.warning("failed to fetch groups during cleanup for website %s: %s", website.id, exc)
|
||||
get_or_backfill_account_links(db, website.id, remote_accounts_map, target_groups)
|
||||
|
||||
candidates: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# 2. Category: missing_remote_account
|
||||
# Local keys that still reference an imported_account_id,
|
||||
# but that account no longer appears in the remote account list.
|
||||
imported_keys = db.query(UpstreamGeneratedKey).filter(
|
||||
UpstreamGeneratedKey.imported_website_id == website.id,
|
||||
UpstreamGeneratedKey.imported_account_id.isnot(None),
|
||||
).all()
|
||||
all_pairs = get_account_mapping_pairs(db, website.id, include_orphaned=True)
|
||||
local_pair_by_account = {str(link.remote_account_id): (key, link) for key, link in all_pairs}
|
||||
imported_pairs = [
|
||||
(key, link) for key, link in all_pairs
|
||||
if key.status != "orphaned" and link.status == "active"
|
||||
]
|
||||
|
||||
for key in imported_keys:
|
||||
acc_id = key.imported_account_id
|
||||
for key, link in imported_pairs:
|
||||
acc_id = str(link.remote_account_id)
|
||||
if acc_id in candidates:
|
||||
continue
|
||||
if acc_id in remote_accounts_map:
|
||||
@@ -1089,18 +1146,18 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
"reason": "目标账号已被手动删除",
|
||||
"cleanup_action": "clear_local_marker",
|
||||
"db_key": key,
|
||||
"db_link": link,
|
||||
}
|
||||
|
||||
# 3. Category: local_orphan_account
|
||||
# Local keys with status=orphaned that still carry an imported_account_id.
|
||||
orphaned_keys = db.query(UpstreamGeneratedKey).filter(
|
||||
UpstreamGeneratedKey.status == "orphaned",
|
||||
UpstreamGeneratedKey.imported_website_id == website.id,
|
||||
UpstreamGeneratedKey.imported_account_id.isnot(None),
|
||||
).all()
|
||||
orphaned_pairs = [
|
||||
(key, link) for key, link in all_pairs
|
||||
if key.status == "orphaned" and link.status == "active"
|
||||
]
|
||||
|
||||
for key in orphaned_keys:
|
||||
acc_id = key.imported_account_id
|
||||
for key, link in orphaned_pairs:
|
||||
acc_id = str(link.remote_account_id)
|
||||
remote_acc = remote_accounts_map.get(acc_id)
|
||||
acc_name = remote_acc.get("name") if remote_acc else key.key_name
|
||||
upstream = db.query(Upstream).filter(Upstream.id == key.upstream_id).first()
|
||||
@@ -1113,6 +1170,7 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
candidates[acc_id]["upstream_key_id"] = key.id
|
||||
candidates[acc_id]["upstream_name"] = upstream_name
|
||||
candidates[acc_id]["source_group_name"] = key.group_name
|
||||
candidates[acc_id]["db_link"] = link
|
||||
continue
|
||||
|
||||
if remote_acc:
|
||||
@@ -1125,6 +1183,7 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
"reason": "本地上游 Key 已归档",
|
||||
"cleanup_action": "delete_remote_and_clear_marker",
|
||||
"db_key": key,
|
||||
"db_link": link,
|
||||
}
|
||||
else:
|
||||
candidates[acc_id] = {
|
||||
@@ -1136,6 +1195,7 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
"reason": "本地上游 Key 已归档,且目标账号已被手动删除",
|
||||
"cleanup_action": "clear_local_marker",
|
||||
"db_key": key,
|
||||
"db_link": link,
|
||||
}
|
||||
|
||||
# 4. Category: remote_orphan_account
|
||||
@@ -1166,10 +1226,12 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
if not key:
|
||||
is_orphan = True
|
||||
reason = "本地找不到对应 Key"
|
||||
elif key.imported_website_id != website.id or key.imported_account_id != aid:
|
||||
local_pair = local_pair_by_account.get(aid)
|
||||
link = local_pair[1] if local_pair else None
|
||||
if key and (local_pair is None or local_pair[0].id != key.id):
|
||||
is_orphan = True
|
||||
reason = "本地 Key 与目标账号关联不一致"
|
||||
elif key.status == 'orphaned':
|
||||
elif key and key.status == 'orphaned':
|
||||
is_orphan = True
|
||||
reason = "本地上游 Key 已归档"
|
||||
|
||||
@@ -1190,6 +1252,7 @@ def _get_cleanup_candidates(db: Session, website: Website, c: Sub2ApiWebsiteClie
|
||||
"reason": reason,
|
||||
"cleanup_action": "delete_remote_account",
|
||||
"db_key": key,
|
||||
"db_link": link,
|
||||
}
|
||||
|
||||
return candidates
|
||||
@@ -1215,8 +1278,23 @@ def _test_candidate_account(c: Sub2ApiWebsiteClient, account_id: str) -> tuple[s
|
||||
return "test_unknown", False, f"测试时发生异常: {e}"
|
||||
|
||||
|
||||
def _clear_local_import_marker(key: UpstreamGeneratedKey, db: Session) -> None:
|
||||
"""清空单条 Key 上的 SmartUp 导入标记字段。"""
|
||||
def _clear_local_import_marker(
|
||||
key: UpstreamGeneratedKey,
|
||||
db: Session,
|
||||
link: UpstreamKeyAccountLink | None = None,
|
||||
) -> None:
|
||||
"""Deactivate one account mapping and refresh the compatibility marker."""
|
||||
if link is not None:
|
||||
if isinstance(link, UpstreamKeyAccountLink):
|
||||
deactivate_account_mapping(db, key, link)
|
||||
else:
|
||||
key.imported_website_id = None
|
||||
key.imported_account_id = None
|
||||
key.imported_at = None
|
||||
key.imported_target_group_id = None
|
||||
key.imported_target_group_name = None
|
||||
db.add(key)
|
||||
return
|
||||
key.imported_website_id = None
|
||||
key.imported_account_id = None
|
||||
key.imported_at = None
|
||||
@@ -1300,7 +1378,7 @@ def execute_cleanup_invalid_accounts(wid: int, db: Session = Depends(get_db), _=
|
||||
if action == "clear_local_marker":
|
||||
key = cand.get("db_key")
|
||||
if key:
|
||||
_clear_local_import_marker(key, db)
|
||||
_clear_local_import_marker(key, db, cand.get("db_link"))
|
||||
action_status = "cleared"
|
||||
msg = "已清除本地导入标记"
|
||||
else:
|
||||
@@ -1320,7 +1398,7 @@ def execute_cleanup_invalid_accounts(wid: int, db: Session = Depends(get_db), _=
|
||||
if action == "delete_remote_and_clear_marker":
|
||||
key = cand.get("db_key")
|
||||
if key:
|
||||
_clear_local_import_marker(key, db)
|
||||
_clear_local_import_marker(key, db, cand.get("db_link"))
|
||||
msg += ",已清除本地导入标记"
|
||||
else:
|
||||
action_status = "failed"
|
||||
@@ -1405,16 +1483,22 @@ def set_website_accounts_concurrency(
|
||||
for acc in remote_accounts:
|
||||
aid = c.extract_id(acc)
|
||||
if aid:
|
||||
remote_map[aid] = acc
|
||||
remote_map[str(aid)] = acc
|
||||
|
||||
keys = db.query(UpstreamGeneratedKey).filter(
|
||||
UpstreamGeneratedKey.imported_website_id == wid,
|
||||
UpstreamGeneratedKey.imported_account_id.isnot(None)
|
||||
).all()
|
||||
target_groups: dict[str, dict[str, Any]] = {}
|
||||
try:
|
||||
for group in c.get_groups(website.groups_endpoint) or []:
|
||||
if group.get("id") is not None:
|
||||
target_groups[str(group["id"])] = group
|
||||
except Exception as exc:
|
||||
logger.warning("failed to fetch groups while setting concurrency for website %s: %s", wid, exc)
|
||||
get_or_backfill_account_links(db, wid, remote_map, target_groups)
|
||||
|
||||
account_pairs = get_account_mapping_pairs(db, wid, include_orphaned=False)
|
||||
|
||||
candidates = {}
|
||||
for key in keys:
|
||||
aid = key.imported_account_id
|
||||
for key, link in account_pairs:
|
||||
aid = str(link.remote_account_id)
|
||||
if aid not in candidates:
|
||||
candidates[aid] = {
|
||||
"account_id": aid,
|
||||
@@ -1595,17 +1679,22 @@ def _sync_upstream_models_generator(wid: int, db: Session):
|
||||
for acc in remote_accounts:
|
||||
aid = c.extract_id(acc)
|
||||
if aid:
|
||||
remote_map[aid] = acc
|
||||
remote_map[str(aid)] = acc
|
||||
|
||||
# 1. 查找候选账号:本地 UpstreamGeneratedKey.imported_website_id == wid 且 imported_account_id 非空
|
||||
keys = db.query(UpstreamGeneratedKey).filter(
|
||||
UpstreamGeneratedKey.imported_website_id == wid,
|
||||
UpstreamGeneratedKey.imported_account_id.isnot(None)
|
||||
).all()
|
||||
target_groups: dict[str, dict[str, Any]] = {}
|
||||
try:
|
||||
for group in c.get_groups(website.groups_endpoint) or []:
|
||||
if group.get("id") is not None:
|
||||
target_groups[str(group["id"])] = group
|
||||
except Exception as exc:
|
||||
logger.warning("failed to fetch groups while syncing models for website %s: %s", wid, exc)
|
||||
get_or_backfill_account_links(db, wid, remote_map, target_groups)
|
||||
|
||||
account_pairs = get_account_mapping_pairs(db, wid, include_orphaned=False)
|
||||
|
||||
candidates = {}
|
||||
for key in keys:
|
||||
aid = key.imported_account_id
|
||||
for key, link in account_pairs:
|
||||
aid = str(link.remote_account_id)
|
||||
if aid not in candidates:
|
||||
candidates[aid] = {
|
||||
"account_id": aid,
|
||||
@@ -1937,6 +2026,11 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
)
|
||||
for row in hist_group_ids:
|
||||
smartup_managed_group_ids.add(str(row[0]))
|
||||
linked_group_ids = db.query(UpstreamKeyAccountLink.target_group).filter(
|
||||
UpstreamKeyAccountLink.website_id == wid,
|
||||
UpstreamKeyAccountLink.status == "active",
|
||||
).distinct().all()
|
||||
smartup_managed_group_ids.update(str(row[0]) for row in linked_group_ids if row[0])
|
||||
|
||||
items: list[OrganizeGroupsItem] = []
|
||||
|
||||
@@ -1989,13 +2083,16 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
with _client(website) as c:
|
||||
# 获取远端分组名称列表和账号列表
|
||||
target_group_names: dict[str, str] = {}
|
||||
target_group_map: dict[str, dict[str, Any]] = {}
|
||||
remote_accounts: list[dict[str, Any]] = []
|
||||
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)
|
||||
group_id = str(g_id)
|
||||
target_group_names[group_id] = g.get("name") or group_id
|
||||
target_group_map[group_id] = g
|
||||
except Exception as e:
|
||||
logger.warning("failed to fetch groups for website %s at organize: %s", wid, e)
|
||||
|
||||
@@ -2019,6 +2116,9 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
if acc_id:
|
||||
remote_account_map[str(acc_id)] = acc
|
||||
|
||||
# 回填遗留 Key 到 UpstreamKeyAccountLink 映射
|
||||
get_or_backfill_account_links(db, wid, remote_account_map, target_group_map)
|
||||
|
||||
# 预载所有相关的上游快照映射
|
||||
all_upstream_ids = set()
|
||||
for b in bindings:
|
||||
@@ -2035,6 +2135,36 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
for b in bindings:
|
||||
target_group_id = b.target_group_id
|
||||
target_group_name = target_group_names.get(str(target_group_id)) or b.target_group_name or str(target_group_id)
|
||||
target_group = target_group_map.get(str(target_group_id))
|
||||
binding_platform = _target_group_platform(target_group)
|
||||
if binding_platform is None:
|
||||
for src in binding_sources(b):
|
||||
uid = src.get("upstream_id")
|
||||
gid = src.get("group_id")
|
||||
if not uid or not gid:
|
||||
continue
|
||||
upstream = db.query(Upstream).filter(Upstream.id == uid).first()
|
||||
sname = src.get("group_name") or gid
|
||||
_item = OrganizeGroupsItem(
|
||||
target_group_id=str(target_group_id),
|
||||
target_group_name=target_group_name,
|
||||
upstream_name=upstream.name if upstream else f"#{uid}",
|
||||
source_group_id=str(gid),
|
||||
source_group_name=str(sname),
|
||||
key_name="",
|
||||
status="failed",
|
||||
message="目标分组不存在或缺少合法 platform,无法确定账号平台",
|
||||
)
|
||||
items.append(_item)
|
||||
yield "item", _item.model_dump()
|
||||
if time.monotonic() - last_event_at >= 10:
|
||||
yield "heartbeat", {}
|
||||
last_event_at = time.monotonic()
|
||||
continue
|
||||
if b.platform != binding_platform or b.target_group_name != target_group_name:
|
||||
b.platform = binding_platform
|
||||
b.target_group_name = target_group_name
|
||||
db.commit()
|
||||
sources = binding_sources(b)
|
||||
|
||||
for src in sources:
|
||||
@@ -2113,18 +2243,33 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
last_event_at = time.monotonic()
|
||||
continue
|
||||
|
||||
# 检测平台类型
|
||||
group_snap = snapshot_groups.get(row.group_id) if isinstance(snapshot_groups, dict) else None
|
||||
platform = _resolve_platform(
|
||||
f"{row.group_name} {row.group_id} {row.key_name}",
|
||||
group_snap,
|
||||
"openai",
|
||||
)
|
||||
|
||||
# 1. 幂等校验:已导入过则检查远端账号是否仍存在
|
||||
if row.imported_website_id == wid and row.imported_account_id:
|
||||
old_account_id = row.imported_account_id
|
||||
# 平台唯一来源:目标分组绑定的 platform 字段
|
||||
platform = binding_platform
|
||||
# 1. 幂等校验:仅按 UpstreamKeyAccountLink (key_id, website_id, platform) 查找,不复用 legacy 字段
|
||||
link = db.query(UpstreamKeyAccountLink).filter(
|
||||
UpstreamKeyAccountLink.upstream_key_id == row.id,
|
||||
UpstreamKeyAccountLink.website_id == wid,
|
||||
UpstreamKeyAccountLink.platform == platform,
|
||||
UpstreamKeyAccountLink.status == "active",
|
||||
).first()
|
||||
if link is None and row.imported_website_id == wid and row.imported_account_id:
|
||||
legacy_account = remote_account_map.get(str(row.imported_account_id))
|
||||
legacy_platform = _normalize_platform(legacy_account.get("platform")) if legacy_account else ""
|
||||
if legacy_account is not None and legacy_platform == platform:
|
||||
link = UpstreamKeyAccountLink(
|
||||
upstream_key_id=row.id,
|
||||
website_id=wid,
|
||||
target_group=str(target_group_id),
|
||||
platform=platform,
|
||||
remote_account_id=str(row.imported_account_id),
|
||||
status="active",
|
||||
)
|
||||
db.add(link)
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
old_account_id = link.remote_account_id if link else None
|
||||
|
||||
if old_account_id:
|
||||
if remote_account_ids is None:
|
||||
# 账号列表获取失败,无法校验状态,保守跳过
|
||||
_item = OrganizeGroupsItem(
|
||||
@@ -2148,7 +2293,17 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
remote_acc = remote_account_map.get(str(old_account_id))
|
||||
|
||||
if remote_acc is not None:
|
||||
# 目标账号存在:检测并对齐目标分组,移除其余已启用的 SmartUp 托管分组,保留非 SmartUp 托管分组
|
||||
remote_platform = _normalize_platform(remote_acc.get("platform"))
|
||||
if remote_platform != platform:
|
||||
link.status = "stale"
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
old_account_id = None
|
||||
is_recreated = False
|
||||
remote_acc = None
|
||||
|
||||
if remote_acc is not None:
|
||||
# 目标账号存在:仅对齐目标分组的 group_ids,绝不修改远端账号 platform
|
||||
try:
|
||||
current_group_ids = remote_acc.get("group_ids") or []
|
||||
if not isinstance(current_group_ids, list):
|
||||
@@ -2157,61 +2312,23 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
numeric_target = _numeric_group_id(target_group_id)
|
||||
target_val = numeric_target if numeric_target is not None else target_group_id
|
||||
|
||||
# 过滤出非 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}
|
||||
|
||||
remote_platform = remote_acc.get("platform")
|
||||
if remote_platform is not None:
|
||||
platform_mismatch = (str(remote_platform).lower() != str(platform).lower())
|
||||
else:
|
||||
platform_mismatch = (str(platform).lower() != "openai")
|
||||
|
||||
update_payload = {}
|
||||
if old_set != new_set:
|
||||
update_payload["group_ids"] = new_group_ids
|
||||
if platform_mismatch:
|
||||
update_payload["platform"] = platform
|
||||
|
||||
if update_payload:
|
||||
c.update_account(old_account_id, update_payload)
|
||||
if "group_ids" in update_payload:
|
||||
remote_acc["group_ids"] = new_group_ids
|
||||
if "platform" in update_payload:
|
||||
# 主动核对防线:防止子站忽略 platform 修改而发生静默失败
|
||||
fresh_accs = c.list_accounts()
|
||||
if fresh_accs is None:
|
||||
raise WebsiteError("复查失败:拉取远端账号列表返回空,无法核对平台修改是否生效")
|
||||
fresh_acc = next((a for a in fresh_accs if str(c.extract_id(a)) == str(old_account_id)), None)
|
||||
if not fresh_acc:
|
||||
raise WebsiteError(f"复查失败:在远端账号列表中未找到要更新的账号 {old_account_id}")
|
||||
|
||||
remote_account_map[str(old_account_id)] = fresh_acc
|
||||
new_platform = fresh_acc.get("platform")
|
||||
if str(new_platform).lower() != str(platform).lower():
|
||||
raise WebsiteError(
|
||||
f"远端账号不支持修改平台属性,尝试将平台从 {remote_platform} 修改为 {platform} 失败"
|
||||
)
|
||||
remote_acc["platform"] = platform
|
||||
|
||||
msg_parts = []
|
||||
if old_set != new_set:
|
||||
msg_parts.append("已迁移到目标分组")
|
||||
if platform_mismatch:
|
||||
msg_parts.append(f"平台从 {remote_platform} 修正为 {platform}")
|
||||
msg = "已存在," + "且".join(msg_parts)
|
||||
c.update_account(old_account_id, {"group_ids": new_group_ids})
|
||||
remote_acc["group_ids"] = new_group_ids
|
||||
msg = "已存在,已迁移到目标分组"
|
||||
else:
|
||||
msg = "已存在且已对齐,已跳过"
|
||||
|
||||
# 补齐本地 DB 的目标分组标记
|
||||
if row.imported_target_group_id != target_group_id:
|
||||
row.imported_target_group_id = target_group_id
|
||||
row.imported_target_group_name = target_group_names.get(str(target_group_id))
|
||||
db.commit()
|
||||
# 更新链接的 target_group(同平台换组迁移)
|
||||
link.target_group = str(target_group_id)
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
|
||||
items.append(_item := OrganizeGroupsItem(
|
||||
target_group_id=str(target_group_id),
|
||||
@@ -2248,14 +2365,13 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
yield "heartbeat", {}
|
||||
last_event_at = time.monotonic()
|
||||
continue
|
||||
else:
|
||||
# 远端已删除,清理标记后进行重建
|
||||
row.imported_website_id = None
|
||||
row.imported_account_id = None
|
||||
row.imported_at = None
|
||||
row.status = "created"
|
||||
elif old_account_id:
|
||||
link.status = "stale"
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
is_recreated = True
|
||||
elif not old_account_id:
|
||||
is_recreated = False
|
||||
else:
|
||||
is_recreated = False
|
||||
|
||||
@@ -2291,6 +2407,7 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
group_name=row.group_name,
|
||||
group_id=row.group_id,
|
||||
key_id=row.id,
|
||||
platform=platform,
|
||||
)
|
||||
priority_val = rate_priority_map.get((str(target_group_id), row.upstream_id, row.group_id), 1)
|
||||
account_body = {
|
||||
@@ -2310,12 +2427,30 @@ def _organize_website_groups_generator(wid: int, db: Session):
|
||||
try:
|
||||
created = c.create_account(account_body)
|
||||
account_id = c.extract_id(created)
|
||||
row.imported_website_id = wid
|
||||
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 = target_group_names.get(str(target_group_id)) if target_group_id else None
|
||||
row.status = "imported"
|
||||
|
||||
# 更新或创建 UpstreamKeyAccountLink 映射(upsert)
|
||||
if account_id:
|
||||
existing_link = db.query(UpstreamKeyAccountLink).filter(
|
||||
UpstreamKeyAccountLink.upstream_key_id == row.id,
|
||||
UpstreamKeyAccountLink.website_id == wid,
|
||||
UpstreamKeyAccountLink.platform == platform,
|
||||
).first()
|
||||
if existing_link:
|
||||
existing_link.remote_account_id = str(account_id)
|
||||
existing_link.target_group = str(target_group_id)
|
||||
existing_link.status = "active"
|
||||
else:
|
||||
new_link = UpstreamKeyAccountLink(
|
||||
upstream_key_id=row.id,
|
||||
website_id=wid,
|
||||
target_group=str(target_group_id),
|
||||
platform=platform,
|
||||
remote_account_id=str(account_id),
|
||||
status="active",
|
||||
)
|
||||
db.add(new_link)
|
||||
db.commit()
|
||||
sync_key_legacy_fields(db, row.id)
|
||||
row.error = None
|
||||
db.commit()
|
||||
|
||||
@@ -2545,14 +2680,16 @@ def create_binding(body: BindingCreate, db: Session = Depends(get_db), _=Depends
|
||||
if body.algorithm not in ALGORITHMS:
|
||||
raise HTTPException(400, "不支持的算法")
|
||||
_ensure_unique_target(db, body.website_id, body.target_group_id)
|
||||
target_group, platform = _fetch_target_group(website, body.target_group_id)
|
||||
row = WebsiteGroupBinding(
|
||||
website_id=body.website_id,
|
||||
target_group_id=body.target_group_id,
|
||||
target_group_name=body.target_group_name,
|
||||
target_group_name=str(target_group.get("name") or body.target_group_name or body.target_group_id),
|
||||
source_groups_json=json.dumps([item.model_dump() for item in body.source_groups], ensure_ascii=False),
|
||||
percent=str(body.percent),
|
||||
algorithm=body.algorithm,
|
||||
enabled=body.enabled,
|
||||
platform=platform,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
@@ -2577,12 +2714,21 @@ def update_binding(bid: int, body: BindingUpdate, db: Session = Depends(get_db),
|
||||
next_website_id = int(data.get("website_id", row.website_id))
|
||||
next_target_group_id = str(data.get("target_group_id", row.target_group_id))
|
||||
_ensure_unique_target(db, next_website_id, next_target_group_id, exclude_id=row.id)
|
||||
website = db.query(Website).filter(Website.id == next_website_id).first()
|
||||
target_changed = next_website_id != row.website_id or next_target_group_id != row.target_group_id
|
||||
if not target_changed and not row.enabled and data.get("enabled") is not True:
|
||||
target_group = {"name": row.target_group_name}
|
||||
platform = row.platform
|
||||
else:
|
||||
target_group, platform = _fetch_target_group(website, next_target_group_id)
|
||||
if "source_groups" in data:
|
||||
row.source_groups_json = json.dumps(data.pop("source_groups"), ensure_ascii=False)
|
||||
if "percent" in data:
|
||||
row.percent = str(data.pop("percent"))
|
||||
for key, value in data.items():
|
||||
setattr(row, key, value)
|
||||
row.target_group_name = str(target_group.get("name") or row.target_group_name or next_target_group_id)
|
||||
row.platform = platform
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
|
||||
Reference in New Issue
Block a user