Files
lisglosips/docs/CDR_MINIMAL_BILLING_RUNBOOK.md

113 lines
3.8 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.
# CDR Minimal Billing Runbook
> 任务:S23 - 最小计费与余额扣减
> 完成时间:2026-06-21 19:55 +08:00
## 1. 目标
S23 在 S22 的 Redis Stream 消费基础上完成最小计费闭环:
- 成功通话 CDR 写入 `raw_cdrs``rated_cdrs`
- 按周期秒数和周期费率计算费用。
- 扣减客户余额,并写入不可变客户余额流水。
- 同一 `event_id` 或同一 `raw_cdr_id` 不重复扣费。
当前系统尚未建立客户侧费率表,因此 S23 使用落地网关的 `billingCycleSec``cycleRate` 同时作为供应商成本和临时客户费用,`grossProfit``0.000000`。正式客户费率和利润计算留给后续计费版本。
## 2. 计费规则
`apps/worker-cdr/src/billing.ts` 提供周期计费函数:
```text
cycles = ceil(duration_sec / billing_cycle_sec)
bill_sec = cycles * billing_cycle_sec
amount = cycles * cycle_rate
```
约束:
- `duration_sec` 必须为非负整数。
- `billing_cycle_sec` 必须为 1 到 60 的整数。
- 金额统一保留 6 位小数。
- `sip_code``2xx``duration_sec > 0` 时才进入计费。
失败 CDR、零时长 CDR、缺少客户或落地网关 ID 的 CDR 只写 `raw_cdrs``ratingStatus` 标记为 `SKIPPED`,不扣余额。
## 3. 写库与幂等
`apps/worker-cdr/src/rating.ts``CdrRatingService` 在单个数据库事务内处理:
1.`raw_cdrs.event_id` 查询是否已处理。
2. 新建 `raw_cdrs`
3. 锁定客户余额行:`SELECT ... FOR UPDATE`
4. 读取落地网关周期费率。
5. 新建 `rated_cdrs`
6. 新建一条负数 `customer_recharges` 作为扣费流水。
7. 更新 `customers.balance`
8.`raw_cdrs.ratingStatus` 标记为 `RATED`
扣费流水约定:
| 字段 | 值 |
| --- | --- |
| `amount` | 负数费用 |
| `idempotencyKey` | `cdr:<event_id>` |
| `remark` | `CDR_CHARGE:<raw_cdr_id>` |
| `createdBy` | `worker-cdr` |
数据库唯一约束共同防止重复扣费:
- `raw_cdrs.event_id`
- `rated_cdrs.raw_cdr_id`
- `customer_recharges.idempotencyKey`
## 4. Worker 行为
`apps/worker-cdr/src/main.ts` 现在连接 Redis 和 MySQL
1.`stream:cdr_payload``billing-workers` Consumer Group 读取 CDR。
2. 解析成功后调用 `CdrRatingService.rate(event)`
3. 处理成功或重复消息后 ACK Redis 消息。
4. 处理异常时沿用 S22 死信机制写入 `stream:cdr_deadletter`
必需环境变量:
```text
REDIS_URL
DATABASE_URL
```
可选环境变量:
```text
CDR_CONSUMER_NAME
CDR_BLOCK_MS
CDR_BATCH_SIZE
CDR_PENDING_IDLE_MS
LISGLOSIPS_LOG_LEVEL
```
## 5. 验证结果
- `corepack pnpm@10.33.0 exec vitest run apps/worker-cdr/src/rating.spec.ts` 通过,3 条测试覆盖周期计费、成功 CDR 扣费幂等、失败 CDR 不扣费。
- `corepack pnpm@10.33.0 --filter @lisglosips/worker-cdr build` 通过。
- `corepack pnpm@10.33.0 typecheck` 通过。
- `corepack pnpm@10.33.0 lint` 通过。
- `corepack pnpm@10.33.0 build` 通过。
本次未连接 B 执行服务部署或真实库写入验证,因为 B sudo 凭据仍未通过校验。S23 仅完成本地代码闭环和构建验收。
## 6. 回滚
代码回滚:
- 恢复 `apps/worker-cdr/src/main.ts` 到 S22 只记录 CDR 的版本。
- 删除或恢复 `apps/worker-cdr/src/billing.ts``apps/worker-cdr/src/rating.ts``apps/worker-cdr/src/rating.spec.ts`
- 恢复 `apps/worker-cdr/package.json``apps/worker-cdr/tsconfig.json`
- 重新执行 `corepack pnpm@10.33.0 build`
数据回滚:
- S23 未执行真实数据库写入验证,无需清理远端数据。
- 若后续环境已运行 worker 并产生扣费数据,回滚前必须先停止 worker,记录待回滚的 `event_id``raw_cdr_id``rated_cdr_id` 和客户流水;不得直接删除余额流水,应通过受控反向流水或备份恢复方案处理。