Files
dpb/backend/app/api/v1/module_bre/audit/service.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

125 lines
4.5 KiB
Python

"""育种审计日志 模块服务层(§3.10;写操作全程可追溯)"""
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException
from .crud import BreedingAuditCRUD
from .model import AuditLogModel
from .schema import AuditLogOutSchema, AuditLogQueryParam
# 供 Service 层在导入汇总/业务路径直接落审计行(不触发 CRUDBase 递归审计)
_ALLOWED_ACTIONS = {"CREATE", "UPDATE", "DELETE", "IMPORT"}
def _fmt(v: Any) -> str | None:
if v is None:
return None
s = str(v)
return s[:500] if len(s) > 500 else s
class AuditLogService:
"""审计日志 —— 只读查询 + 静态写入。写入走 CRUDBase 自动注入,不对外暴露增删改。"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
@staticmethod
async def write(
db: AsyncSession,
*,
entity_type: str,
entity_id: int | None,
action: str,
field_name: str | None = None,
old_value: Any = None,
new_value: Any = None,
created_id: int | None = None,
operator_id: int | None = None,
) -> int:
"""直接落一条审计行(不经 CRUDBase,防递归)。返回新行 id。"""
if action not in _ALLOWED_ACTIONS:
raise CustomException(msg=f"非法审计操作类型: {action}")
row = AuditLogModel(
entity_type=entity_type[:32] if entity_type else entity_type,
entity_id=entity_id,
action=action,
field_name=(field_name[:64] if field_name else field_name),
old_value=_fmt(old_value),
new_value=_fmt(new_value),
created_id=created_id,
operator_id=operator_id,
)
db.add(row)
await db.flush()
return int(row.id)
async def page(
self,
page_no: int,
page_size: int,
search: AuditLogQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[AuditLogOutSchema]:
from app.utils.common_util import search_to_dict
result = await BreedingAuditCRUD(self.auth, self.db).page(
offset=(page_no - 1) * page_size,
limit=page_size,
order_by=order_by or [{"id": "desc"}],
search=search_to_dict(search, {}),
out_schema=AuditLogOutSchema,
)
await self._attach_labels(result.items)
return result
async def get_list(self, search: AuditLogQueryParam | None = None) -> list[AuditLogOutSchema]:
from app.utils.common_util import search_to_dict
objs = await BreedingAuditCRUD(self.auth, self.db).get_list(
search=search_to_dict(search, {}), order_by=[{"id": "desc"}]
)
outs = [AuditLogOutSchema.model_validate(o) for o in objs]
await self._attach_labels(outs)
return outs
async def detail(self, id: int) -> AuditLogOutSchema:
obj = await BreedingAuditCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="该审计日志不存在")
out = AuditLogOutSchema.model_validate(obj)
await self._attach_labels([out])
return out
async def _attach_labels(self, items: list[AuditLogOutSchema]) -> None:
op_ids = {getattr(it, "operator_id") for it in items if getattr(it, "operator_id")}
if op_ids:
from app.api.v1.module_bre.personnel.model import PersonnelModel
rows = (await self.db.execute(
select(PersonnelModel.id, PersonnelModel.name).where(PersonnelModel.id.in_(op_ids))
)).all()
op_map = {r.id: r.name for r in rows}
for it in items:
it.operator_name = op_map.get(getattr(it, "operator_id"))
async def options(self) -> dict[str, Any]:
"""前端筛选下拉:实体类型 / 操作类型 去重集合。"""
etypes = (await self.db.execute(
select(AuditLogModel.entity_type)
.where(AuditLogModel.entity_type.isnot(None))
.distinct()
)).scalars().all()
actions = (await self.db.execute(
select(AuditLogModel.action)
.where(AuditLogModel.action.isnot(None))
.distinct()
)).scalars().all()
return {"entity_types": sorted(etypes), "actions": sorted(actions)}