2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
2026-05-22 13:44:50 +08:00
|
|
|
|
OpenAPI 全局增强:元数据 + 统一 ErrorResponse 组件 + 领域错误码表。
|
2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
|
from fastapi.openapi.utils import get_openapi
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
def custom_openapi(app: FastAPI) -> dict:
|
|
|
|
|
|
if app.openapi_schema:
|
|
|
|
|
|
return app.openapi_schema
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
base_description = (
|
|
|
|
|
|
"为老年用户提供 AI 驱动的回忆录创作服务:\n"
|
|
|
|
|
|
"语音对话采集 → 素材沉淀 → 章节生成 → PDF 导出。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
openapi_schema = get_openapi(
|
|
|
|
|
|
title="Life Echo API",
|
|
|
|
|
|
version="1.0.0",
|
2026-05-22 13:44:50 +08:00
|
|
|
|
summary="岁月留书 — 口述回忆录生产平台",
|
|
|
|
|
|
description=base_description + _error_code_catalog_markdown(),
|
2026-03-18 17:18:23 +08:00
|
|
|
|
routes=app.routes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-22 13:44:50 +08:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
app.openapi_schema = openapi_schema
|
|
|
|
|
|
return app.openapi_schema
|