diff --git a/app/Http/Controllers/Admin/AgentController.php b/app/Http/Controllers/Admin/AgentController.php new file mode 100644 index 00000000..b9ba23ec --- /dev/null +++ b/app/Http/Controllers/Admin/AgentController.php @@ -0,0 +1,136 @@ +paginate($request->get('limit', 30)); + $data = [ + 'code' => 0, + 'msg' => '正在请求中...', + 'count' => $res->total(), + 'data' => $res->items(), + ]; + return response()->json($data); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $sips = Sip::orderByDesc('id')->get(); + return view('admin.agent.create',compact('sips')); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(AgentRequest $request) + { + $data = $request->all(); + $data['contact'] = 'user/'.$data['contact']; + if (Agent::create($data)){ + return redirect(route('admin.agent'))->with(['success'=>'添加成功']); + } + return back()->withErrors(['error'=>'添加失败']); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function show($id) + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + $model = Agent::findOrFail($id); + $sips = Sip::orderByDesc('id')->get(); + return view('admin.agent.edit',compact('model','sips')); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\Response + */ + public function update(AgentRequest $request, $id) + { + $model = Agent::findOrFail($id); + $data = $request->except(['_method','_token']); + $data['contact'] = 'user/'.$data['contact']; + DB::beginTransaction(); + try{ + DB::table('tiers')->where('agent',$model->name)->update(['agent'=>$data['name']]); + DB::table('agents')->where('id',$model->id)->update($data); + DB::commit(); + return redirect(route('admin.agent'))->with(['success'=>'更新成功']); + }catch (\Exception $exception){ + DB::rollback(); + return back()->withErrors(['error'=>'更新失败']); + } + + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy(Request $request) + { + $ids = $request->get('ids'); + if (empty($ids)){ + return response()->json(['code'=>1,'msg'=>'请选择删除项']); + } + $names = Agent::whereIn('id',$ids)->pluck('name'); + DB::beginTransaction(); + try{ + DB::table('tiers')->whereIn('agent',$names)->delete(); + DB::table('agents')->whereIn('id',$ids)->delete(); + DB::commit(); + return response()->json(['code'=>0,'msg'=>'删除成功']); + }catch (\Exception $e) { + DB::rollBack(); + return response()->json(['code'=>1,'msg'=>'删除失败','data'=>$e->getMessage()]); + } + } +} diff --git a/app/Http/Controllers/Admin/GatewayController.php b/app/Http/Controllers/Admin/GatewayController.php index cf2c2336..a8c931c0 100644 --- a/app/Http/Controllers/Admin/GatewayController.php +++ b/app/Http/Controllers/Admin/GatewayController.php @@ -49,7 +49,7 @@ class GatewayController extends Controller */ public function store(GatewayRequest $request) { - $data = $request->all(['name','realm','username','password']); + $data = $request->except(['_method','_token']); if (Gateway::create($data)){ return redirect(route('admin.gateway'))->with(['success'=>'添加成功']); } @@ -89,7 +89,7 @@ class GatewayController extends Controller public function update(Request $request, $id) { $model = Gateway::findOrFail($id); - $data = $request->all(['name','realm','username','password']); + $data = $request->except(['_method','_token']); if ($model->update($data)){ return redirect(route('admin.gateway'))->with(['success'=>'更新成功']); } diff --git a/app/Http/Controllers/Admin/QueueController.php b/app/Http/Controllers/Admin/QueueController.php index 19943b0f..0d6f1694 100644 --- a/app/Http/Controllers/Admin/QueueController.php +++ b/app/Http/Controllers/Admin/QueueController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Requests\QueueRequest; +use App\Models\Agent; use App\Models\Queue; use Illuminate\Http\Request; use App\Http\Controllers\Controller; @@ -22,7 +23,7 @@ class QueueController extends Controller public function data(Request $request) { - $res = Queue::orderByDesc('id')->paginate($request->get('limit', 30)); + $res = Queue::withCount('agents')->orderByDesc('id')->paginate($request->get('limit', 30)); $data = [ 'code' => 0, 'msg' => '正在请求中...', @@ -52,7 +53,7 @@ class QueueController extends Controller { $data = $request->all(); if (Queue::create($data)){ - return redirect(route('admin.queue'))->with(['success'=>'添加成功']); + return redirect(route('admin.queue'))->with(['success'=>'添加成功,请更新配置']); } return back()->withErrors(['error'=>'添加失败']); } @@ -90,11 +91,18 @@ class QueueController extends Controller public function update(QueueRequest $request, $id) { $model = Queue::findOrFail($id); - $data = $request->all(); - if ($model->update($data)){ - return redirect(route('admin.queue'))->with(['success'=>'更新成功']); + $data = $request->except(['_method','_token']); + DB::beginTransaction(); + try{ + DB::table('queue')->where('id',$model->id)->update($data); + DB::table('tiers')->where('queue',$model->name)->update(['queue'=>$data['name']]); + DB::commit(); + return redirect(route('admin.queue'))->with(['success'=>'更新成功,请更新配置']); + }catch (\Exception $exception){ + DB::rollback(); + return back()->withErrors(['error'=>'更新失败']); } - return back()->withErrors(['error'=>'更新失败']); + } /** @@ -112,8 +120,8 @@ class QueueController extends Controller $queues = Queue::whereIn('id',$ids)->pluck('name'); DB::beginTransaction(); try{ - DB::table('tiers')->where('queue','in',$queues)->delete(); - DB::table('queue')->where('id','in',$ids)->delete(); + DB::table('tiers')->whereIn('queue',$queues)->delete(); + DB::table('queue')->whereIn('id',$ids)->delete(); DB::commit(); return response()->json(['code'=>0,'msg'=>'删除成功,请更新配置']); }catch (\Exception $e) { @@ -124,17 +132,68 @@ class QueueController extends Controller public function updateXml() { - + $queues = Queue::get(); + if ($queues->isEmpty()){ + return response()->json(['code'=>1,'msg'=>'无数据需要更新']); + } + try{ + $xml = "\n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + foreach ($queues->toArray() as $q){ + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + } + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= " \n"; + $xml .= "\n"; + file_put_contents(config('freeswitch.callcenter_dir'),$xml); + //生产环境,并且debug关闭的情况下自动更新网关注册信息 + if (config('app.env')=='production' && config('app.debug')==false){ + $freeswitch = new \Freeswitchesl(); + $freeswitch->bgapi("reload callcenter"); + } + return response()->json(['code'=>0,'msg'=>'更新成功']); + }catch (\Exception $exception){ + return response()->json(['code'=>1,'msg'=>'更新失败','data'=>$exception->getMessage()]); + } } - public function agent() + public function agent($id) { - + $queue = Queue::with('agents')->findOrFail($id); + $agents = Agent::orderByDesc('id')->get(); + return view('admin.queue.agent',compact('queue','agents')); } - public function assignAgent() + public function assignAgent(Request $request,$id) { - + $queue = Queue::with('agents')->findOrFail($id); + $names = $request->get('agents',[]); + if ($queue->agents()->sync($names)){ + return redirect(route('admin.queue'))->with(['success'=>'更新成功']); + } + return back()->withErrors(['error'=>'更新失败']); } } diff --git a/app/Http/Controllers/Admin/TaskController.php b/app/Http/Controllers/Admin/TaskController.php new file mode 100644 index 00000000..7b1c8702 --- /dev/null +++ b/app/Http/Controllers/Admin/TaskController.php @@ -0,0 +1,240 @@ +paginate($request->get('limit', 30)); + $data = [ + 'code' => 0, + 'msg' => '正在请求中...', + 'count' => $res->total(), + 'data' => $res->items(), + ]; + return response()->json($data); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $queues = Queue::orderByDesc('id')->get(); + $gateways = Gateway::orderByDesc('id')->get(); + return view('admin.task.create',compact('queues','gateways')); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(TaskRequest $request) + { + $data = $request->except(['_method','_token']); + if (Task::create($data)){ + return redirect(route('admin.task'))->with(['success'=>'添加成功']); + } + return back()->withErrors(['error'=>'添加失败']); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function show(Request $request, $id) + { + $task = Task::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('tiers')->where('queue',$task->queue_name)->pluck('agent'); + $agents = Agent::whereIn('name',$tiers)->get(); + return response()->json(['code'=>0, 'msg'=>'请求成功', 'data'=>$agents]); + } + return view('admin.task.show',compact('task','percent')); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + $model = Task::findOrFail($id); + $queues = Queue::orderByDesc('id')->get(); + $gateways = Gateway::orderByDesc('id')->get(); + return view('admin.task.edit',compact('model', 'queues', 'gateways')); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\Response + */ + public function update(TaskRequest $request, $id) + { + $data = $request->except(['_method','_token']); + $model = Task::findOrFail($id); + if ($model->update($data)){ + return redirect(route('admin.task'))->with(['success'=>'更新成功']); + } + return back()->withErrors(['error'=>'更新失败']); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy(Request $request) + { + $ids = $request->get('ids'); + if (empty($ids)){ + return response()->json(['code'=>1,'msg'=>'请选择删除项']); + } + if (Task::destroy($ids)){ + return response()->json(['code'=>0,'msg'=>'删除成功']); + } + return response()->json(['code'=>1,'msg'=>'删除失败']); + } + + public function setStatus(Request $request) + { + $ids = $request->get('ids',[]); + if (count($ids)!=1){ + return response()->json(['code'=>1,'msg'=>'请选择一条记录']); + } + $task = Task::withCount('calls')->find($ids[0]); + if ($task==null){ + return response()->json(['code'=>1,'msg'=>'任务不存在']); + } + if ($task->status==3){ + return response()->json(['code'=>1,'msg'=>'任务已完成,禁止操作']); + } + $status = $request->get('status',1); + + if ($status==2&&$task->calls_count==0){ + return response()->json(['code'=>1,'msg'=>'任务未导入号码,禁止操作']); + } + if ($status==1&&$task->status!=2){ + return response()->json(['code'=>1,'msg'=>'任务未启动,禁止操作']); + } + + if ($task->update(['status'=>$status])){ + return response()->json(['code'=>0,'msg'=>'更新成功']); + } + return response()->json(['code'=>1,'msg'=>'更新失败']); + } + + public function importCall(Request $request, $id) + { + set_time_limit(0); + $task = Task::find($id); + if ($task==null){ + return response()->json(['code'=>1,'msg'=>'任务不存在']); + } + + $file = $request->file('file'); + if ($file->isValid()){ + $allowed_extensions = ['csv']; + //上传文件最大大小,单位M 500Kb大约4万条数据 + $maxSize = 1; + //检测类型 + $ext = $file->getClientOriginalExtension(); + if (!in_array(strtolower($ext),$allowed_extensions)){ + return response()->json(['code'=>1,'msg'=>"请上传".implode(",",$allowed_extensions)."格式"]); + } + //检测大小 + if ($file->getClientSize() > $maxSize*1024*1024){ + return response()->json(['code'=>1,'msg'=>"图片大小限制".$maxSize."M"]); + } + //上传到七牛云 + $newFile = Uuid::uuid().".".$file->getClientOriginalExtension(); + try{ + $disk = QiniuStorage::disk('qiniu'); + $disk->put($newFile,file_get_contents($file->getRealPath())); + $url = $disk->downloadUrl($newFile); + }catch (\Exception $exception){ + return response()->json(['code'=>1,'msg'=>'文件上传失败','data'=>$exception->getMessage()]); + } + //文件内容读取 + $data = []; + try{ + $fp = fopen($url,"r"); + while(!feof($fp)) + { + $line = fgetcsv($fp); + if ($line){ + foreach ($line as $phone){ + array_push($data,$phone); + } + } + } + fclose($fp); + //去重,去空 + $data = array_filter(array_unique($data)); + }catch (\Exception $exception){ + return response()->json(['code'=>1,'msg'=>'读取文件内容错误','data'=>$exception->getMessage()]); + } + + //写入数据库 + if (!empty($data)){ + DB::beginTransaction(); + try{ + foreach ($data as $d){ + DB::table('call')->insert([ + 'task_id' => $task->id, + 'phone' => $d, + 'created_at'=> Carbon::now(), + 'updated_at'=> Carbon::now(), + ]); + } + DB::commit(); + return response()->json(['code'=>0,'msg'=>'导入完成']); + }catch (\Exception $exception){ + DB::rollBack(); + return response()->json(['code'=>1,'msg'=>'导入失败','data'=>$exception->getMessage()]); + } + } + return response()->json(['code'=>1,'msg'=>'导入数据为空']); + } + return response()->json(['code'=>1,'msg'=>'上传失败','data'=>$file->getErrorMessage()]); + + } + +} diff --git a/app/Http/Controllers/PublicController.php b/app/Http/Controllers/PublicController.php index 5e9d0786..d57d723f 100644 --- a/app/Http/Controllers/PublicController.php +++ b/app/Http/Controllers/PublicController.php @@ -1,7 +1,6 @@ 'required|string|min:2|unique:agents,name,'.$this->id.',id', + 'contact' => 'required', + 'status' => 'required|string|in:Logged Out,Available,Available (On Demand),On Break', + 'state' => 'required|string|in:Idle,Waiting,In a queue call', + 'max_no_answer' => 'required|numeric|min:0', + 'wrap_up_time' => 'required|numeric|min:0', + 'reject_delay_time' => 'required|numeric|min:0', + 'busy_delay_time' => 'required|numeric|min:0', + 'no_answer_delay_time' => 'required|numeric|min:0', + ]; + } + + public function attributes() + { + return [ + 'name' => '坐席名称', + 'contact' => '分机号', + 'status' => '坐席状态', + 'state' => '呼叫状态', + 'max_no_answer' => '最大无应答次数', + 'wrap_up_time' => '通话间隔', + 'reject_delay_time' => '拒接间隔时间', + 'busy_delay_time' => '忙重试间隔时间', + 'no_answer_delay_time' => '无应答重试间隔', + ]; + } + +} diff --git a/app/Http/Requests/QueueRequest.php b/app/Http/Requests/QueueRequest.php index 5535b3db..71262f69 100644 --- a/app/Http/Requests/QueueRequest.php +++ b/app/Http/Requests/QueueRequest.php @@ -13,7 +13,7 @@ class QueueRequest extends FormRequest */ public function authorize() { - return false; + return true; } /** diff --git a/app/Http/Requests/TaskRequest.php b/app/Http/Requests/TaskRequest.php new file mode 100644 index 00000000..d5fae198 --- /dev/null +++ b/app/Http/Requests/TaskRequest.php @@ -0,0 +1,48 @@ + 'required', + 'datetime_start' => 'required|date_format:Y-m-d H\:i\:s|before:datetime_end', + 'datetime_end' => 'required|date_format:Y-m-d H\:i\:s|after:datetime_start', + 'gateway_id' => 'required|exists:gateway,id', + 'queue_id' => 'required|exists:queue,id', + 'max_channel' => 'required|numeric|min:0', + ]; + } + + public function attributes() + { + return [ + 'name' => '名称', + 'datetime_start' => '开始时间', + 'datetime_end' => '结束时间', + 'gateway_id' => '网关', + 'queue_id' => '队列', + 'max_channel' => '最大并发', + ]; + } + +} diff --git a/app/Models/Agent.php b/app/Models/Agent.php new file mode 100644 index 00000000..62699cb3 --- /dev/null +++ b/app/Models/Agent.php @@ -0,0 +1,38 @@ +attributes['contact_name'] = str_after($this->contact,'user/'); + } + + public function getStatusNameAttribute() + { + return $this->attributes['status_name'] = array_get(config('freeswitch.agent_status'),$this->status); + } + + public function getStateNameAttribute() + { + return $this->attributes['state_name'] = array_get(config('freeswitch.agent_state'),$this->state); + } + +} diff --git a/app/Models/Call.php b/app/Models/Call.php new file mode 100644 index 00000000..c754b8a6 --- /dev/null +++ b/app/Models/Call.php @@ -0,0 +1,10 @@ +belongsToMany('App\Models\Agent','tiers','queue','agent','name','name'); + } + + public function getStrategyNameAttribute() + { + return $this->attributes['strategy_name'] = array_get(config('freeswitch.strategy'),$this->strategy); + } + } diff --git a/app/Models/Task.php b/app/Models/Task.php new file mode 100644 index 00000000..77752daa --- /dev/null +++ b/app/Models/Task.php @@ -0,0 +1,71 @@ +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->display_name; + } + + //总呼叫数 + 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=5 + public function failCalls() + { + return $this->hasMany('App\Models\Call','task_id','id')->where('status',5); + } + +} diff --git a/config/filesystems.php b/config/filesystems.php index bd3015a4..dd68bec2 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -66,13 +66,13 @@ return [ 'qiniu' => [ 'driver' => 'qiniu', 'domains' => [ - 'default' => 'o95vn3gtk.bkt.clouddn.com', //你的七牛域名 + 'default' => 'static.nicaicai.top', //你的七牛域名 'https' => '', //你的HTTPS域名 'custom' => '', //Useless 没啥用,请直接使用上面的 default 项 ], 'access_key'=> '1JdLd1hJq8j99yKGwgPgE_p0s8PAQ3UNZIKtLXaV', //AccessKey 'secret_key'=> 'e-mpwUbtfF8cv0aCUOvqimlIdexaavYtV_yjyhQG', //SecretKey - 'bucket' => 'muzilong', //Bucket名字 + 'bucket' => 'company', //Bucket名字 'notify_url'=> '', //持久化处理回调地址 'access' => 'public' //空间访问控制 public 或 private ], diff --git a/config/freeswitch.php b/config/freeswitch.php index 67930b79..b1903424 100644 --- a/config/freeswitch.php +++ b/config/freeswitch.php @@ -4,6 +4,9 @@ return [ //网关目录 'gateway_dir' => '/usr/local/freeswitch/etc/freeswitch/sip_profiles/external/', + //callcenter_conf_dir + 'callcenter_dir' => '/usr/local/freeswitch/etc/freeswitch/autoload_configs/callcenter.conf.xml', + //application 'application' => [ 'set' => '设置变量', @@ -17,10 +20,11 @@ return [ 'park' => '停泊', 'transfer' => '呼叫转移', 'info' => '显示信息', - + 'lua' => 'lua脚本', + 'detect_speech'=> 'detect_speech', ], - // + //队列响铃模式 'strategy' => [ 'ring-all' => '所有振铃', 'longest-idle-agent' => '空闲时长最长振铃', @@ -31,6 +35,18 @@ return [ 'sequentially-by-agent-order' => '优先级振铃', 'random' => '随机振铃', ], - + //坐席状态 + 'agent_status' => [ + 'Logged Out' => '签出', + 'Available' => '示闲', + 'Available (On Demand)' => '示闲(通话完成后自动示忙)', + 'On Break' => '示忙', + ], + //坐席呼叫状态 + 'agent_state' => [ + 'Idle' => '空闲中(不会分配话务)', + 'Waiting' => '空闲中(等待分配话务)', + 'In a queue call' => '通话中' + ], ]; diff --git a/database/migrations/2019_05_07_111537_create_gateway_table.php b/database/migrations/2019_05_07_111537_create_gateway_table.php index 155c0125..6db2d923 100644 --- a/database/migrations/2019_05_07_111537_create_gateway_table.php +++ b/database/migrations/2019_05_07_111537_create_gateway_table.php @@ -19,6 +19,8 @@ class CreateGatewayTable extends Migration $table->string('realm')->comment('网关IP,如果端口不是5060,默认格式为:xxx.xxx.xxx.xxx:port'); $table->string('username')->comment('帐号'); $table->string('password')->comment('密码'); + $table->string('prefix')->nullable()->comment('前缀'); + $table->string('outbound_caller_id')->nullable()->comment('出局号码'); $table->timestamps(); }); } diff --git a/database/migrations/2019_05_16_173543_create_callcenter_table.php b/database/migrations/2019_05_16_173543_create_callcenter_table.php index c61d9b9b..de392982 100644 --- a/database/migrations/2019_05_16_173543_create_callcenter_table.php +++ b/database/migrations/2019_05_16_173543_create_callcenter_table.php @@ -44,11 +44,11 @@ class CreateCallcenterTable extends Migration Schema::create('agents', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable()->comment('坐席号码'); - $table->string('system')->nullable()->comment('single_box'); + $table->string('system')->default('single_box')->comment('single_box'); $table->string('uuid')->nullable()->comment('uuid'); $table->string('type')->default('callback')->comment('callback 或者 uuid-standby'); $table->string('contact')->nullable()->comment('呼叫字符串'); - $table->string('status')->default('Available')->comment('坐席状态Logged Out签出,Available示闲,Available (On Demand),On Break休息/示忙'); + $table->string('status')->default('Available')->comment('坐席状态Logged Out签出,Available示闲,Available (On Demand)接通电话完成后示忙,On Break休息/示忙'); $table->string('state')->default('Waiting')->comment('坐席呼叫状态 Idle坐席空闲中,但是不会分配话务,Waiting坐席空闲中,正在等待分配话务,In a queue call正在通话'); $table->integer('max_no_answer')->default(0)->comment('最大无应答次数,超过次数status变为On Break状态'); $table->integer('wrap_up_time')->default(0)->comment('通话完成间隔时间,成功处理一个通话后,多久才会有电话进入的等待时长'); @@ -64,6 +64,7 @@ class CreateCallcenterTable extends Migration $table->integer('talk_time')->default(0); $table->integer('ready_time')->default(0); $table->integer('external_calls_count')->default(0); + $table->timestamps(); }); Schema::create('tiers', function (Blueprint $table) { diff --git a/database/migrations/2019_05_22_090855_create_task.php b/database/migrations/2019_05_22_090855_create_task.php new file mode 100644 index 00000000..23df47da --- /dev/null +++ b/database/migrations/2019_05_22_090855_create_task.php @@ -0,0 +1,38 @@ +increments('id'); + $table->string('name')->comment('任务名称'); + $table->timestamp('datetime_start')->nullable()->comment('任务开始时间'); + $table->timestamp('datetime_end')->nullable()->comment('任务结束时间'); + $table->integer('gateway_id')->comment('出局网关ID'); + $table->integer('queue_id')->nullable()->comment('转接队列ID'); + $table->integer('max_channel')->default(0)->comment('最大并发'); + $table->tinyInteger('status')->default(1)->comment('状态,1-停止,2-启动,3-完成'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('task'); + } +} diff --git a/database/migrations/2019_05_22_094625_create_call.php b/database/migrations/2019_05_22_094625_create_call.php new file mode 100644 index 00000000..c91dc134 --- /dev/null +++ b/database/migrations/2019_05_22_094625_create_call.php @@ -0,0 +1,44 @@ +increments('id'); + $table->unsignedInteger('task_id')->comment('对应任务ID'); + $table->string('phone')->comment('待呼叫号码'); + $table->tinyInteger('status')->default(1)->comment('1-待呼叫,2-呼叫中,3-队列中,4-接通成功,5-接通失败'); + $table->string('uuid')->nullable()->comment('通话UUID'); + $table->timestamp('datetime_originate')->nullable()->comment('呼叫时间'); + $table->timestamp('datetime_answer')->nullable()->comment('应答时间'); + $table->timestamp('datetime_entry_queue')->nullable()->comment('转接到队列时间'); + $table->timestamp('datetime_transfer_agent')->nullable()->comment('队列转接到坐席接听时间'); + $table->string('agent')->nullable()->comment('接听坐席'); + $table->timestamp('datetime_hangup')->nullable()->comment('挂断时间'); + $table->integer('duration')->default(0)->comment('通话时长'); + $table->string('fail_cause')->nullable()->comment('失败原因'); + $table->timestamps(); + $table->foreign('task_id')->references('id')->on('task')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('call'); + } +} diff --git a/database/seeds/UserTableSeeder.php b/database/seeds/UserTableSeeder.php index e6150ea4..034c86d8 100644 --- a/database/seeds/UserTableSeeder.php +++ b/database/seeds/UserTableSeeder.php @@ -155,7 +155,6 @@ class UserTableSeeder extends Seeder 'route' => 'admin.queue', 'icon_id' => '12', 'child' => [ - ['name' => 'pbx.queue.show', 'display_name' => '详情','route'=>'admin.queue.show'], ['name' => 'pbx.queue.create', 'display_name' => '添加','route'=>'admin.queue.create'], ['name' => 'pbx.queue.edit', 'display_name' => '编辑','route'=>'admin.queue.edit'], ['name' => 'pbx.queue.destroy', 'display_name' => '删除','route'=>'admin.queue.destroy'], @@ -163,6 +162,17 @@ class UserTableSeeder extends Seeder ['name' => 'pbx.queue.agent', 'display_name' => '分配分机','route'=>'admin.queue.agent'], ] ], + [ + 'name' => 'pbx.agent', + 'display_name' => '坐席管理', + 'route' => 'admin.agent', + 'icon_id' => '12', + 'child' => [ + ['name' => 'pbx.agent.create', 'display_name' => '添加','route'=>'admin.agent.create'], + ['name' => 'pbx.agent.edit', 'display_name' => '编辑','route'=>'admin.agent.edit'], + ['name' => 'pbx.agent.destroy', 'display_name' => '删除','route'=>'admin.agent.destroy'], + ] + ], ], ], [ @@ -184,6 +194,28 @@ class UserTableSeeder extends Seeder ], ] ], + [ + 'name' => 'ai.manage', + 'display_name' => '批量外呼', + 'route' => '', + 'icon_id' => '103', + 'child' => [ + [ + 'name' => 'ai.task', + 'display_name' => '任务管理', + 'route' => 'admin.task', + 'icon_id' => '12', + 'child' => [ + ['name' => 'ai.task.create', 'display_name' => '添加','route'=>'admin.task.create'], + ['name' => 'ai.task.edit', 'display_name' => '编辑','route'=>'admin.task.edit'], + ['name' => 'ai.task.destroy', 'display_name' => '删除','route'=>'admin.task.destroy'], + ['name' => 'ai.task.show', 'display_name' => '呼叫详情','route'=>'admin.task.show'], + ['name' => 'ai.task.setStatus', 'display_name' => '更新状态','route'=>'admin.task.setStatus'], + ['name' => 'ai.task.importCall', 'display_name' => '导入号码','route'=>'admin.task.importCall'], + ] + ], + ] + ], ]; foreach ($permissions as $pem1) { diff --git a/public/static/outgoing.csv b/public/static/outgoing.csv new file mode 100644 index 00000000..76cfebf0 --- /dev/null +++ b/public/static/outgoing.csv @@ -0,0 +1,7 @@ +18908221080 +13512293513 +13512293514 +13512293515 +13512293516 +13512293517 +13512293517 \ No newline at end of file diff --git a/resources/views/admin/agent/_form.blade.php b/resources/views/admin/agent/_form.blade.php new file mode 100644 index 00000000..983e206e --- /dev/null +++ b/resources/views/admin/agent/_form.blade.php @@ -0,0 +1,70 @@ +{{csrf_field()}} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ @foreach(config('freeswitch.agent_status') as $k=>$v) + status)&&$model->status==$k) || (!isset($model->status)&&$k=='Available') ) checked @endif > + @endforeach +
+
+
+ +
+ @foreach(config('freeswitch.agent_state') as $k=>$v) + state)&&$model->state==$k) || (!isset($model->state)&&$k=='Waiting')) checked @endif > + @endforeach +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + 返 回 +
+
\ No newline at end of file diff --git a/resources/views/admin/agent/create.blade.php b/resources/views/admin/agent/create.blade.php new file mode 100644 index 00000000..ff8fd9b7 --- /dev/null +++ b/resources/views/admin/agent/create.blade.php @@ -0,0 +1,14 @@ +@extends('admin.base') + +@section('content') +
+
+

添加坐席

+
+
+
+ @include('admin.agent._form') +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/agent/edit.blade.php b/resources/views/admin/agent/edit.blade.php new file mode 100644 index 00000000..2ce725ab --- /dev/null +++ b/resources/views/admin/agent/edit.blade.php @@ -0,0 +1,15 @@ +@extends('admin.base') + +@section('content') +
+
+

更新坐席

+
+
+
+ {{method_field('put')}} + @include('admin.agent._form') +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/admin/agent/index.blade.php b/resources/views/admin/agent/index.blade.php new file mode 100644 index 00000000..d9d61a14 --- /dev/null +++ b/resources/views/admin/agent/index.blade.php @@ -0,0 +1,100 @@ +@extends('admin.base') + +@section('content') +
+
+
+ + 添 加 +
+ +
+
+
+ +
+
+@endsection + +@section('script') + +@endsection \ No newline at end of file diff --git a/resources/views/admin/gateway/_form.blade.php b/resources/views/admin/gateway/_form.blade.php index b9be87d0..4f1fc0d1 100644 --- a/resources/views/admin/gateway/_form.blade.php +++ b/resources/views/admin/gateway/_form.blade.php @@ -23,6 +23,18 @@ +
+ +
+ +
+
+
+ +
+ +
+
diff --git a/resources/views/admin/gateway/index.blade.php b/resources/views/admin/gateway/index.blade.php index 7e10d8bb..29cc40c4 100644 --- a/resources/views/admin/gateway/index.blade.php +++ b/resources/views/admin/gateway/index.blade.php @@ -41,6 +41,8 @@ ,{field: 'realm', title: '地址'} ,{field: 'username', title: '帐号'} ,{field: 'password', title: '密码'} + ,{field: 'prefix', title: '前缀'} + ,{field: 'outbound_caller_id', title: '出局号码'} ,{field: 'created_at', title: '添加时间'} ,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作'} ]] diff --git a/resources/views/admin/queue/_form.blade.php b/resources/views/admin/queue/_form.blade.php index a43b5e65..a807189a 100644 --- a/resources/views/admin/queue/_form.blade.php +++ b/resources/views/admin/queue/_form.blade.php @@ -8,11 +8,7 @@
- @if(isset($model)) - - @else - - @endif +
diff --git a/resources/views/admin/queue/agent.blade.php b/resources/views/admin/queue/agent.blade.php new file mode 100644 index 00000000..4cccd354 --- /dev/null +++ b/resources/views/admin/queue/agent.blade.php @@ -0,0 +1,36 @@ +@extends('admin.base') + +@section('content') + +
+
+

队列【{{$queue->display_name}}】分配坐席

+
+
+
+ {{csrf_field()}} + {{method_field('put')}} +
+ +
+ @forelse($agents as $agent) + agents->isNotEmpty()&&$queue->agents->contains($agent) ? 'checked' : '' }} > + @empty +
还没有坐席
+ @endforelse +
+
+
+
+ + 返 回 +
+
+
+
+
+@endsection + + diff --git a/resources/views/admin/queue/index.blade.php b/resources/views/admin/queue/index.blade.php index ff57a51b..b5bac599 100644 --- a/resources/views/admin/queue/index.blade.php +++ b/resources/views/admin/queue/index.blade.php @@ -15,6 +15,7 @@ @@ -39,7 +40,8 @@ ,{field: 'id', title: 'ID', sort: true,width:80} ,{field: 'display_name', title: '名称'} ,{field: 'name', title: '标识'} - ,{field: 'strategy', title: '振铃策略'} + ,{field: 'strategy_name', title: '振铃策略'} + ,{field: 'agents_count', title: '坐席数'} ,{field: 'created_at', title: '添加时间'} ,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作'} ]] @@ -51,7 +53,7 @@ ,layEvent = obj.event; //获得 lay-event 对应的值 if(layEvent === 'del'){ layer.confirm('确认删除吗?', function(index){ - $.post("{{ route('admin.gateway.destroy') }}",{_method:'delete',ids:[data.id]},function (result) { + $.post("{{ route('admin.queue.destroy') }}",{_method:'delete',ids:[data.id]},function (result) { if (result.code==0){ obj.del(); //删除对应行(tr)的DOM结构 } @@ -61,7 +63,9 @@ }); }); } else if(layEvent === 'edit'){ - location.href = '/admin/gateway/'+data.id+'/edit'; + location.href = '/admin/queue/'+data.id+'/edit'; + } else if(layEvent === 'agent'){ + location.href = '/admin/queue/'+data.id+'/agent'; } }); @@ -77,7 +81,7 @@ } if (ids.length>0){ layer.confirm('确认删除吗?', function(index){ - $.post("{{ route('admin.gateway.destroy') }}",{_method:'delete',ids:ids},function (result) { + $.post("{{ route('admin.queue.destroy') }}",{_method:'delete',ids:ids},function (result) { if (result.code==0){ dataTable.reload() } @@ -93,8 +97,8 @@ //更新配置 $("#updateXml").click(function () { - layer.confirm('该操作将重新注册所有网关,确认操作吗?', function(index){ - $.post("{{ route('admin.gateway.updateXml') }}",{_method:'post',_token:'{{csrf_token()}}'},function (result) { + layer.confirm('该操作将更新所有队列信息,确认操作吗?', function(index){ + $.post("{{ route('admin.queue.updateXml') }}",{_method:'post',_token:'{{csrf_token()}}'},function (result) { var icon = result.code==0?6:5; layer.msg(result.msg,{icon:icon}) }); diff --git a/resources/views/admin/task/_form.blade.php b/resources/views/admin/task/_form.blade.php new file mode 100644 index 00000000..bf848ad7 --- /dev/null +++ b/resources/views/admin/task/_form.blade.php @@ -0,0 +1,53 @@ +{{csrf_field()}} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + 返 回 +
+
\ No newline at end of file diff --git a/resources/views/admin/task/_js.blade.php b/resources/views/admin/task/_js.blade.php new file mode 100644 index 00000000..7da4ee8c --- /dev/null +++ b/resources/views/admin/task/_js.blade.php @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/resources/views/admin/task/create.blade.php b/resources/views/admin/task/create.blade.php new file mode 100644 index 00000000..3fe088e0 --- /dev/null +++ b/resources/views/admin/task/create.blade.php @@ -0,0 +1,18 @@ +@extends('admin.base') + +@section('content') +
+
+

添加任务

+
+
+
+ @include('admin.task._form') +
+
+
+@endsection + +@section('script') + @include('admin.task._js') +@endsection diff --git a/resources/views/admin/task/edit.blade.php b/resources/views/admin/task/edit.blade.php new file mode 100644 index 00000000..ad25ef79 --- /dev/null +++ b/resources/views/admin/task/edit.blade.php @@ -0,0 +1,19 @@ +@extends('admin.base') + +@section('content') +
+
+

更新任务

+
+
+
+ {{method_field('put')}} + @include('admin.task._form') +
+
+
+@endsection + +@section('script') + @include('admin.task._js') +@endsection \ No newline at end of file diff --git a/resources/views/admin/task/index.blade.php b/resources/views/admin/task/index.blade.php new file mode 100644 index 00000000..39bbd4bb --- /dev/null +++ b/resources/views/admin/task/index.blade.php @@ -0,0 +1,199 @@ +@extends('admin.base') + +@section('content') +
+
+
+ + + + 添 加 + 模板下载 +
+
+
+
+ + + +
+
+@endsection + +@section('script') + +@endsection \ No newline at end of file diff --git a/resources/views/admin/task/show.blade.php b/resources/views/admin/task/show.blade.php new file mode 100644 index 00000000..930226a2 --- /dev/null +++ b/resources/views/admin/task/show.blade.php @@ -0,0 +1,172 @@ +@extends('admin.base') + +@section('content') +
+
+ 任务信息 + 返回 +
+
+
+
+
+
+ +
{{$task->name}}
+
+
+
+
+ +
{{$task->datetime_start}}
+
+
+
+
+ +
{{$task->datetime_end}}
+
+
+
+
+ +
{{$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 \ No newline at end of file diff --git a/routes/admin.php b/routes/admin.php index c2ec41c6..291df151 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -213,11 +213,49 @@ Route::group(['namespace'=>'Admin','prefix'=>'admin','middleware'=>['auth','perm //分配坐席 Route::get('queue/{id}/agent','QueueController@agent')->name('admin.queue.agent')->middleware('permission:pbx.queue.agent'); Route::put('queue/{id}/assignAgent','QueueController@assignAgent')->name('admin.queue.assignAgent')->middleware('permission:pbx.queue.agent'); + }); + //坐席管理 + Route::group(['middleware'=>'permission:pbx.agent'],function (){ + Route::get('agent','AgentController@index')->name('admin.agent'); + Route::get('agent/data','AgentController@data')->name('admin.agent.data'); + //添加 + Route::get('agent/create','AgentController@create')->name('admin.agent.create')->middleware('permission:pbx.agent.create'); + Route::post('agent/store','AgentController@store')->name('admin.agent.store')->middleware('permission:pbx.agent.create'); + //编辑 + Route::get('agent/{id}/edit','AgentController@edit')->name('admin.agent.edit')->middleware('permission:pbx.agent.edit'); + Route::put('agent/{id}/update','AgentController@update')->name('admin.agent.update')->middleware('permission:pbx.agent.edit'); + //删除 + Route::delete('agent/destroy','AgentController@destroy')->name('admin.agent.destroy')->middleware('permission:pbx.agent.destroy'); }); }); +//批量外呼 +Route::group(['namespace'=>'Admin','prefix'=>'admin','middleware'=>['auth','permission:ai.manage']],function (){ + + //任务管理 + Route::group(['middleware'=>'permission:ai.task'],function (){ + Route::get('task','TaskController@index')->name('admin.task'); + Route::get('task/data','TaskController@data')->name('admin.task.data'); + //详情 + Route::match(['get','post'],'task/{id}/show','TaskController@show')->name('admin.task.show'); + //添加 + Route::get('task/create','TaskController@create')->name('admin.task.create')->middleware('permission:ai.task.create'); + Route::post('task/store','TaskController@store')->name('admin.task.store')->middleware('permission:ai.task.create'); + //编辑 + Route::get('task/{id}/edit','TaskController@edit')->name('admin.task.edit')->middleware('permission:ai.task.edit'); + Route::put('task/{id}/update','TaskController@update')->name('admin.task.update')->middleware('permission:ai.task.edit'); + //删除 + Route::delete('task/destroy','TaskController@destroy')->name('admin.task.destroy')->middleware('permission:ai.task.destroy'); + //设置状态 + Route::post('task/setStatus','TaskController@setStatus')->name('admin.task.setStatus')->middleware('permission:ai.task.setStatus'); + //导入号码 + Route::post('task/{id}/importCall','TaskController@importCall')->name('admin.task.importCall')->middleware('permission:ai.task.importCall'); + }); + +}); + //录音管理 Route::group(['namespace'=>'Admin','prefix'=>'admin','middleware'=>['auth','permission:record.manage']],function (){ @@ -233,3 +271,5 @@ Route::group(['namespace'=>'Admin','prefix'=>'admin','middleware'=>['auth','perm Route::get('cdr/{id}/show','CdrController@show')->name('admin.cdr.show'); }); }); + +