# 零录入票据归集跨租户隔离与失败回滚

日期：2026-07-16
文档路径：document/development/2026-07-16/dev-logs/bugs/zero-entry-receipt-tenant-and-rollback.md

## 修复记录
- 09:35：记录 bug 修复：零录入票据归集跨租户隔离与失败回滚。（bug-log:54754b55）
  - Git 提交检查：2026-07-16 09:35 CST 执行 `git fetch --all --prune` 成功；`origin/main` 没有远端新提交；本地 ahead 11 条，依次是 `54754b55` 个人费用申请记忆、`211f85d9` 统一核验申请工作流、`5b246307` 申请预览决策、`a662cfe6` 申请反馈账本、`5ed34c2b` 历史费用单回填、`11275e4b` 迁移所有权安全、`1347366b` 费用时间线与草稿事件安全、`22669a90` 统一费用事件时间线、`a616b30c` AI 申请提交事务、`653eda05` Bearer 会话、`661990b2` 事务化费用事件；这些提交均为本轮开始前已有的本地检查点，本次未合并或改写历史。
  - 修改：`receipt_folder.py` 把票据命名空间从仅用户名收口为租户与用户联合边界，并用稳定摘要消除有损清洗碰撞；`attachment_association_jobs.py` 同时校验任务租户和 owner，平台管理员也不能跨租户读取。`expense_receipt_matcher.py` 改用纯只读本人 Claim 查询并要求每份票据独立满足证据门槛。`expense_receipt_association.py` 将 Claim、Case、Link、业务事件、票据元数据和新旧附件目录纳入统一失败补偿，避免事件或文件中途异常留下半关联状态。
  - 操作：新增持久化 `attachment_association_jobs` 模型、仓储和 `20260716_0006` 迁移；任务按租户、owner、票据集合和 generation 去重，运行态使用租约、`attempt_count + running` 栅栏、进程锁和 PostgreSQL advisory lock。同票据与同 Claim 分别串行化，Claim 锁内清理旧事务并重新匹配；待确认或失败任务保留原代审计历史，以新 generation 重新评估，自动关联成功代继续幂等复用。低置信、相近候选、混入无关票据、无草稿和仅有申请统一返回需要确认的零写入结果；同步扩展前端任务协议、会话恢复、候选卡片、幂等重放和成功风险复核提示，并更新功能 CONCEPT/TODO。
  - 验证：所有后端验证均在项目主容器内执行并设置 60 秒超时。后端归集专项 20 项、归集与相邻服务及迁移所有权组合回归 71 项通过，覆盖纯只读确认、逐票据证据、事件失败、文件元数据失败、既有申请 Case 保留、同票据并发、不同票据并发同 Claim、租约过期恢复、旧 worker/迟到回调栅栏、待确认重评估、失败代际历史保留、普通用户及管理员跨租户隔离；前端关联链路组合回归 29 项通过；相关 Python 文件 `ruff --select F,I,UP` 通过；Vite 生产构建通过。一次性 tmpfs PostgreSQL 17 中完整迁移循环 4 项通过，临时数据库已清理，持久开发库未修改。
  - 影响：同用户名在不同租户的票据、任务和候选 Claim 不再互相可见；自动归集中途失败不会留下已改 Claim、孤立 Case/Link/Event、错误票据状态或被覆盖的旧附件目录；低证据和混合票据批次只提示确认，不扩大自动化权限；任务重启或多 worker 后仍可查询和恢复。非默认租户原先落在旧用户名目录中的历史票据改为 fail-closed，不做未经确认的跨目录迁移。
