diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ce29a7..965bfae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### [v1.3.4](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.4)(2021-05-13) + +### 更新 + +- 前台增加问答功能 +- 优化标签功能 +- 优化文章功能以及全文搜索 +- 优化课程评价,咨询,文章等相关统计 +- 后台增加提问和回答审核功能 +- 后台增加查看用户在线记录 修正后台编辑角色权限错误 +- 优化前台界面 + ### [v1.3.3](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.3)(2021-04-30) ### 更新 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/ArticleFavoriteList.php b/app/Builders/ArticleFavoriteList.php index 20e856a6..b81a55d7 100644 --- a/app/Builders/ArticleFavoriteList.php +++ b/app/Builders/ArticleFavoriteList.php @@ -4,6 +4,7 @@ namespace App\Builders; use App\Repos\Article as ArticleRepo; use App\Repos\User as UserRepo; +use Phalcon\Text; class ArticleFavoriteList extends Builder { @@ -38,7 +39,8 @@ class ArticleFavoriteList extends Builder $columns = [ 'id', 'title', 'cover', - 'view_count', 'like_count', 'comment_count', 'favorite_count', + 'view_count', 'like_count', + 'comment_count', 'favorite_count', ]; $articles = $articleRepo->findByIds($ids, $columns); @@ -48,7 +50,11 @@ class ArticleFavoriteList extends Builder $result = []; foreach ($articles->toArray() as $article) { - $article['cover'] = $baseUrl . $article['cover']; + + if (!empty($article['cover']) && !Text::startsWith($article['cover'], 'http')) { + $article['cover'] = $baseUrl . $article['cover']; + } + $result[$article['id']] = $article; } diff --git a/app/Builders/QuestionFavoriteList.php b/app/Builders/QuestionFavoriteList.php new file mode 100644 index 00000000..0f2cba55 --- /dev/null +++ b/app/Builders/QuestionFavoriteList.php @@ -0,0 +1,84 @@ +getQuestions($relations); + + foreach ($relations as $key => $value) { + $relations[$key]['question'] = $questions[$value['question_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 getQuestions(array $relations) + { + $ids = kg_array_column($relations, 'question_id'); + + $questionRepo = new QuestionRepo(); + + $columns = [ + 'id', 'title', 'cover', + 'view_count', 'like_count', + 'answer_count', 'favorite_count', + ]; + + $questions = $questionRepo->findByIds($ids, $columns); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($questions->toArray() as $question) { + + if (!empty($question['cover']) && !Text::startsWith($question['cover'], 'http')) { + $question['cover'] = $baseUrl . $question['cover']; + } + + $result[$question['id']] = $question; + } + + 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/Builders/QuestionList.php b/app/Builders/QuestionList.php new file mode 100644 index 00000000..3987fac9 --- /dev/null +++ b/app/Builders/QuestionList.php @@ -0,0 +1,86 @@ + $question) { + $questions[$key]['tags'] = json_decode($question['tags'], true); + } + + return $questions; + } + + public function handleCategories(array $articles) + { + $categories = $this->getCategories(); + + foreach ($articles as $key => $article) { + $articles[$key]['category'] = $categories[$article['category_id']] ?? new \stdClass(); + } + + return $articles; + } + + 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 getCategories() + { + $cache = new CategoryListCache(); + + $items = $cache->get(CategoryModel::TYPE_QUESTION); + + if (empty($items)) return []; + + $result = []; + + foreach ($items as $item) { + $result[$item['id']] = [ + 'id' => $item['id'], + 'name' => $item['name'], + ]; + } + + return $result; + } + + 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/SitemapTask.php b/app/Console/Tasks/SitemapTask.php index ad686103..a86751c8 100644 --- a/app/Console/Tasks/SitemapTask.php +++ b/app/Console/Tasks/SitemapTask.php @@ -8,6 +8,7 @@ use App\Models\Course as CourseModel; use App\Models\Help as HelpModel; use App\Models\ImGroup as ImGroupModel; use App\Models\Page as PageModel; +use App\Models\Question as QuestionModel; use App\Models\Topic as TopicModel; use App\Models\User as UserModel; use App\Services\Service as AppService; @@ -37,6 +38,7 @@ class SitemapTask extends Task $this->addIndex(); $this->addCourses(); $this->addArticles(); + $this->addQuestions(); $this->addTeachers(); $this->addTopics(); $this->addImGroups(); @@ -66,7 +68,11 @@ class SitemapTask extends Task /** * @var Resultset|CourseModel[] $courses */ - $courses = CourseModel::query()->where('published = 1')->execute(); + $courses = CourseModel::query() + ->where('published = 1') + ->orderBy('id DESC') + ->limit(500) + ->execute(); if ($courses->count() == 0) return; @@ -81,7 +87,11 @@ class SitemapTask extends Task /** * @var Resultset|ArticleModel[] $articles */ - $articles = ArticleModel::query()->where('published = 1')->execute(); + $articles = ArticleModel::query() + ->where('published = :published:', ['published' => ArticleModel::PUBLISH_APPROVED]) + ->orderBy('id DESC') + ->limit(500) + ->execute(); if ($articles->count() == 0) return; @@ -91,6 +101,25 @@ class SitemapTask extends Task } } + protected function addQuestions() + { + /** + * @var Resultset|QuestionModel[] $questions + */ + $questions = QuestionModel::query() + ->where('published = :published:', ['published' => QuestionModel::PUBLISH_APPROVED]) + ->orderBy('id DESC') + ->limit(500) + ->execute(); + + if ($questions->count() == 0) return; + + foreach ($questions as $question) { + $loc = sprintf('%s/question/%s', $this->siteUrl, $question->id); + $this->sitemap->addItem($loc, 0.8); + } + } + protected function addTeachers() { /** 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/AnswerController.php b/app/Http/Admin/Controllers/AnswerController.php new file mode 100644 index 00000000..e22ca8f3 --- /dev/null +++ b/app/Http/Admin/Controllers/AnswerController.php @@ -0,0 +1,201 @@ +getPublishTypes(); + + $this->view->setVar('publish_types', $publishTypes); + } + + /** + * @Get("/list", name="admin.answer.list") + */ + public function listAction() + { + $answerService = new AnswerService(); + + $pager = $answerService->getAnswers(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.answer.add") + */ + public function addAction() + { + $id = $this->request->getQuery('question_id', 'int', 0); + + $questionService = new QuestionService(); + + $question = $questionService->getQuestion($id); + + $referer = $this->request->getHTTPReferer(); + + $this->view->setVar('question', $question); + $this->view->setVar('referer', $referer); + } + + /** + * @Get("/{id:[0-9]+}/edit", name="admin.answer.edit") + */ + public function editAction($id) + { + $answerService = new AnswerService(); + + $answer = $answerService->getAnswer($id); + + $questionService = new QuestionService(); + + $question = $questionService->getQuestion($answer->question_id); + + $referer = $this->request->getHTTPReferer(); + + $this->view->setVar('referer', $referer); + $this->view->setVar('question', $question); + $this->view->setVar('answer', $answer); + } + + /** + * @Get("/{id:[0-9]+}/show", name="admin.answer.show") + */ + public function showAction($id) + { + $answerService = new AnswerService(); + + $answer = $answerService->getAnswer($id); + + $this->view->setVar('answer', $answer); + } + + /** + * @Post("/create", name="admin.answer.create") + */ + public function createAction() + { + $answerService = new AnswerService(); + + $answerService->createAnswer(); + + $location = $this->request->getPost('referer'); + + if (empty($location)) { + $location = $this->url->get(['for' => 'admin.question.list']); + } + + $content = [ + 'location' => $location, + 'msg' => '回答问题成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/update", name="admin.answer.update") + */ + public function updateAction($id) + { + $answerService = new AnswerService(); + + $answerService->updateAnswer($id); + + $location = $this->request->getPost('referer'); + + if (empty($location)) { + $location = $this->url->get(['for' => 'admin.answer.list']); + } + + $content = [ + 'location' => $location, + 'msg' => '更新回答成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/delete", name="admin.answer.delete") + */ + public function deleteAction($id) + { + $answerService = new AnswerService(); + + $answerService->deleteAnswer($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除回答成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/restore", name="admin.answer.restore") + */ + public function restoreAction($id) + { + $answerService = new AnswerService(); + + $answerService->restoreAnswer($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原回答成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Route("/{id:[0-9]+}/review", name="admin.answer.review") + */ + public function reviewAction($id) + { + $answerService = new AnswerService(); + + $answer = $answerService->getAnswer($id); + + if ($this->request->isPost()) { + + $answerService->reviewAnswer($id); + + $location = $this->url->get(['for' => 'admin.mod.answers']); + + $content = [ + 'location' => $location, + 'msg' => '审核回答成功', + ]; + + return $this->jsonSuccess($content); + } + + $reasons = $answerService->getReasons(); + + $questionService = new QuestionService(); + + $question = $questionService->getQuestion($answer->question_id); + + $this->view->setVar('reasons', $reasons); + $this->view->setVar('question', $question); + $this->view->setVar('answer', $answer); + } + +} diff --git a/app/Http/Admin/Controllers/ArticleController.php b/app/Http/Admin/Controllers/ArticleController.php index 7fd4a3b4..26080afb 100644 --- a/app/Http/Admin/Controllers/ArticleController.php +++ b/app/Http/Admin/Controllers/ArticleController.php @@ -54,19 +54,6 @@ class ArticleController extends Controller $this->view->setVar('pager', $pager); } - /** - * @Get("/list/pending", name="admin.article.pending_list") - */ - public function pendingListAction() - { - $articleService = new ArticleService(); - - $pager = $articleService->getPendingArticles(); - - $this->view->pick('article/pending_list'); - $this->view->setVar('pager', $pager); - } - /** * @Get("/add", name="admin.article.add") */ @@ -106,10 +93,8 @@ class ArticleController extends Controller { $articleService = new ArticleService(); - $rejectOptions = $articleService->getRejectOptions(); $article = $articleService->getArticle($id); - $this->view->setVar('reject_options', $rejectOptions); $this->view->setVar('article', $article); } @@ -184,22 +169,31 @@ class ArticleController extends Controller } /** - * @Post("/{id:[0-9]+}/review", name="admin.article.review") + * @Route("/{id:[0-9]+}/review", name="admin.article.review") */ public function reviewAction($id) { $articleService = new ArticleService(); - $articleService->reviewArticle($id); + if ($this->request->isPost()) { - $location = $this->url->get(['for' => 'admin.article.pending_list']); + $articleService->reviewArticle($id); - $content = [ - 'location' => $location, - 'msg' => '审核文章成功', - ]; + $location = $this->url->get(['for' => 'admin.mod.articles']); - return $this->jsonSuccess($content); + $content = [ + 'location' => $location, + 'msg' => '审核文章成功', + ]; + + return $this->jsonSuccess($content); + } + + $reasons = $articleService->getReasons(); + $article = $articleService->getArticle($id); + + $this->view->setVar('reasons', $reasons); + $this->view->setVar('article', $article); } } diff --git a/app/Http/Admin/Controllers/ModerationController.php b/app/Http/Admin/Controllers/ModerationController.php index 75c81808..0ec36a01 100644 --- a/app/Http/Admin/Controllers/ModerationController.php +++ b/app/Http/Admin/Controllers/ModerationController.php @@ -22,4 +22,28 @@ class ModerationController extends Controller $this->view->setVar('pager', $pager); } + /** + * @Get("/questions", name="admin.mod.questions") + */ + public function questionsAction() + { + $modService = new ModerationService(); + + $pager = $modService->getQuestions(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/answers", name="admin.mod.answers") + */ + public function answersAction() + { + $modService = new ModerationService(); + + $pager = $modService->getAnswers(); + + $this->view->setVar('pager', $pager); + } + } diff --git a/app/Http/Admin/Controllers/QuestionController.php b/app/Http/Admin/Controllers/QuestionController.php new file mode 100644 index 00000000..ef92e49d --- /dev/null +++ b/app/Http/Admin/Controllers/QuestionController.php @@ -0,0 +1,195 @@ +url->get( + ['for' => 'admin.category.list'], + ['type' => CategoryModel::TYPE_ARTICLE] + ); + + $this->response->redirect($location); + } + + /** + * @Get("/search", name="admin.question.search") + */ + public function searchAction() + { + $questionService = new QuestionService(); + + $publishTypes = $questionService->getPublishTypes(); + $categories = $questionService->getCategories(); + $xmTags = $questionService->getXmTags(0); + + $this->view->setVar('publish_types', $publishTypes); + $this->view->setVar('categories', $categories); + $this->view->setVar('xm_tags', $xmTags); + } + + /** + * @Get("/list", name="admin.question.list") + */ + public function listAction() + { + $questionService = new QuestionService(); + + $pager = $questionService->getQuestions(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.question.add") + */ + public function addAction() + { + $questionService = new QuestionService(); + + $categories = $questionService->getCategories(); + + $this->view->setVar('categories', $categories); + } + + /** + * @Get("/{id:[0-9]+}/edit", name="admin.question.edit") + */ + public function editAction($id) + { + $questionService = new QuestionService(); + + $publishTypes = $questionService->getPublishTypes(); + $categories = $questionService->getCategories(); + $question = $questionService->getQuestion($id); + $xmTags = $questionService->getXmTags($id); + + $this->view->setVar('publish_types', $publishTypes); + $this->view->setVar('categories', $categories); + $this->view->setVar('question', $question); + $this->view->setVar('xm_tags', $xmTags); + } + + /** + * @Get("/{id:[0-9]+}/show", name="admin.question.show") + */ + public function showAction($id) + { + $questionService = new QuestionService(); + + $question = $questionService->getQuestion($id); + + $this->view->setVar('question', $question); + } + + /** + * @Post("/create", name="admin.question.create") + */ + public function createAction() + { + $questionService = new QuestionService(); + + $question = $questionService->createQuestion(); + + $location = $this->url->get([ + 'for' => 'admin.question.edit', + 'id' => $question->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建问题成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/update", name="admin.question.update") + */ + public function updateAction($id) + { + $questionService = new QuestionService(); + + $questionService->updateQuestion($id); + + $content = ['msg' => '更新问题成功']; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/delete", name="admin.question.delete") + */ + public function deleteAction($id) + { + $questionService = new QuestionService(); + + $questionService->deleteQuestion($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除问题成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/restore", name="admin.question.restore") + */ + public function restoreAction($id) + { + $questionService = new QuestionService(); + + $questionService->restoreQuestion($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原问题成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Route("/{id:[0-9]+}/review", name="admin.question.review") + */ + public function reviewAction($id) + { + $questionService = new QuestionService(); + + if ($this->request->isPost()) { + + $questionService->reviewQuestion($id); + + $location = $this->url->get(['for' => 'admin.mod.questions']); + + $content = [ + 'location' => $location, + 'msg' => '审核问题成功', + ]; + + return $this->jsonSuccess($content); + } + + $reasons = $questionService->getReasons(); + $question = $questionService->getQuestion($id); + + $this->view->setVar('reasons', $reasons); + $this->view->setVar('question', $question); + } + +} diff --git a/app/Http/Admin/Controllers/UploadController.php b/app/Http/Admin/Controllers/UploadController.php index 25edb530..b34c5f3a 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") */ @@ -127,7 +148,6 @@ class UploadController extends Controller $items['user_avatar'] = $service->uploadDefaultUserAvatar(); $items['group_avatar'] = $service->uploadDefaultGroupAvatar(); - $items['article_cover'] = $service->uploadDefaultArticleCover(); $items['course_cover'] = $service->uploadDefaultCourseCover(); $items['package_cover'] = $service->uploadDefaultPackageCover(); $items['gift_cover'] = $service->uploadDefaultGiftCover(); diff --git a/app/Http/Admin/Controllers/UserController.php b/app/Http/Admin/Controllers/UserController.php index 5d106768..b53e405d 100644 --- a/app/Http/Admin/Controllers/UserController.php +++ b/app/Http/Admin/Controllers/UserController.php @@ -94,6 +94,18 @@ class UserController extends Controller $this->view->setVar('admin_roles', $adminRoles); } + /** + * @Get("/{id:[0-9]+}/online", name="admin.user.online") + */ + public function onlineAction($id) + { + $userService = new UserService(); + + $pager = $userService->getOnlineLogs($id); + + $this->view->setVar('pager', $pager); + } + /** * @Post("/{id:[0-9]+}/update", name="admin.user.update") */ diff --git a/app/Http/Admin/Services/Answer.php b/app/Http/Admin/Services/Answer.php new file mode 100644 index 00000000..b1fd1263 --- /dev/null +++ b/app/Http/Admin/Services/Answer.php @@ -0,0 +1,295 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $answerRepo = new AnswerRepo(); + + $pager = $answerRepo->paginate($params, $sort, $page, $limit); + + return $this->handleAnswers($pager); + } + + public function getAnswer($id) + { + return $this->findOrFail($id); + } + + public function createAnswer() + { + $post = $this->request->getPost(); + + $user = $this->getLoginUser(); + + $validator = new AnswerValidator(); + + $question = $validator->checkQuestion($post['question_id']); + + $answer = new AnswerModel(); + + $answer->owner_id = $user->id; + $answer->question_id = $question->id; + $answer->published = AnswerModel::PUBLISH_APPROVED; + $answer->content = $validator->checkContent($post['content']); + + $answer->create(); + + $this->recountQuestionAnswers($question); + $this->recountUserAnswers($user); + $this->handleAnswerPostPoint($answer); + $this->handleQuestionAnsweredNotice($answer); + + return $answer; + } + + public function updateAnswer($id) + { + $answer = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new AnswerValidator(); + + $data = []; + + if (isset($post['content'])) { + $data['content'] = $validator->checkContent($post['content']); + } + + if (isset($post['published'])) { + + $data['published'] = $validator->checkPublishStatus($post['published']); + + $question = $this->findQuestion($answer->question_id); + + $this->recountQuestionAnswers($question); + + $user = $this->findUser($answer->owner_id); + + $this->recountUserAnswers($user); + } + + $answer->update($data); + + return $answer; + } + + public function deleteAnswer($id) + { + $answer = $this->findOrFail($id); + + $answer->deleted = 1; + + $answer->update(); + + $question = $this->findQuestion($answer->question_id); + + $this->recountQuestionAnswers($question); + + $owner = $this->findUser($answer->owner_id); + + $this->recountUserAnswers($owner); + + return $answer; + } + + public function restoreAnswer($id) + { + $answer = $this->findOrFail($id); + + $answer->deleted = 0; + + $answer->update(); + + $question = $this->findQuestion($answer->question_id); + + $this->recountQuestionAnswers($question); + + $owner = $this->findUser($answer->owner_id); + + $this->recountUserAnswers($owner); + + return $answer; + } + + public function reviewAnswer($id) + { + $type = $this->request->getPost('type', ['trim', 'string']); + $reason = $this->request->getPost('reason', ['trim', 'string']); + + $answer = $this->findOrFail($id); + + $validator = new AnswerValidator(); + + if ($type == 'approve') { + $answer->published = AnswerModel::PUBLISH_APPROVED; + } elseif ($type == 'reject') { + $validator->checkRejectReason($reason); + $answer->published = AnswerModel::PUBLISH_REJECTED; + } + + $answer->update(); + + $question = $this->findQuestion($answer->question_id); + + $this->recountQuestionAnswers($question); + + $owner = $this->findUser($answer->owner_id); + + $this->recountUserAnswers($owner); + + $sender = $this->getLoginUser(); + + if ($type == 'approve') { + + $this->handleAnswerPostPoint($answer); + $this->handleAnswerApprovedNotice($answer, $sender); + + $this->eventsManager->fire('Answer:afterApprove', $this, $answer); + + } elseif ($type == 'reject') { + + $options = ReasonModel::answerRejectOptions(); + + if (array_key_exists($reason, $options)) { + $reason = $options[$reason]; + } + + $this->handleAnswerRejectedNotice($answer, $sender, $reason); + + $this->eventsManager->fire('Answer:afterReject', $this, $answer); + } + + return $answer; + } + + protected function findOrFail($id) + { + $validator = new AnswerValidator(); + + return $validator->checkAnswer($id); + } + + protected function findQuestion($id) + { + $questionRepo = new QuestionRepo(); + + return $questionRepo->findById($id); + } + + protected function findUser($id) + { + $userRepo = new UserRepo(); + + return $userRepo->findById($id); + } + + protected function handleAnswers($pager) + { + if ($pager->total_items > 0) { + + $builder = new AnswerListBuilder(); + + $items = $pager->items->toArray(); + + $pipeA = $builder->handleQuestions($items); + $pipeB = $builder->handleUsers($pipeA); + $pipeC = $builder->objects($pipeB); + + $pager->items = $pipeC; + } + + return $pager; + } + + protected function recountQuestionAnswers(QuestionModel $question) + { + $questionRepo = new QuestionRepo(); + + $answerCount = $questionRepo->countAnswers($question->id); + + $question->answer_count = $answerCount; + + $question->update(); + } + + protected function recountUserAnswers(UserModel $user) + { + $userRepo = new UserRepo(); + + $answerCount = $userRepo->countAnswers($user->id); + + $user->answer_count = $answerCount; + + $user->update(); + } + + protected function handleQuestionAnsweredNotice(AnswerModel $answer) + { + $notice = new QuestionAnsweredNotice(); + + $notice->handle($answer); + } + + protected function handleAnswerApprovedNotice(AnswerModel $answer, UserModel $sender) + { + $notice = new AnswerApprovedNotice(); + + $notice->handle($answer, $sender); + } + + protected function handleAnswerRejectedNotice(AnswerModel $answer, UserModel $sender, $reason) + { + $notice = new AnswerRejectedNotice(); + + $notice->handle($answer, $sender, $reason); + + } + + protected function handleAnswerPostPoint(AnswerModel $answer) + { + $service = new AnswerPostPointHistory(); + + $service->handle($answer); + } + +} diff --git a/app/Http/Admin/Services/Article.php b/app/Http/Admin/Services/Article.php index 98674a74..fa39df84 100644 --- a/app/Http/Admin/Services/Article.php +++ b/app/Http/Admin/Services/Article.php @@ -7,15 +7,14 @@ use App\Caches\Article as ArticleCache; use App\Library\Paginator\Query as PagerQuery; use App\Library\Utils\Word as WordUtil; use App\Models\Article as ArticleModel; -use App\Models\ArticleTag as ArticleTagModel; use App\Models\Category as CategoryModel; use App\Models\Reason as ReasonModel; use App\Models\User as UserModel; use App\Repos\Article as ArticleRepo; -use App\Repos\ArticleTag as ArticleTagRepo; use App\Repos\Category as CategoryRepo; use App\Repos\Tag as TagRepo; use App\Repos\User as UserRepo; +use App\Services\Logic\Article\ArticleDataTrait; use App\Services\Logic\Notice\System\ArticleApproved as ArticleApprovedNotice; use App\Services\Logic\Notice\System\ArticleRejected as ArticleRejectedNotice; use App\Services\Logic\Point\History\ArticlePost as ArticlePostPointHistory; @@ -25,20 +24,13 @@ use App\Validators\Article as ArticleValidator; class Article extends Service { - public function getArticleModel() - { - $article = new ArticleModel(); - - $article->afterFetch(); - - return $article; - } + use ArticleDataTrait; public function getXmTags($id) { $tagRepo = new TagRepo(); - $allTags = $tagRepo->findAll(['published' => 1], 'priority'); + $allTags = $tagRepo->findAll(['published' => 1]); if ($allTags->count() == 0) return []; @@ -86,7 +78,7 @@ class Article extends Service return ArticleModel::sourceTypes(); } - public function getRejectOptions() + public function getReasons() { return ReasonModel::articleRejectOptions(); } @@ -132,13 +124,14 @@ class Article extends Service $article = new ArticleModel(); + $article->published = ArticleModel::PUBLISH_APPROVED; $article->owner_id = $user->id; $article->category_id = $category->id; $article->title = $title; $article->create(); - $this->incrUserArticleCount($user); + $this->recountUserArticles($user); $this->eventsManager->fire('Article:afterCreate', $this, $article); @@ -164,14 +157,6 @@ class Article extends Service $data['title'] = $validator->checkTitle($post['title']); } - if (isset($post['cover'])) { - $data['cover'] = $validator->checkCover($post['cover']); - } - - if (isset($post['summary'])) { - $data['summary'] = $validator->checkSummary($post['summary']); - } - if (isset($post['content'])) { $data['content'] = $validator->checkContent($post['content']); $data['word_count'] = WordUtil::getWordCount($data['content']); @@ -184,8 +169,8 @@ class Article extends Service } } - if (isset($post['allow_comment'])) { - $data['allow_comment'] = $post['allow_comment']; + if (isset($post['closed'])) { + $data['closed'] = $validator->checkCloseStatus($post['closed']); } if (isset($post['private'])) { @@ -197,7 +182,12 @@ class Article extends Service } if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + + $owner = $this->findUser($article->owner_id); + + $this->recountUserArticles($owner); } if (isset($post['xm_tag_ids'])) { @@ -225,7 +215,7 @@ class Article extends Service $owner = $userRepo->findById($article->owner_id); - $this->decrUserArticleCount($owner); + $this->recountUserArticles($owner); $this->rebuildArticleIndex($article); @@ -246,7 +236,7 @@ class Article extends Service $owner = $userRepo->findById($article->owner_id); - $this->incrUserArticleCount($owner); + $this->recountUserArticles($owner); $this->rebuildArticleIndex($article); @@ -262,14 +252,25 @@ class Article extends Service $article = $this->findOrFail($id); + $validator = new ArticleValidator(); + if ($type == 'approve') { + $article->published = ArticleModel::PUBLISH_APPROVED; + } elseif ($type == 'reject') { + + $validator->checkRejectReason($reason); + $article->published = ArticleModel::PUBLISH_REJECTED; } $article->update(); + $owner = $this->findUser($article->owner_id); + + $this->recountUserArticles($owner); + $sender = $this->getLoginUser(); if ($type == 'approve') { @@ -309,59 +310,11 @@ class Article extends Service return $validator->checkArticle($id); } - protected function saveTags(ArticleModel $article, $tagIds) + protected function findUser($id) { - $originTagIds = []; + $userRepo = new UserRepo(); - /** - * 修改数据后,afterFetch设置的属性会失效,重新执行 - */ - $article->afterFetch(); - - if ($article->tags) { - $originTagIds = kg_array_column($article->tags, 'id'); - } - - $newTagIds = $tagIds ? explode(',', $tagIds) : []; - $addedTagIds = array_diff($newTagIds, $originTagIds); - - if ($addedTagIds) { - foreach ($addedTagIds as $tagId) { - $articleTag = new ArticleTagModel(); - $articleTag->article_id = $article->id; - $articleTag->tag_id = $tagId; - $articleTag->create(); - } - } - - $deletedTagIds = array_diff($originTagIds, $newTagIds); - - if ($deletedTagIds) { - $articleTagRepo = new ArticleTagRepo(); - foreach ($deletedTagIds as $tagId) { - $articleTag = $articleTagRepo->findArticleTag($article->id, $tagId); - if ($articleTag) { - $articleTag->delete(); - } - } - } - - $articleTags = []; - - if ($newTagIds) { - $tagRepo = new TagRepo(); - $tags = $tagRepo->findByIds($newTagIds); - if ($tags->count() > 0) { - $articleTags = []; - foreach ($tags as $tag) { - $articleTags[] = ['id' => $tag->id, 'name' => $tag->name]; - } - } - } - - $article->tags = $articleTags; - - $article->update(); + return $userRepo->findById($id); } protected function handleArticles($pager) @@ -383,21 +336,17 @@ class Article extends Service return $pager; } - protected function incrUserArticleCount(UserModel $user) + protected function recountUserArticles(UserModel $user) { - $user->article_count += 1; + $userRepo = new UserRepo(); + + $articleCount = $userRepo->countArticles($user->id); + + $user->article_count = $articleCount; $user->update(); } - protected function decrUserArticleCount(UserModel $user) - { - if ($user->article_count > 0) { - $user->article_count -= 1; - $user->update(); - } - } - protected function rebuildArticleCache(ArticleModel $article) { $cache = new ArticleCache(); diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 1c7871e3..b62914ae 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -272,12 +272,6 @@ class AuthNode extends Service 'type' => 'button', 'route' => 'admin.article.edit', ], - [ - 'id' => '1-7-6', - 'title' => '文章分类', - 'type' => 'menu', - 'route' => 'admin.article.category', - ], [ 'id' => '1-7-5', 'title' => '删除文章', @@ -288,7 +282,7 @@ class AuthNode extends Service 'id' => '1-7-9', 'title' => '文章详情', 'type' => 'button', - 'route' => 'admin.article.review', + 'route' => 'admin.article.show', ], [ 'id' => '1-7-10', @@ -298,6 +292,104 @@ class AuthNode extends Service ], ], ], + [ + 'id' => '1-10', + 'title' => '问题管理', + 'type' => 'menu', + 'children' => [ + [ + 'id' => '1-10-1', + 'title' => '问题列表', + 'type' => 'menu', + 'route' => 'admin.question.list', + ], + [ + 'id' => '1-10-2', + 'title' => '搜索问题', + 'type' => 'menu', + 'route' => 'admin.question.search', + ], + [ + 'id' => '1-10-3', + 'title' => '添加问题', + 'type' => 'menu', + 'route' => 'admin.question.add', + ], + [ + 'id' => '1-10-4', + 'title' => '编辑问题', + 'type' => 'button', + 'route' => 'admin.question.edit', + ], + [ + 'id' => '1-10-5', + 'title' => '删除问题', + 'type' => 'button', + 'route' => 'admin.question.delete', + ], + [ + 'id' => '1-10-9', + 'title' => '问题详情', + 'type' => 'button', + 'route' => 'admin.question.show', + ], + [ + 'id' => '1-10-10', + 'title' => '审核问题', + 'type' => 'button', + 'route' => 'admin.question.review', + ], + ], + ], + [ + 'id' => '1-11', + 'title' => '回答管理', + 'type' => 'menu', + 'children' => [ + [ + 'id' => '1-11-1', + 'title' => '回答列表', + 'type' => 'menu', + 'route' => 'admin.answer.list', + ], + [ + 'id' => '1-11-2', + 'title' => '搜索回答', + 'type' => 'menu', + 'route' => 'admin.answer.search', + ], + [ + 'id' => '1-11-3', + 'title' => '添加答案', + 'type' => 'button', + 'route' => 'admin.answer.add', + ], + [ + 'id' => '1-11-4', + 'title' => '编辑回答', + 'type' => 'button', + 'route' => 'admin.answer.edit', + ], + [ + 'id' => '1-11-5', + 'title' => '删除回答', + 'type' => 'button', + 'route' => 'admin.answer.delete', + ], + [ + 'id' => '1-11-9', + 'title' => '回答详情', + 'type' => 'button', + 'route' => 'admin.answer.show', + ], + [ + 'id' => '1-11-10', + 'title' => '审核回答', + 'type' => 'button', + 'route' => 'admin.answer.review', + ], + ], + ], [ 'id' => '1-8', 'title' => '标签管理', @@ -387,6 +479,18 @@ class AuthNode extends Service 'type' => 'menu', 'route' => 'admin.mod.articles', ], + [ + 'id' => '2-10-2', + 'title' => '问题列表', + 'type' => 'menu', + 'route' => 'admin.mod.questions', + ], + [ + 'id' => '2-10-3', + 'title' => '回答列表', + 'type' => 'menu', + 'route' => 'admin.mod.answers', + ], ], ], [ @@ -542,6 +646,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 +670,6 @@ class AuthNode extends Service 'type' => 'button', 'route' => 'admin.slide.delete', ], - [ - 'id' => '2-5-5', - 'title' => '搜索轮播', - 'type' => 'menu', - 'route' => 'admin.slide.search', - ], ], ], [ @@ -851,7 +955,13 @@ class AuthNode extends Service 'title' => '编辑用户', 'type' => 'button', 'route' => 'admin.user.edit', - ] + ], + [ + 'id' => '4-1-5', + 'title' => '在线记录', + 'type' => 'button', + 'route' => 'admin.user.online', + ], ], ], [ diff --git a/app/Http/Admin/Services/Consult.php b/app/Http/Admin/Services/Consult.php index 667b99a7..cb93d9a5 100644 --- a/app/Http/Admin/Services/Consult.php +++ b/app/Http/Admin/Services/Consult.php @@ -4,7 +4,10 @@ namespace App\Http\Admin\Services; use App\Builders\ConsultList as ConsultListBuilder; use App\Library\Paginator\Query as PagerQuery; +use App\Models\Chapter as ChapterModel; use App\Models\Consult as ConsultModel; +use App\Models\Course as CourseModel; +use App\Repos\Chapter as ChapterRepo; use App\Repos\Consult as ConsultRepo; use App\Repos\Course as CourseRepo; use App\Services\Logic\Notice\ConsultReply as ConsultReplyNotice; @@ -74,6 +77,7 @@ class Consult extends Service if (isset($post['published'])) { $data['published'] = $validator->checkPublishStatus($post['published']); + $this->handleItemConsults($consult); } $consult->update($data); @@ -93,13 +97,7 @@ class Consult extends Service $consult->update(); - $courseRepo = new CourseRepo(); - - $course = $courseRepo->findById($consult->course_id); - - $course->consult_count -= 1; - - $course->update(); + $this->handleItemConsults($consult); } public function restoreConsult($id) @@ -110,13 +108,20 @@ class Consult extends Service $consult->update(); - $courseRepo = new CourseRepo(); + $this->handleItemConsults($consult); + } - $course = $courseRepo->findById($consult->course_id); + protected function handleItemConsults(ConsultModel $consult) + { + if ($consult->course_id > 0) { + $course = $this->findCourse($consult->course_id); + $this->recountCourseConsults($course); + } - $course->consult_count += 1; - - $course->update(); + if ($consult->chapter_id > 0) { + $chapter = $this->findChapter($consult->chapter_id); + $this->recountChapterConsults($chapter); + } } protected function handleReplyNotice(ConsultModel $consult) @@ -133,6 +138,42 @@ class Consult extends Service return $validator->checkConsult($id); } + protected function findCourse($id) + { + $courseRepo = new CourseRepo(); + + return $courseRepo->findById($id); + } + + protected function findChapter($id) + { + $chapterRepo = new ChapterRepo(); + + return $chapterRepo->findById($id); + } + + protected function recountCourseConsults(CourseModel $course) + { + $courseRepo = new CourseRepo(); + + $consultCount = $courseRepo->countConsults($course->id); + + $course->consult_count = $consultCount; + + $course->update(); + } + + protected function recountChapterConsults(ChapterModel $chapter) + { + $chapterRepo = new ChapterRepo(); + + $consultCount = $chapterRepo->countConsults($chapter->id); + + $chapter->consult_count = $consultCount; + + $chapter->update(); + } + protected function handleConsults($pager) { if ($pager->total_items > 0) { diff --git a/app/Http/Admin/Services/Moderation.php b/app/Http/Admin/Services/Moderation.php index 68b69cc0..fbf3cfa7 100644 --- a/app/Http/Admin/Services/Moderation.php +++ b/app/Http/Admin/Services/Moderation.php @@ -2,10 +2,16 @@ namespace App\Http\Admin\Services; +use App\Builders\AnswerList as AnswerListBuilder; use App\Builders\ArticleList as ArticleListBuilder; +use App\Builders\QuestionList as QuestionListBuilder; use App\Library\Paginator\Query as PagerQuery; +use App\Models\Answer as AnswerModel; use App\Models\Article as ArticleModel; +use App\Models\Question as QuestionModel; +use App\Repos\Answer as AnswerRepo; use App\Repos\Article as ArticleRepo; +use App\Repos\Question as QuestionRepo; class Moderation extends Service { @@ -30,6 +36,46 @@ class Moderation extends Service return $this->handleArticles($pager); } + public function getQuestions() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['published'] = QuestionModel::PUBLISH_PENDING; + $params['deleted'] = 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $questionRepo = new QuestionRepo(); + + $pager = $questionRepo->paginate($params, $sort, $page, $limit); + + return $this->handleQuestions($pager); + } + + public function getAnswers() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + $params['published'] = AnswerModel::PUBLISH_PENDING; + $params['deleted'] = 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $answerRepo = new AnswerRepo(); + + $pager = $answerRepo->paginate($params, $sort, $page, $limit); + + return $this->handleAnswers($pager); + } + protected function handleArticles($pager) { if ($pager->total_items > 0) { @@ -49,4 +95,41 @@ class Moderation extends Service return $pager; } + protected function handleQuestions($pager) + { + if ($pager->total_items > 0) { + + $builder = new QuestionListBuilder(); + + $items = $pager->items->toArray(); + + $pipeA = $builder->handleQuestions($items); + $pipeB = $builder->handleCategories($pipeA); + $pipeC = $builder->handleUsers($pipeB); + $pipeD = $builder->objects($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + + protected function handleAnswers($pager) + { + if ($pager->total_items > 0) { + + $builder = new AnswerListBuilder(); + + $items = $pager->items->toArray(); + + $pipeA = $builder->handleQuestions($items); + $pipeB = $builder->handleUsers($pipeA); + $pipeC = $builder->objects($pipeB); + + $pager->items = $pipeC; + } + + return $pager; + } + } diff --git a/app/Http/Admin/Services/Question.php b/app/Http/Admin/Services/Question.php new file mode 100644 index 00000000..eda8ffd3 --- /dev/null +++ b/app/Http/Admin/Services/Question.php @@ -0,0 +1,349 @@ +findAll(['published' => 1]); + + if ($allTags->count() == 0) return []; + + $questionTagIds = []; + + if ($id > 0) { + $question = $this->findOrFail($id); + if (!empty($question->tags)) { + $questionTagIds = kg_array_column($question->tags, 'id'); + } + } + + $list = []; + + foreach ($allTags as $tag) { + $selected = in_array($tag->id, $questionTagIds); + $list[] = [ + 'name' => $tag->name, + 'value' => $tag->id, + 'selected' => $selected, + ]; + } + + return $list; + } + + public function getCategories() + { + $categoryRepo = new CategoryRepo(); + + return $categoryRepo->findAll([ + 'type' => CategoryModel::TYPE_ARTICLE, + 'level' => 1, + 'published' => 1, + ]); + } + + public function getPublishTypes() + { + return QuestionModel::publishTypes(); + } + + public function getReasons() + { + return ReasonModel::questionRejectOptions(); + } + + public function getQuestions() + { + $pagerQuery = new PagerQuery(); + + $params = $pagerQuery->getParams(); + + if (!empty($params['xm_tag_ids'])) { + $params['tag_id'] = explode(',', $params['xm_tag_ids']); + } + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $questionRepo = new QuestionRepo(); + + $pager = $questionRepo->paginate($params, $sort, $page, $limit); + + return $this->handleQuestions($pager); + } + + public function getQuestion($id) + { + return $this->findOrFail($id); + } + + public function createQuestion() + { + $post = $this->request->getPost(); + + $user = $this->getLoginUser(); + + $validator = new QuestionValidator(); + + $title = $validator->checkTitle($post['title']); + + $question = new QuestionModel(); + + $question->published = QuestionModel::PUBLISH_APPROVED; + $question->owner_id = $user->id; + $question->title = $title; + + $question->create(); + + $this->recountUserQuestions($user); + + $this->eventsManager->fire('Question:afterCreate', $this, $question); + + return $question; + } + + public function updateQuestion($id) + { + $post = $this->request->getPost(); + + $question = $this->findOrFail($id); + + $validator = new QuestionValidator(); + + $data = []; + + if (isset($post['category_id'])) { + $category = $validator->checkCategory($post['category_id']); + $data['category_id'] = $category->id; + } + + if (isset($post['title'])) { + $data['title'] = $validator->checkTitle($post['title']); + } + + if (isset($post['content'])) { + $data['content'] = $validator->checkContent($post['content']); + } + + if (isset($post['anonymous'])) { + $data['anonymous'] = $validator->checkAnonymousStatus($post['anonymous']); + } + + if (isset($post['closed'])) { + $data['closed'] = $validator->checkCloseStatus($post['closed']); + } + + if (isset($post['published'])) { + + $data['published'] = $validator->checkPublishStatus($post['published']); + + $owner = $this->findUser($question->owner_id); + + $this->recountUserQuestions($owner); + } + + if (isset($post['xm_tag_ids'])) { + $this->saveTags($question, $post['xm_tag_ids']); + } + + $question->update($data); + + $this->rebuildQuestionIndex($question); + + $this->eventsManager->fire('Question:afterUpdate', $this, $question); + + return $question; + } + + public function deleteQuestion($id) + { + $question = $this->findOrFail($id); + + $question->deleted = 1; + + $question->update(); + + $userRepo = new UserRepo(); + + $owner = $userRepo->findById($question->owner_id); + + $this->recountUserQuestions($owner); + + $this->rebuildQuestionIndex($question); + + $this->eventsManager->fire('Question:afterDelete', $this, $question); + + return $question; + } + + public function restoreQuestion($id) + { + $question = $this->findOrFail($id); + + $question->deleted = 0; + + $question->update(); + + $userRepo = new UserRepo(); + + $owner = $userRepo->findById($question->owner_id); + + $this->recountUserQuestions($owner); + + $this->rebuildQuestionIndex($question); + + $this->eventsManager->fire('Question:afterRestore', $this, $question); + + return $question; + } + + public function reviewQuestion($id) + { + $type = $this->request->getPost('type', ['trim', 'string']); + $reason = $this->request->getPost('reason', ['trim', 'string']); + + $question = $this->findOrFail($id); + + $validator = new QuestionValidator(); + + if ($type == 'approve') { + $question->published = QuestionModel::PUBLISH_APPROVED; + } elseif ($type == 'reject') { + $validator->checkRejectReason($reason); + $question->published = QuestionModel::PUBLISH_REJECTED; + } + + $question->update(); + + $owner = $this->findUser($question->owner_id); + + $this->recountUserQuestions($owner); + + $sender = $this->getLoginUser(); + + if ($type == 'approve') { + + $this->rebuildQuestionIndex($question); + + $this->handlePostPoint($question); + + $notice = new QuestionApprovedNotice(); + + $notice->handle($question, $sender); + + $this->eventsManager->fire('Question:afterApprove', $this, $question); + + } elseif ($type == 'reject') { + + $options = ReasonModel::questionRejectOptions(); + + if (array_key_exists($reason, $options)) { + $reason = $options[$reason]; + } + + $notice = new QuestionRejectedNotice(); + + $notice->handle($question, $sender, $reason); + + $this->eventsManager->fire('Question:afterReject', $this, $question); + } + + return $question; + } + + protected function findOrFail($id) + { + $validator = new QuestionValidator(); + + return $validator->checkQuestion($id); + } + + protected function findUser($id) + { + $userRepo = new UserRepo(); + + return $userRepo->findById($id); + } + + protected function handleQuestions($pager) + { + if ($pager->total_items > 0) { + + $builder = new QuestionListBuilder(); + + $items = $pager->items->toArray(); + + $pipeA = $builder->handleQuestions($items); + $pipeB = $builder->handleCategories($pipeA); + $pipeC = $builder->handleUsers($pipeB); + $pipeD = $builder->objects($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + + protected function recountUserQuestions(UserModel $user) + { + $userRepo = new UserRepo(); + + $questionCount = $userRepo->countQuestions($user->id); + + $user->question_count = $questionCount; + + $user->update(); + } + + protected function rebuildQuestionCache(QuestionModel $question) + { + $cache = new QuestionCache(); + + $cache->rebuild($question->id); + } + + protected function rebuildQuestionIndex(QuestionModel $question) + { + $sync = new QuestionIndexSync(); + + $sync->addItem($question->id); + } + + protected function handlePostPoint(QuestionModel $question) + { + if ($question->published != QuestionModel::PUBLISH_APPROVED) return; + + $service = new QuestionPostPointHistory(); + + $service->handle($question); + } + +} diff --git a/app/Http/Admin/Services/Review.php b/app/Http/Admin/Services/Review.php index 5ae43fb6..e04c41fb 100644 --- a/app/Http/Admin/Services/Review.php +++ b/app/Http/Admin/Services/Review.php @@ -4,6 +4,7 @@ namespace App\Http\Admin\Services; use App\Builders\ReviewList as ReviewListBuilder; use App\Library\Paginator\Query as PagerQuery; +use App\Models\Course as CourseModel; use App\Repos\Course as CourseRepo; use App\Repos\Review as ReviewRepo; use App\Services\CourseStat as CourseStatService; @@ -47,6 +48,8 @@ class Review extends Service { $review = $this->findOrFail($id); + $course = $this->findCourse($review->course_id); + $post = $this->request->getPost(); $validator = new ReviewValidator(); @@ -71,11 +74,12 @@ class Review extends Service if (isset($post['published'])) { $data['published'] = $validator->checkPublishStatus($post['published']); + $this->recountCourseReviews($course); } $review->update($data); - $this->updateCourseRating($review->course_id); + $this->updateCourseRating($course); return $review; } @@ -88,7 +92,9 @@ class Review extends Service $review->update(); - $this->decrCourseReviewCount($review->course_id); + $course = $this->findCourse($review->course_id); + + $this->recountCourseReviews($course); } public function restoreReview($id) @@ -99,7 +105,9 @@ class Review extends Service $review->update(); - $this->incrCourseReviewCount($review->course_id); + $course = $this->findCourse($review->course_id); + + $this->recountCourseReviews($course); } protected function findOrFail($id) @@ -109,33 +117,29 @@ class Review extends Service return $validator->checkReview($id); } - protected function incrCourseReviewCount($courseId) + protected function findCourse($id) { $courseRepo = new CourseRepo(); - $course = $courseRepo->findById($courseId); + return $courseRepo->findById($id); + } - $course->review_count -= 1; + protected function recountCourseReviews(CourseModel $course) + { + $courseRepo = new CourseRepo(); + + $reviewCount = $courseRepo->countReviews($course->id); + + $course->review_count = $reviewCount; $course->update(); } - protected function decrCourseReviewCount($courseId) - { - $courseRepo = new CourseRepo(); - - $course = $courseRepo->findById($courseId); - - $course->review_count += 1; - - $course->update(); - } - - protected function updateCourseRating($courseId) + protected function updateCourseRating(CourseModel $course) { $service = new CourseStatService(); - $service->updateRating($courseId); + $service->updateRating($course->id); } protected function handleReviews($pager) diff --git a/app/Http/Admin/Services/Tag.php b/app/Http/Admin/Services/Tag.php index b0a75ef5..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(); @@ -42,7 +42,7 @@ class Tag extends Service $tag = new TagModel(); $tag->name = $validator->checkName($post['name']); - $tag->published = $validator->checkPublishStatus($post['published']); + $tag->priority = $validator->checkPriority($post['priority']); $tag->create(); @@ -68,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/Services/User.php b/app/Http/Admin/Services/User.php index 7ebf83c9..ac95ca4e 100644 --- a/app/Http/Admin/Services/User.php +++ b/app/Http/Admin/Services/User.php @@ -10,6 +10,7 @@ use App\Models\Account as AccountModel; use App\Models\ImUser as ImUserModel; use App\Models\User as UserModel; use App\Repos\Account as AccountRepo; +use App\Repos\Online as OnlineRepo; use App\Repos\Role as RoleRepo; use App\Repos\User as UserRepo; use App\Validators\Account as AccountValidator; @@ -30,6 +31,25 @@ class User extends Service return $roleRepo->findAll(['deleted' => 0]); } + public function getOnlineLogs($id) + { + $user = $this->findOrFail($id); + + $pageQuery = new PaginateQuery(); + + $params = $pageQuery->getParams(); + + $params['user_id'] = $user->id; + + $sort = $pageQuery->getSort(); + $page = $pageQuery->getPage(); + $limit = $pageQuery->getLimit(); + + $onlineRepo = new OnlineRepo(); + + return $onlineRepo->paginate($params, $sort, $page, $limit); + } + public function getUsers() { $pageQuery = new PaginateQuery(); diff --git a/app/Http/Admin/Views/answer/add.volt b/app/Http/Admin/Views/answer/add.volt new file mode 100644 index 00000000..4b6a5dcf --- /dev/null +++ b/app/Http/Admin/Views/answer/add.volt @@ -0,0 +1,41 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + +
+ +{% endblock %} + +{% block link_css %} + + {{ css_link('https://cdn.jsdelivr.net/npm/vditor/dist/index.css', false) }} + +{% endblock %} + +{% block include_js %} + + {{ js_include('https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js', false) }} + {{ js_include('admin/js/vditor.js') }} + +{% endblock %} \ No newline at end of file diff --git a/app/Http/Admin/Views/answer/edit.volt b/app/Http/Admin/Views/answer/edit.volt new file mode 100644 index 00000000..d11df81c --- /dev/null +++ b/app/Http/Admin/Views/answer/edit.volt @@ -0,0 +1,40 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + + + +{% endblock %} + +{% block link_css %} + + {{ css_link('https://cdn.jsdelivr.net/npm/vditor/dist/index.css', false) }} + +{% endblock %} + +{% block include_js %} + + {{ js_include('https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js', false) }} + {{ js_include('admin/js/vditor.js') }} + +{% endblock %} \ No newline at end of file diff --git a/app/Http/Admin/Views/answer/list.volt b/app/Http/Admin/Views/answer/list.volt new file mode 100644 index 00000000..697a5f19 --- /dev/null +++ b/app/Http/Admin/Views/answer/list.volt @@ -0,0 +1,64 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + + {{ partial('macros/answer') }} + +信息 | +评论 | +点赞 | +状态 | +操作 | +
---|---|---|---|---|
+
+ 回答:{{ item.summary }} +作者:{{ item.owner.name }} 创建:{{ date('Y-m-d',item.create_time) }} + |
+ {{ item.comment_count }} | +{{ item.like_count }} | +{{ publish_status(item.published) }} | ++ + | +