From 896bbd8eebd93a7a72a39b26722946c2c7464019 Mon Sep 17 00:00:00 2001 From: yueli Date: Tue, 29 Sep 2026 12:03:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(audit):=20=E4=BF=AE=E5=A4=8D=20=5Fcollect?= =?UTF-8?q?=5Fsnapshot=20=E5=85=B3=E7=B3=BB=E5=AE=88=E5=8D=AB=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E5=AF=BC=E8=87=B4=E7=9A=84=E5=AE=A1=E8=AE=A1=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasattr(attr, 'property') 对 inspect(obj).attrs 产出的 AttributeState 恒为 False,这个守卫从未生效过 —— 关系属性一律被 getattr() 取了值。 lazy='dynamic' 的关系(如 SysWarehouseLocation.children)拿到的是 AppenderQuery,_serialize_value 对非基础类型原样放行,最终 json.dumps 兜底走 str(query) → query.session → Session.flush(),而此刻正在 flush 中, 抛 InvalidRequestError("Session is already flushing"),整条审计被 except 吞掉。 后果: - sys_warehouse_location 的新建/删除审计长期为 0 条 (UPDATE 走 attr.history 而非 getattr,所以不受影响,症状是间歇性的) - 批量生成 3050 个节点时逐行抛 3050 次异常:请求 873ms → 修复后 349ms 改为按 inspect(cls).relationships 的名字排除;before_update_listener 里的同一处坏守卫一并修正。 另把 warehouse 登记进 AGGREGATE_PATH_MARKERS:库位批量生成一次可产出 3000+ 节点,未登记时一次点击写 3000+ 条审计,登记后只留一条带 targets 清单的记录。 --- inventory-backend/app/core/audit_listener.py | 27 ++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/inventory-backend/app/core/audit_listener.py b/inventory-backend/app/core/audit_listener.py index be5edae..26f8dbc 100644 --- a/inventory-backend/app/core/audit_listener.py +++ b/inventory-backend/app/core/audit_listener.py @@ -149,6 +149,10 @@ AGGREGATE_PATH_MARKERS = ( 'inbound/stock/stocktake/generate-missing', 'outbound', 'import/execute', + # 库位管理:批量生成一次可产出 3000+ 个节点,批量删除同理。 + # 未登记时一次点击就写 3000+ 条审计(且叠加 _collect_snapshot 旧守卫的逐行异常), + # 登记后一次点击只留一条带 targets 清单的记录。 + 'warehouse', ) # 聚合日志里保留的受影响对象清单上限。 @@ -571,12 +575,21 @@ def _collect_snapshot(target): """收集对象全字段快照(跳过忽略字段与关系属性)""" snap = {} state = inspect(target) + # ★ 关系属性必须按**名字**排除。原守卫写的是 + # hasattr(attr, 'property') and hasattr(attr.property, 'direction') + # 而 state.attrs 产出的是 AttributeState —— 它没有 .property,守卫恒为 False, + # 关系属性从来没被跳过过。后果不只是"快照里多几个字段": + # lazy='dynamic' 的关系(如 SysWarehouseLocation.children)取值得到的是 + # AppenderQuery,_serialize_value 对非基础类型原样放行,最终 json.dumps + # 兜底走 str(query) → query.session → Session.flush(),而此刻正在 flush 中 + # → InvalidRequestError("Session is already flushing") → 整条审计被 except 吞掉。 + # 实测库位的新建/删除审计因此长期为 0 条,批量生成 3000 个节点会逐行抛 3000 次。 + rel_keys = set(inspect(target.__class__).relationships.keys()) for attr in state.attrs: key = attr.key - if _should_skip_field(key): + if key in rel_keys: continue - # 跳过关系属性(如 .base / .material),它们不是列 - if hasattr(attr, 'property') and hasattr(attr.property, 'direction'): + if _should_skip_field(key): continue snap[key] = _serialize_value(getattr(target, key, None)) return snap @@ -604,11 +617,15 @@ def before_update_listener(mapper, connection, target): try: state = inspect(target) changes = {} + # 同 _collect_snapshot:关系属性按名字排除(原守卫基于不存在的 .property,恒为 False)。 + # 这里尤其危险 —— 关系一旦有 history,新旧值会被原样放进 changes, + # 后续 json.dumps 兜底 str() 到 AppenderQuery 就是同一个 flush 崩溃。 + rel_keys = set(inspect(target.__class__).relationships.keys()) for attr in state.attrs: key = attr.key - if _should_skip_field(key): + if key in rel_keys: continue - if hasattr(attr, 'property') and hasattr(attr.property, 'direction'): + if _should_skip_field(key): continue if not attr.history.has_changes(): continue