response.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. """统一响应类与业务错误码定义。
  2. 所有接口响应统一使用 `{code, data, msg}` 信封格式:
  3. - code: 0 表示成功;其他值代表不同的失败类型
  4. - data: 业务数据(成功时)或 None(失败时)
  5. - msg: 描述信息
  6. 错误码使用 `(code, default_msg)` 的元组定义,code 和默认提示一一对应。
  7. """
  8. from __future__ import annotations
  9. from enum import Enum
  10. from typing import Any, Generic, TypeVar
  11. from fastapi.responses import JSONResponse
  12. from pydantic import BaseModel, Field
  13. T = TypeVar("T")
  14. class ResponseCode(Enum):
  15. """业务错误码枚举:每个成员都是 (code, default_msg) 元组。
  16. 分段规则:
  17. 0 成功
  18. 1000-1999 通用错误(参数、请求格式等)
  19. 2000-2999 业务逻辑错误(缓存未就绪、资源不存在等)
  20. 3000-3999 外部依赖错误(Zendesk、上游服务等)
  21. 9000-9999 服务器内部错误
  22. 用法:
  23. ResponseCode.SUCCESS.code # -> 0
  24. ResponseCode.SUCCESS.msg # -> "成功"
  25. """
  26. # 成功
  27. SUCCESS = (0, "成功")
  28. # 通用错误 1xxx
  29. PARAM_INVALID = (1001, "参数验证失败")
  30. REQUEST_INVALID = (1002, "请求格式不正确")
  31. METHOD_NOT_ALLOWED = (1003, "请求方法不允许")
  32. NOT_FOUND = (1004, "资源不存在")
  33. # 业务错误 2xxx
  34. CACHE_NOT_READY = (2001, "FAQ 缓存未就绪,请稍后重试")
  35. RESOURCE_NOT_FOUND = (2002, "业务资源不存在")
  36. # 外部依赖错误 3xxx
  37. UPSTREAM_ERROR = (3001, "上游服务调用失败")
  38. UPSTREAM_TIMEOUT = (3002, "上游服务响应超时")
  39. # 服务器错误 9xxx
  40. INTERNAL_ERROR = (9999, "服务器内部错误,请稍后重试")
  41. def __init__(self, code: int, msg: str) -> None:
  42. self.code = code
  43. self.msg = msg
  44. def __repr__(self) -> str: # pragma: no cover
  45. return f"<{self.__class__.__name__}.{self.name}: code={self.code} msg={self.msg!r}>"
  46. class ApiResponse(BaseModel, Generic[T]):
  47. """统一 API 响应模型。
  48. 用于在路由 `response_model` 中声明,让 OpenAPI 文档展示标准结构。
  49. 用法示例:
  50. @router.get("/search", response_model=ApiResponse[SearchData])
  51. async def search(...) -> ApiResponse[SearchData]:
  52. return ApiResponse.ok(data=SearchData(...))
  53. # 失败响应(自动使用错误码对应的默认 msg)
  54. return ApiResponse.fail(ResponseCode.PARAM_INVALID)
  55. # 失败响应(覆盖默认 msg)
  56. return ApiResponse.fail(ResponseCode.UPSTREAM_ERROR, msg="Zendesk 限流")
  57. """
  58. code: int = Field(default=0, description="业务码,0=成功,非0=失败")
  59. data: T | None = Field(default=None, description="业务数据,失败时为 null")
  60. msg: str = Field(default="成功", description="描述信息")
  61. @classmethod
  62. def ok(
  63. cls,
  64. data: T | None = None,
  65. msg: str | None = None,
  66. ) -> "ApiResponse[T]":
  67. """构造成功响应。msg 为空时使用 SUCCESS 默认提示。"""
  68. return cls(
  69. code=ResponseCode.SUCCESS.code,
  70. data=data,
  71. msg=msg if msg is not None else ResponseCode.SUCCESS.msg,
  72. )
  73. @classmethod
  74. def fail(
  75. cls,
  76. code: ResponseCode,
  77. msg: str | None = None,
  78. data: T | None = None,
  79. ) -> "ApiResponse[T]":
  80. """构造失败响应。msg 为空时使用错误码自带的默认提示。"""
  81. return cls(
  82. code=code.code,
  83. data=data,
  84. msg=msg if msg is not None else code.msg,
  85. )
  86. def success_response(
  87. data: Any = None,
  88. msg: str | None = None,
  89. http_status: int = 200,
  90. ) -> JSONResponse:
  91. """构造成功的 JSONResponse(适用于异常处理器、中间件等场景)。"""
  92. return JSONResponse(
  93. status_code=http_status,
  94. content={
  95. "code": ResponseCode.SUCCESS.code,
  96. "data": data,
  97. "msg": msg if msg is not None else ResponseCode.SUCCESS.msg,
  98. },
  99. )
  100. def error_response(
  101. code: ResponseCode,
  102. msg: str | None = None,
  103. data: Any = None,
  104. http_status: int = 200,
  105. headers: dict[str, str] | None = None,
  106. ) -> JSONResponse:
  107. """构造失败的 JSONResponse(适用于异常处理器、中间件等场景)。
  108. Args:
  109. code: 业务错误码枚举
  110. msg: 错误提示,为空时使用错误码自带的默认提示
  111. data: 附加数据(如参数验证错误的详细字段列表)
  112. http_status: HTTP 状态码,默认 200(业务错误也走 200,由 code 区分成败)
  113. headers: 额外响应头(如 401 必须带 WWW-Authenticate 才能让浏览器弹登录框)
  114. 返回示例:
  115. {"code": 1001, "data": [...], "msg": "参数验证失败"}
  116. """
  117. return JSONResponse(
  118. status_code=http_status,
  119. content={
  120. "code": code.code,
  121. "data": data,
  122. "msg": msg if msg is not None else code.msg,
  123. },
  124. headers=headers,
  125. )
  126. class BusinessError(Exception):
  127. """业务异常基类,用于在业务层抛出后由全局处理器统一转为 ApiResponse。
  128. 用法:
  129. raise BusinessError(ResponseCode.CACHE_NOT_READY)
  130. raise BusinessError(ResponseCode.UPSTREAM_ERROR, msg="Zendesk 限流")
  131. """
  132. def __init__(
  133. self,
  134. code: ResponseCode,
  135. msg: str | None = None,
  136. data: Any = None,
  137. http_status: int = 200,
  138. ) -> None:
  139. self.response_code = code
  140. self.code = code.code
  141. self.msg = msg if msg is not None else code.msg
  142. self.data = data
  143. self.http_status = http_status
  144. super().__init__(self.msg)