"""审计:和业务修改在同一个事务里写入,写失败整个操作就失败(架构 §3.10)。""" from __future__ import annotations import json from typing import Any from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from bid_doctor.audit.models import AuditEvent from bid_doctor.core.crypto import sha256 from bid_doctor.core.db import TenantContext from bid_doctor.core.ids import uuid7 async def record( session: AsyncSession, ctx: TenantContext, action: str, object_type: str, object_id: UUID | None, *, project_id: UUID | None = None, before: dict[str, Any] | None = None, after: dict[str, Any] | None = None, actor_kind: str = "user", ) -> None: digest = ( sha256(json.dumps([before, after], ensure_ascii=False, sort_keys=True, default=str)) if before or after else None ) session.add( AuditEvent( id=uuid7(), tenant_id=ctx.tenant_id, actor_kind=actor_kind, actor_user_id=ctx.user_id if actor_kind == "user" else None, actor_name=ctx.actor_name or None, action=action, object_type=object_type, object_id=object_id, project_id=project_id, before=before, after=after, content_digest=digest, request_id=ctx.request_id or None, ) ) async def events_for(session: AsyncSession, object_type: str, object_id: UUID) -> list[AuditEvent]: """某个对象的修改记录,按时间倒序(FR-M2-08「谁在何时改了什么」)。""" rows = await session.execute( select(AuditEvent) .where(AuditEvent.object_type == object_type, AuditEvent.object_id == object_id) .order_by(AuditEvent.created_at.desc(), AuditEvent.id.desc()) ) return list(rows.scalars().all()) async def last_before(session: AsyncSession, action: str, object_id: UUID) -> dict[str, Any] | None: """某个对象最近一次某种操作之前的状态(例如撤销归档时回到归档前的项目状态)。""" row = ( await session.execute( select(AuditEvent.before) .where(AuditEvent.action == action, AuditEvent.object_id == object_id) .order_by(AuditEvent.id.desc()) .limit(1) ) ).scalar_one_or_none() return row if isinstance(row, dict) else None