外呼任务生命周期
本文说明 callout-server 从任务调度到通话终态的完整边界。实现位于 src/services/campaign/outbound-task/;外部只使用两个入口——dispatch.ts 的 dispatchCampaignBatch(手动派发由 src/routes/campaign-routes.ts 调用)与 scheduler.ts 的 runCalloutScheduler(后台调度器,由 src/workers/index.ts 定时调用)。
入口与执行时序
- 启动入口:管理端先调用
POST /api/campaigns/preflight预检;POST /api/campaigns/status-update执行action=start时读取任务、按统一工作量口径(getCampaignWorkSummary)复核一次, 并重新取服务端时间校验计划结束时间,不满足则返回 409(select + 汇总 + update,无事务与行锁)。 - 自动入口:后台 scheduler 扫描
campaigns.status=running的任务,先刷新完成度,再检查计划窗口、 策略、工作时段、调度间隔和当前可派发联系人后派发。 - 手动入口:
POST /api/campaigns/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 提交结果由提交阶段回写更新。
领取、容量与提交边界
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。
启动、等待与完成边界
POST /api/campaigns/status-update执行action=start(切换为running)时,必须存在pending、未耗尽重试次数的failed,或dialing/submitted活跃联系人;否则返回 HTTP 409, 状态保持不变。POST /api/campaigns/update只保存任务配置,不做启动校验。draft、paused、completed和running均可执行启动预检;archived不可启动。completed任务只有在后来新增待处理联系人后才能重新启动。- 允许在
scheduledStartAt之前启动,任务以running等待计划开始。预检的scheduled_window检查项只在设置了scheduledEndAt且服务端当前时间大于或等于结束时间时失败;实际启动接口 复用该结束时间判断,窗口已到期时返回 HTTP 409 并保持状态不变,即使此前预检通过也不能启动。 - 派发计划窗口采用服务端时间的
[scheduledStartAt, scheduledEndAt),未设置的边界不限制。 开始前或结束时及之后,调度器正常跳过派发,不调用派发入口、不创建批次、不记录排期错误事件。 开始时间到达后按现有调度周期,在策略、工作时段、间隔和容量允许时派发;领取事务再次校验 两个时间边界。计划结束仅限制新派发,已建立通话沿用通话生命周期收尾。 - 完成条件固定为
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 到期后再次领取。
模块导航
| 文件 | 职责 |
|---|---|
scheduler.ts | running campaign 扫描、工作时段与间隔 |
dispatch.ts | Redis 锁及领取、提交、收尾编排 |
claim.ts | 短事务领取与 PreparedAttempt 创建 |
capacity.ts | cell、线路、号码池和监控容量 |
originate.ts | FreeSWITCH 网关、提交结果和持久化 |
progress.ts | campaign 工作量与完成度统一统计 |
state.ts | attempt/contact 状态补丁与 payload 合并 |