geo/backend/app/main.py

342 lines
12 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.

import logging
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, Response
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
# 必须在其他模块 import 之前初始化 JSON 日志
from app.logging_config import setup_logging
setup_logging()
# Sentry 初始化DSN 为空时自动禁用)
import sentry_sdk
from app.config import settings as _sentry_settings
if _sentry_settings.SENTRY_DSN:
sentry_sdk.init(
dsn=_sentry_settings.SENTRY_DSN,
traces_sample_rate=0.1,
environment=_sentry_settings.ENVIRONMENT,
)
from fastapi.middleware.cors import CORSMiddleware
from app.api.admin import router as admin_router
from app.api.content import router as content_router
from app.api.contents import router as contents_router
from app.api.clients import router as clients_router
from app.api.organization import router as organization_router
from app.api.agents import router as agents_router
from app.api.knowledge import router as knowledge_router
from app.api.distribution import router as distribution_router
from app.api.analytics import router as analytics_router
from app.api.lifecycle import router as lifecycle_router
from app.api.auth import router as auth_router
from app.api.citations import router as citations_router
from app.api.queries import router as queries_router
from app.api.reports import router as reports_router
from app.api.subscriptions import router as subscription_router
from app.api.alerts import router as alerts_router
from app.api.dashboard import router as dashboard_router
from app.api.brands import router as brands_router
from app.api.diagnosis import router as diagnosis_router
from app.api.onboarding import router as onboarding_router
from app.api.platforms import router as platforms_router
from app.api.platform_rules import router as platform_rules_router
from app.api.image import router as image_router
from app.api.knowledge_graph import router as knowledge_graph_router
from app.api.ai_engines import router as ai_engines_router
from app.api.detection import router as detection_router
from app.api.api_keys import router as api_keys_router
from app.api.usage import router as usage_router
from app.api.strategy import router as strategy_router
from app.api.competitor_analysis import router as competitor_analysis_router
from app.api.trends import router as trends_router
from app.api.schema_advisor import router as schema_advisor_router
from app.api.monitoring import router as monitoring_router
from app.api.health_score import router as health_score_router
from app.api.payments import router as payments_router
from app.api.attribution import router as attribution_router
from app.config import settings
from app.database import engine, Base
from app.schemas.common import ErrorResponse, ErrorCode
from app.middleware.rate_limit import RateLimitMiddleware
from app.middleware.logging_middleware import RequestLoggingMiddleware
from app.middleware.request_id import RequestIdMiddleware
from app.middleware.metrics import MetricsMiddleware, MonitoringMiddleware
from app.database import get_db
from app.workers.scheduler import query_scheduler
@asynccontextmanager
async def lifespan(app: FastAPI):
import app.models
import app.middleware.prometheus_metrics
from app.middleware.prometheus_metrics import SERVICE_INFO
import os
SERVICE_INFO.info({
"version": "1.0.0",
"environment": os.getenv("ENVIRONMENT", "development"),
})
async with engine.begin() as conn:
await conn.execute(text("SELECT 1"))
query_scheduler.start()
yield
# 关闭全局 Redis 连接池
from app.core.redis import close_redis
await close_redis()
await query_scheduler.shutdown()
app = FastAPI(
title="GEO Platform API",
version="1.0.0",
lifespan=lifespan,
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
"""统一 HTTP 异常响应格式。"""
code = ErrorCode.from_status(exc.status_code)
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
detail=str(exc.detail),
code=code,
).model_dump(mode="json"),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
"""统一参数校验异常响应格式。"""
return JSONResponse(
status_code=422,
content=ErrorResponse(
detail="请求参数校验失败",
code=ErrorCode.VALIDATION_ERROR,
extra={"errors": exc.errors()},
).model_dump(mode="json"),
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""兜底异常处理器,避免内部错误泄漏给客户端。"""
logging.getLogger(__name__).exception("Unhandled exception: %s", exc)
return JSONResponse(
status_code=500,
content=ErrorResponse(
detail="服务器内部错误,请稍后重试",
code=ErrorCode.INTERNAL_ERROR,
).model_dump(mode="json"),
)
_allow_origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",") if origin.strip()]
if not _allow_origins:
_allow_origins = ["http://localhost:3000"]
import os
_is_dev = os.getenv("ENVIRONMENT", "development") == "development"
app.add_middleware(
CORSMiddleware,
allow_origins=_allow_origins if not _is_dev else ["*"],
allow_credentials=not _is_dev,
allow_methods=["*"],
allow_headers=["*"],
)
# 安全响应头
@app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
# 中间件注册顺序FastAPI 后进先出,最后注册的最先执行)
# 执行链RequestId → Metrics → RateLimit → RequestLogging → CORS → SecurityHeaders
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(MetricsMiddleware)
app.add_middleware(MonitoringMiddleware)
app.add_middleware(RequestIdMiddleware)
app.include_router(auth_router, prefix="/api/v1/auth", tags=["认证"])
app.include_router(queries_router, prefix="/api/v1/queries", tags=["查询词"])
app.include_router(citations_router, prefix="/api/v1/citations", tags=["引用数据"])
app.include_router(reports_router, prefix="/api/v1/reports", tags=["报告"])
app.include_router(subscription_router)
app.include_router(admin_router)
app.include_router(agents_router, prefix="/api/v1/agents", tags=["Agent管理"])
app.include_router(lifecycle_router, prefix="/api/v1/lifecycle", tags=["lifecycle"])
app.include_router(knowledge_router, prefix="/api/v1/knowledge", tags=["知识库"])
app.include_router(content_router, prefix="/api/v1/content", tags=["内容生产"])
app.include_router(contents_router, prefix="/api/v1/contents", tags=["内容管理"])
app.include_router(organization_router)
app.include_router(clients_router, prefix="/api/v1/clients", tags=["客户管理"])
app.include_router(distribution_router, prefix="/api/v1/distribution", tags=["内容分发"])
app.include_router(analytics_router, prefix="/api/v1/analytics", tags=["监测优化"])
app.include_router(alerts_router, prefix="/api/v1/alerts", tags=["告警通知"])
app.include_router(dashboard_router, prefix="/api/v1/dashboard", tags=["仪表盘"])
app.include_router(brands_router, prefix="/api/v1/brands", tags=["品牌管理"])
app.include_router(diagnosis_router, prefix="/api/v1/diagnosis", tags=["诊断服务"])
app.include_router(onboarding_router, prefix="/api/v1")
app.include_router(platforms_router, prefix="/api/v1")
app.include_router(platform_rules_router)
app.include_router(image_router, prefix="/api/v1")
app.include_router(knowledge_graph_router, prefix="/api/v1")
app.include_router(ai_engines_router, prefix="/api/v1/ai-engines", tags=["AI引擎查询"])
app.include_router(detection_router, prefix="/api/v1/detection", tags=["定时检测任务"])
app.include_router(api_keys_router, prefix="/api/v1/api-keys", tags=["API Key管理"])
app.include_router(usage_router, prefix="/api/v1/usage", tags=["用量追踪"])
app.include_router(strategy_router, prefix="/api/v1/strategy", tags=["GEO方案"])
app.include_router(competitor_analysis_router, prefix="/api/v1/competitor", tags=["竞品分析"])
app.include_router(schema_advisor_router, prefix="/api/v1/schema", tags=["Schema建议"])
app.include_router(trends_router, prefix="/api/v1/trends", tags=["趋势洞察"])
app.include_router(monitoring_router, prefix="/api/v1/monitoring", tags=["效果追踪"])
app.include_router(health_score_router, prefix="/api/v1/public", tags=["公开API"])
app.include_router(payments_router)
app.include_router(attribution_router, prefix="/api/v1/attribution", tags=["效果归因"])
@app.get("/health", tags=["可观测性"])
async def health_check():
"""存活检查Liveness服务进程是否运行正常。不依赖外部服务。"""
return {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
}
@app.get("/ready", tags=["可观测性"])
async def readiness_check(db: AsyncSession = Depends(get_db)):
"""就绪检查Readiness依赖服务DB / Redis是否就绪。
供 Kubernetes readinessProbe / Docker healthcheck 使用。
不需要认证。
"""
# --- 检查数据库 ---
try:
await db.execute(text("SELECT 1"))
db_ok = True
except Exception:
db_ok = False
# --- 检查 Redis ---
redis_ok = False
try:
from app.core.redis import get_redis
redis_client = await get_redis()
await redis_client.ping()
redis_ok = True
except Exception:
pass
all_ok = db_ok and redis_ok
return JSONResponse(
status_code=200 if all_ok else 503,
content={
"status": "ready" if all_ok else "not_ready",
"checks": {
"database": "ok" if db_ok else "error",
"redis": "ok" if redis_ok else "error",
},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
)
@app.get("/metrics", tags=["可观测性"])
async def metrics():
"""Prometheus指标端点"""
return Response(
content=generate_latest(),
media_type=CONTENT_TYPE_LATEST
)
# ---- 详细健康检查端点 ----
from app.services.health_checker import HealthChecker
from app.services.app_state import app_state
@app.get("/health/detailed", tags=["可观测性"])
async def detailed_health(
db: AsyncSession = Depends(get_db),
):
"""
详细健康检查
返回所有依赖组件的健康状态:
- database: 数据库连接
- redis: Redis缓存
- llm_providers: LLM服务提供商
- storage: 文件存储
状态:
- healthy: 所有组件正常
- degraded: 部分组件异常,但仍可服务
- unhealthy: 核心组件异常
"""
checker = HealthChecker(db, settings.REDIS_URL)
health_result = await checker.check_all()
# 添加应用信息
health_result["app"] = app_state.get_info()
return health_result
@app.get("/health/liveness", tags=["可观测性"])
async def liveness():
"""
存活探针
用于Kubernetes livenessProbe
只要应用进程存活就返回200
"""
return {"status": "alive"}
@app.get("/health/readiness", tags=["可观测性"])
async def readiness(db: AsyncSession = Depends(get_db)):
"""
就绪探针
用于Kubernetes readinessProbe
检查核心依赖是否就绪
"""
checker = HealthChecker(db, settings.REDIS_URL)
# 只检查核心依赖数据库和Redis
db_result = await checker.check_database()
redis_result = await checker.check_redis()
all_ok = db_result.healthy and redis_result.healthy
return JSONResponse(
status_code=200 if all_ok else 503,
content={
"status": "ready" if all_ok else "not_ready",
"checks": {
"database": db_result.healthy,
"redis": redis_result.healthy,
},
},
)