diff --git a/app/Builders/AnswerList.php b/app/Builders/AnswerList.php new file mode 100644 index 00000000..4633f7b6 --- /dev/null +++ b/app/Builders/AnswerList.php @@ -0,0 +1,70 @@ +getQuestions($answers); + + foreach ($answers as $key => $answer) { + $answers[$key]['question'] = $questions[$answer['question_id']] ?? new \stdClass(); + } + + return $answers; + } + + public function handleUsers(array $answers) + { + $users = $this->getUsers($answers); + + foreach ($answers as $key => $answer) { + $answers[$key]['owner'] = $users[$answer['owner_id']] ?? new \stdClass(); + } + + return $answers; + } + + public function getQuestions(array $answers) + { + $ids = kg_array_column($answers, 'question_id'); + + $questionRepo = new QuestionRepo(); + + $questions = $questionRepo->findByIds($ids, ['id', 'title']); + + $result = []; + + foreach ($questions->toArray() as $question) { + $result[$question['id']] = $question; + } + + return $result; + } + + public function getUsers(array $answers) + { + $ids = kg_array_column($answers, 'owner_id'); + + $userRepo = new UserRepo(); + + $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($users->toArray() as $user) { + $user['avatar'] = $baseUrl . $user['avatar']; + $result[$user['id']] = $user; + } + + return $result; + } + +} diff --git a/app/Builders/QuestionList.php b/app/Builders/QuestionList.php new file mode 100644 index 00000000..95299dfb --- /dev/null +++ b/app/Builders/QuestionList.php @@ -0,0 +1,53 @@ + $question) { + $questions[$key]['tags'] = json_decode($question['tags'], true); + } + + return $questions; + } + + public function handleUsers(array $questions) + { + $users = $this->getUsers($questions); + + foreach ($questions as $key => $question) { + $questions[$key]['owner'] = $users[$question['owner_id']] ?? new \stdClass(); + $questions[$key]['last_replier'] = $users[$question['last_replier_id']] ?? new \stdClass(); + } + + return $questions; + } + + public function getUsers($questions) + { + $ownerIds = kg_array_column($questions, 'owner_id'); + $lastReplierIds = kg_array_column($questions, 'last_replier_id'); + $ids = array_merge($ownerIds, $lastReplierIds); + + $userRepo = new UserRepo(); + + $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($users->toArray() as $user) { + $user['avatar'] = $baseUrl . $user['avatar']; + $result[$user['id']] = $user; + } + + return $result; + } + +} diff --git a/app/Builders/TagFollowList.php b/app/Builders/TagFollowList.php new file mode 100644 index 00000000..c4b83377 --- /dev/null +++ b/app/Builders/TagFollowList.php @@ -0,0 +1,75 @@ +getTags($relations); + + foreach ($relations as $key => $value) { + $relations[$key]['tag'] = $tags[$value['tag_id']] ?? new \stdClass(); + } + + return $relations; + } + + public function handleUsers(array $relations) + { + $users = $this->getUsers($relations); + + foreach ($relations as $key => $value) { + $relations[$key]['user'] = $users[$value['user_id']] ?? new \stdClass(); + } + + return $relations; + } + + public function getTags(array $relations) + { + $ids = kg_array_column($relations, 'tag_id'); + + $tagRepo = new TagRepo(); + + $columns = ['id', 'name', 'alias', 'icon', 'follow_count']; + + $tags = $tagRepo->findByIds($ids, $columns); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($tags->toArray() as $tag) { + $tag['icon'] = $baseUrl . $tag['icon']; + $result[$tag['id']] = $tag; + } + + return $result; + } + + public function getUsers(array $relations) + { + $ids = kg_array_column($relations, 'user_id'); + + $userRepo = new UserRepo(); + + $users = $userRepo->findByIds($ids, ['id', 'name', 'avatar']); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($users->toArray() as $user) { + $user['avatar'] = $baseUrl . $user['avatar']; + $result[$user['id']] = $user; + } + + return $result; + } + +} diff --git a/app/Caches/HotQuestionList.php b/app/Caches/HotQuestionList.php new file mode 100644 index 00000000..22d115af --- /dev/null +++ b/app/Caches/HotQuestionList.php @@ -0,0 +1,114 @@ +findWeeklyHotQuestions(); + + if ($questions->count() > 0) { + return $this->handleQuestions($questions); + } + + $questions = $this->findMonthlyHotQuestions(); + + if ($questions->count() > 0) { + return $this->handleQuestions($questions); + } + + $questions = $this->findYearlyHotQuestions(); + + if ($questions->count() > 0) { + return $this->handleQuestions($questions); + } + + return []; + } + + /** + * @param QuestionModel[] $questions + * @return array + */ + protected function handleQuestions($questions) + { + $result = []; + + foreach ($questions as $question) { + $result[] = [ + 'id' => $question->id, + 'title' => $question->title, + ]; + } + + return $result; + } + + /** + * @param int $limit + * @return ResultsetInterface|Resultset|QuestionModel[] + */ + protected function findWeeklyHotQuestions($limit = 10) + { + $createTime = strtotime('monday this week'); + + return $this->findHotQuestions($createTime, $limit); + } + + /** + * @param int $limit + * @return ResultsetInterface|Resultset|QuestionModel[] + */ + protected function findMonthlyHotQuestions($limit = 10) + { + $createTime = strtotime(date('Y-m-01')); + + return $this->findHotQuestions($createTime, $limit); + } + + /** + * @param int $limit + * @return ResultsetInterface|Resultset|QuestionModel[] + */ + protected function findYearlyHotQuestions($limit = 10) + { + $createTime = strtotime(date('Y-01-01')); + + return $this->findHotQuestions($createTime, $limit); + } + + /** + * @param int $createTime + * @param int $limit + * @return ResultsetInterface|Resultset|QuestionModel[] + */ + protected function findHotQuestions($createTime, $limit = 10) + { + return QuestionModel::query() + ->where('create_time > :create_time:', ['create_time' => $createTime]) + ->orderBy('score DESC') + ->limit($limit) + ->execute(); + } + +} diff --git a/app/Caches/MaxAnswerId.php b/app/Caches/MaxAnswerId.php new file mode 100644 index 00000000..02320473 --- /dev/null +++ b/app/Caches/MaxAnswerId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_answer_id'; + } + + public function getContent($id = null) + { + $answer = AnswerModel::findFirst(['order' => 'id DESC']); + + return $answer->id ?? 0; + } + +} diff --git a/app/Caches/MaxQuestionId.php b/app/Caches/MaxQuestionId.php new file mode 100644 index 00000000..71b92f58 --- /dev/null +++ b/app/Caches/MaxQuestionId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_question_id'; + } + + public function getContent($id = null) + { + $question = QuestionModel::findFirst(['order' => 'id DESC']); + + return $question->id ?? 0; + } + +} diff --git a/app/Caches/Question.php b/app/Caches/Question.php new file mode 100644 index 00000000..b536fe37 --- /dev/null +++ b/app/Caches/Question.php @@ -0,0 +1,31 @@ +lifetime; + } + + public function getKey($id = null) + { + return "question:{$id}"; + } + + public function getContent($id = null) + { + $questionRepo = new QuestionRepo(); + + $question = $questionRepo->findById($id); + + return $question ?: null; + } + +} diff --git a/app/Caches/ArticleRelatedList.php b/app/Caches/TaggedArticleList.php similarity index 75% rename from app/Caches/ArticleRelatedList.php rename to app/Caches/TaggedArticleList.php index 78c97f4b..722e484d 100644 --- a/app/Caches/ArticleRelatedList.php +++ b/app/Caches/TaggedArticleList.php @@ -5,11 +5,9 @@ namespace App\Caches; use App\Models\Article as ArticleModel; use App\Repos\Article as ArticleRepo; -class ArticleRelatedList extends Cache +class TaggedArticleList extends Cache { - protected $articleId; - protected $limit = 5; protected $lifetime = 1 * 86400; @@ -21,25 +19,15 @@ class ArticleRelatedList extends Cache public function getKey($id = null) { - return "article_related_list:{$id}"; + return "tagged_article_list:{$id}"; } public function getContent($id = null) { - $this->articleId = $id; - $articleRepo = new ArticleRepo(); - $article = $articleRepo->findById($id); - - if (empty($article->tags)) return []; - - $tagIds = kg_array_column($article->tags, 'id'); - - $randKey = array_rand($tagIds); - $where = [ - 'tag_id' => $tagIds[$randKey], + 'tag_id' => $id, 'published' => ArticleModel::PUBLISH_APPROVED, ]; @@ -61,7 +49,7 @@ class ArticleRelatedList extends Cache $count = 0; foreach ($articles as $article) { - if ($article->id != $this->articleId && $count < $this->limit) { + if ($count < $this->limit) { $result[] = [ 'id' => $article->id, 'title' => $article->title, diff --git a/app/Caches/TaggedQuestionList.php b/app/Caches/TaggedQuestionList.php new file mode 100644 index 00000000..e1bb7c2a --- /dev/null +++ b/app/Caches/TaggedQuestionList.php @@ -0,0 +1,69 @@ +lifetime; + } + + public function getKey($id = null) + { + return "tagged_question_list:{$id}"; + } + + public function getContent($id = null) + { + + $questionRepo = new QuestionRepo(); + + $where = [ + 'tag_id' => $id, + 'published' => QuestionModel::PUBLISH_APPROVED, + ]; + + $pager = $questionRepo->paginate($where); + + if ($pager->total_items == 0) return []; + + return $this->handleContent($pager->items); + } + + /** + * @param QuestionModel[] $questions + * @return array + */ + public function handleContent($questions) + { + $result = []; + + $count = 0; + + foreach ($questions as $question) { + if ($count < $this->limit) { + $result[] = [ + 'id' => $question->id, + 'title' => $question->title, + 'view_count' => $question->view_count, + 'like_count' => $question->like_count, + 'answer_count' => $question->answer_count, + 'favorite_count' => $question->favorite_count, + ]; + $count++; + } + } + + return $result; + } + +} diff --git a/app/Caches/TopAnswererList.php b/app/Caches/TopAnswererList.php new file mode 100644 index 00000000..c4f0aaf9 --- /dev/null +++ b/app/Caches/TopAnswererList.php @@ -0,0 +1,103 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'question_top_answerer_list'; + } + + public function getContent($id = null) + { + $rankings = $this->findWeeklyAuthorRankings(); + + if ($rankings->count() > 0) { + $userIds = kg_array_column($rankings->toArray(), 'author_id'); + return $this->handleUsers($userIds); + } + + $randOwners = $this->findRandArticleOwners(); + + if ($randOwners->count() > 0) { + $userIds = kg_array_column($randOwners->toArray(), 'owner_id'); + return $this->handleUsers($userIds); + } + + return []; + } + + protected function handleUsers($userIds) + { + $userRepo = new UserRepo(); + + $users = $userRepo->findByIds($userIds); + + $result = []; + + foreach ($users as $user) { + $result[] = [ + 'id' => $user->id, + 'name' => $user->name, + 'avatar' => $user->avatar, + 'title' => $user->title, + 'about' => $user->about, + 'vip' => $user->vip, + ]; + } + + return $result; + } + + /** + * @param int $limit + * @return ResultsetInterface|Resultset + */ + protected function findRandArticleOwners($limit = 10) + { + return ArticleModel::query() + ->columns(['owner_id']) + ->orderBy('RAND()') + ->limit($limit) + ->execute(); + } + + /** + * @param int $limit + * @return ResultsetInterface|Resultset + */ + protected function findWeeklyAuthorRankings($limit = 10) + { + $createTime = strtotime('monday this week'); + + $columns = [ + 'author_id' => 'a.owner_id', + 'like_count' => 'count(al.user_id)', + ]; + + return $this->modelsManager->createBuilder() + ->columns($columns) + ->addFrom(ArticleLikeModel::class, 'al') + ->join(ArticleModel::class, 'al.article_id = a.id', 'a') + ->where('al.create_time > :create_time:', ['create_time' => $createTime]) + ->groupBy('author_id') + ->orderBy('like_count DESC') + ->limit($limit)->getQuery()->execute(); + } + +} diff --git a/app/Console/Tasks/ArticleIndexTask.php b/app/Console/Tasks/ArticleIndexTask.php index addf2e4f..5528fbe0 100644 --- a/app/Console/Tasks/ArticleIndexTask.php +++ b/app/Console/Tasks/ArticleIndexTask.php @@ -118,7 +118,7 @@ class ArticleIndexTask extends Task protected function findArticles() { return ArticleModel::query() - ->where('published = 1') + ->where('published = :published:', ['published' => ArticleModel::PUBLISH_APPROVED]) ->execute(); } diff --git a/app/Console/Tasks/QuestionIndexTask.php b/app/Console/Tasks/QuestionIndexTask.php new file mode 100644 index 00000000..b6bfbc6a --- /dev/null +++ b/app/Console/Tasks/QuestionIndexTask.php @@ -0,0 +1,125 @@ +searchQuestions($query); + + var_export($result); + } + + /** + * 清空索引 + * + * @command: php console.php question_index clean + */ + public function cleanAction() + { + $this->cleanQuestionIndex(); + } + + /** + * 重建索引 + * + * @command: php console.php question_index rebuild + */ + public function rebuildAction() + { + $this->rebuildQuestionIndex(); + } + + /** + * 清空索引 + */ + protected function cleanQuestionIndex() + { + $handler = new QuestionSearcher(); + + $index = $handler->getXS()->getIndex(); + + echo '------ start clean question index ------' . PHP_EOL; + + $index->clean(); + + echo '------ end clean question index ------' . PHP_EOL; + } + + /** + * 重建索引 + */ + protected function rebuildQuestionIndex() + { + $questions = $this->findQuestions(); + + if ($questions->count() == 0) return; + + $handler = new QuestionSearcher(); + + $documenter = new QuestionDocument(); + + $index = $handler->getXS()->getIndex(); + + echo '------ start rebuild question index ------' . PHP_EOL; + + $index->beginRebuild(); + + foreach ($questions as $question) { + $document = $documenter->setDocument($question); + $index->add($document); + } + + $index->endRebuild(); + + echo '------ end rebuild question index ------' . PHP_EOL; + } + + /** + * 搜索文章 + * + * @param string $query + * @return array + * @throws \XSException + */ + protected function searchQuestions($query) + { + $handler = new QuestionSearcher(); + + return $handler->search($query); + } + + /** + * 查找文章 + * + * @return ResultsetInterface|Resultset|QuestionModel[] + */ + protected function findQuestions() + { + return QuestionModel::query() + ->where('published = :published:', ['published' => QuestionModel::PUBLISH_APPROVED]) + ->execute(); + } + +} diff --git a/app/Console/Tasks/SyncArticleIndexTask.php b/app/Console/Tasks/SyncArticleIndexTask.php index ace75ba4..dbd0ea51 100644 --- a/app/Console/Tasks/SyncArticleIndexTask.php +++ b/app/Console/Tasks/SyncArticleIndexTask.php @@ -2,6 +2,7 @@ namespace App\Console\Tasks; +use App\Models\Article as ArticleModel; use App\Repos\Article as ArticleRepo; use App\Services\Search\ArticleDocument; use App\Services\Search\ArticleSearcher; @@ -38,7 +39,7 @@ class SyncArticleIndexTask extends Task $doc = $document->setDocument($article); - if ($article->published == 1) { + if ($article->published == ArticleModel::PUBLISH_APPROVED) { $index->update($doc); } else { $index->del($article->id); diff --git a/app/Console/Tasks/SyncArticleScoreTask.php b/app/Console/Tasks/SyncArticleScoreTask.php new file mode 100644 index 00000000..b6ee3376 --- /dev/null +++ b/app/Console/Tasks/SyncArticleScoreTask.php @@ -0,0 +1,44 @@ +getRedis(); + + $key = $this->getSyncKey(); + + $articleIds = $redis->sRandMember($key, 1000); + + if (!$articleIds) return; + + $articleRepo = new ArticleRepo(); + + $articles = $articleRepo->findByIds($articleIds); + + if ($articles->count() == 0) return; + + $service = new ArticleScoreService(); + + foreach ($articles as $article) { + $service->handle($article); + } + + $redis->sRem($key, ...$articleIds); + } + + protected function getSyncKey() + { + $sync = new ArticleScoreSync(); + + return $sync->getSyncKey(); + } + +} diff --git a/app/Console/Tasks/SyncCourseScoreTask.php b/app/Console/Tasks/SyncCourseScoreTask.php index 3a789633..b5cf1901 100644 --- a/app/Console/Tasks/SyncCourseScoreTask.php +++ b/app/Console/Tasks/SyncCourseScoreTask.php @@ -3,8 +3,8 @@ namespace App\Console\Tasks; use App\Repos\Course as CourseRepo; -use App\Services\CourseStat as CourseStatService; use App\Services\Sync\CourseScore as CourseScoreSync; +use App\Services\Utils\CourseScore as CourseScoreService; class SyncCourseScoreTask extends Task { @@ -25,10 +25,10 @@ class SyncCourseScoreTask extends Task if ($courses->count() == 0) return; - $statService = new CourseStatService(); + $service = new CourseScoreService(); foreach ($courses as $course) { - $statService->updateScore($course->id); + $service->handle($course); } $redis->sRem($key, ...$courseIds); diff --git a/app/Console/Tasks/SyncQuestionIndexTask.php b/app/Console/Tasks/SyncQuestionIndexTask.php new file mode 100644 index 00000000..6c99a6a5 --- /dev/null +++ b/app/Console/Tasks/SyncQuestionIndexTask.php @@ -0,0 +1,61 @@ +getRedis(); + + $key = $this->getSyncKey(); + + $questionIds = $redis->sRandMember($key, 1000); + + if (!$questionIds) return; + + $questionRepo = new QuestionRepo(); + + $questions = $questionRepo->findByIds($questionIds); + + if ($questions->count() == 0) return; + + $document = new QuestionDocument(); + + $handler = new QuestionSearcher(); + + $index = $handler->getXS()->getIndex(); + + $index->openBuffer(); + + foreach ($questions as $question) { + + $doc = $document->setDocument($question); + + if ($question->published == QuestionModel::PUBLISH_APPROVED) { + $index->update($doc); + } else { + $index->del($question->id); + } + } + + $index->closeBuffer(); + + $redis->sRem($key, ...$questionIds); + } + + protected function getSyncKey() + { + $sync = new QuestionIndexSync(); + + return $sync->getSyncKey(); + } + +} diff --git a/app/Console/Tasks/SyncQuestionScoreTask.php b/app/Console/Tasks/SyncQuestionScoreTask.php new file mode 100644 index 00000000..44bc0086 --- /dev/null +++ b/app/Console/Tasks/SyncQuestionScoreTask.php @@ -0,0 +1,44 @@ +getRedis(); + + $key = $this->getSyncKey(); + + $questionIds = $redis->sRandMember($key, 1000); + + if (!$questionIds) return; + + $questionRepo = new QuestionRepo(); + + $questions = $questionRepo->findByIds($questionIds); + + if ($questions->count() == 0) return; + + $service = new QuestionScoreService(); + + foreach ($questions as $question) { + $service->handle($question); + } + + $redis->sRem($key, ...$questionIds); + } + + protected function getSyncKey() + { + $sync = new QuestionScoreSync(); + + return $sync->getSyncKey(); + } + +} diff --git a/app/Http/Admin/Controllers/UploadController.php b/app/Http/Admin/Controllers/UploadController.php index 25edb530..22f2e82f 100644 --- a/app/Http/Admin/Controllers/UploadController.php +++ b/app/Http/Admin/Controllers/UploadController.php @@ -53,6 +53,27 @@ class UploadController extends Controller return $this->jsonSuccess(['data' => $data]); } + /** + * @Post("/icon/img", name="admin.upload.icon_img") + */ + public function uploadIconImageAction() + { + $service = new StorageService(); + + $file = $service->uploadIconImage(); + + if (!$file) { + return $this->jsonError(['msg' => '上传文件失败']); + } + + $data = [ + 'src' => $service->getImageUrl($file->path), + 'title' => $file->name, + ]; + + return $this->jsonSuccess(['data' => $data]); + } + /** * @Post("/cover/img", name="admin.upload.cover_img") */ diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 1c7871e3..04e00268 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -542,6 +542,12 @@ class AuthNode extends Service 'type' => 'menu', 'route' => 'admin.slide.list', ], + [ + 'id' => '2-5-5', + 'title' => '搜索轮播', + 'type' => 'menu', + 'route' => 'admin.slide.search', + ], [ 'id' => '2-5-2', 'title' => '添加轮播', @@ -560,12 +566,6 @@ class AuthNode extends Service 'type' => 'button', 'route' => 'admin.slide.delete', ], - [ - 'id' => '2-5-5', - 'title' => '搜索轮播', - 'type' => 'menu', - 'route' => 'admin.slide.search', - ], ], ], [ diff --git a/app/Http/Admin/Services/Tag.php b/app/Http/Admin/Services/Tag.php index 61a86c5e..584a8086 100644 --- a/app/Http/Admin/Services/Tag.php +++ b/app/Http/Admin/Services/Tag.php @@ -19,7 +19,7 @@ class Tag extends Service $params['deleted'] = $params['deleted'] ?? 0; - $sort = 'priority'; + $sort = $pagerQuery->getSort(); $page = $pagerQuery->getPage(); $limit = $pagerQuery->getLimit(); @@ -43,7 +43,6 @@ class Tag extends Service $tag->name = $validator->checkName($post['name']); $tag->priority = $validator->checkPriority($post['priority']); - $tag->published = $validator->checkPublishStatus($post['published']); $tag->create(); @@ -69,6 +68,10 @@ class Tag extends Service } } + if (isset($post['icon'])) { + $data['icon'] = $validator->checkIcon($post['icon']); + } + if (isset($post['priority'])) { $data['priority'] = $validator->checkPriority($post['priority']); } diff --git a/app/Http/Admin/Views/tag/add.volt b/app/Http/Admin/Views/tag/add.volt index 8dc363f1..b2d30288 100644 --- a/app/Http/Admin/Views/tag/add.volt +++ b/app/Http/Admin/Views/tag/add.volt @@ -12,19 +12,6 @@ -