From 9916633d88396ef6d25b49536442fc48b48bde45 Mon Sep 17 00:00:00 2001 From: "lilong@dgg.net" <123456789> Date: Tue, 13 Apr 2021 19:21:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BE=A4=E5=91=BC=E7=9B=91?= =?UTF-8?q?=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/SwooleCallcenterRun.php | 156 +++++++++++++ app/Console/Commands/SwooleHttp.php | 54 +++++ .../Callcenter/QueueController.php | 9 +- app/Http/Controllers/Callcenter/Task.php | 11 - .../Controllers/Callcenter/TaskController.php | 187 ++++++++++++++++ app/Imports/CallImport.php | 30 +++ app/Models/Call.php | 22 ++ app/Models/Task.php | 63 ++++++ app/Service/Callcenter.php | 123 +++++++++++ config/freeswitch.php | 11 + ...2021_04_02_154147_callcenter_task_call.php | 3 + database/seeds/MenuTableSeeder.php | 8 + database/seeds/UserTableSeeder.php | 12 + public/template/calls.xlsx | Bin 0 -> 9724 bytes public/template/outgoing.csv | 7 - .../views/callcenter/task/_form.blade.php | 65 ++++++ resources/views/callcenter/task/_js.blade.php | 14 ++ .../views/callcenter/task/create.blade.php | 15 ++ .../views/callcenter/task/edit.blade.php | 16 ++ .../views/callcenter/task/import.blade.php | 63 ++++++ .../views/callcenter/task/index.blade.php | 150 +++++++++++++ .../views/callcenter/task/show.blade.php | 206 ++++++++++++++++++ routes/web.php | 21 ++ 23 files changed, 1227 insertions(+), 19 deletions(-) create mode 100644 app/Console/Commands/SwooleCallcenterRun.php delete mode 100644 app/Http/Controllers/Callcenter/Task.php create mode 100644 app/Http/Controllers/Callcenter/TaskController.php create mode 100644 app/Imports/CallImport.php create mode 100644 app/Models/Call.php create mode 100644 app/Service/Callcenter.php create mode 100644 public/template/calls.xlsx delete mode 100644 public/template/outgoing.csv create mode 100644 resources/views/callcenter/task/_form.blade.php create mode 100644 resources/views/callcenter/task/_js.blade.php create mode 100644 resources/views/callcenter/task/create.blade.php create mode 100644 resources/views/callcenter/task/edit.blade.php create mode 100644 resources/views/callcenter/task/import.blade.php create mode 100644 resources/views/callcenter/task/index.blade.php create mode 100644 resources/views/callcenter/task/show.blade.php diff --git a/app/Console/Commands/SwooleCallcenterRun.php b/app/Console/Commands/SwooleCallcenterRun.php new file mode 100644 index 00000000..91dfe544 --- /dev/null +++ b/app/Console/Commands/SwooleCallcenterRun.php @@ -0,0 +1,156 @@ +where(['status'=>2])->select(['id'])->get(); + if ($res->isNotEmpty()) { + foreach ($res as $key => $task) { + Redis::rPush($redis_key,$task->id); + } + } + \Swoole\Coroutine\run(function () use ($redis_key) { + //启动服务 + while (true) { + $task_id = Redis::lPop($redis_key); + if ($task_id == null) { + sleep(10); + continue; + } + \Swoole\Coroutine::create(function () use ($task_id){ + while (true){ + $task = Task::with(['queue.sips','gateway']) + ->where('status',2) + ->where('id',$task_id) + ->first(); + //检测是否有启动的任务 + if ($task == null ){ + break; + } + + //检测执行日期 + $now_date = strtotime(date('Y-m-d')); + if ( $now_date < strtotime($task->date_start) || $now_date > strtotime($task->date_end) ) { + //延迟10秒 + sleep(10); + Log::info("任务ID:".$task->id."运行日期不满足"); + continue; + } + + //检测执行时间 + $now_time = strtotime(date('H:i:s')); + if ( $now_time < strtotime($task->time_start) || $now_time > strtotime($task->time_end) ) { + //延迟10秒 + sleep(10); + Log::info("任务ID:".$task->id."运行时间不满足"); + continue; + } + + //检测网关信息 + if ($task->gateway==null){ + Log::info("任务ID:".$task->id." 的网关不存在,任务停止"); + $task->update(['status'=>1]); + break; + } + + //检测队列 + if ($task->queue==null){ + Log::info("任务ID:".$task->id." 的队列不存在,任务停止"); + $task->update(['status'=>1]); + break; + } + //检测队列是否有坐席 + if ($task->queue->sips->isEmpty()){ + Log::info("任务ID:".$task->id." 的队列无坐席存在,任务停止"); + $task->update(['status'=>1]); + break; + } + //并发数调节 + $channel = 0; + $members = 0; + foreach ($task->queue->sips as $sip){ + if ($sip->status==1 && $sip->state=='down'){ + $members++; + } + } + if ($members === 0){ + Log::info("任务ID:".$task->name." 无空闲坐席,sleep:1秒"); + sleep(1); + continue; + }else{ + if ($task->max_channel==0){ + $channel = $members; + }else{ + $channel = $task->max_channel > $members ? $members : $task->max_channel; + } + } + + //如果通道数还是0,则不需要呼叫 + if ($channel == 0) { + Log::info("任务ID:".$task->name." 的并发不需要呼叫"); + sleep(10); + continue; + } + + //进行呼叫 + $calls = Call::where('task_id',$task->id)->where('status',1)->orderBy('id','asc')->take($channel)->get(); + if ($calls->isEmpty()){ + Log::info("任务:".$task->name."已完成"); + $task->update(['status'=>3]); + break; + } + foreach ($calls as $call){ + go(function () use ($call,$task){ + (new Callcenter($call,$task))->run(); + }); + sleep(2); + } + sleep(6); + } + }); + } + }); + } +} diff --git a/app/Console/Commands/SwooleHttp.php b/app/Console/Commands/SwooleHttp.php index c87d81f7..964bc29c 100644 --- a/app/Console/Commands/SwooleHttp.php +++ b/app/Console/Commands/SwooleHttp.php @@ -50,6 +50,7 @@ class SwooleHttp extends Command 'directory' => '/usr/local/freeswitch/conf/directory/default/', //拨号计划下面默认分为default(呼出)和public(呼入) 'dialplan' => '/usr/local/freeswitch/conf/dialplan/', + 'callcenter' => '/usr/local/freeswitch/conf/autoload_configs/callcenter.conf.xml' ]; $http->on('request', function ($request, $response) use ($conf) { if($request->server['request_method'] == 'POST'){ @@ -137,7 +138,60 @@ class SwooleHttp extends Command exec($command."\""."reloadxml"."\""); $return = ['code'=>0,'msg'=>'拨号计划更新成功']; break; + case '/callcenter': + $xml = "\n"; + $xml .= "\t\n"; + $xml .= "\t\t\n"; + $xml .= "\t\t\n"; + $xml .= "\t\t\n"; + $xml .= "\t\t\n"; + $xml .= "\t\t\n"; + $xml .= "\t\n"; + //---------------------------------- 写入队列信息 ------------------------------------ + $xml .= "\t\n"; + foreach ($data['queues'] as $queue){ + $xml .= "\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + //$xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\t\n"; + $xml .= "\t\t\n"; + } + $xml .= "\t\n"; + //---------------------------------- 写入坐席信息 ------------------------------------ + $xml .= "\t\n"; + foreach ($data['agents'] as $agent){ + $contact = "[leg_timeout=10]user/".$agent['username']; + $xml .= "\t\t\n"; + } + $xml .= "\t\n"; + + //---------------------------------- 写入队列-坐席信息 ------------------------------------ + $xml .= "\t\n"; + foreach ($data['queues'] as $queue){ + if (isset($queue['agents'])&&!empty($queue['agents'])) { + foreach ($queue['agents'] as $agent){ + $xml .= "\t\t\n"; + } + } + } + $xml .= "\t\n"; + $xml .= "\n"; + //生成配置文件 + file_put_contents($conf['callcenter'],$xml); + exec($command."\""."reload mod_callcenter"."\""); + $return = ['code'=>0,'msg'=>'分机更新成功']; + break; case '/favicon.ico': $response->status(404); $response->end(); diff --git a/app/Http/Controllers/Callcenter/QueueController.php b/app/Http/Controllers/Callcenter/QueueController.php index ab1cc662..c4aad17b 100644 --- a/app/Http/Controllers/Callcenter/QueueController.php +++ b/app/Http/Controllers/Callcenter/QueueController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Callcenter; use App\Http\Controllers\Controller; +use App\Models\Sip; use GuzzleHttp\Client; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -42,6 +43,7 @@ class QueueController extends Controller 'name' => $data['name'], 'strategy' => $data['strategy'], 'max_wait_time' => $data['max_wait_time'], + 'created_at' => date('Y-m-d H:i:s'), ]); foreach ($data['sips'] as $sipId){ DB::table('queue_sip')->insert([ @@ -108,11 +110,16 @@ class QueueController extends Controller public function updateXml() { $queues = Queue::with('sips')->get()->toArray(); + $sipIds = DB::table('queue_sip')->pluck('sip_id')->toArray(); + $agents = Sip::query()->whereIn('id',$sipIds)->get()->toArray(); try{ $client = new Client(); $client->post(config('freeswitch.swoole_http_url.callcenter'), [ - 'json' => $queues, + 'json' => [ + 'queues' => $queues, + 'agents' => $agents, + ], 'timeout' => 30 ] ); diff --git a/app/Http/Controllers/Callcenter/Task.php b/app/Http/Controllers/Callcenter/Task.php deleted file mode 100644 index 987adf9c..00000000 --- a/app/Http/Controllers/Callcenter/Task.php +++ /dev/null @@ -1,11 +0,0 @@ -ajax()){ + $res = Task::query()->withCount('calls')->orderByDesc('id')->paginate($request->get('limit', 30)); + return $this->success('ok',$res->items(),$res->total()); + } + return View::make('callcenter.task.index'); + } + + public function create() + { + $queues = Queue::query()->orderByDesc('id')->get(); + $gateways = Gateway::query()->orderByDesc('id')->get(); + return View::make('callcenter.task.create',compact('queues','gateways')); + } + + public function store(Request $request) + { + $data = $request->all([ + 'name', + 'date_start', + 'date_end', + 'time_start', + 'time_end', + 'gateway_id', + 'queue_id', + 'max_channel', + ]); + try { + Task::create($data); + return $this->success(); + }catch (\Exception $exception){ + Log::error('添加任务异常:'.$exception->getMessage()); + return $this->error(); + } + } + + public function edit($id) + { + $model = Task::query()->findOrFail($id); + $queues = Queue::query()->orderByDesc('id')->get(); + $gateways = Gateway::query()->orderByDesc('id')->get(); + return View::make('callcenter.task.edit',compact('model', 'queues', 'gateways')); + } + + public function update(Request $request,$id) + { + $data = $request->all([ + 'name', + 'date_start', + 'date_end', + 'time_start', + 'time_end', + 'gateway_id', + 'queue_id', + 'max_channel', + ]); + $model = Task::query()->findOrFail($id); + try { + $model->update($data); + return $this->success(); + }catch (\Exception $exception){ + Log::error('更新任务异常:'.$exception->getMessage()); + return $this->error(); + } + } + + public function destroy(Request $request) + { + $ids = $request->get('ids'); + if (empty($ids)){ + return $this->error('请选择删除项'); + } + DB::beginTransaction(); + try { + Task::destroy($ids); + + DB::commit(); + return $this->success(); + }catch (\Exception $exception){ + DB::rollBack(); + Log::error('删除群呼任务异常:'.$exception->getMessage()); + return $this->error(); + } + } + + public function show(Request $request,$id) + { + $task = Task::query()->withCount(['calls','hasCalls','missCalls','successCalls','failCalls'])->findOrFail($id); + $percent = $task->calls_count>0?100*round(($task->has_calls_count)/($task->calls_count),4).'%':'0.00%'; + if ($request->isMethod('post')){ + $tiers = DB::table('queue_agent')->where('queue_id',$task->queue_id)->pluck('agent_id'); + return response()->json(['code'=>0, 'msg'=>'请求成功']); + } + return view('callcenter.task.show',compact('task','percent')); + } + + public function setStatus(Request $request) + { + $ids = $request->get('ids',[]); + if (count($ids)!=1){ + return $this->error('请选择一条记录'); + } + $task = Task::query()->withCount('calls')->find($ids[0]); + if ($task==null){ + return $this->error('任务不存在'); + } + if ($task->status==3){ + return $this->error('任务已完成,禁止操作'); + } + $status = $request->get('status',1); + + if ($status==2&&$task->calls_count==0){ + return $this->error('任务未导入号码,禁止操作'); + } + if ($status==1&&$task->status!=2){ + return $this->error('任务未启动,禁止操作'); + } + try { + $task->update(['status'=>$status]); + $key = config('freeswitch.redis_key.callcenter_task'); + Redis::rPush($key,$task->id); + return $this->success(); + }catch (\Exception $exception){ + Log::error('设置任务状态异常:'.$exception->getMessage()); + return $this->error(); + } + } + + public function importCall(Request $request, $id) + { + $model = Task::query()->findOrFail($id); + if ($request->ajax()){ + $file = $request->input('file'); + if ($file == null){ + return $this->error('请先上传文件'); + } + $xlsFile = public_path().$file; + try{ + Excel::import(new CallImport($id), $xlsFile); + return $this->success('导入成功'); + }catch (\Exception $exception){ + Log::error('导入失败:'.$exception->getMessage()); + return $this->error('导入失败'); + } + } + return View::make('callcenter.task.import',compact('model')); + } + + public function calls(Request $request) + { + $data = $request->all(['task_id','phone']); + $res = Call::query() + ->when($data['phone'],function ($q) use($data){ + return $q->where('phone','like','%'.$data['phone'].'%'); + }) + ->where('task_id',$data['task_id']) + ->orderBy('id','asc') + ->paginate($request->get('limit', 30)); + foreach ($res->items() as $item){ + $item->status_name = Arr::get(config('freeswitch.callcenter_call_status'),$item->status,'-'); + } + return $this->success('ok',$res->items(),$res->total()); + } + +} diff --git a/app/Imports/CallImport.php b/app/Imports/CallImport.php new file mode 100644 index 00000000..f07dc578 --- /dev/null +++ b/app/Imports/CallImport.php @@ -0,0 +1,30 @@ +task_id = $task_id; + } + + public function model(array $row) + { + if (!isset($row[0]) || !preg_match('/\d{7,11}/',$row[0]) ) { + return null; + } + return new Call([ + 'task_id' => $this->task_id, + 'phone' => $row[0], + ]); + } +} diff --git a/app/Models/Call.php b/app/Models/Call.php new file mode 100644 index 00000000..a01e2c57 --- /dev/null +++ b/app/Models/Call.php @@ -0,0 +1,22 @@ +hasOne('App\Models\Sip','id','sip_id'); + } + + public function user() + { + return $this->hasOne('App\Models\User','id','user_id'); + } + +} diff --git a/app/Models/Task.php b/app/Models/Task.php index 58531793..0629f09f 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -8,4 +8,67 @@ class Task extends Model { protected $table = 'task'; protected $guarded = ['id']; + + protected $appends = ['gateway_name','queue_name','date','time']; + + public function gateway() + { + return $this->hasOne('App\Models\Gateway','id','gateway_id'); + } + + public function queue() + { + return $this->hasOne('App\Models\Queue','id','queue_id'); + } + + public function getGatewayNameAttribute() + { + return $this->attributes['gateway_name'] = $this->gateway->name; + } + + public function getQueueNameAttribute() + { + return $this->attributes['queue_name'] = $this->queue->name; + } + + public function getDateAttribute() + { + return $this->attributes['date'] = $this->date_start . ' / '. $this->date_end; + } + + public function getTimeAttribute() + { + return $this->attributes['time'] = $this->time_start . ' - '. $this->time_end; + } + + //总呼叫数 + public function calls() + { + return $this->hasMany('App\Models\Call','task_id','id'); + } + + //已呼叫数 status !=1 + public function hasCalls() + { + return $this->hasMany('App\Models\Call','task_id','id')->where('status','!=',1); + } + + //漏接数 status=3 + public function missCalls() + { + return $this->hasMany('App\Models\Call','task_id','id')->where('status',3); + } + + //呼叫成功数 status=4 + public function successCalls() + { + return $this->hasMany('App\Models\Call','task_id','id')->where('status',4); + } + + //呼叫失败数 status=2 + public function failCalls() + { + return $this->hasMany('App\Models\Call','task_id','id')->where('status',2); + } + } diff --git a/app/Service/Callcenter.php b/app/Service/Callcenter.php new file mode 100644 index 00000000..4a9b945a --- /dev/null +++ b/app/Service/Callcenter.php @@ -0,0 +1,123 @@ +call = $call; + $this->fs = new SwooleFreeswitch(); + if (!$this->fs->connect()) { + return false; + } + + } + + public function run() + { + $record_url = config('freeswitch.record_url'); + $fs_dir = '/usr/local/freeswitch'; + + $uuid = uuid_generate(); + //更新为正在呼叫 + $this->call->update([ + 'status' => 2, + 'uuid' => $uuid, + 'datetime_originate_phone' => date('Y-m-d H:i:s') + ]); + Log::info("更新号码: " . $this->call->phone . " 状态为:2"); + + $phone = $this->task->gateway->prefix ? $this->task->gateway->prefix . $this->call->phone : $this->call->phone; + $varStr = "{origination_uuid=" . $uuid . "}"; + $varStr .= "{ignore_early_media=true}"; + $varStr .= "{effective_caller_id_number=" . $this->call->phone . "}"; + $varStr .= "{effective_caller_id_name=" . $this->call->phone . "}"; + if ($this->task->gateway->outbound_caller_id) { + $varStr .= "{origination_caller_id_number=" . $this->task->gateway->outbound_caller_id . "}"; + $varStr .= "{origination_caller_id_name=" . $this->task->gateway->outbound_caller_id . "}"; + } + $varStr .= "{cc_export_vars=effective_caller_id_number,effective_caller_id_name}"; + $dail_string = "originate " . $varStr . "sofia/gateway/gw" . $this->task->gateway->id . "/" . $phone . " &callcenter(queue" . $this->task->queue->id . ")"; + Log::info("呼叫:" . $dail_string); + + $this->fs->bgapi($dail_string); + $this->fs->events("CUSTOM callcenter::info"); + $this->fs->filteruuid($this->call->uuid); + + while (true) { + $received_parameters = $this->fs->recvEvent(); + if (!empty($received_parameters)) { + $json = $this->fs->serialize($received_parameters); + $action = Arr::get($json, "CC-Action"); + $uuid = Arr::get($json, "CC-Member-Session-UUID"); + switch ($action) { + //呼叫进入队列 + case 'member-queue-start': + $this->call->update([ + 'datetime_entry_queue' => date('Y-m-d H:i:s'), + 'status' => 3 + ]); + break; + // 坐席应答 + case 'bridge-agent-start': + $agent_name = Arr::get($json, "CC-Agent"); + $id = (int)Str::after($agent_name, 'agent'); + $filepath = $fs_dir . '/recordings/' . date('Y/m/d/'); + $file = $filepath . 'callcenter_' . $uuid . '.wav'; + $this->fs->bgapi("uuid_record " . $uuid . " start " . $file . " 1800"); + $this->call->update([ + 'datetime_agent_answered' => date('Y-m-d H:i:s'), + 'status' => 4, + 'agent_id' => $id, + 'record_file' => str_replace($fs_dir, $record_url, $file), + ]); + break; + //坐席结束 + case 'bridge-agent-end': + $this->call->update([ + 'datetime_end' => date('Y-m-d H:i:s'), + 'status' => 4 + ]); + break; + //桥接结束,通话结束 + case 'member-queue-end': + $cause = Arr::get($json, "CC-Cause"); + $answered_time = Arr::get($json, "CC-Agent-Answered-Time"); + $leaving_time = Arr::get($json, "CC-Member-Leaving-Time"); + if ($cause == 'Cancel') { + $billsec = 0; + } else { + if ($leaving_time && $answered_time) { + $billsec = $leaving_time - $answered_time > 0 ? $leaving_time - $answered_time : 0; + } else { + $billsec = 0; + } + } + $this->call->update([ + 'billsec' => $billsec, + ]); + break; + default: + } + } + } + $this->fs->disconnect(); + } + +} diff --git a/config/freeswitch.php b/config/freeswitch.php index 2aa42d8a..ab5e9c52 100644 --- a/config/freeswitch.php +++ b/config/freeswitch.php @@ -31,6 +31,8 @@ return [ 'directory' => 'http://127.0.0.1:9501/directory', //生成拨号计划 'dialplan' => 'http://127.0.0.1:9501/dialplan', + //生成群呼 + 'callcenter' => 'http://127.0.0.1:9501/callcenter', ], 'esl' => [ @@ -42,6 +44,7 @@ return [ 'redis_key' => [ 'dial' => 'dial_uuid_queue', 'api' => 'api_exec_queue', + 'callcenter_task' => 'callcenter_task_queue', ], 'record_url' => env('APP_URL','http://localhost'), 'host' => env('FS_HOST','127.0.0.1'), @@ -85,4 +88,12 @@ return [ 4 => '微信', 5 => '其它', ], + + //群呼状态 + 'callcenter_call_status' => [ + 1 => '待呼叫', + 2 => '呼叫失败', + 3 => '漏接', + 4 => '成功', + ], ]; diff --git a/database/migrations/2021_04_02_154147_callcenter_task_call.php b/database/migrations/2021_04_02_154147_callcenter_task_call.php index 164b99e9..28660646 100644 --- a/database/migrations/2021_04_02_154147_callcenter_task_call.php +++ b/database/migrations/2021_04_02_154147_callcenter_task_call.php @@ -18,6 +18,9 @@ class CallcenterTaskCall extends Migration $table->unsignedBigInteger('task_id')->comment('任务ID'); $table->string('phone')->comment('待呼叫号码'); $table->tinyInteger('status')->default(1)->comment('1-待呼叫,2-呼叫中,3-队列等待,4-已通话'); + $table->string('uuid')->nullable()->comment('UUID'); + $table->string('aleg_uuid')->nullable()->comment('客户通话UUID'); + $table->string('bleg_uuid')->nullable()->comment('坐席通话UUID'); $table->string('uuid')->nullable()->comment('客户通话UUID'); $table->timestamp('datetime_originate_phone')->nullable()->comment('呼叫时间'); $table->timestamp('datetime_entry_queue')->nullable()->comment('进入队列时间'); diff --git a/database/seeds/MenuTableSeeder.php b/database/seeds/MenuTableSeeder.php index 88787bd0..324149e6 100644 --- a/database/seeds/MenuTableSeeder.php +++ b/database/seeds/MenuTableSeeder.php @@ -116,6 +116,14 @@ class MenuTableSeeder extends Seeder 'type' => 1, 'permission_name' => 'callcenter.queue', ], + [ + 'name' => '任务管理', + 'route' => 'callcenter.task', + 'url' => null, + 'icon' => 'layui-icon-template-1', + 'type' => 1, + 'permission_name' => 'callcenter.task', + ], ] ], [ diff --git a/database/seeds/UserTableSeeder.php b/database/seeds/UserTableSeeder.php index 4ba4e2ea..13cac881 100644 --- a/database/seeds/UserTableSeeder.php +++ b/database/seeds/UserTableSeeder.php @@ -137,6 +137,18 @@ class UserTableSeeder extends Seeder ['name' => 'callcenter.queue.updateXml', 'display_name' => '更新配置'], ] ], + [ + 'name' => 'callcenter.task', + 'display_name' => '任务管理', + 'child' => [ + ['name' => 'callcenter.task.create', 'display_name' => '添加'], + ['name' => 'callcenter.task.show', 'display_name' => '详情'], + ['name' => 'callcenter.task.edit', 'display_name' => '编辑'], + ['name' => 'callcenter.task.destroy', 'display_name' => '删除'], + ['name' => 'callcenter.task.importCall', 'display_name' => '导入号码'], + ['name' => 'callcenter.task.setStatus', 'display_name' => '设置状态'], + ] + ], ], ], [ diff --git a/public/template/calls.xlsx b/public/template/calls.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9ed6a89348d8a21f3f27a06cfe6d35a2d478fa00 GIT binary patch literal 9724 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 BXcGVc literal 0 HcmV?d00001 diff --git a/public/template/outgoing.csv b/public/template/outgoing.csv deleted file mode 100644 index 76cfebf0..00000000 --- a/public/template/outgoing.csv +++ /dev/null @@ -1,7 +0,0 @@ -18908221080 -13512293513 -13512293514 -13512293515 -13512293516 -13512293517 -13512293517 \ No newline at end of file diff --git a/resources/views/callcenter/task/_form.blade.php b/resources/views/callcenter/task/_form.blade.php new file mode 100644 index 00000000..b97c0e30 --- /dev/null +++ b/resources/views/callcenter/task/_form.blade.php @@ -0,0 +1,65 @@ +{{csrf_field()}} +
+ +
+ +
+
+
+
+ +
+ +
+
-
+
+ +
+
+
+
+
+ +
+ +
+
-
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
最大并发,默认:0 为不限制,系统将自动调节
+
+
+
+ +
+
diff --git a/resources/views/callcenter/task/_js.blade.php b/resources/views/callcenter/task/_js.blade.php new file mode 100644 index 00000000..93bdd5fe --- /dev/null +++ b/resources/views/callcenter/task/_js.blade.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/resources/views/callcenter/task/create.blade.php b/resources/views/callcenter/task/create.blade.php new file mode 100644 index 00000000..ab20fce7 --- /dev/null +++ b/resources/views/callcenter/task/create.blade.php @@ -0,0 +1,15 @@ +@extends('base') + +@section('content') +
+
+
+ @include('callcenter.task._form') +
+
+
+@endsection + +@section('script') + @include('callcenter.task._js') +@endsection diff --git a/resources/views/callcenter/task/edit.blade.php b/resources/views/callcenter/task/edit.blade.php new file mode 100644 index 00000000..b5a40536 --- /dev/null +++ b/resources/views/callcenter/task/edit.blade.php @@ -0,0 +1,16 @@ +@extends('base') + +@section('content') +
+
+
+ {{method_field('put')}} + @include('callcenter.task._form') +
+
+
+@endsection + +@section('script') + @include('callcenter.task._js') +@endsection diff --git a/resources/views/callcenter/task/import.blade.php b/resources/views/callcenter/task/import.blade.php new file mode 100644 index 00000000..15ce4e25 --- /dev/null +++ b/resources/views/callcenter/task/import.blade.php @@ -0,0 +1,63 @@ +@extends('base') + +@section('content') +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + +
+
+
+
+
+@endsection + +@section('script') + +@endsection diff --git a/resources/views/callcenter/task/index.blade.php b/resources/views/callcenter/task/index.blade.php new file mode 100644 index 00000000..97aeefd3 --- /dev/null +++ b/resources/views/callcenter/task/index.blade.php @@ -0,0 +1,150 @@ +@extends('base') + +@section('content') +
+
+
+ @can('callcenter.task.destroy') + + @endcan + @can('callcenter.task.setStatus') + + + @endcan + @can('callcenter.task.create') + + @endcan + 模板下载 +
+
+
+
+ + +
+
+@endsection + +@section('script') + +@endsection diff --git a/resources/views/callcenter/task/show.blade.php b/resources/views/callcenter/task/show.blade.php new file mode 100644 index 00000000..58b5acc0 --- /dev/null +++ b/resources/views/callcenter/task/show.blade.php @@ -0,0 +1,206 @@ +@extends('base') + +@section('content') +
+
+
+
+
+
+ +
{{$task->name}}
+
+
+
+
+ +
{{$task->date}}
+
+
+
+
+ +
{{$task->time}}
+
+
+
+
+ +
{{$task->gateway_name}}
+
+
+
+
+ +
{{$task->queue_name}}
+
+
+
+
+ +
{{$task->max_channel}}
+
+
+
+
+ +
{{$task->calls_count}}
+
+
+
+
+ +
{{$task->has_calls_count}}
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
呼叫图表
+
+
+
+
+
+
+
+
呼叫结果
+
+ + + + + + + + + +
状态数量占比
成功{{$task->success_calls_count}}{{$task->has_calls_count>0?100*round($task->success_calls_count/$task->has_calls_count,4).'%':'0.00%'}}
失败{{$task->fail_calls_count}}{{$task->has_calls_count>0?100*round($task->fail_calls_count/$task->has_calls_count,4).'%':'0.00%'}}
漏接{{$task->miss_calls_count}}{{$task->has_calls_count>0?100*round($task->miss_calls_count/$task->has_calls_count,4).'%':'0.00%'}}
+
+
+
+
+
+
+ +
+
+ 呼叫记录 +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +@endsection + +@section('script') + +@endsection diff --git a/routes/web.php b/routes/web.php index 756e1763..3af7c952 100644 --- a/routes/web.php +++ b/routes/web.php @@ -211,6 +211,27 @@ Route::group(['prefix'=>'callcenter','namespace'=>'Callcenter','middleware'=>['a Route::post('queue/updateXml','QueueController@updateXml')->name('callcenter.queue.updateXml')->middleware('permission:callcenter.queue.updateXml'); }); + //任务管理 + Route::group([],function (){ + Route::get('task','TaskController@index')->name('callcenter.task')->middleware('permission:callcenter.task'); + //添加 + Route::get('task/create','TaskController@create')->name('callcenter.task.create')->middleware('permission:callcenter.task.create'); + Route::post('task/store','TaskController@store')->name('callcenter.task.store')->middleware('permission:callcenter.task.create'); + //编辑 + Route::get('task/{id}/edit','TaskController@edit')->name('callcenter.task.edit')->middleware('permission:callcenter.task.edit'); + Route::put('task/{id}/update','TaskController@update')->name('callcenter.task.update')->middleware('permission:callcenter.task.edit'); + //删除 + Route::delete('task/destroy','TaskController@destroy')->name('callcenter.task.destroy')->middleware('permission:callcenter.task.destroy'); + //详情 + Route::get('task/{id}/show','TaskController@show')->name('callcenter.task.show')->middleware('permission:callcenter.task.show'); + //设置状态 + Route::post('task/setStatus','TaskController@setStatus')->name('callcenter.task.setStatus')->middleware('permission:callcenter.task.setStatus'); + //导入号码 + Route::match(['get','post'],'task/{id}/importCall','TaskController@importCall')->name('callcenter.task.importCall')->middleware('permission:callcenter.task.importCall'); + //呼叫记录 + Route::get('task/calls','TaskController@calls')->name('callcenter.task.calls'); + }); + });