# 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:` | | `remark` | `CDR_CHARGE:` | | `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` 和客户流水;不得直接删除余额流水,应通过受控反向流水或备份恢复方案处理。