"""安全审计日志服务。 提供 log_action() 用于在任意路由中记录安全相关事件。日志写入数据库 audit_logs 表, 同时输出到 logger,便于后续接入文件/集中式日志收集。 """ from __future__ import annotations import logging from typing import Any, Optional from fastapi import Request from sqlalchemy.orm import Session from models.audit import AuditLog from models.user import User logger = logging.getLogger(__name__) def _user_label(user: Optional[User]) -> str: """返回用户可读标识:优先 username,其次 phone,最后 id。""" if user is None: return "" for attr in ("username", "phone", "name"): val = getattr(user, attr, None) if val: return str(val) return str(getattr(user, "id", "")) def _client_ip(request: Optional[Request]) -> str: if request is None: return "" try: fwd = request.headers.get("x-forwarded-for") if fwd: return fwd.split(",")[0].strip()[:64] client = getattr(request, "client", None) if client and client.host: return client.host[:64] except Exception: pass return "" def _user_agent(request: Optional[Request]) -> str: if request is None: return "" try: return (request.headers.get("user-agent") or "")[:255] except Exception: return "" def log_action( db: Session, *, action: str, user: Optional[User] = None, request: Optional[Request] = None, target_type: str = "", target_id: Any = "", detail: str = "", status: str = "success", ) -> None: """记录一条审计日志。失败不影响主流程。""" try: log = AuditLog( user_id=user.id if user else None, username=_user_label(user), action=action[:64], target_type=target_type[:32] if target_type else "", target_id=str(target_id)[:64] if target_id else "", detail=detail[:2000] if detail else "", ip=_client_ip(request), user_agent=_user_agent(request), status=status[:16] if status else "success", ) db.add(log) db.commit() except Exception as exc: # pragma: no cover - 日志失败不应中断业务 logger.warning("audit log failed: %s", exc) try: db.rollback() except Exception: pass # 同时输出到日志通道 logger.info( "audit action=%s user=%s target=%s/%s status=%s ip=%s", action, _user_label(user), target_type, target_id, status, _client_ip(request), )