""" 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