Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

OutboundCall 外呼

外呼域完整手册——campaign 与 per-call 两种实现模型、CampaignDialer 编排引擎、境内/境外号码分流、节流与暂停/恢复/停止状态机、7 家厂商对照表。

Last updated

OutboundCall 域统一了 7 家外呼供应商。和其他域最大的不同在于:它的抽象包除了接口,还内含一套 provider-agnostic 的拨号编排引擎(CampaignDialer / CampaignRuntime)。原因是国内厂商走”预测式外呼”的服务端活动模型,国际厂商只提供 per-call 单次呼叫 API,需要客户端自己编排节流、暂停/恢复/停止。

本页是完整手册,从两种实现模型到编排引擎内部机制,从境内/境外号码分流到活动状态机。

域结构

Bitzsoft.Integrations.OutboundCall ← 抽象层(接口 + 编排引擎)
├── Bitzsoft.Integrations.OutboundCall.Tencent ← 腾讯云(campaign 模型)
├── Bitzsoft.Integrations.OutboundCall.Aliyun ← 阿里云(campaign 模型)
├── Bitzsoft.Integrations.OutboundCall.Volcengine ← 火山引擎(campaign 模型)
├── Bitzsoft.Integrations.OutboundCall.Yuntongxun ← 容联云通讯(campaign 模型)
├── Bitzsoft.Integrations.OutboundCall.Huawei ← 华为云(campaign 模型)
├── Bitzsoft.Integrations.OutboundCall.Twilio ← Twilio(per-call 模型)
├── Bitzsoft.Integrations.OutboundCall.Vonage ← Vonage(per-call 模型)
└── Bitzsoft.Integrations.OutboundCall.All ← 聚合包

两种实现模型

7 家厂商分两阵营,但都实现同一个 IOutboundCallService,消费者无感知:

  • campaign 模型(腾讯云 / 阿里云 / 火山引擎 / 容联云 / 华为云):供应商服务端管理活动生命周期、号码队列和并发节流。CreateCampaignAsync / StartCampaignAsync 等方法直接调用供应商活动 API。
  • per-call 模型(Twilio / Vonage):供应商只提供单次呼叫 API,没有服务端活动概念。抽象层的 CampaignDialer 在客户端编排队列、节流、暂停/恢复/停止,把 per-call 厂商”伪装”成 campaign 模型。
国际厂商:per-call 模型国内厂商:campaign 模型抽象层

IOutboundCallService
统一接口(9 方法)

CampaignDialer
编排引擎

CampaignRuntime
内存运行态

腾讯云

服务端活动 API
节流·暂停·恢复·停止

阿里云

火山引擎

容联云

华为云

Twilio

Vonage

统一接口(9 方法)

IOutboundCallService 围绕”活动(Campaign)“这一核心概念组织,共 9 个方法:

public interface IOutboundCallService
{
string ProviderName { get; }
// 活动生命周期
Task<CreateCampaignResponse> CreateCampaignAsync(CreateCampaignRequest request, CancellationToken ct = default);
Task<OutboundCallResult> ImportContactsAsync(ImportContactsRequest request, CancellationToken ct = default);
Task<OutboundCallResult> StartCampaignAsync(StartCampaignRequest request, CancellationToken ct = default);
Task<OutboundCallResult> PauseCampaignAsync(string campaignId, CancellationToken ct = default);
Task<OutboundCallResult> ResumeCampaignAsync(string campaignId, CancellationToken ct = default);
Task<OutboundCallResult> StopCampaignAsync(string campaignId, CancellationToken ct = default);
// 查询
Task<List<CampaignInfo>> ListCampaignsAsync(int pageIndex = 1, int pageSize = 20, CancellationToken ct = default);
Task<CampaignInfo> GetCampaignAsync(string campaignId, CancellationToken ct = default);
Task<List<CallRecord>> QueryCallRecordsAsync(QueryCallRecordsRequest request, CancellationToken ct = default);
}

生命周期操作的统一返回是 OutboundCallResult { Success, ErrorMessage?, RequestId? }RequestId 便于在厂商控制台排查。

境内/境外号码分流

CreateCampaignRequest.Region 指定外呼区域,供应商实现据此选择不同的主叫号码池和应用配置:

public class CreateCampaignRequest
{
public PhoneRegion Region { get; set; } = PhoneRegion.Mainland; // 默认中国大陆
// ...
}
Region 值含义供应商使用的配置
Mainland中国大陆(区号 86)Options.Callers + SdkAppId + CnIvrId
Overseas海外(区号非 86)Options.OverseaCallers + OverseaSdkAppId + EnIvrId
Unknown无效号码触发号码清洗告警

号码在导入前会经过 PhoneNumberClassifier 清洗和校验,PhoneRegion.Unknown 的号码会被过滤。

编排引擎:CampaignDialer

CampaignDialer 是 per-call 厂商共用的编排引擎,provider-agnostic:实际呼叫由注入的 CallCreator 委托执行,引擎只管队列推进和状态机。

// 呼叫创建委托——由具体厂商提供(含 429 退避)
public delegate Task<CallAttempt> CallCreator(string callee, CancellationToken cancellationToken);
public sealed class CampaignDialer
{
public Task StartAsync(CampaignRuntime runtime, CallCreator creator, int cps, CancellationTokenSource cts);
public void Pause(CampaignRuntime runtime); // 保留断点
public void Resume(CampaignRuntime runtime, CallCreator creator, int cps); // 从断点继续
public void Stop(CampaignRuntime runtime); // 终态
public async Task WaitAsync(CampaignRuntime runtime); // 测试/同步语义用
}

节流(throttling)

拨号按 cps(calls per second) 串行节流,相邻两次拨号间隔 1000 / cps 毫秒,由 Task.Delay 控制。cps 由供应商配置或调用方传入。

暂停/恢复断点

CampaignRuntime.NextIndex 记录下一个待呼叫联系人的索引。Pause 取消当前拨号循环(通过 CancellationTokenSource.Cancel)但不重置索引Resume 从同一位置继续,不会重复拨打已处理的号码。

代次守卫(Generation)

每次 Start / Resume 自增 CampaignRuntime.Generation,拨号循环发现代次与启动时不一致立即退出。这防止暂停后旧循环和新循环重叠或重复记账——是纵深防御,即使取消信号没及时传播也不会重复拨号。

Resume 时会回收旧 CTS 句柄再建新 CTS,避免多次暂停/恢复累积句柄泄漏。

异常处理

循环内 catch (OperationCanceledException) 单独处理(暂停/停止的正常退出路径),其余异常把活动状态置为 Failed 并记录。CallCreator 委托应观察其 CancellationToken 以便在 Pause/Stop 时及时退出,避免对在途联系人重复拨号。

CampaignRuntime 内存运行态

per-call 厂商的活动元数据和联系人队列在客户端内存里维护(因为 per-call 厂商没有服务端 campaign)。CampaignRuntime 是这个运行态:

字段类型说明
Idstring活动 ID(GUID)
ContactsList<string>被叫号码列表
NextIndexint下一个待呼叫联系人索引(暂停/恢复断点)
StatusCampaignStatus活动状态
Generationint拨号循环代次(防止重叠)
CtsCancellationTokenSource?控制拨号循环的取消令牌
CalledCountint已呼叫数
InitiatedCountint已发起数(获 sid)
CallSidsList<string>已记录的通话 sid 集合

活动状态机

状态含义可流转到
Pending已创建未启动Running
Running运行中Paused / Completed / Stopped / Failed
Paused已暂停(可恢复)Running
Completed全部联系人拨完(终态)
Stopped已终止(终态)
Failed拨号循环异常(终态)
Unknown未知(厂商返回未识别状态)

CampaignDialer.StartAsync 在状态为 RunningPaused 时直接返回(幂等),避免重复启动。

厂商清单

厂商Provider ID实现模型区域专长DI 方法
腾讯云Tencentcampaign境内 + 境外AddTencentOutboundCall()
阿里云Aliyuncampaign境内 + 境外AddAliyunOutboundCall()
火山引擎Volcenginecampaign境内AddVolcengineOutboundCall()
容联云通讯Yuntongxuncampaign境内AddYuntongxunOutboundCall()
华为云Huaweicampaign境内 + 境外AddHuaweiOutboundCall()
TwilioTwilioper-call海外(全球)AddTwilioOutboundCall()
VonageVonageper-call海外(全球)AddVonageOutboundCall()

国内业务优先选 campaign 模型厂商(服务端节流更稳),跨境业务选 Twilio / Vonage(per-call 模型靠 CampaignDialer 编排)。

注册

单厂商

// ① 国内厂商——服务端 campaign 模型
builder.Services.AddTencentOutboundCall(builder.Configuration.GetSection("OutboundCall:Tencent"));
// ② 国际厂商——客户端 per-call 模型(内部用 CampaignDialer 编排)
builder.Services.AddTwilioOutboundCall(builder.Configuration.GetSection("OutboundCall:Twilio"));

全量聚合

builder.Services.AddBitzsoftOutboundCallAll(builder.Configuration, "OutboundCall");

按配置节存在性自动注册:

{
"OutboundCall": {
"Tencent": { "SecretId": "...", "SecretKey": "...", "SdkAppId": "...", "Callers": ["..."] },
"Twilio": { "AccountSid": "...", "AuthToken": "...", "FromNumber": "+1..." }
}
}

消费

完整活动生命周期

public class CampaignManager(IOutboundCallService outbound)
{
public async Task RunCampaignAsync(List<string> contacts)
{
// ① 创建活动(国内厂商在服务端创建,国际厂商在内存创建)
var campaign = await outbound.CreateCampaignAsync(new CreateCampaignRequest
{
Name = "回访活动",
Region = PhoneRegion.Mainland,
Callees = contacts,
MaxAttempts = 2,
});
// ② 导入号码(部分供应商在 CreateCampaign 时已导入)
await outbound.ImportContactsAsync(new ImportContactsRequest
{
CampaignId = campaign.CampaignId,
Contacts = contacts,
});
// ③ 启动
await outbound.StartCampaignAsync(new StartCampaignRequest { CampaignId = campaign.CampaignId });
// ④ 暂停 / 恢复 / 停止操作一致
// await outbound.PauseCampaignAsync(campaign.CampaignId);
// await outbound.ResumeCampaignAsync(campaign.CampaignId);
}
}

查询通话记录

public class CallRecordService(IOutboundCallService outbound)
{
public async Task<List<CallRecord>> GetRecordsAsync(string campaignId)
{
return await outbound.QueryCallRecordsAsync(new QueryCallRecordsRequest
{
CampaignId = campaignId,
StartTime = DateTimeOffset.UtcNow.AddDays(-7),
});
}
}

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%