feat(audit): 扩大 GET 采集范围 + 修正动作文案

【扩大采集:核心业务数据的「查看详情」】
新增 _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 人 × 每天数百次
   ≈ 上万条/天)。若嫌吵,去掉该前缀一行即可。

注:读接口鉴权已在上一提交补齐,故这些"查看详情"记录能正确挂上操作人。
This commit is contained in:
2026-09-21 13:47:44 +08:00
parent 3f34652b07
commit 192c8ee9cc
2 changed files with 48 additions and 3 deletions

View File

@ -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_ACTIONPUT → 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(

View File

@ -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": "标为已读",
}