feat: Cloudflare多账号子资产管理(zone/worker/r2/tunnel/mail子资产模型+模态框)
This commit is contained in:
+2
-2
@@ -29,7 +29,7 @@ engine = assets_engine
|
|||||||
|
|
||||||
def init_db() -> None:
|
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
|
from app.models.monitor import ( # noqa: F401
|
||||||
EventLog,
|
EventLog,
|
||||||
MetricPoint,
|
MetricPoint,
|
||||||
@@ -38,7 +38,7 @@ def init_db() -> None:
|
|||||||
)
|
)
|
||||||
from app.models.provider import Provider # noqa: F401
|
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]
|
metric_models = [MetricPoint, ServerInfo, SecurityCheck, EventLog]
|
||||||
|
|
||||||
SQLModel.metadata.create_all(
|
SQLModel.metadata.create_all(
|
||||||
|
|||||||
@@ -162,3 +162,31 @@ class AIAccount(SQLModel, table=True):
|
|||||||
last_synced_at: Optional[datetime] = Field(
|
last_synced_at: Optional[datetime] = Field(
|
||||||
default=None, description="最近一次自动同步余额/用量的时间"
|
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="子资产状态")
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ class AIAccountIn(SQLModel):
|
|||||||
monthly_limit: Optional[float] = None
|
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
|
vps_detail: Optional[VPSDetailIn] = None
|
||||||
domain_detail: Optional[DomainDetailIn] = None
|
domain_detail: Optional[DomainDetailIn] = None
|
||||||
ai_detail: Optional[AIAccountIn] = None
|
ai_detail: Optional[AIAccountIn] = None
|
||||||
|
cloudflare_detail: Optional[CloudflareDetailIn] = None
|
||||||
|
|
||||||
|
|
||||||
class AssetUpdate(SQLModel):
|
class AssetUpdate(SQLModel):
|
||||||
@@ -113,6 +124,7 @@ class AssetUpdate(SQLModel):
|
|||||||
vps_detail: Optional[VPSDetailIn] = None
|
vps_detail: Optional[VPSDetailIn] = None
|
||||||
domain_detail: Optional[DomainDetailIn] = None
|
domain_detail: Optional[DomainDetailIn] = None
|
||||||
ai_detail: Optional[AIAccountIn] = None
|
ai_detail: Optional[AIAccountIn] = None
|
||||||
|
cloudflare_detail: Optional[CloudflareDetailIn] = None
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -160,6 +172,11 @@ class AIAccountRead(SQLModel):
|
|||||||
has_api_key: bool = False
|
has_api_key: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflareDetailRead(CloudflareDetailIn):
|
||||||
|
id: int
|
||||||
|
asset_id: int
|
||||||
|
|
||||||
|
|
||||||
class AssetRead(AssetBase):
|
class AssetRead(AssetBase):
|
||||||
"""资产完整输出"""
|
"""资产完整输出"""
|
||||||
|
|
||||||
@@ -171,3 +188,4 @@ class AssetRead(AssetBase):
|
|||||||
vps_detail: Optional[VPSDetailRead] = None
|
vps_detail: Optional[VPSDetailRead] = None
|
||||||
domain_detail: Optional[DomainDetailRead] = None
|
domain_detail: Optional[DomainDetailRead] = None
|
||||||
ai_detail: Optional[AIAccountRead] = None
|
ai_detail: Optional[AIAccountRead] = None
|
||||||
|
cloudflare_detail: Optional[CloudflareDetailRead] = None
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from app.models.asset import (
|
|||||||
Asset,
|
Asset,
|
||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetType,
|
AssetType,
|
||||||
|
CloudflareDetail,
|
||||||
DomainDetail,
|
DomainDetail,
|
||||||
VPSDetail,
|
VPSDetail,
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,7 @@ from app.schemas.asset import (
|
|||||||
AssetCreate,
|
AssetCreate,
|
||||||
AssetRead,
|
AssetRead,
|
||||||
AssetUpdate,
|
AssetUpdate,
|
||||||
|
CloudflareDetailRead,
|
||||||
DomainDetailRead,
|
DomainDetailRead,
|
||||||
VPSDetailRead,
|
VPSDetailRead,
|
||||||
)
|
)
|
||||||
@@ -33,6 +35,7 @@ DETAIL_MAP = {
|
|||||||
AssetType.VPS: ("vps_detail", VPSDetail),
|
AssetType.VPS: ("vps_detail", VPSDetail),
|
||||||
AssetType.DOMAIN: ("domain_detail", DomainDetail),
|
AssetType.DOMAIN: ("domain_detail", DomainDetail),
|
||||||
AssetType.AI_AGENT: ("ai_detail", AIAccount),
|
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 = AIAccountRead.model_validate(detail)
|
||||||
read.has_api_key = bool(detail.api_key_encrypted or detail.api_key)
|
read.has_api_key = bool(detail.api_key_encrypted or detail.api_key)
|
||||||
return read
|
return read
|
||||||
|
if isinstance(detail, CloudflareDetail):
|
||||||
|
return CloudflareDetailRead.model_validate(detail)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +99,8 @@ def _to_read(session: Session, asset: Asset, detail) -> AssetRead:
|
|||||||
read.domain_detail = _detail_to_read(detail)
|
read.domain_detail = _detail_to_read(detail)
|
||||||
elif isinstance(detail, AIAccount):
|
elif isinstance(detail, AIAccount):
|
||||||
read.ai_detail = _detail_to_read(detail)
|
read.ai_detail = _detail_to_read(detail)
|
||||||
|
elif isinstance(detail, CloudflareDetail):
|
||||||
|
read.cloudflare_detail = _detail_to_read(detail)
|
||||||
return read
|
return read
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +158,7 @@ def create_asset(session: Session, data: AssetCreate) -> AssetRead:
|
|||||||
)
|
)
|
||||||
|
|
||||||
asset_data = data.model_dump(
|
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)
|
asset = Asset(**asset_data)
|
||||||
session.add(asset)
|
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="资产不存在")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="资产不存在")
|
||||||
|
|
||||||
main_fields = data.model_dump(
|
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():
|
for key, value in main_fields.items():
|
||||||
setattr(asset, key, value)
|
setattr(asset, key, value)
|
||||||
|
|||||||
@@ -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: '' },
|
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 },
|
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 },
|
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') {
|
} else if (f.asset_type === 'ai_agent') {
|
||||||
const a = f.ai_detail;
|
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 };
|
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;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -101,6 +105,7 @@ function openAssetEdit(a) {
|
|||||||
if (a.vps_detail) Object.assign(form.vps_detail, a.vps_detail);
|
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.domain_detail) Object.assign(form.domain_detail, a.domain_detail);
|
||||||
if (a.ai_detail) Object.assign(form.ai_detail, a.ai_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 = '';
|
form.vps_detail.ssh_key = ''; form.vps_detail.password = '';
|
||||||
store.assetModal = { show: true, editing: a.id, form };
|
store.assetModal = { show: true, editing: a.id, form };
|
||||||
}
|
}
|
||||||
@@ -830,6 +835,17 @@ const AssetModal = {
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset v-if="f.asset_type==='cloudflare'" class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
||||||
|
<legend class="text-xs font-medium text-slate-600 dark:text-slate-300 px-1">Cloudflare 子资产详情</legend>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<label class="block"><span class="text-xs text-slate-400">账号邮箱</span><input v-model="f.cloudflare_detail.account_email" placeholder="账号邮箱(区分多账号)" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||||
|
<label class="block"><span class="text-xs text-slate-400">子资产类型</span><select v-model="f.cloudflare_detail.sub_type" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"><option value="zone">zone(解析)</option><option value="worker">Worker</option><option value="r2">R2</option><option value="tunnel">Tunnel</option><option value="mail">Mail</option><option value="dns_record">DNS记录</option><option value="other">其他</option></select></label>
|
||||||
|
<label class="block"><span class="text-xs text-slate-400">子资产名称</span><input v-model="f.cloudflare_detail.sub_name" placeholder="如 worker 名 / bucket 名" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||||
|
<label class="block"><span class="text-xs text-slate-400">关联 zone/域名</span><input v-model="f.cloudflare_detail.zone_name" placeholder="example.com" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||||
|
<label class="col-span-2 block"><span class="text-xs text-slate-400">状态</span><input v-model="f.cloudflare_detail.status" placeholder="active / paused 等" class="mt-1 w-full px-2 py-1.5 rounded border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<label class="block"><span class="text-xs text-slate-500">备注</span>
|
<label class="block"><span class="text-xs text-slate-500">备注</span>
|
||||||
<textarea v-model="f.remark" rows="2" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></textarea></label>
|
<textarea v-model="f.remark" rows="2" class="mt-1 w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm"></textarea></label>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user