feat: 平台预设字典+凭证加密+分库备份+Agent采集+移动端优先前端重做
This commit is contained in:
@@ -9,6 +9,7 @@ from typing import List, Optional
|
||||
from fastapi import HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core import crypto
|
||||
from app.models.asset import (
|
||||
AIAccount,
|
||||
Asset,
|
||||
@@ -17,6 +18,7 @@ from app.models.asset import (
|
||||
DomainDetail,
|
||||
VPSDetail,
|
||||
)
|
||||
from app.models.provider import Provider
|
||||
from app.schemas.asset import (
|
||||
AIAccountRead,
|
||||
AssetCreate,
|
||||
@@ -56,19 +58,69 @@ def _get_detail(session: Session, asset: Asset):
|
||||
return session.exec(select(model).where(model.asset_id == asset.id)).first()
|
||||
|
||||
|
||||
def _to_read(asset: Asset, detail) -> AssetRead:
|
||||
def _detail_to_read(detail):
|
||||
"""详情模型转输出模型(VPS 计算凭证存在标记,不输出敏感明文)"""
|
||||
if isinstance(detail, VPSDetail):
|
||||
read = VPSDetailRead.model_validate(detail)
|
||||
read.has_ssh_key = bool(detail.ssh_key_encrypted)
|
||||
read.has_password = bool(detail.password_encrypted)
|
||||
return read
|
||||
if isinstance(detail, DomainDetail):
|
||||
return DomainDetailRead.model_validate(detail)
|
||||
if isinstance(detail, AIAccount):
|
||||
return AIAccountRead.model_validate(detail)
|
||||
return None
|
||||
|
||||
|
||||
def _provider_name(session: Session, asset: Asset) -> Optional[str]:
|
||||
"""取关联平台显示名"""
|
||||
if asset.provider_id:
|
||||
provider = session.get(Provider, asset.provider_id)
|
||||
if provider:
|
||||
return provider.name
|
||||
return None
|
||||
|
||||
|
||||
def _to_read(session: Session, asset: Asset, detail) -> AssetRead:
|
||||
"""组装 AssetRead 输出(主表 + 详情 + 计算字段)"""
|
||||
read = AssetRead.model_validate(asset)
|
||||
read.days_to_expiry = _days_to_expiry(asset.expiry_date)
|
||||
read.provider_name = _provider_name(session, asset)
|
||||
if isinstance(detail, VPSDetail):
|
||||
read.vps_detail = VPSDetailRead.model_validate(detail)
|
||||
read.vps_detail = _detail_to_read(detail)
|
||||
elif isinstance(detail, DomainDetail):
|
||||
read.domain_detail = DomainDetailRead.model_validate(detail)
|
||||
read.domain_detail = _detail_to_read(detail)
|
||||
elif isinstance(detail, AIAccount):
|
||||
read.ai_detail = AIAccountRead.model_validate(detail)
|
||||
read.ai_detail = _detail_to_read(detail)
|
||||
return read
|
||||
|
||||
|
||||
def _build_detail(asset_type: AssetType, asset_id: int, detail_in):
|
||||
"""构建详情模型(VPS 加密凭证)"""
|
||||
_, model = DETAIL_MAP[asset_type]
|
||||
if asset_type == AssetType.VPS:
|
||||
data = detail_in.model_dump(exclude={"ssh_key", "password"})
|
||||
data["ssh_key_encrypted"] = crypto.encrypt(detail_in.ssh_key)
|
||||
data["password_encrypted"] = crypto.encrypt(detail_in.password)
|
||||
return model(asset_id=asset_id, **data)
|
||||
return model(asset_id=asset_id, **detail_in.model_dump())
|
||||
|
||||
|
||||
def _apply_detail_update(asset_type: AssetType, existing, detail_in) -> None:
|
||||
"""更新详情字段(VPS 加密凭证;凭证为 None 时保留原值)"""
|
||||
if asset_type == AssetType.VPS:
|
||||
data = detail_in.model_dump(exclude={"ssh_key", "password"})
|
||||
for key, value in data.items():
|
||||
setattr(existing, key, value)
|
||||
if detail_in.ssh_key is not None:
|
||||
existing.ssh_key_encrypted = crypto.encrypt(detail_in.ssh_key)
|
||||
if detail_in.password is not None:
|
||||
existing.password_encrypted = crypto.encrypt(detail_in.password)
|
||||
else:
|
||||
for key, value in detail_in.model_dump().items():
|
||||
setattr(existing, key, value)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# CRUD
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -96,12 +148,12 @@ def create_asset(session: Session, data: AssetCreate) -> AssetRead:
|
||||
|
||||
detail = None
|
||||
if model is not None and detail_in is not None:
|
||||
detail = model(asset_id=asset.id, **detail_in.model_dump())
|
||||
detail = _build_detail(data.asset_type, asset.id, detail_in)
|
||||
session.add(detail)
|
||||
session.commit()
|
||||
session.refresh(detail)
|
||||
|
||||
return _to_read(asset, detail)
|
||||
return _to_read(session, asset, detail)
|
||||
|
||||
|
||||
def get_asset(session: Session, asset_id: int) -> AssetRead:
|
||||
@@ -109,7 +161,7 @@ def get_asset(session: Session, asset_id: int) -> AssetRead:
|
||||
asset = session.get(Asset, asset_id)
|
||||
if not asset:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在")
|
||||
return _to_read(asset, _get_detail(session, asset))
|
||||
return _to_read(session, asset, _get_detail(session, asset))
|
||||
|
||||
|
||||
def update_asset(session: Session, asset_id: int, data: AssetUpdate) -> AssetRead:
|
||||
@@ -136,12 +188,11 @@ def update_asset(session: Session, asset_id: int, data: AssetUpdate) -> AssetRea
|
||||
).first()
|
||||
if detail_in is not None:
|
||||
if existing:
|
||||
for key, value in detail_in.model_dump().items():
|
||||
setattr(existing, key, value)
|
||||
_apply_detail_update(asset.asset_type, existing, detail_in)
|
||||
session.add(existing)
|
||||
detail = existing
|
||||
else:
|
||||
detail = model(asset_id=asset.id, **detail_in.model_dump())
|
||||
detail = _build_detail(asset.asset_type, asset.id, detail_in)
|
||||
session.add(detail)
|
||||
else:
|
||||
detail = existing
|
||||
@@ -150,7 +201,7 @@ def update_asset(session: Session, asset_id: int, data: AssetUpdate) -> AssetRea
|
||||
session.refresh(asset)
|
||||
if detail is not None:
|
||||
session.refresh(detail)
|
||||
return _to_read(asset, detail)
|
||||
return _to_read(session, asset, detail)
|
||||
|
||||
|
||||
def delete_asset(session: Session, asset_id: int) -> None:
|
||||
@@ -190,7 +241,7 @@ def list_assets(
|
||||
stmt = stmt.order_by(sort_col.desc() if order == "desc" else sort_col.asc())
|
||||
|
||||
assets = session.exec(stmt).all()
|
||||
return [_to_read(a, _get_detail(session, a)) for a in assets]
|
||||
return [_to_read(session, a, _get_detail(session, a)) for a in assets]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -241,6 +292,6 @@ def get_expiring(session: Session, days: int = 30) -> List[AssetRead]:
|
||||
for a in assets:
|
||||
delta = (a.expiry_date - today).days
|
||||
if 0 <= delta <= days:
|
||||
result.append(_to_read(a, _get_detail(session, a)))
|
||||
result.append(_to_read(session, a, _get_detail(session, a)))
|
||||
result.sort(key=lambda x: x.days_to_expiry if x.days_to_expiry is not None else 10**9)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user