"""admin_audit_log 写入 + 查询。 审计日志只增不改不删——任何写操作经 app.admin.audit.write_audit 落一条。 """ from __future__ import annotations from sqlalchemy import select from sqlalchemy.orm import Session from app.models.admin import AdminAuditLog def add_audit_log( db: Session, *, admin_id: int, admin_username: str, action: str, target_type: str, target_id: str | None = None, detail: dict | None = None, ip: str | None = None, commit: bool = True, ) -> AdminAuditLog: """插一条审计。commit=False 时只 flush,让调用方把审计和业务写操作放同一事务。""" log = AdminAuditLog( admin_id=admin_id, admin_username=admin_username, action=action, target_type=target_type, target_id=target_id, detail=detail, ip=ip, ) db.add(log) if commit: db.commit() db.refresh(log) else: db.flush() return log def list_audit_logs( db: Session, *, action: str | None = None, target_type: str | None = None, admin_id: int | None = None, limit: int = 50, cursor: int | None = None, ) -> tuple[list[AdminAuditLog], int | None]: """游标分页(id 倒序),与现有 list_* 约定一致。返回 (rows, next_cursor)。""" stmt = select(AdminAuditLog) if action: stmt = stmt.where(AdminAuditLog.action == action) if target_type: stmt = stmt.where(AdminAuditLog.target_type == target_type) if admin_id is not None: stmt = stmt.where(AdminAuditLog.admin_id == admin_id) if cursor is not None: stmt = stmt.where(AdminAuditLog.id < cursor) stmt = stmt.order_by(AdminAuditLog.id.desc()) rows = list(db.execute(stmt.limit(limit + 1)).scalars().all()) has_more = len(rows) > limit items = rows[:limit] # next_cursor 必须是"本页返回的最后一条"的 id(下一页查 id < 它),不能用 rows[limit]—— # rows[limit] 是探测下一页用的第 limit+1 条,它既不在本页也不在下页 → 每页边界丢一条。 next_cursor = items[-1].id if has_more else None return items, next_cursor