feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# 商业资源权威边界闭环 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
只对已经通过可信边界且随业务事务成功持久化的连接器事件和附件源文件写入计量,并在任何拒绝、失败或回滚时先释放额度、恢复文件,不生成虚假用量。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
商业底座已经具备套餐、权益、硬配额、执行前预占和追加式用量/成本事实,但真实资源入口仍存在两处断点:
|
||||
|
||||
- 金融连接器能验证 HMAC、阻断重放与 payload 冲突并持久化事件,却没有把“首次接受且提交成功”接到 `events` 权威计量。
|
||||
- 报销附件会在上传开始时直接删除旧目录再写新文件。若配额在写入后才判断,旧文件已经遭到破坏;若数据库随后回滚,文件系统和商业事实还可能与业务记录分叉。
|
||||
- 独立商业 Session 不能提交调用方业务 Session。反过来,商业结算也不能早于业务提交,否则数据库回滚后仍会留下客户用量。
|
||||
- SQLite 单连接测试环境中,另开 Session 检查“未配置计量器”会回滚同一底层连接上的业务事务;未配置路径必须先在调用方 Session 只读短路。
|
||||
|
||||
因此,本能力把“预占—业务变更—事务终态—结算/释放”定义为统一资源协议,而不是在端点成功返回前后零散补记用量。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 金融连接器只对新建、已认证、已提交的 `FinancialConnectorEvent` 计量一个 `events`。
|
||||
- [G2] 鉴权失败、稳定重放、payload 冲突、配额拒绝和业务事务回滚均不产生连接器用量。
|
||||
- [G3] 附件上传按源文件 `bytes` 在覆盖旧文件前执行硬配额预占。
|
||||
- [G4] 附件业务事务回滚时恢复旧文件树、删除未提交的新文件并释放预占;提交后才追加用量。
|
||||
- [G5] 删除单附件、费用明细和整张报销单时,把文件删除绑定到数据库事务;回滚恢复、提交后最终清理。
|
||||
- [G6] 商业事实只保存哈希化操作身份、固定工具维度、数量和安全来源,不保存外部事件原文、关联号、单号、文件名或文件正文。
|
||||
- [G7] 同一已释放操作允许在相同账期重新预占,支持业务回滚后的安全重试。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 本切片不把附件 `bytes` 定义为实时磁盘容量;它表示成功持久化的源文件写入流量。
|
||||
- [NG2] 删除附件不追加正向用量,也不自动冲销历史写入事实;容量型 GB-month 计费需要独立快照账本。
|
||||
- [NG3] 不对连接器重放、鉴权失败和冲突运营事件收费。
|
||||
- [NG4] 不在商业事实中复制金融 payload、OCR 文本、文件名、报销事由或客户外部引用。
|
||||
- [NG5] 不在本切片新增商业配置页面或改变套餐价格。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 企业员工上传或替换报销附件:平台先检查源文件字节配额,再覆盖文件;数据库提交后才形成用量。
|
||||
- 企业员工删除附件、费用明细或草稿报销单:删除操作不被配额阻止,数据库失败时文件恢复。
|
||||
- 金融连接器发送回执:只有首次通过签名验证、去重并持久化的事件消费一个连接器事件额度。
|
||||
- 连接器因网络重试再次发送相同事件:返回既有业务响应,不重复预占或计量。
|
||||
- 平台运营排查账单:能看到 `connector/financial.ingest/events` 或 `storage/attachment.upload/bytes`,但看不到业务原文和敏感标识。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 连接器权威观察器:认证和首次事件判定之后预占,业务事务 `after_commit` 后结算。
|
||||
- 附件权威观察器:固定使用 `storage + attachment.upload + bytes`,以 `len(content)` 作为执行前与执行后的同一权威数量。
|
||||
- 事务回调批次:同一 Session 可登记多个资源操作;提交按登记顺序结算,回滚按相反顺序补偿。
|
||||
- 文件暂存事务:旧文件或目录先原子重命名为同目录隐藏备份;提交删除备份,回滚删除新目录并恢复原路径。
|
||||
- 删除事务:单附件、费用明细附件和整张报销单附件树均先暂存,数据库提交后才最终删除。
|
||||
- 已释放预占重开:相同指纹、相同订阅/权益/账期的 `released` 预占可重新进入 `reserved`,并重新执行硬配额判断。
|
||||
- 未配置兼容:调用方 Session 先只读确认没有匹配计量器,直接兼容执行且不创建商业事实。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端
|
||||
|
||||
- 当前不新增页面。
|
||||
- 附件上传端点接受可选 `X-Request-ID`,商业硬配额拒绝返回 HTTP 429;现有 400/404 业务错误保持不变。
|
||||
- 客户端应为同一次上传重试复用 `X-Request-ID`。未提供时,服务端用认证会话、租户、Claim/Item 哈希和内容摘要形成保守幂等身份。
|
||||
|
||||
### 后端
|
||||
|
||||
- `FinancialConnectorCommercialObserver` 使用认证后配置租户、配置摘要和请求指纹构造哈希身份,固定工具维度为 `connector/financial.ingest`。
|
||||
- `FinancialConnectorIngestionService` 先完成认证、外部事件串行化和既有事实判断;只有新事件才申请预占。事件 flush 成功后把完成/释放绑定到调用方 Session。
|
||||
- `CommercialTransactionCallbacks` 把多个独立商业操作绑定到一个业务事务。`after_commit` 执行完成回调,`after_rollback` 逆序执行补偿回调;回调异常只记录日志并保留可补偿预占,不伪装业务提交失败。
|
||||
- `ExpenseClaimAttachmentCommercialObserver` 固定使用 `bytes`,配额拒绝发生在任何旧文件移动、`rmtree`、`unlink` 或新文件写入之前。
|
||||
- `ExpenseClaimAttachmentFileTransaction` 负责旧路径暂存、提交清理和回滚恢复;上传、删除附件、删除费用明细和删除报销单复用同一协议。
|
||||
- `CommercialRuntimeReservationService` 允许同一指纹的 `released` 预占在相同账期重新预占;重新检查当前合同、计量器快照和硬配额。
|
||||
- OCR 与 RuntimeChat 的直接商业桥同样增加调用方 Session 的只读未配置短路,避免 SQLite 单连接环境的独立 Session 回滚调用方事务。
|
||||
|
||||
### 算法/规则
|
||||
|
||||
- 连接器的权威数量恒为一个已提交事件:`actual_events = 1`。
|
||||
- 附件的权威数量为请求中源文件真实字节数:`actual_bytes = len(content)`。
|
||||
- 连接器只允许 `quantity_basis=events`;附件只允许 `quantity_basis=bytes`。错误基准在预占前失败关闭。
|
||||
- 同一业务事务包含多个附件操作时,回滚补偿使用 LIFO,确保同一 Item 连续替换也能恢复到事务开始前状态。
|
||||
|
||||
### 数据
|
||||
|
||||
- 不新增数据库表和迁移。
|
||||
- 复用 `commercial_runtime_reservations`、`usage_meter_events` 和可选 `commercial_cost_events`。
|
||||
- 连接器用量 metadata 只包含固定 meter 版本、哈希化 operation call、固定工具名、数量基准、权益、预占、provider 代码、结果和安全来源。
|
||||
- 附件用量不包含文件名、storage key、Claim/Item 原值、MIME、OCR 文本或文件正文。
|
||||
|
||||
### 权限
|
||||
|
||||
- 连接器租户只来自 HMAC 认证后配置,不能由未验证 header 或 payload 单独决定。
|
||||
- 附件租户只来自 `CurrentUserContext.tenant_id`,Claim 和 Item 已先通过费用单租户访问策略校验。
|
||||
- 付费或配额状态不能放宽现有报销状态、附件可变性、连接器签名、租户或财务对账规则。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 没有匹配计量器:保持原业务兼容,不写预占、用量或成本。
|
||||
- 商业配置错误、重复计量器、错误数量基准或硬配额不足:在业务/文件副作用前拒绝。
|
||||
- 业务事务回滚:释放预占;连接器不留事件用量,附件恢复原文件。
|
||||
- 商业完成回调异常:业务提交不反转;预占保持可审计状态,由既有补偿流程处理。
|
||||
- 文件恢复失败:记录错误并保留隐藏备份,不把失败删除误报为成功;需要运维根据事务日志和隐藏路径人工恢复。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 硬配额判断
|
||||
|
||||
```text
|
||||
allowed = used_quantity + held_quantity + requested_quantity <= hard_limit
|
||||
```
|
||||
|
||||
- 连接器 `requested_quantity = 1 event`。
|
||||
- 附件 `requested_quantity = len(content) bytes`。
|
||||
- 判断在订阅与权益锁内完成;拒绝时真实连接器处理和附件破坏性操作尚未发生。
|
||||
|
||||
### 业务事务终态
|
||||
|
||||
```text
|
||||
permit -> business mutation -> commit -> usage(actual_quantity)
|
||||
-> rollback -> release reservation + restore files
|
||||
```
|
||||
|
||||
### 附件写入计量口径
|
||||
|
||||
```text
|
||||
billable_attachment_bytes = Σ len(source_content)
|
||||
```
|
||||
|
||||
- 只汇总成功随业务事务提交的源文件写入。
|
||||
- OCR 临时文件、预览图、metadata 和隐藏事务备份不进入本 meter。
|
||||
- 删除不产生负数;实时容量需另建周期快照和保留量口径。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 连接器:首次提交、稳定重放、payload 冲突、签名失败、业务回滚、回滚后重试、硬配额拒绝和敏感 metadata 反向断言。
|
||||
- 附件商业:bytes 提交、回滚释放、错误 basis、硬配额拒绝发生在旧文件移动前、相同事务连续替换逆序恢复。
|
||||
- 附件文件:单附件删除、费用明细删除、整单删除的 commit/rollback 文件终态。
|
||||
- 既有回归:连接器服务/端点/配置生命周期、附件归集任务、票据夹、报销端点、附件专项、OCR 与 RuntimeChat。
|
||||
- 质量:相关 Python 文件 Ruff;核心服务、Mixin 和端点继续低于 800 行。
|
||||
- 所有后端测试只在 `local-x-financial-linux` 容器内执行,单命令超时不超过 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- [A1] 每个首次提交的连接器事件最多一个 `events` 用量;重放、鉴权失败和冲突为零。
|
||||
- [A2] 连接器或附件业务回滚后 `usage_meter_events` 为零,预占为 `released`,相同操作可重新预占。
|
||||
- [A3] 附件配额拒绝时原文件内容和路径完全不变。
|
||||
- [A4] 附件数据库回滚后原文件恢复;提交后新文件/删除结果与数据库一致。
|
||||
- [A5] 用量 metadata 中不存在 external event ID、correlation ID、报销单号、文件名或业务正文。
|
||||
- [A6] 连接器、附件归集、报销端点、OCR、RuntimeChat 和商业直接运行相关回归在容器内通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 本地文件系统与数据库不是分布式事务。进程在文件暂存后、事务终态回调前被强杀时,隐藏备份可能需要启动恢复器扫描;当前正常异常和显式 commit/rollback 已闭环。
|
||||
- `X-Request-ID` 当前为可选。缺失时采用内容绑定的保守幂等身份,优先避免客户端网络重试重复收费;若未来按每次写请求收费,应把请求 ID 升级为必填并为业务上传建立持久幂等记录。
|
||||
- `bytes` 是源文件成功写入流量,不等于实时保留容量。若商业模式采用存储包或 GB-month,需要新增周期容量快照、删除后容量释放和归档层级计价。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成连接器首次接受事件的 `events` 预占—提交后结算,并确保重放、鉴权失败、payload 冲突、配额拒绝和回滚不计量。
|
||||
- 2026-07-17:完成附件源文件 `bytes` 执行前硬配额、事务文件暂存、上传/删除/费用明细删除/整单删除的提交与回滚闭环。
|
||||
- 2026-07-17:完成事务回调批次逆序补偿、未配置计量器调用方 Session 短路和已释放预占同账期安全重开。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 商业资源权威边界闭环 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 任务边界、计量口径和安全约束以 CONCEPT 对应章节为准。
|
||||
- `[x]` 只表示已有代码或容器验证证据;没有证据不得勾选。
|
||||
- 所有后端测试必须在 `local-x-financial-linux` 容器内运行,单命令最长 60 秒。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点连接器认证、重放、冲突、业务 commit 和 operational event 边界。证据:`financial_connector_ingestion.py`、`financial_connectors.py` 与连接器服务/端点测试。
|
||||
- [x] [CONCEPT: 背景与问题] 盘点附件上传覆盖、单附件删除、费用明细删除、整单删除和批量归集事务。证据:`expense_claim_attachment_operations.py`、`expense_claims.py`、`expense_receipt_association.py`。
|
||||
- [x] [CONCEPT: 目标与非目标] 冻结连接器 `events` 与附件源文件写入 `bytes` 两个唯一 meter,不把删除伪装成正向用量。证据:两个商业 observer 的固定工具名和 required basis。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 后端] 定义业务事务提交后结算、回滚释放的回调协议。证据:`commercial_transaction_callbacks.py`。
|
||||
- [x] [CONCEPT: 算法/规则] 定义 `events=1` 与 `bytes=len(content)` 的权威数量,错误 basis 预占前拒绝。证据:`required_quantity_basis` 与资源边界测试。
|
||||
- [x] [CONCEPT: 数据] 定义商业 metadata 脱敏白名单,不复制业务原文与敏感标识。证据:`test_connector_only_meters_new_authenticated_committed_event` 反向断言。
|
||||
|
||||
## 3. 后端实现
|
||||
|
||||
- [x] [CONCEPT: 后端] 连接器在认证和既有事件判断之后预占,事件提交后结算。证据:`financial_connector_commercial.py`、`financial_connector_ingestion.py`。
|
||||
- [x] [CONCEPT: 降级策略] 连接器重放、鉴权失败、冲突和事务回滚不写用量。证据:`test_commercial_resource_boundaries.py`。
|
||||
- [x] [CONCEPT: 后端] 附件在任何旧文件移动或新文件写入前按源文件 bytes 预占。证据:`stage_attachment_replacement()` 调用顺序与配额拒绝测试。
|
||||
- [x] [CONCEPT: 后端] 上传回滚恢复旧目录,提交后清理备份并追加用量。证据:附件 commit/rollback 资源测试。
|
||||
- [x] [CONCEPT: 后端] 单附件、费用明细和整张报销单删除均绑定数据库事务。证据:`stage_attachment_deletion()`、`stage_claim_attachment_deletion()` 与既有删除回归。
|
||||
- [x] [CONCEPT: 后端] 同一 Session 多操作提交顺序结算、回滚逆序恢复。证据:事务批次实现与连续替换 LIFO 测试。
|
||||
- [x] [CONCEPT: 降级策略] 未配置 meter 时在调用方 Session 只读短路,不让独立 SQLite Session 回滚业务。证据:Direct bridge `lookup_session`、附件归集 31 项通过。
|
||||
- [x] [CONCEPT: 后端] 允许相同 released 预占在相同账期重开并重新检查配额。证据:Direct operation released retry 测试与连接器回滚后重试测试。
|
||||
|
||||
## 4. 算法/规则实现
|
||||
|
||||
- [x] [CONCEPT: 硬配额判断] 使用 `used + held + requested <= hard_limit` 的既有锁内配额算法。证据:`CommercialRuntimeReservationService.reserve()` 与资源配额拒绝测试。
|
||||
- [x] [CONCEPT: 附件写入计量口径] 只结算成功持久化的源文件 bytes,不包含 OCR/预览/metadata。证据:observer authoritative quantity 与 metadata。
|
||||
- [x] [CONCEPT: 业务事务终态] 回滚不记 usage/cost,已释放操作可再次 permit。证据:商业直接运行与资源边界回归。
|
||||
|
||||
## 5. 前端实现
|
||||
|
||||
- [x] [CONCEPT: 前端] 上传端点接收 `X-Request-ID` 并把商业拒绝映射为 HTTP 429。证据:`reimbursements.py`。
|
||||
- [x] [CONCEPT: 目标与非目标] 本切片不新增商业或附件页面。证据:CONCEPT 非目标;本轮无前端文件变更。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 连接器首次提交、重放、冲突、鉴权失败、回滚、重试、配额和脱敏通过。证据:容器内 `test_commercial_resource_boundaries.py` 9 项通过;商业资源、Direct、reservation、OCR、RuntimeChat 与连接器组合最终 `63 passed`。
|
||||
- [x] [CONCEPT: 测试方案] 商业 Direct、OCR、RuntimeChat 相关回归通过。证据:容器内组合 33 项通过;新增 released retry 后 Direct + 资源组合 22 项通过;最终商业/连接器组合 63 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 附件归集和票据夹回归通过。证据:容器内与报销端点最终组合 `54 passed`。
|
||||
- [x] [CONCEPT: 测试方案] 报销端点与附件专项通过。证据:容器内报销/归集组合 54 项、Expense Claim attachment `20 passed, 101 deselected`。
|
||||
- [x] [CONCEPT: 测试方案] 连接器既有服务、端点和配置生命周期回归通过。证据:容器内 15 项通过;最终纳入商业/连接器组合 63 项通过。
|
||||
- [x] [CONCEPT: 指标与验收] 相关 Python 文件 Ruff、compileall 通过,核心文件均低于 800 行。证据:容器检查退出码 0;最大相关文件 `expense_claim_attachment_operations.py` 为 787 行。
|
||||
|
||||
## 7. 文档收尾
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 创建 CONCEPT 与分阶段 TODO,记录 meter、事务、降级和验证口径。证据:本目录两份文档。
|
||||
- [x] [CONCEPT: 风险与开放问题] 记录进程强杀恢复、可选请求 ID 和 GB-month 容量计费边界。证据:CONCEPT 风险章节。
|
||||
Reference in New Issue
Block a user