diff --git a/backend/app/core/audit_middleware.py b/backend/app/core/audit_middleware.py index cfa8969..f42f1b0 100644 --- a/backend/app/core/audit_middleware.py +++ b/backend/app/core/audit_middleware.py @@ -39,6 +39,22 @@ _MUTATING_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"}) # 读操作里需要留痕的(导出/下载/打印属于「读」,但把数据带出了系统) _SENSITIVE_READ_KEYWORDS = frozenset({"export", "download", "print"}) +# 核心业务模块 —— 这些前缀下的「查看详情」GET 也采集, +# 用于回答「谁在什么时候看过哪条业务数据」,而不只是「谁改过」。 +# +# ⚠️ 只覆盖【核心业务实体】: +# products —— 移动端扫码查询 GET /products/scan/{sn} 是车间最高频的读操作 +# tasks —— 查看任务详情 /tasks/{id} +# records —— 任务记录 +# notifications / orders —— 见下方 _is_bare_list 的说明 +_TRACKED_READ_PREFIXES = ( + "/api/v1/notifications", + "/api/v1/tasks", + "/api/v1/orders", + "/api/v1/products", + "/api/v1/records", +) + # 永久忽略的路径前缀 _IGNORED_PREFIXES = ("/health", "/docs", "/redoc", "/openapi.json") @@ -81,6 +97,10 @@ _SEGMENT_ACTION: dict[str, str] = { "spawn": "spawn", "complete": "complete", "end": "end", + # 消息已读:PUT /notifications/{id}/read。 + # 没有这一条时会回退到 _METHOD_ACTION(PUT → update → "修改"), + # 把"点开一条通知"记成"修改了某样东西",语义完全走样。 + "read": "mark_read", } _METHOD_ACTION: dict[str, str] = { @@ -97,6 +117,20 @@ _NON_ID_SEGMENTS = frozenset( ) +def _is_bare_list(path: str) -> bool: + """判断是否只是「拉整个列表」(如 GET /api/v1/tasks/)。 + + 这类请求【不采集】,理由: + · 列表接口被前端高频轮询(消息、任务列表尤其明显),逐条留痕会让 + audit_logs 迅速膨胀,真正有价值的操作反而被淹没; + · 「查看详情」(/tasks/{id}) 才代表用户真的点开了某条业务数据。 + + 判定用「去掉末尾斜杠后是否恰好等于某个受跟踪前缀」, + 比正则更直观,也天然把查询串排除在外(request.url.path 不含 ?query)。 + """ + return path.rstrip("/") in _TRACKED_READ_PREFIXES + + def _derive_module_and_action(path: str, method: str) -> tuple[str, str, str | None]: """由请求路径与 HTTP 方法推导 (module, action, target_id)""" parts = [p for p in path.split("/") if p] @@ -141,7 +175,12 @@ class AuditMiddleware(BaseHTTPMiddleware): return True if request.method == "GET": lowered = path.lower() - return any(kw in lowered for kw in _SENSITIVE_READ_KEYWORDS) + if any(kw in lowered for kw in _SENSITIVE_READ_KEYWORDS): + return True + # 核心业务数据的「查看详情」也留痕(证明用户在真的使用系统) + if path.startswith(_TRACKED_READ_PREFIXES): + return not _is_bare_list(path) + return False return False async def dispatch( diff --git a/backend/app/services/audit_service.py b/backend/app/services/audit_service.py index 7021c94..b44d963 100644 --- a/backend/app/services/audit_service.py +++ b/backend/app/services/audit_service.py @@ -62,11 +62,16 @@ ACTION_LABELS: dict[str, str] = { "create": "新增", "update": "修改", "delete": "删除", - "read": "查询", + # 只用于被采集的 GET(核心业务详情 / 敏感读)。 + # 叫「查看详情」而不是「查询」:前者说明用户确实点开了某条业务数据, + # 后者容易被误解成"随便搜了一下"。 + "read": "查看详情", "export": "导出", "login": "登录", "logout": "登出", - "refresh": "刷新令牌", + # 刷新令牌 = 用户重新开始使用系统(token 2 小时一换,7 天免登录), + # 业务上视作一次「上线」,比"刷新令牌"这种技术词更贴近车间口径 + "refresh": "上线", "print": "打印", "upload": "上传", "finalize": "收口", @@ -77,6 +82,7 @@ ACTION_LABELS: dict[str, str] = { "spawn": "派发", "end": "结束分支", "complete": "完结", + "mark_read": "标为已读", }