Files
story/docs/usage-cost-quota.md
T

161 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
# 用量、费用与额度开发说明
> 对应数据库迁移: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天、不返回任务编号 |
2026-08-29 21:56:51 +08:00
| `GET /api/v1/ops/usage-overview` | 运营查看及以上 | 查看整体通话时长与可用费用 | 客户组织授权;可按家庭、设备筛选 |
| `GET /api/v1/households/{id}/usage-overview` | 活动家庭成员 | 查看本家庭通话时长与可用费用 | 服务端反查家庭关系;只能筛选本家庭设备 |
2026-08-26 06:41:07 +08:00
| `GET /api/v1/conversations/{id}/usage` | 活动家庭成员 | 查看单次通话用量和费用 | 服务端反查家庭关系 |
| `POST /callbacks/v1/vertc/voice-chat` | 签名供应商 | 接收`voice_chat.usage` | HMAC、时间窗、事件幂等 |
| `POST /device/v2/conversations` | 签名设备 | 创建通话并冻结预算 | 创建前统一额度检查 |
所有Web响应使用统一`request_id + data`信封。运营和家庭汇总只返回标准化分类、指标、数量、单位、费用和币种,不返回供应商任务、供应商事件、内部价格版本或费率编号。
2026-08-29 21:56:51 +08:00
通话用量概览以平台已经持久化的`conversation.started_at / ended_at`计算时长,因此不依赖供应商账单接口。费用只有在最终用量已经核价并生成`conversation_cost_line`时才标记为可用;前端默认展示通话时长,在当前筛选范围存在费用行时开放费用视图。家庭端的家庭编号取自URL,但服务端仍以登录身份的活动家庭成员关系重新授权;传入其他家庭或其他家庭设备都会被拒绝。运营端先验证所选客户组织的运营账号,再将家庭、设备筛选限制在该客户组织内。
2026-08-26 06:41:07 +08:00
## 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. 在类生产环境执行高并发回调、活动通话和大时间范围汇总压测。