feat: Cloudflare多账号子资产管理(zone/worker/r2/tunnel/mail子资产模型+模态框)

This commit is contained in:
gouki
2026-08-02 23:41:49 +00:00
parent dd2638601a
commit af17294f24
5 changed files with 73 additions and 4 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ engine = assets_engine
def init_db() -> None:
"""分库建表(幂等,可重复调用)"""
from app.models.asset import AIAccount, Asset, DomainDetail, VPSDetail # noqa: F401
from app.models.asset import AIAccount, Asset, CloudflareDetail, DomainDetail, VPSDetail # noqa: F401
from app.models.monitor import ( # noqa: F401
EventLog,
MetricPoint,
@@ -38,7 +38,7 @@ def init_db() -> None:
)
from app.models.provider import Provider # noqa: F401
asset_models = [Provider, Asset, VPSDetail, DomainDetail, AIAccount]
asset_models = [Provider, Asset, VPSDetail, DomainDetail, AIAccount, CloudflareDetail]
metric_models = [MetricPoint, ServerInfo, SecurityCheck, EventLog]
SQLModel.metadata.create_all(
+28
View File
@@ -162,3 +162,31 @@ class AIAccount(SQLModel, table=True):
last_synced_at: Optional[datetime] = Field(
default=None, description="最近一次自动同步余额/用量的时间"
)
class CloudflareDetail(SQLModel, table=True):
"""Cloudflare 子资产详情(一对一关联 Asset)
一个 Cloudflare 账号可有多个子资产(zone/worker/r2/tunnel/mail)。
每个子资产是一个 Asset(asset_type=cloudflare) + CloudflareDetail。
"""
__tablename__ = "cloudflare_details"
id: Optional[int] = Field(default=None, primary_key=True)
asset_id: int = Field(
foreign_key="assets.id", unique=True, index=True, description="关联资产 ID"
)
account_email: Optional[str] = Field(
default=None, index=True, description="Cloudflare 账号邮箱(区分多账号)"
)
sub_type: str = Field(
default="zone", index=True, description="子资产类型:zone/worker/r2/tunnel/mail/dns_record/other"
)
sub_name: Optional[str] = Field(
default=None, description="子资产名称(如 worker 名、bucket 名)"
)
zone_name: Optional[str] = Field(
default=None, index=True, description="关联的 zone/域名"
)
status: Optional[str] = Field(default=None, description="子资产状态")
+18
View File
@@ -61,6 +61,16 @@ class AIAccountIn(SQLModel):
monthly_limit: Optional[float] = None
class CloudflareDetailIn(SQLModel):
"""Cloudflare 子资产详情输入"""
account_email: Optional[str] = None
sub_type: str = "zone"
sub_name: Optional[str] = None
zone_name: Optional[str] = None
status: Optional[str] = None
# --------------------------------------------------------------------------- #
# 资产主表模型
# --------------------------------------------------------------------------- #
@@ -90,6 +100,7 @@ class AssetCreate(AssetBase):
vps_detail: Optional[VPSDetailIn] = None
domain_detail: Optional[DomainDetailIn] = None
ai_detail: Optional[AIAccountIn] = None
cloudflare_detail: Optional[CloudflareDetailIn] = None
class AssetUpdate(SQLModel):
@@ -113,6 +124,7 @@ class AssetUpdate(SQLModel):
vps_detail: Optional[VPSDetailIn] = None
domain_detail: Optional[DomainDetailIn] = None
ai_detail: Optional[AIAccountIn] = None
cloudflare_detail: Optional[CloudflareDetailIn] = None
# --------------------------------------------------------------------------- #
@@ -160,6 +172,11 @@ class AIAccountRead(SQLModel):
has_api_key: bool = False
class CloudflareDetailRead(CloudflareDetailIn):
id: int
asset_id: int
class AssetRead(AssetBase):
"""资产完整输出"""
@@ -171,3 +188,4 @@ class AssetRead(AssetBase):
vps_detail: Optional[VPSDetailRead] = None
domain_detail: Optional[DomainDetailRead] = None
ai_detail: Optional[AIAccountRead] = None
cloudflare_detail: Optional[CloudflareDetailRead] = None
+9 -2
View File
@@ -15,6 +15,7 @@ from app.models.asset import (
Asset,
AssetStatus,
AssetType,
CloudflareDetail,
DomainDetail,
VPSDetail,
)
@@ -24,6 +25,7 @@ from app.schemas.asset import (
AssetCreate,
AssetRead,
AssetUpdate,
CloudflareDetailRead,
DomainDetailRead,
VPSDetailRead,
)
@@ -33,6 +35,7 @@ DETAIL_MAP = {
AssetType.VPS: ("vps_detail", VPSDetail),
AssetType.DOMAIN: ("domain_detail", DomainDetail),
AssetType.AI_AGENT: ("ai_detail", AIAccount),
AssetType.CLOUDFLARE: ("cloudflare_detail", CloudflareDetail),
}
# 允许排序的字段白名单
@@ -71,6 +74,8 @@ def _detail_to_read(detail):
read = AIAccountRead.model_validate(detail)
read.has_api_key = bool(detail.api_key_encrypted or detail.api_key)
return read
if isinstance(detail, CloudflareDetail):
return CloudflareDetailRead.model_validate(detail)
return None
@@ -94,6 +99,8 @@ def _to_read(session: Session, asset: Asset, detail) -> AssetRead:
read.domain_detail = _detail_to_read(detail)
elif isinstance(detail, AIAccount):
read.ai_detail = _detail_to_read(detail)
elif isinstance(detail, CloudflareDetail):
read.cloudflare_detail = _detail_to_read(detail)
return read
@@ -151,7 +158,7 @@ def create_asset(session: Session, data: AssetCreate) -> AssetRead:
)
asset_data = data.model_dump(
exclude={"vps_detail", "domain_detail", "ai_detail"}
exclude={"vps_detail", "domain_detail", "ai_detail", "cloudflare_detail"}
)
asset = Asset(**asset_data)
session.add(asset)
@@ -183,7 +190,7 @@ def update_asset(session: Session, asset_id: int, data: AssetUpdate) -> AssetRea
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在")
main_fields = data.model_dump(
exclude_unset=True, exclude={"vps_detail", "domain_detail", "ai_detail"}
exclude_unset=True, exclude={"vps_detail", "domain_detail", "ai_detail", "cloudflare_detail"}
)
for key, value in main_fields.items():
setattr(asset, key, value)