From af17294f2447747f1d393ec291f3017a78a8520c Mon Sep 17 00:00:00 2001 From: gouki Date: Sun, 2 Aug 2026 23:41:49 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Cloudflare=E5=A4=9A=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=AD=90=E8=B5=84=E4=BA=A7=E7=AE=A1=E7=90=86=EF=BC=88zone/work?= =?UTF-8?q?er/r2/tunnel/mail=E5=AD=90=E8=B5=84=E4=BA=A7=E6=A8=A1=E5=9E=8B+?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=A1=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/database.py | 4 ++-- app/models/asset.py | 28 ++++++++++++++++++++++++++++ app/schemas/asset.py | 18 ++++++++++++++++++ app/services/asset_service.py | 11 +++++++++-- static/js/app.js | 16 ++++++++++++++++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/app/database.py b/app/database.py index 2cc765c..f44d04c 100644 --- a/app/database.py +++ b/app/database.py @@ -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( diff --git a/app/models/asset.py b/app/models/asset.py index 6103f9d..5f96beb 100644 --- a/app/models/asset.py +++ b/app/models/asset.py @@ -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="子资产状态") diff --git a/app/schemas/asset.py b/app/schemas/asset.py index 0cc0234..2bdaca7 100644 --- a/app/schemas/asset.py +++ b/app/schemas/asset.py @@ -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 diff --git a/app/services/asset_service.py b/app/services/asset_service.py index 05466c7..0b544dc 100644 --- a/app/services/asset_service.py +++ b/app/services/asset_service.py @@ -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) diff --git a/static/js/app.js b/static/js/app.js index 86735c6..12b4181 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -35,6 +35,7 @@ function emptyAssetForm() { vps_detail: { ip_address: '', tailscale_ip: '', region: '', os: '', cpu_cores: 1, memory_gb: 1, disk_gb: 20, bandwidth_gb: null, ssh_port: 22, panel_url: '', ssh_user: '', login_method: 'key', ssh_key: '', password: '', purpose: '' }, domain_detail: { domain_name: '', registrar: '', dns_provider: '', cloudflare_account: '', is_using: true, redirect_target: '', bind_asset_id: null }, ai_detail: { provider: '', api_key: '', plan: '', balance: null, currency: 'USD', monthly_usage: null, monthly_limit: null }, + cloudflare_detail: { account_email: '', sub_type: 'zone', sub_name: '', zone_name: '', status: '' }, }; } @@ -56,6 +57,9 @@ function buildAssetPayload(f) { } else if (f.asset_type === 'ai_agent') { const a = f.ai_detail; p.ai_detail = { provider: a.provider, api_key: a.api_key || null, plan: a.plan || null, balance: a.balance ? Number(a.balance) : null, currency: a.currency || 'USD', monthly_usage: a.monthly_usage ? Number(a.monthly_usage) : null, monthly_limit: a.monthly_limit ? Number(a.monthly_limit) : null }; + } else if (f.asset_type === 'cloudflare') { + const c = f.cloudflare_detail; + p.cloudflare_detail = { account_email: c.account_email || null, sub_type: c.sub_type || 'zone', sub_name: c.sub_name || null, zone_name: c.zone_name || null, status: c.status || null }; } return p; } @@ -101,6 +105,7 @@ function openAssetEdit(a) { if (a.vps_detail) Object.assign(form.vps_detail, a.vps_detail); if (a.domain_detail) Object.assign(form.domain_detail, a.domain_detail); if (a.ai_detail) Object.assign(form.ai_detail, a.ai_detail); + if (a.cloudflare_detail) Object.assign(form.cloudflare_detail, a.cloudflare_detail); form.vps_detail.ssh_key = ''; form.vps_detail.password = ''; store.assetModal = { show: true, editing: a.id, form }; } @@ -830,6 +835,17 @@ const AssetModal = { +
+ Cloudflare 子资产详情 +
+ + + + + +
+
+