fix: reuse upstream keys for account import
This commit is contained in:
@@ -5,7 +5,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ from app.schemas.upstream import (
|
|||||||
GeneratedUpstreamKeyResponse,
|
GeneratedUpstreamKeyResponse,
|
||||||
UpstreamCreate, UpstreamUpdate, UpstreamResponse, SnapshotResponse, TestResult
|
UpstreamCreate, UpstreamUpdate, UpstreamResponse, SnapshotResponse, TestResult
|
||||||
)
|
)
|
||||||
from app.services.upstream_client import UpstreamClient, UpstreamError, build_snapshot, mask_secret
|
from app.services.upstream_client import UpstreamClient, UpstreamError, build_snapshot, mask_secret, _extract_key_value
|
||||||
from app.services.snapshot_service import diff_snapshots
|
from app.services.snapshot_service import diff_snapshots
|
||||||
from app.services import scheduler as sched_svc
|
from app.services import scheduler as sched_svc
|
||||||
from app.services import webhook_service
|
from app.services import webhook_service
|
||||||
@@ -65,6 +65,7 @@ def _key_response(row: UpstreamGeneratedKey, include_value: bool = False) -> Gen
|
|||||||
imported_at=row.imported_at,
|
imported_at=row.imported_at,
|
||||||
created_at=row.created_at,
|
created_at=row.created_at,
|
||||||
updated_at=row.updated_at,
|
updated_at=row.updated_at,
|
||||||
|
has_key_value=bool(row.key_value),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +79,17 @@ def _mask_auth_config(auth_type: str, cfg: dict) -> dict:
|
|||||||
return masked
|
return masked
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_plaintext_key(payload: dict[str, Any] | None) -> str:
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return ""
|
||||||
|
key_value = _extract_key_value(payload)
|
||||||
|
if not key_value:
|
||||||
|
return ""
|
||||||
|
if "*" in key_value:
|
||||||
|
return ""
|
||||||
|
return key_value
|
||||||
|
|
||||||
|
|
||||||
def _to_response(u: Upstream) -> UpstreamResponse:
|
def _to_response(u: Upstream) -> UpstreamResponse:
|
||||||
cfg = json.loads(u.auth_config_json or "{}")
|
cfg = json.loads(u.auth_config_json or "{}")
|
||||||
return UpstreamResponse(
|
return UpstreamResponse(
|
||||||
@@ -164,9 +176,21 @@ def _ensure_group_key(
|
|||||||
except Exception:
|
except Exception:
|
||||||
existing = None
|
existing = None
|
||||||
if existing:
|
if existing:
|
||||||
|
key_id = str(existing.get("id") or "")
|
||||||
|
key_value = _extract_plaintext_key(existing)
|
||||||
|
masked = mask_secret(key_value) if key_value else (existing.get("masked_key") or existing.get("key") or "")
|
||||||
|
row.key_id = key_id or row.key_id
|
||||||
|
if key_value:
|
||||||
|
row.key_value = key_value
|
||||||
|
row.masked_key = masked
|
||||||
|
elif masked:
|
||||||
|
row.masked_key = str(masked)
|
||||||
|
row.raw_json = json.dumps(existing, ensure_ascii=False)
|
||||||
row.status = "exists"
|
row.status = "exists"
|
||||||
row.updated_at = datetime.now(timezone.utc)
|
row.updated_at = datetime.now(timezone.utc)
|
||||||
|
db.add(row)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
db.refresh(row)
|
||||||
return _key_response(row, include_value=False)
|
return _key_response(row, include_value=False)
|
||||||
# 远端不存在,需要重新创建
|
# 远端不存在,需要重新创建
|
||||||
row.status = "replaced"
|
row.status = "replaced"
|
||||||
@@ -175,10 +199,16 @@ def _ensure_group_key(
|
|||||||
existing = client.find_smartup_group_key(gid, stable_name, prefix)
|
existing = client.find_smartup_group_key(gid, stable_name, prefix)
|
||||||
if existing:
|
if existing:
|
||||||
key_id = str(existing.get("id") or "")
|
key_id = str(existing.get("id") or "")
|
||||||
masked = existing.get("masked_key") or existing.get("key") or ""
|
key_value = _extract_plaintext_key(existing)
|
||||||
|
masked = mask_secret(key_value) if key_value else (existing.get("masked_key") or existing.get("key") or "")
|
||||||
if row:
|
if row:
|
||||||
row.key_id = key_id or row.key_id
|
row.key_id = key_id or row.key_id
|
||||||
row.masked_key = masked or row.masked_key
|
if key_value:
|
||||||
|
row.key_value = key_value
|
||||||
|
row.masked_key = masked
|
||||||
|
elif masked:
|
||||||
|
row.masked_key = str(masked)
|
||||||
|
row.raw_json = json.dumps(existing, ensure_ascii=False)
|
||||||
row.status = "exists"
|
row.status = "exists"
|
||||||
row.updated_at = datetime.now(timezone.utc)
|
row.updated_at = datetime.now(timezone.utc)
|
||||||
else:
|
else:
|
||||||
@@ -188,13 +218,13 @@ def _ensure_group_key(
|
|||||||
group_name=gname,
|
group_name=gname,
|
||||||
key_id=key_id or None,
|
key_id=key_id or None,
|
||||||
key_name=stable_name,
|
key_name=stable_name,
|
||||||
key_value="",
|
key_value=key_value or "",
|
||||||
masked_key=masked,
|
masked_key=masked,
|
||||||
raw_json=json.dumps(existing, ensure_ascii=False),
|
raw_json=json.dumps(existing, ensure_ascii=False),
|
||||||
managed_prefix=prefix,
|
managed_prefix=prefix,
|
||||||
status="exists",
|
status="exists",
|
||||||
)
|
)
|
||||||
db.add(row)
|
db.add(row)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(row)
|
db.refresh(row)
|
||||||
return _key_response(row, include_value=False)
|
return _key_response(row, include_value=False)
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class GeneratedUpstreamKeyResponse(BaseModel):
|
|||||||
imported_at: Optional[datetime] = None
|
imported_at: Optional[datetime] = None
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
has_key_value: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GenerateKeysByGroupsResponse(BaseModel):
|
class GenerateKeysByGroupsResponse(BaseModel):
|
||||||
|
|||||||
@@ -223,13 +223,25 @@ def _sync_upstream_keys(upstream_id: int, snapshot: dict[str, Any], captured_at:
|
|||||||
for row in key_rows:
|
for row in key_rows:
|
||||||
# 1. 分组已不在当前快照中 → 删除本地记录
|
# 1. 分组已不在当前快照中 → 删除本地记录
|
||||||
if row.group_id not in active_group_ids:
|
if row.group_id not in active_group_ids:
|
||||||
db.delete(row)
|
if row.imported_website_id and row.imported_account_id:
|
||||||
logger.info("removed key %s (group %s no longer in snapshot)", row.id, row.group_id)
|
row.status = "orphaned"
|
||||||
|
row.error = "来源分组已不存在"
|
||||||
|
row.updated_at = captured_at
|
||||||
|
logger.info("marked key %s orphaned (group %s no longer in snapshot)", row.id, row.group_id)
|
||||||
|
else:
|
||||||
|
db.delete(row)
|
||||||
|
logger.info("removed key %s (group %s no longer in snapshot)", row.id, row.group_id)
|
||||||
continue
|
continue
|
||||||
# 2. 远端查询成功但 key_id 不在列表中 → 删除本地记录
|
# 2. 远端查询成功但 key_id 不在列表中 → 删除本地记录
|
||||||
if row.key_id and remote_key_ids is not None and row.key_id not in remote_key_ids:
|
if row.key_id and remote_key_ids is not None and row.key_id not in remote_key_ids:
|
||||||
db.delete(row)
|
if row.imported_website_id and row.imported_account_id:
|
||||||
logger.info("removed key %s (key_id %s gone from remote)", row.id, row.key_id)
|
row.status = "orphaned"
|
||||||
|
row.error = "远端 Key 已不存在"
|
||||||
|
row.updated_at = captured_at
|
||||||
|
logger.info("marked key %s orphaned (key_id %s gone from remote)", row.id, row.key_id)
|
||||||
|
else:
|
||||||
|
db.delete(row)
|
||||||
|
logger.info("removed key %s (key_id %s gone from remote)", row.id, row.key_id)
|
||||||
continue
|
continue
|
||||||
# 3. 更新同步时间戳(仅当查询成功且 Key 仍在远端时)
|
# 3. 更新同步时间戳(仅当查询成功且 Key 仍在远端时)
|
||||||
if remote_key_ids is not None and row.key_id in remote_key_ids:
|
if remote_key_ids is not None and row.key_id in remote_key_ids:
|
||||||
|
|||||||
@@ -262,6 +262,64 @@ def test_ensure_group_key_reuses_old_record(db_session, monkeypatch):
|
|||||||
assert rows[0].key_value == "sk-new-value"
|
assert rows[0].key_value == "sk-new-value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_group_key_backfills_plaintext_from_remote_existing_key(db_session):
|
||||||
|
"""远端已存在的 SmartUp Key 如果列表接口返回明文,应补写到本地 key_value。"""
|
||||||
|
from app.routers.upstreams import _ensure_group_key
|
||||||
|
from app.models.upstream_key import UpstreamGeneratedKey
|
||||||
|
from app.schemas.upstream import GenerateKeysByGroupsRequest
|
||||||
|
from app.services.upstream_client import mask_secret
|
||||||
|
|
||||||
|
upstream = Upstream(name="Test", base_url="http://local", api_prefix="/api/v1",
|
||||||
|
auth_type="bearer", auth_config_json="{}",
|
||||||
|
groups_endpoint="/groups", rate_endpoint="/rates")
|
||||||
|
db_session.add(upstream)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(upstream)
|
||||||
|
|
||||||
|
db_session.add(UpstreamGeneratedKey(
|
||||||
|
upstream_id=upstream.id,
|
||||||
|
group_id="vip",
|
||||||
|
group_name="VIP",
|
||||||
|
key_name="SmartUp-Test-vip",
|
||||||
|
key_value="",
|
||||||
|
masked_key="sk-old-masked",
|
||||||
|
key_id="remote-123",
|
||||||
|
managed_prefix="SmartUp",
|
||||||
|
))
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
class MockClient:
|
||||||
|
def find_smartup_group_key(self, gid, name, prefix):
|
||||||
|
return {
|
||||||
|
"id": "remote-123",
|
||||||
|
"name": "SmartUp-Test-vip",
|
||||||
|
"key": "sk-remote-plain-value-1234567890abcdef",
|
||||||
|
"masked_key": "sk-re************cdef",
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_api_key(self, *args, **kwargs):
|
||||||
|
raise AssertionError("create_api_key should not be called when remote key exists")
|
||||||
|
|
||||||
|
group = {"id": "vip", "name": "VIP", "rate_multiplier": 1}
|
||||||
|
body = GenerateKeysByGroupsRequest(
|
||||||
|
group_ids=["vip"],
|
||||||
|
name_prefix="SmartUp",
|
||||||
|
quota=0,
|
||||||
|
endpoint="/keys",
|
||||||
|
)
|
||||||
|
result = _ensure_group_key(db_session, MockClient(), upstream, group, "SmartUp", body)
|
||||||
|
|
||||||
|
assert result.status == "exists"
|
||||||
|
assert result.has_key_value is True
|
||||||
|
row = db_session.query(UpstreamGeneratedKey).filter(
|
||||||
|
UpstreamGeneratedKey.upstream_id == upstream.id,
|
||||||
|
UpstreamGeneratedKey.group_id == "vip",
|
||||||
|
).one()
|
||||||
|
assert row.key_value == "sk-remote-plain-value-1234567890abcdef"
|
||||||
|
assert row.masked_key == mask_secret(row.key_value)
|
||||||
|
assert row.status == "exists"
|
||||||
|
|
||||||
|
|
||||||
def test_sync_removes_remote_key_when_list_empty(db_session, monkeypatch):
|
def test_sync_removes_remote_key_when_list_empty(db_session, monkeypatch):
|
||||||
"""同步函数在远端返回空列表时应删除本地 key_id 对应的记录。"""
|
"""同步函数在远端返回空列表时应删除本地 key_id 对应的记录。"""
|
||||||
from app.services import scheduler as sched_mod
|
from app.services import scheduler as sched_mod
|
||||||
@@ -310,6 +368,71 @@ def test_sync_removes_remote_key_when_list_empty(db_session, monkeypatch):
|
|||||||
assert len(remaining) == 0, f"expected 0 after sync with empty remote, got {len(remaining)}"
|
assert len(remaining) == 0, f"expected 0 after sync with empty remote, got {len(remaining)}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_marks_imported_key_orphaned_when_remote_key_missing(db_session, monkeypatch):
|
||||||
|
"""已导入账号管理的 Key 远端消失时保留本地行,避免丢失目标账号关联。"""
|
||||||
|
from app.services import scheduler as sched_mod
|
||||||
|
from app.models.upstream_key import UpstreamGeneratedKey
|
||||||
|
from app.services.upstream_client import UpstreamClient
|
||||||
|
|
||||||
|
website = Website(
|
||||||
|
name="Target",
|
||||||
|
site_type="sub2api",
|
||||||
|
base_url="http://target.local",
|
||||||
|
api_prefix="/api/v1/admin",
|
||||||
|
auth_type="api_key",
|
||||||
|
auth_config_json="{}",
|
||||||
|
groups_endpoint="/groups",
|
||||||
|
group_update_endpoint="/groups/{id}",
|
||||||
|
)
|
||||||
|
upstream = Upstream(name="Test", base_url="http://local", api_prefix="/api/v1",
|
||||||
|
auth_type="bearer", auth_config_json="{}",
|
||||||
|
groups_endpoint="/groups", rate_endpoint="/rates")
|
||||||
|
db_session.add_all([website, upstream])
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(website)
|
||||||
|
db_session.refresh(upstream)
|
||||||
|
|
||||||
|
db_session.add(UpstreamGeneratedKey(
|
||||||
|
upstream_id=upstream.id,
|
||||||
|
group_id="vip",
|
||||||
|
group_name="VIP",
|
||||||
|
key_name="SmartUp-Test-vip",
|
||||||
|
key_value="sk-vip",
|
||||||
|
managed_prefix="SmartUp",
|
||||||
|
key_id="remote-key-id",
|
||||||
|
imported_website_id=website.id,
|
||||||
|
imported_account_id="account-101",
|
||||||
|
))
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
monkeypatch.setattr(UpstreamClient, "list_api_keys", lambda self, **kw: [])
|
||||||
|
monkeypatch.setattr(UpstreamClient, "login", lambda self: None)
|
||||||
|
monkeypatch.setattr(UpstreamClient, "close", lambda self: None)
|
||||||
|
monkeypatch.setattr(UpstreamClient, "__enter__", lambda self: self)
|
||||||
|
monkeypatch.setattr(UpstreamClient, "__exit__", lambda self, *a: None)
|
||||||
|
monkeypatch.setattr(sched_mod, "SessionLocal", lambda: db_session)
|
||||||
|
original_close = db_session.close
|
||||||
|
monkeypatch.setattr(db_session, "close", lambda: None)
|
||||||
|
|
||||||
|
snapshot = {
|
||||||
|
"upstream_id": upstream.id,
|
||||||
|
"groups": {"vip": {"group_id": "vip", "rate": "1"}},
|
||||||
|
"captured_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
captured_at = datetime.now(timezone.utc)
|
||||||
|
sched_mod._sync_upstream_keys(upstream.id, snapshot, captured_at)
|
||||||
|
|
||||||
|
monkeypatch.setattr(db_session, "close", original_close)
|
||||||
|
remaining = db_session.query(UpstreamGeneratedKey).all()
|
||||||
|
assert len(remaining) == 1
|
||||||
|
row = remaining[0]
|
||||||
|
assert row.status == "orphaned"
|
||||||
|
assert row.imported_website_id == website.id
|
||||||
|
assert row.imported_account_id == "account-101"
|
||||||
|
assert row.error == "远端 Key 已不存在"
|
||||||
|
|
||||||
|
|
||||||
def test_migration_function_integration(monkeypatch):
|
def test_migration_function_integration(monkeypatch):
|
||||||
"""直接调用 _migrate_upstream_generated_keys() 验证列新增和索引创建。"""
|
"""直接调用 _migrate_upstream_generated_keys() 验证列新增和索引创建。"""
|
||||||
from app.database import _migrate_upstream_generated_keys, engine as real_engine
|
from app.database import _migrate_upstream_generated_keys, engine as real_engine
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export interface GeneratedUpstreamKey {
|
|||||||
imported_account_id: string | null
|
imported_account_id: string | null
|
||||||
imported_at: string | null
|
imported_at: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
|
has_key_value: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateKeysByGroupsForm {
|
export interface GenerateKeysByGroupsForm {
|
||||||
|
|||||||
@@ -98,6 +98,16 @@
|
|||||||
<div class="panel-title">我的网站分组</div>
|
<div class="panel-title">我的网站分组</div>
|
||||||
<div class="panel-sub">{{ selectedWebsite?.name || '请选择网站' }}</div>
|
<div class="panel-sub">{{ selectedWebsite?.name || '请选择网站' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<el-select
|
||||||
|
:model-value="selectedWebsite?.id"
|
||||||
|
size="small"
|
||||||
|
filterable
|
||||||
|
class="site-switcher"
|
||||||
|
placeholder="切换网站"
|
||||||
|
@change="onSelectedWebsiteChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="site in websites" :key="site.id" :label="site.name" :value="site.id" />
|
||||||
|
</el-select>
|
||||||
<el-button size="small" :disabled="!selectedWebsite" :loading="groupsLoading" @click="loadWebsiteGroups">拉取分组</el-button>
|
<el-button size="small" :disabled="!selectedWebsite" :loading="groupsLoading" @click="loadWebsiteGroups">拉取分组</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="websiteGroups" v-loading="groupsLoading" row-key="id" size="small" style="width:100%">
|
<el-table :data="websiteGroups" v-loading="groupsLoading" row-key="id" size="small" style="width:100%">
|
||||||
@@ -116,11 +126,24 @@
|
|||||||
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div class="panel-title">分组绑定</div>
|
<div>
|
||||||
|
<div class="panel-title">分组绑定</div>
|
||||||
|
<div class="panel-sub">{{ selectedWebsite?.name || '请选择网站' }}</div>
|
||||||
|
</div>
|
||||||
|
<el-select
|
||||||
|
:model-value="selectedWebsite?.id"
|
||||||
|
size="small"
|
||||||
|
filterable
|
||||||
|
class="site-switcher"
|
||||||
|
placeholder="切换网站"
|
||||||
|
@change="onSelectedWebsiteChange"
|
||||||
|
>
|
||||||
|
<el-option v-for="site in websites" :key="site.id" :label="site.name" :value="site.id" />
|
||||||
|
</el-select>
|
||||||
<el-button size="small" text :disabled="websites.length === 0" @click="openBindingCreate(selectedWebsite || websites[0])">新增绑定</el-button>
|
<el-button size="small" text :disabled="websites.length === 0" @click="openBindingCreate(selectedWebsite || websites[0])">新增绑定</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="binding-list" v-loading="bindingLoading">
|
<div class="binding-list" v-loading="bindingLoading">
|
||||||
<div v-for="binding in bindings" :key="binding.id" class="binding-item">
|
<div v-for="binding in selectedWebsiteBindings" :key="binding.id" class="binding-item">
|
||||||
<div class="binding-main">
|
<div class="binding-main">
|
||||||
<div class="binding-title">{{ binding.website_name }} / {{ binding.target_group_name || binding.target_group_id }}</div>
|
<div class="binding-title">{{ binding.website_name }} / {{ binding.target_group_name || binding.target_group_id }}</div>
|
||||||
<div class="binding-meta">
|
<div class="binding-meta">
|
||||||
@@ -135,7 +158,7 @@
|
|||||||
<el-button size="small" text type="danger" @click="deleteBinding(binding)"><el-icon><Delete /></el-icon></el-button>
|
<el-button size="small" text type="danger" @click="deleteBinding(binding)"><el-icon><Delete /></el-icon></el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!bindingLoading && bindings.length === 0" class="empty-hint">暂无绑定</div>
|
<div v-if="!bindingLoading && selectedWebsiteBindings.length === 0" class="empty-hint">暂无绑定</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,7 +354,7 @@
|
|||||||
<el-icon><Refresh /></el-icon>刷新导入状态
|
<el-icon><Refresh /></el-icon>刷新导入状态
|
||||||
</el-button>
|
</el-button>
|
||||||
<span v-if="importSyncStatus" style="font-size:12px;color:var(--text-muted);margin-left:8px">
|
<span v-if="importSyncStatus" style="font-size:12px;color:var(--text-muted);margin-left:8px">
|
||||||
已校验 {{ importSyncStatus.total }} 个,清除 {{ importSyncStatus.cleared }} 个失效标记
|
已校验已导入标记 {{ importSyncStatus.total }} 个,清除 {{ importSyncStatus.cleared }} 个失效标记
|
||||||
</span>
|
</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -366,6 +389,9 @@
|
|||||||
<el-button size="small" text @click="selectAllImportableKeys">全选可导入 Key</el-button>
|
<el-button size="small" text @click="selectAllImportableKeys">全选可导入 Key</el-button>
|
||||||
<el-button size="small" text @click="clearImportAccountSelection">清空</el-button>
|
<el-button size="small" text @click="clearImportAccountSelection">清空</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="importGeneratedKeys.length" class="import-stats">
|
||||||
|
总计 {{ importGeneratedKeys.length }} 个,现可导入 {{ importableGeneratedKeys.length }} 个,已导入 {{ importedGeneratedKeyCount }} 个,无明文 {{ missingPlaintextKeyCount }} 个
|
||||||
|
</div>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="importAccountsForm.upstream_key_ids"
|
v-model="importAccountsForm.upstream_key_ids"
|
||||||
multiple
|
multiple
|
||||||
@@ -573,6 +599,7 @@ const importSourceGroups = computed(() => snapshotsByUpstream.value[importGroups
|
|||||||
|
|
||||||
function isImportableGeneratedKey(item: GeneratedUpstreamKey) {
|
function isImportableGeneratedKey(item: GeneratedUpstreamKey) {
|
||||||
return item.id !== null
|
return item.id !== null
|
||||||
|
&& item.has_key_value
|
||||||
&& item.status !== 'failed'
|
&& item.status !== 'failed'
|
||||||
&& !(item.imported_website_id === importAccountsForm.value.website_id && item.imported_account_id)
|
&& !(item.imported_website_id === importAccountsForm.value.website_id && item.imported_account_id)
|
||||||
}
|
}
|
||||||
@@ -581,6 +608,21 @@ const importableGeneratedKeys = computed(() =>
|
|||||||
importGeneratedKeys.value.filter(isImportableGeneratedKey),
|
importGeneratedKeys.value.filter(isImportableGeneratedKey),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const importedGeneratedKeyCount = computed(() =>
|
||||||
|
importGeneratedKeys.value.filter((item) =>
|
||||||
|
item.imported_website_id === importAccountsForm.value.website_id && Boolean(item.imported_account_id),
|
||||||
|
).length,
|
||||||
|
)
|
||||||
|
|
||||||
|
const missingPlaintextKeyCount = computed(() =>
|
||||||
|
importGeneratedKeys.value.filter((item) => !item.has_key_value).length,
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedWebsiteBindings = computed(() => {
|
||||||
|
if (!selectedWebsite.value) return []
|
||||||
|
return bindings.value.filter((binding) => binding.website_id === selectedWebsite.value?.id)
|
||||||
|
})
|
||||||
|
|
||||||
const selectedAccountGroups = computed(() => {
|
const selectedAccountGroups = computed(() => {
|
||||||
const selected = new Set(importAccountsForm.value.upstream_key_ids)
|
const selected = new Set(importAccountsForm.value.upstream_key_ids)
|
||||||
const rows = importableGeneratedKeys.value.filter((item) => item.id !== null && selected.has(item.id))
|
const rows = importableGeneratedKeys.value.filter((item) => item.id !== null && selected.has(item.id))
|
||||||
@@ -605,6 +647,14 @@ async function loadWebsites() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onSelectedWebsiteChange(value: number | string) {
|
||||||
|
const nextId = Number(value)
|
||||||
|
const next = websites.value.find((site) => site.id === nextId)
|
||||||
|
if (!next) return
|
||||||
|
selectedWebsite.value = next
|
||||||
|
await loadWebsiteGroups()
|
||||||
|
}
|
||||||
|
|
||||||
async function loadUpstreamGroups() {
|
async function loadUpstreamGroups() {
|
||||||
const upstreamRes = await upstreamsApi.list()
|
const upstreamRes = await upstreamsApi.list()
|
||||||
upstreams.value = upstreamRes.data
|
upstreams.value = upstreamRes.data
|
||||||
@@ -682,12 +732,13 @@ async function loadLogs() {
|
|||||||
async function syncImportStatus() {
|
async function syncImportStatus() {
|
||||||
const websiteId = importAccountsForm.value.website_id
|
const websiteId = importAccountsForm.value.website_id
|
||||||
const upstreamId = importAccountsForm.value.upstream_id
|
const upstreamId = importAccountsForm.value.upstream_id
|
||||||
if (!websiteId || !upstreamId) return
|
if (!websiteId || !upstreamId) return false
|
||||||
syncingImportStatus.value = true
|
syncingImportStatus.value = true
|
||||||
|
let reloaded = false
|
||||||
try {
|
try {
|
||||||
const res = await websitesApi.syncImportedUpstreamKeys(websiteId, { upstream_id: upstreamId })
|
const res = await websitesApi.syncImportedUpstreamKeys(websiteId, { upstream_id: upstreamId })
|
||||||
// 校验请求完成时表单未切换
|
// 校验请求完成时表单未切换
|
||||||
if (importAccountsForm.value.website_id !== websiteId || importAccountsForm.value.upstream_id !== upstreamId) return
|
if (importAccountsForm.value.website_id !== websiteId || importAccountsForm.value.upstream_id !== upstreamId) return false
|
||||||
const items = res.data.items
|
const items = res.data.items
|
||||||
importSyncStatus.value = {
|
importSyncStatus.value = {
|
||||||
total: items.length,
|
total: items.length,
|
||||||
@@ -699,12 +750,14 @@ async function syncImportStatus() {
|
|||||||
}
|
}
|
||||||
if (importAccountsForm.value.upstream_id === upstreamId) {
|
if (importAccountsForm.value.upstream_id === upstreamId) {
|
||||||
await loadImportGeneratedKeys(upstreamId)
|
await loadImportGeneratedKeys(upstreamId)
|
||||||
|
reloaded = true
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e.response?.data?.detail || '同步导入状态失败')
|
ElMessage.error(e.response?.data?.detail || '同步导入状态失败')
|
||||||
} finally {
|
} finally {
|
||||||
syncingImportStatus.value = false
|
syncingImportStatus.value = false
|
||||||
}
|
}
|
||||||
|
return reloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadImportGeneratedKeys(upstreamId: number) {
|
async function loadImportGeneratedKeys(upstreamId: number) {
|
||||||
@@ -977,30 +1030,26 @@ async function openImportAccounts(site?: WebsiteData | null) {
|
|||||||
}
|
}
|
||||||
importAccountResults.value = []
|
importAccountResults.value = []
|
||||||
importSyncStatus.value = null
|
importSyncStatus.value = null
|
||||||
await Promise.all([
|
await loadImportTargetGroups(importAccountsForm.value.website_id)
|
||||||
loadImportTargetGroups(importAccountsForm.value.website_id),
|
|
||||||
loadImportGeneratedKeys(importAccountsForm.value.upstream_id),
|
|
||||||
])
|
|
||||||
// 打开弹窗后自动同步导入状态(校验远端账号是否仍存在)
|
// 打开弹窗后自动同步导入状态(校验远端账号是否仍存在)
|
||||||
await syncImportStatus()
|
const reloaded = await syncImportStatus()
|
||||||
await loadImportGeneratedKeys(importAccountsForm.value.upstream_id)
|
if (!reloaded) await loadImportGeneratedKeys(importAccountsForm.value.upstream_id)
|
||||||
importAccountsDialog.value = true
|
importAccountsDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onImportAccountWebsiteChange(value: number) {
|
async function onImportAccountWebsiteChange(value: number) {
|
||||||
importAccountsForm.value.target_group_map = {}
|
importAccountsForm.value.target_group_map = {}
|
||||||
await loadImportTargetGroups(value)
|
await loadImportTargetGroups(value)
|
||||||
await syncImportStatus()
|
const reloaded = await syncImportStatus()
|
||||||
await loadImportGeneratedKeys(importAccountsForm.value.upstream_id)
|
if (!reloaded) await loadImportGeneratedKeys(importAccountsForm.value.upstream_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onImportAccountUpstreamChange(value: number) {
|
async function onImportAccountUpstreamChange(value: number) {
|
||||||
importAccountsForm.value.upstream_key_ids = []
|
importAccountsForm.value.upstream_key_ids = []
|
||||||
importAccountsForm.value.target_group_map = {}
|
importAccountsForm.value.target_group_map = {}
|
||||||
importAccountResults.value = []
|
importAccountResults.value = []
|
||||||
await loadImportGeneratedKeys(value)
|
const reloaded = await syncImportStatus()
|
||||||
await syncImportStatus()
|
if (!reloaded) await loadImportGeneratedKeys(value)
|
||||||
await loadImportGeneratedKeys(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlatformModeChange(value: string) {
|
function onPlatformModeChange(value: string) {
|
||||||
@@ -1199,6 +1248,11 @@ onMounted(loadAll)
|
|||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.site-switcher {
|
||||||
|
width: 180px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.binding-actions {
|
.binding-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1216,6 +1270,11 @@ onMounted(loadAll)
|
|||||||
--el-button-border-color: var(--el-color-danger-light-5, #fbc4c4);
|
--el-button-border-color: var(--el-color-danger-light-5, #fbc4c4);
|
||||||
}
|
}
|
||||||
.binding-list { min-height: 120px; }
|
.binding-list { min-height: 120px; }
|
||||||
|
.import-stats {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
.binding-item {
|
.binding-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user