完善webrtc打电话功能

This commit is contained in:
lilong 2019-10-31 17:40:29 +08:00
parent b6390db61b
commit b661c87eb9
127 changed files with 20620 additions and 5 deletions

View File

@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use App\Models\Call;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class asr_listen extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'asr:listen';
/**
* The console command description.
*
* @var string
*/
protected $description = 'asr listen';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//定义日志
$monolog = Log::getMonolog();
$monolog->popHandler();
Log::useFiles(storage_path('logs/asr_listen.log'));
$fs = new \Freeswitchesl();
if (!$fs->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
echo "ESL未连接";
return 1;
}
$fs->events('json', 'CUSTOM asr');
while (true){
$received_parameters = $fs->recvEvent();
if (!empty($received_parameters)) {
//记录日志
$info = $fs->serialize($received_parameters,"json");
Log::info($info);
$response = $fs->getHeader($received_parameters,"ASR-Response");
$response = json_decode(urldecode($response),true);
if($response['status_code']==200&&isset($response['result'])&&$response['result']['status_code']==0&&!empty($response['result']['text'])){
$text = $response['result']['text'];
$uuid = $fs->getHeader($received_parameters,"Core-UUID");
DB::table('cdr_asr')->insert([
'uuid' => $uuid,
'text' => $text,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
}
}
$fs->disconnect();
}
}

View File

@ -0,0 +1,129 @@
<?php
namespace App\Console\Commands;
use App\Models\Agent;
use App\Models\Call;
use Illuminate\Console\Command;
use Log;
class callcenter_listen extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'callcenter:listen';
/**
* The console command description.
*
* @var string
*/
protected $description = 'callcenter listen';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//定义日志
$monolog = Log::getMonolog();
$monolog->popHandler();
Log::useFiles(storage_path('logs/callcenter_listen.log'));
$fs = new \Freeswitchesl();
if (!$fs->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
echo "ESL未连接";
return 1;
}
$fs->events('json', 'CUSTOM callcenter::info');
while (true){
$received_parameters = $fs->recvEvent();
if (!empty($received_parameters)) {
//记录日志
$info = $fs->serialize($received_parameters,"json");
Log::info($info);
$action = $fs->getHeader($received_parameters,"CC-Action");
switch ($action){
//坐席状态
case 'agent-status-change':
$agent_name = $fs->getHeader($received_parameters,"CC-Agent");
$status = $fs->getHeader($received_parameters,"CC-Agent-Status");
Agent::where('name',$agent_name)->update(['status'=>$status]);
break;
//坐席呼叫状态
case 'agent-state-change':
$agent_name = $fs->getHeader($received_parameters,"CC-Agent");
$state = $fs->getHeader($received_parameters,"CC-Agent-State");
Agent::where('name',$agent_name)->update(['state'=>$state]);
break;
//呼叫进入队列
case 'member-queue-start':
$uuid = $fs->getHeader($received_parameters,"CC-Member-Session-UUID");
$datetime = urldecode($fs->getHeader($received_parameters,"variable_cc_queue_joined_epoch"));
Call::where('uuid',$uuid)->update(['datetime_entry_queue'=>date('Y-m-d H:i:s',$datetime),'status'=>3]);
break;
//呼叫坐席
case 'agent-offering':
$uuid = $fs->getHeader($received_parameters,"CC-Member-Session-UUID");
$agent_name = $fs->getHeader($received_parameters,"CC-Agent");
$datetime = urldecode($fs->getHeader($received_parameters,"Event-Date-Local"));
Call::where('uuid',$uuid)->update(['datetime_agent_called'=>$datetime,'agent_name'=>$agent_name]);
break;
// 坐席应答
case 'bridge-agent-start':
$uuid = $fs->getHeader($received_parameters,"CC-Member-Session-UUID");
$datetime = $fs->getHeader($received_parameters,"CC-Agent-Answered-Time");
Call::where('uuid',$uuid)->update(['datetime_agent_answered'=>date('Y-m-d H:i:s',$datetime),'status'=>4]);
break;
//坐席结束
case 'bridge-agent-end':
$uuid = $fs->getHeader($received_parameters,"CC-Member-Session-UUID");
$datetime = $fs->getHeader($received_parameters,"CC-Bridge-Terminated-Time");
Call::where('uuid',$uuid)->update(['datetime_end'=>date('Y-m-d H:i:s',$datetime),'status'=>4]);
break;
//桥接结束,通话结束
case 'member-queue-end':
$uuid = $fs->getHeader($received_parameters,"CC-Member-Session-UUID");
$cause = $fs->getHeader($received_parameters,"CC-Cause");
$answered_time = $fs->getHeader($received_parameters,"CC-Agent-Answered-Time");
$leaving_time = $fs->getHeader($received_parameters,"CC-Member-Leaving-Time");
$joined_time = $fs->getHeader($received_parameters,"CC-Member-Joined-Time");
if ($leaving_time && $joined_time){
$duration = $leaving_time - $joined_time > 0 ? $leaving_time - $joined_time : 0;
}else{
$duration = 0;
}
if ($leaving_time && $answered_time){
$billsec = $leaving_time - $answered_time > 0 ? $leaving_time - $answered_time : 0;
}else{
$billsec = 0;
}
Call::where('uuid',$uuid)->update([
'cause' => $cause,
'duration' => $duration,
'billsec' => $billsec,
]);
break;
default:
break;
}
}
}
$fs->disconnect();
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace App\Console\Commands;
use App\Models\Call;
use App\Models\Task;
use Carbon\Carbon;
use Faker\Provider\Uuid;
use Illuminate\Console\Command;
use Log;
class callcenter_start extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'callcenter:start';
/**
* The console command description.
*
* @var string
*/
protected $description = 'callcenter start';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//定义日志
$monolog = Log::getMonolog();
$monolog->popHandler();
Log::useFiles(storage_path('logs/task.log'));
$fs = new \Freeswitchesl();
if ($fs->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
while (true){
$now_date = date('Y-m-d');
$now_time = date('H:i:s');
$tasks = Task::with(['queue','gateway'])
->where('status',2)
->where('date_start','<=',$now_date)
->where('date_end','>=',$now_date)
->where('time_start','<=',$now_time)
->where('time_end','>=',$now_time)
->get();
//检测是否有启动的任务
if ($tasks->isEmpty()){
//延迟5秒
usleep(5000000);
continue;
}
//循环任务
foreach ($tasks as $task){
//检测网关信息
if ($task->gateway==null){
Log::info("任务ID".$task->name." 的网关不存在");
continue;
}
$gw_info = $fs->api("sofia status gateway gw".$task->gateway->id);
if (trim($gw_info)=='Invalid Gateway!'){
Log::info("任务ID ".$task->name."的网关 ".$task->gateway->name."的网关配置不存在");
continue;
}
$gw_status = 0;
foreach (explode("\n",$gw_info) as $str){
if (str_contains($str,"REGED")){
$gw_status = 1;
}
}
if ($gw_status==0){
Log::info("任务ID ".$task->name."的网关 ".$task->gateway->name."未注册成功");
continue;
}
//检测队列
if ($task->queue==null){
Log::info("任务ID".$task->name." 的队列不存在");
continue;
}
//并发数调节
$channel = 0;
if ($task->max_channel){
$channel = $task->max_channel;
}else{
//队列总空闲坐席数
$wait_num = $fs->api("callcenter_config queue count agents ".$task->queue->name." Available Waiting");
$channel = (int)$wait_num;
}
//如果通道数还是0则不需要呼叫
if ($channel==0) continue;
//否则进行呼叫
Log::info("任务:".$task->name." 将呼叫 ".$channel." 个号码");
$calls = Call::where('status',1)->orderBy('id','asc')->limit($channel)->get();
if ($calls->isEmpty()){
Log::info("任务:".$task->name."已完成");
$task->update(['status'=>3]);
continue;
}
foreach ($calls as $call){
$uuid = Uuid::uuid();
//更新为正在呼叫
$call->update(['status'=>2,'uuid'=>$uuid]);
Log::info("更新号码: ".$call->phone." 状态为2");
$phone = $task->gateway->prefix ? $task->gateway->prefix.$call->phone : $call->phone;
$var_str = "{origination_uuid=".$uuid."}";
if ($task->gateway->outbound_caller_id) $var_str = "{origination_uuid=".$uuid.",outbound_caller_id=".$task->gateway->outbound_caller_id."}";
$dail_string = "originate ".$var_str."sofia/gateway/gw".$task->gateway->id."/".$phone." &callcenter(".$task->queue->name.")";
Log::info("呼叫:".$dail_string);
$fs->bgapi($dail_string);
usleep(500);
}
}
}
return 0;
}else{
echo "ESL未连接";
}
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Action;
use App\Models\Condition;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ActionController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index($condition_id)
{
$condition = Condition::findOrFail($condition_id);
return view('admin.dialplan.action.index',compact('condition'));
}
public function data(Request $request,$condition_id)
{
$res = Action::where('condition_id',$condition_id)->orderBy('sort')->orderBy('id')->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($condition_id)
{
$condition = Condition::findOrFail($condition_id);
return view('admin.dialplan.action.create',compact('condition'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request,$condition_id)
{
$condition = Condition::findOrFail($condition_id);
$data = $request->all(['display_name','application','data','sort']);
$data['condition_id'] = $condition->id;
if (Action::create($data)){
return redirect(route('admin.action',['condition_id'=>$condition->id]))->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($condition_id,$id)
{
$condition = Condition::findOrFail($condition_id);
$model = Action::findOrFail($id);
return view('admin.dialplan.action.edit',compact('condition','model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $condition_id, $id)
{
$condition = Condition::findOrFail($condition_id);
$model = Action::findOrFail($id);
$data = $request->all(['display_name','application','data','sort']);
if ($model->update($data)){
return redirect(route('admin.action',['condition_id'=>$condition->id]))->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 (Action::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\AgentRequest;
use App\Models\Agent;
use App\Models\Sip;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class AgentController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.agent.index');
}
public function data(Request $request)
{
$res = Agent::orderByDesc('id')->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()
{
return view('admin.agent.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(AgentRequest $request)
{
$data = $request->all();
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);
return view('admin.agent.edit',compact('model'));
}
/**
* 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']);
if ($model->update($data)){
return redirect(route('admin.agent'))->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 (Agent::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
}

View File

@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Audio;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class AudioController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.audio.index');
}
public function data(Request $request)
{
$res = Audio::orderBy('id')->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()
{
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$text = $request->get('text');
if ($text==null){
return response()->json(['code'=>1,'msg'=>'请输入待合成文本']);
}
$url = 'http://api.xfyun.cn/v1/service/v1/tts';
$appid = config('freeswitch.xfyun.appid');
$apikey = config('freeswitch.xfyun.apikey');
$param = array (
'auf' => 'audio/L16;rate=8000',
'aue' => 'raw',
'voice_name' => 'xiaoyan',
'speed' => '50', //这三个参数必需是字符串
'volume' => '50', //这三个参数必需是字符串
'pitch' => '50', //这三个参数必需是字符串
'engine_type' => 'intp65',
);
$time = (string)time();
$xparam = base64_encode(json_encode(($param)));
$checksum = md5($apikey.$time.$xparam);
$header = array(
'X-CurTime:'.$time,
'X-Param:'.$xparam,
'X-Appid:'.$appid,
'X-CheckSum:'.$checksum,
'X-Real-Ip:127.0.0.1',
'Content-Type:application/x-www-form-urlencoded; charset=utf-8'
);
$content = [
'text' => $text,
];
try{
$response = $this->tocurl($url, $header, $content);
$header = $response['header'];
if($header['content_type'] == 'audio/mpeg'){
$ext = $param['aue']=='raw'?'.wav':'.mp3';
$filename = config('freeswitch.xfyun.sounds').$time.$ext;
file_put_contents($filename, $response['body']);
Audio::create(array_merge($param,[
'url' => $filename,
'text' => $text
]));
return response()->json(['code'=>0,'msg'=>'合成成功','data'=>['url'=>$filename]]);
}
return response()->json(['code'=>1,'msg'=>'合成失败','data'=>json_decode($response['body'],true)]);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'合成失败:'.$exception->getMessage()]);
}
}
/**
* 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)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request)
{
$ids = $request->get('ids');
$audio = Audio::where('id',$ids[0])->first();
if ($ids == null){
return response()->json(['code'=>1,'msg'=>'请选择删除项']);
}
try{
if (file_exists($audio->url)){
unlink($audio->url);
}
$audio->delete();
return response()->json(['code'=>0,'msg'=>'删除成功']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'删除失败:'.$exception->getMessage()]);
}
}
public function tocurl($url, $header, $content){
$ch = curl_init();
if(substr($url,0,5)=='https'){
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, true); // 从证书中检查SSL加密算法是否存在
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($content));
$response = curl_exec($ch);
$error=curl_error($ch);
//var_dump($error);
if($error){
die($error);
}
$header = curl_getinfo($ch);
curl_close($ch);
$data = array('header' => $header,'body' => $response);
return $data;
}
}

View File

@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\BillRequest;
use App\Models\Bill;
use App\Models\Merchant;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use DB;
class BillController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$merchants = Merchant::orderByDesc('id')->get();
return view('admin.bill.index',compact('merchants'));
}
public function data(Request $request)
{
$merchant_id = $request->post('merchant_id');
$type = $request->post('type');
$created_at_start = $request->post('created_at_start');
$created_at_end = $request->post('created_at_end');
$res = Bill::with('merchant')->orderBy('id','desc')
->when($merchant_id,function ($query) use ($merchant_id){
return $query->where('merchant_id',$merchant_id);
})
->when($type,function ($query) use ($type){
return $query->where('type',$type);
})
->when($created_at_start&&!$created_at_end,function ($query) use ($created_at_start){
return $query->where('created_at','>=',$created_at_start);
})
->when(!$created_at_start&&$created_at_end,function ($query) use ($created_at_end){
return $query->where('created_at','<=',$created_at_end);
})
->when($created_at_start&&$created_at_end,function ($query) use ($created_at_start,$created_at_end){
return $query->whereBetween('created_at',[$created_at_start,$created_at_end]);
})
->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()
{
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(BillRequest $request)
{
$data = $request->all(['merchant_id','type','money','remark']);
$merchant = Merchant::where('id',$data['merchant_id'])->first();
if (!$merchant){
return response()->json(['code'=>1, 'msg'=>'商户不存在']);
}
$data = array_prepend($data, $request->user()->id, 'created_user_id');
//金额转换
$data['money'] = $data['type']==1?abs($data['money']):-1*abs($data['money']);
//开启事务
DB::beginTransaction();
try{
DB::table('bill')->insert(array_merge($data,['created_at'=>Carbon::now(),'updated_at'=>Carbon::now()]));
DB::table('merchant')->where('id',$data['merchant_id'])->increment('money',$data['money']);
DB::commit();
return response()->json(['code'=>0,'msg'=>'操作成功']);
}catch (\Exception $exception){
DB::rollback();
return response()->json(['code'=>1,'msg'=>'操作失败:'.$exception->getMessage()]);
}
}
/**
* 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)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Asr;
use App\Models\Cdr;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CdrController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.cdr.index');
}
public function data(Request $request)
{
$query = Cdr::query();
$search = $request->all(['caller_id_number','destination_number','start_stamp_start','start_stamp_end']);
if ($search['caller_id_number']){
$query = $query->where('caller_id_number',$search['caller_id_number']);
}
if ($search['destination_number']){
$query = $query->where('destination_number',$search['destination_number']);
}
if ($search['start_stamp_start'] && !$search['start_stamp_end']){
$query = $query->where('start_stamp','>=',$search['start_stamp_start']);
}else if (!$search['start_stamp_start'] && $search['start_stamp_end']){
$query = $query->where('start_stamp','<=',$search['start_stamp_end']);
}else if ($search['start_stamp_start'] && $search['start_stamp_end']){
$query = $query->whereBetween('start_stamp',[$search['start_stamp_start'],$search['start_stamp_end']]);
}
$res = $query->orderByDesc('id')->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()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$cdr = Cdr::find($id);
$record = [];
if ($cdr!=null){
$record = Asr::whereIn('uuid',[$cdr->aleg_uuid,$cdr->bleg_uuid])->orderByDesc('id')->get();
}
return view('admin.cdr.asr',compact('record'));
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
/**
* 播放录音
* @param $uuid
* @return array
*/
public function play($uuid)
{
$cdr = Cdr::where(['aleg_uuid'=>$uuid])->first();
if ($cdr==null){
return ['code'=>'1','msg'=>'通话记录不存在'];
}
if (empty($cdr->sofia_record_file)){
return ['code'=>'1','msg'=>'未找到录音文件'];
}
return ['code'=>0,'msg'=>'请求成功','data'=>$cdr->sofia_record_file];
}
/**
* 下载录音
* @param $uuid
* @return \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function download($uuid)
{
$cdr = Cdr::where(['aleg_uuid'=>$uuid])->first();
if ($cdr==null){
return back()->withErrors(['error'=>'通话记录不存在']);
}
if (!file_exists($cdr->sofia_record_file)){
return back()->withErrors(['error'=>'未找到录音文件']);
}
return response()->download($cdr->sofia_record_file,$uuid.".wav");
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\ConditionRequest;
use App\Models\Condition;
use App\Models\Extension;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ConditionController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index($extension_id)
{
$extension = Extension::findOrFail($extension_id);
return view('admin.dialplan.condition.index',compact('extension'));
}
public function data(Request $request,$extension_id)
{
$res = Condition::where('extension_id',$extension_id)->orderBy('sort')->orderBy('id')->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($extension_id)
{
$extension = Extension::findOrFail($extension_id);
return view('admin.dialplan.condition.create',compact('extension'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(ConditionRequest $request,$extension_id)
{
$extension = Extension::findOrFail($extension_id);
$data = $request->all(['display_name','field','expression','break','sort']);
$data['extension_id'] = $extension->id;
if (Condition::create($data)){
return redirect(route('admin.condition',['extension_id'=>$extension->id]))->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($extension_id,$id)
{
$extension = Extension::findOrFail($extension_id);
$model = Condition::findOrFail($id);
return view('admin.dialplan.condition.edit',compact('extension','model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $extension_id, $id)
{
$extension = Extension::findOrFail($extension_id);
$model = Condition::findOrFail($id);
$data = $request->all(['display_name','field','expression','break','sort']);
if ($model->update($data)){
return redirect(route('admin.condition',['extension_id'=>$extension->id]))->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 (Condition::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Digits;
use function foo\func;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class DigitsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$ivr_id = $request->get('ivr_id');
return view('admin.digits.index',compact('ivr_id'));
}
public function data(Request $request)
{
$ivr_id = $request->get('ivr_id');
$res = Digits::when($ivr_id,function ($query) use ($ivr_id){
$query->where('ivr_id',$ivr_id);
})->orderByDesc('id')->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()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$data = $request->get('parm');
try{
foreach ($data as $d){
Digits::create($d);
}
return response()->json(['code'=>0,'msg'=>'添加成功']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'添加失败']);
}
}
/**
* 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)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\ExtensionRequest;
use App\Models\Extension;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class ExtensionController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.dialplan.extension.index');
}
public function data(Request $request)
{
$query = Extension::query();
$res = $query->orderBy('sort')->orderBy('id')->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()
{
return view('admin.dialplan.extension.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(ExtensionRequest $request)
{
$data = $request->all(['display_name','name','sort','continue','context']);
if (Extension::create($data)){
return redirect(route('admin.extension'))->with(['success'=>'添加成功']);
}
return back()->withErrors(['error'=>'添加失败']);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$extension = Extension::with('conditions')->findOrFail($id);
return view('admin.dialplan.extension.show',compact('extension'));
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
$model = Extension::findOrFail($id);
return view('admin.dialplan.extension.edit',compact('model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(ExtensionRequest $request, $id)
{
$model = Extension::findOrFail($id);
$data = $request->all(['display_name','name','sort','continue','context']);
if ($model->update($data)){
return redirect(route('admin.extension'))->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 (Extension::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\GatewayRequest;
use App\Models\Gateway;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class GatewayController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.gateway.index');
}
public function data(Request $request)
{
$res = Gateway::orderByDesc('id')->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()
{
return view('admin.gateway.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(GatewayRequest $request)
{
$data = $request->except(['_method','_token']);
if (Gateway::create($data)){
return redirect(route('admin.gateway'))->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 = Gateway::findOrFail($id);
return view('admin.gateway.edit',compact('model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(GatewayRequest $request, $id)
{
$model = Gateway::findOrFail($id);
$data = $request->except(['_method','_token']);
if ($model->update($data)){
return redirect(route('admin.gateway'))->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 (Gateway::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
public function updateXml()
{
set_time_limit(0);
$gateway = Gateway::get();
if ($gateway->isEmpty()){
return response()->json(['code'=>1,'msg'=>'无数据需要更新']);
}
try{
foreach ($gateway as $gw){
$xml = "<include>\n";
$xml .= " <gateway name=\"gw".$gw->id."\">\n";
$xml .= " <param name=\"username\" value=\"".$gw->username."\"/>\n";
$xml .= " <param name=\"realm\" value=\"".$gw->realm."\"/>\n";
$xml .= " <param name=\"password\" value=\"".$gw->password."\"/>\n";
$xml .= " </gateway>\n";
$xml .= "</include>";
file_put_contents(config('freeswitch.gateway_dir')."gw".$gw->id.".xml",$xml);
}
//生产环境并且debug关闭的情况下自动更新网关注册信息
if (config('app.env')=='production' && config('app.debug')==false){
$freeswitch = new \Freeswitchesl();
if (!$freeswitch->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
return response()->json(['code'=>1,'msg'=>'ESL未连接']);
}
$freeswitch->bgapi("sofia profile external rescan");
$freeswitch->disconnect();
return response()->json(['code'=>0,'msg'=>'更新成功']);
}
return response()->json(['code'=>1,'msg'=>'请在生产环境下更新配置']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'更新失败','data'=>$exception->getMessage()]);
}
}
}

View File

@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\IvrRequest;
use App\Models\Digits;
use App\Models\Ivr;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class IvrController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.ivr.index');
}
public function data(Request $request)
{
$res = Ivr::orderByDesc('id')->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()
{
return view('admin.ivr.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(IvrRequest $request)
{
$data = $request->all();
try{
Ivr::create($data);
return redirect()->to(route('admin.ivr'))->with(['success'=>'添加成功']);
}catch (\Exception $exception){
return back()->withInput()->withErrors('添加失败:'.$exception->getMessage());
}
}
/**
* 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 = Ivr::findOrFail($id);
return view('admin.ivr.edit',compact('model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(IvrRequest $request, $id)
{
$model = Ivr::findOrFail($id);
$data = $request->all();
try{
$model->update($data);
return redirect()->to(route('admin.ivr'))->with(['success'=>'更新成功']);
}catch (\Exception $exception){
return back()->withInput()->withErrors('更新失败:'.$exception->getMessage());
}
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request)
{
$ids = $request->get('ids');
$ivr = Ivr::whereIn('id',$ids)->first();
if ($ivr==null){
return response()->json(['code'=>1,'msg'=>'记录不存在']);
}
$digits = Digits::where('ivr_id',$ivr->id)->orWhere(function ($query) use ($ivr) {
$query->where('action','menu-sub')->where('param',$ivr->name);
})->count();
if ($digits){
return response()->json(['code'=>1,'msg'=>'IVR被使用禁止删除']);
}
try{
$ivr->delete();
return response()->json(['code'=>0,'msg'=>'删除成功']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'删除失败:'.$exception->getMessage()]);
}
}
public function updateXml()
{
set_time_limit(0);
$datas = Ivr::with('digits')->get();
if ($datas->isEmpty()){
return response()->json(['code'=>1,'msg'=>'无数据需要更新']);
}
try{
foreach ($datas as $data){
$xml ="<include>\n";
$xml .="<menu name=\"".$data->name."\"\n";
$xml .="greet-long=\"".$data->greet_long."\"\n";
$xml .="greet-short=\"".$data->greet_short."\"\n";
$xml .="invalid-sound=\"".$data->invalid_sound."\"\n";
$xml .="exit-sound=\"".$data->exit_sound."\"\n";
$xml .="confirm-macro=\"".$data->confirm_macro."\"\n";
$xml .="confirm-key=\"".$data->confirm_key."\"\n";
$xml .="tts-engine=\"".$data->tts_engine."\"\n";
$xml .="tts-voice=\"".$data->tts_voice."\"\n";
$xml .="confirm-attempts=\"".$data->confirm_attempts."\"\n";
$xml .="timeout=\"".$data->timeout."\"\n";
$xml .="inter-digit-timeout=\"".$data->inter_digit_timeout."\"\n";
$xml .="max-failures=\"".$data->max_failures."\"\n";
$xml .="max-timeouts=\"".$data->max_timeout."\"\n";
$xml .="digit-len=\"".$data->digit_len."\">\n";
if ($data->digits->isNotEmpty()){
foreach ($data->digits as $digit){
$xml .="<entry action=\"".$digit->action."\" digits=\"".$digit->digit."\" param=\"".$digit->param."\"/>\n";
}
}
$xml .="</menu>\n";
$xml .="</include>\n";
file_put_contents(config('freeswitch.ivr_dir').$data->name.".xml",$xml);
}
//生产环境并且debug关闭的情况下自动更新网关注册信息
if (config('app.env')=='production' && config('app.debug')==false){
$freeswitch = new \Freeswitchesl();
if (!$freeswitch->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
return response()->json(['code'=>1,'msg'=>'ESL未连接']);
}
$freeswitch->bgapi("reloadxml");
$freeswitch->disconnect();
return response()->json(['code'=>0,'msg'=>'更新成功']);
}
return response()->json(['code'=>1,'msg'=>'请在生产环境下更新配置']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'更新失败','data'=>$exception->getMessage()]);
}
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\MerchantCreateRequest;
use App\Http\Requests\MerchantRequest;
use App\Http\Requests\MerchantUpdateRequest;
use App\Models\Gateway;
use App\Models\Merchant;
use Faker\Provider\Uuid;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class MerchantController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.merchant.index');
}
public function data(Request $request)
{
$data = $request->all(['username','company_name','status','expires_at_start','expires_at_end']);
$res = Merchant::withCount('sips')->orderBy('id','desc')
->when($data['username'],function ($query) use ($data){
return $query->where('username','like','%'.$data['username'].'%');
})
->when($data['company_name'],function ($query) use ($data){
return $query->where('company_name','like','%'.$data['company_name'].'%');
})
->when($data['status'],function ($query) use ($data){
return $query->where('status',$data['status']);
})
->when($data['expires_at_start']&&!$data['expires_at_end'],function ($query) use ($data){
return $query->where('expires_at','>=',$data['expires_at_start']);
})
->when(!$data['expires_at_start']&&$data['expires_at_end'],function ($query) use ($data){
return $query->where('expires_at','<=',$data['expires_at_end']);
})
->when($data['expires_at_start']&&$data['expires_at_end'],function ($query) use ($data){
return $query->whereBetween('expires_at',[$data['expires_at_start'],$data['expires_at_end']]);
})
->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()
{
return view('admin.merchant.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(MerchantCreateRequest $request)
{
$data = $request->all();
$data = array_prepend($data, Uuid::uuid(), 'uuid');
$data = array_prepend($data, $request->user()->id, 'created_user_id');
$data['password'] = bcrypt($data['password']);
try{
Merchant::create($data);
return redirect()->to(route('admin.merchant'))->with(['success'=>'添加成功']);
}catch (\Exception $e){
return back()->withInput()->withErrors($e->getMessage());
}
}
/**
* 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 = Merchant::findOrFail($id);
return view('admin.merchant.edit',compact('model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(MerchantUpdateRequest $request, $id)
{
$model = Merchant::findOrFail($id);
$data = $request->all();
if (isset($data['password'])&&!empty($data['password'])){
$data['password'] = bcrypt($data['password']);
}else{
array_pull($data,'password');
}
try{
$model->update($data);
return redirect()->to(route('admin.merchant'))->with(['success'=>'更新成功']);
}catch (\Exception $e){
return back()->withErrors($e->getMessage());
}
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy(Request $request)
{
$ids = $request->get('ids');
//验证参数
if (!is_array($ids)||empty($ids)){
return response()->json(['code'=>1, 'msg'=>'请选择删除项']);
}
//删除
try{
Merchant::whereIn('id',$ids)->delete();
return response()->json(['code'=>0, 'msg'=>'删除成功']);
}catch (\Exception $exception){
return response()->json(['code'=>1, 'msg'=>$exception->getMessage()]);
}
}
/**
* 帐单
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function bill(Request $request)
{
$merchant_id = $request->get('merchant_id');
$merchant = Merchant::findOrFail($merchant_id);
return view('admin.merchant.bill',compact('merchant'));
}
public function gateway($id)
{
$merchant = Merchant::with('gateways')->findOrFail($id);
$gateways = Gateway::get();
foreach ($gateways as $gateway){
if ($merchant->gateways->isNotEmpty()){
foreach ($merchant->gateways as $g1){
if ($g1->id == $gateway->id){
$gateway->rate = $g1->pivot->rate;
}
}
}
}
return view('admin.merchant.gateway',compact('merchant','gateways'));
}
public function assignGateway(Request $request, $id)
{
$merchant = Merchant::with('gateways')->findOrFail($id);
$gateway_ids = $request->get('gateways',[]);
try{
$sync_data = [];
foreach ($gateway_ids as $v){
if (isset($v['id']) && is_numeric($v['rate'])){
$sync_data[$v['id']] = ['rate'=>$v['rate']];
}
}
$merchant->gateways()->sync($sync_data);
return redirect()->to(route('admin.merchant'))->with(['success'=>'更新成功']);
}catch (\Exception $exception){
return back()->withErrors('更新失败');
}
}
}

View File

@ -0,0 +1,211 @@
<?php
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;
use DB;
class QueueController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.queue.index');
}
public function data(Request $request)
{
$res = Queue::withCount(['agents'])->orderByDesc('id')->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()
{
return view('admin.queue.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(QueueRequest $request)
{
$data = $request->all();
if (Queue::create($data)){
return redirect(route('admin.queue'))->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 = Queue::findOrFail($id);
return view('admin.queue.edit',compact('model'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(QueueRequest $request, $id)
{
$model = Queue::findOrFail($id);
$data = $request->except(['_method','_token']);
if ($model->update($data)){
return redirect(route('admin.queue'))->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 (Queue::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功,请更新配置']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
public function updateXml()
{
$queues = Queue::with('agents')->whereHas('agents')->get();
if ($queues->isEmpty()){
return response()->json(['code'=>1,'msg'=>'无数据需要更新']);
}
try{
//生产环境并且debug关闭的情况下自动更新网关注册信息
if (config('app.env')=='production' && config('app.debug')==false){
$freeswitch = new \Freeswitchesl();
if (!$freeswitch->connect(config('freeswitch.event_socket.host'), config('freeswitch.event_socket.port'), config('freeswitch.event_socket.password'))){
return response()->json(['code'=>1,'msg'=>'ESL未连接']);
}
$queues = Queue::with("agents")->get();
$agents = Agent::get();
$xml = "<configuration name=\"callcenter.conf\" description=\"CallCenter\">\n";
$xml .= " <settings>\n";
$xml .= " <!--<param name=\"odbc-dsn\" value=\"dsn:user:pass\"/>-->\n";
$xml .= " <!--<param name=\"dbname\" value=\"/dev/shm/callcenter.db\"/>-->\n";
$xml .= " <!--<param name=\"cc-instance-id\" value=\"single_box\"/>-->\n";
$xml .= " <param name=\"truncate-tiers-on-load\" value=\"true\"/>\n";
$xml .= " <param name=\"truncate-agents-on-load\" value=\"true\"/>\n";
$xml .= " </settings>\n";
//---------------------------------- 写入队列信息 ------------------------------------
$xml .= "<queues>\n";
foreach ($queues as $queue){
$xml .= " <queue name=\"".$queue->name."\">\n";
$xml .= " <param name=\"strategy\" value=\"".$queue->strategy."\"/>\n";
$xml .= " <param name=\"moh-sound\" value=\"".$queue->moh_sound."\"/>\n";
$xml .= " <param name=\"record-template\" value=\"\$\${recordings_dir}/\${strftime(%Y)}/\${strftime(%m)}/\${strftime(%d)}/.\${destination_number}.\${caller_id_number}.\${uuid}.wav\"/>\n";
$xml .= " <param name=\"time-base-score\" value=\"".$queue->time_base_score."\"/>\n";
$xml .= " <param name=\"max-wait-time\" value=\"".$queue->max_wait_time."\"/>\n";
$xml .= " <param name=\"max-wait-time-with-no-agent\" value=\"".$queue->max_wait_time_with_no_agent."\"/>\n";
$xml .= " <param name=\"max-wait-time-with-no-agent-time-reached\" value=\"".$queue->max_wait_time_with_no_agent_time_reached."\"/>\n";
$xml .= " <param name=\"tier-rules-apply\" value=\"".$queue->tier_rules_apply."\"/>\n";
$xml .= " <param name=\"tier-rule-wait-second\" value=\"".$queue->tier_rule_wait_second."\"/>\n";
$xml .= " <param name=\"tier-rule-wait-multiply-level\" value=\"".$queue->tier_rule_wait_multiply_level."\"/>\n";
$xml .= " <param name=\"tier-rule-no-agent-no-wait\" value=\"".$queue->tier_rule_no_agent_no_wait."\"/>\n";
$xml .= " <param name=\"discard-abandoned-after\" value=\"".$queue->discard_abandoned_after."\"/>\n";
$xml .= " <param name=\"abandoned-resume-allowed\" value=\"".$queue->abandoned_resume_allowed."\"/>\n";
$xml .= " </queue>\n";
}
$xml .= "</queues>\n";
//---------------------------------- 写入坐席信息 ------------------------------------
$xml .= "<agents>\n";
foreach ($agents as $agent){
$xml .= "<agent name=\"".$agent->name."\" type=\"".$agent->type."\" contact=\"[leg_timeout=10]".$agent->originate_type."/".$agent->originate_number."\" status=\"".$agent->status."\" max-no-answer=\"".$agent->max_no_answer."\" wrap-up-time=\"".$agent->wrap_up_time."\" reject-delay-time=\"".$agent->reject_delay_time."\" busy-delay-time=\"".$agent->busy_delay_time."\" no-answer-delay-time=\"".$agent->no_answer_delay_time."\" />\n";
}
$xml .= "</agents>\n";
//---------------------------------- 写入队列-坐席信息 ------------------------------------
$xml .= "<tiers>\n";
foreach ($queues as $queue){
foreach ($queue->agents as $agent){
$xml .= "<tier agent=\"".$agent->name."\" queue=\"".$queue->name."\" level=\"1\" position=\"1\"/>\n";
}
}
$xml .= "</tiers>\n";
$xml .= "</configuration>\n";
//生成配置文件
file_put_contents(config('freeswitch.callcenter_dir'),$xml);
$freeswitch->bgapi("reload mod_callcenter");
$freeswitch->disconnect();
return response()->json(['code'=>0,'msg'=>'更新成功']);
}
return response()->json(['code'=>1,'msg'=>'请在生产环境下更新配置']);
}catch (\Exception $exception){
return response()->json(['code'=>1,'msg'=>'更新失败','data'=>$exception->getMessage()]);
}
}
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(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'=>'更新失败']);
}
}

View File

@ -0,0 +1,223 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\SipListRequest;
use App\Http\Requests\SipRequest;
use App\Models\Merchant;
use App\Models\Sip;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Validator;
class SipController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.sip.index');
}
public function data(Request $request)
{
$query = Sip::query();
$username = $request->get('username');
if ($username){
$query = $query->where('username',$username);
}
$res = $query->orderByDesc('id')->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()
{
$merchants = Merchant::orderByDesc('id')->where('status',1)->get();
return view('admin.sip.create',compact('merchants'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(SipRequest $request)
{
$data = $request->all([
'merchant_gateway',
'gateway_id',
'expense_id',
'merchant_id',
'username',
'password',
'effective_caller_id_name',
'effective_caller_id_number',
'outbound_caller_id_name',
'outbound_caller_id_number',
]);
$mg = explode(',',$data['merchant_gateway']);
$data['merchant_id'] = $mg[0];
$data['gateway_id'] = $mg[1];
if ($data['effective_caller_id_name']==null){
$data['effective_caller_id_name'] = $data['username'];
}
if ($data['effective_caller_id_number']==null){
$data['effective_caller_id_number'] = $data['username'];
}
//验证商户允许的最大分机数
$merchant = Merchant::withCount('sips')->findOrFail($data['merchant_id']);
if ($merchant->sips_count >= $merchant->sip_num){
return back()->withInput()->withErrors(['error'=>'添加失败:超出商户最大允许分机数量']);
}
try{
Sip::create($data);
return redirect(route('admin.sip'))->with(['success'=>'添加成功']);
}catch (\Exception $exception){
return back()->withInput()->withErrors(['error'=>'添加失败:'.$exception->getMessage()]);
}
}
/**
* 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 = Sip::findOrFail($id);
$merchants = Merchant::orderByDesc('id')->where('status',1)->get();
return view('admin.sip.edit',compact('model','merchants'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(SipRequest $request, $id)
{
$model = Sip::findOrFail($id);
$data = $request->all([
'merchant_gateway',
'gateway_id',
'expense_id',
'merchant_id',
'username',
'password',
'effective_caller_id_name',
'effective_caller_id_number',
'outbound_caller_id_name',
'outbound_caller_id_number',
]);
$mg = explode(',',$data['merchant_gateway']);
$data['merchant_id'] = $mg[0];
$data['gateway_id'] = $mg[1];
if ($data['effective_caller_id_name']==null){
$data['effective_caller_id_name'] = $data['username'];
}
if ($data['effective_caller_id_number']==null){
$data['effective_caller_id_number'] = $data['username'];
}
try{
$model->update($data);
return redirect(route('admin.sip'))->with(['success'=>'更新成功']);
}catch (\Exception $exception){
return back()->withErrors(['error'=>'更新失败:'.$exception->getMessage()]);
}
}
/**
* 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 (Sip::destroy($ids)){
return response()->json(['code'=>0,'msg'=>'删除成功']);
}
return response()->json(['code'=>1,'msg'=>'删除失败']);
}
public function createList()
{
$merchants = Merchant::orderByDesc('id')->where('status',1)->get();
return view('admin.sip.create_list',compact('merchants'));
}
public function storeList(SipListRequest $request)
{
$data = $request->all(['sip_start','sip_end','password','merchant_gateway']);
$mg = explode(',',$data['merchant_gateway']);
$data['merchant_id'] = $mg[0];
$data['gateway_id'] = $mg[1];
if ($data['sip_start'] <= $data['sip_end']){
//验证商户允许的最大分机数
$merchant = Merchant::withCount('sips')->findOrFail($data['merchant_id']);
if (($merchant->sips_count+($data['sip_end']-$data['sip_start']+1)) >= $merchant->sip_num){
return back()->withInput()->withErrors(['error'=>'添加失败:超出商户最大允许分机数量']);
}
//开启事务
DB::beginTransaction();
try{
for ($i=$data['sip_start'];$i<=$data['sip_end'];$i++){
DB::table('sip')->insert([
'merchant_id' => $data['merchant_id'],
'gateway_id' => $data['gateway_id'],
'username' => $i,
'password' => $data['password'],
'effective_caller_id_name' => $i,
'effective_caller_id_number' => $i,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
]);
}
DB::commit();
return redirect(route('admin.sip'))->with(['success'=>'添加成功']);
}catch (\Exception $e) {
DB::rollback();
return back()->withInput()->withErrors(['error'=>'添加失败:'.$e->getMessage()]);
}
}
return back()->withInput()->withErrors(['error'=>'开始分机号必须小于等于结束分机号']);
}
}

View File

@ -0,0 +1,241 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\TaskRequest;
use App\Models\Agent;
use App\Models\Gateway;
use App\Models\Queue;
use App\Models\Task;
use Carbon\Carbon;
use Faker\Provider\Uuid;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
use DB;
use Log;
class TaskController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.task.index');
}
public function data(Request $request)
{
$res = Task::orderByDesc('id')->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('queue_agent')->where('queue_id',$task->queue_id)->pluck('agent_id');
$agents = Agent::whereIn('id',$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()]);
}
}

View File

@ -51,4 +51,371 @@ class ApiController extends Controller
}
return response()->json($data);
}
/**
* 分机动态注册
* @param Request $request
* @return bool
*/
public function directory(Request $request)
{
$sips = Sip::get();
$groups = Group::with('sips')->whereHas('sips')->get();
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
$xml .= "<document type=\"freeswitch/xml\">\n";
$xml .= "<section name=\"directory\" >\n";
$xml .= "<domain name=\"\$\${domain}\">\n";
$xml .= "<params>\n";
$xml .= "<param name=\"dial-string\" value=\"{presence_id=\${dialed_user}@\${dialed_domain}}\${sofia_contact(\${dialed_user}@\${dialed_domain})}\"/>\n";
$xml .= "</params>\n";
$xml .= "<groups>\n";
//默认用户组default
$xml .= "<group name=\"default\">\n";
$xml .= " <users>\n";
foreach ($sips as $sip){
$outbound_caller_id_number = $sip->outbound_caller_id_number??"\$\${outbound_caller_id}";
$xml .= " <user id=\"".$sip->username."\">\n";
$xml .= " <params>";
$xml .= " <param name=\"password\" value=\"".$sip->password."\"/>\n";
$xml .= " <param name=\"vm-password\" value=\"".$sip->password."\"/>\n";
$xml .= " </params>\n";
$xml .= " <variables>\n";
$xml .= " <variable name=\"toll_allow\" value=\"domestic,international,local\"/>\n";
$xml .= " <variable name=\"accountcode\" value=\"".$sip->username."\"/>\n";
$xml .= " <variable name=\"user_context\" value=\"".$sip->context."\"/>\n";
$xml .= " <variable name=\"effective_caller_id_name\" value=\"".$sip->effective_caller_id_name."\"/>\n";
$xml .= " <variable name=\"effective_caller_id_number\" value=\"".$sip->effective_caller_id_number."\"/>\n";
$xml .= " <variable name=\"outbound_caller_id_name\" value=\"\$\${outbound_caller_name}\"/>\n";
$xml .= " <variable name=\"outbound_caller_id_number\" value=\"".$outbound_caller_id_number."\"/>\n";
$xml .= " </variables>\n";
$xml .= " </user>";
}
$xml .= " </users>\n";
$xml .= "</group>\n";
//自定义用户组
foreach ($groups as $group){
$xml .= "<group name=\"".$group->name."\">\n";
$xml .= " <users>\n";
foreach ($group->sips as $sip){
$xml .= " <user id=\"".$sip->username."\" type=\"pointer\"/>";
}
$xml .= " </users>\n";
$xml .= "</group>\n";
}
$xml .= "</groups>\n";
$xml .= "</domain>\n";
$xml .= "</section>\n";
$xml .= "</document>\n";
return response($xml,200)->header("Content-type","text/xml");
}
/**
* 动态拨号计划
* @param Request $request
* @return mixed
*/
public function dialplan(Request $request)
{
if ($request->get('section')=='dialplan'){
$context = $request->get('Caller-Context','default');
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
$xml .= "<document type=\"freeswitch/xml\">\n";
$xml .= "<section name=\"dialplan\" description=\"RE Dial Plan For FreeSwitch\">\n";
$xml .= "<context name=\"".$context."\">\n";
//拨号计划
$extension = Extension::with('conditions')->whereHas('conditions')->where('context',$context)->orderBy('sort')->orderBy('id')->get();
foreach ($extension as $exten){
$xml .= "<extension name=\"" . $exten->name . "\" continue=\"" . $exten->continue . "\" >\n";
if ($exten->conditions->isNotEmpty()){
foreach ($exten->conditions as $condition){
$xml .= "<condition field=\"" . $condition->field . "\" expression=\"" . $condition->expression . "\" break=\"" . $condition->break . "\">\n";
if ($condition->actions->isNotEmpty()){
foreach ($condition->actions as $action){
$xml .= "<action application=\"" . $action->application . "\" data=\"" . $action->data . "\" />\n";
}
}
$xml .= "</condition>\n";
}
}
$xml .= "</extension>\n";
}
$xml .= "</context>\n";
$xml .= "</section>\n";
$xml .= "</document>\n";
return response($xml,200)->header("Content-type","text/xml");
}
}
/**
* 动态configuration 包含动态网关
* @param Request $request
* @return mixed
*/
public function configuration(Request $request)
{
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
$xml .= "<document type=\"freeswitch/xml\">\n";
$xml .= "<section name=\"configuration\" description=\"FreeSwitch configuration\">\n";
$xml .= "<configuration name=\"sofia.conf\" description=\"sofia Endpoint\">\n";
$xml .= " <global_settings>\n";
$xml .= " <param name=\"log-level\" value=\"0\"/>\n";
$xml .= " <!-- <param name=\"auto-restart\" value=\"false\"/> -->\n";
$xml .= " <param name=\"debug-presence\" value=\"0\"/>\n";
$xml .= " <!-- <param name=\"capture-server\" value=\"udp:homer.domain.com:5060\"/> -->\n";
$xml .= " <!-- <param name=\"capture-server\" value=\"udp:homer.domain.com:5060;hep=3;capture_id=100\"/> -->\n";
$xml .= " </global_settings>\n";
$xml .= " <profiles>\n";
$xml .= " <profile name=\"external\">\n";
$xml .= " <gateways>\n";
$gateways = Gateway::orderByDesc('id')->get();
foreach ($gateways as $gateway){
$xml .= " <gateway name=\"gw".$gateway->id."\">\n";
$xml .= " <param name=\"username\" value=\"".$gateway->username."\"/>\n";
$xml .= " <param name=\"realm\" value=\"".$gateway->realm."\"/>\n";
$xml .= " <param name=\"password\" value=\"".$gateway->password."\"/>\n";
$xml .= " </gateway>\n";
}
$xml .= " </gateways>\n";
$xml .= " <aliases>\n";
$xml .= " </aliases>\n";
$xml .= " <domains>\n";
$xml .= " <domain name=\"all\" alias=\"false\" parse=\"true\"/>\n";
$xml .= " </domains>\n";
$xml .= " <settings>\n";
$xml .= " <param name=\"debug\" value=\"0\"/>\n";
$xml .= " <!-- If you want FreeSWITCH to shutdown if this profile fails to load, uncomment the next line. -->\n";
$xml .= " <!-- <param name=\"shutdown-on-fail\" value=\"true\"/> -->\n";
$xml .= " <param name=\"sip-trace\" value=\"no\"/>\n";
$xml .= " <param name=\"sip-capture\" value=\"no\"/>\n";
$xml .= " <param name=\"rfc2833-pt\" value=\"101\"/>\n";
$xml .= " <!-- RFC 5626 : Send reg-id and sip.instance -->\n";
$xml .= " <!--<param name=\"enable-rfc-5626\" value=\"true\"/> -->\n";
$xml .= " <param name=\"sip-port\" value=\"\$\${external_sip_port}\"/>\n";
$xml .= " <param name=\"dialplan\" value=\"XML\"/>\n";
$xml .= " <param name=\"context\" value=\"public\"/>\n";
$xml .= " <param name=\"dtmf-duration\" value=\"2000\"/>\n";
$xml .= " <param name=\"inbound-codec-prefs\" value=\"\$$\{global_codec_prefs}\"/>\n";
$xml .= " <param name=\"outbound-codec-prefs\" value=\"\$\${outbound_codec_prefs}\"/>\n";
$xml .= " <param name=\"hold-music\" value=\"\$\${hold_music}\"/>\n";
$xml .= " <param name=\"rtp-timer-name\" value=\"soft\"/>\n";
$xml .= " <!--<param name=\"enable-100rel\" value=\"true\"/>-->\n";
$xml .= " <!--<param name=\"disable-srv503\" value=\"true\"/>-->\n";
$xml .= " <!-- This could be set to \"passive\" -->\n";
$xml .= " <param name=\"local-network-acl\" value=\"localnet.auto\"/>\n";
$xml .= " <param name=\"manage-presence\" value=\"false\"/>\n";
$xml .= " <!-- Name of the db to use for this profile -->\n";
$xml .= " <!--<param name=\"dbname\" value=\"share_presence\"/>-->\n";
$xml .= " <!--<param name=\"presence-hosts\" value=\"\$\${domain}\"/>-->\n";
$xml .= " <!--<param name=\"force-register-domain\" value=\"\$\${domain}\"/>-->\n";
$xml .= " <!--all inbound reg will stored in the db using this domain -->\n";
$xml .= " <!--<param name=\"force-register-db-domain\" value=\"\$\${domain}\"/>--> \n";
$xml .= " <!--<param name=\"aggressive-nat-detection\" value=\"true\"/>-->\n";
$xml .= " <param name=\"inbound-codec-negotiation\" value=\"generous\"/>\n";
$xml .= " <param name=\"nonce-ttl\" value=\"60\"/>\n";
$xml .= " <param name=\"auth-calls\" value=\"false\"/>\n";
$xml .= " <param name=\"inbound-late-negotiation\" value=\"true\"/>\n";
$xml .= " <param name=\"inbound-zrtp-passthru\" value=\"true\"/>\n";
$xml .= " <param name=\"rtp-ip\" value=\"\$\${local_ip_v4}\"/>\n";
$xml .= " <param name=\"sip-ip\" value=\"\$\${local_ip_v4}\"/>\n";
$xml .= " <param name=\"ext-rtp-ip\" value=\"auto-nat\"/>\n";
$xml .= " <param name=\"ext-sip-ip\" value=\"auto-nat\"/>\n";
$xml .= " <param name=\"rtp-timeout-sec\" value=\"300\"/>\n";
$xml .= " <param name=\"rtp-hold-timeout-sec\" value=\"1800\"/>\n";
$xml .= " <!--<param name=\"enable-3pcc\" value=\"true\"/>-->\n";
$xml .= " <param name=\"tls\" value=\"\$\${external_ssl_enable}\"/>\n";
$xml .= " <param name=\"tls-only\" value=\"false\"/>\n";
$xml .= " <param name=\"tls-bind-params\" value=\"transport=tls\"/>\n";
$xml .= " <param name=\"tls-sip-port\" value=\"\$\${external_tls_port}\"/>\n";
$xml .= " <!--<param name=\"tls-cert-dir\" value=\"\"/>-->\n";
$xml .= " <param name=\"tls-passphrase\" value=\"\"/>\n";
$xml .= " <!-- Verify the date on TLS certificates -->\n";
$xml .= " <param name=\"tls-verify-date\" value=\"true\"/>\n";
$xml .= " <param name=\"tls-verify-policy\" value=\"none\"/>\n";
$xml .= " <param name=\"tls-verify-depth\" value=\"2\"/>\n";
$xml .= " <param name=\"tls-verify-in-subjects\" value=\"\"/>\n";
$xml .= " <param name=\"tls-version\" value=\"\$\${sip_tls_version}\"/>\n";
$xml .= " </settings>\n";
$xml .= " </profile>\n";
//$xml .= file_get_contents('etc_freeswitch/sip_profiles/internal.xml');
$xml .= " </profiles>\n";
$xml .= "</configuration>\n";
$xml .= "</section>\n";
$xml .= "</document>\n";
return response($xml,200)->header("Content-type","text/xml");
}
/**
* 动态configuration 包含动态网关
* @param Request $request
* @return mixed
*/
public function configuration1(Request $request)
{
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n";
$xml .= "<document type=\"freeswitch/xml\">\n";
$xml .= "<section name=\"configuration\" description=\"FreeSwitch configuration\">\n";
foreach (scandir('etc_freeswitch/autoload_configs/') as $conf){
if ($conf=='.'||$conf=='..')
{
continue;
}elseif ($conf=='sofia.conf.xml') {
$xml .= "<configuration name=\"sofia.conf\" description=\"sofia Endpoint\">\n";
$xml .= " <global_settings>\n";
$xml .= " <param name=\"log-level\" value=\"0\"/>\n";
$xml .= " <!-- <param name=\"auto-restart\" value=\"false\"/> -->\n";
$xml .= " <param name=\"debug-presence\" value=\"0\"/>\n";
$xml .= " <!-- <param name=\"capture-server\" value=\"udp:homer.domain.com:5060\"/> -->\n";
$xml .= " <!-- <param name=\"capture-server\" value=\"udp:homer.domain.com:5060;hep=3;capture_id=100\"/> -->\n";
$xml .= " </global_settings>\n";
$xml .= " <profiles>\n";
$xml .= " <profile name=\"external\">\n";
$xml .= " <gateways>\n";
$gateways = Gateway::orderByDesc('id')->get();
foreach ($gateways as $gateway){
$xml .= " <gateway name=\"gw".$gateway->id."\">\n";
$xml .= " <param name=\"username\" value=\"".$gateway->username."\"/>\n";
$xml .= " <param name=\"realm\" value=\"".$gateway->realm."\"/>\n";
$xml .= " <param name=\"password\" value=\"".$gateway->password."\"/>\n";
$xml .= " </gateway>\n";
}
$xml .= " </gateways>\n";
$xml .= " <aliases>\n";
$xml .= " </aliases>\n";
$xml .= " <domains>\n";
$xml .= " <domain name=\"all\" alias=\"false\" parse=\"true\"/>\n";
$xml .= " </domains>\n";
$xml .= " <settings>\n";
$xml .= " <param name=\"debug\" value=\"0\"/>\n";
$xml .= " <!-- If you want FreeSWITCH to shutdown if this profile fails to load, uncomment the next line. -->\n";
$xml .= " <!-- <param name=\"shutdown-on-fail\" value=\"true\"/> -->\n";
$xml .= " <param name=\"sip-trace\" value=\"no\"/>\n";
$xml .= " <param name=\"sip-capture\" value=\"no\"/>\n";
$xml .= " <param name=\"rfc2833-pt\" value=\"101\"/>\n";
$xml .= " <!-- RFC 5626 : Send reg-id and sip.instance -->\n";
$xml .= " <!--<param name=\"enable-rfc-5626\" value=\"true\"/> -->\n";
$xml .= " <param name=\"sip-port\" value=\"\$\${external_sip_port}\"/>\n";
$xml .= " <param name=\"dialplan\" value=\"XML\"/>\n";
$xml .= " <param name=\"context\" value=\"public\"/>\n";
$xml .= " <param name=\"dtmf-duration\" value=\"2000\"/>\n";
$xml .= " <param name=\"inbound-codec-prefs\" value=\"\$$\{global_codec_prefs}\"/>\n";
$xml .= " <param name=\"outbound-codec-prefs\" value=\"\$\${outbound_codec_prefs}\"/>\n";
$xml .= " <param name=\"hold-music\" value=\"\$\${hold_music}\"/>\n";
$xml .= " <param name=\"rtp-timer-name\" value=\"soft\"/>\n";
$xml .= " <!--<param name=\"enable-100rel\" value=\"true\"/>-->\n";
$xml .= " <!--<param name=\"disable-srv503\" value=\"true\"/>-->\n";
$xml .= " <!-- This could be set to \"passive\" -->\n";
$xml .= " <param name=\"local-network-acl\" value=\"localnet.auto\"/>\n";
$xml .= " <param name=\"manage-presence\" value=\"false\"/>\n";
$xml .= " <!-- Name of the db to use for this profile -->\n";
$xml .= " <!--<param name=\"dbname\" value=\"share_presence\"/>-->\n";
$xml .= " <!--<param name=\"presence-hosts\" value=\"\$\${domain}\"/>-->\n";
$xml .= " <!--<param name=\"force-register-domain\" value=\"\$\${domain}\"/>-->\n";
$xml .= " <!--all inbound reg will stored in the db using this domain -->\n";
$xml .= " <!--<param name=\"force-register-db-domain\" value=\"\$\${domain}\"/>--> \n";
$xml .= " <!--<param name=\"aggressive-nat-detection\" value=\"true\"/>-->\n";
$xml .= " <param name=\"inbound-codec-negotiation\" value=\"generous\"/>\n";
$xml .= " <param name=\"nonce-ttl\" value=\"60\"/>\n";
$xml .= " <param name=\"auth-calls\" value=\"false\"/>\n";
$xml .= " <param name=\"inbound-late-negotiation\" value=\"true\"/>\n";
$xml .= " <param name=\"inbound-zrtp-passthru\" value=\"true\"/>\n";
$xml .= " <param name=\"rtp-ip\" value=\"\$\${local_ip_v4}\"/>\n";
$xml .= " <param name=\"sip-ip\" value=\"\$\${local_ip_v4}\"/>\n";
$xml .= " <param name=\"ext-rtp-ip\" value=\"auto-nat\"/>\n";
$xml .= " <param name=\"ext-sip-ip\" value=\"auto-nat\"/>\n";
$xml .= " <param name=\"rtp-timeout-sec\" value=\"300\"/>\n";
$xml .= " <param name=\"rtp-hold-timeout-sec\" value=\"1800\"/>\n";
$xml .= " <!--<param name=\"enable-3pcc\" value=\"true\"/>-->\n";
$xml .= " <param name=\"tls\" value=\"\$\${external_ssl_enable}\"/>\n";
$xml .= " <param name=\"tls-only\" value=\"false\"/>\n";
$xml .= " <param name=\"tls-bind-params\" value=\"transport=tls\"/>\n";
$xml .= " <param name=\"tls-sip-port\" value=\"\$\${external_tls_port}\"/>\n";
$xml .= " <!--<param name=\"tls-cert-dir\" value=\"\"/>-->\n";
$xml .= " <param name=\"tls-passphrase\" value=\"\"/>\n";
$xml .= " <!-- Verify the date on TLS certificates -->\n";
$xml .= " <param name=\"tls-verify-date\" value=\"true\"/>\n";
$xml .= " <param name=\"tls-verify-policy\" value=\"none\"/>\n";
$xml .= " <param name=\"tls-verify-depth\" value=\"2\"/>\n";
$xml .= " <param name=\"tls-verify-in-subjects\" value=\"\"/>\n";
$xml .= " <param name=\"tls-version\" value=\"\$\${sip_tls_version}\"/>\n";
$xml .= " </settings>\n";
$xml .= " </profile>\n";
$xml .= file_get_contents('etc_freeswitch/sip_profiles/internal.xml');
$xml .= " </profiles>\n";
$xml .= "</configuration>\n";
}elseif ($conf=='ivr.conf.xml'){
$xml .= "<configuration name=\"ivr.conf\" description=\"IVR menus\">\n";
$xml .= "<menus>\n";
foreach (scandir("etc_freeswitch/ivr_menus") as $file){
if ($file=='.'||$file=='..') continue;
$xml .= file_get_contents('etc_freeswitch/ivr_menus/'.$file);
}
$xml .= "</menus>\n";
$xml .= "</configuration>\n";
}elseif ($conf=='dingaling.conf.xml'){
$xml .= "<configuration name=\"dingaling.conf\" description=\"XMPP Jingle Endpoint\">\n";
$xml .= "<settings>\n";
$xml .= "<param name=\"debug\" value=\"0\"/>\n";
$xml .= "<param name=\"codec-prefs\" value=\"H264,PCMU\"/>\n";
$xml .= "</settings>\n";
foreach (scandir("etc_freeswitch/jingle_profiles") as $file){
if ($file=='.'||$file=='..') continue;
$xml .= file_get_contents('etc_freeswitch/jingle_profiles/'.$file);
}
$xml .= "</configuration>\n";
}elseif ($conf=='skinny.conf.xml'){
$xml .= "<configuration name=\"skinny.conf\" description=\"Skinny Endpoints\">\n";
$xml .= "<profiles>\n";
foreach (scandir("etc_freeswitch/sip_profiles") as $file){
if ($file=='.'||$file=='..') continue;
$xml .= file_get_contents('etc_freeswitch/sip_profiles/'.$file);
}
$xml .= "</profiles>\n";
$xml .= "</configuration>\n";
}elseif ($conf=='unimrcp.conf.xml'){
$xml .= "<configuration name=\"unimrcp.conf\" description=\"UniMRCP Client\">\n";
$xml .= "<settings>\n";
$xml .= "<param name=\"default-tts-profile\" value=\"voxeo-prophecy8.0-mrcp1\"/>\n";
$xml .= "<param name=\"default-asr-profile\" value=\"voxeo-prophecy8.0-mrcp1\"/>\n";
$xml .= "<param name=\"log-level\" value=\"DEBUG\"/>\n";
$xml .= "<param name=\"enable-profile-events\" value=\"false\"/>\n";
$xml .= "<param name=\"max-connection-count\" value=\"100\"/>\n";
$xml .= "<param name=\"offer-new-connection\" value=\"1\"/>\n";
$xml .= "<param name=\"request-timeout\" value=\"3000\"/>\n";
$xml .= "</settings>\n";
$xml .= "<profiles>\n";
foreach (scandir("etc_freeswitch/mrcp_profiles") as $file){
if ($file=='.'||$file=='..') continue;
$xml .= file_get_contents('etc_freeswitch/mrcp_profiles/'.$file);
}
$xml .= "</profiles>\n";
$xml .= "</configuration>\n";
}else{
$xml .= file_get_contents('etc_freeswitch/autoload_configs/'.$conf);
}
}
$xml .= "</section>\n";
$xml .= "</document>\n";
return response($xml,200)->header("Content-type","text/xml");
}
}

19
app/Models/Action.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
class Action extends Model
{
protected $table = 'action';
protected $fillable = ['display_name','application','data','sort','condition_id'];
protected $appends = ['application_name'];
public function getApplicationNameAttribute()
{
return Arr::get(config('freeswitch.application'),$this->application)."".$this->application."";
}
}

40
app/Models/Agent.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Agent extends Model
{
protected $table = 'agent';
protected $fillable = [
'display_name',
'name',
'originate_type',
'originate_number',
'status',
'max_no_answer',
'wrap_up_time',
'reject_delay_time',
'busy_delay_time',
'no_answer_delay_time',
];
protected $appends = ['originate_type_name','status_name'];
public function getOriginateTypeNameAttribute()
{
return $this->attributes['originate_type_name'] = array_get([
'user' => '分机',
'group' => '分机组',
'gateway' => '网关',
],$this->originate_type);
}
public function getStatusNameAttribute()
{
return $this->attributes['status_name'] = array_get(config('freeswitch.agent_status'),$this->status);
}
}

10
app/Models/Asr.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Asr extends Model
{
protected $table = 'cdr_asr';
}

21
app/Models/Audio.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Audio extends Model
{
protected $table = 'audio';
protected $fillable = [
'url',
'text',
'auf',
'aue',
'voice_name',
'speed',
'volume',
'pitch',
'engine_type',
];
}

28
app/Models/Bill.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Bill extends Model
{
protected $table = 'bill';
protected $fillable = ['merchant_id','type','money','remark','created_user_id'];
protected $appends = ['created_user_name'];
public function getCreatedUserNameAttribute()
{
return $this->attributes['created_user_name'] = $this->user->name??'系统操作';
}
public function user()
{
return $this->belongsTo('App\Models\User', 'created_user_id', 'id')->withDefault();
}
public function merchant()
{
return $this->belongsTo('App\Models\Merchant', 'merchant_id', 'id');
}
}

10
app/Models/Bleg.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Bleg extends Model
{
protected $table = 'cdr_b_leg';
}

13
app/Models/Call.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Call extends Model
{
protected $table = 'call';
protected $guarded = ['id'];
}

24
app/Models/Cdr.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Cdr extends Model
{
protected $table = 'cdr_a_leg';
public function bleg()
{
return $this->hasOne('App\Models\Bleg','aleg_uuid','aleg_uuid');
}
public function getBillsecAttribute($value)
{
if (!empty($this->bleg_uuid)){
$value = $this->bleg->billsec;
}
return $value;
}
}

18
app/Models/Condition.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Condition extends Model
{
protected $table = 'condition';
protected $fillable = ['display_name','field','expression','break','extension_id','sort'];
protected $with = 'actions';
public function actions()
{
return $this->hasMany('App\Models\Action','condition_id','id')->orderBy('sort')->orderBy('id');
}
}

29
app/Models/Digits.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Digits extends Model
{
protected $table = 'digits';
protected $fillable = ['ivr_id','action','digits','param'];
protected $appends = ['action_name','ivr_name'];
public function getActionNameAttribute()
{
return $this->attributes['action_name'] = array_get(config('freeswitch.ivr_action'),$this->action);
}
public function getIvrNameAttribute()
{
return $this->attributes['ivr_name'] = $this->ivr->display_name."".$this->ivr->name.")";
}
public function ivr()
{
return $this->belongsTo('App\Models\Ivr');
}
}

11
app/Models/District.php Normal file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class District extends Model
{
protected $table = 'districts';
protected $fillable = ['citycode','adcode','name','center','level'];
}

24
app/Models/Extension.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
class Extension extends Model
{
protected $table = 'extension';
protected $fillable = ['display_name','name','sort','continue','context'];
protected $appends = ['context_name'];
public function getContextNameAttribute()
{
return Arr::get(['default'=>'呼出','public'=>'呼入'],$this->context);
}
public function conditions()
{
return $this->hasMany('App\Models\Condition','extension_id','id')->orderBy('sort')->orderBy('id');
}
}

12
app/Models/Gateway.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Gateway extends Model
{
protected $table = 'gateway';
protected $fillable = ['name','realm','username','password','prefix','outbound_caller_id','rate'];
}

17
app/Models/Icon.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Icon extends Model
{
protected $table = 'icons';
protected $fillable = ['unicode','class','name','sort'];
//对应菜单
public function menus()
{
return $this->hasMany('App\Models\Menu','icon_id','id');
}
}

34
app/Models/Ivr.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Ivr extends Model
{
protected $table = 'ivr';
protected $fillable = [
'display_name',
'name',
'greet_long',
'greet_short',
'invalid_sound',
'exit_sound',
'confirm_macro',
'confirm_key',
'tts_engine',
'tts_voice',
'confirm_attempts',
'timeout',
'inter_digit_timeout',
'max_failures',
'max_timeouts',
'digit_len',
];
public function digits()
{
return $this->hasMany('App\Models\Digits');
}
}

66
app/Models/Merchant.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Merchant extends Model
{
use SoftDeletes;
protected $table = 'merchant';
protected $fillable = [
'uuid',
'username',
'password',
'status',
'company_name',
'expires_at',
'sip_num',
'money',
'created_user_id',
];
protected $hidden = ['uuid','password'];
protected $dates = ['expires_at'];
protected $appends = ['status_name','created_user_name'];
public function getStatusNameAttribute()
{
return $this->attributes['status_name'] = array_get(config('freeswitch.merchant_status'),$this->status);
}
public function getCreatedUserNameAttribute()
{
return $this->attributes['created_user_name'] = $this->user->name??'未知';
}
/**
* 创建用户
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\Models\User', 'created_user_id', 'id');
}
/**
* 可使用的网关
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function gateways()
{
return $this->belongsToMany('App\Models\Gateway', 'merchant_gateway','gateway_id','merchant_id')->withPivot(['rate']);
}
/**
* 拥有的分机
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function sips()
{
return $this->hasMany('App\Models\Sip','merchant_id','id');
}
}

30
app/Models/Queue.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Queue extends Model
{
protected $table = 'queue';
protected $fillable = [
'display_name',
'name',
'strategy',
'max_wait_time',
];
protected $appends = ['strategy_name'];
public function agents()
{
return $this->belongsToMany('App\Models\Agent','queue_agent');
}
public function getStrategyNameAttribute()
{
return $this->attributes['strategy_name'] = array_get(config('freeswitch.strategy'),$this->strategy);
}
}

23
app/Models/Sip.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Sip extends Model
{
protected $table = 'sip';
protected $fillable = [
'username',
'password',
'effective_caller_id_name',
'effective_caller_id_number',
'outbound_caller_id_name',
'outbound_caller_id_number',
'merchant_id',
'gateway_id',
'expense_id',
];
}

84
app/Models/Task.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $table = 'task';
protected $fillable = [
'name',
'date_start',
'date_end',
'time_start',
'time_end',
'gateway_id',
'queue_id',
'max_channel',
'status',
];
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->display_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=5
public function failCalls()
{
return $this->hasMany('App\Models\Call','task_id','id')->where('status',5);
}
}

224
bootstrap/Freeswitchesl.php Normal file
View File

@ -0,0 +1,224 @@
<?php
class Freeswitchesl {
public function __construct() {
$this->socket = "";
$this->sorts = "";
$this->length = 1024;
}
public function eliminate($parameter)
{
$array = array(" "," ","\t","\n","\r");
return str_replace($array, '', $parameter);
}
public function eliminateLine($parameter)
{
return str_replace("\n\n", "\n", $parameter);
}
public function typeClear($response)
{
$commenType = array("Content-Type: text/event-xml\n","Content-Type: text/event-plain\n","Content-Type: text/event-json\n");
return str_replace($commenType, '', $response);
}
public function connect($host,$port,$password)
{
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$connection = socket_connect($this->socket, $host,$port);
$connect = false;
$error = "";
while ($socket_info = @socket_read($this->socket, 1024, PHP_NORMAL_READ)) {
$eliminate_socket_info = $this->eliminate($socket_info);
if ($eliminate_socket_info == "Content-Type:auth/request") {
socket_write($this->socket, "auth ".$password."\r\n\r\n");
}elseif ($eliminate_socket_info == "") {
continue;
}elseif ($eliminate_socket_info == "Content-Type:command/reply") {
continue;
}elseif ($eliminate_socket_info == "Reply-Text:+OKaccepted") {
$connect = true;
break;
} else {
$error .= $eliminate_socket_info."\r\n";
}
}
if (!$connect) {
echo $error;
}
return $connect;
}
public function api($api,$args="")
{
if ($this->socket) {
socket_write($this->socket, "api ".$api." ".$args."\r\n\r\n");
}
$response = $this->recvEvent("common");
return $response;
}
public function bgapi($api,$args="",$custom_job_uuid="")
{
if ($this->socket) {
socket_write($this->socket, "bgapi ".$api." ".$args." ".$custom_job_uuid."\r\n\r\n");
}
return "executed";
}
public function execute($app,$args,$uuid)
{
if ($this->socket) {
$str = "sendmsg ".$uuid."\ncall-command: execute\nexecute-app-name: ".$app."\nexecute-app-arg: ".$args."\n\n";
socket_write($this->socket, $str);
}
$response = $this->recvEvent("common");
return $response;
}
public function executeAsync($app,$args,$uuid)
{
if ($this->socket) {
$str = "sendmsg ".$uuid."\ncall-command: executeAsync\nexecute-app-name: ".$app."\nexecute-app-arg: ".$args."\n\n";
socket_write($this->socket, $str);
}
return "executed";
}
public function sendmsg($uuid)
{
if ($this->socket) {
socket_write($this->socket, "sendmsg ".$uuid."\r\n\r\n");
}
return "executed";
}
public function events($sorts,$args)
{
$this->sorts = $sorts;
if ($sorts == "json") {
$sorts = "xml";
}
if ($this->socket) {
socket_write($this->socket, "event ".$sorts." ".$args."\r\n\r\n");
}
return true;
}
public function getHeader($response,$args)
{
$serialize = $this->serialize($response,"json");
$serializearray = json_decode($serialize);
try {
return $serializearray->$args;
} catch (Exception $e) {
return "";
}
}
public function recvEvent($type="event")
{
$response = '';
$length = 0;
$x = 0;
while ($socket_info = @socket_read($this->socket, 1024, PHP_NORMAL_READ)){
$x++;
usleep(100);
if ($length > 0) {
$response .= $socket_info;
}
if ($length == 0 && strpos($socket_info, 'Content-Length:') !== false) {
$lengtharray = explode("Content-Length:",$socket_info);
if ($type == "event") {
$length = (int)$lengtharray[1]+30;
} else {
$length = (int)$lengtharray[1];
}
}
if ($length > 0 && strlen($response) >= $length) {
break;
}
if ($x > 10000) break;
}
if ($this->sorts == "json" && $type == "event") {
$response = $this->typeClear($response);
$responsedata = simplexml_load_string($response);
$response = [];
foreach ($responsedata->headers->children() as $key => $value) {
$response[(string)$key] = (string)$value;
}
return json_encode($response);
} else {
$response = $this->eliminateLine($response);
}
return $response;
}
public function serialize($response,$type)
{
$response = $this->typeClear($response);
if ($this->sorts == $type) return $response;
if ($this->sorts == "json") {
$responsedata = json_decode($response);
if ($type == "plain") {
$response = "";
foreach ($responsedata as $key => $value) {
$responseline = $key.": ".$value."\r\n";
$response .= $responseline;
}
} else {
$response = "<event>\r\n <headers>\r\n";
foreach ($responsedata as $key => $value) {
$responseline = " <".$key.">".$value."</".$key.">"."\r\n";
$response .= $responseline;
}
$response .= " </headers>\r\n</event>";
}
return $response;
} elseif ($this->sorts == "xml") {
$responsedata = simplexml_load_string($response);
if ($type == "plain") {
$response = "";
foreach ($responsedata->headers->children() as $key => $value) {
$responseline = (string)$key.": ".(string)$value."\r\n";
$response .= $responseline;
}
return $response;
} else {
$response = [];
foreach ($responsedata->headers->children() as $key => $value) {
$response[(string)$key] = (string)$value;
}
return json_encode($response);
}
} else {
$response = str_replace("\n", '","', $response);
$response = str_replace(": ", '":"', $response);
$response = substr($response, 0, -2);
$response = '{"'.$response.'}';
if ($type == "json") return $response;
$responsedata = json_decode($response);
$response = "<event>\r\n <headers>\r\n";
foreach ($responsedata as $key => $value) {
$responseline = " <".$key.">".$value."</".$key.">"."\r\n";
$response .= $responseline;
}
$response .= " </headers>\r\n</event>";
return $response;
}
}
public function disconnect()
{
socket_close($this->socket);
}
}

View File

@ -40,6 +40,9 @@
"classmap": [
"database/seeds",
"database/factories"
],
"files": [
"bootstrap/Freeswitchesl.php"
]
},
"autoload-dev": {

85
config/freeswitch.php Normal file
View File

@ -0,0 +1,85 @@
<?php
return [
//socket连接授权
'event_socket' => [
'host' => '172.16.2.12',
'port' => 8021,
'password' => 'ClueCon',
],
//网关目录
'gateway_dir' => '/usr/local/freeswitch/etc/freeswitch/sip_profiles/external/',
//IVR 目录
'ivr_dir' => '/usr/local/freeswitch/etc/freeswitch/ivr_menus/',
//callcenter目录
'callcenter_dir' => '/usr/local/freeswitch/etc/freeswitch/autoload_configs/callcenter.conf.xml',
//application
'application' => [
'set' => '设置变量',
'answer' => '应答',
'sleep' => '睡眠',
'hangup' => '挂断',
'record_session' => '录音',
'export' => '导入变量',
'bridge' => '桥接呼叫',
'echo' => '回音',
'park' => '停泊',
'transfer' => '呼叫转移',
'info' => '显示信息',
'lua' => 'lua脚本',
'detect_speech'=> 'detect_speech',
'play_and_detect_speech'=>'play_and_detect_speech',
'log' => 'log',
],
//队列响铃模式
'strategy' => [
'top-down' => '顺序振铃',
'ring-all' => '所有振铃',
'longest-idle-agent' => '空闲时长最长振铃',
'round-robin' => '轮循振铃',
'agent-with-least-talk-time' => '通话时长最小振铃',
'agent-with-fewest-calls' => '接听最少振铃',
'sequentially-by-agent-order' => '优先级振铃',
'random' => '随机振铃',
],
//坐席状态status
'agent_status' => [
'Logged Out' => '签出',
'Available' => '示闲',
'On Break' => '休息(不接收呼叫)',
],
//坐席呼叫状态state
'agent_state' => [
'Idle' => '空闲(不接收呼叫)',
'Waiting' => '等待',
'Receiving' => '电话呼入',
'In a queue call' => '通话中',
],
//商户状态
'merchant_status' => [
1 => '正常',
2 => '禁用',
],
//讯飞在线语音合成参数
'xfyun' => [
'appid' => '5bc842c0',
'apikey' => '33e3e90871156122614cbe26d2992ab0',
'sounds' => '/usr/local/freeswitch/share/freeswitch/sounds/en/us/callie/custom/8000/'
],
//IVR
'ivr_action' => [
'menu-exec-app' => '应用',
'menu-sub' => '子菜单',
'enu-top' => '父菜单',
],
];

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateDistrictTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('districts', function (Blueprint $table) {
$table->increments('id');
$table->string('adcode')->comment('行政编码');
$table->string('name')->comment('名字');
$table->string('center')->comment('经纬度');
$table->string('level')->comment('级别');
$table->integer('parent_id')->default(0);
$table->integer('sort')->default(0)->comment('排序');
$table->timestamps();
});
\DB::statement("ALTER TABLE `districts` comment '地区表'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('districts');
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateGatewayTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('gateway', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->comment('网关名称');
$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->decimal('rate',10,2)->default(0)->comment('费率:每分钟多少元');
$table->timestamps();
});
\DB::statement("ALTER TABLE `gateway` comment '中继网关表'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('gateway');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateExtensionTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('extension', function (Blueprint $table) {
$table->increments('id');
$table->tinyInteger('sort')->default(0)->comment('序号');
$table->string('display_name')->comment('名称');
$table->string('name')->unique()->comment('标识符');
$table->string('continue')->default("false")->comment('true:表示不管该extension中是否有condition匹配都继续执行dialplan。false表示如果该extension中有匹配的condition那么就停止了dialplan。false是默认值');
$table->string('context')->default('default')->comment('标识呼出还是呼入default==呼出public==呼入');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('extension');
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateConditionTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('condition', function (Blueprint $table) {
$table->increments('id');
$table->tinyInteger('sort')->default(0)->comment('序号');
$table->string('display_name')->comment('名称');
$table->string('field')->default('destination_number')->comment('字段,默认被叫号码');
$table->string('expression')->nullable()->comment('正则');
$table->string('break')->default('on-false')->comment('on-false(默认),on-true,always,never');
$table->unsignedInteger('extension_id')->comment('所属拨号计划的ID');
$table->timestamps();
$table->foreign('extension_id')->references('id')->on('extension')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('condition');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateActionTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('action', function (Blueprint $table) {
$table->increments('id');
$table->tinyInteger('sort')->default(0)->comment('序号');
$table->string('display_name')->comment('名称');
$table->string('application')->comment('应用');
$table->string('data')->nullable()->comment('数据');
$table->unsignedInteger('condition_id')->comment('路由规则ID');
$table->timestamps();
$table->foreign('condition_id')->references('id')->on('condition')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('action');
}
}

View File

@ -0,0 +1,76 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCdrABLeg extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('cdr_ab_leg', function (Blueprint $table) {
$table->increments('id');
$table->string('uuid')->comment('通话uuid');
$table->string('caller_id_name')->nullable()->comment('主叫昵称');
$table->string('caller_id_number')->nullable()->comment('主叫号码');
$table->string('destination_number')->nullable()->comment('被叫号码');
$table->timestamp('start_stamp')->nullable()->comment('呼叫发起时间');
$table->timestamp('answer_stamp')->nullable()->comment('呼叫应答时间');
$table->timestamp('end_stamp')->nullable()->comment('呼叫结束时间');
$table->integer('duration')->default(0)->comment('总通话时长(主叫通话时长)');
$table->integer('billsec')->default(0)->comment('接听时长(被叫通话时长)');
$table->string('hangup_cause')->nullable()->comment('挂断原因');
$table->string('sofia_record_file')->nullable()->comment('录音文件地址');
$table->string('extend_content')->nullable()->comment('预留扩展字段内容');
});
Schema::create('cdr_a_leg', function (Blueprint $table) {
$table->increments('id');
$table->string('aleg_uuid')->comment('aleg_uuid');
$table->string('bleg_uuid')->nullable()->comment('bleg_uuid');
$table->string('caller_id_name')->nullable()->comment('主叫昵称');
$table->string('caller_id_number')->nullable()->comment('主叫号码');
$table->string('destination_number')->nullable()->comment('被叫号码');
$table->timestamp('start_stamp')->nullable()->comment('呼叫发起时间');
$table->timestamp('answer_stamp')->nullable()->comment('呼叫应答时间');
$table->timestamp('end_stamp')->nullable()->comment('呼叫结束时间');
$table->integer('duration')->default(0)->comment('总通话时长(主叫通话时长)');
$table->integer('billsec')->default(0)->comment('接听时长(被叫通话时长)');
$table->string('hangup_cause')->nullable()->comment('挂断原因');
$table->string('sofia_record_file')->nullable()->comment('录音文件地址');
$table->string('extend_content')->nullable()->comment('预留扩展字段内容');
});
Schema::create('cdr_b_leg', function (Blueprint $table) {
$table->increments('id');
$table->string('aleg_uuid')->comment('aleg_uuid');
$table->string('bleg_uuid')->comment('bleg_uuid');
$table->string('caller_id_name')->nullable()->comment('主叫昵称');
$table->string('caller_id_number')->nullable()->comment('主叫号码');
$table->string('destination_number')->nullable()->comment('被叫号码');
$table->timestamp('start_stamp')->nullable()->comment('呼叫发起时间');
$table->timestamp('answer_stamp')->nullable()->comment('呼叫应答时间');
$table->timestamp('end_stamp')->nullable()->comment('呼叫结束时间');
$table->integer('duration')->default(0)->comment('总通话时长(主叫通话时长)');
$table->integer('billsec')->default(0)->comment('接听时长(被叫通话时长)');
$table->string('hangup_cause')->nullable()->comment('挂断原因');
$table->string('sofia_record_file')->nullable()->comment('录音文件地址');
$table->string('extend_content')->nullable()->comment('预留扩展字段内容');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cdr_ab_leg');
Schema::dropIfExists('cdr_a_leg');
Schema::dropIfExists('cdr_b_leg');
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCdrAsrTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('cdr_asr', function (Blueprint $table) {
$table->increments('id');
$table->string('uuid')->comment('当前识别通道uuid');
$table->string('text')->comment('识别结果');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cdr_asr');
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTask extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('task', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->comment('任务名称');
$table->date('date_start')->nullable()->comment('任务开始日期');
$table->date('date_end')->nullable()->comment('任务结束日期');
$table->time('time_start')->nullable()->comment('任务开始时间');
$table->time('time_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');
}
}

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCall extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('call', function (Blueprint $table) {
$table->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_phone')->nullable()->comment('呼叫时间');
$table->timestamp('datetime_entry_queue')->nullable()->comment('进入队列时间');
$table->timestamp('datetime_agent_called')->nullable()->comment('呼叫坐席时间');
$table->timestamp('datetime_agent_answered')->nullable()->comment('坐席应答时间');
$table->timestamp('datetime_end')->nullable()->comment('结束通话时间');
$table->string('agent_name')->nullable()->comment('接听坐席');
$table->integer('duration')->default(0)->comment('主叫通话时长');
$table->integer('billsec')->default(0)->comment('被叫通话时长');
$table->string('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');
}
}

View File

@ -0,0 +1,81 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCallcenterTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('queue', function (Blueprint $table) {
$table->increments('id');
$table->string('display_name')->comment('队列名称');
$table->string('name')->unique()->comment('队列号码');
$table->string('strategy')->default('top-down')->comment('振铃策略');
$table->string('moh_sound')->default('$${hold_music}')->comment('队列语音');
$table->string('time_base_score')->default('system');
$table->integer('max_wait_time')->default(0)->comment('最大等待时间超过时间未被接通将退出callcenter0为禁用');
$table->integer('max_wait_time_with_no_agent')->default(0)->comment('无成员没有成员的状态是available等待超时时间: 超出时间电话会退出callcenter 0为禁用');
$table->integer('max_wait_time_with_no_agent_time_reached')->default(5)->comment('如果有电话有因为max-wait-time-with-no-agent的原因退出队列 队列将在延迟一定时间不允许新的电话呼入到队列');
$table->string('tier_rules_apply')->default('false')->comment('梯队匹配');
$table->integer('tier_rule_wait_second')->default(300)->comment('梯队的等待时间(进入下个梯队的时间)');
$table->string('tier_rule_wait_multiply_level')->default('true')->comment('梯队等待级别');
$table->string('tier_rule_no_agent_no_wait')->default('true')->comment('是否跳过no-agent的梯队(no-agent就是这个梯队中不存在状态为available的成员agent )');
$table->integer('discard_abandoned_after')->default(60)->comment('最大丢弃时长丢弃超过此时长将不可以恢复与abandoned_resume_allowed同时生效');
$table->string('abandoned_resume_allowed')->default('false')->comment('丢弃后是否允许恢复或者重新进入队列');
$table->timestamps();
});
Schema::create('agent', function (Blueprint $table) {
$table->increments('id');
$table->string('display_name')->comment('坐席名称');
$table->string('name')->unique()->comment('坐席号码');
$table->string('type')->default('callback');
$table->string('originate_type')->default('user')->comment('呼叫类型user-分机group-分机组gateway-网关');
$table->string('originate_number')->comment('呼叫号码');
$table->string('status')->default('Available')->comment('Logged Out签出Available空闲Available (On Demand)一次空闲On Break未忙');
$table->string('state')->nullable()->comment('坐席呼叫状态');
$table->integer('max_no_answer')->default(3)->comment('最大无应答次数超过次数status变为On Break状态');
$table->integer('wrap_up_time')->default(1)->comment('通话完成间隔时间,成功处理一个通话后,多久才会有电话进入等待时长');
$table->integer('reject_delay_time')->default(10)->comment('挂机间隔时间,来电拒接后多久才会有电话进入的等待时长');
$table->integer('busy_delay_time')->default(10)->comment('忙重试间隔时间,来电遇忙后多久才会有电话进入的等待时长');
$table->integer('no_answer_delay_time')->default(10)->comment('无应答重试间隔,来电无应答后多久才会有电话进入的等待时长');
$table->timestamps();
});
Schema::create('queue_agent', function (Blueprint $table) {
$table->unsignedInteger('queue_id');
$table->unsignedInteger('agent_id');
$table->foreign('queue_id')
->references('id')
->on('queue')
->onDelete('cascade');
$table->foreign('agent_id')
->references('id')
->on('agent')
->onDelete('cascade');
$table->primary(['queue_id', 'agent_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('queue_agent');
Schema::dropIfExists('agent');
Schema::dropIfExists('queue');
}
}

View File

@ -0,0 +1,42 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Merchant extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('merchant', function (Blueprint $table) {
$table->increments('id');
$table->uuid('uuid');
$table->string('username')->unique()->comment('商户帐号');
$table->string('password')->comment('商户密码');
$table->tinyInteger('status')->default(1)->comment('商户状态:1正常2禁用');
$table->string('company_name')->nullable()->comment('公司名称');
$table->timestamp('expires_at')->nullable()->comment('到期时间');
$table->integer('sip_num')->default(0)->comment('可添加的分机数量');
$table->decimal('money',10)->default(0)->comment('帐户余额');
$table->unsignedInteger('created_user_id')->default(0)->comment('创建用户ID');
$table->timestamps();
$table->softDeletes();
});
\DB::statement("ALTER TABLE `merchant` comment '商户表'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('merchant');
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Sip extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sip', function (Blueprint $table) {
$table->increments('id');
$table->string('username')->unique()->comment('分机号');
$table->string('password')->comment('分机密码');
$table->string('context')->comment('拨号文本')->default('default');
$table->string('effective_caller_id_name')->nullable()->comment('外显名称,针对分机与分机');
$table->string('effective_caller_id_number')->nullable()->comment('外显号码,针对分机与分机');
$table->string('outbound_caller_id_name')->nullable()->comment('出局名称,针对中继');
$table->string('outbound_caller_id_number')->nullable()->comment('出局名称,针对中继');
$table->unsignedInteger('merchant_id')->nullable()->comment('商户ID');
$table->unsignedBigInteger('gateway_id')->nullable()->comment('网关ID');
$table->unsignedBigInteger('expense_id')->nullable()->comment('资费套餐ID');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sip');
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Bill extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('bill', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('merchant_id')->comment('商户ID');
$table->tinyInteger('type')->comment('流水类型1增加2减少');
$table->decimal('money',10)->default(0)->comment('金额');
$table->text('remark')->comment('备注');
$table->unsignedInteger('created_user_id')->default(0)->comment('user用户ID');
$table->timestamps();
});
\DB::statement("ALTER TABLE `bill` comment '商户帐单表'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('bill');
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class MerchantGateway extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('merchant_gateway', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('merchant_id')->comment('商户ID');
$table->unsignedInteger('gateway_id')->comment('网关ID');
$table->decimal('rate',10,2)->comment('每分钟多少钱,单位:元。');
});
\DB::statement("ALTER TABLE `merchant_gateway` comment '商户-网关多对多表'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('merchant_gateway');
}
}

View File

@ -0,0 +1,48 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Ivr extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('ivr', function (Blueprint $table) {
$table->increments('id');
$table->string('display_name')->comment('名称');
$table->string('name')->unique()->comment('标识');
$table->string('greet_long')->comment('开始的欢迎音');
$table->string('greet_short')->comment('简短提示音,用户长时间没有按键提示');
$table->string('invalid_sound')->default('ivr/ivr-that_was_an_invalid_entry.wav')->comment('错误提示音,如果用户按错了键,则会使用该提示');
$table->string('exit_sound')->default('voicemail/vm-goodbye.wav')->comment('菜单退出时提示音');
$table->string('confirm_macro')->nullable()->default('')->comment('确认宏');
$table->string('confirm_key')->nullable()->default('')->comment('确认键');
$table->string('tts_engine')->nullable()->default('flite')->comment('语音合成引擎');
$table->string('tts_voice')->nullable()->default('rms')->comment('语音合成声音');
$table->string('confirm_attempts')->nullable()->default('3')->comment('确认次数');
$table->integer('timeout')->default(10000)->comment('超时时间(毫秒),即多长时间没有收到按键就超时,播放其他提示音');
$table->integer('inter_digit_timeout')->default(2000)->comment('两次按键的最大间隔(毫秒)');
$table->integer('max_failures')->default(3)->comment('用户按键错误的次数');
$table->integer('max_timeouts')->default(3)->comment('最大超时次数');
$table->integer('digit_len')->default(4)->comment('菜单项的长度,即最大收号位数');
$table->timestamps();
});
\DB::statement("ALTER TABLE `ivr` comment 'ivr语音导航'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('ivr');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Digits extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('digits', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('ivr_id')->comment('IVR ID');
$table->string('action')->comment('action应用:menu-exec-app、menu-sub、menu-top');
$table->string('digits')->comment('用户按键数值0-9或者正则表达式');
$table->string('param')->nullable()->comment('应用执行参数,可为空');
$table->timestamps();
});
\DB::statement("ALTER TABLE `ivr` comment 'ivr语音导航'");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('digits');
}
}

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class Audio extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('audio', function (Blueprint $table) {
$table->increments('id');
$table->string('url')->comment('音频路径');
$table->text('text')->comment('待合成文字');
$table->string('auf')->comment('音频采样率audio/L16;rate=16000 audio/L16;rate=8000');
$table->string('aue')->comment('音频编码aw未压缩的wav格式lamemp3格式');
$table->string('voice_name')->comment('发音人xiaoyanaisjiuxuaisxpingaisjingeraisbabyxu');
$table->string('speed')->comment('语速500-100');
$table->string('volume')->comment('音量500-100');
$table->string('pitch')->comment('音高500-100');
$table->string('engine_type')->comment('aisound普通效果
intp65中文
intp65_en英文
mtts小语种需配合小语种发音人使用
x优化效果
默认为intp65');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('audio');
}
}

View File

@ -14,6 +14,7 @@ class DatabaseSeeder extends Seeder
$this->call([
UserSeeder::class,
ConfigGroupSeeder::class,
PbxSeeder::class,
]);
}
}

View File

@ -0,0 +1,80 @@
<?php
use Illuminate\Database\Seeder;
class DistrictTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
set_time_limit(0);
//清空表
\App\Models\District::truncate();
/**
规则:设置显示下级行政区级数(行政区级别包括:国家、省/直辖市、市、区/县、乡镇/街道多级数据)
可选值0、1、2、3等数字并以此类推
0:不返回下级行政区;
1:返回下一级行政区;
2:返回下两级行政区;
3:返回下三级行政区;
*/
$dep = 3;
$url = 'http://restapi.amap.com/v3/config/district?key=5f6d1733b6b08927f8c44f9fd70e1026&subdistrict='.$dep;
$client = new \GuzzleHttp\Client();
$response = $client->get($url);
if ($response->getStatusCode()==200){
$res = \GuzzleHttp\json_decode($response->getBody());
$data = $res->districts[0]->districts;
foreach ($data as $d1){
//插入省
$province = \App\Models\District::create([
'adcode' => $d1->adcode,
'name' => $d1->name,
'center' => $d1->center,
'level' => $d1->level
]);
if (isset($d1->districts) && !empty($d1->districts)){
foreach ($d1->districts as $d2){
//插入市
$city = \App\Models\District::create([
'adcode' => $d2->adcode,
'name' => $d2->name,
'center' => $d2->center,
'level' => $d2->level,
'parent_id' => $province->id
]);
if (isset($d2->districts) && !empty($d2->districts)){
foreach ($d2->districts as $d3){
//插入区县
$qu = \App\Models\District::create([
'adcode' => $d3->adcode,
'name' => $d3->name,
'center' => $d3->center,
'level' => $d3->level,
'parent_id' => $city->id
]);
if (isset($d3->districts) && !empty($d3->districts)){
foreach ($d3->districts as $d4){
//插入乡镇
$zhen = \App\Models\District::create([
'adcode' => $d4->adcode,
'name' => $d4->name,
'center' => $d4->center,
'level' => $d4->level,
'parent_id' => $qu->id
]);
}
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,227 @@
<?php
use Illuminate\Database\Seeder;
class PbxSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
//清空表
\Illuminate\Support\Facades\DB::statement('SET FOREIGN_KEY_CHECKS=0;');
\App\Models\Action::truncate();
\App\Models\Condition::truncate();
\App\Models\Extension::truncate();
\Illuminate\Support\Facades\DB::statement('SET FOREIGN_KEY_CHECKS=1;');
//拨号计划
$data = [
[
'display_name' => '本地分机互拨',
'name' => 'Local_Extension',
'context' => 'default',
'sort' => 0,
'conditions' => [
[
'display_name' => '规则一',
'field' => 'destination_number',
'expression' => '^(\d{5,9})$',
'break' => 'on-false',
'sort' => 0,
'actions' => [
[
'display_name' => '系统应答',
'application' => 'answer',
'data' => null,
'sort' => 0,
],
[
'display_name' => '媒体绕过',
'application' => 'set',
'data' => 'bypass_media=true',
'sort' => 1,
],
[
'display_name' => '被叫挂断后主叫也挂断',
'application' => 'set',
'data' => 'hangup_after_bridge=true',
'sort' => 2,
],
[
'display_name' => '呼叫',
'application' => 'bridge',
'data' => 'user/$1',
'sort' => 3,
],
]
],
],
],
[
'display_name' => '回音测试',
'name' => 'echo_test',
'context' => 'default',
'sort' => 1,
'conditions' => [
[
'display_name' => '规则一',
'field' => 'destination_number',
'expression' => '^9996$',
'break' => 'on-false',
'sort' => 0,
'actions' => [
[
'display_name' => '系统应答',
'application' => 'answer',
'data' => null,
'sort' => 0,
],
[
'display_name' => '回音',
'application' => 'echo',
'data' => null,
'sort' => 1,
],
],
],
],
],
[
'display_name' => '控制台显示info',
'name' => 'info_test',
'context' => 'default',
'sort' => 1,
'conditions' => [
[
'display_name' => '规则一',
'field' => 'destination_number',
'expression' => '^9992$',
'break' => 'on-false',
'sort' => 0,
'actions' => [
[
'display_name' => '系统应答',
'application' => 'answer',
'data' => null,
'sort' => 0,
],
[
'display_name' => '输出信息',
'application' => 'info',
'data' => null,
'sort' => 1,
],
[
'display_name' => '挂断',
'application' => 'hangup',
'data' => null,
'sort' => 2,
],
],
],
],
],
[
'display_name' => '内线拨打外线电话',
'name' => 'internal_to_external',
'context' => 'default',
'sort' => 1,
'conditions' => [
[
'display_name' => '规则一',
'field' => 'destination_number',
'expression' => '^(\d{11})$',
'break' => 'on-false',
'sort' => 0,
'actions' => [
[
'display_name' => '系统应答',
'application' => 'answer',
'data' => null,
'sort' => 0,
],
[
'display_name' => '主叫随被叫一起挂断',
'application' => 'set',
'data' => 'hangup_after_bridge=true',
'sort' => 1,
],
[
'display_name' => '设置录音文件',
'application' => 'set',
'data' => 'sofia_record_file=$${base_dir}/var/lib/freeswitch/recordings/${strftime(%Y)}/${strftime(%m)}/${strftime(%d)}/${strftime(%Y-%m-%d-%H-%M-%S)}_${destination_number}_${caller_id_number}.wav',
'sort' => 2,
],
[
'display_name' => '接通后才进行录音',
'application' => 'set',
'data' => 'media_bug_answer_req=true',
'sort' => 3,
],
[
'display_name' => '最小录音时间',
'application' => 'set',
'data' => 'RECORD_MIN_SEC=3',
'sort' => 4,
],
[
'display_name' => '立体声',
'application' => 'set',
'data' => 'RECORD_STEREO=true',
'sort' => 5,
],
[
'display_name' => '录音',
'application' => 'record_session',
'data' => '${sofia_record_file}',
'sort' => 6,
],
[
'display_name' => '呼叫',
'application' => 'bridge',
'data' => '{outbound_caller_id=900013}sofia/gateway/900013/$1',
'sort' => 7,
],
],
],
],
],
];
foreach ($data as $d1){
$extension = \App\Models\Extension::create([
'display_name' => $d1['display_name'],
'name' => $d1['name'],
'context' => $d1['context'],
'sort' => $d1['sort'],
]);
if (isset($d1['conditions'])&&!empty($d1['conditions'])){
foreach ($d1['conditions'] as $d2){
$condition = \App\Models\Condition::create([
'display_name' => $d2['display_name'],
'field' => $d2['field'],
'expression' => $d2['expression'],
'break' => $d2['break'],
'sort' => $d2['sort'],
'extension_id' => $extension->id,
]);
if (isset($d2['actions'])&&!empty($d2['actions'])){
foreach ($d2['actions'] as $d3){
\App\Models\Action::create([
'display_name' => $d3['display_name'],
'application' => $d3['application'],
'data' => $d3['data'],
'sort' => $d3['sort'],
'condition_id' => $condition->id,
]);
}
}
}
}
}
}
}

View File

@ -117,6 +117,158 @@ class UserSeeder extends Seeder
],
]
],
[
'name' => 'pbx.manage',
'display_name' => '平台管理',
'route' => '',
'icon' => 'layui-icon-fonts-strong',
'child' => [
[
'name' => 'pbx.merchant',
'display_name' => '商户管理',
'route' => 'admin.merchant',
'child' => [
['name' => 'pbx.merchant.create', 'display_name' => '添加','route'=>'admin.merchant.create'],
['name' => 'pbx.merchant.edit', 'display_name' => '编辑','route'=>'admin.merchant.edit'],
['name' => 'pbx.merchant.destroy', 'display_name' => '删除','route'=>'admin.merchant.destroy'],
['name' => 'pbx.merchant.gateway', 'display_name' => '分配网关','route'=>'admin.merchant.gateway'],
]
],
[
'name' => 'pbx.bill',
'display_name' => '帐单管理',
'route' => 'admin.bill',
'child' => [
['name' => 'pbx.bill.create', 'display_name' => '添加','route'=>'admin.bill.create'],
]
],
[
'name' => 'pbx.sip',
'display_name' => '分机管理',
'route' => 'admin.sip',
'child' => [
['name' => 'pbx.sip.create', 'display_name' => '添加','route'=>'admin.sip.create'],
['name' => 'pbx.sip.create_list', 'display_name' => '批量添加','route'=>'admin.sip.create_list'],
['name' => 'pbx.sip.edit', 'display_name' => '编辑','route'=>'admin.sip.edit'],
['name' => 'pbx.sip.destroy', 'display_name' => '删除','route'=>'admin.sip.destroy'],
]
],
[
'name' => 'pbx.gateway',
'display_name' => '网关管理',
'route' => 'admin.gateway',
'child' => [
['name' => 'pbx.gateway.create', 'display_name' => '添加','route'=>'admin.gateway.create'],
['name' => 'pbx.gateway.edit', 'display_name' => '编辑','route'=>'admin.gateway.edit'],
['name' => 'pbx.gateway.destroy', 'display_name' => '删除','route'=>'admin.gateway.destroy'],
['name' => 'pbx.gateway.updateXml', 'display_name' => '更新配置','route'=>'admin.gateway.updateXml'],
]
],
[
'name' => 'pbx.extension',
'display_name' => '拨号计划',
'route' => 'admin.extension',
'child' => [
['name' => 'pbx.extension.show', 'display_name' => '详情','route'=>'admin.extension.show'],
['name' => 'pbx.extension.create', 'display_name' => '添加','route'=>'admin.extension.create'],
['name' => 'pbx.extension.edit', 'display_name' => '编辑','route'=>'admin.extension.edit'],
['name' => 'pbx.extension.destroy', 'display_name' => '删除','route'=>'admin.extension.destroy'],
]
],
[
'name' => 'pbx.queue',
'display_name' => '队列管理',
'route' => 'admin.queue',
'child' => [
['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'],
['name' => 'pbx.queue.updateXml', 'display_name' => '更新配置','route'=>'admin.queue.updateXml'],
['name' => 'pbx.queue.agent', 'display_name' => '分配坐席','route'=>'admin.queue.agent'],
['name' => 'pbx.queue.agentStatus', 'display_name' => '坐席状态','route'=>'admin.queue.agentStatus'],
]
],
[
'name' => 'pbx.agent',
'display_name' => '坐席管理',
'route' => 'admin.agent',
'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'],
]
],
[
'name' => 'pbx.ivr',
'display_name' => 'IVR管理',
'route' => 'admin.ivr',
'child' => [
['name' => 'pbx.ivr.create', 'display_name' => '添加','route'=>'admin.ivr.create'],
['name' => 'pbx.ivr.edit', 'display_name' => '编辑','route'=>'admin.ivr.edit'],
['name' => 'pbx.ivr.destroy', 'display_name' => '删除','route'=>'admin.ivr.destroy'],
['name' => 'pbx.ivr.updateXml', 'display_name' => '更新配置','route'=>'admin.ivr.updateXml'],
]
],
[
'name' => 'pbx.digits',
'display_name' => '按键管理',
'route' => 'admin.digits',
'child' => [
['name' => 'pbx.digits.create', 'display_name' => '添加','route'=>'admin.digits.create'],
['name' => 'pbx.digits.edit', 'display_name' => '编辑','route'=>'admin.digits.edit'],
['name' => 'pbx.digits.destroy', 'display_name' => '删除','route'=>'admin.digits.destroy'],
]
],
[
'name' => 'pbx.audio',
'display_name' => '音频管理',
'route' => 'admin.audio',
'child' => [
['name' => 'pbx.audio.create', 'display_name' => '添加','route'=>'admin.audio.create'],
['name' => 'pbx.audio.destroy', 'display_name' => '删除','route'=>'admin.audio.destroy'],
]
],
],
],
[
'name' => 'record.manage',
'display_name' => '录音管理',
'route' => '',
'icon' => 'layui-icon-theme',
'child' => [
[
'name' => 'record.cdr',
'display_name' => 'CDR录音',
'route' => 'admin.cdr',
'child' => [
['name' => 'pbx.cdr.show', 'display_name' => '通话详单','route'=>'admin.cdr.show'],
['name' => 'pbx.cdr.play', 'display_name' => '播放','route'=>'admin.cdr.play'],
['name' => 'pbx.cdr.download', 'display_name' => '下载','route'=>'admin.cdr.download'],
]
],
]
],
[
'name' => 'ai.manage',
'display_name' => '批量外呼',
'route' => '',
'icon' => 'layui-icon-carousel',
'child' => [
[
'name' => 'ai.task',
'display_name' => '任务管理',
'route' => 'admin.task',
'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'],
]
],
]
],
[
'name' => 'information',
'display_name' => '资讯管理',

Binary file not shown.

Binary file not shown.

Binary file not shown.

209
public/webrtc/index.html Normal file
View File

@ -0,0 +1,209 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/sip.js" type="text/javascript"></script>
</head>
<body>
<video id="remoteVideo" name="remoteVideo" style=" background:#0C6"></video>
<video id="localVideo" name="localVideo" muted="muted" style="background:#f00"></video>
<br>
<input type="input" id='number' placeholder="number">
<input type="checkbox" id="autopickup" name="autopickup" value="1" />自动接听
<br>
<input type="button" onClick="callphone()" id='ringout' value="拨号">
<input type="button" onClick="acceptphone()" id='acceptphone' value="接听">
<input type="button" onClick="hangup()" id='hangup' value="挂机">
<input type="button" onClick="hold()" id='hold' value="保持"><br>
<br>
<input type="number" id='dtmf' placeholder="dtmf"><br>
<input type="button" onClick="senddtmf()" value="send dtmf"><br>
<br>
<div style="background:#0C6;width:100%; height:20px;">注册状态:<span id='state'>已断开</span></div>
<div style="background:#0ff;width:100%; height:20px;">呼叫状态:<span id='state0'></span></div>
<div style="background:#fC6;width:100%; overflow:visible">对端状态:<span id='state2'></span></div>
<div style="background:#cc0;width:100%; overflow:visible">本端状态:<span id='state3'></span></div>
<div id="play" style="display:none"></div>
<script type="text/javascript">
var userAgentSession;
var holdstate = false;
var userAgent = new SIP.UA({
uri: '1003@call.dgg188.cn',
wsServers: ['wss://call.dgg188.cn:7443'],
authorizationUser: '1003',
password: 'dgg123456',
hackIpInContact: true,
rtcpMuxPolicy: 'negotiate',
hackWssInTransport: true
});
//注册成功
userAgent.on('registered', function () {
$('#state').text('已连接');
});
//未注册成功
userAgent.on('unregistered', function () {
$('#state').text('已断开');
});
//监听来电
userAgent.on('invite', function (session) {
userAgentSession = session;
userAgentSession.on('bye', function (request) {//挂机
$('#state0').html('bye:');
});
userAgentSession.on('terminated', function (message, cause) {//结束
$('#state0').html('terminated:' + message + '|cause:' + cause);
});
play('ringin', 'start');
alert('来电事件');
if ($('#autopickup').is(':checked')) {
acceptphone();
}
});
//监听是否接了电话
userAgent.on('accepted', function (data) {
$('#state3').text('accepted:' + data);
});
//监听拒接
userAgent.on('rejected', function (response, cause) {
$('#state3').text('rejected:' + response + '|cause:' + cause);
});
//监听拨号
userAgent.on('progress', function (response) {
$('#state3').text('progress:' + response);
});
//错误
userAgent.on('failed', function (response, cause) {
$('#state3').text('failed:' + response + '|cause:' + cause);
});
//监听结束
userAgent.on('ended', function (response, cause) {
$('#state3').text('ended:' + response + '|cause:' + cause);
});
//播放音乐
function play(type, action) {
if (action == 'start') {
if (type == 'ringin') {
audio = 'ringin.wav';
loop = ' loop="loop"';
} else if (type == 'ringout') {
audio = 'ringout.wav';
loop = ' loop="loop"';
} else if (type == 'hangup') {
audio = 'hangup.wav';
loop = '';
}
if (type != '') {
$('#play').html('<audio autoplay="autoplay" ' + loop + '><source src="audio/' + audio + '"'
+ 'type="audio/wav"/><source src="audio/' + audio + '" type="audio/mpeg"/></audio>');
}
} else if (action == 'end') {
$('#play').html('');
}
}
//接听来电
function acceptphone() {
userAgentSession.accept({
media: {
render: {
remote: document.getElementById('remoteVideo'),
local: document.getElementById('localVideo')
},
constraints: {
audio: true,
video: false
}
}
});
play('ringin', 'end');
alert('摘机事件');
}
//发送dtmf
function senddtmf() {
var dtmf = $('#dtmf').val();
if (dtmf){
userAgentSession.dtmf(dtmf);
}
}
//挂机
function hangup() {
play('hangup', 'start');
userAgentSession.terminate();
play('hangup', 'end');
alert('挂机事件');
}
//呼叫保持
function hold() {
if (holdstate) {
holdstate = false;
userAgentSession.unhold();
} else {
userAgentSession = true;
sss.hold();
}
}
//拨打电话,呼出
function callphone() {
var phone = $('#number').val();
if (!phone) return false;
var options = {
media: {
constraints: {
audio: true,
video: false
},
render: {
remote: document.getElementById('remoteVideo'),
local: document.getElementById('localVideo')
}
}
};
userAgentSession = userAgent.invite('sip:' + phone + '@call.dgg188.cn', options);
//接听
userAgentSession.on('accepted', function (data) {
$('#state0').html('接听');
});
//取消呼叫
userAgentSession.on('cancel', function () {
$('#state0').html('取消呼叫');
});
//呼叫失败
userAgentSession.on('failed', function (response, cause) {
$('#state0').html('呼叫失败');
});
//无法接通
userAgentSession.on('rejected', function (response, cause) {
$('#state0').html('无法接通');
});
//挂机
userAgentSession.on('bye', function (request) {
$('#state0').html('挂机');
});
//结束
userAgentSession.on('terminated', function (message, cause) {
$('#state0').html('结束');
});
//拨号中
userAgentSession.on('progress', function (response) {
$('#state0').html('拨号中');
if (response.status_code === 183 && response.body && this.hasOffer && !this.dialog) {
if (!response.hasHeader('require') || response.getHeader('require').indexOf('100rel') === -1) {
if (this.mediaHandler.hasDescription(response)) {
if (!this.createDialog(response, 'UAC')) return;
this.hasAnswer = true;
this.dialog.pracked.push(response.getHeader('rseq'));
this.status = 11;
this.mute();
this.mediaHandler.setDescription(response).catch((reason) => {
this.logger.warn(reason);
this.failed(response, C.causes.BAD_MEDIA_DESCRIPTION);
this.terminate({status_code: 488, reason_phrase: 'Bad Media Description'});
})
}
}
}
});
}
</script>
</body>
</html>

18
public/webrtc/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

11865
public/webrtc/js/sip.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,78 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">坐席名称</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="display_name" lay-verify="required" value="{{$model->display_name??old('display_name')}}" placeholder="请输入坐席名称">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">坐席号码</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" maxlength="4" name="name" lay-verify="required|number" value="{{$model->name??old('name')}}" placeholder="坐席号码">
</div>
<div class="layui-form-mid layui-word-aux">请输入坐席号码7000-7999</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">状态</label>
<div class="layui-input-block">
@foreach(config('freeswitch.agent_status') as $k => $v)
<input type="radio" name="status" value="{{$k}}" title="{{$v}}" @if( (!isset($model->status)&&$k=='Available') || (isset($model->status)&&$model->status==$k) ) checked @endif >
@endforeach
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">呼叫类型</label>
<div class="layui-input-block">
<input type="radio" name="originate_type" value="user" title="分机" @if( !isset($model->originate_type) || (isset($model->originate_type)&&$model->originate_type=='user') ) checked @endif >
<input type="radio" name="originate_type" value="group" title="分机组" @if(isset($model->originate_type)&&$model->originate_type=='group') checked @endif >
<input type="radio" name="originate_type" value="gateway" title="网关" @if(isset($model->originate_type)&&$model->originate_type=='gateway') checked @endif >
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">呼叫号码</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="originate_number" lay-verify="required" value="{{$model->originate_number??old('originate_number')}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">分机号码 / 分机组标识 / 网关呼出 ,与类型对应</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">无应答数</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="max_no_answer" lay-verify="required|number" value="{{$model->max_no_answer??3}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">最大无应答次数,超过次数将不再分配话务</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">通话间隔</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="wrap_up_time" lay-verify="required|number" value="{{$model->wrap_up_time??1}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">通话完成间隔时间,成功处理一个通话后,多久才会有电话进入等待时长</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">挂机间隔</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="reject_delay_time" lay-verify="required|number" value="{{$model->reject_delay_time??1}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">挂机间隔时间,来电拒接后多久才会有电话进入的等待时长</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">繁忙间隔</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="busy_delay_time" lay-verify="required|number" value="{{$model->busy_delay_time??1}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">忙重试间隔时间,来电遇忙后多久才会有电话进入的等待时长</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">未接间隔</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="no_answer_delay_time" lay-verify="required|number" value="{{$model->no_answer_delay_time??1}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">无应答重试间隔,来电无应答后多久才会有电话进入的等待时长</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.agent')}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,14 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加坐席</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.agent.store')}}" method="post" class="layui-form">
@include('admin.agent._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新坐席</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.agent.update',['id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.agent._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,101 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm layui-btn-danger" id="listDelete"> </button>
<a class="layui-btn layui-btn-sm" href="{{ route('admin.agent.create') }}"> </a>
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.agent.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
//,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'display_name', title: '坐席名称'}
,{field: 'name', title: '坐席号码'}
,{field: 'originate_type_name', title: '呼叫类型'}
,{field: 'originate_number', title: '呼叫号码'}
,{field: 'status_name', title: '状态'}
,{field: 'max_no_answer', title: '无应答数'}
,{field: 'wrap_up_time', title: '通话间隔'}
,{field: 'reject_delay_time', title: '挂机间隔'}
,{field: 'busy_delay_time', title: '繁忙间隔'}
,{field: 'no_answer_delay_time', title: '未接间隔'}
,{fixed: 'right', width: 150, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.agent.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/agent/'+data.id+'/edit';
}
});
//按钮批量删除
$("#listDelete").click(function () {
var ids = []
var hasCheck = table.checkStatus('dataTable')
var hasCheckData = hasCheck.data
if (hasCheckData.length>0){
$.each(hasCheckData,function (index,element) {
ids.push(element.id)
})
}
if (ids.length>0){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.agent.destroy') }}",{_method:'delete',ids:ids},function (result) {
if (result.code==0){
dataTable.reload()
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
}else {
layer.msg('请选择删除项',{icon:5})
}
})
})
</script>
@endsection

View File

@ -0,0 +1,94 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<form class="layui-form">
<div class="layui-form-item">
<textarea name="text" class="layui-textarea" placeholder="请输入要合成的文本"></textarea>
</div>
<div class="layui-form-item">
<button lay-submit lay-filter="tts" class="layui-btn">合成</button>
</div>
</form>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
@can('pbx.audio.destroy')
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
@endcan
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.audio.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
//,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'text', title: '文本'}
,{field: 'url', title: '地址'}
,{field: 'auf', title: '采样率'}
,{field: 'aue', title: '编码'}
,{field: 'voice_name', title: '发音人'}
,{field: 'speed', title: '语速'}
,{field: 'volume', title: '音量'}
,{field: 'pitch', title: '音高'}
,{field: 'engine_type', title: '引擎'}
,{fixed: 'right', width: 150, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.audio.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
}
});
//提交
form.on('submit(tts)', function (data) {
layer.load();
parms = data.field;
parms['_token'] = "{{csrf_token()}}";
$.post("{{route('admin.audio.store')}}",parms,function (res) {
layer.closeAll('loading');
if (res.code==0){
layer.msg(res.msg,{icon:6},function () {
dataTable.reload({
page:{curr:1}
});
})
} else {
layer.msg(res.msg,{icon:5})
}
});
return false;
});
})
</script>
@endsection

View File

@ -31,7 +31,7 @@
base: '/static/admin/layuiadmin/' //静态资源所在路径
}).extend({
index: 'lib/index' //主入口模块
}).use(['layer'],function () {
}).use(['layer','jquery'],function () {
var $ = layui.jquery;
var layer = layui.layer;

View File

@ -0,0 +1,100 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<form class="layui-form">
<div class="layui-form-item">
<div class="layui-inline">
<label for="" class="layui-form-label">商家</label>
<div class="layui-input-inline">
<select name="merchant_id">
<option value="">请选择</option>
@foreach($merchants as $merchant)
<option value="{{$merchant->id}}">{{$merchant->username}}{{$merchant->company_name}}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">类型</label>
<div class="layui-input-inline" style="width: 100px;">
<select name="type">
<option value="">请选择</option>
<option value="1">增加</option>
<option value="2">减少</option>
</select>
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">时间</label>
<div class="layui-input-inline" style="width: 150px">
<input type="text" name="created_at_start" id="created_at_start" placeholder="开始时间" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">-</div>
<div class="layui-input-inline" style="width: 150px">
<input type="text" name="created_at_end" id="created_at_end" placeholder="结束时间" class="layui-input">
</div>
</div>
<div class="layui-inline">
<div class="layui-input-inline" style="width: 100px">
<button class="layui-btn" lay-submit lay-filter="search" > </button>
</div>
</div>
</div>
</form>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form','element','laydate'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
var laydate = layui.laydate;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.bill.data') }}"
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'company_name', title: '公司名称', templet:function (d) {
return d.merchant.company_name;
}}
,{field: 'username', title: '商家帐号', templet:function (d) {
return d.merchant.username;
}}
,{field: 'type', title: '类型', width: 80, templet:function (d) {
return d.type==1?'<span class="layui-badge layui-bg-green">增加</span>':'<span class="layui-badge layui-bg-cyan">减少</span>';
}}
,{field: 'money', title: '金额'}
,{field: 'remark', title: '备注'}
,{field: 'created_user_name', title: '创建人'}
,{field: 'created_at', title: '时间'}
]]
});
//时间选择
laydate.render({elem:'#created_at_start',type:'datetime'})
laydate.render({elem:'#created_at_end',type:'datetime'})
//搜索
form.on('submit(search)', function (data) {
parms = data.field;
dataTable.reload({
where:parms,
page:{curr:1}
});
return false;
});
})
</script>
@endsection

View File

@ -0,0 +1,43 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header">
<span class="layui-bg-cyan">通话详单需要开启语音识别事件监听!!!</span>
</div>
<div class="layui-card-body">
<table class="layui-table">
<thead>
<tr>
<th>UUID</th>
<th>内容</th>
<th>时间</th>
</tr>
</thead>
<tbody>
@forelse($record as $v)
<tr>
<td>{{$v->uuid}}</td>
<td>{{$v->text}}</td>
<td>{{$v->created_at}}</td>
</tr>
@empty
<tr><td colspan="3" align="center">暂无数据</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form','laydate'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
var laydate = layui.laydate;
})
</script>
@endsection

View File

@ -0,0 +1,128 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<form class="layui-form">
<button class="layui-btn layui-btn-sm" lay-submit lay-filter="*" >搜索</button>
<div class="layui-form-item">
<div class="layui-inline">
<label for="" class="layui-form-label">主叫号码</label>
<div class="layui-input-block">
<input type="text" name="caller_id_number" class="layui-input" placeholder="主叫号码">
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">被叫号码</label>
<div class="layui-input-block">
<input type="text" name="destination_number" class="layui-input" placeholder="被叫号码">
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">呼叫时间</label>
<div class="layui-input-inline" style="width: 160px">
<input type="text" name="start_stamp_start" id="start_stamp_start" class="layui-input" placeholder="开始时间">
</div>
<div class="layui-form-mid layui-word-aux">-</div>
<div class="layui-input-inline" style="width: 160px">
<input type="text" name="start_stamp_end" id="start_stamp_end" class="layui-input" placeholder="结束时间">
</div>
</div>
</div>
</form>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="show">通话详单</a>
<a class="layui-btn layui-btn-sm" lay-event="play">播放</a>
<a class="layui-btn layui-btn-sm" lay-event="download">下载</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form','laydate'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
var laydate = layui.laydate;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.cdr.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
//{checkbox: true,fixed: true}
{field: 'id', title: 'ID', sort: true,width:80,fixed:'left'}
,{field: 'caller_id_number', title: '主叫号码',width:100, style:'color:green'}
,{field: 'destination_number', title: '被叫号码',width:120, style:'color:#2F4056'}
,{field: 'start_stamp', title: '呼叫时间', sort: true,width:160}
,{field: 'answer_stamp', title: '应答时间', sort: true,width:160}
,{field: 'end_stamp', title: '挂断时间', sort: true,width:160}
,{field: 'duration', title: '主叫时长(秒)', sort: true, width:120, style:'color:#2F4056'}
,{field: 'billsec', title: '被叫时长(秒)', sort: true, width:120, style:'color: green'}
,{field: 'hangup_cause', title: '挂断原因', width:200}
,{field: 'aleg_uuid', title: '主叫UUID', width:300}
,{field: 'bleg_uuid', title: '被叫UUID', width:300}
,{field: 'caller_id_name', title: '主叫名称', width:160}
,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作',fixed:'right'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'show'){
layer.open({
title : '通话详单',
shadeClose : true,
type : 2,
area : ['800px','600px'],
content : '/admin/cdr/'+data.id+'/show'
})
} else if (layEvent === 'play'){
var index = layer.load()
$.get('/admin/cdr/'+data.aleg_uuid+'/play',function (res) {
layer.close(index);
if (res.code==0){
var _html = '<div style="padding:20px;">';
_html += '<audio controls="controls" autoplay src="'+res.data+'"></audio>';
_html += '</div>';
layer.open({
title : '播放录音',
type : 1,
area : ['360px','auto'],
content : _html
})
}else {
layer.msg(res.msg,{icon:5})
}
})
} else if (layEvent === 'download'){
location.href = '/admin/cdr/'+data.aleg_uuid+'/download';
}
});
//时间选择
laydate.render({type: 'datetime', elem: '#start_stamp_start'});
laydate.render({type: 'datetime', elem: '#start_stamp_end'});
//监听搜索提交
form.on('submit(*)', function(data){
dataTable.reload({
where: data.field,
page: {curr:1}
})
return false;
});
})
</script>
@endsection

View File

@ -0,0 +1,36 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="display_name" lay-verify="required" value="{{$model->display_name??old('display_name')}}" placeholder="如:挂断">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">应用</label>
<div class="layui-input-block">
<select name="application">
<option ></option>
@foreach(config('freeswitch.application') as $key => $val)
<option value="{{$key}}" @if(isset($model->application)&&$model->application==$key) selected @endif >{{$val}}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">数据</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="data" value="{{$model->data??old('data')}}" placeholder="某些应用不需要数据可留空">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">序号</label>
<div class="layui-input-block">
<input class="layui-input" type="number" name="sort" lay-verify="required" value="{{$model->sort??0}}" >
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.action',['condition_id'=>$condition->id])}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,14 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加拨号应用</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.action.store',['condition_id'=>$condition->id])}}" method="post" class="layui-form">
@include('admin.dialplan.action._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新拨号应用</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.action.update',['condition_id'=>$condition->id,'id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.dialplan.action._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,95 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm layui-btn-danger" id="listDelete"> </button>
<a class="layui-btn layui-btn-sm" href="{{ route('admin.action.create',['condition_id'=>$condition->id]) }}"> </a>
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.action.data',['condition_id'=>$condition->id]) }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'display_name', title: '名称'}
,{field: 'application_name', title: '应用'}
,{field: 'data', title: '数据'}
,{field: 'sort', title: '序号',width:80}
,{field: 'created_at', title: '添加时间',width:170}
,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.action.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/condition/'+data.condition_id+'/action/'+data.id+'/edit';
}
});
//按钮批量删除
$("#listDelete").click(function () {
var ids = []
var hasCheck = table.checkStatus('dataTable')
var hasCheckData = hasCheck.data
if (hasCheckData.length>0){
$.each(hasCheckData,function (index,element) {
ids.push(element.id)
})
}
if (ids.length>0){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.action.destroy') }}",{_method:'delete',ids:ids},function (result) {
if (result.code==0){
dataTable.reload()
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
}else {
layer.msg('请选择删除项',{icon:5})
}
})
})
</script>
@endsection

View File

@ -0,0 +1,42 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="display_name" lay-verify="required" value="{{$model->display_name??old('display_name')}}" placeholder="如:条件一">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">字段</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="field" lay-verify="required" value="{{$model->field??'destination_number'}}" placeholder="默认destination_number">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">正则</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="expression" lay-verify="required" value="{{$model->expression??old('expression')}}" placeholder="正则表达式">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">break</label>
<div class="layui-input-block">
<select name="break">
<option value="on-false" @if(isset($model->break)&&$model->break=='on-false') selected @endif >on-false</option>
<option value="on-true" @if(isset($model->break)&&$model->break=='on-true') selected @endif >on-true</option>
<option value="always" @if(isset($model->break)&&$model->break=='always') selected @endif >always</option>
<option value="never" @if(isset($model->break)&&$model->break=='never') selected @endif >never</option>
</select>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">序号</label>
<div class="layui-input-block">
<input class="layui-input" type="number" name="sort" lay-verify="required" value="{{$model->sort??0}}" >
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.condition',['extension_id'=>$extension->id])}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,14 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加拨号规则</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.condition.store',['extension_id'=>$extension->id])}}" method="post" class="layui-form">
@include('admin.dialplan.condition._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新拨号规则</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.condition.update',['extension_id'=>$extension->id,'id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.dialplan.condition._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,99 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm layui-btn-danger" id="listDelete"> </button>
<a class="layui-btn layui-btn-sm" href="{{ route('admin.condition.create',['extension_id'=>$extension->id]) }}"> </a>
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-sm" lay-event="action">拨号应用</a>
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.condition.data',['extension_id'=>$extension->id]) }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'display_name', title: '名称'}
,{field: 'field', title: '字段'}
,{field: 'expression', title: '正则'}
,{field: 'break', title: 'break'}
,{field: 'sort', title: '序号',width:80}
,{field: 'created_at', title: '添加时间',width:170}
,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.condition.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/extension/'+data.extension_id+'/condition/'+data.id+'/edit';
} else if(layEvent === 'action'){
newTab('/admin/condition/'+data.id+'/action',data.display_name+' - 拨号应用');
}
});
//按钮批量删除
$("#listDelete").click(function () {
var ids = []
var hasCheck = table.checkStatus('dataTable')
var hasCheckData = hasCheck.data
if (hasCheckData.length>0){
$.each(hasCheckData,function (index,element) {
ids.push(element.id)
})
}
if (ids.length>0){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.condition.destroy') }}",{_method:'delete',ids:ids},function (result) {
if (result.code==0){
dataTable.reload()
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
}else {
layer.msg('请选择删除项',{icon:5})
}
})
})
</script>
@endsection

View File

@ -0,0 +1,40 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">名称</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="display_name" lay-verify="required" value="{{$model->display_name??old('display_name')}}" placeholder="如:内线呼内线">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">标识符</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="name" lay-verify="required" value="{{$model->name??old('name')}}" placeholder="local_extension">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">类型</label>
<div class="layui-input-block">
<input type="radio" name="context" value="default" title="呼出" @if(!isset($model->context) || $model->context=="default") checked @endif>
<input type="radio" name="context" value="public" title="呼入" @if(isset($model->context)&&$model->context=="public") checked @endif>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">continue</label>
<div class="layui-input-inline">
<input type="radio" name="continue" value="false" title="false" @if(!isset($model->continue) || $model->continue=="false") checked @endif>
<input type="radio" name="continue" value="true" title="true" @if(isset($model->continue)&&$model->continue=="true") checked @endif >
</div>
<div class="layui-form-mid layui-word-aux">true:表示不管该extension中是否有condition匹配都继续执行dialplan。false表示如果该extension中有匹配的condition那么就停止了dialplan</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">序号</label>
<div class="layui-input-block">
<input class="layui-input" type="number" name="sort" lay-verify="required" value="{{$model->sort??0}}" >
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.extension')}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,14 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加拨号计划</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.extension.store')}}" method="post" class="layui-form">
@include('admin.dialplan.extension._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新拨号计划</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.extension.update',['id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.dialplan.extension._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,103 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm layui-btn-danger" id="listDelete"> </button>
<a class="layui-btn layui-btn-sm" href="{{ route('admin.extension.create') }}"> </a>
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="show">详情</a>
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-sm" lay-event="condition">拨号规则</a>
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form','jquery'],function () {
var $ = layui.jquery;
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.extension.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'display_name', title: '名称'}
,{field: 'name', title: '标识符'}
,{field: 'context_name', title: '类型'}
,{field: 'continue', title: 'continue'}
,{field: 'sort', title: '序号',width:80}
,{field: 'created_at', title: '添加时间',width:170}
,{fixed: 'right', width: 260, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.extension.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/extension/'+data.id+'/edit';
} else if(layEvent === 'condition'){
newTab('/admin/extension/'+data.id+'/condition',data.display_name+' - 拨号规则');
} else if(layEvent === 'show'){
newTab('/admin/extension/'+data.id+'/show',data.display_name+' - 详情');
}
});
//按钮批量删除
$("#listDelete").click(function () {
var ids = []
var hasCheck = table.checkStatus('dataTable')
var hasCheckData = hasCheck.data
if (hasCheckData.length>0){
$.each(hasCheckData,function (index,element) {
ids.push(element.id)
})
}
if (ids.length>0){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.extension.destroy') }}",{_method:'delete',ids:ids},function (result) {
if (result.code==0){
dataTable.reload()
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
}else {
layer.msg('请选择删除项',{icon:5})
}
})
})
</script>
@endsection

View File

@ -0,0 +1,55 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>拨号计划详情</h2>
</div>
<div class="layui-card-body">
<table class="layui-table" lay-skin="nob">
<tr>
@php
$tips1 = "名称:".$extension->display_name."<br/>标识符:".$extension->name."<br/>continue".$extension->continue."<br/>类型:".$extension->context_name;
@endphp
<td onmouseleave="layer.closeAll()" onmouseenter="layer.tips('{{$tips1}}', this, {tips: 2,time:0});" >
<span>{{$extension->display_name}}</span>
</td>
<td>
@if($extension->conditions->isNotEmpty())
<table class="layui-table" lay-skin="row">
@foreach($extension->conditions as $condition)
<tr>
@php
$tips2 = "名称:".$condition->display_name."<br/>字段:".$condition->field."<br/>正则:".$condition->expression."<br/>break".$condition->break;
@endphp
<td onmouseleave="layer.closeAll()" onmouseenter="layer.tips('{{$tips2}}', this, {tips: 4,time:0});" >
<span class="layui-badge layui-bg-cyan">{{$condition->sort}}</span>
<span>{{$condition->display_name}}</span>
</td>
<td>
@if($condition->actions->isNotEmpty())
<table class="layui-table" lay-skin="line">
@foreach($condition->actions as $action)
<tr>
@php
$tips3 = "名称:".$action->display_name."<br/>应用:".$action->application_name."<br/>数据:".$action->data;
@endphp
<td onmouseleave="layer.closeAll()" onmouseenter="layer.tips('{{$tips3}}', this, {tips: 4,time:0});">
<span class="layui-badge layui-bg-cyan">{{$action->sort}}</span>
<span>{{$action->display_name}}</span>
</td>
</tr>
@endforeach
</table>
@endif
</td>
</tr>
@endforeach
</table>
@endif
</td>
</tr>
</table>
</div>
</div>
@endsection

View File

@ -0,0 +1,167 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<form class="layui-form">
<div id="digits-box">
<div class="layui-form-item">
<div class="layui-inline">
<label for="" class="layui-form-label">按键</label>
<div class="layui-input-inline" style="width: 80px">
<input type="number" lay-verify="required|number" class="layui-input digits" placeholder="0-9">
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">应用</label>
<div class="layui-input-inline">
<select class="action" lay-verify="required">
<option value="">请选择</option>
<option value="menu-exec-app">应用</option>
<option value="mmenu-sub">子菜单</option>
<option value="enu-top">父菜单</option>
</select>
</div>
</div>
<div class="layui-inline">
<label for="" class="layui-form-label">参数</label>
<div class="layui-input-inline">
<input type="text" class="layui-input param" placeholder="请输入应用参数">
</div>
</div>
<div class="layui-inline">
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm" onclick="digits_add();">增加</button>
<button type="button" class="layui-btn layui-btn-primary layui-btn-sm">删除</button>
</div>
</div>
</div>
<div class="layui-form-item">
<button lay-submit lay-filter="digits-submit" class="layui-btn">确认</button>
</div>
</form>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
@can('pbx.digits.destroy')
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
@endcan
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
window.digits_add = function() {
var _html = '<div class="layui-form-item">\n' +
' <div class="layui-inline">\n' +
' <label for="" class="layui-form-label">按键</label>\n' +
' <div class="layui-input-inline" style="width: 80px">\n' +
' <input type="number" lay-verify="required|number" class="layui-input digits" placeholder="0-9">\n' +
' </div>\n' +
' </div>\n' +
' <div class="layui-inline">\n' +
' <label for="" class="layui-form-label">应用</label>\n' +
' <div class="layui-input-inline">\n' +
' <select class="action" lay-verify="required">\n' +
' <option value="">请选择</option>\n' +
' <option value="menu-exec-app">应用</option>\n' +
' <option value="mmenu-sub">子菜单</option>\n' +
' <option value="enu-top">父菜单</option>\n' +
' </select>\n' +
' </div>\n' +
' </div>\n' +
' <div class="layui-inline">\n' +
' <label for="" class="layui-form-label">参数</label>\n' +
' <div class="layui-input-inline">\n' +
' <input type="text" class="layui-input param" placeholder="请输入应用参数">\n' +
' </div>\n' +
' </div>\n' +
' <div class="layui-inline">\n' +
' <button type="button" class="layui-btn layui-btn-primary layui-btn-sm" onclick="digits_add();">增加</button>\n' +
' <button type="button" class="layui-btn layui-btn-primary layui-btn-sm" onclick="digits_del(this);">删除</button>\n' +
' </div>\n' +
' </div>'
$("#digits-box").append(_html);
form.render();
}
window.digits_del = function (obj) {
$(obj).parent().parent('.layui-form-item').remove();
}
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.digits.data',['ivr_id'=>$ivr_id]) }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
//,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'ivr_name', title: 'IVR名称'}
,{field: 'digits', title: '按键'}
,{field: 'action_name', title: '应用'}
,{field: 'param', title: '参数'}
,{field: 'created_at', title: '添加时间'}
,{fixed: 'right', width: 150, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.audio.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
}
});
//提交
form.on('submit(digits-submit)', function (data) {
layer.load();
var parm = [];
$("#digits-box .layui-form-item").each(function (index,elem) {
parm[index] = {
"digits":$(elem).find(".digits").val(),
"action":$(elem).find(".action").val(),
"param":$(elem).find(".param").val(),
"ivr_id":"{{$ivr_id}}"
}
})
if (parm.length<=0){
layer.closeAll('loading');
return false;
}
$.post("{{route('admin.digits.store')}}",{parm:parm,_token:"{{csrf_token()}}"},function (res) {
layer.closeAll('loading');
if (res.code==0){
layer.msg(res.msg,{icon:6},function () {
location.reload()
})
} else {
layer.msg(res.msg,{icon:5})
}
});
return false;
});
})
</script>
@endsection

View File

@ -0,0 +1,51 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">网关名称</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="name" lay-verify="required" value="{{$model->name??old('name')}}" placeholder="如:联通">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">网关地址</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="realm" lay-verify="required" value="{{$model->realm??old('realm')}}" placeholder="格式192.168.254.100:5066">
</div>
<div class="layui-form-mid layui-word-aux">默认5060端口</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">帐号</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="username" lay-verify="required" value="{{$model->username??old('username')}}" placeholder="Job">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">密码</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="password" lay-verify="required" value="{{$model->password??old('password')}}" placeholder="123456">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">费率</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="rate" lay-verify="required" value="{{$model->rate??old('rate')}}" placeholder="0.01">
</div>
<div class="layui-form-mid layui-word-aux">/分钟</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">前缀</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="prefix" value="{{$model->prefix??old('prefix')}}" placeholder="非必填">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">出局号码</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="outbound_caller_id" value="{{$model->outbound_caller_id??old('outbound_caller_id')}}" placeholder="非必填">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.gateway')}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,14 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加网关</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.gateway.store')}}" method="post" class="layui-form">
@include('admin.gateway._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,15 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新网关</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.gateway.update',['id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.gateway._form')
</form>
</div>
</div>
@endsection

View File

@ -0,0 +1,109 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
<button class="layui-btn layui-btn-sm layui-btn-danger" id="listDelete"> </button>
<a class="layui-btn layui-btn-sm" href="{{ route('admin.gateway.create') }}"> </a>
<button class="layui-btn layui-btn-sm" id="updateXml">更新配置</button>
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.gateway.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'name', title: '名称'}
,{field: 'realm', title: '地址'}
,{field: 'username', title: '帐号'}
,{field: 'password', title: '密码'}
,{field: 'rate', title: '费率(元/分钟)'}
,{field: 'prefix', title: '前缀'}
,{field: 'outbound_caller_id', title: '出局号码'}
,{field: 'created_at', title: '添加时间'}
,{fixed: 'right', width: 220, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.gateway.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/gateway/'+data.id+'/edit';
}
});
//按钮批量删除
$("#listDelete").click(function () {
var ids = []
var hasCheck = table.checkStatus('dataTable')
var hasCheckData = hasCheck.data
if (hasCheckData.length>0){
$.each(hasCheckData,function (index,element) {
ids.push(element.id)
})
}
if (ids.length>0){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.gateway.destroy') }}",{_method:'delete',ids:ids},function (result) {
if (result.code==0){
dataTable.reload()
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
}else {
layer.msg('请选择删除项',{icon:5})
}
})
//更新配置
$("#updateXml").click(function () {
layer.confirm('该操作将重新注册所有网关,确认操作吗?', function(index){
$.post("{{ route('admin.gateway.updateXml') }}",{_method:'post',_token:'{{csrf_token()}}'},function (result) {
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
})
})
</script>
@endsection

View File

@ -0,0 +1,74 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">显示名称</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="display_name" lay-verify="required" value="{{$model->display_name??old('display_name')}}" placeholder="请输入名称">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">标识</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="name" lay-verify="required" value="{{$model->name??old('name')}}" placeholder="请输入唯一标识如demo1">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">欢迎音</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="greet_long" lay-verify="required" value="{{$model->greet_long??old('greet_long')}}" placeholder="路径或者在线合成">
</div>
<div class="layui-word-aux layui-form-mid">首次进入语音导航的欢迎音</div>
<div class="layui-input-inline">
<button type="button" class="layui-btn tts-btn">在线合成</button>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">简短提示</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="greet_short" lay-verify="required" value="{{$model->greet_short??old('greet_short')}}" placeholder="路径或者在线合成">
</div>
<div class="layui-word-aux layui-form-mid">用户长时间没有按键时提示</div>
<div class="layui-input-inline">
<button type="button" class="layui-btn tts-btn">在线合成</button>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">超时时间</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="timeout" lay-verify="required" value="{{$model->timeout??10000}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">多长时间没有收到按键就超时(毫秒)</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">按键间隔</label>
<div class="layui-input-inline">
<input class="layui-input" type="number" name="inter_digit_timeout" lay-verify="required|number" value="{{$model->inter_digit_timeout??2000}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">两次按键的最大间隔(毫秒)</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">错误次数</label>
<div class="layui-input-inline">
<input class="layui-input" type="number" name="max_failures" lay-verify="required|number" value="{{$model->max_failures??3}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">用户按键错误的次数</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">超时次数</label>
<div class="layui-input-inline">
<input class="layui-input" type="number" name="max_timeouts" lay-verify="required|number" value="{{$model->max_timeouts??3}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">挂机间隔时间,来电拒接后多久才会有电话进入的等待时长</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">按键位数</label>
<div class="layui-input-inline">
<input class="layui-input" type="number" name="digit_len" lay-verify="required|number" value="{{$model->digit_len??4}}" placeholder="">
</div>
<div class="layui-form-mid layui-word-aux">菜单项的长度,即最大收号位数</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.ivr')}}" class="layui-btn" > </a>
</div>
</div>

View File

@ -0,0 +1,18 @@
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
$(".tts-btn").click(function () {
layer.open({
type:2,
title:'语音在线合成',
area:['80%','80%'],
shadeClose:true,
content:"{{route('admin.audio')}}"
})
})
})
</script>

View File

@ -0,0 +1,18 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>添加IVR</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.ivr.store')}}" method="post" class="layui-form">
@include('admin.ivr._form')
</form>
</div>
</div>
@endsection
@section('script')
@include('admin.ivr._js')
@endsection

View File

@ -0,0 +1,19 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<h2>更新IVR</h2>
</div>
<div class="layui-card-body">
<form action="{{route('admin.ivr.update',['id'=>$model->id])}}" method="post" class="layui-form">
{{method_field('put')}}
@include('admin.ivr._form')
</form>
</div>
</div>
@endsection
@section('script')
@include('admin.ivr._js')
@endsection

View File

@ -0,0 +1,99 @@
@extends('admin.base')
@section('content')
<div class="layui-card">
<div class="layui-card-header layuiadmin-card-header-auto">
<div class="layui-btn-group">
@can('pbx.ivr.create')
<a class="layui-btn layui-btn-sm" href="{{ route('admin.ivr.create') }}"> </a>
@endcan
@can('pbx.ivr.updateXml')
<button class="layui-btn layui-btn-sm" id="updateXml" >更新配置</button>
@endcan
</div>
</div>
<div class="layui-card-body">
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="options">
<div class="layui-btn-group">
<a class="layui-btn layui-btn-sm" lay-event="digits">按键</a>
@can('pbx.ivr.edit')
<a class="layui-btn layui-btn-sm" lay-event="edit">编辑</a>
@endcan
@can('pbx.ivr.destroy')
<a class="layui-btn layui-btn-danger layui-btn-sm " lay-event="del">删除</a>
@endcan
</div>
</script>
</div>
</div>
@endsection
@section('script')
<script>
layui.use(['layer','table','form'],function () {
var layer = layui.layer;
var form = layui.form;
var table = layui.table;
//用户表格初始化
var dataTable = table.render({
elem: '#dataTable'
,height: 500
,url: "{{ route('admin.ivr.data') }}" //数据接口
,page: true //开启分页
,cols: [[ //表头
{checkbox: true,fixed: true}
//,{field: 'id', title: 'ID', sort: true,width:80}
,{field: 'display_name', title: '名称'}
,{field: 'name', title: '标识'}
,{field: 'greet_long', title: '欢迎音'}
,{field: 'greet_short', title: '简短提示'}
,{field: 'timeout', title: '超时时间(毫秒)'}
,{field: 'inter_digit_timeout', title: '按键间隔(毫秒)'}
,{field: 'max_failures', title: '错误次数'}
,{field: 'max_timeouts', title: '超时次数'}
,{field: 'digit_len', title: '按键位数'}
,{fixed: 'right', width: 150, align:'center', toolbar: '#options', title:'操作'}
]]
});
//监听工具条
table.on('tool(dataTable)', function(obj){ //注tool是工具条事件名dataTable是table原始容器的属性 lay-filter="对应的值"
var data = obj.data //获得当前行数据
,layEvent = obj.event; //获得 lay-event 对应的值
if(layEvent === 'del'){
layer.confirm('确认删除吗?', function(index){
$.post("{{ route('admin.ivr.destroy') }}",{_method:'delete',ids:[data.id]},function (result) {
if (result.code==0){
obj.del(); //删除对应行tr的DOM结构
}
layer.close(index);
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
});
} else if(layEvent === 'edit'){
location.href = '/admin/ivr/'+data.id+'/edit';
} else if(layEvent === 'digits'){
layer.open({
type:2,
title:'IVR按键菜单',
area:['80%','80%'],
shadeClose:true,
content:"/admin/digits?ivr_id="+data.id
})
}
});
//更新配置
$("#updateXml").click(function () {
layer.confirm('该操作将重新配置所有IVR确认操作吗', function(index){
$.post("{{ route('admin.ivr.updateXml') }}",{_method:'post',_token:'{{csrf_token()}}'},function (result) {
var icon = result.code==0?6:5;
layer.msg(result.msg,{icon:icon})
});
})
})
})
</script>
@endsection

View File

@ -0,0 +1,50 @@
{{csrf_field()}}
<div class="layui-form-item">
<label for="" class="layui-form-label">帐号</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="username" lay-verify="required" value="{{$model->username??old('username')}}" placeholder="商家帐号">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">密码</label>
<div class="layui-input-inline">
<input class="layui-input" type="password" name="password" @if(!isset($model)) lay-verify="required" @endif value="{{old('password')}}" placeholder="商家密码">
</div>
<div class="layui-word-aux layui-form-mid">不修改则留空</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">公司名称</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="company_name" lay-verify="required" value="{{$model->company_name??old('company_name')}}" placeholder="公司名称">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">分机数量</label>
<div class="layui-input-inline">
<input class="layui-input" type="number" name="sip_num" lay-verify="required|number" value="{{$model->sip_num??old('sip_num')}}" placeholder="分机数量">
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">到期时间</label>
<div class="layui-input-inline">
<input class="layui-input" type="text" name="expires_at" id="expires_at" lay-verify="required" value="{{$model->expires_at??old('expires_at')}}" placeholder="点击选择" readonly>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label">状态</label>
<div class="layui-input-inline">
<select name="status" lay-verify="required">
<option value="">请选择</option>
@foreach(config('freeswitch.merchant_status') as $k=>$v)
<option value="{{$k}}" @if(isset($model)&&$model->status==$k) selected @endif >{{$v}}</option>
@endforeach
</select>
</div>
</div>
<div class="layui-form-item">
<label for="" class="layui-form-label"></label>
<div class="layui-input-inline">
<button type="submit" class="layui-btn" lay-submit lay-filter="*" > </button>
<a href="{{route('admin.merchant')}}" class="layui-btn" > </a>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More