From 192c8ee9cc09d2c93b98ef7ffca68ac65961a2f7 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 21 Sep 2026 13:47:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(audit):=20=E6=89=A9=E5=A4=A7=20GET=20?= =?UTF-8?q?=E9=87=87=E9=9B=86=E8=8C=83=E5=9B=B4=20+=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【扩大采集:核心业务数据的「查看详情」】 新增 _TRACKED_READ_PREFIXES(notifications / tasks / orders / products / records)—— products 是补的:GET /products/scan/{sn}(扫码查询) 是整个车间最高频的读操作,不采它等于没采"活跃度"。 _should_audit 的 GET 分支改为三级判断: 敏感读(export/download/print) → 受跟踪前缀且非裸列表 → 否则不采 用 _is_bare_list 跳过「拉整个列表」: · 列表接口被前端高频轮询(消息、任务列表尤其明显),逐条留痕会让 audit_logs 迅速膨胀,真正有价值的操作反而被淹没; · 只有「查看详情」(/tasks/{id}) 才代表用户真的点开了某条业务数据。 判定用「去掉末尾斜杠后是否恰好等于某前缀」,天然排除查询串。 【修正文案】 - "read": "查询" → "查看详情"(前者易被误解成"随便搜了一下") - 新增 "mark_read": "标为已读",并在 _SEGMENT_ACTION 补 "read" 映射 —— 否则 PUT /notifications/{id}/read 会回退到 _METHOD_ACTION(PUT→update), 把"点开一条通知"记成"修改了某样东西" - "refresh": "刷新令牌" → "上线"(token 2 小时一换,业务上视作一次上线) ⚠️ 两点须知: 1. notifications / orders 目录下【只有列表路由】,而列表按规则不采集, 故这两个模块不会产生查看记录 —— 后端没有"查看单条消息"的接口。 且消息列表被前端轮询,采集它反而会造成日志爆炸,跳过是正确的。 2. products 被纳入后,工人每扫一次码就多一条记录(50 人 × 每天数百次 ≈ 上万条/天)。若嫌吵,去掉该前缀一行即可。 注:读接口鉴权已在上一提交补齐,故这些"查看详情"记录能正确挂上操作人。 --- backend/app/core/audit_middleware.py | 41 ++++++++++++++++++++++++++- backend/app/services/audit_service.py | 10 +++++-- 2 files changed, 48 insertions(+), 3 deletions(-) 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": "标为已读", }