diff --git a/app/Console/Tasks/CleanLogTask.php b/app/Console/Tasks/CleanLogTask.php index e198d702..581d6163 100644 --- a/app/Console/Tasks/CleanLogTask.php +++ b/app/Console/Tasks/CleanLogTask.php @@ -26,6 +26,7 @@ class CleanLogTask extends Task $this->cleanOrderLog(); $this->cleanRefundLog(); $this->cleanPointLog(); + $this->cleanDingTalkLog(); $this->cleanNoticeLog(); $this->cleanOtherLog(); } @@ -234,6 +235,18 @@ class CleanLogTask extends Task $this->whitelist[] = $type; } + /** + * 清理钉钉日志 + */ + protected function cleanDingTalkLog() + { + $type = 'dingtalk'; + + $this->cleanLog($type, 7); + + $this->whitelist[] = $type; + } + /** * 清理通知日志 */ diff --git a/app/Console/Tasks/LiveTeacherNoticeTask.php b/app/Console/Tasks/LiveTeacherNoticeTask.php new file mode 100644 index 00000000..49e25ed7 --- /dev/null +++ b/app/Console/Tasks/LiveTeacherNoticeTask.php @@ -0,0 +1,81 @@ +findLives(); + + if ($lives->count() == 0) return; + + $redis = $this->getRedis(); + + $keyName = $this->getCacheKeyName(); + + foreach ($lives as $live) { + $redis->sAdd($keyName, $live->chapter_id); + } + + $redis->expire($keyName, 86400); + } + + /** + * 消费讲师提醒 + */ + public function consumeAction() + { + $redis = $this->getRedis(); + + $keyName = $this->getCacheKeyName(); + + $chapterIds = $redis->sMembers($keyName); + + if (count($chapterIds) == 0) return; + + $chapterRepo = new ChapterRepo(); + + $notice = new LiveTeacherNotice(); + + foreach ($chapterIds as $chapterId) { + + $live = $chapterRepo->findChapterLive($chapterId); + + if ($live->start_time - time() < 30 * 60) { + + $notice->handle($live); + + $redis->sRem($keyName, $chapterId); + } + } + } + + /** + * @return ResultsetInterface|Resultset|ChapterLiveModel[] + */ + protected function findLives() + { + $today = strtotime(date('Ymd')); + + return ChapterLiveModel::query() + ->betweenWhere('start_time', $today, $today + 86400) + ->execute(); + } + + protected function getCacheKeyName() + { + return 'live_teacher_notice'; + } + +} \ No newline at end of file diff --git a/app/Console/Tasks/MonitorTask.php b/app/Console/Tasks/MonitorTask.php new file mode 100644 index 00000000..89481454 --- /dev/null +++ b/app/Console/Tasks/MonitorTask.php @@ -0,0 +1,180 @@ + $this->checkCPU(), + 'disk' => $this->checkDisk(), + 'mysql' => $this->checkMysql(), + 'redis' => $this->checkRedis(), + 'xunsearch' => $this->checkXunSearch(), + 'websocket' => $this->checkWebSocket(), + ]; + + foreach ($items as $key => $value) { + if (empty($value)) { + unset($items[$key]); + } + } + + if (empty($items)) return; + + $notice = new DingTalkNotice(); + + $content = implode("\n", $items); + + $notice->atTechSupport($content); + } + + protected function checkCPU() + { + $coreCount = $this->getCpuCoreCount(); + + $cpu = ServerInfo::cpu(); + + if ($cpu[1] > $coreCount / 2) { + return sprintf("cpu负载超过%s", $cpu[1]); + } + } + + protected function checkDisk() + { + $disk = ServerInfo::disk(); + + if ($disk['percent'] > 80) { + return sprintf("disk空间超过%s%%", $disk['percent']); + } + } + + protected function checkMysql() + { + try { + + $benchmark = new Benchmark(); + + $benchmark->start(); + + $user = UserModel::findFirst(); + + $benchmark->stop(); + + $elapsedTime = $benchmark->getElapsedTime(); + + if ($user === false) { + return sprintf("mysql查询失败"); + } + + if ($elapsedTime > 1) { + return sprintf("mysql查询响应超过%s秒", round($elapsedTime, 2)); + } + + } catch (\Exception $e) { + return sprintf("mysql可能存在异常"); + } + } + + protected function checkRedis() + { + try { + + $benchmark = new Benchmark(); + + $benchmark->start(); + + $site = $this->getSettings('site'); + + $benchmark->stop(); + + $elapsedTime = $benchmark->getElapsedTime(); + + if (empty($site)) { + return sprintf("redis查询失败"); + } + + if ($elapsedTime > 1) { + return sprintf("redis查询响应超过%s秒", round($elapsedTime, 2)); + } + + } catch (\Exception $e) { + return sprintf("redis可能存在异常"); + } + } + + protected function checkXunSearch() + { + try { + + $benchmark = new Benchmark(); + + $benchmark->start(); + + $searcher = new UserSearcher(); + + $user = $searcher->search('id:10000'); + + $benchmark->stop(); + + $elapsedTime = $benchmark->getElapsedTime(); + + if (empty($user)) { + return sprintf("xunsearch搜索失败"); + } + + if ($elapsedTime > 1) { + return sprintf("xunsearch搜索响应超过%s秒", round($elapsedTime, 2)); + } + + } catch (\Exception $e) { + return sprintf("xunsearch可能存在异常"); + } + } + + protected function checkWebSocket() + { + try { + + $benchmark = new Benchmark(); + + $config = $this->getConfig(); + + Gateway::$registerAddress = $config->path('websocket.register_address'); + + $benchmark->start(); + + Gateway::isUidOnline(10000); + + $benchmark->stop(); + + $elapsedTime = $benchmark->getElapsedTime(); + + if ($elapsedTime > 1) { + return sprintf("websocket响应超过%s秒", round($elapsedTime, 2)); + } + + } catch (\Exception $e) { + return sprintf("websocket可能存在异常"); + } + } + + protected function getCpuCoreCount() + { + $cpuInfo = file_get_contents('/proc/cpuinfo'); + + preg_match_all('/^processor/m', $cpuInfo, $matches); + + return count($matches[0]); + } + +} \ No newline at end of file diff --git a/app/Http/Admin/Controllers/SettingController.php b/app/Http/Admin/Controllers/SettingController.php index 76cff636..c7734972 100644 --- a/app/Http/Admin/Controllers/SettingController.php +++ b/app/Http/Admin/Controllers/SettingController.php @@ -378,4 +378,30 @@ class SettingController extends Controller } } + /** + * @Route("/dingtalk/robot", name="admin.setting.dingtalk_robot") + */ + public function dingtalkRobotAction() + { + $section = 'dingtalk.robot'; + + $settingService = new SettingService(); + + if ($this->request->isPost()) { + + $data = $this->request->getPost(); + + $settingService->updatePointSettings($section, $data); + + return $this->jsonSuccess(['msg' => '更新配置成功']); + + } else { + + $robot = $settingService->getSettings($section); + + $this->view->pick('setting/dingtalk_robot'); + $this->view->setVar('robot', $robot); + } + } + } diff --git a/app/Http/Admin/Controllers/TestController.php b/app/Http/Admin/Controllers/TestController.php index 4db114d2..d159935d 100644 --- a/app/Http/Admin/Controllers/TestController.php +++ b/app/Http/Admin/Controllers/TestController.php @@ -6,6 +6,7 @@ use App\Http\Admin\Services\AlipayTest as AlipayTestService; use App\Http\Admin\Services\Setting as SettingService; use App\Http\Admin\Services\WxpayTest as WxpayTestService; use App\Services\Captcha as CaptchaService; +use App\Services\DingTalkNotice as DingTalkNoticeService; use App\Services\Live as LiveService; use App\Services\Mail\Test as TestMailService; use App\Services\MyStorage as StorageService; @@ -250,4 +251,20 @@ class TestController extends Controller return $this->jsonSuccess(['status' => $status]); } + /** + * @Post("/dingtalk/robot", name="admin.test.dingtalk_robot") + */ + public function dingTalkRobotAction() + { + $noticeService = new DingTalkNoticeService(); + + $result = $noticeService->test(); + + if ($result) { + return $this->jsonSuccess(['msg' => '发送消息成功,请到钉钉确认']); + } else { + return $this->jsonError(['msg' => '发送消息失败,请检查配置']); + } + } + } \ No newline at end of file diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 20dedbb5..374aa629 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -800,6 +800,12 @@ class AuthNode extends Service 'type' => 'menu', 'route' => 'admin.setting.wechat_oa', ], + [ + 'id' => '5-1-15', + 'title' => '钉钉机器人', + 'type' => 'menu', + 'route' => 'admin.setting.dingtalk_robot', + ], [ 'id' => '5-1-14', 'title' => '积分设置', diff --git a/app/Http/Admin/Views/setting/dingtalk_robot.volt b/app/Http/Admin/Views/setting/dingtalk_robot.volt new file mode 100644 index 00000000..f243c6c0 --- /dev/null +++ b/app/Http/Admin/Views/setting/dingtalk_robot.volt @@ -0,0 +1,55 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + +
+
+ 钉钉机器人 +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+
+ 通知测试 +
+
+ +
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/Library/Benchmark.php b/app/Library/Benchmark.php new file mode 100644 index 00000000..e6943787 --- /dev/null +++ b/app/Library/Benchmark.php @@ -0,0 +1,27 @@ +startTime = microtime(true); + } + + public function stop() + { + $this->endTime = microtime(true); + } + + public function getElapsedTime() + { + return $this->endTime - $this->startTime; + } + +} \ No newline at end of file diff --git a/app/Library/Helper.php b/app/Library/Helper.php index af1d528a..47363e5c 100644 --- a/app/Library/Helper.php +++ b/app/Library/Helper.php @@ -35,6 +35,24 @@ function kg_substr($str, $start, $length, $suffix = '...') return $str == $result ? $str : $result . $suffix; } +/** + * 占位替换 + * + * @param string $str + * @param array $data + * @return string + */ +function kg_ph_replace($str, $data = []) +{ + if (empty($data)) return $str; + + foreach ($data as $key => $value) { + $str = str_replace('{' . $key . '}', $value, $str); + } + + return $str; +} + /** * uniqid封装 * diff --git a/app/Models/Task.php b/app/Models/Task.php index 58fb19fc..ad9f6953 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -14,10 +14,11 @@ class Task extends Model const TYPE_LUCKY_GIFT_DELIVER = 4; // 抽奖礼品派发 const TYPE_NOTICE_ACCOUNT_LOGIN = 11; // 帐号登录通知 - const TYPE_NOTICE_LIVE_BEGIN = 12; // 直播开始通知 + const TYPE_NOTICE_LIVE_BEGIN = 12; // 直播学员通知 const TYPE_NOTICE_ORDER_FINISH = 13; // 订单完成通知 const TYPE_NOTICE_REFUND_FINISH = 14; // 退款完成通知 const TYPE_NOTICE_CONSULT_REPLY = 15; // 咨询回复通知 + const TYPE_NOTICE_LIVE_TEACHER = 16; // 直播讲师通知 /** * 优先级 diff --git a/app/Services/DingTalk/Notice/ConsultCreate.php b/app/Services/DingTalk/Notice/ConsultCreate.php new file mode 100644 index 00000000..d88d3b24 --- /dev/null +++ b/app/Services/DingTalk/Notice/ConsultCreate.php @@ -0,0 +1,32 @@ +findById($consult->owner_id); + + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($consult->course_id); + + $content = kg_ph_replace("{user.name} 对课程:{course.title} 发起了咨询:\n{consult.question}", [ + 'user.name' => $user->name, + 'course.title' => $course->title, + 'consult.question' => $consult->question, + ]); + + $this->atCourseTeacher($course->id, $content); + } + +} \ No newline at end of file diff --git a/app/Services/DingTalk/Notice/CustomService.php b/app/Services/DingTalk/Notice/CustomService.php new file mode 100644 index 00000000..78b870ff --- /dev/null +++ b/app/Services/DingTalk/Notice/CustomService.php @@ -0,0 +1,36 @@ +sender_id}"; + + $cache = $this->getCache(); + + $content = $cache->get($keyName); + + if ($content) return; + + $cache->save($keyName, 1, 3600); + + $userRepo = new UserRepo(); + + $sender = $userRepo->findById($message->sender_id); + + $content = kg_ph_replace("{user} 通过在线客服给你发送了消息:{message}", [ + 'user' => $sender->name, + 'message' => $message->content, + ]); + + $this->atCustomService($content); + } + +} \ No newline at end of file diff --git a/app/Services/DingTalk/Notice/LiveTeacher.php b/app/Services/DingTalk/Notice/LiveTeacher.php new file mode 100644 index 00000000..1e4ce7f8 --- /dev/null +++ b/app/Services/DingTalk/Notice/LiveTeacher.php @@ -0,0 +1,26 @@ +findById($live->course_id); + + $content = kg_ph_replace("课程:{course.title} 计划于 {live.start_time} 开播,不要错过直播时间哦!", [ + 'course.title' => $course->title, + 'live.start_time' => date('Y-m-d H:i', $live->start_time), + ]); + + $this->atCourseTeacher($course->id, $content); + } + +} \ No newline at end of file diff --git a/app/Services/DingTalk/Notice/OrderFinish.php b/app/Services/DingTalk/Notice/OrderFinish.php new file mode 100644 index 00000000..cc360b57 --- /dev/null +++ b/app/Services/DingTalk/Notice/OrderFinish.php @@ -0,0 +1,27 @@ +findById($order->owner_id); + + $text = kg_ph_replace("开单啦,{user.name} 同学完成了订单!\n订单名称:{order.subject}\n订单金额:¥{order.amount}", [ + 'user.name' => $user->name, + 'order.subject' => $order->subject, + 'order.amount' => $order->amount, + ]); + + $this->send(['text' => $text]); + } + +} \ No newline at end of file diff --git a/app/Services/DingTalkNotice.php b/app/Services/DingTalkNotice.php new file mode 100644 index 00000000..fad0c1f0 --- /dev/null +++ b/app/Services/DingTalkNotice.php @@ -0,0 +1,214 @@ +settings = $this->getSettings('dingtalk.robot'); + + $this->logger = $this->getLogger('dingtalk'); + } + + /** + * 测试消息 + * + * @return bool + */ + public function test() + { + $params = [ + 'msgtype' => 'text', + 'text' => ['content' => '我是一条测试消息啦!'], + ]; + + return $this->send($params); + } + + /** + * 给技术人员发消息 + * + * @param string $content + * @return bool + */ + public function atTechSupport($content) + { + $atMobiles = $this->parseAtMobiles($this->settings['ts_mobiles']); + $atContent = $this->buildAtContent($content, $atMobiles); + + $params = [ + 'msgtype' => 'text', + 'text' => ['content' => $atContent], + 'at' => ['atMobiles' => $atMobiles], + ]; + + return $this->send($params); + } + + /** + * 给客服人员发消息 + * + * @param string $content + * @return bool + */ + public function atCustomService($content) + { + $atMobiles = $this->parseAtMobiles($this->settings['cs_mobiles']); + $atContent = $this->buildAtContent($content, $atMobiles); + + $params = [ + 'msgtype' => 'text', + 'text' => ['content' => $atContent], + 'at' => ['atMobiles' => $atMobiles], + ]; + + return $this->send($params); + } + + /** + * 给课程讲师发消息 + * + * @param int $courseId + * @param string $content + * @return bool + */ + public function atCourseTeacher($courseId, $content) + { + $courseRepo = new CourseRepo(); + + $course = $courseRepo->findById($courseId); + + $accountRepo = new AccountRepo(); + + $account = $accountRepo->findById($course->teacher_id); + + if (empty($account->phone)) { + return false; + } + + $atMobiles = $account->phone; + + $atContent = $this->buildAtContent($content, $atMobiles); + + $params = [ + 'msgtype' => 'text', + 'text' => ['content' => $atContent], + 'at' => ['atMobiles' => $atMobiles], + ]; + + return $this->send($params); + } + + /** + * 发送消息 + * + * @param array $params + * @return bool + */ + public function send($params) + { + if (isset($params['msgtype'])) { + $params['msgtype'] = 'text'; + } + + $webHook = "https://oapi.dingtalk.com/robot/send?access_token=%s×tamp=%s&sign=%s"; + + $appSecret = $this->settings['app_secret']; + $appToken = $this->settings['app_token']; + + $timestamp = time() * 1000; + $data = sprintf("%s\n%s", $timestamp, $appSecret); + $sign = urlencode(base64_encode(hash_hmac('sha256', $data, $appSecret, true))); + $postUrl = sprintf($webHook, $appToken, $timestamp, $sign); + + try { + + $client = new HttpClient(); + + $response = $client->post($postUrl, ['json' => $params]); + + $content = $response->getBody()->getContents(); + + $content = json_decode($content, true); + + $this->logger->debug('Send Message Request ' . kg_json_encode($params)); + + $this->logger->debug('Send Message Response ' . kg_json_encode($content)); + + $result = $content['errcode'] == 0; + + if ($result == false) { + $this->logger->error('Send Message Failed ' . kg_json_encode($content)); + } + + } catch (\Exception $e) { + + $this->logger->error('Send Message Exception ' . kg_json_encode([ + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'message' => $e->getMessage(), + ])); + + $result = false; + } + + return $result; + } + + /** + * @param string $mobiles + * @return array + */ + protected function parseAtMobiles($mobiles) + { + if (empty($mobiles)) { + return []; + } + + $mobiles = explode(',', $mobiles); + + return array_map(function ($mobile) { + return trim($mobile); + }, $mobiles); + } + + /** + * @param string $content + * @param array $mobiles + * @return string + */ + protected function buildAtContent($content, $mobiles = []) + { + if (count($mobiles) == 0) { + return $content; + } + + $result = ''; + + foreach ($mobiles as $mobile) { + $result .= sprintf('@%s ', $mobile); + } + + $result .= $content; + + return $result; + } + +} \ No newline at end of file diff --git a/db/migrations/20210215024511_data_202102151130.php b/db/migrations/20210215024511_data_202102151130.php new file mode 100644 index 00000000..f5fd6419 --- /dev/null +++ b/db/migrations/20210215024511_data_202102151130.php @@ -0,0 +1,42 @@ + 'dingtalk.robot', + 'item_key' => 'app_secret', + 'item_value' => '', + ], + [ + 'section' => 'dingtalk.robot', + 'item_key' => 'app_token', + 'item_value' => '', + ], + [ + 'section' => 'dingtalk.robot', + 'item_key' => 'ts_mobiles', + 'item_value' => '', + ], + [ + 'section' => 'dingtalk.robot', + 'item_key' => 'cs_mobiles', + 'item_value' => '', + ], + ]; + + $this->table('kg_setting')->insert($rows)->save(); + } + + public function down() + { + $this->getQueryBuilder() + ->delete('kg_setting') + ->where(['section' => 'dingtalk.robot']) + ->execute(); + } + +} diff --git a/scheduler.php b/scheduler.php index 666bb7e4..000900f3 100644 --- a/scheduler.php +++ b/scheduler.php @@ -25,6 +25,9 @@ $scheduler->php($script, $bin, ['--task' => 'vod_event', '--action' => 'main']) $scheduler->php($script, $bin, ['--task' => 'close_trade', '--action' => 'main']) ->at('*/13 * * * *'); +$scheduler->php($script, $bin, ['--task' => 'monitor', '--action' => 'main']) + ->at('*/12 * * * *'); + $scheduler->php($script, $bin, ['--task' => 'point_gift_deliver', '--action' => 'main']) ->at('*/11 * * * *');