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客服登陆
+
+
+
+
+
+
+
+
+
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' => [],
+];