新增 ADR-002、计费流程说明和 0069 增量迁移,建立独立计费状态、结算 Outbox、显式免费与钱包约束。历史成功未扣费任务仅进入人工复核,不执行追扣。\n\n验证:迁移安全验证与 tests/ci/migrations-test.sh 通过。
87 lines
5.4 KiB
Markdown
87 lines
5.4 KiB
Markdown
# ADR-002:计费正确性闭环与即时估价
|
||
|
||
## 状态
|
||
|
||
Accepted
|
||
|
||
## 日期
|
||
|
||
2026-07-20
|
||
|
||
## 背景
|
||
|
||
Gateway 已具备 PostgreSQL 本地钱包、任务冻结、任务扣费和钱包流水,但任务终态与钱包结算分属不同事务。进程在任务成功写入后、扣费前或释放前退出时,可能留下成功未扣费或永久冻结。现有估价只计算首选候选,缺价会退化为普通零价,且文本输出上限和精度规则与正式结算并不完全一致。
|
||
|
||
Comfy 的业务流程证明了三项做法有参考价值:任务状态与计费状态分离;计费操作使用独立令牌和过期接管;参数变化后立即生成只读估价。Gateway 不采用 Comfy 的三十分钟虚拟预占、结算时刷新最新价格、公开估价接口和负余额策略,因为这些做法会削弱本地钱包作为唯一账务事实源的约束。
|
||
|
||
## 决策
|
||
|
||
### 账务事实源与状态
|
||
|
||
Gateway PostgreSQL 本地钱包是唯一账务事实源,不与 Comfy 或其他系统双写。任务状态与计费状态独立:生成成功可以处于 `succeeded + pending`,结算失败不得篡改生成结果。
|
||
|
||
计费状态为:
|
||
|
||
- `not_started`:尚未产生账务动作。
|
||
- `pending`:结算或释放已进入 Outbox。
|
||
- `processing`:Worker 已领取账务动作。
|
||
- `settled`:已幂等扣费并释放冻结。
|
||
- `released`:失败或取消任务的冻结已释放。
|
||
- `retryable_failed`:可自动重试。
|
||
- `manual_review`:上游结果不明或连续失败,需要人工处理。
|
||
- `not_required`:模拟任务、无本地钱包用户或其他明确无需计费的任务。
|
||
|
||
### 统一计价
|
||
|
||
估价、提交冻结和最终账单统一使用 `effective-pricing-v2`。规则优先级为平台模型、平台、基准模型;只接受处于生效期内、状态有效、币种为 `resource` 且计算器受支持的规则。普通零价、缺价或失效规则返回 `503 pricing_unavailable`;只有 `isFree=true` 的显式免费规则允许零费用。
|
||
|
||
金额决策使用 9 位十进制定点数。文本费用按实际 Token 比例计算,不按千 Token 向上取整;视频以五秒为一个单位向上取整。估价对所有有效候选计算并以最大值作为冻结上限,首选候选费用继续作为 `totalAmount`。
|
||
|
||
前端估价是只读提示,不构成价格承诺。正式提交必须重新计价、冻结,并保存不可变计价快照。
|
||
|
||
### 结算闭环
|
||
|
||
成功 Attempt、任务成功结果、最终金额、计价快照和 `settle` Outbox 在同一个数据库事务写入;失败和取消任务原子写入 `release` Outbox。Worker 使用 `FOR UPDATE SKIP LOCKED` 批量领取,锁超时后允许其他实例接管,并在钱包行锁事务中完成幂等扣费或释放、任务计费状态和 Outbox 完成状态。
|
||
|
||
上游提交状态分为 `not_submitted`、`submitting` 和 `response_received`。处于 `submitting` 且结果不明的任务保留冻结并进入人工复核,不自动重试上游。
|
||
|
||
### 请求幂等与执行租约
|
||
|
||
生成接口可选接受单值 `Idempotency-Key`。数据库只保存键摘要以及规范化请求摘要;同键同请求按协议重放,同键不同请求返回 `409 idempotency_key_reused`,流式重复返回 `409 idempotency_stream_replay_unsupported`。日志和审计记录不得包含原始键或完整请求正文。
|
||
|
||
任务由带随机 `execution_token` 的五分钟租约领取,每三十秒续约。运行态更新和终态提交必须匹配令牌,旧 Worker 不得覆盖接管后的新结果。
|
||
|
||
## 账务不变量
|
||
|
||
1. `balance >= frozen_balance >= 0`。
|
||
2. 同一任务和币种最多存在一笔有效扣费。
|
||
3. 一个冻结必须最终被结算消费或释放,不能同时发生两者。
|
||
4. 正式任务缺价时不得调用上游。
|
||
5. 最终扣费只能使用提交时保存的计价快照,不能读取更新后的价格。
|
||
6. 估价不得创建任务、冻结余额或写钱包流水。
|
||
7. 历史成功未扣费任务只标记人工复核,不自动追扣。
|
||
|
||
## 异常矩阵
|
||
|
||
| 发生点 | 任务状态 | 计费状态 | 自动动作 |
|
||
| --- | --- | --- | --- |
|
||
| 计价失败 | 未创建或失败 | `not_started` | 返回 `pricing_unavailable`,不调用上游 |
|
||
| 冻结失败 | 已创建 | `not_started` | 返回余额不足,不调用上游 |
|
||
| 上游提交前失败 | 失败 | `pending` | Outbox 释放冻结 |
|
||
| 上游提交结果不明 | 运行中或失败 | `manual_review` | 保留冻结,禁止自动重试上游 |
|
||
| 上游明确失败 | 失败 | `pending` | Outbox 释放冻结 |
|
||
| 生成成功、结算待执行 | 成功 | `pending` | 返回成功结果,Worker 异步结算 |
|
||
| 结算暂时失败 | 成功 | `retryable_failed` | 指数退避重试 |
|
||
| 结算连续失败 | 成功 | `manual_review` | 管理员审计化重试 |
|
||
|
||
## 历史数据策略
|
||
|
||
迁移只分类,不产生历史补扣:已有任务扣费流水的成功任务标记 `settled`;失败或取消任务的遗留冻结进入释放队列;成功但无扣费流水的生产任务进入 `manual_review`。
|
||
|
||
## 后果
|
||
|
||
- 成功结果和账务暂时失败可以独立呈现,调用方必须读取 `billingStatus`。
|
||
- 计价规则编辑器必须要求显式免费,并在规则不可用时阻止生产生成。
|
||
- 结算具有最终一致性,因此需要积压、延迟、重试和人工复核监控。
|
||
- 数据库增量在回滚时保留;应用切换到 `hold` 后拒绝新的生产任务,同时继续运行结算 Worker。
|