refactor: 凭证下沉账号层——平台不再持有API凭证;账号支持登录凭证+API配置,SDK测试/同步改为账号维度,同步产出自动挂账号;含存量数据自动迁移
This commit is contained in:
+110
-21
@@ -3,6 +3,10 @@
|
||||
将适配器返回的标准化资产(NormalizedVPS / NormalizedDomain / AccountInfo)
|
||||
写入或更新到资产库。以 (provider_id, external_id) 作为去重键,
|
||||
已存在则更新状态/详情,不存在则新建资产。
|
||||
|
||||
凭证层级:API 配置存在账号(Account.api_config_encrypted)上,
|
||||
同步按账号维度进行(test_account/sync_account);同步产出的资产
|
||||
自动挂到该账号名下(Asset.account)。平台不再持有凭证。
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -20,6 +24,7 @@ from app.core import crypto
|
||||
from app.core.timeutils import utcnow
|
||||
from app.models.asset import (
|
||||
AIAccount,
|
||||
Account,
|
||||
Asset,
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
@@ -32,7 +37,7 @@ _VALID_STATUS = {s.value for s in AssetStatus}
|
||||
|
||||
|
||||
def _load_config(provider: Provider) -> dict:
|
||||
"""解密平台的 API 配置 JSON"""
|
||||
"""(已弃用)解密平台级 API 配置,仅为兼容历史数据保留"""
|
||||
plain = crypto.decrypt(provider.api_config_encrypted)
|
||||
if not plain:
|
||||
return {}
|
||||
@@ -42,6 +47,30 @@ def _load_config(provider: Provider) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _load_account_config(account: Account) -> dict:
|
||||
"""解密账号的 API 配置 JSON"""
|
||||
plain = crypto.decrypt(account.api_config_encrypted)
|
||||
if not plain:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(plain)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return {}
|
||||
|
||||
|
||||
def _find_provider_by_platform(session: Session, platform: str) -> Provider:
|
||||
"""按账号的 platform(slug 或名称)定位平台"""
|
||||
provider = session.exec(select(Provider).where(Provider.slug == platform)).first()
|
||||
if not provider:
|
||||
provider = session.exec(select(Provider).where(Provider.name == platform)).first()
|
||||
if not provider:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"账号所属平台不存在:{platform}(请先在账号编辑中选择有效平台)",
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
def _get_provider(session: Session, provider_id: int) -> Provider:
|
||||
provider = session.get(Provider, provider_id)
|
||||
if not provider:
|
||||
@@ -49,7 +78,7 @@ def _get_provider(session: Session, provider_id: int) -> Provider:
|
||||
return provider
|
||||
|
||||
|
||||
def _build_adapter(provider: Provider) -> BaseAdapter:
|
||||
def _build_adapter(provider: Provider, config: dict) -> BaseAdapter:
|
||||
if not provider.sdk_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="该平台未配置 SDK 类型(sdk_type)"
|
||||
@@ -59,7 +88,27 @@ def _build_adapter(provider: Provider) -> BaseAdapter:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"暂不支持的 SDK 类型:{provider.sdk_type}",
|
||||
)
|
||||
return registry.get_adapter(provider.sdk_type, _load_config(provider))
|
||||
return registry.get_adapter(provider.sdk_type, config)
|
||||
|
||||
|
||||
def _get_account(session: Session, account_id: int) -> Account:
|
||||
account = session.get(Account, account_id)
|
||||
if not account:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="账号不存在")
|
||||
return account
|
||||
|
||||
|
||||
def _build_adapter_for_account(session: Session, account: Account):
|
||||
"""按账号构建适配器:平台定 sdk_type,账号提供凭证"""
|
||||
if not account.platform:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该账号未指定所属平台,无法确定 SDK 类型",
|
||||
)
|
||||
provider = _find_provider_by_platform(session, account.platform)
|
||||
config = _load_account_config(account)
|
||||
adapter = _build_adapter(provider, config)
|
||||
return provider, adapter
|
||||
|
||||
|
||||
def _norm_status(raw: Optional[str]) -> AssetStatus:
|
||||
@@ -72,15 +121,37 @@ def _missing_config(adapter: BaseAdapter) -> list:
|
||||
|
||||
|
||||
def test_provider(session: Session, provider_id: int) -> dict:
|
||||
"""测试平台连接 / 凭证有效性"""
|
||||
"""(兼容入口)测试平台连接:凭证已下沉到账号,自动找该平台第一个配了凭证的账号"""
|
||||
provider = _get_provider(session, provider_id)
|
||||
adapter = _build_adapter(provider)
|
||||
base = {"capabilities": adapter.capabilities(), "sdk_type": provider.sdk_type}
|
||||
account = _first_account_with_config(session, provider)
|
||||
if not account:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="凭证已改为在账号上配置:请先在该平台的账号管理中新建账号并填写 API 配置",
|
||||
)
|
||||
return test_account(session, account.id)
|
||||
|
||||
|
||||
def _first_account_with_config(session: Session, provider: Provider):
|
||||
"""找该平台下第一个配置了 API 凭证的账号(兼容旧入口)"""
|
||||
return session.exec(
|
||||
select(Account)
|
||||
.where(Account.platform == provider.slug)
|
||||
.where(Account.api_config_encrypted.is_not(None)) # type: ignore[union-attr]
|
||||
.order_by(Account.id.asc())
|
||||
).first()
|
||||
|
||||
|
||||
def test_account(session: Session, account_id: int) -> dict:
|
||||
"""测试账号凭证有效性(按账号 platform 定 SDK,凭证取自账号)"""
|
||||
account = _get_account(session, account_id)
|
||||
provider, adapter = _build_adapter_for_account(session, account)
|
||||
base = {"capabilities": adapter.capabilities(), "sdk_type": provider.sdk_type, "account": account.name}
|
||||
missing = _missing_config(adapter)
|
||||
if missing:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": f"缺少凭证配置:{', '.join(missing)}(请在平台编辑里填写 API 配置)",
|
||||
"message": f"缺少凭证配置:{', '.join(missing)}(请在账号编辑里填写 API 配置)",
|
||||
**base,
|
||||
}
|
||||
result = adapter.test_connection()
|
||||
@@ -98,7 +169,7 @@ def _find_asset(session: Session, provider_id: int, external_id: str, asset_type
|
||||
).first()
|
||||
|
||||
|
||||
def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter) -> dict:
|
||||
def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter, account_name: Optional[str] = None) -> dict:
|
||||
created = updated = 0
|
||||
for vps in adapter.list_vps():
|
||||
existing = _find_asset(session, provider.id, vps.external_id, AssetType.VPS)
|
||||
@@ -108,6 +179,8 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter) -> dic
|
||||
if vps.monthly_cost is not None:
|
||||
existing.cost = vps.monthly_cost
|
||||
existing.currency = vps.currency
|
||||
if account_name:
|
||||
existing.account = account_name
|
||||
session.add(existing)
|
||||
detail = session.exec(
|
||||
select(VPSDetail).where(VPSDetail.asset_id == existing.id)
|
||||
@@ -128,6 +201,7 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter) -> dic
|
||||
provider=provider.slug,
|
||||
provider_id=provider.id,
|
||||
external_id=vps.external_id,
|
||||
account=account_name,
|
||||
status=_norm_status(vps.status),
|
||||
cost=vps.monthly_cost or 0,
|
||||
currency=vps.currency,
|
||||
@@ -150,7 +224,7 @@ def _sync_vps(session: Session, provider: Provider, adapter: BaseAdapter) -> dic
|
||||
return {"created": created, "updated": updated}
|
||||
|
||||
|
||||
def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter) -> dict:
|
||||
def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter, account_name: Optional[str] = None) -> dict:
|
||||
created = updated = 0
|
||||
for dom in adapter.list_domains():
|
||||
existing = _find_asset(session, provider.id, dom.external_id, AssetType.DOMAIN)
|
||||
@@ -158,6 +232,8 @@ def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter) ->
|
||||
existing.status = _norm_status(dom.status)
|
||||
if dom.expiry_date:
|
||||
existing.expiry_date = dom.expiry_date
|
||||
if account_name:
|
||||
existing.account = account_name
|
||||
session.add(existing)
|
||||
detail = session.exec(
|
||||
select(DomainDetail).where(DomainDetail.asset_id == existing.id)
|
||||
@@ -174,6 +250,7 @@ def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter) ->
|
||||
provider=provider.slug,
|
||||
provider_id=provider.id,
|
||||
external_id=dom.external_id,
|
||||
account=account_name,
|
||||
status=_norm_status(dom.status),
|
||||
expiry_date=dom.expiry_date,
|
||||
)
|
||||
@@ -192,39 +269,51 @@ def _sync_domains(session: Session, provider: Provider, adapter: BaseAdapter) ->
|
||||
|
||||
|
||||
def sync_provider(session: Session, provider_id: int) -> dict:
|
||||
"""同步平台资产到本地库"""
|
||||
"""(兼容入口)同步平台资产:凭证已下沉到账号,自动找该平台第一个配了凭证的账号"""
|
||||
provider = _get_provider(session, provider_id)
|
||||
adapter = _build_adapter(provider)
|
||||
account = _first_account_with_config(session, provider)
|
||||
if not account:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="凭证已改为在账号上配置:请先在该平台的账号管理中新建账号并填写 API 配置",
|
||||
)
|
||||
return sync_account(session, account.id)
|
||||
|
||||
|
||||
def sync_account(session: Session, account_id: int) -> dict:
|
||||
"""同步账号资产到本地库(凭证取自账号,同步产出自动挂到该账号名下)"""
|
||||
account = _get_account(session, account_id)
|
||||
provider, adapter = _build_adapter_for_account(session, account)
|
||||
missing = _missing_config(adapter)
|
||||
if missing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"缺少凭证配置:{', '.join(missing)}(请先在平台编辑里填写 API 配置)",
|
||||
detail=f"缺少凭证配置:{', '.join(missing)}(请先在账号编辑里填写 API 配置)",
|
||||
)
|
||||
caps = adapter.capabilities()
|
||||
result = {"provider": provider.slug, "sdk_type": provider.sdk_type}
|
||||
result = {"provider": provider.slug, "sdk_type": provider.sdk_type, "account": account.name}
|
||||
|
||||
if caps["list_vps"]:
|
||||
try:
|
||||
result["vps"] = _sync_vps(session, provider, adapter)
|
||||
result["vps"] = _sync_vps(session, provider, adapter, account.name)
|
||||
except Exception as e: # noqa: BLE001
|
||||
result["vps_error"] = str(e)
|
||||
if caps["list_domains"]:
|
||||
try:
|
||||
result["domains"] = _sync_domains(session, provider, adapter)
|
||||
result["domains"] = _sync_domains(session, provider, adapter, account.name)
|
||||
except Exception as e: # noqa: BLE001
|
||||
result["domains_error"] = str(e)
|
||||
if caps["get_account"]:
|
||||
try:
|
||||
result["account"] = adapter.get_account().to_dict()
|
||||
result["account_info"] = adapter.get_account().to_dict()
|
||||
except Exception as e: # noqa: BLE001
|
||||
result["account_error"] = str(e)
|
||||
result["account_info_error"] = str(e)
|
||||
|
||||
provider.last_synced_at = utcnow()
|
||||
session.add(provider)
|
||||
account.last_synced_at = utcnow()
|
||||
session.add(account)
|
||||
session.commit()
|
||||
result["last_synced_at"] = provider.last_synced_at.isoformat()
|
||||
logger.info("同步平台 provider=%s result=%s", provider.slug, result)
|
||||
result["last_synced_at"] = account.last_synced_at.isoformat()
|
||||
logger.info("同步账号 account=%s provider=%s result=%s", account.name, provider.slug, result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user