125 lines
4.5 KiB
Python
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)}
|