"""作废/撤单标记守卫 — voucher_details 的统一过滤入口(2026-09-14) **单一事实来源 = 标记表 `cma.voucher_void_marks`**(`entity_id + voucher_no + voucher_date` 三键 + `revoked_at IS NULL` 判定),与 DB 视图 `cma.v_voucher_details_active` 的定义逐字一致。 为什么用 NOT EXISTS 子查询而不是直接读视图: `VoucherDetail` ORM 模型同时被**写入方**使用(智享通推送落 `voucher_details`),把 ORM 映射到视图会让写入路径失效(视图非可更新)。本模块的过滤条件与视图定义同口径, 且是全库**唯一**实现(`scripts/kpi_value_collector.py` 亦委派到此处)→ 无口径漂移。 用法(任何消费 voucher_details 的查询): q = db.query(...).filter(...) q = apply_voucher_guard(db, q, VoucherDetail) # 非凭证明细源表原样返回 纪律:**禁硬编码凭证号**。今后任何撤单只需往标记表写一行(或置 revoked_at),代码零改动。 兼容:标记表不存在/不可读(测试库、旧库)→ 自动跳过过滤并告警一次,不阻断业务。 """ import logging import threading from sqlalchemy import MetaData, Table, and_, exists logger = logging.getLogger("cma.voucher_guard") VOID_MARK_TABLE = "voucher_void_marks" _lock = threading.Lock() _table_cache = {} # engine -> Table(存在)| False(不存在) _warned = set() # engine(已告警过的,避免刷日志) def reset_cache() -> None: """清空反射缓存(单测在「建表/删表」之间必须调用)。""" with _lock: _table_cache.clear() _warned.clear() def _bind_key(db): try: bind = db.get_bind() except Exception: # noqa: BLE001 - 拿不到绑定对象时退化为不缓存 return None return getattr(bind, "engine", bind) def void_mark_table(db): """作废标记表(按 engine 反射并缓存);表不存在/不可读 → None。""" key = _bind_key(db) if key is not None: with _lock: if key in _table_cache: cached = _table_cache[key] # 注意: Table 对象无真值(__bool__ 未定义),必须显式判 False return None if cached is False else cached try: # 走 Session 自身连接(而非 engine): 测试库 StaticPool 下两者是同一 DBAPI 连接, # 另开 engine 连接会在归还时回滚掉 Session 未提交的写入;此处保持同一事务。 bind = db.connection() if hasattr(db, "connection") else db.get_bind() table = Table(VOID_MARK_TABLE, MetaData(), autoload_with=bind) except Exception as e: # noqa: BLE001 - 兼容无标记表的环境 with _lock: if key is not None: _table_cache[key] = False if key not in _warned: _warned.add(key) logger.warning("作废标记表 %s 不可用 → 本次不做作废过滤: %s", VOID_MARK_TABLE, e) return None with _lock: if key is not None: _table_cache[key] = table logger.info("作废标记过滤已接入: %s(撤销标记后自动重新计入)", VOID_MARK_TABLE) return table def void_mark_table_exists(db) -> bool: return void_mark_table(db) is not None def void_exists_clause(marks, src): """作废判定子句: NOT EXISTS(标记表 三键匹配 且 revoked_at IS NULL)。 marks = 标记表 Table;src = VoucherDetail 或 aliased(VoucherDetail)。 """ return ~exists().where(and_( marks.c.entity_id == src.entity_id, marks.c.voucher_no == src.voucher_no, marks.c.voucher_date == src.voucher_date, marks.c.revoked_at.is_(None), )) def has_voucher_keys(src) -> bool: """src 是否带凭证键(voucher_no + voucher_date)——即「是凭证明细源表」。""" cols = getattr(src, "c", None) for name in ("voucher_no", "voucher_date"): ok = (name in cols) if cols is not None else hasattr(src, name) if not ok: return False return True def exclude_void(db, query, src): """给凭证明细查询挂上 NOT EXISTS(voucher_void_marks) 过滤(全库唯一口径实现)。 标记表不存在 → 原样返回。 """ marks = void_mark_table(db) if marks is None: return query return query.filter(void_exists_clause(marks, src)) def apply_voucher_guard(db, query, src): """凡指向凭证明细的查询一律过作废过滤(非凭证明细源表原样返回)。""" if has_voucher_keys(src): return exclude_void(db, query, src) return query