refactor(backend): 错误码收敛为单一事实来源,统一异常写法

错误码(t16)
- HTTP 语义只由 core/exceptions.py 的 RET.code → fastapi.status 映射决定;
  映射表显式登记 -1→400 与全部「数值即 HTTP 状态」的 RET 成员,_DEFAULT_BUSINESS_STATUS
  降为「新增码忘登记」的安全网
- 移除冗余 status_code:131 处调用点改为纯 code 表达;security.py/dependencies.py
  的 status_code=401/403 去除(映射表已给);get_or_404 → code=RET.NOT_FOUND.code
- 新增静态护栏(AST):app/ 下除 exceptions.py 外不得出现 status_code 传参,
  路由装饰器 201 白名单显式放行并断言白名单非空

异常写法统一(t19)
- 删除 CustomException.internal 及 132 处包装:意外异常直接冒泡,由全局
  @app.exception_handler(Exception) 统一映射 5xx + 通用文案 + 日志;
  需要运维上下文处改为 logger.exception("<业务动作>失败") + raise
- 构造器不再接受 status_code 参数(属性保留,由 code 派生)→ 结构上杜绝第二来源
- 修复 6 处「宽泛 except 把 400 包成 500」的语义反转(补 except CustomException: raise),
  并加 AST 护栏防止回潮
- 全局 SQLAlchemyError:非完整性错误由 400 改为 503(连接类)/500(其它),
  不再把驱动原文/异常类型拼进客户端 msg;顺带修掉不存在的
  status.HTTP_403_SERVICE_UNAVAILABLE 常量(命中即 AttributeError→500)

验证:pytest 97 passed / ruff 全绿;AST 审计残留 0;护栏红绿测试通过;
线上实测验证码过期仍 400 并保留业务文案、无令牌 401。
This commit is contained in:
zhangtao
2026-10-04 05:44:21 +08:00
parent c73daa7eae
commit 644d262cad
31 changed files with 824 additions and 363 deletions
+30 -19
View File
@@ -10,9 +10,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, load_only, selectinload
from sqlalchemy.sql.elements import ColumnElement
from app.common.enums import RET
from app.core.base_model import ModelMixin
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
OutSchemaType = TypeVar("OutSchemaType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
@@ -77,8 +79,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
sql = sql.options(opt)
result: Result = await self.db.execute(sql)
return result.scalars().first()
except Exception as e:
raise CustomException(msg=f"获取查询失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('获取查询失败')
raise
async def get_or_404(
self,
@@ -97,7 +100,7 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
kwargs["id"] = id
obj = await self.get(preload=preload, include_deleted=include_deleted, **kwargs)
if not obj:
raise CustomException(msg=msg, status_code=404)
raise CustomException(msg=msg, code=RET.NOT_FOUND.code)
return out_schema.model_validate(obj) if out_schema else obj
async def exists(self, include_deleted: bool = False, **kwargs) -> bool:
@@ -111,8 +114,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
count_sql = select(func.count()).select_from(self.model).where(*conditions)
result: Result = await self.db.execute(count_sql)
return result.scalar() or 0
except Exception as e:
raise CustomException(msg=f"统计失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('统计失败')
raise
async def get_list(
self,
@@ -133,8 +137,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
sql = sql.options(opt)
result: Result = await self.db.execute(sql)
return result.scalars().all()
except Exception as e:
raise CustomException(msg=f"列表查询失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('列表查询失败')
raise
async def page(
self,
@@ -181,8 +186,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
has_next=offset + limit < total,
items=items,
)
except Exception as e:
raise CustomException(msg=f"分页查询失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('分页查询失败')
raise
# ── 写入 ──────────────────────────────────────────────────────────
@@ -217,8 +223,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
obj = result.scalar_one()
return obj
except Exception as e:
raise CustomException(msg=f"创建失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('创建失败')
raise
async def update(self, id: int, data: UpdateSchemaType | dict[str, Any]) -> ModelType:
"""更新记录。用 exclude_unset / exclude_none 准确表达前端意图。"""
@@ -255,8 +262,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
return obj
except CustomException:
raise
except Exception as e:
raise CustomException(msg=f"更新失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('更新失败')
raise
async def delete(self, ids: list[int]) -> None:
"""软删除优先,无软删除则物理删除。"""
@@ -271,8 +279,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
sql = delete(self.model).where(pk.in_(ids))
await self.db.execute(sql)
await self.db.flush()
except Exception as e:
raise CustomException(msg=f"删除失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('删除失败')
raise
async def clear(self) -> None:
"""清空整表。软删除模式下相当于"回收站清空",只清理已删标记的记录。"""
@@ -285,8 +294,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
sql = delete(self.model)
await self.db.execute(sql)
await self.db.flush()
except Exception as e:
raise CustomException(msg=f"清空失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('清空失败')
raise
async def set(self, ids: list[int], include_deleted: bool = False, **kwargs) -> None:
"""批量更新。软删除模式下默认跳过已删除的记录。"""
@@ -298,8 +308,9 @@ class CRUDBase[ModelType: ModelMixin, CreateSchemaType, UpdateSchemaType]:
sql = sql.values(**kwargs)
await self.db.execute(sql)
await self.db.flush()
except Exception as e:
raise CustomException(msg=f"批量更新失败: {e!s}", status_code=500) from e
except Exception:
logger.exception('批量更新失败')
raise
# ── 条件与排序 ────────────────────────────────────────────────────
+13 -13
View File
@@ -94,32 +94,32 @@ async def _authenticate(
) -> AuthSchema:
"""核心认证逻辑(HTTP 与 WebSocket 共享)"""
if not token:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
# 处理Bearer token(兼容无空格/无前缀输入,避免 IndexError)
if token.startswith("Bearer"):
token = token[len("Bearer") :].strip()
if not token:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
# 滑动模式下跳过 JWT exp 校验,由 Redis session TTL 决定实际有效期
payload = decode_access_token(token, verify_exp=not settings.TOKEN_SLIDING_EXPIRE)
if not payload or payload.is_refresh:
raise CustomException(msg="非法凭证", code=RET.INVALID_CREDENTIALS.code, status_code=401)
raise CustomException(msg="非法凭证", code=RET.INVALID_CREDENTIALS.code)
session_id = payload.sub
if not session_id:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
session_key = f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}"
raw = await RedisCURD(redis).get(session_key)
if not raw:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
user_info = json.loads(raw)
# 校验 session 数据完整性
if not user_info.get("session_id"):
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
# 滑动过期续期:以 USER_SESSION 为存活判据与续期主体,且受绝对上限约束
if settings.TOKEN_SLIDING_EXPIRE:
@@ -147,7 +147,7 @@ async def _authenticate(
)
if expired:
await crud.delete(session_key)
raise CustomException(msg="会话超过最大存活时长,请重新登录", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="会话超过最大存活时长,请重新登录", code=RET.UNAUTHORIZED.code)
# 续期必须落在存活判据(USER_SESSION)上,否则续期无效、判据形同虚设
await crud.expire(key=session_key, expire=settings.REFRESH_TOKEN_EXPIRE_SECONDS)
await crud.expire(
@@ -157,16 +157,16 @@ async def _authenticate(
username = user_info.get("user_name")
if not username:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
user_status = user_info.get("user_status", 0)
user_id = user_info.get("user_id")
if user_status == 1:
raise CustomException(msg="用户已被停用", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="用户已被停用", code=RET.UNAUTHORIZED.code)
if not user_id:
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code, status_code=401)
raise CustomException(msg="认证已失效", code=RET.UNAUTHORIZED.code)
# 每请求查库校验用户仍存在且未删除:token 只证明签发时身份,不证明现在。
from app.modules.system.user.model import UserModel # 延迟导入:core 导入期不依赖业务层(守卫不变式 3)
@@ -179,7 +179,7 @@ async def _authenticate(
.first()
)
if not user_obj:
raise CustomException(msg="用户不存在", code=RET.NOT_FOUND.code, status_code=401)
raise CustomException(msg="用户不存在", code=RET.UNAUTHORIZED.code)
user = CoreUserSchema.model_validate(user_obj)
return AuthSchema(
@@ -225,10 +225,10 @@ class AuthPermission:
user_permissions = set[Any](auth.permissions)
if not user_permissions:
raise CustomException(msg="无权限操作", code=RET.FORBIDDEN.code, status_code=403)
raise CustomException(msg="无权限操作", code=RET.FORBIDDEN.code)
if not any(perm in user_permissions for perm in self.permissions):
logger.error(f"用户缺少任何所需的权限: {self.permissions}")
raise CustomException(msg="无权限操作", code=RET.NO_PERMISSION.code, status_code=403)
raise CustomException(msg="无权限操作", code=RET.NO_PERMISSION.code)
return auth
+79 -22
View File
@@ -3,7 +3,7 @@ from typing import Any
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError, ResponseValidationError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.exc import DisconnectionError, IntegrityError, InterfaceError, SQLAlchemyError
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
@@ -42,33 +42,51 @@ def require_superadmin(func):
# 避免模块导入期就打 DeprecationWarning
_HTTP_422: int = getattr(status, "HTTP_422_UNPROCESSABLE_CONTENT", 422)
# 业务错误码 → HTTP 状态码。只映射语义明确的码;其余业务异常统一按 _DEFAULT_BUSINESS_STATUS 返回。
# 说明:历史上 CustomException 的 status_code 默认 500,导致「验证码过期」「参数校验失败」这类
# 客户端错误也返回 500 —— 前端无法区分「我错了」与「服务器坏了」,监控/告警也被污染。
# 业务错误码 → HTTP 状态码:**HTTP 语义的唯一事实来源**。
# 约定(t16):
# - 业务代码只传 code(`raise CustomException(msg=..., code=...)`),构造器不接受 status_code;
# - 本表未命中的 code 才会落到 _DEFAULT_BUSINESS_STATUS(仅为「新增 code 忘了登记」的安全网,
# 不应作为正常路径依赖);
# - 历史上 status_code 默认 500 让「验证码过期」「参数校验失败」也返回 500,客户端无法区分
# 「我错了」与「服务器坏了」,监控/告警同样被污染,故收敛到本表。
_CODE_TO_HTTP_STATUS: dict[int, int] = {
# ① 默认业务异常码:客户端可修正 → 400
RET.EXCEPTION.code: status.HTTP_400_BAD_REQUEST,
# ② 数值本身就是 HTTP 状态码的 RET 成员:显式登记(不再依赖安全网兜底)
RET.BAD_REQUEST.code: status.HTTP_400_BAD_REQUEST,
RET.UNAUTHORIZED.code: status.HTTP_401_UNAUTHORIZED,
RET.FORBIDDEN.code: status.HTTP_403_FORBIDDEN,
RET.NOT_FOUND.code: status.HTTP_404_NOT_FOUND,
RET.BAD_METHOD.code: status.HTTP_405_METHOD_NOT_ALLOWED,
RET.NOT_ACCEPTABLE.code: status.HTTP_406_NOT_ACCEPTABLE,
RET.CONFLICT.code: status.HTTP_409_CONFLICT,
RET.GONE.code: status.HTTP_410_GONE,
RET.PRECONDITION_FAILED.code: status.HTTP_412_PRECONDITION_FAILED,
RET.UNSUPPORTED_MEDIA_TYPE.code: status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
RET.UNPROCESSABLE_ENTITY.code: _HTTP_422,
RET.TOO_MANY_REQUESTS.code: status.HTTP_429_TOO_MANY_REQUESTS,
RET.INTERNAL_SERVER_ERROR.code: status.HTTP_500_INTERNAL_SERVER_ERROR,
RET.NOT_IMPLEMENTED.code: status.HTTP_501_NOT_IMPLEMENTED,
RET.BAD_GATEWAY.code: status.HTTP_502_BAD_GATEWAY,
RET.SERVICE_UNAVAILABLE.code: status.HTTP_503_SERVICE_UNAVAILABLE,
RET.GATEWAY_TIMEOUT.code: status.HTTP_504_GATEWAY_TIMEOUT,
RET.HTTP_VERSION_NOT_SUPPORTED.code: status.HTTP_505_HTTP_VERSION_NOT_SUPPORTED,
# ③ 自定义业务码
RET.DATAEXIST.code: status.HTTP_409_CONFLICT,
RET.PARAMERR.code: status.HTTP_400_BAD_REQUEST,
RET.TIMEOUT.code: status.HTTP_504_GATEWAY_TIMEOUT,
RET.RATE_LIMIT_EXCEEDED.code: status.HTTP_429_TOO_MANY_REQUESTS,
# 认证类业务码(对应前端 ResultEnum)
# ④ 认证类业务码(对应前端 ResultEnum)
RET.INVALID_TOKEN.code: status.HTTP_401_UNAUTHORIZED,
RET.EXPIRED_TOKEN.code: status.HTTP_401_UNAUTHORIZED,
RET.INVALID_CREDENTIALS.code: status.HTTP_401_UNAUTHORIZED,
RET.TOKEN_EXPIRED.code: status.HTTP_401_UNAUTHORIZED,
RET.NO_PERMISSION.code: status.HTTP_403_FORBIDDEN,
# 显式声明为服务端错误的码(供内部包装器使用,如 CRUD「xx失败」兜底)
# ⑤ 显式声明为服务端故障的业务码(供极少数已确认属服务端故障的业务场景使用)
RET.SERVERERR.code: status.HTTP_500_INTERNAL_SERVER_ERROR,
}
# 未映射业务码的默认状态:业务异常按「客户端请求错误」处理(而不是 500)
# 未登记业务码的安全网:按「客户端请求错误」处理(正常路径应命中上面的表;新增 RET 码请同步登记)
_DEFAULT_BUSINESS_STATUS: int = status.HTTP_400_BAD_REQUEST
@@ -87,32 +105,62 @@ def resolve_http_status(code: int | None) -> int:
class CustomException(Exception):
"""业务异常。
"""业务异常:系统内**唯一**的业务错误写法是 ``raise CustomException(msg=..., code=...)``。
``status_code`` 未显式传入时按 ``code`` 推导(见 :func:`resolve_http_status`),
默认 400(客户端可修正)而非 500;只有确属服务端故障的场景才应显式传 500,
或使用 ``code=RET.SERVERERR.code``。
约定(t19):
- 构造器不接受 ``status_code``:HTTP 状态由 ``code`` 经 :func:`resolve_http_status` 推导
(``status_code`` 只作只读派生属性),从结构上杜绝「业务码 + 显式状态码」两个事实来源;
- **意外异常不要在这里包装**:让原始异常冒泡,由全局 ``Exception`` / ``SQLAlchemyError``
处理器统一映射为 5xx + 通用文案 + 日志;需要运维上下文时在抛出处
``logger.exception("<业务动作>失败")`` 后再 ``raise``(保留原始异常类型);
- 宽泛 ``except`` 若确实要把异常转换成业务错误,必须先 ``except CustomException: raise``。
"""
def __init__(
self,
msg: str = RET.EXCEPTION.msg,
code: int = RET.EXCEPTION.code,
status_code: int | None = None,
data: Any | None = None,
success: bool = False,
) -> None:
super().__init__(msg)
self.status_code = status_code if status_code is not None else resolve_http_status(code)
self.code = code
self.msg = msg
self.data = data
self.success = success
# 只读派生属性:HTTP 状态一律由 code 经映射表推导(构造器不再接受 status_code)
self.status_code = resolve_http_status(code)
def __str__(self) -> str:
return self.msg
# 连接类故障的文案特征(DBAPI 驱动差异大,异常类型优先、文案兜底)
_CONNECTION_HINTS: tuple[str, ...] = (
"connect",
"connection",
"server has gone away",
"lost connection",
"connection refused",
"connection reset",
"pool",
"timed out",
)
def _is_connection_detail(detail: str | None) -> bool:
"""判断异常细节是否属于「连接类」故障(→ 503 而不是 500)。
参数:
- detail (str | None): 驱动返回的原始错误文本。
返回:
- bool: 是否连接类。
"""
text = (detail or "").lower()
return any(hint in text for hint in _CONNECTION_HINTS)
def handle_exception(app: FastAPI) -> None:
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException) -> JSONResponse:
@@ -169,13 +217,20 @@ def handle_exception(app: FastAPI) -> None:
@app.exception_handler(SQLAlchemyError)
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError) -> JSONResponse:
exc_type = type(exc).__name__
"""数据库异常统一出口。
分类(t19):
- ``IntegrityError``:约束类问题,按具体原因给出可操作文案(409 等);
- 其它:**不再返回 400**(把服务端故障说成客户端的错会让监控失明),
连接类(InterfaceError/DisconnectionError/连接中断文案)→ 503,其余 → 500;
- 文案不再拼 ``exc_type``/驱动原文,细节只进日志(``logger.opt(exception=...)`` 带堆栈)。
"""
if isinstance(exc, IntegrityError):
detail = str(exc.orig) if exc.orig else str(exc)
expose_detail = detail if settings.ENVIRONMENT != EnvironmentEnum.PROD else None
if "connect" in detail or "connection" in detail:
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_403_SERVICE_UNAVAILABLE, data=expose_detail)
logger.opt(exception=exc).error("[数据库异常] {} {}", request.method, request.url.path)
if _is_connection_detail(detail):
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_503_SERVICE_UNAVAILABLE, data=expose_detail)
if "Duplicate entry" in detail:
return ErrorResponse(msg="数据重复,请检查唯一字段", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
if "foreign key constraint" in detail:
@@ -184,9 +239,11 @@ def handle_exception(app: FastAPI) -> None:
return ErrorResponse(msg="必填字段缺失", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
return ErrorResponse(msg="数据已存在或违反完整性约束", status_code=status.HTTP_409_CONFLICT, data=expose_detail)
logger.error("[数据库异常] {} {} | type={} | detail={}", request.method, request.url.path, exc_type, exc)
data = str(exc) if settings.ENVIRONMENT != EnvironmentEnum.PROD else None
return ErrorResponse(msg=f"数据库操作失败: {exc_type}", status_code=status.HTTP_400_BAD_REQUEST, data=data)
detail = str(getattr(exc, "orig", None) or exc)
logger.opt(exception=exc).error("[数据库异常] {} {}", request.method, request.url.path)
if isinstance(exc, (InterfaceError, DisconnectionError)) or _is_connection_detail(detail):
return ErrorResponse(msg="数据库连接失败", status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
return ErrorResponse(msg="数据库操作失败", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
@app.exception_handler(ValueError)
async def value_exception_handler(request: Request, exc: ValueError) -> JSONResponse:
@@ -195,12 +252,12 @@ def handle_exception(app: FastAPI) -> None:
@app.exception_handler(Exception)
async def all_exception_handler(request: Request, exc: Exception) -> JSONResponse:
exc_type = type(exc).__name__
logger.error(
logger.opt(exception=exc).error(
"[未捕获异常] {} {} | type={} | detail={}",
request.method,
request.url.path,
exc_type,
type(exc).__name__,
exc,
)
# 细节只进日志/堆栈:客户端只得到通用文案(生产不回显内部信息)
return ErrorResponse(msg="服务器内部错误", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
+1 -1
View File
@@ -122,7 +122,7 @@ class CustomHTTPSRedirectMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.url.scheme != "https" and request.headers.get("X-Forwarded-Proto") != "https":
url = request.url.replace(scheme="https")
return RedirectResponse(url, status_code=301)
return RedirectResponse(url, 301)
return await call_next(request)
+6 -6
View File
@@ -47,7 +47,7 @@ class CustomOAuth2PasswordBearer(OAuth2PasswordBearer):
if not authorization or scheme.lower() != settings.TOKEN_TYPE.lower():
if self.auto_error:
raise CustomException(msg="认证失败,请登录后再试", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="认证失败,请登录后再试", code=RET.TOKEN_EXPIRED.code)
return None
return token
@@ -130,7 +130,7 @@ def decode_access_token(token: str, verify_exp: bool = True) -> JWTPayloadSchema
- CustomException: 解析失败时抛出,状态码为401。
"""
if not token:
raise CustomException(msg="认证不存在,请重新登录", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="认证不存在,请重新登录", code=RET.TOKEN_EXPIRED.code)
try:
options: dict = {}
@@ -140,15 +140,15 @@ def decode_access_token(token: str, verify_exp: bool = True) -> JWTPayloadSchema
online_user_info = payload.get("sub")
if not online_user_info:
raise CustomException(msg="无效认证,请重新登录", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="无效认证,请重新登录", code=RET.TOKEN_EXPIRED.code)
return JWTPayloadSchema(**payload)
except (jwt.InvalidSignatureError, jwt.DecodeError):
raise CustomException(msg="无效认证,请重新登录", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="无效认证,请重新登录", code=RET.TOKEN_EXPIRED.code)
except jwt.ExpiredSignatureError:
raise CustomException(msg="认证已过期,请重新登录", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="认证已过期,请重新登录", code=RET.TOKEN_EXPIRED.code)
except jwt.InvalidTokenError:
raise CustomException(msg="token已失效,请重新登录", code=RET.TOKEN_EXPIRED.code, status_code=401)
raise CustomException(msg="token已失效,请重新登录", code=RET.TOKEN_EXPIRED.code)
+16 -6
View File
@@ -8,6 +8,7 @@ from app.common.constant import DATE_DISPLAY_FMT, DATETIME_DISPLAY_FMT, TIME_DIS
from app.common.enums import RET
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.password_util import PwdUtil
# 自定义日期时间字符串类型
@@ -80,8 +81,11 @@ def datetime_validator(value: str | datetime) -> datetime:
return datetime.strptime(value, DATETIME_DISPLAY_FMT)
if isinstance(value, datetime):
return value
except Exception:
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式")
except CustomException:
raise
except Exception as e:
logger.exception("日期时间格式校验失败")
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式") from e
def date_validator(value: str | date) -> date:
@@ -101,8 +105,11 @@ def date_validator(value: str | date) -> date:
return datetime.strptime(value, DATE_DISPLAY_FMT).date()
if isinstance(value, date):
return value
except Exception:
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式")
except CustomException:
raise
except Exception as e:
logger.exception("日期格式校验失败")
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式") from e
def time_validator(value: str | time) -> time:
@@ -122,8 +129,11 @@ def time_validator(value: str | time) -> time:
return datetime.strptime(value, TIME_DISPLAY_FMT).time()
if isinstance(value, time):
return value
except Exception:
raise CustomException(code=RET.ERROR.code, msg="无效的时间格式")
except CustomException:
raise
except Exception as e:
logger.exception("时间格式校验失败")
raise CustomException(code=RET.ERROR.code, msg="无效的时间格式") from e
def email_validator(value: str) -> str: