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:
DjangoAdmin Dev
2026-08-07 14:29:07 +08:00
co-authored by Claude
parent d33dc1fc34
commit a6e094378c
4 changed files with 154 additions and 0 deletions
+12
View File
@@ -22,6 +22,7 @@ from flask import request
from werkzeug.exceptions import HTTPException
from utils import R
from src.core.exceptions import BizError
logger = logging.getLogger(__name__)
@@ -66,6 +67,17 @@ def register_error_handlers(app):
msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误"
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)
def handle_exception(e):
# HTTPException 子类(如 abort(403))已有专用 handler,不重复处理
+65
View File
@@ -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
+33
View File
@@ -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)
+44
View File
@@ -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)