外观
外呼任务生命周期
本文说明 callout-server 从任务调度到通话终态的完整边界。实现入口位于 src/services/outbound-task/;公共域入口只暴露 dispatchCampaignBatch 与 runCalloutScheduler。
入口与执行时序
- 启动入口:管理端把任务状态更新为
running前先调用预检;后端在状态更新事务内再次锁定 任务并校验统一工作量,覆盖并发变化和直接调用 API 的情况。 - 自动入口:后台 scheduler 扫描
campaigns.status=running的任务,先刷新完成度,再检查策略、 工作时段、调度间隔和当前可派发联系人后派发。 - 手动入口:
POST /api/campaigns/:id/dispatch完成权限、参数和审计处理后,调用同一个dispatchCampaignBatch。 - 两个入口都使用 campaign 级 Redis 锁;领取事务还使用 campaign/line 行锁、cell 容量锁和
FOR UPDATE SKIP LOCKED,多实例之间不会重复领取同一联系人。
originate 被 FreeSWITCH 接受只代表提交成功,不代表响铃、接通或业务完成。接通后的通话控制 归 callflow-esl;callout-server 继续保存任务、尝试和最终结果。
四类核心记录
| 记录 | 职责 | 关键生命周期 |
|---|---|---|
campaigns | 任务配置、策略入口、并发上限与整体进度 | draft → running → paused/completed/archived |
contacts | 一个任务内的被叫工作项,也是并发与重试统计真值 | pending → dialing → submitted → completed/failed |
callout_batches | 一次领取和提交循环的汇总、来源与计数 | running → completed/cancelled/failed |
call_attempts | 单个联系人一次 originate 的请求快照和通话结果 | accepted/rejected/error 加独立 callStatus |
当前为兼容历史数据,事务内预创建 attempt 时仍写 status=accepted,即使真正的 originate 尚未 提交。该字段语义后续可通过独立 schema 迁移引入 prepared,不在本次结构重构中改变。
领取、容量与提交边界
claim.ts在一个短事务内校验任务状态、计划时间、策略、线路和 cell,计算任务/线路/ 号码池/cell/严格监控容量。- 可派发联系人包括
pending,以及未耗尽重试次数且nextRetryAt已到的failed。 - 事务使用
FOR UPDATE SKIP LOCKED领取联系人,写入dialing,创建 batch 和 attempt,返回PreparedAttempt[]后立即提交事务。 originate.ts在事务外并行调用本批所有OutboundCallGateway;批次数量已经受请求上限、 任务、线路/号码池、cell 和严格盯屏容量共同裁剪。每路结果独立落库并原子增加 batch 的submittedCount/failedCount,全部完成后再以汇总结果校准最终计数,避免长事务包围网络调用。progress.ts用统一口径统计 pending、active、retryable、dispatchable 和 remaining work, 在无剩余工作且无活跃通话时完成 campaign。
启动、等待与完成边界
PATCH /api/campaigns/:id切换到running时,必须存在pending、未耗尽重试次数的failed,或dialing/submitted活跃联系人;否则返回 HTTP 409,事务整体回滚并保留原状态。draft、paused、completed和running均可执行启动预检;archived不可启动。completed任务只有在后来新增待处理联系人后才能重新启动。- 完成条件固定为
remainingWork === 0 && active === 0。全部成功、全部失败且重试耗尽,或 联系人池为空,都会在 running 任务的下一轮调度扫描中推进为completed。 - 尚未到
nextRetryAt的失败联系人属于 remaining work;dialing/submitted属于 active。 这两类任务继续保持running,当前轮不派发,也不创建空批次。 - 预检中的
pending_contacts检查编码为兼容现有调用方保持不变,但提示同时展示当前可派发、 等待重试和活跃通话数量。
三个终态来源
| 来源 | 典型场景 | 写入内容 | 完成度处理 |
|---|---|---|---|
POST /api/call-results | callflow-esl 正常完成业务 | 通话状态、时间、录音、转写、业务结果、DNC/情绪分析标记 | 刷新 campaign 完成度 |
| FreeSWITCH 通道事件 | 接通或挂机事件先到、业务未回写 | answered/completed/no_answer/busy/failed 与挂机原因、时长 | 挂机终态后刷新完成度 |
| 生命周期超时对账 | contact 长期卡在 dialing/submitted | attempt/contact 标记失败并安排重试 | 按受影响 campaign 刷新完成度 |
三条路径都应幂等地更新同一 attempt/contact。业务层 payload.result 与 originate 快照合并,不能 覆盖 outboundPayload。如果某次失败仍满足重试条件,campaign 保持 running,等待 nextRetryAt 到期后再次领取。
模块导航
| 文件 | 职责 |
|---|---|
index.ts | 外呼任务域唯一公共入口 |
scheduler.ts | running campaign 扫描、工作时段与间隔 |
dispatch.ts | Redis 锁及领取、提交、收尾编排 |
claim.ts | 短事务领取与 PreparedAttempt 创建 |
capacity.ts | cell、线路、号码池和监控容量 |
originate.ts | FreeSWITCH 网关、提交结果和持久化 |
progress.ts | campaign 工作量与完成度统一统计 |
state.ts | attempt/contact 状态补丁与 payload 合并 |