refactor: P4a 服务层改造地基(BizError + base_repository + base_service)
- src/core/exceptions.py:BizError/NotFoundError/PermissionDeniedError - 全局异常处理器接入 BizError → R.failed(含事务回滚语义) - src/common/base_repository.py:泛型 CRUD 仓储基类 - src/common/base_service.py:服务基类(入参数据/出参数据范式) 【验收】BizError 端到端转换为 R.failed(code=403/ok=false);应用启动 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
d33dc1fc34
commit
a6e094378c
@@ -22,6 +22,7 @@ from flask import request
|
|||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from utils import R
|
from utils import R
|
||||||
|
from src.core.exceptions import BizError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -66,6 +67,17 @@ def register_error_handlers(app):
|
|||||||
msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误"
|
msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误"
|
||||||
return R.failed(msg=msg, code=500), 500
|
return R.failed(msg=msg, code=500), 500
|
||||||
|
|
||||||
|
@app.errorhandler(BizError)
|
||||||
|
def handle_biz_error(e):
|
||||||
|
"""
|
||||||
|
业务异常处理器
|
||||||
|
|
||||||
|
业务代码 raise BizError(...) 时统一转换为 R.failed 响应,
|
||||||
|
并回滚当前事务(teardown_request 处理)。
|
||||||
|
"""
|
||||||
|
logger.info(f"业务异常: {e.msg} (code={e.code})")
|
||||||
|
return R.failed(msg=e.msg, code=e.code, data=e.data, **e.kwargs)
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
def handle_exception(e):
|
def handle_exception(e):
|
||||||
# HTTPException 子类(如 abort(403))已有专用 handler,不重复处理
|
# HTTPException 子类(如 abort(403))已有专用 handler,不重复处理
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
仓储基类
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
封装通用 CRUD 查询。业务模块的 Repository 继承此类并指定 model。
|
||||||
|
依赖 BaseModel 的通用字段(id / is_delete / create_* / update_*)。
|
||||||
|
|
||||||
|
设计要点:
|
||||||
|
- 查询默认过滤已删除记录(is_delete == 0)
|
||||||
|
- 不持有事务:保存沿用 BaseDB.save(),由 AOP 事务拦截器统一提交
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRepository:
|
||||||
|
"""泛型仓储基类"""
|
||||||
|
|
||||||
|
#: 子类指定模型类
|
||||||
|
model = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, obj_id) -> Optional[Any]:
|
||||||
|
"""按主键查询未删除记录;ID 无效时返回 None"""
|
||||||
|
if not obj_id or int(obj_id) <= 0:
|
||||||
|
return None
|
||||||
|
return cls.model.query.filter(
|
||||||
|
and_(cls.model.id == int(obj_id), cls.model.is_delete == 0)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_active(cls, order_by=None) -> List[Any]:
|
||||||
|
"""查询全部未删除记录"""
|
||||||
|
query = cls.model.query.filter(cls.model.is_delete == 0)
|
||||||
|
if order_by is not None:
|
||||||
|
query = query.order_by(order_by)
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def paginate(cls, query, page: int, page_size: int):
|
||||||
|
"""分页查询:返回 (items, total, pages)"""
|
||||||
|
total = query.count()
|
||||||
|
pages = (total + page_size - 1) // page_size if total > 0 else 0
|
||||||
|
items = query.limit(page_size).offset((page - 1) * page_size).all()
|
||||||
|
return items, total, pages
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_by_id(cls, obj_id, data: dict) -> int:
|
||||||
|
"""按主键更新字段,返回影响行数"""
|
||||||
|
return cls.model.query.filter_by(id=obj_id).update(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, **kwargs) -> Any:
|
||||||
|
"""创建并保存实例,返回实例对象"""
|
||||||
|
obj = cls.model(**kwargs)
|
||||||
|
obj.save()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls, obj) -> Any:
|
||||||
|
"""保存实例,返回实例对象"""
|
||||||
|
obj.save()
|
||||||
|
return obj
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
服务基类
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
提供服务层通用骨架。服务层编码规范:
|
||||||
|
- 函数入参为数据(dict / 标量),出参为纯数据
|
||||||
|
- 业务失败通过 `raise BizError` 表达(见 src/core/exceptions.py)
|
||||||
|
- 不直接读取 request、不返回 R 对象(request 解析与 R 包装由 router 层负责)
|
||||||
|
- 数据访问走 repository(见 src/common/base_repository.py)
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
class PositionService(BaseService):
|
||||||
|
repository = PositionRepository
|
||||||
|
|
||||||
|
def add_position(self, data):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
from src.common.base_repository import BaseRepository
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
"""服务基类"""
|
||||||
|
|
||||||
|
#: 子类指定仓储类
|
||||||
|
repository: BaseRepository = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_info(cls, obj_id):
|
||||||
|
"""通用:按 ID 查询模型对象"""
|
||||||
|
if cls.repository is None:
|
||||||
|
return None
|
||||||
|
return cls.repository.get_by_id(obj_id)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
业务异常定义
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
业务代码通过 `raise BizError(...)` 表达业务失败,由全局异常处理器
|
||||||
|
统一转换为 R.failed 响应。业务层不再 `return R.failed`,使服务函数
|
||||||
|
保持「入参数据、出参数据」的纯函数形态(可单元测试)。
|
||||||
|
|
||||||
|
使用示例:
|
||||||
|
from src.core.exceptions import BizError
|
||||||
|
|
||||||
|
def add_position(data):
|
||||||
|
form = PositionForm(data=data)
|
||||||
|
if not form.validate():
|
||||||
|
raise BizError(regular.get_err(form))
|
||||||
|
position.save()
|
||||||
|
"""
|
||||||
|
|
||||||
|
from src.common.constants.common import MESSAGE_FAIL
|
||||||
|
|
||||||
|
|
||||||
|
class BizError(Exception):
|
||||||
|
"""业务异常:统一转为 R.failed 响应"""
|
||||||
|
|
||||||
|
def __init__(self, msg: str = MESSAGE_FAIL, code: int = 1, data=None, **kwargs):
|
||||||
|
self.msg = msg
|
||||||
|
self.code = code
|
||||||
|
self.data = data
|
||||||
|
self.kwargs = kwargs
|
||||||
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(BizError):
|
||||||
|
"""资源不存在(默认提示)"""
|
||||||
|
|
||||||
|
def __init__(self, msg: str = "资源不存在", **kwargs):
|
||||||
|
super().__init__(msg=msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionDeniedError(BizError):
|
||||||
|
"""权限不足"""
|
||||||
|
|
||||||
|
def __init__(self, msg: str = "权限不足", code: int = 403, **kwargs):
|
||||||
|
super().__init__(msg=msg, code=code, **kwargs)
|
||||||
Reference in New Issue
Block a user