ForcePilot/backend/server/routers/external_systems/environment_router.py
Kris 67ddd748e8 refactor(external_systems_router): 优化多路由代码并新增功能
1. 为告警、删除系统/环境/工具接口添加操作者字段
2. 重构看板路由使用专用服务创建函数
3. 新增工具健康检查接口并完善执行重试注释
2026-07-11 05:40:58 +08:00

508 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Environment 子域 Router。
外部系统限界上下文的环境管理 API覆盖环境 CRUD / 默认 / 启停 / 克隆 /
连通性测试 / 差异 / 导出 / 健康状态 / 统计 / 跨系统查询 /
批量克隆 / 批量启停。所有端点通过 ``create_use_cases_from_db`` 装配 use_cases
经 ``environment_service`` 端口调用用例。
路径顺序约束:静态路径(``/diff`` / ``/active`` / ``/by-key`` / ``/stats`` /
``/cross-system`` / ``/batch-test-connectivity`` / ``/batch-clone`` /
``/batch-enabled``)必须在 ``/{env_id}`` 前声明,避免被路径参数捕获。
认证策略:查询类端点使用 ``get_required_user``,写操作端点使用 ``get_admin_user``。
``export_environment`` 因 ``include_secrets=true`` 会泄露明文密钥,统一要求 ``get_admin_user``。
"""
from __future__ import annotations
import json
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
from yuxi.external_systems.use_cases.dto.environment import (
BatchCloneEnvironmentsInput,
BatchEnableEnvironmentsInput,
BulkTestEnvironmentsInput,
CloneEnvironmentInput,
CreateEnvironmentInput,
CrossSystemEnvironmentsInput,
DeleteEnvironmentInput,
DiffEnvironmentsInput,
DisableEnvironmentInput,
EnableEnvironmentInput,
ExportEnvironmentInput,
GetActiveEnvironmentInput,
GetEnvironmentByKeyInput,
GetEnvironmentHealthInput,
GetEnvironmentInput,
GetEnvironmentStatsInput,
ListEnvironmentsInput,
SetDefaultEnvironmentInput,
TestEnvironmentConnectionInput,
UpdateEnvironmentInput,
)
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
environment_router = APIRouter(
prefix="/environments",
tags=["external-systems-environment"],
)
# ---------------- Request Schemas ----------------
class CreateEnvironmentRequest(BaseModel):
"""创建环境请求体。字段对齐 ``CreateEnvironmentInput``。
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,在边界层拦截非法输入:
``env_key`` 对齐 String(32) 并沿用 slug 标识符格式,``name`` 对齐 String(128)。
"""
model_config = ConfigDict(frozen=True)
system_id: int
env_key: str = Field(..., min_length=1, max_length=32, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$")
name: str = Field(..., min_length=1, max_length=128)
connection_config: dict[str, Any] = Field(default_factory=dict)
auth_config: dict[str, Any] | None = None
is_default: bool = False
enabled: bool = True
class UpdateEnvironmentRequest(BaseModel):
"""更新环境请求体。``id`` 与 ``system_id`` 由路径/Query 提供,不在 body 中。
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,仅透传客户端显式设置的字段。
不含 ``is_default``:默认环境切换必须通过 ``PATCH /{env_id}/default`` 端点,
该端点会清除同系统其他环境的默认标记,避免违反部分唯一约束。
"""
model_config = ConfigDict(frozen=True)
name: str | None = Field(default=None, min_length=1, max_length=128)
connection_config: dict[str, Any] | None = None
auth_config: dict[str, Any] | None = None
enabled: bool | None = None
class SetEnvironmentEnabledRequest(BaseModel):
"""启停环境请求体。按 ``enabled`` 值分派 enable/disable。"""
model_config = ConfigDict(frozen=True)
enabled: bool
class CloneEnvironmentRequest(BaseModel):
"""克隆环境请求体。``id`` 与 ``system_id`` 由路径/Query 提供。
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,``new_env_key`` 与
``CreateEnvironmentRequest.env_key`` 保持一致格式。
"""
model_config = ConfigDict(frozen=True)
new_env_key: str = Field(..., min_length=1, max_length=32, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$")
new_name: str | None = Field(default=None, min_length=1, max_length=128)
class BatchTestConnectivityRequest(BaseModel):
"""批量测试连通性请求体。字段对齐 ``BulkTestEnvironmentsInput``。"""
model_config = ConfigDict(frozen=True)
system_id: int
env_keys: list[str] | None = None
class BatchCloneEnvironmentsRequest(BaseModel):
"""批量克隆环境请求体。字段对齐 ``BatchCloneEnvironmentsInput``(不含 ``created_by``)。
``created_by`` 由 ``current_user.uid`` 填充,不在请求体中暴露。
"""
model_config = ConfigDict(frozen=True)
system_id: int
source_env_ids: list[int]
new_env_key_suffix: str = Field(..., min_length=1, max_length=32)
new_name_suffix: str | None = Field(default=None, min_length=1, max_length=128)
class BatchEnableEnvironmentsRequest(BaseModel):
"""批量启停环境请求体。字段对齐 ``BatchEnableEnvironmentsInput``(不含 ``updated_by``)。
``updated_by`` 由 ``current_user.uid`` 填充,不在请求体中暴露。
"""
model_config = ConfigDict(frozen=True)
system_id: int
env_ids: list[int]
enabled: bool
# ---------------- Endpoints ----------------
@environment_router.get("/diff", response_model=dict)
async def diff_environments(
system_id: int = Query(...),
source_env_key: str = Query(..., max_length=32),
target_env_key: str = Query(..., max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""对比两个环境的配置差异。"""
use_cases = create_use_cases_from_db(db)
input_dto = DiffEnvironmentsInput(
system_id=system_id,
source_env_key=source_env_key,
target_env_key=target_env_key,
)
output = await use_cases.environment_service.diff_environments(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.get("", response_model=dict)
async def list_environments(
system_id: int = Query(...),
env_key: str | None = Query(None, max_length=32),
enabled: bool | None = Query(None),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""分页列出指定外部系统下的环境。"""
use_cases = create_use_cases_from_db(db)
input_dto = ListEnvironmentsInput(
system_id=system_id,
env_key=env_key,
enabled=enabled,
page=offset // limit + 1,
page_size=limit,
)
output = await use_cases.environment_service.list_environments(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("", response_model=dict)
async def create_environment(
body: CreateEnvironmentRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""创建环境。``created_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = CreateEnvironmentInput(**body.model_dump(), created_by=current_user.uid)
output = await use_cases.environment_service.create_environment(input_dto)
return {"success": True, "data": output.model_dump()}
# ========== 静态路径端点(必须在 /{env_id} 之前声明) ==========
@environment_router.get("/active", response_model=dict)
async def get_active_environment(
system_id: int = Query(...),
env_key: str | None = Query(None, max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取当前生效的环境配置。
指定 ``env_key`` 返回对应环境,否则返回默认环境,都没有返回 ``data=null``。
"""
use_cases = create_use_cases_from_db(db)
input_dto = GetActiveEnvironmentInput(system_id=system_id, env_key=env_key)
output = await use_cases.environment_service.get_active_environment(input_dto)
return {"success": True, "data": output.model_dump() if output else None}
@environment_router.get("/by-key", response_model=dict)
async def get_environment_by_key(
system_id: int = Query(...),
env_key: str = Query(..., max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""按 ``system_id`` + ``env_key`` 获取环境详情,不存在时抛 404。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetEnvironmentByKeyInput(system_id=system_id, env_key=env_key)
output = await use_cases.environment_service.get_environment_by_key(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.get("/stats", response_model=dict)
async def get_environment_stats(
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""统计指定系统下的环境数量(按启用状态、默认标记分组)。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetEnvironmentStatsInput(system_id=system_id)
output = await use_cases.environment_service.get_environment_stats(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.get("/cross-system", response_model=dict)
async def list_cross_system_environments(
env_key: str = Query(..., max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""跨系统查询同 ``env_key`` 的环境列表。"""
use_cases = create_use_cases_from_db(db)
input_dto = CrossSystemEnvironmentsInput(env_key=env_key)
output = await use_cases.environment_service.list_cross_system_environments(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("/batch-test-connectivity", response_model=dict)
async def batch_test_environment_connectivity(
body: BatchTestConnectivityRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量测试环境连通性(并发上限 5"""
use_cases = create_use_cases_from_db(db)
input_dto = BulkTestEnvironmentsInput(
system_id=body.system_id,
env_keys=body.env_keys,
)
output = await use_cases.environment_service.bulk_test_environments(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("/batch-clone", response_model=dict)
async def batch_clone_environments(
body: BatchCloneEnvironmentsRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量克隆环境配置。``created_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = BatchCloneEnvironmentsInput(
system_id=body.system_id,
source_env_ids=body.source_env_ids,
new_env_key_suffix=body.new_env_key_suffix,
new_name_suffix=body.new_name_suffix,
created_by=current_user.uid,
)
output = await use_cases.environment_service.batch_clone_environments(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("/batch-enabled", response_model=dict)
async def batch_set_environments_enabled(
body: BatchEnableEnvironmentsRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量启用或禁用多个环境。``updated_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = BatchEnableEnvironmentsInput(
system_id=body.system_id,
env_ids=body.env_ids,
enabled=body.enabled,
updated_by=current_user.uid,
)
output = await use_cases.environment_service.batch_set_environments_enabled(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.get("/{env_id}", response_model=dict)
async def get_environment(
env_id: int,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取环境详情。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetEnvironmentInput(id=env_id, system_id=system_id)
output = await use_cases.environment_service.get_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.put("/{env_id}", response_model=dict)
async def update_environment(
env_id: int,
body: UpdateEnvironmentRequest,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新环境。仅透传客户端显式设置的字段。``updated_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = UpdateEnvironmentInput(
id=env_id,
system_id=system_id,
updated_by=current_user.uid,
**body.model_dump(exclude_unset=True),
)
output = await use_cases.environment_service.update_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.delete("/{env_id}", response_model=dict)
async def delete_environment(
env_id: int,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""删除环境。"""
use_cases = create_use_cases_from_db(db)
input_dto = DeleteEnvironmentInput(id=env_id, system_id=system_id, deleted_by=current_user.uid)
output = await use_cases.environment_service.delete_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.patch("/{env_id}/default", response_model=dict)
async def set_default_environment(
env_id: int,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""设置默认环境。``updated_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = SetDefaultEnvironmentInput(
id=env_id,
system_id=system_id,
updated_by=current_user.uid,
)
output = await use_cases.environment_service.set_default_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.patch("/{env_id}/enabled", response_model=dict)
async def set_environment_enabled(
env_id: int,
body: SetEnvironmentEnabledRequest,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启用/禁用环境。按 ``enabled`` 值分派 ``enable_environment``/``disable_environment``。
``updated_by`` 由 ``current_user.uid`` 填充。
"""
use_cases = create_use_cases_from_db(db)
if body.enabled:
input_dto = EnableEnvironmentInput(
id=env_id,
system_id=system_id,
updated_by=current_user.uid,
)
output = await use_cases.environment_service.enable_environment(input_dto)
else:
input_dto = DisableEnvironmentInput(
id=env_id,
system_id=system_id,
updated_by=current_user.uid,
)
output = await use_cases.environment_service.disable_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("/{env_id}/clone", response_model=dict)
async def clone_environment(
env_id: int,
body: CloneEnvironmentRequest,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""克隆环境。``created_by`` 由 ``current_user.uid`` 填充。"""
use_cases = create_use_cases_from_db(db)
input_dto = CloneEnvironmentInput(
id=env_id,
system_id=system_id,
new_env_key=body.new_env_key,
new_name=body.new_name,
created_by=current_user.uid,
)
output = await use_cases.environment_service.clone_environment(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.post("/{env_id}/test-connectivity", response_model=dict)
async def test_environment_connection(
env_id: int,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""测试环境连通性。"""
use_cases = create_use_cases_from_db(db)
input_dto = TestEnvironmentConnectionInput(id=env_id, system_id=system_id)
output = await use_cases.environment_service.test_environment_connection(input_dto)
return {"success": True, "data": output.model_dump()}
@environment_router.get("/{env_id}/export")
async def export_environment(
env_id: int,
system_id: int = Query(...),
include_secrets: bool = Query(False),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> StreamingResponse:
"""导出环境配置为 JSON 文件下载流。
``include_secrets=false``(默认)返回脱敏配置;``include_secrets=true``
返回已解密的明文配置。由于 ``include_secrets=true`` 会泄露明文密钥,
本端点统一要求管理员权限,对齐写操作的权限梯度。
返回 ``StreamingResponse````application/octet-stream``
对齐 ``system_router.export_systems`` 模式。
"""
use_cases = create_use_cases_from_db(db)
input_dto = ExportEnvironmentInput(
id=env_id,
system_id=system_id,
include_secrets=include_secrets,
)
output = await use_cases.environment_service.export_environment(input_dto)
payload = json.dumps(
output.model_dump(),
ensure_ascii=False,
default=str,
).encode("utf-8")
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
filename = f"environment_{env_id}_{output.env_key}_{timestamp}.json"
async def _stream():
yield payload
return StreamingResponse(
_stream(),
media_type="application/octet-stream",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
@environment_router.get("/{env_id}/health", response_model=dict)
async def get_environment_health(
env_id: int,
system_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询环境最近健康状态(基于审计日志推导,不执行实时探测)。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetEnvironmentHealthInput(id=env_id, system_id=system_id)
output = await use_cases.environment_service.get_environment_health(input_dto)
return {"success": True, "data": output.model_dump()}