基于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>时,默认会在第一个坐席接通后自动挂断其余呼叫,不需要额外处理断开逻辑,只需做以下调整即可实现追踪:
- 给线索呼叫生成唯一标识,提前和线索ID绑定存入数据库
- 每个坐席的
statusCallback地址带上对应坐席ID、当前线索ID参数 - 在
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
相关产品推荐
相关产品推荐

