You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Twilio实现顺振呼叫功能及代码报错、坐席追踪问题咨询

问题定位与解决方案

问题1:webhook带GET参数报错的根因与修复

核心错误点如下:

  • 控制器结构错误:multi_call、agent_call对应的判断逻辑直接写在了leadCall方法外部,不属于任何控制器方法,而路由中/outbound指向的outboundMultiCall方法完全不存在,自然触发报错
  • 类未引入:代码中使用了Lead模型,但未在开头加入use App\Lead;声明
  • CSRF校验拦截:Twilio的webhook请求属于外部第三方请求,默认不带Laravel的CSRF令牌,未将对应路由排除CSRF校验会直接返回419错误
  • Twilio号码格式错误:$twilio_number没有带国家码前缀,不符合Twilio号码格式要求

问题2:首接坐席追踪的实现方案

Twilio的<Dial>标签下同时添加多个<Number>时,默认会在第一个坐席接通后自动挂断其余呼叫,不需要额外处理断开逻辑,只需做以下调整即可实现追踪:

  1. 给线索呼叫生成唯一标识,提前和线索ID绑定存入数据库
  2. 每个坐席的statusCallback地址带上对应坐席ID、当前线索ID参数
  3. 在agent_call的回调逻辑中加锁判断当前线索是否已经分配过坐席,首次回调就将坐席信息存入数据库,后续回调直接忽略即可

完整修复代码

控制器代码

<?php

namespace App\Http\Controllers;

use App\Lead;
use App\User;
use Illuminate\Http\Request;
use Twilio\Rest\Client;
use Twilio\TwiML\VoiceResponse;
use Exception;

class TwilioController extends Controller
{
    // 触发线索呼叫的方法
    public function leadCall(Request $request)
    {
        $lead = Lead::where('id', $request->lead_id)->first();
        if (!$lead) {
            return response()->json(['msg' => '线索不存在'], 400);
        }
    
        $country_code = "+1";
        $visitor_phone = $country_code . $lead->details_phone;
        // 传递线索ID到回调
        $url = "https://www.samplewebsite.com/outbound?multi_call=true&lead_id=" . $lead->id;

        // Twilio凭证建议放到.env文件中,不要硬编码
        $AccountSid = env('TWILIO_ACCOUNT_SID', 'xyz');
        $AuthToken = env('TWILIO_AUTH_TOKEN', 'xyz');
        // Twilio号码需要带国家码
        $twilio_number = env('TWILIO_PHONE_NUMBER', '+11234567890');

        $client = new Client($AccountSid, $AuthToken);
        
        try {
            $call = $client->account->calls->create(
                $visitor_phone, 
                $twilio_number,
                ["url" =>  $url]
            );
            // 可选:将本次呼叫Sid和线索ID绑定存入数据库,方便后续追踪
            // LeadCall::create(['lead_id' => $lead->id, 'call_sid' => $call->sid, 'status' => 'calling']);
        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 500);
        }

        return response()->json(['msg' => '呼叫已发起']);
    }

    // 路由指向的outbound回调方法
    public function outboundMultiCall(Request $request)
    {
        // 处理同时拨打坐席逻辑
        if ($request->input('multi_call')) {
            $leadId = $request->input('lead_id');
            // 提前判断线索是否有效,避免无效呼叫
            if (!$leadId || !Lead::find($leadId)) {
                $twiml = new VoiceResponse();
                $twiml->say('线索无效,呼叫已结束', ['voice' => 'alice']);
                return response($twiml, 200)->header('Content-Type', 'text/xml');
            }

            // 筛选10名可接听的坐席
            $users = User::where('live_call', 1)->whereNotNull('phone')->take(10)->get();

            $twiml = new VoiceResponse();
            $twiml->say('Please hold, we are connecting your call to one of our agent.', ['voice' => 'alice']);
            
            $dial = $twiml->dial();
            
            foreach ($users as $user) {
                // 每个坐席的回调带上坐席ID和线索ID
                $callbackUrl = "https://www.samplewebsite.com/outbound?agent_call=true&agent_id=" . $user->id . "&lead_id=" . $leadId;
                $dial->number($user->phone, [
                    'statusCallbackEvent' => 'answered',
                    'statusCallback' => $callbackUrl,
                    'statusCallbackMethod' => 'GET'
                ]);
            }

            return response($twiml, 200)->header('Content-Type', 'text/xml');
        }

        // 处理坐席接通回调逻辑
        if ($request->input('agent_call')) {
            $agentId = $request->input('agent_id');
            $leadId = $request->input('lead_id');

            // 加锁防止并发回调重复写入,判断当前线索是否已经分配过坐席
            \DB::beginTransaction();
            try {
                $lead = Lead::where('id', $leadId)->lockForUpdate()->first();
                // 如果已经分配过坐席,直接返回
                if ($lead->agent_id) {
                    \DB::commit();
                    return response('已分配坐席', 200);
                }

                // 写入坐席信息,更新线索状态
                $lead->agent_id = $agentId;
                $lead->answer_time = now();
                $lead->save();

                // 给对应坐席加评分
                $agent = User::find($agentId);
                $agent->call_score += 1;
                $agent->save();

                \DB::commit();
            } catch (Exception $e) {
                \DB::rollBack();
                \Log::error('坐席分配失败:' . $e->getMessage());
            }

            return response('分配完成', 200);
        }

        // 默认返回挂断
        $twiml = new VoiceResponse();
        $twiml->hangup();
        return response($twiml, 200)->header('Content-Type', 'text/xml');
    }
}

路由调整

routes/web.php修改为:

Route::post('lead-call', 'TwilioController@leadCall')->name('leadCall');
// 兼容Twilio的GET/POST回调请求
Route::any('outbound', 'TwilioController@outboundMultiCall')->name('outbound');

CSRF排除配置

在app/Http/Middleware/VerifyCsrfToken.php的$except数组中添加路由,避免Twilio请求被拦截:

protected $except = [
    'lead-call',
    'outbound',
];

内容的提问来源于stack exchange,提问作者Jonib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 13:54:02