Files
story/docs/usage-cost-quota.md
2026-08-29 21:56:51 +08:00

161 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用量、费用与额度开发说明
> 对应数据库迁移:V21
> 对应接口契约:OpenAPI 0.24.0
> 适用范围:本地模拟供应商、客户组织运营工作台和家庭通话详情
## 1. 目标与边界
本模块解决四个问题:按通话接收并去重供应商最终用量;使用当时有效的不可变价格版本核价;向运营人员和家庭成员提供不含供应商内部编号的汇总;在新通话创建前执行客户组织或设备的日/月费用、并发通话和单次时长策略。
当前实现覆盖本地完整业务链,但不把模拟结果扩张为真实供应商验收。供应商账单对账、真实报价同步、汇率换算、异常费用增长告警和真实Agent“完成当前轮后停止”仍需后续外部联调或告警批次完成。
## 2. 核心设计
### 2.1 标准化用量分类
| 分类代码 | 含义 | 示例指标与单位 |
|---|---|---|
| `RTC` | 实时音视频资源 | `connected_minute / minute` |
| `END_TO_END_VOICE` | 端到端语音智能体 | `agent_minute / minute` |
| `ASR` | 语音识别 | `audio_minute / minute` |
| `LLM` | 大模型推理 | `token / token` |
| `TTS` | 语音合成 | `character / character` |
| `STORAGE` | 媒体或结果存储 | `gigabyte_day / gb_day` |
| `POST_PROCESSING` | 通话后整理任务 | `job / job` |
数量以最多六位小数的十进制字符串接收,避免JSON浮点数导致计量漂移。单价和费用均使用`bigint`保存百万分之一货币单位,例如`132450`表示`CNY 0.132450`。
### 2.2 价格版本
- 运营操作人员创建`DRAFT`价格版本,价格正文和全部费率一经创建不可修改。
- 创建人不能发布自己的版本;运营审批人员使用`If-Match`发布。
- 同一客户组织、同一供应商只保留一个`PUBLISHED`版本,旧版本变为`RETIRED`。
- 费用行冻结价格版本、具体费率、数量、单价、费用和币种。后续调价不会重算历史费用。
- 已发布的其他供应商价格和活动额度策略必须使用兼容币种,防止额度统计绕过。
管理端从具体草稿行打开页面内“审批并发布价格版本”确认区,不再使用浏览器输入弹窗。确认区固定价格版本公开编号、供应商、版本号、币种、费率项数、生效时间、创建人和行版本;发布依据必填且最长500字符。提交前必须确认审批人不是创建人,且报价/合同引用、币种、全部费率和生效时间已经复核,并明确旧发布版本会退役、历史费用不会重算。取消、切换客户组织或列表刷新会清除草稿,不发送发布请求;服务端继续执行角色、禁止自批、状态和`If-Match`校验。
### 2.3 用量幂等与核价事务
供应商发送`voice_chat.usage / FINAL`事件。服务端先验证回调签名、时间偏差和字段,再在同一数据库事务内完成:
1. 以`provider + event_id`保存供应商事件摘要;相同编号、不同正文返回冲突。
2. 以客户组织、供应商、来源事件、分类、指标和单位建立用量唯一键。
3. 完全相同的重复用量读取已有费用行,不产生第二次费用。
4. 按事件发生时间选择已经发布且生效的价格版本和精确费率。
5. 使用数据库十进制运算计算并四舍五入为整数微单位。
6. 同时提交供应商事件、用量事件和冻结费用行;任一步失败均整体回滚。
供应商原始事件表只保存正文摘要和`usage_count`,不复制用量明细或业务正文。
### 2.4 额度策略
额度策略按`TENANT`或`DEVICE`范围版本化保存;同一范围的新版本激活时,原活动版本退役。策略包含:
- 账期时区和币种;
- 日费用上限、月费用上限;
- 最大同时通话数;
- 单次最长通话时长;
- `STOP_IMMEDIATELY`或`COMPLETE_CURRENT_TURN`在途处置动作。
创建通话时先使用策略时区计算日/月边界,再分别检查客户组织和设备累计费用及并发数。两级策略同时存在时逐级校验,并冻结更严格的费用及时长边界。达到硬额度返回HTTP 429和稳定错误码`USAGE_QUOTA_EXCEEDED`。
通话创建成功后,策略编号、账期边界、限额、最长时长、处置动作和创建时累计费用写入通话预算快照。后续修改策略不会改写已有通话。达到最长时长时,心跳返回`stop`或`stop_after_turn`;后台恢复扫描会对`STOP_IMMEDIATELY`通话可靠投递停止任务。
## 3. 接口设计
| 方法与路径 | 角色 | 用途 | 关键控制 |
|---|---|---|---|
| `GET /api/v1/ops/price-books` | 运营查看及以上 | 查询价格历史和费率 | 必须选择客户组织 |
| `POST /api/v1/ops/price-books` | 运营操作及以上 | 创建不可变价格草稿 | `Idempotency-Key` |
| `PATCH /api/v1/ops/price-books/{id}/publish` | 运营审批及以上 | 双人发布价格版本 | `If-Match`、禁止自批 |
| `GET /api/v1/ops/quota-policies` | 运营查看及以上 | 查询活动与历史额度 | 客户组织隔离 |
| `POST /api/v1/ops/quota-policies` | 运营审批及以上 | 创建并激活策略版本 | `Idempotency-Key`、事务审计 |
| `GET /api/v1/ops/cost-summary` | 运营查看及以上 | 按时间范围聚合费用 | 最大366天、不返回任务编号 |
| `GET /api/v1/ops/usage-overview` | 运营查看及以上 | 查看整体通话时长与可用费用 | 客户组织授权;可按家庭、设备筛选 |
| `GET /api/v1/households/{id}/usage-overview` | 活动家庭成员 | 查看本家庭通话时长与可用费用 | 服务端反查家庭关系;只能筛选本家庭设备 |
| `GET /api/v1/conversations/{id}/usage` | 活动家庭成员 | 查看单次通话用量和费用 | 服务端反查家庭关系 |
| `POST /callbacks/v1/vertc/voice-chat` | 签名供应商 | 接收`voice_chat.usage` | HMAC、时间窗、事件幂等 |
| `POST /device/v2/conversations` | 签名设备 | 创建通话并冻结预算 | 创建前统一额度检查 |
所有Web响应使用统一`request_id + data`信封。运营和家庭汇总只返回标准化分类、指标、数量、单位、费用和币种,不返回供应商任务、供应商事件、内部价格版本或费率编号。
通话用量概览以平台已经持久化的`conversation.started_at / ended_at`计算时长,因此不依赖供应商账单接口。费用只有在最终用量已经核价并生成`conversation_cost_line`时才标记为可用;前端默认展示通话时长,在当前筛选范围存在费用行时开放费用视图。家庭端的家庭编号取自URL,但服务端仍以登录身份的活动家庭成员关系重新授权;传入其他家庭或其他家庭设备都会被拒绝。运营端先验证所选客户组织的运营账号,再将家庭、设备筛选限制在该客户组织内。
## 4. 数据库设计
| 表 | 作用 | 关键约束 |
|---|---|---|
| `billing_price_book` | 供应商价格版本 | 租户+供应商+版本唯一;单一发布版 |
| `billing_price_rate` | 价格版本下的费率 | 价格版本+分类+指标+单位唯一 |
| `usage_quota_policy` | 客户组织/设备额度版本 | 每范围单一活动版本;时区和币种受校验 |
| `conversation_usage_event` | 标准化最终用量 | 来源事件和计量维度唯一;通话删除级联 |
| `conversation_cost_line` | 不可变费用行 | 每用量事件一条;冻结版本、单价和费用 |
| `conversation_usage_budget` | 通话预算快照 | 每通话一条;随通话删除级联 |
金额不使用浮点字段,币种使用三位大写代码,数量使用`numeric(24,6)`。费用行对价格版本和费率使用外键,保证历史可追溯;主体隐私删除时,用量、费用和预算随通话级联清理。
## 5. 业务流程
### 5.1 价格发布与用量入账
```text
运营创建价格草稿
→ 另一名审批人复核并发布
→ 供应商发送最终用量签名回调
→ 事件编号与正文摘要去重
→ 标准化分类/指标/单位校验
→ 按发生时间命中价格版本与费率
→ 冻结用量、单价和费用
→ 运营汇总与家庭通话详情可见
```
### 5.2 新通话额度检查
```text
设备提交签名创建请求
→ 读取客户组织与设备活动策略
→ 按策略时区计算日/月账期
→ 校验累计费用
→ 校验同时通话数
├─ 达到任一硬边界:429 / USAGE_QUOTA_EXCEEDED
└─ 未达到:创建通话并冻结预算快照
```
### 5.3 通话时长处置
```text
通话持续运行
→ 当前时间超过冻结的最长时长
├─ STOP_IMMEDIATELY:心跳返回stop,Worker可靠投递停止
└─ COMPLETE_CURRENT_TURN:心跳返回stop_after_turn
```
`COMPLETE_CURRENT_TURN`目前已经形成设备动作契约,但真实供应商/Agent对“轮次结束”的确认和可靠停止仍属于外部联调项。
## 6. 错误与安全处理
- 缺少费率或价格尚未发布:整个回调事务回滚,供应商可重试。
- 同一供应商事件编号对应不同正文:HTTP 409 `PROVIDER_EVENT_CONFLICT`。
- 达到费用或并发硬额度:HTTP 429 `USAGE_QUOTA_EXCEEDED`。
- 价格创建人自批:HTTP 409 `SELF_APPROVAL_FORBIDDEN`。
- `If-Match`过期:HTTP 412 `VERSION_CONFLICT`。
- 跨客户组织、非家庭成员或角色不足:HTTP 403,不泄露资源是否存在。
- 回调处理内部失败记录结构化服务端错误,但客户端只得到可重试通用错误,不返回SQL或内部资源信息。
## 7. 验证证据
`scripts/smoke-billing.ps1`使用真实本地OIDC、设备HMAC、供应商回调HMAC和PostgreSQL验证:租户隔离、价格草稿幂等、禁止自批、双人发布、七类最终用量、重复计费去重、同编号异文冲突、冻结总费用、运营汇总脱敏、家庭汇总脱敏、预算快照和硬额度429。脚本结束后清理全部合成价格、额度、通话、用量、费用、审计和幂等记录。
V21已完成单版本`down 1`和重新`up`,最终为`21|false`。实际Chrome 1440×1100检查了运营用量费用工作台,未发现失败请求、控制台错误或横向溢出;截图保存在`test/e2e/screenshots/operations/ops-billing-workspace.png`。
价格发布页面增量按低强度策略复用真实OIDC登录,只使用一条由另一运营账号创建的临时草稿检查目标/行版本绑定、空原因阻断、双人审批影响确认、取消关闭以及零发布PATCH;同时检查零横向溢出、零控制台错误和零非预期失败请求。脚本为`test/e2e/visual-ops-billing-publish.mjs`,截图为`test/e2e/screenshots/ops-billing-price-publish.png`,价格及费率夹具在`finally`中精确清理。该无提交检查用于证明页面不会误发布,不替代既有服务端双人审批纵向烟测。
## 8. 后续工作
1. 在告警批次增加费用异常增长、额度临界、核价失败和供应商用量迟到告警及处置台账。
2. 对接真实供应商账单接口,建立供应商账单与平台费用行的日对账和差异处理。
3. 在真实Agent能力明确后完成`stop_after_turn`可靠确认与超时降级。
4. 在类生产环境执行高并发回调、活动通话和大时间范围汇总压测。