"""平台超管审计写入 helper。失败绝不阻断主流程(审计是旁路)。""" def _client_ip(request): if request is None: return None forwarded = request.META.get("HTTP_X_FORWARDED_FOR", "") if forwarded: return forwarded.split(",")[0].strip() return request.META.get("REMOTE_ADDR") or None def _json_safe(value): """把任意值压成 JSONField 可存的结构(UUID/Decimal/datetime 等转字符串)。 失败兜底成 {_repr}。审计字段绝不能因不可序列化而抛错、污染外层事务。""" import json from django.core.serializers.json import DjangoJSONEncoder if value is None: return None try: return json.loads(json.dumps(value, cls=DjangoJSONEncoder)) except Exception: # noqa: BLE001 return {"_repr": str(value)} def log_admin_action( request, action, *, target_type="", target_id="", target_name="", before=None, after=None, ): """记录一条平台超管操作审计。action 用动词短语(如 'invite.issue' / 'team.disable')。 operator_name 取当前登录用户名快照。**用 savepoint 包裹**:审计写入失败只回滚自身, 绝不把调用方所在事务标脏(否则后续查询会 TransactionManagementError)。""" from django.db import transaction from .models import AdminAuditLog try: user = getattr(request, "user", None) operator = user if (user is not None and getattr(user, "is_authenticated", False)) else None with transaction.atomic(): AdminAuditLog.objects.create( operator=operator, operator_name=(getattr(operator, "username", "") or ""), action=action, target_type=target_type or "", target_id=str(target_id or ""), target_name=target_name or "", before=_json_safe(before), after=_json_safe(after), ip_address=_client_ip(request), ) except Exception: # noqa: BLE001 — 审计失败不应阻断主流程 pass