diff --git a/app/Builders/ImMessageList.php b/app/Builders/ImMessageList.php new file mode 100644 index 00000000..f71b7fc1 --- /dev/null +++ b/app/Builders/ImMessageList.php @@ -0,0 +1,41 @@ +getUsers($messages); + + foreach ($messages as $key => $message) { + $messages[$key]['user'] = $users[$message['user_id']] ?? new \stdClass(); + } + + return $messages; + } + + public function getUsers(array $messages) + { + $ids = kg_array_column($messages, 'user_id'); + + $userRepo = new UserRepo(); + + $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']); + + $baseUrl = kg_ci_base_url(); + + $result = []; + + foreach ($users->toArray() as $user) { + $user['avatar'] = $baseUrl . $user['avatar']; + $result[$user['id']] = $user; + } + + return $result; + } + +} diff --git a/app/Caches/ImChatGroup.php b/app/Caches/ImChatGroup.php new file mode 100644 index 00000000..780b4ed4 --- /dev/null +++ b/app/Caches/ImChatGroup.php @@ -0,0 +1,31 @@ +lifetime; + } + + public function getKey($id = null) + { + return "im_chat_group:{$id}"; + } + + public function getContent($id = null) + { + $groupRepo = new ImChatGroupRepo(); + + $group = $groupRepo->findById($id); + + return $group ?: null; + } + +} diff --git a/app/Caches/MaxImChatGroupId.php b/app/Caches/MaxImChatGroupId.php new file mode 100644 index 00000000..9502f7d9 --- /dev/null +++ b/app/Caches/MaxImChatGroupId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_im_chat_group_id'; + } + + public function getContent($id = null) + { + $group = ImChatGroupModel::findFirst(['order' => 'id DESC']); + + return $group->id ?? 0; + } + +} diff --git a/app/Http/Web/Controllers/LayerController.php b/app/Http/Web/Controllers/LayerController.php new file mode 100644 index 00000000..e0568fd9 --- /dev/null +++ b/app/Http/Web/Controllers/LayerController.php @@ -0,0 +1,50 @@ +isNotSafeRequest()) { + $this->checkHttpReferer(); + $this->checkCsrfToken(); + } + + $this->checkRateLimit(); + + return true; + } + + public function initialize() + { + $this->authUser = $this->getAuthUser(); + + $this->view->setVar('auth_user', $this->authUser); + } + + protected function getAuthUser() + { + /** + * @var WebAuth $auth + */ + $auth = $this->getDI()->get('auth'); + + return $auth->getCurrentUser(); + } + +} diff --git a/app/Http/Web/Controllers/MessengerController.php b/app/Http/Web/Controllers/MessengerController.php index 59a63ddd..f7a69217 100644 --- a/app/Http/Web/Controllers/MessengerController.php +++ b/app/Http/Web/Controllers/MessengerController.php @@ -4,11 +4,12 @@ namespace App\Http\Web\Controllers; use App\Http\Web\Services\Messenger as MessengerService; use App\Traits\Response as ResponseTrait; +use Phalcon\Mvc\View; /** * @RoutePrefix("/im") */ -class MessengerController extends \Phalcon\Mvc\Controller +class MessengerController extends LayerController { use ResponseTrait; @@ -30,24 +31,11 @@ class MessengerController extends \Phalcon\Mvc\Controller */ public function groupMembersAction() { - $data = [ - 'list' => [ - [ - 'id' => '1000', - 'username' => '闲心', - 'sign' => '我是如此的不寒而栗', - 'status' => 'online', - ], - [ - 'id' => '1001', - 'username' => '妹儿美', - 'sign' => '我是如此的不寒而栗', - 'status' => 'online', - ] - ] - ]; + $service = new MessengerService(); - return $this->jsonSuccess(['data' => $data]); + $list = $service->getGroupUsers(); + + return $this->jsonSuccess(['data' => ['list' => $list]]); } /** @@ -63,7 +51,25 @@ class MessengerController extends \Phalcon\Mvc\Controller */ public function chatLogAction() { + $service = new MessengerService(); + $pager = $service->getChatLog(); + + $this->view->setRenderLevel(View::LEVEL_ACTION_VIEW); + $this->view->pick('messenger/chat_log'); + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/chat/history", name="im.chat_history") + */ + public function chatHistoryAction() + { + $service = new MessengerService(); + + $pager = $service->getChatLog(); + + return $this->jsonPaginate($pager); } /** diff --git a/app/Http/Web/Services/Messenger.php b/app/Http/Web/Services/Messenger.php index 41210f00..47f9c281 100644 --- a/app/Http/Web/Services/Messenger.php +++ b/app/Http/Web/Services/Messenger.php @@ -2,15 +2,20 @@ namespace App\Http\Web\Services; +use App\Builders\ImMessageList as ImMessageListBuilder; +use App\Library\Paginator\Query as PagerQuery; +use App\Models\ImFriendMessage as ImFriendMessageModel; +use App\Repos\ImChatGroup as ImChatGroupRepo; +use App\Repos\ImFriendMessage as ImFriendMessageRepo; +use App\Repos\ImGroupMessage as ImGroupMessageRepo; use App\Repos\User as UserRepo; -use App\Services\Frontend\UserTrait; +use App\Validators\ImChatGroup as ImChatGroupValidator; +use App\Validators\ImMessage as ImMessageValidator; use GatewayClient\Gateway; class Messenger extends Service { - use UserTrait; - public function init() { $user = $this->getLoginUser(); @@ -34,6 +39,77 @@ class Messenger extends Service ]; } + public function getGroupUsers() + { + $id = $this->request->getQuery('id'); + + $validator = new ImChatGroupValidator(); + + $group = $validator->checkGroupCache($id); + + $groupRepo = new ImChatGroupRepo(); + + $users = $groupRepo->findGroupUsers($group->id); + + if ($users->count() == 0) { + return []; + } + + $baseUrl = kg_ci_base_url(); + + $result = []; + + foreach ($users->toArray() as $user) { + $user['avatar'] = $baseUrl . $user['avatar']; + $result[] = [ + 'id' => $user['id'], + 'username' => $user['name'], + 'avatar' => $user['avatar'], + 'sign' => $user['sign'], + ]; + } + + return $result; + } + + public function getChatLog() + { + $user = $this->getLoginUser(); + + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $validator = new ImMessageValidator(); + + $validator->checkType($params['type']); + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + if ($params['type'] == 'friend') { + + $params['chat_id'] = ImFriendMessageModel::getChatId($user->id, $params['id']); + + $messageRepo = new ImFriendMessageRepo(); + + $pager = $messageRepo->paginate($params, $sort, $page, $limit); + + return $this->handleChatLog($pager); + + } elseif ($params['type'] == 'group') { + + $params['group_id'] = $params['id']; + + $messageRepo = new ImGroupMessageRepo(); + + $pager = $messageRepo->paginate($params, $sort, $page, $limit); + + return $this->handleChatLog($pager); + } + } + public function bindUser() { $user = $this->getLoginUser(); @@ -54,6 +130,13 @@ class Messenger extends Service } } + /** + * @todo 发送未读消息 + */ + + /** + * @todo 发送盒子消息 + */ } public function sendMessage() @@ -74,6 +157,10 @@ class Messenger extends Service 'mine' => false, ]; + if ($to['type'] == 'group') { + $content['id'] = $to['id']; + } + $message = json_encode([ 'type' => 'show_message', 'content' => $content, @@ -83,12 +170,20 @@ class Messenger extends Service if ($to['type'] == 'friend') { - Gateway::sendToUid($to['id'], $message); + /** + * 不推送自己给自己发送的消息 + */ + if ($user->id != $to['id']) { + Gateway::sendToUid($to['id'], $message); + } } elseif ($to['type'] == 'group') { $excludeClientId = null; + /** + * 不推送自己在群组中发的消息 + */ if ($user->id == $from['id']) { $excludeClientId = Gateway::getClientIdByUid($user->id); } @@ -176,6 +271,38 @@ class Messenger extends Service return $result; } + protected function handleChatLog($pager) + { + if ($pager->total_items == 0) { + return $pager; + } + + $messages = $pager->items->toArray(); + + $builder = new ImMessageListBuilder(); + + $users = $builder->getUsers($messages); + + $items = []; + + foreach ($messages as $message) { + + $user = $user = $users[$message['user_id']] ?? new \stdClass(); + + $items[] = [ + 'id' => $message['id'], + 'content' => $message['content'], + 'create_time' => $message['create_time'], + 'timestamp' => $message['create_time'] * 1000, + 'user' => $user, + ]; + } + + $pager->items = $items; + + return $pager; + } + protected function getGroupName($groupId) { return "group_{$groupId}"; diff --git a/app/Http/Web/Views/messenger/chat_log.volt b/app/Http/Web/Views/messenger/chat_log.volt new file mode 100644 index 00000000..151c5bf7 --- /dev/null +++ b/app/Http/Web/Views/messenger/chat_log.volt @@ -0,0 +1,91 @@ + + + + + + 聊天记录 + + + + + +
+ +
+ +
+ + + + + + + + + diff --git a/app/Http/Web/Views/templates/layer.volt b/app/Http/Web/Views/templates/layer.volt index 87bcc7a5..95dd51e5 100644 --- a/app/Http/Web/Views/templates/layer.volt +++ b/app/Http/Web/Views/templates/layer.volt @@ -3,19 +3,16 @@ - - - {{ site_seo.getTitle() }} {{ icon_link('favicon.ico') }} {{ css_link('lib/layui/css/layui.css') }} {{ css_link('web/css/common.css') }} {% block link_css %}{% endblock %} {% block inline_css %}{% endblock %} - + {% block content %}{% endblock %} -{{ js_include('lib/layui/layui.all.js') }} +{{ js_include('lib/layui/layui.js') }} {{ js_include('web/js/common.js') }} {% block include_js %}{% endblock %} {% block inline_js %}{% endblock %} diff --git a/app/Repos/ImGroup.php b/app/Repos/ImChatGroup.php similarity index 86% rename from app/Repos/ImGroup.php rename to app/Repos/ImChatGroup.php index 27a98105..3dd954d3 100644 --- a/app/Repos/ImGroup.php +++ b/app/Repos/ImChatGroup.php @@ -3,21 +3,21 @@ namespace App\Repos; use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder; -use App\Models\ImChatGroup as ImGroupModel; +use App\Models\ImChatGroup as ImChatGroupModel; use App\Models\ImChatGroupUser as ImGroupUserModel; use App\Models\User as UserModel; use Phalcon\Mvc\Model; use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\ResultsetInterface; -class ImGroup extends Repository +class ImChatGroup extends Repository { public function paginate($where = [], $sort = 'latest', $page = 1, $limit = 15) { $builder = $this->modelsManager->createBuilder(); - $builder->from(ImGroupModel::class); + $builder->from(ImChatGroupModel::class); $builder->where('1 = 1'); @@ -52,21 +52,21 @@ class ImGroup extends Repository /** * @param int $id - * @return ImGroupModel|Model|bool + * @return ImChatGroupModel|Model|bool */ public function findById($id) { - return ImGroupModel::findFirst($id); + return ImChatGroupModel::findFirst($id); } /** * @param array $ids * @param string|array $columns - * @return ResultsetInterface|Resultset|ImGroupModel[] + * @return ResultsetInterface|Resultset|ImChatGroupModel[] */ public function findByIds($ids, $columns = '*') { - return ImGroupModel::query() + return ImChatGroupModel::query() ->columns($columns) ->inWhere('id', $ids) ->execute(); diff --git a/app/Repos/ImFriendGroup.php b/app/Repos/ImFriendGroup.php new file mode 100644 index 00000000..e68ece3e --- /dev/null +++ b/app/Repos/ImFriendGroup.php @@ -0,0 +1,90 @@ +modelsManager->createBuilder(); + + $builder->from(ImFriendGroupModel::class); + + $builder->where('1 = 1'); + + if (!empty($where['user_id'])) { + $builder->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]); + } + + if (!empty($where['name'])) { + $builder->andWhere('name LIKE :name:', ['name' => "%{$where['name']}%"]); + } + + if (isset($where['deleted'])) { + $builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]); + } + + switch ($sort) { + default: + $orderBy = 'id DESC'; + break; + } + + $builder->orderBy($orderBy); + + $pager = new PagerQueryBuilder([ + 'builder' => $builder, + 'page' => $page, + 'limit' => $limit, + ]); + + return $pager->paginate(); + } + + /** + * @param int $id + * @return ImFriendGroupModel|Model|bool + */ + public function findById($id) + { + return ImFriendGroupModel::findFirst($id); + } + + /** + * @param array $ids + * @param string|array $columns + * @return ResultsetInterface|Resultset|ImFriendGroupModel[] + */ + public function findByIds($ids, $columns = '*') + { + return ImFriendGroupModel::query() + ->columns($columns) + ->inWhere('id', $ids) + ->execute(); + } + + /** + * @param int $groupId + * @return ResultsetInterface|Resultset|UserModel[] + */ + public function findGroupUsers($groupId) + { + return $this->modelsManager->createBuilder() + ->columns('u.*') + ->addFrom(UserModel::class, 'u') + ->join(ImFriendModel::class, 'u.id = f.user_id', 'f') + ->where('f.group_id = :group_id:', ['group_id' => $groupId]) + ->andWhere('u.deleted = 0') + ->getQuery()->execute(); + } + +} diff --git a/app/Repos/ImFriendMessage.php b/app/Repos/ImFriendMessage.php new file mode 100644 index 00000000..878241b5 --- /dev/null +++ b/app/Repos/ImFriendMessage.php @@ -0,0 +1,76 @@ +modelsManager->createBuilder(); + + $builder->from(ImFriendMessageModel::class); + + $builder->where('1 = 1'); + + if (!empty($where['chat_id'])) { + $builder->andWhere('chat_id = :chat_id:', ['chat_id' => $where['chat_id']]); + } + + if (!empty($where['user_id'])) { + $builder->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]); + } + + if (isset($where['deleted'])) { + $builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]); + } + + switch ($sort) { + case 'oldest': + $orderBy = 'id ASC'; + break; + default: + $orderBy = 'id DESC'; + break; + } + + $builder->orderBy($orderBy); + + $pager = new PagerQueryBuilder([ + 'builder' => $builder, + 'page' => $page, + 'limit' => $limit, + ]); + + return $pager->paginate(); + } + + /** + * @param int $id + * @return ImFriendMessageModel|Model|bool + */ + public function findById($id) + { + return ImFriendMessageModel::findFirst($id); + } + + /** + * @param array $ids + * @param string|array $columns + * @return ResultsetInterface|Resultset|ImFriendMessageModel[] + */ + public function findByIds($ids, $columns = '*') + { + return ImFriendMessageModel::query() + ->columns($columns) + ->inWhere('id', $ids) + ->execute(); + } + +} diff --git a/app/Repos/ImGroupMessage.php b/app/Repos/ImGroupMessage.php new file mode 100644 index 00000000..c23179fc --- /dev/null +++ b/app/Repos/ImGroupMessage.php @@ -0,0 +1,76 @@ +modelsManager->createBuilder(); + + $builder->from(ImGroupMessageModel::class); + + $builder->where('1 = 1'); + + if (!empty($where['group_id'])) { + $builder->andWhere('group_id = :group_id:', ['group_id' => $where['group_id']]); + } + + if (!empty($where['user_id'])) { + $builder->andWhere('user_id = :user_id:', ['user_id' => $where['user_id']]); + } + + if (isset($where['deleted'])) { + $builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]); + } + + switch ($sort) { + case 'oldest': + $orderBy = 'id ASC'; + break; + default: + $orderBy = 'id DESC'; + break; + } + + $builder->orderBy($orderBy); + + $pager = new PagerQueryBuilder([ + 'builder' => $builder, + 'page' => $page, + 'limit' => $limit, + ]); + + return $pager->paginate(); + } + + /** + * @param int $id + * @return ImGroupMessageModel|Model|bool + */ + public function findById($id) + { + return ImGroupMessageModel::findFirst($id); + } + + /** + * @param array $ids + * @param string|array $columns + * @return ResultsetInterface|Resultset|ImGroupMessageModel[] + */ + public function findByIds($ids, $columns = '*') + { + return ImGroupMessageModel::query() + ->columns($columns) + ->inWhere('id', $ids) + ->execute(); + } + +} diff --git a/app/Validators/ImChatGroup.php b/app/Validators/ImChatGroup.php new file mode 100644 index 00000000..58e294ab --- /dev/null +++ b/app/Validators/ImChatGroup.php @@ -0,0 +1,88 @@ +get(); + + /** + * 防止缓存穿透 + */ + if ($id < 1 || $id > $maxGroupId) { + throw new BadRequestException('im_chat_group.not_found'); + } + + $groupCache = new ImChatGroupCache(); + + $group = $groupCache->get($id); + + if (!$group) { + throw new BadRequestException('im_chat_group.not_found'); + } + + return $group; + } + + public function checkGroup($id) + { + $groupRepo = new ImChatGroupRepo(); + + $group = $groupRepo->findById($id); + + if (!$group) { + throw new BadRequestException('im_chat_group.not_found'); + } + + return $group; + } + + public function checkName($name) + { + $value = $this->filter->sanitize($name, ['trim', 'string']); + + $length = kg_strlen($value); + + if ($length < 2) { + throw new BadRequestException('im_chat_group.name_too_short'); + } + + if ($length > 50) { + throw new BadRequestException('im_chat_group.name_too_long'); + } + + return $value; + } + + public function checkAbout($name) + { + $value = $this->filter->sanitize($name, ['trim', 'string']); + + $length = kg_strlen($value); + + if ($length > 255) { + throw new BadRequestException('im_chat_group.about_too_long'); + } + + return $value; + } + +} diff --git a/app/Validators/ImFriendGroup.php b/app/Validators/ImFriendGroup.php new file mode 100644 index 00000000..806b48c0 --- /dev/null +++ b/app/Validators/ImFriendGroup.php @@ -0,0 +1,41 @@ +findById($id); + + if (!$group) { + throw new BadRequestException('im_friend_group.not_found'); + } + + return $group; + } + + public function checkName($name) + { + $value = $this->filter->sanitize($name, ['trim', 'string']); + + $length = kg_strlen($value); + + if ($length < 2) { + throw new BadRequestException('im_friend_group.name_too_short'); + } + + if ($length > 15) { + throw new BadRequestException('im_friend_group.name_too_long'); + } + + return $value; + } + +} diff --git a/app/Validators/ImMessage.php b/app/Validators/ImMessage.php new file mode 100644 index 00000000..2e24cdfa --- /dev/null +++ b/app/Validators/ImMessage.php @@ -0,0 +1,64 @@ +findById($id); + + if (!$message) { + throw new BadRequestException('im_message.not_found'); + } + + return $message; + } + + public function checkGroupMessage($id) + { + $messageRepo = new ImGroupMessageRepo(); + + $message = $messageRepo->findById($id); + + if (!$message) { + throw new BadRequestException('im_message.not_found'); + } + + return $message; + } + + public function checkType($type) + { + if (!in_array($type, ['friend', 'group'])) { + throw new BadRequestException('im_message.invalid_type'); + } + + return $type; + } + + public function checkContent($content) + { + $value = $this->filter->sanitize($content, ['trim', 'string']); + + $length = kg_strlen($value); + + if ($length < 1) { + throw new BadRequestException('im_message.content_too_short'); + } + + if ($length > 1000) { + throw new BadRequestException('im_message.content_too_long'); + } + + return $value; + } + +} diff --git a/public/static/web/css/common.css b/public/static/web/css/common.css index f2a54b36..cc6bcb3f 100644 --- a/public/static/web/css/common.css +++ b/public/static/web/css/common.css @@ -1281,4 +1281,14 @@ .order-item span { margin-right: 8px; +} + +.layer .layim-chat-main { + height: auto; +} + +.layim-chat-user img { + width: 40px; + height: 40px; + border-radius: 100%; } \ No newline at end of file