"""请求上下文透传(迭代第 3 轮)。 **问题**:审计需要 IP / User-Agent,但业务服务层(`sales.services.confirm_sales_bill` 等) 拿不到 `request` 对象——它们是被视图调用的纯函数,签名里加 request 会污染业务 API。 **方案**:中间件在请求进入时把 request 存进 `contextvars.ContextVar`, 服务层通过 `current_request()` 读取。用 contextvars 而非 threading.local 是为了 在 ASGI(Graniot/asyncio)下也能正确隔离协程——threading.local 在异步场景会串请求。 安全性:只读用途(审计取 IP/UA),不做鉴权决策;请求结束即清除。 """ from __future__ import annotations from contextvars import ContextVar from typing import Optional _current_request: ContextVar[Optional[object]] = ContextVar( "dealerhub_current_request", default=None ) def set_current_request(request) -> None: """中间件调用:绑定当前请求。""" _current_request.set(request) def clear_current_request() -> None: """中间件调用:解绑(防止跨请求泄漏)。""" _current_request.set(None) def current_request(): """服务层调用:取当前请求(可能为 None,如后台任务/测试)。""" return _current_request.get() class RequestContextMiddleware: """把 request 放进 contextvar,供服务层读取(审计用)。 注意:本中间件**不**做鉴权或租户解析(那些由 TenantMiddleware / DRF 负责), 仅提供"当前请求"的只读视图。 """ def __init__(self, get_response): self.get_response = get_response def __call__(self, request): token = _current_request.set(request) try: return self.get_response(request) finally: # 用 reset 精确恢复到进入前的值,避免异步并发下互相覆盖 _current_request.reset(token)