prepare backend-only fastapi deployment

This commit is contained in:
刘金宝
2026-06-01 17:32:18 +08:00
parent 338e2c8e1d
commit 132155c280
59 changed files with 374 additions and 9155 deletions
+4 -5
View File
@@ -82,12 +82,11 @@ class Settings(BaseModel):
report_storage_dir: str = Field(default_factory=lambda: os.getenv("REPORT_STORAGE_DIR", "./storage/reports"))
runtime_memory_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("RUNTIME_MEMORY_TTL_SECONDS", "7200")))
runtime_memory_backend: str = Field(default_factory=lambda: os.getenv("RUNTIME_MEMORY_BACKEND", "memory"))
redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0"))
auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "false").lower() == "true")
redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://redis:6379/0"))
auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "true").lower() == "true")
auth_user_me_url: str = Field(default_factory=lambda: os.getenv("AUTH_USER_ME_URL", ""))
auth_timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_TIMEOUT_SECONDS", "5")))
auth_cache_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_CACHE_TTL_SECONDS", "300")))
auth_allow_demo_user_id: bool = Field(default_factory=lambda: os.getenv("AUTH_ALLOW_DEMO_USER_ID", "true").lower() == "true")
def as_public_dict(self) -> dict[str, Any]:
"""配置展示:返回允许暴露给 Demo 前端的功能开关。"""
@@ -107,8 +106,8 @@ class Settings(BaseModel):
"llm_reasoning_effort": self.llm_reasoning_effort,
"llm_fast_max_tokens": self.llm_fast_max_tokens,
"runtime_memory_backend": self.runtime_memory_backend,
"auth_validate_enabled": self.auth_validate_enabled,
"auth_source": "django_user_center" if self.auth_validate_enabled else "demo_header",
"auth_validate_enabled": True,
"auth_source": "django_user_center",
}
+2 -1
View File
@@ -15,4 +15,5 @@ class UserContext:
user_agent: str | None = None
username: str | None = None
display_name: str | None = None
auth_source: str = "demo_header"
auth_source: str = "django_user_center"
profile: dict | None = None
+8 -8
View File
@@ -16,10 +16,10 @@ def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(AppError)
async def handle_app_error(request: Request, exc: AppError) -> JSONResponse:
logger.warning(
"business_error code=%s path=%s user_id=%s",
"business_error code=%s path=%s request_id=%s",
exc.code,
request.url.path,
request.headers.get("X-User-Id"),
request.headers.get("X-Request-Id"),
)
return JSONResponse(
status_code=exc.status_code,
@@ -29,9 +29,9 @@ def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(RequestValidationError)
async def handle_validation_error(request: Request, exc: RequestValidationError) -> JSONResponse:
logger.warning(
"validation_error path=%s user_id=%s errors=%s",
"validation_error path=%s request_id=%s errors=%s",
request.url.path,
request.headers.get("X-User-Id"),
request.headers.get("X-Request-Id"),
exc.errors(),
)
return JSONResponse(
@@ -42,9 +42,9 @@ def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(SQLAlchemyError)
async def handle_database_error(request: Request, exc: SQLAlchemyError) -> JSONResponse:
logger.exception(
"database_error path=%s user_id=%s",
"database_error path=%s request_id=%s",
request.url.path,
request.headers.get("X-User-Id"),
request.headers.get("X-Request-Id"),
)
return JSONResponse(
status_code=500,
@@ -54,9 +54,9 @@ def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(Exception)
async def handle_unexpected_error(request: Request, exc: Exception) -> JSONResponse:
logger.exception(
"unexpected_error path=%s user_id=%s",
"unexpected_error path=%s request_id=%s",
request.url.path,
request.headers.get("X-User-Id"),
request.headers.get("X-Request-Id"),
)
return JSONResponse(
status_code=500,
+16 -33
View File
@@ -1,51 +1,34 @@
from fastapi import Header, Request
from fastapi import Header, Request, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.core.config import settings
from app.core.context import UserContext
from app.core.exceptions import AppError
from app.services.external_auth_service import ExternalAuthService
bearer_scheme = HTTPBearer(auto_error=False, description="Django 用户中心 access token")
async def get_user_context(
request: Request,
x_user_id: str | None = Header(default=None, alias="X-User-Id"),
x_tenant_id: str | None = Header(default=None, alias="X-Tenant-Id"),
x_user_role: str | None = Header(default=None, alias="X-User-Role"),
x_class_id: str | None = Header(default=None, alias="X-Class-Id"),
credentials: HTTPAuthorizationCredentials | None = Security(bearer_scheme),
x_entry_scene: str | None = Header(default=None, alias="X-Entry-Scene"),
x_request_id: str | None = Header(default=None, alias="X-Request-Id"),
) -> UserContext:
"""用户校验:正式联调优先调用 Django 用户中心,Demo 模式兼容 X-User-Id"""
if settings.auth_validate_enabled and (request.headers.get("Authorization") or request.headers.get("Cookie")):
user = await ExternalAuthService().authenticate(request)
return UserContext(
user_id=user.user_id,
tenant_id=user.tenant_id or x_tenant_id,
role=user.role or x_user_role,
class_id=x_class_id,
entry_scene=x_entry_scene,
request_id=x_request_id,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("User-Agent"),
username=user.username,
display_name=user.display_name,
auth_source=user.source,
)
if settings.auth_validate_enabled and not settings.auth_allow_demo_user_id:
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization or Cookie is required", 401)
if not x_user_id or not x_user_id.strip():
raise AppError("USER_ID_REQUIRED", "X-User-Id header is required", 401)
"""用户校验:只接受宿主系统 access token,并转发 Django 用户中心 `/me` 获取真实用户"""
if not credentials or not credentials.credentials.strip():
raise AppError("AUTH_CREDENTIAL_REQUIRED", "Authorization header is required", 401)
user = await ExternalAuthService().authenticate(request)
return UserContext(
user_id=x_user_id.strip(),
tenant_id=x_tenant_id,
role=x_user_role,
class_id=x_class_id,
user_id=user.user_id,
tenant_id=user.tenant_id,
role=user.role,
entry_scene=x_entry_scene,
request_id=x_request_id,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("User-Agent"),
auth_source="demo_header",
username=user.username,
display_name=user.display_name,
auth_source=user.source,
profile=user.profile,
)