跳到正文

外呼任务生命周期 ​

本文说明 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 提交结果由提交阶段回写更新。

领取、容量与提交边界 ​

  1. claim.ts 在一个短事务内校验任务状态、计划时间、策略、线路和 cell,计算任务/线路/ 号码池/cell/严格监控容量。
  2. 可派发联系人包括 pending,以及未耗尽重试次数且 nextRetryAt 已到的 failed。
  3. 事务使用 FOR UPDATE SKIP LOCKED 领取联系人,写入 dialing,创建 batch 和 attempt,返回 PreparedAttempt[] 后立即提交事务。
  4. originate.ts 在事务外并行调用本批所有 OutboundCallGateway;批次数量已经受请求上限、 任务、线路/号码池、cell 和严格盯屏容量共同裁剪。每路结果独立落库并原子增加 batch 的 submittedCount / failedCount,全部完成后再以汇总结果校准最终计数,避免长事务包围网络调用。
  5. 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-resultscallflow-esl 正常完成业务通话状态、时间、录音、转写、业务结果、DNC/情绪分析标记刷新 campaign 完成度
FreeSWITCH 通道事件接通或挂机事件先到、业务未回写answered/completed/no_answer/busy/failed 与挂机原因、时长挂机终态后刷新完成度
生命周期超时对账contact 长期卡在 dialing/submittedattempt/contact 标记失败并安排重试按受影响 campaign 刷新完成度

三条路径都应幂等地更新同一 attempt/contact。业务层 payload.result 与 originate 快照合并,不能 覆盖 outboundPayload。如果某次失败仍满足重试条件,campaign 保持 running,等待 nextRetryAt 到期后再次领取。

模块导航 ​

文件职责
scheduler.tsrunning campaign 扫描、工作时段与间隔
dispatch.tsRedis 锁及领取、提交、收尾编排
claim.ts短事务领取与 PreparedAttempt 创建
capacity.tscell、线路、号码池和监控容量
originate.tsFreeSWITCH 网关、提交结果和持久化
progress.tscampaign 工作量与完成度统一统计
state.tsattempt/contact 状态补丁与 payload 合并

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。