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