在实现了使用VOIP客户端拨打8000号码后,播报语音验证码的功能后,我发现了一个最大的缺点,就是这需要用户主动去操作。这对于想使用API集成无法实现。
在思考之后,我决定使用一个可以调用API就呼叫VOIP客户端,当用户接通后,播报语音验证码的功能。当实现这个功能后,它的好处是显而易见的。比如,可以集成到嵌入式,集成到第三方网站。
那么该如何实现它呢?
一、 系统原理
传统的拨号方案(Dialplan)是静态的,而 ARI 允许我们动态控制。整个“API 触发呼叫并播报”的流程如下:
- 触发阶段:第三方系统通过 API 向 Go 服务发送呼叫请求(包含目标 ID 和验证码)。
- 呼叫发起(Originate):Go 服务调用 Asterisk ARI 的 /channels 接口。此时 Asterisk 会尝试向 PJSIP 终端(或通过中继向手机)发起呼叫。
- 接通监听(Stasis Start):一旦用户接起电话,该通道会被移交给一个名为 Stasis 的应用。此时 Go 服务会收到一个“通道已接通”的 WebSocket 事件。
- 语音合成与播放:Go 服务识别到接通后,调用播报指令(可以播放预录音文件,或对接 TTS 引擎生成的语音流)。
- 挂断处理:播报完毕后,服务发送挂断指令,释放资源。
二、系统架构
[第三方API] --> [Go 后端服务] --(REST API)--> [Asterisk ARI]
| |
(WebSocket) (PJSIP/IMS)
| |
[接通状态回调] <--- [用户终端接听]
三、 核心代码实现 (Golang)
假设你使用了 GitHub 上的 go-ari 库。
1. 初始化 ARI 客户端
import (
"github.com/v5"
"github.com/v5/client/native"
)
// 连接到 Asterisk ARI
cl, err := native.Connect(&native.Options{
Application: "voice-verify", // 必须与 asterisk.conf 配置一致
Username: "admin",
Password: "password",
URL: "http://localhost:8088/ari",
})
2. 实现呼叫并播报逻辑
这是核心逻辑:接收参数 -> 发起呼叫 -> 监听接通 -> 播放语音。
a. 接收参数
// 呼叫
router.POST("/call", func(c *gin.Context) {
var req arimanager.CallRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
callID, err := ariManager.ExecuteCall(req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{
"callId": callID,
"status": "queued",
})
})
当收到参数时,就进行内部呼叫,并返回第三方,目前呼叫已经进入队列。
b. 发起呼叫
func (m *ARIManager) makeARICall(target string, callType CallType, payload map[string]string) (string, error) {
// 构建变量映射
variables := map[string]string{
"CALL_ID": target, // 自定义变量
"CALL_TYPE": string(callType),
}
// 添加 payload 到变量
for key, value := range payload {
variables[fmt.Sprintf("CALL_%s", strings.ToUpper(key))] = value
}
// 发起 ARI 呼叫
channel, err := m.client.Channel().Originate(nil, ari.OriginateRequest{
Endpoint: fmt.Sprintf("PJSIP/%s", target),
App: m.client.ApplicationName(),
Extension: "s",
Context: "from-ari",
Timeout: 30, // 超时时间(秒)
CallerID: fmt.Sprintf("\"Call System\" <%s>", m.client.ApplicationName()),
Variables: variables,
})
if err != nil {
return "", fmt.Errorf("ARI originate failed: %v", err)
}
return channel.ID(), nil
}
这里执行真正的呼叫,它会查找Context上下文,并进入到对应拨号计划中,将上面的信息塞入通道。
c. 监听接通
func (m *ARIManager) handleVerification(ctx context.Context, h *ari.ChannelHandle) {
if err := h.Answer(); err != nil {
shared.Log.Error("failed to answer verification call", "error", err)
return
}
// 从变量获取验证码
code, _ := h.GetVariable("CALL_CODE")
// 播放验证码
if err := m.playVerificationCode(ctx, h, code); err != nil {
shared.Log.Error("failed to play verification code", "error", err)
return
}
shared.Log.Info("verification playback completed", "code", code)
}
当在应用中收到事件后,马上进行状态,等待用户接听,用户接听之后,从通道中获取验证码,然后开始调用播放方法。
d. 播放语音
func (m *ARIManager) playVerificationCode(ctx context.Context, h *ari.ChannelHandle, code string) error {
if err := play.Play(ctx, h, play.URI("sound:beep")).Err(); err != nil {
return err
}
// 播放欢迎提示
if err := play.Play(ctx, h, play.URI("sound:vcode/code")).Err(); err != nil {
return err
}
// 逐位播放数字
for _, digit := range code {
digitSound := fmt.Sprintf("sound:vcode/%c", digit)
if err := play.Play(ctx, h, play.URI(digitSound)).Err(); err != nil {
return err
}
// 数字间短暂暂停
// time.Sleep(100 * time.Millisecond)
}
// 播放结束提示
if err := play.Play(ctx, h, play.URI("sound:vcode/thanks")).Err(); err != nil {
return err
}
return nil
}
这是真正播放验证码的地方,它调用系统的play函数将音频流传输给用户。
四、 关键配置 (Asterisk 端)
为了让 Go 代码接管通话,你需要配置 ari.conf 开启接口,并在拨号方案中定义应用。
1. http.conf
[general]
enabled=yes
bindaddr=127.0.0.1
bindport=8088
2. ari.conf
[general]
enabled = yes
pretty = yes
[admin]
type = user
read_only = no
password = password
3. extensions.conf
虽然是 API 触发,但涉及到内部呼转,还需要配置拨号计划,需要从通道中获取值:
[from-ari]
exten => _X.,1,NoOp(External API Call)
same => n,Stasis(voice-verify) ; 进入 Go 监听的应用名
same => n,Hangup()
当没有对应的拨号计划时,就转到这个应用。
五、 这种方式的优势
- 无缝集成:通过标准 HTTP RESTful API,嵌入式设备(ESP8266/ESP32)只需发送一个简单的 POST 请求给 Go 后端,或者接收到MQTT某的指令,Go调用POST请求,就能让指定终端响起电话。
- 高到达率:语音验证码比短信更难被拦截软件屏蔽,且自带“强提醒”属性(响铃)。
- 动态可扩展:
- 本地测试:呼叫 PJSIP/8000 等 VoIP 软电话。
- 生产环境:对接运营商的 SIP 中继 (SIP Trunk),呼叫地址改为 PJSIP/手机号@运营商网关,即可实现全国手机拨打。
- 成本控制:使用开源的 Asterisk,只需支付运营商的分钟数费用,无需支付昂贵的第三方聚合服务费。
六、 总结
通过 Go + ARI + Asterisk 的组合,成功将传统的 VoIP 通信转换为了 通信能力平台 (CPaaS)。这种方案不仅解决了用户主动操作的痛点,更为后续的自动化预警(如嵌入式传感器触发报警电话)打下了坚实的技术基础。