Files
life-echo/api/app/core/openapi.py
Sully 53e0065e3e refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
2026-05-22 13:44:50 +08:00

132 lines
5.0 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.
"""
OpenAPI 全局增强:元数据 + 统一 ErrorResponse 组件 + 领域错误码表。
"""
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from app.core.error_codes import ALL_ERROR_CODES, ERROR_CODE_ENUM
ERROR_RESPONSE_REF = "#/components/schemas/ErrorResponse"
ERROR_CODE_REF = "#/components/schemas/ErrorCode"
COMMON_ERROR_RESPONSES: dict[int, dict] = {
400: {"description": "请求参数错误", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
401: {"description": "认证失败", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
403: {"description": "权限不足", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
404: {"description": "资源不存在", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
409: {"description": "资源冲突", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
422: {"description": "请求体验证失败", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
429: {
"description": "配额已用尽QUOTA_EXCEEDED或请求频率超限RATE_LIMITED",
"content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}},
},
500: {"description": "内部服务器错误", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
502: {"description": "外部服务异常", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
503: {"description": "服务不可用", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
504: {"description": "网关超时", "content": {"application/json": {"schema": {"$ref": ERROR_RESPONSE_REF}}}},
}
def error_responses(
*status_codes: int,
descriptions: dict[int, str] | None = None,
) -> dict[int, dict]:
"""Pick reusable OpenAPI error response entries by HTTP status code."""
out: dict[int, dict] = {}
for code in status_codes:
entry = dict(COMMON_ERROR_RESPONSES[code])
if descriptions and code in descriptions:
entry["description"] = descriptions[code]
out[code] = entry
return out
def _error_code_catalog_markdown() -> str:
lines = [
"",
"### 错误响应格式",
"",
"所有 HTTP 错误返回 `application/json`",
"",
"```json",
'{ "error_code": "NOT_FOUND", "message": "资源不存在", "request_id": "req_xxx" }',
"```",
"",
"组件 `ErrorCode` / `DomainErrorCode` 列出机器可读码;`message` 为面向用户的说明。",
"",
"| error_code | HTTP | 域 | 说明 |",
"|------------|------|-----|------|",
]
for entry in ALL_ERROR_CODES:
status = entry["http_status"]
status_cell = str(status) if status else ""
lines.append(
f"| `{entry['code']}` | {status_cell} | {entry['domain']} | {entry['description']} |"
)
return "\n".join(lines)
def custom_openapi(app: FastAPI) -> dict:
if app.openapi_schema:
return app.openapi_schema
base_description = (
"为老年用户提供 AI 驱动的回忆录创作服务:\n"
"语音对话采集 → 素材沉淀 → 章节生成 → PDF 导出。"
)
openapi_schema = get_openapi(
title="Life Echo API",
version="1.0.0",
summary="岁月留书 — 口述回忆录生产平台",
description=base_description + _error_code_catalog_markdown(),
routes=app.routes,
)
components = openapi_schema.setdefault("components", {})
schemas = components.setdefault("schemas", {})
schemas["ErrorCode"] = {
"title": "ErrorCode",
"type": "string",
"enum": ERROR_CODE_ENUM,
"description": "机器可读错误码(全局 + 领域);完整说明见 API 描述中的错误码表。",
}
domain_codes = sorted(
{e["code"] for e in ALL_ERROR_CODES if e["domain"] != "core"}
)
schemas["DomainErrorCode"] = {
"title": "DomainErrorCode",
"type": "string",
"enum": domain_codes,
"description": "业务领域错误码auth / payment 等),与通用 ErrorCode 并存。",
}
schemas["ErrorResponse"] = {
"title": "ErrorResponse",
"type": "object",
"required": ["error_code", "message", "request_id"],
"properties": {
"error_code": {
"allOf": [{"$ref": ERROR_CODE_REF}],
"description": "机器可读错误码",
"example": "NOT_FOUND",
},
"message": {
"type": "string",
"description": "面向用户的错误说明",
"example": "资源不存在",
},
"request_id": {
"type": "string",
"description": "请求追踪 ID",
"example": "req_abc123",
},
},
}
app.openapi_schema = openapi_schema
return app.openapi_schema