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
@@ -104,7 +104,7 @@ async def oauth_login_redirect_controller(
provider=provider,
redirect_uri=redirect_uri,
)
return RedirectContentResponse(url=url, status_code=302)
return RedirectContentResponse(url, 302)
@AuthRouter.get("/oauth/{provider}/callback", summary="第三方OAuth回调", include_in_schema=False)
@@ -125,7 +125,7 @@ async def oauth_callback_controller(
code=code,
state=state,
)
return RedirectContentResponse(url=url, status_code=302)
return RedirectContentResponse(url, 302)
# =================================================== #
@@ -73,7 +73,8 @@ def _require_credentials(provider: OAuthProvider) -> tuple[str, str]:
raise CustomException(msg="不支持的 OAuth 渠道")
if not cid or not sec:
# 渠道密钥为空属**部署配置缺失**:客户端无论怎么改请求都无法成功,按 500 返回以免监控误判为客户端错误
raise CustomException(msg=f"{provider} OAuth 未配置(客户端密钥为空)", status_code=500)
logger.error(f"{provider} OAuth 未配置(客户端密钥为空)")
raise RuntimeError(f"{provider} OAuth 未配置(客户端密钥为空)")
return cid, sec
@@ -137,7 +138,7 @@ async def _http_json(method: str, url: str, **kwargs: Any) -> Any:
except json.JSONDecodeError:
text = r.text
logger.error(f"OAuth 非 JSON 响应: {text[:500]}")
raise CustomException(msg="OAuth 接口返回异常", status_code=500)
raise
async def _http_text(method: str, url: str, **kwargs: Any) -> str:
@@ -438,6 +439,10 @@ async def start_oauth_login(
)
except CustomException as e:
return _frontend_error_redirect(redirect_uri or fallback, e.msg)
except Exception:
# 浏览器跳转边界:意外故障不向用户抛裸 500,细节进日志、用户回到错误页(t19)
logger.exception("OAuth 授权入口处理失败")
return _frontend_error_redirect(redirect_uri or fallback, "登录服务暂时不可用,请稍后重试")
async def finish_oauth_login(
@@ -472,6 +477,10 @@ async def finish_oauth_login(
)
except CustomException as e:
return _frontend_error_redirect(await _frontend(), e.msg)
except Exception:
# 同上:OAuth 回调是浏览器跳转边界,意外故障统一回错误页
logger.exception("OAuth 回调处理失败")
return _frontend_error_redirect(await _frontend(), "登录服务暂时不可用,请稍后重试")
return _frontend_success_redirect(
frontend,
token.access_token,
+2 -1
View File
@@ -83,7 +83,8 @@ async def auto_register_login_user(
return existing
# 原始异常被吞掉(可能是 DB/唯一约束/驱动等未知服务端故障):按 500 返回,
# 避免监控把内部故障误判为客户端错误;确实的客户端问题由 UserService 的 CustomException 透传
raise CustomException(msg=fail_msg, status_code=500)
logger.exception('自动注册第三方登录用户失败')
raise
user = await UserCRUD(auth, db).get(username=username)
if not user:
raise CustomException(msg=fail_msg)
@@ -67,7 +67,7 @@ async def _http_json(method: str, url: str, **kwargs: Any) -> dict:
return r.json()
except json.JSONDecodeError:
logger.error(f"微信 API 非 JSON 响应: {r.text[:500]}")
raise CustomException(msg="微信接口返回异常", status_code=500)
raise
async def _get_access_token(redis: Redis) -> str: