diff --git a/.gitignore b/.gitignore index bf6d78e4..95099374 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /vendor /config/config.php /config/xs.course.ini +/config/xs.article.ini /config/xs.group.ini /config/xs.user.ini /config/alipay/*.crt diff --git a/CHANGELOG.md b/CHANGELOG.md index cb5bebab..2a68ff21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +### [v1.3.1](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.1)(2021-04-09) + +### 更新 + +- 后台增加文章功能 +- 后台增加标签功能 +- 增加文章全文检索 +- 整理命名空间别名 +- 更新部分链接打开方式 +- xm-select搜索忽略大小写 +- 补充遗漏的面授模型章节相关迁移文件 +- 修正上次字段整理导致的字段不存在问题 +- 修正上次整理发布字段导致的添加单页和帮助错误 + ### [v1.3.0](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.0)(2021-03-26) ### 更新 diff --git a/app/Builders/ArticleFavoriteList.php b/app/Builders/ArticleFavoriteList.php new file mode 100644 index 00000000..20e856a6 --- /dev/null +++ b/app/Builders/ArticleFavoriteList.php @@ -0,0 +1,78 @@ +getArticles($relations); + + foreach ($relations as $key => $value) { + $relations[$key]['article'] = $articles[$value['article_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 getArticles(array $relations) + { + $ids = kg_array_column($relations, 'article_id'); + + $articleRepo = new ArticleRepo(); + + $columns = [ + 'id', 'title', 'cover', + 'view_count', 'like_count', 'comment_count', 'favorite_count', + ]; + + $articles = $articleRepo->findByIds($ids, $columns); + + $baseUrl = kg_cos_url(); + + $result = []; + + foreach ($articles->toArray() as $article) { + $article['cover'] = $baseUrl . $article['cover']; + $result[$article['id']] = $article; + } + + 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/ArticleList.php b/app/Builders/ArticleList.php new file mode 100644 index 00000000..be639665 --- /dev/null +++ b/app/Builders/ArticleList.php @@ -0,0 +1,83 @@ + $article) { + $articles[$key]['tags'] = json_decode($article['tags'], true); + } + + return $articles; + } + + 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 $articles) + { + $users = $this->getUsers($articles); + + foreach ($articles as $key => $article) { + $articles[$key]['owner'] = $users[$article['owner_id']] ?? new \stdClass(); + } + + return $articles; + } + + public function getCategories() + { + $cache = new CategoryListCache(); + + $items = $cache->get(CategoryModel::TYPE_ARTICLE); + + if (empty($items)) return []; + + $result = []; + + foreach ($items as $item) { + $result[$item['id']] = [ + 'id' => $item['id'], + 'name' => $item['name'], + ]; + } + + return $result; + } + + public function getUsers($articles) + { + $ids = kg_array_column($articles, '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/CommentList.php b/app/Builders/CommentList.php new file mode 100644 index 00000000..562629b4 --- /dev/null +++ b/app/Builders/CommentList.php @@ -0,0 +1,44 @@ +getUsers($comments); + + foreach ($comments as $key => $comment) { + $comments[$key]['owner'] = $users[$comment['owner_id']] ?? new \stdClass(); + $comments[$key]['to_user'] = $users[$comment['to_user_id']] ?? new \stdClass(); + } + + return $comments; + } + + public function getUsers(array $comments) + { + $ownerIds = kg_array_column($comments, 'owner_id'); + $toUserIds = kg_array_column($comments, 'to_user_id'); + $ids = array_merge($ownerIds, $toUserIds); + + $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/CourseList.php b/app/Builders/CourseList.php index 74f4430b..d2bcb60b 100644 --- a/app/Builders/CourseList.php +++ b/app/Builders/CourseList.php @@ -37,9 +37,7 @@ class CourseList extends Builder $items = $cache->get(CategoryModel::TYPE_COURSE); - if (empty($items)) { - return []; - } + if (empty($items)) return []; $result = []; diff --git a/app/Caches/Article.php b/app/Caches/Article.php new file mode 100644 index 00000000..1deb93e9 --- /dev/null +++ b/app/Caches/Article.php @@ -0,0 +1,31 @@ +lifetime; + } + + public function getKey($id = null) + { + return "article:{$id}"; + } + + public function getContent($id = null) + { + $articleRepo = new ArticleRepo(); + + $article = $articleRepo->findById($id); + + return $article ?: null; + } + +} diff --git a/app/Caches/ArticleHotAuthorList.php b/app/Caches/ArticleHotAuthorList.php new file mode 100644 index 00000000..ff4440be --- /dev/null +++ b/app/Caches/ArticleHotAuthorList.php @@ -0,0 +1,103 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'article_hot_author_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/Caches/ArticleRelatedList.php b/app/Caches/ArticleRelatedList.php new file mode 100644 index 00000000..de602a27 --- /dev/null +++ b/app/Caches/ArticleRelatedList.php @@ -0,0 +1,81 @@ +lifetime; + } + + public function getKey($id = null) + { + return "article_related_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], + 'published' => 1, + ]; + + $pager = $articleRepo->paginate($where); + + if ($pager->total_items == 0) return []; + + return $this->handleContent($pager->items); + } + + /** + * @param ArticleModel[] $articles + * @return array + */ + public function handleContent($articles) + { + $result = []; + + $count = 0; + + foreach ($articles as $article) { + if ($article->id != $this->articleId && $count < $this->limit) { + $result[] = [ + 'id' => $article->id, + 'title' => $article->title, + 'cover' => $article->cover, + 'view_count' => $article->view_count, + 'like_count' => $article->like_count, + 'comment_count' => $article->comment_count, + 'favorite_count' => $article->favorite_count, + ]; + $count++; + } + } + + return $result; + } + +} diff --git a/app/Caches/MaxArticleId.php b/app/Caches/MaxArticleId.php new file mode 100644 index 00000000..7fa89590 --- /dev/null +++ b/app/Caches/MaxArticleId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_article_id'; + } + + public function getContent($id = null) + { + $article = ArticleModel::findFirst(['order' => 'id DESC']); + + return $article->id ?? 0; + } + +} diff --git a/app/Caches/MaxCommentId.php b/app/Caches/MaxCommentId.php new file mode 100644 index 00000000..92c3c96d --- /dev/null +++ b/app/Caches/MaxCommentId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_comment_id'; + } + + public function getContent($id = null) + { + $comment = CommentModel::findFirst(['order' => 'id DESC']); + + return $comment->id ?? 0; + } + +} diff --git a/app/Caches/MaxTagId.php b/app/Caches/MaxTagId.php new file mode 100644 index 00000000..8faf25e3 --- /dev/null +++ b/app/Caches/MaxTagId.php @@ -0,0 +1,29 @@ +lifetime; + } + + public function getKey($id = null) + { + return 'max_tag_id'; + } + + public function getContent($id = null) + { + $tag = TagModel::findFirst(['order' => 'id DESC']); + + return $tag->id ?? 0; + } + +} diff --git a/app/Caches/Tag.php b/app/Caches/Tag.php new file mode 100644 index 00000000..58ba0922 --- /dev/null +++ b/app/Caches/Tag.php @@ -0,0 +1,31 @@ +lifetime; + } + + public function getKey($id = null) + { + return "tag:{$id}"; + } + + public function getContent($id = null) + { + $tagRepo = new TagRepo(); + + $tag = $tagRepo->findById($id); + + return $tag ?: null; + } + +} diff --git a/app/Caches/UserDailyCounter.php b/app/Caches/UserDailyCounter.php index 3382ace4..6af20260 100644 --- a/app/Caches/UserDailyCounter.php +++ b/app/Caches/UserDailyCounter.php @@ -28,6 +28,8 @@ class UserDailyCounter extends Counter 'chapter_like_count' => 0, 'consult_like_count' => 0, 'review_like_count' => 0, + 'article_like_count' => 0, + 'comment_like_count' => 0, ]; } diff --git a/app/Console/Tasks/ArticleIndexTask.php b/app/Console/Tasks/ArticleIndexTask.php new file mode 100644 index 00000000..addf2e4f --- /dev/null +++ b/app/Console/Tasks/ArticleIndexTask.php @@ -0,0 +1,125 @@ +searchArticles($query); + + var_export($result); + } + + /** + * 清空索引 + * + * @command: php console.php article_index clean + */ + public function cleanAction() + { + $this->cleanArticleIndex(); + } + + /** + * 重建索引 + * + * @command: php console.php article_index rebuild + */ + public function rebuildAction() + { + $this->rebuildArticleIndex(); + } + + /** + * 清空索引 + */ + protected function cleanArticleIndex() + { + $handler = new ArticleSearcher(); + + $index = $handler->getXS()->getIndex(); + + echo '------ start clean article index ------' . PHP_EOL; + + $index->clean(); + + echo '------ end clean article index ------' . PHP_EOL; + } + + /** + * 重建索引 + */ + protected function rebuildArticleIndex() + { + $articles = $this->findArticles(); + + if ($articles->count() == 0) return; + + $handler = new ArticleSearcher(); + + $documenter = new ArticleDocument(); + + $index = $handler->getXS()->getIndex(); + + echo '------ start rebuild article index ------' . PHP_EOL; + + $index->beginRebuild(); + + foreach ($articles as $article) { + $document = $documenter->setDocument($article); + $index->add($document); + } + + $index->endRebuild(); + + echo '------ end rebuild article index ------' . PHP_EOL; + } + + /** + * 搜索文章 + * + * @param string $query + * @return array + * @throws \XSException + */ + protected function searchArticles($query) + { + $handler = new ArticleSearcher(); + + return $handler->search($query); + } + + /** + * 查找文章 + * + * @return ResultsetInterface|Resultset|ArticleModel[] + */ + protected function findArticles() + { + return ArticleModel::query() + ->where('published = 1') + ->execute(); + } + +} diff --git a/app/Console/Tasks/SitemapTask.php b/app/Console/Tasks/SitemapTask.php index 712e0c7a..ad686103 100644 --- a/app/Console/Tasks/SitemapTask.php +++ b/app/Console/Tasks/SitemapTask.php @@ -3,6 +3,7 @@ namespace App\Console\Tasks; use App\Library\Sitemap; +use App\Models\Article as ArticleModel; use App\Models\Course as CourseModel; use App\Models\Help as HelpModel; use App\Models\ImGroup as ImGroupModel; @@ -35,6 +36,7 @@ class SitemapTask extends Task $this->addIndex(); $this->addCourses(); + $this->addArticles(); $this->addTeachers(); $this->addTopics(); $this->addImGroups(); @@ -74,6 +76,21 @@ class SitemapTask extends Task } } + protected function addArticles() + { + /** + * @var Resultset|ArticleModel[] $articles + */ + $articles = ArticleModel::query()->where('published = 1')->execute(); + + if ($articles->count() == 0) return; + + foreach ($articles as $article) { + $loc = sprintf('%s/article/%s', $this->siteUrl, $article->id); + $this->sitemap->addItem($loc, 0.8); + } + } + protected function addTeachers() { /** diff --git a/app/Console/Tasks/SyncArticleIndexTask.php b/app/Console/Tasks/SyncArticleIndexTask.php new file mode 100644 index 00000000..ace75ba4 --- /dev/null +++ b/app/Console/Tasks/SyncArticleIndexTask.php @@ -0,0 +1,60 @@ +getRedis(); + + $key = $this->getSyncKey(); + + $articleIds = $redis->sRandMember($key, 1000); + + if (!$articleIds) return; + + $articleRepo = new ArticleRepo(); + + $articles = $articleRepo->findByIds($articleIds); + + if ($articles->count() == 0) return; + + $document = new ArticleDocument(); + + $handler = new ArticleSearcher(); + + $index = $handler->getXS()->getIndex(); + + $index->openBuffer(); + + foreach ($articles as $article) { + + $doc = $document->setDocument($article); + + if ($article->published == 1) { + $index->update($doc); + } else { + $index->del($article->id); + } + } + + $index->closeBuffer(); + + $redis->sRem($key, ...$articleIds); + } + + protected function getSyncKey() + { + $sync = new ArticleIndexSync(); + + return $sync->getSyncKey(); + } + +} diff --git a/app/Http/Admin/Controllers/ArticleController.php b/app/Http/Admin/Controllers/ArticleController.php new file mode 100644 index 00000000..14a3b3af --- /dev/null +++ b/app/Http/Admin/Controllers/ArticleController.php @@ -0,0 +1,155 @@ +url->get( + ['for' => 'admin.category.list'], + ['type' => CategoryModel::TYPE_ARTICLE] + ); + + $this->response->redirect($location); + } + + /** + * @Get("/search", name="admin.article.search") + */ + public function searchAction() + { + $articleService = new ArticleService(); + + $sourceTypes = $articleService->getSourceTypes(); + $categories = $articleService->getCategories(); + $xmTags = $articleService->getXmTags(0); + + $this->view->setVar('source_types', $sourceTypes); + $this->view->setVar('categories', $categories); + $this->view->setVar('xm_tags', $xmTags); + } + + /** + * @Get("/list", name="admin.article.list") + */ + public function listAction() + { + $articleService = new ArticleService(); + + $pager = $articleService->getArticles(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/add", name="admin.article.add") + */ + public function addAction() + { + $articleService = new ArticleService(); + + $categories = $articleService->getCategories(); + + $this->view->setVar('categories', $categories); + } + + /** + * @Post("/create", name="admin.article.create") + */ + public function createAction() + { + $articleService = new ArticleService(); + + $article = $articleService->createArticle(); + + $location = $this->url->get([ + 'for' => 'admin.article.edit', + 'id' => $article->id, + ]); + + $content = [ + 'location' => $location, + 'msg' => '创建文章成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Get("/{id:[0-9]+}/edit", name="admin.article.edit") + */ + public function editAction($id) + { + $articleService = new ArticleService(); + + $sourceTypes = $articleService->getSourceTypes(); + $categories = $articleService->getCategories(); + $article = $articleService->getArticle($id); + $xmTags = $articleService->getXmTags($id); + + $this->view->setVar('source_types', $sourceTypes); + $this->view->setVar('categories', $categories); + $this->view->setVar('article', $article); + $this->view->setVar('xm_tags', $xmTags); + } + + /** + * @Post("/{id:[0-9]+}/update", name="admin.article.update") + */ + public function updateAction($id) + { + $articleService = new ArticleService(); + + $articleService->updateArticle($id); + + $content = ['msg' => '更新文章成功']; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/delete", name="admin.article.delete") + */ + public function deleteAction($id) + { + $articleService = new ArticleService(); + + $articleService->deleteArticle($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除文章成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/restore", name="admin.article.restore") + */ + public function restoreAction($id) + { + $articleService = new ArticleService(); + + $articleService->restoreArticle($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原文章成功', + ]; + + return $this->jsonSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/CommentController.php b/app/Http/Admin/Controllers/CommentController.php new file mode 100644 index 00000000..c67e875e --- /dev/null +++ b/app/Http/Admin/Controllers/CommentController.php @@ -0,0 +1,81 @@ +getComments(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Post("/{id:[0-9]+}/update", name="admin.comment.update") + */ + public function updateAction($id) + { + $commentService = new CommentService(); + + $commentService->updateComment($id); + + $content = ['msg' => '更新评论成功']; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/delete", name="admin.comment.delete") + */ + public function deleteAction($id) + { + $commentService = new CommentService(); + + $commentService->deleteComment($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '删除评论成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/restore", name="admin.comment.restore") + */ + public function restoreAction($id) + { + $commentService = new CommentService(); + + $commentService->restoreComment($id); + + $content = [ + 'location' => $this->request->getHTTPReferer(), + 'msg' => '还原评论成功', + ]; + + return $this->jsonSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/Controller.php b/app/Http/Admin/Controllers/Controller.php index d62078aa..bed99de6 100644 --- a/app/Http/Admin/Controllers/Controller.php +++ b/app/Http/Admin/Controllers/Controller.php @@ -58,7 +58,7 @@ class Controller extends \Phalcon\Mvc\Controller * 特例白名单 */ $whitelist = [ - 'controllers' => ['public', 'index', 'vod', 'upload', 'test'], + 'controllers' => ['public', 'index', 'upload', 'test'], 'routes' => [], ]; diff --git a/app/Http/Admin/Controllers/PublicController.php b/app/Http/Admin/Controllers/PublicController.php index e5b84bbc..7db202ed 100644 --- a/app/Http/Admin/Controllers/PublicController.php +++ b/app/Http/Admin/Controllers/PublicController.php @@ -50,4 +50,17 @@ class PublicController extends \Phalcon\Mvc\Controller $this->view->setVar('region', $region); } + /** + * @Get("/vod/player", name="admin.vod_player") + */ + public function vodPlayerAction() + { + $chapterId = $this->request->getQuery('chapter_id', 'int'); + $playUrl = $this->request->getQuery('play_url', 'string'); + + $this->view->pick('public/vod_player'); + $this->view->setVar('chapter_id', $chapterId); + $this->view->setVar('play_url', $playUrl); + } + } diff --git a/app/Http/Admin/Controllers/TagController.php b/app/Http/Admin/Controllers/TagController.php new file mode 100644 index 00000000..9979134a --- /dev/null +++ b/app/Http/Admin/Controllers/TagController.php @@ -0,0 +1,132 @@ +getTags(); + + $this->view->setVar('pager', $pager); + } + + /** + * @Get("/search", name="admin.tag.search") + */ + public function searchAction() + { + + } + + /** + * @Get("/add", name="admin.tag.add") + */ + public function addAction() + { + + } + + /** + * @Get("/{id:[0-9]+}/edit", name="admin.tag.edit") + */ + public function editAction($id) + { + $tagService = new TagService; + + $tag2 = $tagService->getTag($id); + + /** + * 注意:"tag"变量被volt引擎内置占用,另取名字避免冲突 + */ + $this->view->setVar('tag2', $tag2); + } + + /** + * @Post("/create", name="admin.tag.create") + */ + public function createAction() + { + $tagService = new TagService(); + + $tagService->createTag(); + + $location = $this->url->get(['for' => 'admin.tag.list']); + + $content = [ + 'location' => $location, + 'msg' => '创建标签成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/update", name="admin.tag.update") + */ + public function updateAction($id) + { + $tagService = new TagService(); + + $tagService->updateTag($id); + + $location = $this->url->get(['for' => 'admin.tag.list']); + + $content = [ + 'location' => $location, + 'msg' => '更新标签成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/delete", name="admin.tag.delete") + */ + public function deleteAction($id) + { + $tagService = new TagService(); + + $tagService->deleteTag($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '删除标签成功', + ]; + + return $this->jsonSuccess($content); + } + + /** + * @Post("/{id:[0-9]+}/restore", name="admin.tag.restore") + */ + public function restoreAction($id) + { + $tagService = new TagService(); + + $tagService->restoreTag($id); + + $location = $this->request->getHTTPReferer(); + + $content = [ + 'location' => $location, + 'msg' => '还原标签成功', + ]; + + return $this->jsonSuccess($content); + } + +} diff --git a/app/Http/Admin/Controllers/UploadController.php b/app/Http/Admin/Controllers/UploadController.php index a6acddf0..25edb530 100644 --- a/app/Http/Admin/Controllers/UploadController.php +++ b/app/Http/Admin/Controllers/UploadController.php @@ -3,6 +3,7 @@ namespace App\Http\Admin\Controllers; use App\Services\MyStorage as StorageService; +use App\Services\Vod as VodService; /** * @RoutePrefix("/admin/upload") @@ -126,8 +127,10 @@ 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['group_cover'] = $service->uploadDefaultPackageCover(); + $items['package_cover'] = $service->uploadDefaultPackageCover(); + $items['gift_cover'] = $service->uploadDefaultGiftCover(); $items['vip_cover'] = $service->uploadDefaultVipCover(); foreach ($items as $item) { @@ -138,9 +141,9 @@ class UploadController extends Controller } /** - * @Get("/sign", name="admin.upload.sign") + * @Post("/credentials", name="admin.upload.credentials") */ - public function signatureAction() + public function credentialsAction() { $service = new StorageService(); @@ -155,4 +158,16 @@ class UploadController extends Controller return $this->jsonSuccess($data); } + /** + * @Post("/vod/sign", name="admin.upload.vod_sign") + */ + public function vodSignatureAction() + { + $service = new VodService(); + + $sign = $service->getUploadSignature(); + + return $this->jsonSuccess(['sign' => $sign]); + } + } diff --git a/app/Http/Admin/Controllers/VodController.php b/app/Http/Admin/Controllers/VodController.php deleted file mode 100644 index 4ee0f40b..00000000 --- a/app/Http/Admin/Controllers/VodController.php +++ /dev/null @@ -1,38 +0,0 @@ -getUploadSignature(); - - return $this->jsonSuccess(['sign' => $sign]); - } - - /** - * @Get("/player", name="admin.vod.player") - */ - public function playerAction() - { - $chapterId = $this->request->getQuery('chapter_id', 'int'); - $playUrl = $this->request->getQuery('play_url', 'string'); - - $this->view->pick('public/vod_player'); - $this->view->setVar('chapter_id', $chapterId); - $this->view->setVar('play_url', $playUrl); - } - -} diff --git a/app/Http/Admin/Services/Article.php b/app/Http/Admin/Services/Article.php new file mode 100644 index 00000000..8c4dabdd --- /dev/null +++ b/app/Http/Admin/Services/Article.php @@ -0,0 +1,287 @@ +findAll(['published' => 1], 'priority'); + + if ($allTags->count() == 0) return []; + + $articleTagIds = []; + + if ($id > 0) { + $article = $this->findOrFail($id); + if (!empty($article->tags)) { + $articleTagIds = kg_array_column($article->tags, 'id'); + } + } + + $list = []; + + foreach ($allTags as $tag) { + $selected = in_array($tag->id, $articleTagIds); + $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 getSourceTypes() + { + return ArticleModel::sourceTypes(); + } + + public function getArticles() + { + $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(); + + $articleRepo = new ArticleRepo(); + + $pager = $articleRepo->paginate($params, $sort, $page, $limit); + + return $this->handleArticles($pager); + } + + public function getArticle($id) + { + return $this->findOrFail($id); + } + + public function createArticle() + { + $post = $this->request->getPost(); + + $loginUser = $this->getLoginUser(); + + $validator = new ArticleValidator(); + + $category = $validator->checkCategory($post['category_id']); + $title = $validator->checkTitle($post['title']); + + $article = new ArticleModel(); + + $article->owner_id = $loginUser->id; + $article->category_id = $category->id; + $article->title = $title; + + $article->create(); + + return $article; + } + + public function updateArticle($id) + { + $post = $this->request->getPost(); + + $article = $this->findOrFail($id); + + $validator = new ArticleValidator(); + + $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['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']); + } + + if (isset($post['source_type'])) { + $data['source_type'] = $validator->checkSourceType($post['source_type']); + if ($post['source_type'] != ArticleModel::SOURCE_ORIGIN) { + $data['source_url'] = $validator->checkSourceUrl($post['source_url']); + } + } + + if (isset($post['allow_comment'])) { + $data['allow_comment'] = $post['allow_comment']; + } + + if (isset($post['featured'])) { + $data['featured'] = $validator->checkFeatureStatus($post['featured']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + if (isset($post['xm_tag_ids'])) { + $this->saveTags($article, $post['xm_tag_ids']); + } + + $article->update($data); + + return $article; + } + + public function deleteArticle($id) + { + $article = $this->findOrFail($id); + $article->deleted = 1; + $article->update(); + + return $article; + } + + public function restoreArticle($id) + { + $article = $this->findOrFail($id); + $article->deleted = 0; + $article->update(); + + return $article; + } + + protected function findOrFail($id) + { + $validator = new ArticleValidator(); + + return $validator->checkArticle($id); + } + + protected function rebuildArticleCache(ArticleModel $article) + { + $cache = new ArticleCache(); + + $cache->rebuild($article->id); + } + + protected function rebuildArticleIndex(ArticleModel $article) + { + $sync = new ArticleIndexSync(); + + $sync->addItem($article->id); + } + + protected function saveTags(ArticleModel $article, $tagIds) + { + $originTagIds = []; + + 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(); + } + + protected function handleArticles($pager) + { + if ($pager->total_items > 0) { + + $builder = new ArticleListBuilder(); + + $items = $pager->items->toArray(); + + $pipeA = $builder->handleArticles($items); + $pipeB = $builder->handleCategories($pipeA); + $pipeC = $builder->handleUsers($pipeB); + $pipeD = $builder->objects($pipeC); + + $pager->items = $pipeD; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/AuthNode.php b/app/Http/Admin/Services/AuthNode.php index 646c0dec..4e260dc9 100644 --- a/app/Http/Admin/Services/AuthNode.php +++ b/app/Http/Admin/Services/AuthNode.php @@ -243,6 +243,117 @@ class AuthNode extends Service ], ], ], + [ + 'id' => '1-7', + 'title' => '文章管理', + 'type' => 'menu', + 'children' => [ + [ + 'id' => '1-7-1', + 'title' => '文章列表', + 'type' => 'menu', + 'route' => 'admin.article.list', + ], + [ + 'id' => '1-7-2', + 'title' => '搜索文章', + 'type' => 'menu', + 'route' => 'admin.article.search', + ], + [ + 'id' => '1-7-3', + 'title' => '添加文章', + 'type' => 'menu', + 'route' => 'admin.article.add', + ], + [ + 'id' => '1-7-4', + 'title' => '编辑文章', + 'type' => 'button', + 'route' => 'admin.article.edit', + ], + [ + 'id' => '1-7-5', + 'title' => '删除文章', + 'type' => 'button', + 'route' => 'admin.article.delete', + ], + [ + 'id' => '1-7-6', + 'title' => '文章分类', + 'type' => 'menu', + 'route' => 'admin.article.category', + ], + ], + ], + [ + 'id' => '1-8', + 'title' => '标签管理', + 'type' => 'menu', + 'children' => [ + [ + 'id' => '1-8-1', + 'title' => '标签列表', + 'type' => 'menu', + 'route' => 'admin.tag.list', + ], + [ + 'id' => '1-8-2', + 'title' => '搜索标签', + 'type' => 'menu', + 'route' => 'admin.tag.search', + ], + [ + 'id' => '1-8-3', + 'title' => '添加标签', + 'type' => 'menu', + 'route' => 'admin.tag.add', + ], + [ + 'id' => '1-8-4', + 'title' => '编辑标签', + 'type' => 'button', + 'route' => 'admin.tag.edit', + ], + [ + 'id' => '1-8-5', + 'title' => '删除标签', + 'type' => 'button', + 'route' => 'admin.tag.delete', + ], + ], + ], + [ + 'id' => '1-9', + 'title' => '评论管理', + 'type' => 'button', + 'children' => [ + [ + 'id' => '1-9-1', + 'title' => '评论列表', + 'type' => 'button', + 'route' => 'admin.comment.list', + ], + [ + 'id' => '1-9-2', + 'title' => '搜索评论', + 'type' => 'button', + 'route' => 'admin.comment.search', + ], + [ + 'id' => '1-9-3', + 'title' => '编辑评论', + 'type' => 'button', + 'route' => 'admin.comment.edit', + ], + [ + 'id' => '1-9-4', + 'title' => '删除评论', + 'type' => 'button', + 'route' => 'admin.comment.delete', + ], + ], + ], ], ]; } diff --git a/app/Http/Admin/Services/Category.php b/app/Http/Admin/Services/Category.php index af6087ca..db7fe45b 100644 --- a/app/Http/Admin/Services/Category.php +++ b/app/Http/Admin/Services/Category.php @@ -74,7 +74,6 @@ class Category extends Service $data['type'] = $validator->checkType($post['type']); $data['name'] = $validator->checkName($post['name']); $data['priority'] = $validator->checkPriority($post['priority']); - $data['published'] = $validator->checkPublishStatus($post['published']); $category = new CategoryModel(); diff --git a/app/Http/Admin/Services/Comment.php b/app/Http/Admin/Services/Comment.php new file mode 100644 index 00000000..1410587a --- /dev/null +++ b/app/Http/Admin/Services/Comment.php @@ -0,0 +1,105 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = $pagerQuery->getSort(); + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $commentRepo = new CommentRepo(); + + $pager = $commentRepo->paginate($params, $sort, $page, $limit); + + return $this->handleComments($pager); + } + + public function getComment($id) + { + return $this->findOrFail($id); + } + + public function updateComment($id) + { + $comment = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new CommentValidator(); + + $data = []; + + if (isset($post['content'])) { + $data['content'] = $validator->checkContent($post['content']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $comment->update($data); + + return $comment; + } + + public function deleteComment($id) + { + $page = $this->findOrFail($id); + + $page->deleted = 1; + + $page->update(); + + return $page; + } + + public function restoreComment($id) + { + $page = $this->findOrFail($id); + + $page->deleted = 0; + + $page->update(); + + return $page; + } + + protected function findOrFail($id) + { + $validator = new CommentValidator(); + + return $validator->checkComment($id); + } + + protected function handleComments($pager) + { + if ($pager->total_items > 0) { + + $builder = new CommentListBuilder(); + + $pipeA = $pager->items->toArray(); + $pipeB = $builder->handleUsers($pipeA); + $pipeC = $builder->objects($pipeB); + + $pager->items = $pipeC; + } + + return $pager; + } + +} diff --git a/app/Http/Admin/Services/Course.php b/app/Http/Admin/Services/Course.php index 12c858f4..b4bebaa6 100644 --- a/app/Http/Admin/Services/Course.php +++ b/app/Http/Admin/Services/Course.php @@ -38,13 +38,11 @@ class Course extends Service $params = $pagerQuery->getParams(); if (!empty($params['xm_category_ids'])) { - $xmCategoryIds = explode(',', $params['xm_category_ids']); - $params['category_id'] = count($xmCategoryIds) > 1 ? $xmCategoryIds : $xmCategoryIds[0]; + $params['category_id'] = explode(',', $params['xm_category_ids']); } if (!empty($params['xm_teacher_ids'])) { - $xmTeacherIds = explode(',', $params['xm_teacher_ids']); - $params['teacher_id'] = count($xmTeacherIds) > 1 ? $xmTeacherIds : $xmTeacherIds[0]; + $params['teacher_id'] = explode(',', $params['xm_teacher_ids']); } $params['deleted'] = $params['deleted'] ?? 0; @@ -288,7 +286,7 @@ class Course extends Service $allCategories = $categoryRepo->findAll([ 'type' => CategoryModel::TYPE_COURSE, - 'deleted' => 0, + 'published' => 1, ]); if ($allCategories->count() == 0) return []; @@ -456,12 +454,11 @@ class Course extends Service if ($addedTeacherIds) { foreach ($addedTeacherIds as $teacherId) { $courseTeacher = new CourseUserModel(); - $courseTeacher->create([ - 'course_id' => $course->id, - 'user_id' => $teacherId, - 'role_type' => CourseUserModel::ROLE_TEACHER, - 'source_type' => CourseUserModel::SOURCE_IMPORT, - ]); + $courseTeacher->course_id = $course->id; + $courseTeacher->user_id = $teacherId; + $courseTeacher->role_type = CourseUserModel::ROLE_TEACHER; + $courseTeacher->source_type = CourseUserModel::SOURCE_IMPORT; + $courseTeacher->create(); } } @@ -509,10 +506,9 @@ class Course extends Service if ($addedCategoryIds) { foreach ($addedCategoryIds as $categoryId) { $courseCategory = new CourseCategoryModel(); - $courseCategory->create([ - 'course_id' => $course->id, - 'category_id' => $categoryId, - ]); + $courseCategory->course_id = $course->id; + $courseCategory->category_id = $categoryId; + $courseCategory->create(); } } @@ -568,18 +564,16 @@ class Course extends Service $record = $courseRelatedRepo->findCourseRelated($course->id, $relatedId); if (!$record) { $courseRelated = new CourseRelatedModel(); - $courseRelated->create([ - 'course_id' => $course->id, - 'related_id' => $relatedId, - ]); + $courseRelated->course_id = $course->id; + $courseRelated->related_id = $relatedId; + $courseRelated->create(); } $record = $courseRelatedRepo->findCourseRelated($relatedId, $course->id); if (!$record) { $courseRelated = new CourseRelatedModel(); - $courseRelated->create([ - 'course_id' => $relatedId, - 'related_id' => $course->id, - ]); + $courseRelated->course_id = $relatedId; + $courseRelated->related_id = $course->id; + $courseRelated->create(); } } } diff --git a/app/Http/Admin/Services/Help.php b/app/Http/Admin/Services/Help.php index b29d8722..9a2ee406 100644 --- a/app/Http/Admin/Services/Help.php +++ b/app/Http/Admin/Services/Help.php @@ -64,7 +64,6 @@ class Help extends Service $data['title'] = $validator->checkTitle($post['title']); $data['content'] = $validator->checkContent($post['content']); $data['priority'] = $validator->checkPriority($post['priority']); - $data['published'] = $validator->checkPublishStatus($post['published']); $data['category_id'] = $category->id; $help = new HelpModel(); diff --git a/app/Http/Admin/Services/Nav.php b/app/Http/Admin/Services/Nav.php index fceb868d..a29436f9 100644 --- a/app/Http/Admin/Services/Nav.php +++ b/app/Http/Admin/Services/Nav.php @@ -72,7 +72,6 @@ class Nav extends Service $data['url'] = $validator->checkUrl($post['url']); $data['target'] = $validator->checkTarget($post['target']); $data['position'] = $validator->checkPosition($post['position']); - $data['published'] = $validator->checkPublishStatus($post['published']); $nav = new NavModel(); diff --git a/app/Http/Admin/Services/Page.php b/app/Http/Admin/Services/Page.php index 91f33750..b1df9f1b 100644 --- a/app/Http/Admin/Services/Page.php +++ b/app/Http/Admin/Services/Page.php @@ -43,7 +43,6 @@ class Page extends Service $data['title'] = $validator->checkTitle($post['title']); $data['content'] = $validator->checkContent($post['content']); - $data['published'] = $validator->checkPublishStatus($post['published']); $page = new PageModel(); @@ -109,11 +108,11 @@ class Page extends Service return $page; } - protected function rebuildPageCache(PageModel $help) + protected function rebuildPageCache(PageModel $page) { $cache = new PageCache(); - $cache->rebuild($help->id); + $cache->rebuild($page->id); } protected function findOrFail($id) diff --git a/app/Http/Admin/Services/Setting.php b/app/Http/Admin/Services/Setting.php index 927181c4..cd668a42 100644 --- a/app/Http/Admin/Services/Setting.php +++ b/app/Http/Admin/Services/Setting.php @@ -62,7 +62,7 @@ class Setting extends Service { $oa = $this->getSettings('wechat.oa'); - $oa['notify_url'] = $oa['notify_url'] ?: kg_full_url(['for' => 'home.wechat.oa.notify']); + $oa['notify_url'] = $oa['notify_url'] ?: kg_full_url(['for' => 'home.wechat_oa.notify']); $oa['menu'] = json_decode($oa['menu'], true); diff --git a/app/Http/Admin/Services/Student.php b/app/Http/Admin/Services/Student.php index 188693e8..a8e9ea5c 100644 --- a/app/Http/Admin/Services/Student.php +++ b/app/Http/Admin/Services/Student.php @@ -63,8 +63,6 @@ class Student extends Service $params = $pagerQuery->getParams(); - $params['deleted'] = 0; - $sort = $pagerQuery->getSort(); $page = $pagerQuery->getPage(); $limit = $pagerQuery->getLimit(); diff --git a/app/Http/Admin/Services/Tag.php b/app/Http/Admin/Services/Tag.php new file mode 100644 index 00000000..b0a75ef5 --- /dev/null +++ b/app/Http/Admin/Services/Tag.php @@ -0,0 +1,126 @@ +getParams(); + + $params['deleted'] = $params['deleted'] ?? 0; + + $sort = 'priority'; + $page = $pagerQuery->getPage(); + $limit = $pagerQuery->getLimit(); + + $tagRepo = new TagRepo(); + + return $tagRepo->paginate($params, $sort, $page, $limit); + } + + public function getTag($id) + { + return $this->findOrFail($id); + } + + public function createTag() + { + $post = $this->request->getPost(); + + $validator = new TagValidator(); + + $tag = new TagModel(); + + $tag->name = $validator->checkName($post['name']); + $tag->published = $validator->checkPublishStatus($post['published']); + + $tag->create(); + + $this->rebuildTagCache($tag); + + return $tag; + } + + public function updateTag($id) + { + $tag = $this->findOrFail($id); + + $post = $this->request->getPost(); + + $validator = new TagValidator(); + + $data = []; + + if (isset($post['name'])) { + $data['name'] = $validator->checkName($post['name']); + if ($data['name'] != $tag->name) { + $validator->checkIfNameExists($data['name']); + } + } + + if (isset($post['priority'])) { + $data['priority'] = $validator->checkPriority($post['priority']); + } + + if (isset($post['published'])) { + $data['published'] = $validator->checkPublishStatus($post['published']); + } + + $tag->update($data); + + $this->rebuildTagCache($tag); + + return $tag; + } + + public function deleteTag($id) + { + $tag = $this->findOrFail($id); + + $tag->deleted = 1; + + $tag->update(); + + $this->rebuildTagCache($tag); + + return $tag; + } + + public function restoreTag($id) + { + $tag = $this->findOrFail($id); + + $tag->deleted = 0; + + $tag->update(); + + $this->rebuildTagCache($tag); + + return $tag; + } + + protected function rebuildTagCache(TagModel $tag) + { + $cache = new TagCache(); + + $cache->rebuild($tag->id); + } + + protected function findOrFail($id) + { + $validator = new TagValidator(); + + return $validator->checkTag($id); + } + +} diff --git a/app/Http/Admin/Views/article/add.volt b/app/Http/Admin/Views/article/add.volt new file mode 100644 index 00000000..81e6d1e4 --- /dev/null +++ b/app/Http/Admin/Views/article/add.volt @@ -0,0 +1,35 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + +
+ +{% endblock %} diff --git a/app/Http/Admin/Views/article/edit.volt b/app/Http/Admin/Views/article/edit.volt new file mode 100644 index 00000000..9cfa6e0b --- /dev/null +++ b/app/Http/Admin/Views/article/edit.volt @@ -0,0 +1,74 @@ +{% extends 'templates/main.volt' %} + +{% block content %} + + + +文章 | +来源 | +作者 | +统计 | +推荐 | +评论 | +发布 | +操作 | +
---|---|---|---|---|---|---|---|
+ 标题:{{ item.title }}({{ item.id }}) + +创建:{{ date('Y-m-d H:i',item.create_time) }},更新:{{ date('Y-m-d H:i',item.update_time) }} + |
+ {{ source_info(item.source_type,item.source_url) }} | +
+ 昵称:{{ item.owner.name }} +编号:{{ item.owner.id }} + |
+
+ 浏览:{{ item.view_count }},评论:{{ item.comment_count }} +点赞:{{ item.like_count }},收藏:{{ item.favorite_count }} + |
+ + | + | + | + + | +
评论 | +用户 | +终端 | +发布 | +操作 | +
---|---|---|---|---|
+ 内容:{{ substr(item.content,0,30) }} +时间:{{ date('Y-m-d H:i',item.create_time) }},点赞:{{ item.like_count }} + |
+
+ 昵称:{{ item.owner.name }} +编号:{{ item.owner.id }} + |
+
+ 类型:{{ client_type(item.client_type) }} +地址:查看 + |
+ + | + {% if item.deleted == 0 %} + 删除 + {% else %} + 还原 + {% endif %} + | +