diff --git a/application/.htaccess b/application/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/application/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/application/command.php b/application/command.php new file mode 100644 index 0000000..e1071fa --- /dev/null +++ b/application/command.php @@ -0,0 +1,14 @@ + +// +---------------------------------------------------------------------- + +return [ + 'app\swoole\command\Chat', +]; diff --git a/application/common.php b/application/common.php new file mode 100644 index 0000000..3c537f9 --- /dev/null +++ b/application/common.php @@ -0,0 +1,14 @@ + +// +---------------------------------------------------------------------- + +// 应用公共文件 + + diff --git a/application/index/controller/Base.php b/application/index/controller/Base.php new file mode 100644 index 0000000..9835725 --- /dev/null +++ b/application/index/controller/Base.php @@ -0,0 +1,24 @@ +redirect('login/login'); + } + } +} diff --git a/application/index/controller/Index.php b/application/index/controller/Index.php new file mode 100644 index 0000000..c0e44f9 --- /dev/null +++ b/application/index/controller/Index.php @@ -0,0 +1,54 @@ +where('kefu_code', $kefu_code)->find(); + if(!$kefu_info){ + return '客服不存在'; + } + $visitor_id = uniqid($kefu_info['kefu_id']); + $visitor_name = '游客'.$visitor_id; + $visitor_avatar ='/static/common/images/visitor.jpg'; + $config= Config::pull('swoole_server'); + $port=$config['port']; + $this->assign('port',$port); + $this->assign('code',$kefu_code); + $this->assign('uid',$visitor_id); + $this->assign('name',$visitor_name); + $this->assign('avatar',$visitor_avatar); + return $this->fetch(); + } + + + //获取访客聊天记录 + public function getUserChatLog() + { + $uid = input('param.uid'); + $kefu_code = input('param.kefu_code'); + if (!$uid || !$kefu_code ) { + return '参数错误'; + } + $sql = "SELECT * FROM chat_log WHERE ( from_id = '{$uid}' and to_id ='{$kefu_code}') or (from_id = '{$kefu_code}' and to_id ='{$uid}') order by create_time"; + $list = Db::query($sql); + if (empty($list)) return json($list); + foreach ($list as $key => $item) { + if (strpos($item['from_id'], 'KF_') === false) { + $list[$key]['log'] = 'visitor'; + } else { + $list[$key]['log'] = 'kefu'; + } + } + return json(['code'=>200,'data'=>$list,'msg'=>'操作成功']); + + } +} diff --git a/application/index/controller/Kefu.php b/application/index/controller/Kefu.php new file mode 100644 index 0000000..913fbe9 --- /dev/null +++ b/application/index/controller/Kefu.php @@ -0,0 +1,55 @@ +assign('port',$port); + $this->assign('url',request()->domain().'/index/index/user?kefu_code='.session('kefu_code')); + $this->assign('kefu_name',session('kefu_name')); + $this->assign('kefu_code',session('kefu_code')); + return $this->fetch(); + } + public function getQueue(){ + $kefu_code = input('param.kefu_code'); + $reception_status = input('param.status',1); + $queue = Db::name('visitor_queue')->where('kefu_code',$kefu_code)->where('reception_status',$reception_status)->select(); + return json(['code'=>200,'data'=>$queue,'msg'=>'操作成功']); + + } + + //获取客服聊天记录 + public function getUserChatLog() + { + $uid = input('param.uid'); + $kefu_code = input('param.kefu_code'); + if (!$uid || !$kefu_code ) { + return '参数错误'; + } + $sql = "SELECT * FROM chat_log WHERE ( from_id = '{$kefu_code}' and to_id ='{$uid}') or (from_id = '{$uid}' and to_id ='{$kefu_code}') order by create_time"; + $list = Db::query($sql); + if (empty($list)) return json(['code'=>200,'data'=>$list,'msg'=>'操作成功']); + foreach ($list as $key => $item) { + if (strpos($item['from_id'], 'KF_') === false) { + $list[$key]['log'] = 'visitor'; + } else { + $list[$key]['log'] = 'kefu'; + } + } + return json(['code'=>200,'data'=>$list,'msg'=>'操作成功']); + + } +} diff --git a/application/index/controller/Login.php b/application/index/controller/Login.php new file mode 100644 index 0000000..727abb5 --- /dev/null +++ b/application/index/controller/Login.php @@ -0,0 +1,73 @@ +fetch(); + } + + public function Logining() + { + + $name = input('post.name'); + $password = input('post.password'); + if (empty($name)) { + return $this->error('请输入用户名'); + } + if (empty($password)) { + return $this->error('请输入密码'); + } + $kefu_info = Db::name('kefu_info')->where('kefu_name', $name)->find(); + if ($kefu_info) { + if (md5(trim($password)) != $kefu_info['kefu_password']) { + return $this->error('密码错误'); + } + session('kefu_name', $kefu_info['kefu_name']); + session('kefu_code', $kefu_info['kefu_code']); + } else { + //同一个ip 限制注册3个账号 + $num = Cache::get(request()->ip()); + if ($num > 3) { + return $this->error('同一ip限制注册三个账号'); + } + + //添加客服 + $kefu_data = [ + 'kefu_code' => uniqid('kefu'), + 'kefu_name' => trim($name), + 'kefu_avatar' => '/static/common/images/kefu.jpg', + 'kefu_password' => md5(trim($password)), + 'kefu_status' => 1, + 'online_status' => 0, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ]; + Db::name('kefu_info')->insertGetId($kefu_data); + Db::commit(); + Cache::inc(request()->ip()); + session('kefu_name', $kefu_data['kefu_name']); + session('kefu_code', $kefu_data['kefu_code']); + + } + + return $this->redirect('kefu/index'); + } + public function logout(){ + session('kefu_name',null); + session('kefu_code', null); + return $this->redirect('login/login'); + } +} diff --git a/application/index/view/index/user.html b/application/index/view/index/user.html new file mode 100644 index 0000000..a647621 --- /dev/null +++ b/application/index/view/index/user.html @@ -0,0 +1,68 @@ + + + + + + + 游客聊天页面 + + + + + +
+ + +
+ + +
+
+
+
+ +
+
+
请等待连接客服....
+
+
+ +
+
+
+ + + +
+
+ +
+
+
+ + + + + + + + + + diff --git a/application/index/view/kefu/index.html b/application/index/view/kefu/index.html new file mode 100644 index 0000000..0913063 --- /dev/null +++ b/application/index/view/kefu/index.html @@ -0,0 +1,181 @@ + + + + + + + 客服工作台 + + + + + + +
+ + +
+ + + + + + +
+ + + + + + +
+
+
+
+ +
+
+
+ + + +
+
+
+ +
+
+ +
+ +
+ +
+ + + +
+ + +
+ +
+ + +
+ + + + + + + + + + diff --git a/application/index/view/login/login.html b/application/index/view/login/login.html new file mode 100644 index 0000000..45d07da --- /dev/null +++ b/application/index/view/login/login.html @@ -0,0 +1,34 @@ + + + + + + + Timely客服登陆 + + + + +
+
Timely客服登陆
+
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+ + + + diff --git a/application/provider.php b/application/provider.php new file mode 100644 index 0000000..d0fcd24 --- /dev/null +++ b/application/provider.php @@ -0,0 +1,14 @@ + +// +---------------------------------------------------------------------- + +// 应用容器绑定定义 +return [ +]; diff --git a/application/swoole/command/Chat.php b/application/swoole/command/Chat.php new file mode 100644 index 0000000..32fcf33 --- /dev/null +++ b/application/swoole/command/Chat.php @@ -0,0 +1,261 @@ +setName('chat') + ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload", 'start') + ->addOption('host', 'H', Option::VALUE_OPTIONAL, 'the host of swoole server.', null) + ->addOption('port', 'p', Option::VALUE_OPTIONAL, 'the port of swoole server.', null) + ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the swoole server in daemon mode.') + ->setDescription('chat Swoole Server for ThinkPHP'); + } + + public function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + if (!in_array($action, ['start', 'stop', 'reload', 'restart'])) { + $output->writeln("Invalid argument action:{$action}, Expected start|stop|restart|reload ."); + return false; + } + //timely TIMELY + $brand = <<writeln($brand . PHP_EOL); + $this->init(); + $this->$action(); + + + } + + protected function init() + { + $this->config = Config::pull('swoole_server'); + + if (empty($this->config['pid_file'])) { + $this->config['pid_file'] = Env::get('runtime_path') . 'swoole_server.pid'; + } + + // 避免pid混乱 + $this->config['pid_file'] .= '_' . $this->getPort(); + } + + /** + * 启动server + * @access protected + * @return void + */ + protected function start() + { + $pid = $this->getMasterPid(); + + if ($this->isRunning($pid)) { + $this->output->writeln('swoole server process is already running.'); + return false; + } + + $this->output->writeln('Starting swoole server...'); + + if (!empty($this->config['swoole_class'])) { + $class = $this->config['swoole_class']; + + if (class_exists($class)) { + $swoole = new $class; + if (!$swoole instanceof ThinkServer) { + $this->output->writeln("Swoole Server Class Must extends \\think\\swoole\\Server"); + return false; + } + } else { + $this->output->writeln("Swoole Server Class Not Exists : {$class}"); + return false; + } + } else { + $host = $this->getHost(); + $port = $this->getPort(); + $type = !empty($this->config['type']) ? $this->config['type'] : 'socket'; + $mode = !empty($this->config['mode']) ? $this->config['mode'] : SWOOLE_PROCESS; + $sockType = !empty($this->config['sock_type']) ? $this->config['sock_type'] : SWOOLE_SOCK_TCP; + + switch ($type) { + case 'socket': + $swooleClass = 'Swoole\Websocket\Server'; + break; + case 'http': + $swooleClass = 'Swoole\Http\Server'; + break; + default: + $swooleClass = 'Swoole\Server'; + } + + $swoole = new $swooleClass($host, $port, $mode, $sockType); + + // 开启守护进程模式 + if ($this->input->hasOption('daemon')) { + $this->config['daemonize'] = true; + } + + foreach ($this->config as $name => $val) { + if (0 === strpos($name, 'on')) { + $swoole->on(substr($name, 2), $val); + unset($this->config[$name]); + } + } + + // 设置服务器参数 + $swoole->set($this->config); + + $this->output->writeln("Swoole {$type} server started: <{$host}:{$port}>" . PHP_EOL); + $this->output->writeln('You can exit with `CTRL-C`'); + + // 启动服务 + $swoole->start(); + } + } + + /** + * 柔性重启server + * @access protected + * @return void + */ + protected function reload() + { + // 柔性重启使用管理PID + $pid = $this->getMasterPid(); + + if (!$this->isRunning($pid)) { + $this->output->writeln('no swoole server process running.'); + return false; + } + + $this->output->writeln('Reloading swoole server...'); + Process::kill($pid, SIGUSR1); + $this->output->writeln('> success'); + } + + /** + * 停止server + * @access protected + * @return void + */ + protected function stop() + { + $pid = $this->getMasterPid(); + + if (!$this->isRunning($pid)) { + $this->output->writeln('no swoole server process running.'); + return false; + } + + $this->output->writeln('Stopping swoole server...'); + + Process::kill($pid, SIGTERM); + $this->removePid(); + + $this->output->writeln('> success'); + } + + protected function getHost() + { + if ($this->input->hasOption('host')) { + $host = $this->input->getOption('host'); + } else { + $host = !empty($this->config['host']) ? $this->config['host'] : '0.0.0.0'; + } + + return $host; + } + + /** + * 删除PID文件 + * @access protected + * @return void + */ + protected function removePid() + { + $masterPid = $this->config['pid_file']; + + if (is_file($masterPid)) { + unlink($masterPid); + } + } + + protected function getPort() + { + if ($this->input->hasOption('port')) { + $port = $this->input->getOption('port'); + } else { + $port = !empty($this->config['port']) ? $this->config['port'] : 9501; + } + + return $port; + } + + /** + * 获取主进程PID + * @access protected + * @return int + */ + protected function getMasterPid() + { + $pidFile = $this->config['pid_file']; + + if (is_file($pidFile)) { + $masterPid = (int)file_get_contents($pidFile); + } else { + $masterPid = 0; + } + + return $masterPid; + } + + /** + * 判断PID是否在运行 + * @access protected + * @param int $pid + * @return bool + */ + protected function isRunning($pid) + { + if (empty($pid)) { + return false; + } + + return Process::kill($pid, 0); + } +} diff --git a/application/swoole/service/Event.php b/application/swoole/service/Event.php new file mode 100644 index 0000000..c86c508 --- /dev/null +++ b/application/swoole/service/Event.php @@ -0,0 +1,248 @@ + $data['visitor_id'], + 'client_id' => $fd, + 'visitor_name' => $data['visitor_name'], + 'visitor_avatar' => $data['visitor_avatar'], + 'visitor_ip' => '127.0.0.1', + 'create_time' => date('Y-m-d H:i:s'), + 'reception_status' => 0,//等待客服接待状态 + ]; + QueueLogic::updateQueue($queue); + #2.游客信息更新 + $data = [ + 'visitor_id' => $data['visitor_id'], + 'client_id' => $fd, + 'visitor_name' => $data['visitor_name'], + 'visitor_avatar' => $data['visitor_avatar'], + 'visitor_ip' => '127.0.0.1', + 'create_time' => date('Y-m-d H:i:s'), + 'online_status' => 1 + ]; + Visitor::updateCustomer($data); + //设置游客信息 + self::$online[$fd] = $data['visitor_id']; + self::setVisitor($fd, $data['visitor_id']); + return self::reposon($fd, 200, '上线成功', [], 'online'); + } catch (BaseException $e) { + throw new BaseException('上线错误', 401); + } + } + + /** + * 游客连接客服 + * @param $fd 客户端标识 + * @param $data 请求数据 + */ + public static function visitorToKefu($fd, $data, $server) + { + + #1.分配客服 + $visitor = [ + 'visitor_id' => $data['uid'], + 'visitor_name' => $data['name'], + 'visitor_avatar' => $data['avatar'], + 'visitor_ip' => '127.0.0.1', + 'client_id' => $fd, + 'kefu_code'=>$data['kefu_code'], + ]; + try { + Db::startTrans(); + $kefu_info = KefuLogic::distributionKefu($visitor); + Log::record('分配客服数据:' . json_encode($kefu_info)); + if ($kefu_info['code'] == 200) { + #1.记录服务日志 + $logId = VisitorService::addServiceLog([ + 'visitor_id' => $visitor['visitor_id'], + 'client_id' => $fd, + 'visitor_name' => $visitor['visitor_name'], + 'visitor_avatar' => $visitor['visitor_avatar'], + 'visitor_ip' => $visitor['visitor_ip'], + 'kefu_id' => $kefu_info['data']['kefu_id'], + 'kefu_code' => ltrim($kefu_info['data']['kefu_code'], 'KF_'), + 'start_date' => date('Y-m-d H:i:s'), + ]); + try { + if ($server->exist((int)$kefu_info['data']['kefu_client_id']) == false) { + Db::rollback(); + return self::reposon($fd, 201, '客服不存在或者客服不在线', [], 'visitorToKefu'); + } + $kefu_info['data']['log_id'] = $logId; + // 更新队列表 + $update['reception_status'] = 1;//更改连接状态 + $update['kefu_code'] = ltrim($kefu_info['data']['kefu_code'], 'KF_'); + $update['kefu_client_id'] = $kefu_info['data']['kefu_client_id']; + QueueLogic::updateQueueByCusomerID($visitor['visitor_id'], $update); + #3.绑定客服和游客 bengan + self::$visitor[$fd]['bind_kefu_fd'] = $kefu_info['data']['kefu_client_id']; + self::$visitor[$fd]['bind_kefu_code'] =$kefu_info['data']['kefu_code']; + self::$kefu[$kefu_info['data']['kefu_code']]['visitor_fds'][$visitor['visitor_id']]= $fd; + #end + Db::commit(); + return self::reposon($fd, 200, $kefu_info['msg'], $kefu_info['data'], 'visitorToKefu'); + } catch (Exception $e) { + Db::rollback(); + Log::info('分配客服数据错误信息:' . $e->getMessage()); + //取消客服在线状态 + KefuLogic::setKefuOnlineStatus(ltrim($kefu_info['data']['kefu_code'], 'KF_'), '', 0); + return self::reposon($fd, 401, '请重新尝试分配客服1', [], 'visitorToKefu'); + } + + }else if($kefu_info['code'] == 201){ + return self::reposon($fd, 201, '客服不存在或者客服不在线', [], 'visitorToKefu'); + } + Db::commit(); + } catch (BaseException $e) { + Db::rollback(); + Log::record('分配客服数据错误信息:' . $e->getMessage()); + return self::reposon($fd, 402, '请重新尝试分配客服2', [], 'visitorToKefu'); + } + unset($customer, $kefu_info); + } + + /** + * 聊天 + * @param $fd 客户端标识 + * @param $data 请求数据 + */ + public static function message($fd, $data, $server) + { + Log::record('聊天信息[' . json_encode($data) . ']'); + Log::record('聊天信息1[' . json_encode(self::$online[$fd]) . ']'); + Log::record('聊天信息2[' . json_encode(self::$visitor[$fd]) . ']'); + Log::record('聊天信息3[' . json_encode(self::$kefu) . ']'); + $uid = self::$online[$fd]; + try { + //消息入库 + $chat_log_id = ChatLogLogic::addChatLog($data); + $message = [ + 'name' => $data['from_name'], + 'avatar' => $data['from_avatar'], + 'id' => $data['from_id'], + 'time' => date('Y-m-d H:i:s'), + 'message' => htmlspecialchars($data['message']), + 'log_id' => $chat_log_id + ]; + if (strstr($uid, "KF_") !== false) {//客服发信息给游客 + + } else { //游客发送给客服 + //获取 客服的fd + if(!isset(self::$visitor[$fd]['bind_kefu_code']) || ($server->exist((int)self::$kefu[self::$visitor[$fd]['bind_kefu_code']]['fd']) == false)){ + //更新聊天日志状态 + ChatLogLogic::updateSendStatus($chat_log_id, 2); + return self::reposon($fd, 201, '客服离线', $message, 'message'); + } else { + $kefu_fd = self::$kefu[self::$visitor[$fd]['bind_kefu_code']]['fd']; + $resut = self::reposon((int)$kefu_fd, 200, '来新信息了', $message, 'chatMessage'); + $server->push($resut['fd'], $resut['data']); + } + } + + } catch (BaseException $e) { + return self::reposon($fd, 400, '消息发送失败', $data['message'], 'message'); + } + + return self::reposon($fd, 200, '信息发送成功', $message, 'message'); + + } + public static function disconnect($fd, $server){ + return true; + } + + public static function reposon($fd, $code = 200, $msg = "操作成功", $data = '', $cmd = '') + { + $reposon['fd'] = $fd; + $reposon['data'] = json_encode([ + 'code' => $code, + 'msg' => $msg, + 'data' => $data, + 'cmd' => $cmd, + ], JSON_UNESCAPED_UNICODE); + + return $reposon; + } +} diff --git a/application/swoole/service/Service.php b/application/swoole/service/Service.php new file mode 100644 index 0000000..50797d6 --- /dev/null +++ b/application/swoole/service/Service.php @@ -0,0 +1,72 @@ +fd}\n"; + } + + + public function onMessage($server, $frame) + { + echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; + try { + Log::record('WebSocket请求开始,请求信息[' . json_encode($frame) . ']'); + $data = json_decode($frame->data, true); + $cmd = $data['cmd']; + $messge = $data['data']; + $resut = Event::$cmd($frame->fd, $messge,$server); + $server->push($resut['fd'], $resut['data']); + } catch (BaseException $e) { + Log::record('WebSocket请求异常,异常信息' . $e->getMessage()); + Log::record('WebSocket请求异常,异常信息' . $e->getFile().$e->getLine()); + $res = ['code' => $e->getCode(), 'msg' => $e->getMessage(), 'data' => '', 'cmd' => '']; + } catch (\Error $er) { + Log::record('WebSocket请求异常,异常信息' . $er->getMessage()); + Log::record('WebSocket请求异常,异常信息' . $er->getFile().$er->getLine()); + $res = ['code' => $er->getCode(), 'msg' => $er->getMessage(), 'data' => '', 'cmd' => '']; + } catch (\Exception $era) { + Log::record('WebSocket请求异常,异常信息' . $era->getMessage()); + Log::record('WebSocket请求异常,异常信息' . $era->getFile().$era->getLine()); + $res = ['code' => $era->getCode(), 'msg' => $era->getMessage(), 'data' => '', 'cmd' => '']; + } catch (\ErrorException $ere) { + Log::record('WebSocket请求异常,异常信息' . $ere->getMessage()); + Log::record('WebSocket请求异常,异常信息' . $ere->getFile().$ere->getLine()); + $res = ['code' => $ere->getCode(), 'msg' => $ere->getMessage(), 'data' => '', 'cmd' => '']; + } + if(isset($res)){ + $server->push($frame->fd, json_encode($res)); + } + + } + + public function onRequest($request, $response) + { + $response->end("

Hello Swoole. #" . rand(1000, 9999) . "

"); + } + + public function onClose($server, $fd) + { + Log::record('WebSocket关闭请求开始,请求信息[' . json_encode($server) . ']'); + $resut = Event::disconnect($fd,$server); + echo "client {$fd} closed\n"; + } + + +} diff --git a/application/tags.php b/application/tags.php new file mode 100644 index 0000000..4b18d10 --- /dev/null +++ b/application/tags.php @@ -0,0 +1,28 @@ + +// +---------------------------------------------------------------------- + +// 应用行为扩展定义文件 +return [ + // 应用初始化 + 'app_init' => [], + // 应用开始 + 'app_begin' => [], + // 模块初始化 + 'module_init' => [], + // 操作开始执行 + 'action_begin' => [], + // 视图内容过滤 + 'view_filter' => [], + // 日志写入 + 'log_write' => [], + // 应用结束 + 'app_end' => [], +];