mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-15 13:25:51 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -121,7 +121,7 @@ class DictTypeService:
|
||||
logger.info(f"创建字典类型成功: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建字典类型失败: {e}")
|
||||
raise CustomException(msg="同步字典类型缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
return new_obj_dict
|
||||
|
||||
@@ -176,7 +176,7 @@ class DictTypeService:
|
||||
logger.info(f"更新字典类型成功并刷新缓存: {new_obj_dict}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新字典类型缓存失败: {e}")
|
||||
raise CustomException(msg="同步字典类型缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
return new_obj_dict
|
||||
|
||||
@@ -215,7 +215,7 @@ class DictTypeService:
|
||||
logger.info(f"删除字典类型缓存: {dt}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除字典类型缓存失败: {e}")
|
||||
raise CustomException(msg="同步删除字典缓存失败", status_code=500) from e
|
||||
raise
|
||||
await DictTypeCRUD(self.auth, self.db).delete(ids=ids)
|
||||
|
||||
async def set_available(self, data: BatchSetAvailable) -> None:
|
||||
@@ -330,7 +330,7 @@ class DictDataService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌️ 字典初始化过程发生错误: {e}")
|
||||
raise CustomException(msg="字典数据初始化失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_init_cache(redis: Redis, dict_type: str) -> list[dict]:
|
||||
@@ -376,7 +376,7 @@ class DictDataService:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取字典缓存失败: {e!s}")
|
||||
raise CustomException(msg="获取字典数据失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
async def _refresh_dict_cache(self, redis: Redis, dict_type: str) -> None:
|
||||
"""刷新指定字典类型的 Redis 缓存
|
||||
@@ -420,7 +420,7 @@ class DictDataService:
|
||||
logger.info(f"创建字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建字典数据写入缓存失败: {e}")
|
||||
raise CustomException(msg="同步字典数据缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
return DictDataOutSchema.model_validate(obj)
|
||||
|
||||
@@ -462,7 +462,7 @@ class DictDataService:
|
||||
await self._refresh_dict_cache(redis, dict_type.dict_type)
|
||||
except Exception as e:
|
||||
logger.error(f"刷新旧字典缓存失败: {e}")
|
||||
raise CustomException(msg="同步旧字典数据缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
obj = await DictDataCRUD(self.auth, self.db).update(id=id, data=data)
|
||||
|
||||
@@ -472,7 +472,7 @@ class DictDataService:
|
||||
logger.info(f"更新字典数据写入缓存成功: {obj}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新字典数据写入缓存失败: {e}")
|
||||
raise CustomException(msg="同步字典数据缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
return DictDataOutSchema.model_validate(obj)
|
||||
|
||||
@@ -504,7 +504,7 @@ class DictDataService:
|
||||
logger.info(f"删除字典数据并刷新缓存: dict_type={dt}")
|
||||
except Exception as e:
|
||||
logger.error(f"删除字典数据刷新缓存失败: {e}")
|
||||
raise CustomException(msg="同步删除字典数据缓存失败", status_code=500) from e
|
||||
raise
|
||||
await DictDataCRUD(self.auth, self.db).delete(ids=ids)
|
||||
|
||||
async def set_available(self, data: BatchSetAvailable) -> None:
|
||||
|
||||
@@ -57,10 +57,11 @@ class ParamsService:
|
||||
)
|
||||
if not result:
|
||||
logger.error(f"同步配置到缓存失败: {out}")
|
||||
raise CustomException(msg="同步配置到缓存失败", status_code=500)
|
||||
logger.error("同步配置到缓存失败")
|
||||
raise RuntimeError("同步配置到缓存失败")
|
||||
except Exception as e:
|
||||
logger.error(f"更新系统配置失败: {e}")
|
||||
raise CustomException(msg="同步配置到缓存失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
return out
|
||||
|
||||
@@ -95,7 +96,7 @@ class ParamsService:
|
||||
await ParamsService._sync_configs_to_redis(redis, config_obj)
|
||||
except Exception as e:
|
||||
logger.error(f"❌️ 初始化系统参数到 Redis 失败: {e}")
|
||||
raise CustomException(msg="初始化系统参数到 Redis 失败", status_code=500) from e
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_init_cache(redis: Redis) -> list[dict]:
|
||||
|
||||
@@ -384,7 +384,7 @@ class UserService:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"批量导入用户失败: {e!s}")
|
||||
raise CustomException(msg=f"导入失败: {e!s}", status_code=500) from e
|
||||
raise
|
||||
|
||||
async def _process_import_row(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user