1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-07-15 21:02:20 +08:00

阶段提交

This commit is contained in:
koogua 2021-05-10 21:35:07 +08:00
parent caa34d2675
commit 661034f3c1
100 changed files with 2869 additions and 707 deletions

View File

@ -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) {
if (!empty($article['cover']) && !Text::startsWith($article['cover'], 'http')) {
$article['cover'] = $baseUrl . $article['cover'];
}
$result[$article['id']] = $article;
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Builders;
use App\Repos\Question as QuestionRepo;
use App\Repos\User as UserRepo;
use Phalcon\Text;
class QuestionFavoriteList extends Builder
{
public function handleQuestions(array $relations)
{
$questions = $this->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;
}
}

View File

@ -2,6 +2,8 @@
namespace App\Builders;
use App\Caches\CategoryList as CategoryListCache;
use App\Models\Category as CategoryModel;
use App\Repos\User as UserRepo;
class QuestionList extends Builder
@ -16,6 +18,17 @@ class QuestionList extends Builder
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);
@ -28,6 +41,26 @@ class QuestionList extends Builder
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');

View File

@ -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()
{
/**

View File

@ -0,0 +1,81 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Answer as AnswerService;
/**
* @RoutePrefix("/admin/answer")
*/
class AnswerController extends Controller
{
/**
* @Get("/search", name="admin.answer.search")
*/
public function searchAction()
{
}
/**
* @Get("/list", name="admin.answer.list")
*/
public function listAction()
{
$answerService = new AnswerService();
$pager = $answerService->getAnswers();
$this->view->setVar('pager', $pager);
}
/**
* @Post("/{id:[0-9]+}/update", name="admin.answer.update")
*/
public function updateAction($id)
{
$answerService = new AnswerService();
$answerService->updateAnswer($id);
$content = ['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);
}
}

View File

@ -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,15 +169,17 @@ 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();
if ($this->request->isPost()) {
$articleService->reviewArticle($id);
$location = $this->url->get(['for' => 'admin.article.pending_list']);
$location = $this->url->get(['for' => 'admin.mod.articles']);
$content = [
'location' => $location,
@ -202,4 +189,11 @@ class ArticleController extends Controller
return $this->jsonSuccess($content);
}
$rejectOptions = $articleService->getRejectOptions();
$article = $articleService->getArticle($id);
$this->view->setVar('reject_options', $rejectOptions);
$this->view->setVar('article', $article);
}
}

View File

@ -22,4 +22,16 @@ 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);
}
}

View File

@ -0,0 +1,195 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Question as QuestionService;
use App\Models\Category as CategoryModel;
/**
* @RoutePrefix("/admin/question")
*/
class QuestionController extends Controller
{
/**
* @Get("/category", name="admin.question.category")
*/
public function categoryAction()
{
$location = $this->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);
}
$rejectOptions = $questionService->getRejectOptions();
$question = $questionService->getQuestion($id);
$this->view->setVar('reject_options', $rejectOptions);
$this->view->setVar('question', $question);
}
}

View File

@ -148,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();

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Admin\Services;
use App\Builders\AnswerList as AnswerListBuilder;
use App\Library\Paginator\Query as PagerQuery;
use App\Repos\Answer as AnswerRepo;
use App\Validators\Answer as AnswerValidator;
class Answer extends Service
{
public function getAnswers()
{
$pagerQuery = new PagerQuery();
$params = $pagerQuery->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 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']);
}
$answer->update($data);
return $answer;
}
public function deleteAnswer($id)
{
$page = $this->findOrFail($id);
$page->deleted = 1;
$page->update();
return $page;
}
public function restoreAnswer($id)
{
$page = $this->findOrFail($id);
$page->deleted = 0;
$page->update();
return $page;
}
protected function findOrFail($id)
{
$validator = new AnswerValidator();
return $validator->checkAnswer($id);
}
protected function handleAnswers($pager)
{
if ($pager->total_items > 0) {
$builder = new AnswerListBuilder();
$pipeA = $pager->items->toArray();
$pipeB = $builder->handleUsers($pipeA);
$pipeC = $builder->objects($pipeB);
$pager->items = $pipeC;
}
return $pager;
}
}

View File

@ -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,14 +24,7 @@ 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)
{
@ -164,14 +156,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']);
@ -262,9 +246,12 @@ 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;
}
@ -309,61 +296,6 @@ class Article extends Service
return $validator->checkArticle($id);
}
protected function saveTags(ArticleModel $article, $tagIds)
{
$originTagIds = [];
/**
* 修改数据后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();
}
protected function handleArticles($pager)
{
if ($pager->total_items > 0) {

View File

@ -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,55 @@ 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-8',
'title' => '标签管理',
@ -387,6 +430,12 @@ class AuthNode extends Service
'type' => 'menu',
'route' => 'admin.mod.articles',
],
[
'id' => '2-10-2',
'title' => '问题列表',
'type' => 'menu',
'route' => 'admin.mod.questions',
],
],
],
[

View File

@ -3,9 +3,12 @@
namespace App\Http\Admin\Services;
use App\Builders\ArticleList as ArticleListBuilder;
use App\Builders\QuestionList as QuestionListBuilder;
use App\Library\Paginator\Query as PagerQuery;
use App\Models\Article as ArticleModel;
use App\Models\Question as QuestionModel;
use App\Repos\Article as ArticleRepo;
use App\Repos\Question as QuestionRepo;
class Moderation extends Service
{
@ -30,6 +33,26 @@ 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);
}
protected function handleArticles($pager)
{
if ($pager->total_items > 0) {
@ -49,4 +72,23 @@ 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;
}
}

View File

@ -0,0 +1,336 @@
<?php
namespace App\Http\Admin\Services;
use App\Builders\QuestionList as QuestionListBuilder;
use App\Caches\Question as QuestionCache;
use App\Library\Paginator\Query as PagerQuery;
use App\Models\Category as CategoryModel;
use App\Models\Question as QuestionModel;
use App\Models\Reason as ReasonModel;
use App\Models\User as UserModel;
use App\Repos\Category as CategoryRepo;
use App\Repos\Question as QuestionRepo;
use App\Repos\Tag as TagRepo;
use App\Repos\User as UserRepo;
use App\Services\Logic\Notice\System\QuestionApproved as QuestionApprovedNotice;
use App\Services\Logic\Notice\System\QuestionRejected as QuestionRejectedNotice;
use App\Services\Logic\Point\History\QuestionPost as QuestionPostPointHistory;
use App\Services\Logic\Question\QuestionDataTrait;
use App\Services\Sync\QuestionIndex as QuestionIndexSync;
use App\Validators\Question as QuestionValidator;
class Question extends Service
{
use QuestionDataTrait;
public function getXmTags($id)
{
$tagRepo = new TagRepo();
$allTags = $tagRepo->findAll(['published' => 1], 'priority');
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 getRejectOptions()
{
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->owner_id = $user->id;
$question->title = $title;
$question->create();
$this->incrUserQuestionCount($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']);
}
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->decrUserQuestionCount($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->incrUserQuestionCount($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();
$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 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 incrUserQuestionCount(UserModel $user)
{
$user->question_count += 1;
$user->update();
}
protected function decrUserQuestionCount(UserModel $user)
{
if ($user->question_count > 0) {
$user->question_count -= 1;
$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);
}
}

View File

@ -6,17 +6,6 @@
<fieldset class="layui-elem-field layui-field-title">
<legend>添加文章</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id" lay-verify="required">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}">{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">

View File

@ -33,7 +33,6 @@
{{ js_include('https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js', false) }}
{{ js_include('lib/xm-select.js') }}
{{ js_include('admin/js/cover.upload.js') }}
{{ js_include('admin/js/vditor.js') }}
{% endblock %}

View File

@ -1,33 +1,12 @@
{% set source_url_display = article.source_type == 1 ? 'display:none' : 'display:block' %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.update','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">封面</label>
<div class="layui-input-inline">
<img id="img-cover" class="kg-cover" src="{{ article.cover }}">
<input type="hidden" name="cover" value="{{ article.cover }}">
</div>
<div class="layui-input-inline" style="padding-top:35px;">
<button id="change-cover" class="layui-btn layui-btn-sm" type="button">更换</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" value="{{ article.title }}" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id" lay-verify="required">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}" {% if article.category_id == item.id %}selected="selected"{% endif %}>{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
@ -50,14 +29,6 @@
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布状态</label>
<div class="layui-input-block">
{% for value,title in publish_types %}
<input type="radio" name="published" value="{{ value }}" title="{{ title }}" {% if article.published == value %}checked="checked"{% endif %} lay-filter="source_type">
{% endfor %}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">允许评论</label>
<div class="layui-input-block">

View File

@ -1,20 +1,12 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.update','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">详情</label>
<div class="layui-input-block">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ article.content }}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">简介</label>
<div class="layui-input-block">
<textarea name="summary" class="layui-textarea">{{ article.summary }}</textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<div class="layui-input-block" style="margin:0;">
<button class="kg-submit layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>

View File

@ -52,6 +52,7 @@
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set preview_url = url({'for':'home.article.show','id':item.id}) %}
{% set review_url = url({'for':'admin.article.review','id':item.id}) %}
{% set edit_url = url({'for':'admin.article.edit','id':item.id}) %}
{% set update_url = url({'for':'admin.article.update','id':item.id}) %}
{% set delete_url = url({'for':'admin.article.delete','id':item.id}) %}
@ -85,7 +86,11 @@
<div class="layui-dropdown">
<button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button>
<ul>
{% if item.published == 1 %}
<li><a href="{{ review_url }}">审核文章</a></li>
{% elseif item.published == 2 %}
<li><a href="{{ preview_url }}" target="_blank">预览文章</a></li>
{% endif %}
<li><a href="{{ edit_url }}">编辑文章</a></li>
{% if item.deleted == 0 %}
<li><a href="javascript:" class="kg-delete" data-url="{{ delete_url }}">删除文章</a></li>

View File

@ -2,24 +2,27 @@
{% block content %}
{% set list_url = url({'for':'admin.article.pending_list'}) %}
<fieldset class="layui-elem-field layui-field-title">
<legend>审核内容</legend>
</fieldset>
<div class="kg-nav">
<div class="kg-nav-left">
<span class="layui-breadcrumb">
<a href="{{ list_url }}">审核列表</a>
<a><cite>{{ article.title }}</cite></a>
</span>
<div class="kg-mod-preview">
<div class="title">{{ article.title }}</div>
<div class="content markdown-body">{{ article.content }}</div>
{% if article.tags %}
<div class="tags">
{% for item in article.tags %}
<span class="layui-btn layui-btn-xs">{{ item['name'] }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.article.review','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">内容</label>
<div class="layui-input-block">
<div class="content markdown-body">{{ article.content|parse_markdown }}</div>
</div>
</div>
<fieldset class="layui-elem-field layui-field-title">
<legend>审核意见</legend>
</fieldset>
<form class="layui-form kg-form kg-review-form" method="POST" action="{{ url({'for':'admin.article.review','id':article.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">审核</label>
<div class="layui-input-block">

View File

@ -24,17 +24,6 @@
<input class="layui-input" type="text" name="title" placeholder="标题模糊匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">分类</label>
<div class="layui-input-block">
<select name="category_id">
<option value="">请选择</option>
{% for item in categories %}
<option value="{{ item.id }}">{{ item.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">

View File

@ -40,20 +40,15 @@
</thead>
<tbody>
{% for item in pager.items %}
{% set list_by_id_url = url({'for':'admin.audit.list'},{'user_id':item.user_id}) %}
{% set list_by_ip_url = url({'for':'admin.audit.list'},{'user_ip':item.user_ip}) %}
{% set list_by_route_url = url({'for':'admin.audit.list'},{'req_route':item.req_route}) %}
{% set list_by_path_url = url({'for':'admin.audit.list'},{'req_path':item.req_path}) %}
{% set show_url = url({'for':'admin.audit.show','id':item.id}) %}
<tr>
<td>{{ item.user_id }}</td>
<td><a href="{{ list_by_id_url }}">{{ item.user_name }}</a></td>
<td>{{ item.user_name }}</td>
<td>
<a href="{{ list_by_ip_url }}">{{ item.user_ip }}</a>
<span class="layui-btn layui-btn-xs kg-ip2region" data-ip="{{ item.user_ip }}">位置</span>
<a href="javascript:" class="kg-ip2region" title="查看位置" data-ip="{{ item.user_ip }}">{{ item.user_ip }}</a>
</td>
<td><a href="{{ list_by_route_url }}">{{ item.req_route }}</a></td>
<td><a href="{{ list_by_path_url }}">{{ item.req_path }}</a></td>
<td>{{ item.req_route }}</td>
<td>{{ item.req_path }}</td>
<td>{{ date('Y-m-d H:i:s',item.create_time) }}</td>
<td class="center">
<button class="kg-view layui-btn layui-btn-sm" data-url="{{ show_url }}">详情</button>

View File

@ -0,0 +1,18 @@
{%- macro publish_status(type) %}
{% if type == 1 %}
审核中
{% elseif type == 2 %}
已发布
{% elseif type == 3 %}
未通过
{% else %}
未知
{% endif %}
{%- endmacro %}
{%- macro tags_info(items) %}
{% for item in items %}
{% set comma = loop.last ? '' : ',' %}
{{ item.name ~ comma }}
{% endfor %}
{%- endmacro %}

View File

@ -2,6 +2,7 @@
{% block content %}
{{ partial('macros/common') }}
{{ partial('macros/article') }}
<div class="kg-nav">
@ -18,15 +19,13 @@
<col>
<col>
<col>
<col>
<col width="10%">
</colgroup>
<thead>
<tr>
<th>文章</th>
<th>作者</th>
<th>来源</th>
<th>评论</th>
<th>终端</th>
<th>时间</th>
<th>操作</th>
</tr>
@ -34,14 +33,12 @@
<tbody>
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set show_url = url({'for':'admin.article.show','id':item.id}) %}
{% set review_url = url({'for':'admin.article.review','id':item.id}) %}
<tr>
<td>
<p>标题:{{ item.title }}{{ item.id }}</p>
<p>标题:{{ item.title }}</p>
<p class="meta">
{% if item.category.id is defined %}
<span>分类:{{ item.category.name }}</span>
{% endif %}
<span>来源:{{ source_info(item.source_type,item.source_url) }}</span>
{% if item.tags %}
<span>标签:{{ tags_info(item.tags) }}</span>
{% endif %}
@ -51,13 +48,9 @@
<p>昵称:<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a></p>
<p>编号:{{ item.owner.id }}</p>
</td>
<td>{{ source_info(item.source_type,item.source_url) }}</td>
<td>
{% if item.allow_comment == 1 %}
开启
{% else %}
关闭
{% endif %}
<p>类型:{{ client_type(item.client_type) }}</p>
<p>地址:<a href="javascript:" class="kg-ip2region" title="查看位置" data-ip="{{ item.client_ip }}">{{ item.client_ip }}</a></p>
</td>
<td>
{% if item.update_time > 0 %}
@ -67,7 +60,7 @@
{% endif %}
</td>
<td class="center">
<a href="{{ show_url }}" class="layui-btn layui-btn-sm">详情</a>
<a href="{{ review_url }}" class="layui-btn layui-btn-sm">详情</a>
</td>
</tr>
{% endfor %}
@ -77,3 +70,9 @@
{{ partial('partials/pager') }}
{% endblock %}
{% block include_js %}
{{ js_include('admin/js/ip2region.js') }}
{% endblock %}

View File

@ -0,0 +1,77 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/common') }}
{{ partial('macros/question') }}
<div class="kg-nav">
<div class="kg-nav-left">
<span class="layui-breadcrumb">
<a><cite>问题审核</cite></a>
</span>
</div>
</div>
<table class="layui-table kg-table layui-form">
<colgroup>
<col>
<col>
<col>
<col>
<col width="10%">
</colgroup>
<thead>
<tr>
<th>问题</th>
<th>作者</th>
<th>终端</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set review_url = url({'for':'admin.question.review','id':item.id}) %}
<tr>
<td>
<p>标题:{{ item.title }}</p>
<p class="meta">
{% if item.tags %}
<span>标签:{{ tags_info(item.tags) }}</span>
{% endif %}
</p>
</td>
<td>
<p>昵称:<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a></p>
<p>编号:{{ item.owner.id }}</p>
</td>
<td>
<p>类型:{{ client_type(item.client_type) }}</p>
<p>地址:<a href="javascript:" class="kg-ip2region" title="查看位置" data-ip="{{ item.client_ip }}">{{ item.client_ip }}</a></p>
</td>
<td>
{% if item.update_time > 0 %}
{{ date('Y-m-d H:i:s',item.update_time) }}
{% else %}
{{ date('Y-m-d H:i:s',item.create_time) }}
{% endif %}
</td>
<td class="center">
<a href="{{ review_url }}" class="layui-btn layui-btn-sm">详情</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}
{% block include_js %}
{{ js_include('admin/js/ip2region.js') }}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.question.create'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>添加问题</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button id="kg-submit" class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends 'templates/main.volt' %}
{% block content %}
<fieldset class="layui-elem-field layui-field-title">
<legend>编辑问题</legend>
</fieldset>
<div class="layui-tab layui-tab-brief">
<ul class="layui-tab-title kg-tab-title">
<li class="layui-this">基本信息</li>
<li>内容详情</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
{{ partial('question/edit_basic') }}
</div>
<div class="layui-tab-item">
{{ partial('question/edit_desc') }}
</div>
</div>
</div>
{% 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('lib/xm-select.js') }}
{{ js_include('admin/js/vditor.js') }}
{% endblock %}
{% block inline_js %}
<script>
layui.use(['jquery', 'form'], function () {
xmSelect.render({
el: '#xm-tag-ids',
name: 'xm_tag_ids',
max: 3,
filterable: true,
filterMethod: function (val, item, index, prop) {
return item.name.toLowerCase().indexOf(val.toLowerCase()) !== -1;
},
data: {{ xm_tags|json_encode }}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,35 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.question.update','id':question.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" value="{{ question.title }}" lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
<div id="xm-tag-ids"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">匿名</label>
<div class="layui-input-block">
<input type="radio" name="anonymous" value="1" title="是" {% if question.anonymous == 1 %}checked="checked"{% endif %}>
<input type="radio" name="anonymous" value="0" title="否" {% if question.anonymous == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">关闭</label>
<div class="layui-input-block">
<input type="radio" name="closed" value="1" title="是" {% if question.closed == 1 %}checked="checked"{% endif %}>
<input type="radio" name="closed" value="0" title="否" {% if question.closed == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>

View File

@ -0,0 +1,14 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.question.update','id':question.id}) }}">
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ question.content }}</textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<button class="kg-submit layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>

View File

@ -0,0 +1,160 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/question') }}
{% set add_url = url({'for':'admin.question.add'}) %}
{% set search_url = url({'for':'admin.question.search'}) %}
<div class="kg-nav">
<div class="kg-nav-left">
<span class="layui-breadcrumb">
<a><cite>问题管理</cite></a>
</span>
</div>
<div class="kg-nav-right">
<a class="layui-btn layui-btn-sm" href="{{ add_url }}">
<i class="layui-icon layui-icon-add-1"></i>添加问题
</a>
<a class="layui-btn layui-btn-sm" href="{{ search_url }}">
<i class="layui-icon layui-icon-search"></i>搜索问题
</a>
</div>
</div>
<table class="layui-table kg-table layui-form">
<colgroup>
<col>
<col>
<col>
<col>
<col>
<col>
<col>
<col width="10%">
</colgroup>
<thead>
<tr>
<th>问题</th>
<th>状态</th>
<th>浏览</th>
<th>回答</th>
<th>点赞</th>
<th>收藏</th>
<th>讨论</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set preview_url = url({'for':'home.question.show','id':item.id}) %}
{% set review_url = url({'for':'admin.question.review','id':item.id}) %}
{% set edit_url = url({'for':'admin.question.edit','id':item.id}) %}
{% set update_url = url({'for':'admin.question.update','id':item.id}) %}
{% set delete_url = url({'for':'admin.question.delete','id':item.id}) %}
{% set restore_url = url({'for':'admin.question.restore','id':item.id}) %}
{% set answer_url = url({'for':'admin.answer.list'},{'item_id':item.id}) %}
<tr>
<td>
<p>标题:<a href="{{ edit_url }}">{{ item.title }}</a>{{ item.id }}</p>
<p class="meta">
{% if item.category.id is defined %}
<span>分类:{{ item.category.name }}</span>
{% endif %}
{% if item.tags %}
<span>标签:{{ tags_info(item.tags) }}</span>
{% endif %}
</p>
<p class="meta">
<span>作者:<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a></span>
<span>创建:{{ date('Y-m-d',item.create_time) }}</span>
</p>
</td>
<td>{{ publish_status(item.published) }}</td>
<td>{{ item.view_count }}</td>
<td>{{ item.answer_count }}</td>
<td>{{ item.like_count }}</td>
<td>{{ item.favorite_count }}</td>
<td><input type="checkbox" name="closed" value="1" lay-skin="switch" lay-text="开|关" lay-filter="discuss" data-url="{{ update_url }}" {% if item.closed == 0 %}checked="checked"{% endif %}></td>
<td class="center">
<div class="layui-dropdown">
<button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button>
<ul>
{% if item.published == 1 %}
<li><a href="{{ review_url }}">审核问题</a></li>
{% elseif item.published == 2 %}
<li><a href="{{ preview_url }}" target="_blank">预览问题</a></li>
{% endif %}
<li><a href="{{ edit_url }}">编辑问题</a></li>
{% if item.deleted == 0 %}
<li><a href="javascript:" class="kg-delete" data-url="{{ delete_url }}">删除问题</a></li>
{% else %}
<li><a href="javascript:" class="kg-restore" data-url="{{ restore_url }}">还原问题</a></li>
{% endif %}
<hr>
<li><a href="javascript:" class="kg-answer" data-url="{{ answer_url }}">回答管理</a></li>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}
{% block inline_js %}
<script>
layui.define(['jquery', 'form', 'layer'], function () {
var $ = layui.jquery;
var form = layui.form;
var layer = layui.layer;
form.on('switch(discuss)', function (data) {
var checked = $(this).is(':checked');
var allowDiscuss = checked ? 1 : 0;
var url = $(this).data('url');
var tips = allowDiscuss === 1 ? '确定要开启讨论?' : '确定要关闭讨论?';
layer.confirm(tips, function () {
$.ajax({
type: 'POST',
url: url,
data: {closed: data.value},
success: function (res) {
layer.msg(res.msg, {icon: 1});
},
error: function (xhr) {
var json = JSON.parse(xhr.responseText);
layer.msg(json.msg, {icon: 2});
data.elem.checked = !checked;
form.render();
}
});
}, function () {
data.elem.checked = !checked;
form.render();
});
});
$('.kg-answer').on('click', function () {
var url = $(this).data('url');
layer.open({
type: 2,
title: '回答管理',
area: ['1000px', '600px'],
content: url
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,85 @@
{% extends 'templates/main.volt' %}
{% block content %}
<fieldset class="layui-elem-field layui-field-title">
<legend>审核内容</legend>
</fieldset>
<div class="kg-mod-preview">
<div class="title">{{ question.title }}</div>
<div class="content markdown-body">{{ question.content }}</div>
{% if question.tags %}
<div class="tags">
{% for item in question.tags %}
<span class="layui-btn layui-btn-xs">{{ item['name'] }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<fieldset class="layui-elem-field layui-field-title">
<legend>审核意见</legend>
</fieldset>
<form class="layui-form kg-form kg-review-form" method="POST" action="{{ url({'for':'admin.question.review','id':question.id}) }}">
<div class="layui-form-item">
<label class="layui-form-label">审核</label>
<div class="layui-input-block">
<input type="radio" name="type" value="approve" title="通过" lay-filter="review">
<input type="radio" name="type" value="reject" title="拒绝" lay-filter="review">
</div>
</div>
<div id="reason-block" style="display:none;">
<div class="layui-form-item">
<label class="layui-form-label">理由</label>
<div class="layui-input-block">
<select name="reason">
<option value="">请选择</option>
{% for value,name in reject_options %}
<option value="{{ value }}">{{ name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button id="kg-submit" class="layui-btn" lay-submit="true" lay-filter="go">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}
{% block link_css %}
{{ css_link('home/css/markdown.css') }}
{% endblock %}
{% block inline_js %}
<script>
layui.use(['jquery', 'form'], function () {
var $ = layui.jquery;
var form = layui.form;
form.on('radio(review)', function (data) {
var block = $('#reason-block');
if (data.value === 'approve') {
block.hide();
} else {
block.show();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,93 @@
{% extends 'templates/main.volt' %}
{% block content %}
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.question.list'}) }}">
<fieldset class="layui-elem-field layui-field-title">
<legend>搜索问题</legend>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">问题编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="id" placeholder="问题编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">作者编号</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="owner_id" placeholder="作者编号精确匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标题</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" placeholder="标题模糊匹配">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">标签</label>
<div class="layui-input-block">
<div id="xm-tag-ids"></div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
{% for value,title in publish_types %}
<input type="radio" name="published" value="{{ value }}" title="{{ title }}">
{% endfor %}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">匿名</label>
<div class="layui-input-block">
<input type="radio" name="anonymous" value="1" title="是">
<input type="radio" name="anonymous" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">关闭</label>
<div class="layui-input-block">
<input type="radio" name="closed" value="1" title="是">
<input type="radio" name="closed" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">删除</label>
<div class="layui-input-block">
<input type="radio" name="deleted" value="1" title="是">
<input type="radio" name="deleted" value="0" title="否">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button class="layui-btn" lay-submit="true">提交</button>
<button type="button" class="kg-back layui-btn layui-btn-primary">返回</button>
</div>
</div>
</form>
{% endblock %}
{% block include_js %}
{{ js_include('lib/xm-select.js') }}
{% endblock %}
{% block inline_js %}
<script>
xmSelect.render({
el: '#xm-tag-ids',
name: 'xm_tag_ids',
max: 5,
filterable: true,
data: {{ xm_tags|json_encode }}
});
</script>
{% endblock %}

View File

@ -121,10 +121,6 @@
<td>群组头像</td>
<td>public/static/admin/img/default/group_cover.png</td>
</tr>
<tr>
<td>文章封面</td>
<td>public/static/admin/img/default/article_cover.png</td>
</tr>
<tr>
<td>课程封面</td>
<td>public/static/admin/img/default/course_cover.png</td>

View File

@ -3,6 +3,7 @@
namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Answer as AnswerService;
use App\Http\Home\Services\Question as QuestionService;
use App\Services\Logic\Answer\AnswerAccept as AnswerAcceptService;
use App\Services\Logic\Answer\AnswerCreate as AnswerCreateService;
use App\Services\Logic\Answer\AnswerDelete as AnswerDeleteService;
@ -21,7 +22,13 @@ class AnswerController extends Controller
*/
public function addAction()
{
$id = $this->request->getQuery('question_id', 'int', 0);
$service = new QuestionService();
$question = $service->getQuestion($id);
$this->view->setVar('question', $question);
}
/**
@ -33,6 +40,11 @@ class AnswerController extends Controller
$answer = $service->getAnswer($id);
$service = new QuestionService();
$question = $service->getQuestion($answer->question_id);
$this->view->setVar('question', $question);
$this->view->setVar('answer', $answer);
}
@ -55,9 +67,17 @@ class AnswerController extends Controller
{
$service = new AnswerCreateService();
$service->handle();
$answer = $service->handle();
$content = ['msg' => '创建答案成功'];
$location = $this->url->get([
'for' => 'home.question.show',
'id' => $answer->question_id,
]);
$content = [
'location' => $location,
'msg' => '创建回答成功',
];
return $this->jsonSuccess($content);
}
@ -69,9 +89,17 @@ class AnswerController extends Controller
{
$service = new AnswerUpdateService();
$service->handle($id);
$answer = $service->handle($id);
$content = ['msg' => '更新答案成功'];
$location = $this->url->get([
'for' => 'home.question.show',
'id' => $answer->question_id,
]);
$content = [
'location' => $location,
'msg' => '更新回答成功',
];
return $this->jsonSuccess($content);
}
@ -83,13 +111,16 @@ class AnswerController extends Controller
{
$service = new AnswerDeleteService();
$service->handle($id);
$answer = $service->handle($id);
$location = $this->url->get(['for' => 'home.uc.answers']);
$location = $this->url->get([
'for' => 'home.question.show',
'id' => $answer->question_id,
]);
$content = [
'location' => $location,
'msg' => '删除成功',
'msg' => '删除答成功',
];
return $this->jsonSuccess($content);
@ -123,4 +154,12 @@ class AnswerController extends Controller
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Route("/{id:[0-9]+}/report", name="home.answer.report")
*/
public function reportAction($id)
{
}
}

View File

@ -4,10 +4,13 @@ namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Article as ArticleService;
use App\Http\Home\Services\ArticleQuery as ArticleQueryService;
use App\Services\Logic\Article\ArticleCreate as ArticleCreateService;
use App\Services\Logic\Article\ArticleDelete as ArticleDeleteService;
use App\Services\Logic\Article\ArticleFavorite as ArticleFavoriteService;
use App\Services\Logic\Article\ArticleInfo as ArticleInfoService;
use App\Services\Logic\Article\ArticleLike as ArticleLikeService;
use App\Services\Logic\Article\ArticleList as ArticleListService;
use App\Services\Logic\Article\ArticleUpdate as ArticleUpdateService;
use App\Services\Logic\Article\HotAuthorList as HotAuthorListService;
use App\Services\Logic\Article\RelatedArticleList as RelatedArticleListService;
use Phalcon\Mvc\View;
@ -122,6 +125,7 @@ class ArticleController extends Controller
}
$this->seo->prependTitle($article['title']);
$this->seo->setDescription($article['summary']);
$this->view->setVar('article', $article);
}
@ -144,9 +148,9 @@ class ArticleController extends Controller
*/
public function createAction()
{
$service = new ArticleService();
$service = new ArticleCreateService();
$service->createArticle();
$service->handle();
$location = $this->url->get(['for' => 'home.uc.articles']);
@ -163,9 +167,9 @@ class ArticleController extends Controller
*/
public function updateAction($id)
{
$service = new ArticleService();
$service = new ArticleUpdateService();
$service->updateArticle($id);
$service->handle($id);
$location = $this->url->get(['for' => 'home.uc.articles']);
@ -182,9 +186,9 @@ class ArticleController extends Controller
*/
public function deleteAction($id)
{
$service = new ArticleService();
$service = new ArticleDeleteService();
$service->deleteArticle($id);
$service->handle($id);
$location = $this->url->get(['for' => 'home.uc.articles']);
@ -224,4 +228,12 @@ class ArticleController extends Controller
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Route("/{id:[0-9]+}/report", name="home.article.report")
*/
public function reportAction($id)
{
}
}

View File

@ -121,4 +121,12 @@ class CommentController extends Controller
return $this->jsonSuccess(['msg' => '删除评论成功']);
}
/**
* @Route("/{id:[0-9]+}/report", name="home.comment.report")
*/
public function reportAction($id)
{
}
}

View File

@ -127,6 +127,7 @@ class QuestionController extends Controller
$question = $service->handle($id);
$this->seo->prependTitle($question['title']);
$this->seo->setDescription($question['summary']);
$this->view->setVar('question', $question);
}
@ -249,4 +250,12 @@ class QuestionController extends Controller
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
}
/**
* @Route("/{id:[0-9]+}/report", name="home.question.report")
*/
public function reportAction($id)
{
}
}

View File

@ -2,143 +2,76 @@
namespace App\Http\Home\Services;
use App\Http\Admin\Services\Article as ArticleService;
use App\Library\Utils\Word as WordUtil;
use App\Models\Article as ArticleModel;
use App\Traits\Client as ClientTrait;
use App\Validators\Article as ArticleValidator;
use App\Models\Category as CategoryModel;
use App\Repos\Category as CategoryRepo;
use App\Repos\Tag as TagRepo;
use App\Services\Logic\ArticleTrait;
class Article extends ArticleService
class Article extends Service
{
use ClientTrait;
use ArticleTrait;
public function createArticle()
public function getArticleModel()
{
$post = $this->request->getPost();
$user = $this->getLoginUser();
$article = new ArticleModel();
$data = $this->handlePostData($post);
$data['client_type'] = $this->getClientType();
$data['client_ip'] = $this->getClientIp();
$data['owner_id'] = $user->id;
$article->create($data);
if (isset($post['xm_tag_ids'])) {
$this->saveTags($article, $post['xm_tag_ids']);
}
$this->incrUserArticleCount($user);
$this->eventsManager->fire('Article:afterCreate', $this, $article);
$article->afterFetch();
return $article;
}
public function updateArticle($id)
public function getXmTags($id)
{
$post = $this->request->getPost();
$tagRepo = new TagRepo();
$article = $this->findOrFail($id);
$allTags = $tagRepo->findAll(['published' => 1], 'priority');
$data = $this->handlePostData($post);
if ($allTags->count() == 0) return [];
$data['client_type'] = $this->getClientType();
$data['client_ip'] = $this->getClientIp();
$articleTagIds = [];
if ($article->published == ArticleModel::PUBLISH_REJECTED) {
$data['published'] = ArticleModel::PUBLISH_PENDING;
if ($id > 0) {
$article = $this->checkArticle($id);
if (!empty($article->tags)) {
$articleTagIds = kg_array_column($article->tags, 'id');
}
}
/**
* 当通过审核后,禁止修改部分文章属性
*/
if ($article->published == ArticleModel::PUBLISH_APPROVED) {
unset(
$data['title'],
$data['content'],
$data['cover'],
$data['source_type'],
$data['source_url'],
$data['category_id'],
$post['xm_tag_ids'],
);
$list = [];
foreach ($allTags as $tag) {
$selected = in_array($tag->id, $articleTagIds);
$list[] = [
'name' => $tag->name,
'value' => $tag->id,
'selected' => $selected,
];
}
$article->update($data);
if (isset($post['xm_tag_ids'])) {
$this->saveTags($article, $post['xm_tag_ids']);
return $list;
}
$this->eventsManager->fire('Article:afterUpdate', $this, $article);
return $article;
}
public function deleteArticle($id)
public function getCategories()
{
$article = $this->findOrFail($id);
$categoryRepo = new CategoryRepo();
$user = $this->getLoginUser();
$validator = new ArticleValidator();
$validator->checkOwner($user->id, $article->owner_id);
$article->deleted = 1;
$article->update();
$this->decrUserArticleCount($user);
$this->rebuildArticleIndex($article);
$this->eventsManager->fire('Article:afterDelete', $this, $article);
return $article;
return $categoryRepo->findAll([
'type' => CategoryModel::TYPE_ARTICLE,
'level' => 1,
'published' => 1,
]);
}
protected function handlePostData($post)
public function getSourceTypes()
{
$data = [];
$validator = new ArticleValidator();
$data['title'] = $validator->checkTitle($post['title']);
$data['content'] = $validator->checkContent($post['content']);
$data['word_count'] = WordUtil::getWordCount($data['content']);
if (isset($post['category_id'])) {
$category = $validator->checkCategory($post['category_id']);
$data['category_id'] = $category->id;
return ArticleModel::sourceTypes();
}
if (isset($post['cover'])) {
$data['cover'] = $validator->checkCover($post['cover']);
}
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'] = $validator->checkAllowCommentStatus($post['allow_comment']);
}
if (isset($post['private'])) {
$data['private'] = $validator->checkPrivateStatus($post['private']);
}
return $data;
public function getArticle($id)
{
return $this->checkArticle($id);
}
}

View File

@ -2,7 +2,9 @@
namespace App\Http\Home\Services;
use App\Models\Category as CategoryModel;
use App\Models\Question as QuestionModel;
use App\Repos\Category as CategoryRepo;
use App\Repos\Tag as TagRepo;
use App\Services\Logic\QuestionTrait;
use App\Services\Logic\Service as LogicService;
@ -21,9 +23,15 @@ class Question extends LogicService
return $question;
}
public function getQuestion($id)
public function getCategories()
{
return $this->checkQuestion($id);
$categoryRepo = new CategoryRepo();
return $categoryRepo->findAll([
'type' => CategoryModel::TYPE_ARTICLE,
'level' => 1,
'published' => 1,
]);
}
public function getXmTags($id)
@ -57,5 +65,9 @@ class Question extends LogicService
return $list;
}
public function getQuestion($id)
{
return $this->checkQuestion($id);
}
}

View File

@ -1,8 +1,22 @@
{% extends 'templates/layer.volt' %}
{% extends 'templates/main.volt' %}
{% block content %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a><cite>回答问题</cite></a>
</span>
</div>
<div class="layout-main">
<div class="writer-content wrap">
<form class="layui-form" method="POST" action="{{ url({'for':'home.answer.create'}) }}">
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<input class="layui-input" type="text" name="title" value="{{ question.title }}" readonly="readonly">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
@ -11,11 +25,13 @@
</div>
<div class="layui-form-item center">
<div class="layui-input-block" style="margin:0;">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="add_answer">提交回答</button>
<input type="hidden" name="question_id" value="{{ request.get('question_id') }}">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">发布回答</button>
<input type="hidden" name="question_id" value="{{ question.id }}">
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -1,8 +1,22 @@
{% extends 'templates/layer.volt' %}
{% extends 'templates/main.volt' %}
{% block content %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a><cite>编辑回答</cite></a>
</span>
</div>
<div class="layout-main">
<div class="writer-content wrap">
<form class="layui-form" method="POST" action="{{ url({'for':'home.answer.update','id':answer.id}) }}">
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<input class="layui-input" type="text" name="title" value="{{ question.title }}" readonly="readonly">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin:0;">
<div id="vditor"></div>
@ -11,10 +25,12 @@
</div>
<div class="layui-form-item center">
<div class="layui-input-block" style="margin:0;">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="edit_answer">提交回答</button>
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">发布回答</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -5,8 +5,11 @@
{{ partial('macros/article') }}
{% set article_list_url = url({'for':'home.article.list'}) %}
{% set related_url = url({'for':'home.article.related','id':article.id}) %}
{% set owner_url = url({'for':'home.user.show','id':article.owner.id}) %}
{% set article_report_url = url({'for':'home.article.report','id':article.id}) %}
{% set article_edit_url = url({'for':'home.article.edit','id':article.id}) %}
{% set article_delete_url = url({'for':'home.article.delete','id':article.id}) %}
{% set article_owner_url = url({'for':'home.user.show','id':article.owner.id}) %}
{% set article_related_url = url({'for':'home.article.related','id':article.id}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
@ -29,13 +32,20 @@
<div class="article-info wrap">
<div class="title">{{ article.title }}</div>
<div class="meta">
<div class="left">
<span class="source layui-badge layui-bg-green">{{ source_type(article.source_type) }}</span>
<span class="owner">
<a href="{{ owner_url }}">{{ article.owner.name }}</a>
</span>
<span class="owner"><a href="{{ article_owner_url }}">{{ article.owner.name }}</a></span>
<span class="time">{{ article.create_time|time_ago }}</span>
<span class="view">{{ article.view_count }} 阅读</span>
<span class="word">{{ article.word_count }} 字数</span>
<span class="time" title="{{ date('Y-m-d H:i:s',article.create_time) }}">{{ article.create_time|time_ago }}</span>
<span class="comment">{{ article.comment_count }} 评论</span>
</div>
<div class="right">
<span class="article-report" data-url="{{ article_report_url }}">举报</span>
{% if auth_user.id == article.owner.id %}
<span class="article-edit" data-url="{{ article_edit_url }}">编辑</span>
<span class="kg-delete" data-url="{{ article_delete_url }}">删除</span>
{% endif %}
</div>
</div>
<div class="content markdown-body">{{ article.content }}</div>
{% if article.tags %}
@ -74,7 +84,7 @@
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ owner_url }}" title="{{ article.owner.about }}">{{ article.owner.name }}</a>
<a href="{{ article_owner_url }}" title="{{ article.owner.about }}">{{ article.owner.name }}</a>
</div>
<div class="title layui-elip">{{ article.owner.title|default('初出江湖') }}</div>
</div>
@ -82,7 +92,7 @@
</div>
</div>
</div>
<div class="sidebar" id="sidebar-related" data-url="{{ related_url }}"></div>
<div class="sidebar" id="sidebar-related" data-url="{{ article_related_url }}"></div>
</div>
</div>

View File

@ -20,7 +20,7 @@
</div>
<div class="item" id="toolbar-favorite">
<div class="icon" title="{{ favorite_title }}" data-url="{{ favorite_url }}">
<i class="layui-icon layui-icon-star icon-star {{ favorite_class }}"></i>
<i class="layui-icon icon-star {{ favorite_class }}"></i>
</div>
<div class="text" data-count="{{ article.favorite_count }}">{{ article.favorite_count }}</div>
</div>

View File

@ -1,6 +1,7 @@
{% if pager.total_pages > 0 %}
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set report_url = url({'for':'home.comment.report','id':item.id}) %}
{% set like_url = url({'for':'home.comment.like','id':item.id}) %}
{% set delete_url = url({'for':'home.comment.delete','id':item.id}) %}
{% set reply_url = url({'for':'home.comment.reply','id':item.id}) %}
@ -25,26 +26,26 @@
<div class="column">
<span class="like-count" data-count="{{ item.like_count }}">{{ item.like_count }}</span>
{% if item.me.liked == 1 %}
<span class="action action-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
<span class="action comment-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
{% else %}
<span class="action action-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
<span class="action comment-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
{% endif %}
</div>
<div class="column">
<span class="reply-count" data-count="{{ item.reply_count }}">{{ item.reply_count }}</span>
<span class="action action-toggle" title="展开回应" data-id="{{ item.id }}" data-url="{{ reply_list_url }}">回应</span>
<span class="action comment-toggle" title="展开回应" data-id="{{ item.id }}" data-url="{{ reply_list_url }}">回应</span>
</div>
</div>
<div class="right">
<div class="column">
<span class="action action-reply" data-id="{{ item.id }}">回复</span>
<span class="action comment-reply" data-id="{{ item.id }}">回复</span>
</div>
<div class="column">
<span class="action action-report" data-id="{{ item.id }}">举报</span>
<span class="action comment-report" data-url="{{ report_url }}">举报</span>
</div>
{% if item.owner.id == auth_user.id %}
<div class="column">
<span class="action action-delete" data-id="{{ item.id }}" data-url="{{ delete_url }}">删除</span>
<span class="action comment-delete" data-id="{{ item.id }}" data-url="{{ delete_url }}">删除</span>
</div>
{% endif %}
</div>

View File

@ -3,7 +3,7 @@
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set like_url = url({'for':'home.comment.like','id':item.id}) %}
{% set delete_url = url({'for':'home.comment.delete','id':item.id}) %}
{% set reply_create_url = url({'for':'home.comment.create_reply','id':item.id}) %}
{% set reply_url = url({'for':'home.comment.reply','id':item.id}) %}
<div class="comment-box" id="comment-{{ item.id }}">
<div class="comment-card">
<div class="avatar">
@ -29,22 +29,22 @@
<div class="column">
<span class="like-count" data-count="{{ item.like_count }}">{{ item.like_count }}</span>
{% if item.me.liked == 1 %}
<span class="action action-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
<span class="action comment-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
{% else %}
<span class="action action-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
<span class="action comment-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
{% endif %}
</div>
</div>
<div class="right">
<div class="column">
<span class="action action-reply" data-id="{{ item.id }}">回复</span>
<span class="action comment-reply" data-id="{{ item.id }}">回复</span>
</div>
<div class="column">
<span class="action action-report" data-id="{{ item.id }}">举报</span>
<span class="action comment-report" data-id="{{ item.id }}">举报</span>
</div>
{% if item.owner.id == auth_user.id %}
<div class="column">
<span class="action action-delete" data-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}" data-url="{{ delete_url }}">删除</span>
<span class="action comment-delete" data-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}" data-url="{{ delete_url }}">删除</span>
</div>
{% endif %}
</div>
@ -52,7 +52,7 @@
</div>
</div>
<div class="comment-form" id="comment-form-{{ item.id }}" style="display:none;">
<form class="layui-form" method="post" action="{{ reply_create_url }}">
<form class="layui-form" method="post" action="{{ reply_url }}">
<textarea class="layui-textarea" name="content" placeholder="撰写评论..." lay-verify="required"></textarea>
<div class="footer">
<div class="toolbar"></div>

View File

@ -31,6 +31,29 @@
{% elseif type == 189 %}
{% set article_url = url({'for':'home.article.show','id':info.article.id}) %}
<p>{{ sender.name }} 喜欢了你的文章 <a href="{{ article_url }}" target="_blank">{{ info.article.title }}</a></p>
{% elseif type == 204 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>你的提问 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a> 通过了审核</p>
{% elseif type == 205 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>你的提问 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a> 审核未通过</p>
<p>拒绝原因:{{ info.reason }}</p>
{% elseif type == 206 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>{{ sender.name }} 回答了你的提问 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a></p>
<p>回答内容:{{ info.answer.summary }}</p>
{% elseif type == 208 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>{{ sender.name }} 收藏了你的问题 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a></p>
{% elseif type == 209 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>{{ sender.name }} 喜欢了你的问题 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a></p>
{% elseif type == 209 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>{{ sender.name }} 喜欢了你的问题 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a></p>
{% elseif type == 228 %}
{% set question_url = url({'for':'home.question.show','id':info.question.id}) %}
<p>{{ sender.name }} 喜欢了你对问题 <a href="{{ question_url }}" target="_blank">{{ info.question.title }}</a> 的回答</p>
{% elseif type == 506 %}
<p>{{ sender.name }} 回复了你的评论:{{ info.comment.content }}</p>
<p>回复内容:{{ info.reply.content }}</p>

View File

@ -0,0 +1,17 @@
<div class="layui-card">
<div class="layui-card-header">答题指南</div>
<div class="layui-card-body">
<h3 class="suggest-text">适合作为回答的</h3>
<ul class="suggest-list">
<li><i class="layui-icon layui-icon-ok-circle"></i> 经过验证的有效解决办法</li>
<li><i class="layui-icon layui-icon-ok-circle"></i> 自己的经验指引,对解决问题有帮助</li>
<li><i class="layui-icon layui-icon-ok-circle"></i> 遵循 Markdown 语法排版,表达语义正确</li>
</ul>
<h3 class="not-suggest-text">不该作为回答的</h3>
<ul class="not-suggest-list">
<li><i class="layui-icon layui-icon-close-fill"></i> 询问内容细节或回复楼层</li>
<li><i class="layui-icon layui-icon-close-fill"></i> 与题目无关的内容</li>
<li><i class="layui-icon layui-icon-close-fill"></i> “赞” “顶” “同问” 等毫无意义的内容</li>
</ul>
</div>
</div>

View File

@ -1,11 +1,10 @@
{% if pager.total_pages > 0 %}
{% for item in pager.items %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set accept_url = url({'for':'home.answer.accept','id':item.id}) %}
{% set like_url = url({'for':'home.answer.like','id':item.id}) %}
{% set report_url = url({'for':'home.answer.report','id':item.id}) %}
{% set edit_url = url({'for':'home.answer.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.answer.delete','id':item.id}) %}
{% set report_url = '' %}
<div class="comment-card answer-card" id="answer-{{ item.id }}">
<div class="avatar">
<a href="{{ owner_url }}" title="{{ item.owner.name }}" target="_blank">
@ -13,15 +12,10 @@
</a>
</div>
<div class="info">
{% if item.accepted == 1 %}
<div class="accepted">
<span class="layui-badge layui-bg-green">已采纳</span>
</div>
{% endif %}
<div class="user">
<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a>
</div>
<div class="content">{{ item.content }}</div>
<div class="content markdown-body">{{ item.content }}</div>
<div class="footer">
<div class="left">
<div class="column">
@ -30,27 +24,22 @@
<div class="column">
<span class="like-count" data-count="{{ item.like_count }}">{{ item.like_count }}</span>
{% if item.me.liked == 1 %}
<span class="action action-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
<span class="action answer-like liked" title="取消点赞" data-url="{{ like_url }}">已赞</span>
{% else %}
<span class="action action-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
<span class="action answer-like" title="点赞支持" data-url="{{ like_url }}">点赞</span>
{% endif %}
</div>
</div>
<div class="right">
{% if question.solved == 0 and auth_user.id == question.owner_id %}
<div class="column">
<span class="action action-accept" data-url="{{ accept_url }}">采纳</span>
</div>
{% endif %}
<div class="column">
<span class="action action-report" data-url="{{ report_url }}">举报</span>
<span class="action answer-report" data-url="{{ report_url }}">举报</span>
</div>
{% if auth_user.id == item.owner.id %}
<div class="column">
<span class="action action-edit" data-url="{{ edit_url }}">编辑</span>
<span class="action answer-edit" data-url="{{ edit_url }}">编辑</span>
</div>
<div class="column">
<span class="action action-delete" data-url="{{ delete_url }}">删除</span>
<span class="action kg-delete" data-url="{{ delete_url }}">删除</span>
</div>
{% endif %}
</div>

View File

@ -29,7 +29,7 @@
</div>
<div class="layui-form-item center">
<div class="layui-input-block">
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">提交问题</button>
<button class="layui-btn kg-submit" lay-submit="true" lay-filter="go">发布问题</button>
</div>
</div>
</div>

View File

@ -5,10 +5,13 @@
{{ partial('macros/question') }}
{% set question_list_url = url({'for':'home.question.list'}) %}
{% set add_answer_url = url({'for':'home.answer.add'},{'question_id':question.id}) %}
{% set question_report_url = url({'for':'home.question.report','id':question.id}) %}
{% set question_edit_url = url({'for':'home.question.edit','id':question.id}) %}
{% set question_delete_url = url({'for':'home.question.delete','id':question.id}) %}
{% set question_owner_url = url({'for':'home.user.show','id':question.owner.id}) %}
{% set question_related_url = url({'for':'home.question.related','id':question.id}) %}
{% set answer_add_url = url({'for':'home.answer.add'},{'question_id':question.id}) %}
{% set answer_list_url = url({'for':'home.question.answers','id':question.id}) %}
{% set related_url = url({'for':'home.question.related','id':question.id}) %}
{% set owner_url = url({'for':'home.user.show','id':question.owner.id}) %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
@ -31,12 +34,19 @@
<div class="article-info wrap">
<div class="title">{{ question.title }}</div>
<div class="meta">
<span class="owner">
<a href="{{ owner_url }}">{{ question.owner.name }}</a>
</span>
<div class="left">
<span class="owner"><a href="{{ question_owner_url }}">{{ question.owner.name }}</a></span>
<span class="time">{{ question.create_time|time_ago }}</span>
<span class="view">{{ question.view_count }} 阅读</span>
<span class="answer">{{ question.answer_count }} 回答</span>
<span class="time" title="{{ date('Y-m-d H:i:s',question.create_time) }}">{{ question.create_time|time_ago }}</span>
</div>
<div class="right">
<span class="question-report" data-url="{{ question_report_url }}">举报</span>
{% if auth_user.id == question.owner.id %}
<span class="question-edit" data-url="{{ question_edit_url }}">编辑</span>
<span class="question-delete" data-url="{{ question_delete_url }}">删除</span>
{% endif %}
</div>
</div>
<div class="content markdown-body">{{ question.content }}</div>
{% if question.tags %}
@ -49,9 +59,26 @@
{% endif %}
</div>
<div id="answer-anchor"></div>
<div class="answer-wrap wrap">
<div id="answer-list" data-url="{{ answer_list_url }}"></div>
<div class="answer-tips">
{{ partial('question/answer_tips') }}
</div>
{% if question.answer_count > 0 %}
<div class="layout-content">
<div class="content-wrap wrap">
<div class="layui-tab layui-tab-brief search-tab" lay-filter="answer">
<ul class="layui-tab-title">
<li class="layui-this" data-url="{{ answer_list_url }}?sort=popular">热门回答</li>
<li data-url="{{ answer_list_url }}?sort=latest">最新回答</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div id="answer-list" data-url="{{ answer_list_url }}?sort=popular"></div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="layout-sidebar">
<div class="sidebar">
@ -64,7 +91,7 @@
</div>
<div class="info">
<div class="name layui-elip">
<a href="{{ owner_url }}" title="{{ question.owner.about }}">{{ question.owner.name }}</a>
<a href="{{ question_owner_url }}" title="{{ question.owner.about }}">{{ question.owner.name }}</a>
</div>
<div class="title layui-elip">{{ question.owner.title|default('初出江湖') }}</div>
</div>
@ -74,10 +101,10 @@
</div>
{% if question.me.answered == 0 %}
<div class="sidebar wrap">
<button class="layui-btn layui-btn-fluid btn-answer" data-url="{{ add_answer_url }}">回答问题</button>
<button class="layui-btn layui-btn-fluid btn-answer" data-url="{{ answer_add_url }}">回答问题</button>
</div>
{% endif %}
<div class="sidebar" id="sidebar-related" data-url="{{ related_url }}"></div>
<div class="sidebar" id="sidebar-related" data-url="{{ question_related_url }}"></div>
</div>
</div>

View File

@ -20,7 +20,7 @@
</div>
<div class="item" id="toolbar-favorite">
<div class="icon" title="{{ favorite_title }}" data-url="{{ favorite_url }}">
<i class="layui-icon layui-icon-star icon-star {{ favorite_class }}"></i>
<i class="layui-icon icon-star {{ favorite_class }}"></i>
</div>
<div class="text" data-count="{{ question.favorite_count }}">{{ question.favorite_count }}</div>
</div>

View File

@ -3,7 +3,7 @@
{% for item in pager.items %}
{% set course_url = url({'for':'home.course.show','id':item.id}) %}
{% set teacher_url = url({'for':'home.teacher.show','id':item.teacher.id}) %}
<div class="search-course-card clearfix">
<div class="search-course-card">
<div class="cover">
<a href="{{ course_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">

View File

@ -3,18 +3,18 @@
{% for item in pager.items %}
{% set group_url = url({'for':'home.im_group.show','id':item.id}) %}
{% set owner_url = url({'for':'home.user.show','id':item.owner.id}) %}
{% set item.about = item.about ? item.about : '这个家伙真懒,什么也没有留下!' %}
<div class="search-group-card clearfix">
{% set item.about = item.about|default('这个家伙真懒,什么也没有留下!') %}
<div class="search-group-card">
<div class="avatar">
<a href="{{ group_url }}" target="_blank">
<img src="{{ item.avatar }}!avatar_160" alt="{{ item.name }}">
</a>
</div>
<div class="info">
<div class="name layui-elip">
<div class="name">
<a href="{{ group_url }}" target="_blank">{{ item.name }}</a>
</div>
<div class="about layui-elip">{{ item.about }}</div>
<div class="about">{{ item.about }}</div>
<div class="meta">
<span>组长:<a href="{{ owner_url }}" target="_blank">{{ item.owner.name }}</a></span>
<span>成员:{{ item.user_count }}</span>

View File

@ -17,6 +17,13 @@
<span class="answer">回答:{{ item.answer_count }}</span>
</div>
</div>
{% if item.cover %}
<div class="cover">
<a href="{{ question_url }}" target="_blank">
<img src="{{ item.cover }}!cover_270" alt="{{ item.title }}">
</a>
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -4,18 +4,18 @@
<div class="search-user-list">
{% for item in pager.items %}
{% set user_url = url({'for':'home.user.show','id':item.id}) %}
{% set item.about = item.about ? item.about : '这个家伙真懒,什么也没有留下!' %}
<div class="search-group-card clearfix">
{% set item.about = item.about|default('这个家伙真懒,什么也没有留下!') %}
<div class="search-group-card">
<div class="avatar">
<a href="{{ user_url }}" target="_blank">
<img src="{{ item.avatar }}!avatar_160" alt="{{ item.name }}">
</a>
</div>
<div class="info">
<div class="name layui-elip">
<div class="name">
<a href="{{ user_url }}" target="_blank">{{ item.name }}</a>
</div>
<div class="about layui-elip">{{ item.about }}</div>
<div class="about">{{ item.about }}</div>
<div class="meta">
<span>性别:{{ gender_info(item.gender) }}</span>
<span>地区:{{ item.area }}</span>

View File

@ -5,11 +5,6 @@
{% set list_pager_url = url({'for':'home.tag.list_pager'}) %}
{% set my_pager_url = url({'for':'home.tag.my_pager'}) %}
<div class="layui-breadcrumb breadcrumb">
<a href="/">首页</a>
<a><cite>标签</cite></a>
</div>
<div class="tab-wrap">
<div class="layui-tab layui-tab-brief user-tab" lay-filter="tag">
<ul class="layui-tab-title">

View File

@ -36,8 +36,8 @@
<tbody>
{% for item in pager.items %}
{% set question_url = url({'for':'home.question.show','id':item.question.id}) %}
{% set edit_url = url({'for':'home.review.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.review.delete','id':item.id}) %}
{% set edit_url = url({'for':'home.answer.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.answer.delete','id':item.id}) %}
<tr>
<td>
<p>提问:<a href="{{ question_url }}" target="_blank">{{ item.question.title }}</a></p>
@ -49,8 +49,8 @@
</td>
<td>{{ item.like_count }}</td>
<td>
<button class="layui-btn layui-btn-xs btn-edit-review" data-url="{{ edit_url }}">修改</button>
<button class="layui-btn layui-btn-xs layui-bg-red kg-delete" data-url="{{ delete_url }}">删除</button>
<a href="{{ edit_url }}" class="layui-btn layui-btn-xs">修改</a>
<a href="javascript:" class="layui-btn layui-btn-xs layui-bg-red kg-delete" data-url="{{ delete_url }}">删除</a>
</td>
</tr>
{% endfor %}

View File

@ -2,7 +2,7 @@
{% block content %}
{% set types = {'course':'课程','article':'文章'} %}
{% set types = {'course':'课程','article':'文章','question':'问题'} %}
{% set type = request.get('type','trim','course') %}
<div class="layout-main clearfix">
@ -21,6 +21,8 @@
{{ partial('user/console/favorites_course') }}
{% elseif type == 'article' %}
{{ partial('user/console/favorites_article') }}
{% elseif type == 'question' %}
{{ partial('user/console/favorites_question') }}
{% endif %}
</div>
</div>

View File

@ -26,7 +26,7 @@
<td>{{ item.like_count }}</td>
<td>{{ item.comment_count }}</td>
<td class="center">
<button class="layui-btn layui-btn-sm kg-delete" data-tips="确定要取消收藏吗?" data-url="{{ favorite_url }}">取消</button>
<button class="layui-btn layui-btn-sm layui-bg-red kg-delete" data-tips="确定要取消收藏吗?" data-url="{{ favorite_url }}">取消</button>
</td>
</tr>
{% endfor %}

View File

@ -23,7 +23,7 @@
<td>{{ item.user_count }}</td>
<td>{{ "%0.1f"|format(item.rating) }}</td>
<td class="center">
<button class="layui-btn layui-btn-sm kg-delete" data-tips="确定要取消收藏吗?" data-url="{{ favorite_url }}">取消</button>
<button class="layui-btn layui-btn-sm layui-bg-red kg-delete" data-tips="确定要取消收藏吗?" data-url="{{ favorite_url }}">取消</button>
</td>
</tr>
{% endfor %}

View File

@ -0,0 +1,36 @@
{% if pager.total_pages > 0 %}
<table class="layui-table" lay-size="lg">
<colgroup>
<col>
<col>
<col>
<col>
<col width="12%">
</colgroup>
<thead>
<tr>
<th>问题</th>
<th>浏览</th>
<th>点赞</th>
<th>回答</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in pager.items %}
{% set question_url = url({'for':'home.question.show','id':item.id}) %}
{% set favorite_url = url({'for':'home.question.favorite','id':item.id}) %}
<tr>
<td><a href="{{ question_url }}" target="_blank">{{ item.title }}</a></td>
<td>{{ item.view_count }}</td>
<td>{{ item.like_count }}</td>
<td>{{ item.answer_count }}</td>
<td class="center">
<button class="layui-btn layui-btn-sm layui-bg-red kg-delete" data-tips="确定要取消收藏吗?" data-url="{{ favorite_url }}">取消</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endif %}

View File

@ -203,16 +203,6 @@ function kg_default_group_avatar_path()
return '/img/default/group_avatar.png';
}
/**
* 获取默认文章封面路径
*
* @return string
*/
function kg_default_article_cover_path()
{
return '/img/default/article_cover.png';
}
/**
* 获取默认课程封面路径
*
@ -333,20 +323,6 @@ function kg_cos_group_avatar_url($path, $style = null)
return kg_cos_img_url($path, $style);
}
/**
* 获取文章封面URL
*
* @param string $path
* @param string $style
* @return string
*/
function kg_cos_article_cover_url($path, $style = null)
{
$path = $path ?: kg_default_article_cover_path();
return kg_cos_img_url($path, $style);
}
/**
* 获取课程封面URL
*

View File

@ -43,6 +43,13 @@ class Answer extends Model
*/
public $cover = '';
/**
* 概要
*
* @var string
*/
public $summary = '';
/**
* 内容
*
@ -150,6 +157,10 @@ class Answer extends Model
$this->cover = kg_parse_first_content_image($this->content);
}
if (empty($this->summary)) {
$this->summary = kg_parse_summary($this->content);
}
$this->create_time = time();
}
@ -159,6 +170,10 @@ class Answer extends Model
$this->cover = kg_parse_first_content_image($this->content);
}
if (empty($this->summary)) {
$this->summary = kg_parse_summary($this->content);
}
$this->update_time = time();
}
@ -178,4 +193,12 @@ class Answer extends Model
];
}
public static function sortTypes()
{
return [
'latest' => '最新',
'popular' => '最热',
];
}
}

View File

@ -19,8 +19,11 @@ class PointHistory extends Model
const EVENT_COMMENT_POST = 9; // 发布评论
const EVENT_ARTICLE_POST = 10; // 发布文章
const EVENT_QUESTION_POST = 11; // 发布问题
const EVENT_ANSWER_POST = 12; // 发布答案
const EVENT_ANSWER_ACCEPT = 13; // 采纳答案
const EVENT_ANSWER_POST = 12; // 发布回答
const EVENT_ANSWER_ACCEPTED = 13; // 回答被采纳
const EVENT_ANSWER_LIKED = 14; // 回答被点赞
const EVENT_ARTICLE_LIKED = 15; // 文章被点赞
const EVENT_QUESTION_LIKED = 16; // 提问被点赞
/**
* 主键编号

View File

@ -23,4 +23,22 @@ class Reason
];
}
public static function questionRejectOptions()
{
return [
101 => '没有讨论价值',
102 => '错别字,病句过多',
103 => '内容不实',
104 => '标题夸张',
105 => '题文不符',
106 => '低俗色情',
107 => '广告软文',
108 => '恶意对比',
109 => '涉嫌歧视,恶意抹黑',
110 => '归类与主题不符',
111 => '内容涉嫌违法',
112 => '其它问题',
];
}
}

View File

@ -41,7 +41,7 @@ class Answer extends Repository
}
switch ($sort) {
case 'like':
case 'popular':
$orderBy = 'like_count DESC';
break;
case 'latest':

View File

@ -77,7 +77,7 @@ class Article extends Repository
$orderBy = 'like_count DESC';
break;
case 'popular':
$orderBy = 'view_count DESC';
$orderBy = 'score DESC';
break;
case 'comment':
$orderBy = 'comment_count DESC';

View File

@ -6,7 +6,7 @@ use App\Models\Answer as AnswerModel;
use App\Models\User as UserModel;
use App\Services\Logic\AnswerTrait;
use App\Services\Logic\Notice\System\AnswerAccepted as AnswerAcceptedNotice;
use App\Services\Logic\Point\History\AnswerAccept as AnswerAcceptPointHistory;
use App\Services\Logic\Point\History\AnswerAccepted as AnswerAcceptPointHistory;
use App\Services\Logic\QuestionTrait;
use App\Services\Logic\Service as LogicService;
use App\Validators\Answer as AnswerValidator;
@ -35,7 +35,8 @@ class AnswerAccept extends LogicService
$answer->update();
$question->answer_id = $answer->id;
$question->last_answer_id = $answer->id;
$question->last_reply_time = time();
$question->solved = 1;
$question->update();
@ -43,6 +44,8 @@ class AnswerAccept extends LogicService
$this->handleAcceptNotice($answer, $user);
$this->eventsManager->fire('Answer:afterAccept', $this, $answer);
return $answer;
}
protected function handleAcceptPoint(AnswerModel $answer)

View File

@ -6,10 +6,10 @@ use App\Models\Answer as AnswerModel;
use App\Models\Question as QuestionModel;
use App\Models\User as UserModel;
use App\Services\Logic\AnswerTrait;
use App\Services\Logic\Notice\System\QuestionAnswered as QuestionAnsweredNotice;
use App\Services\Logic\Point\History\AnswerPost as AnswerPostPointHistory;
use App\Services\Logic\QuestionTrait;
use App\Services\Logic\Service as LogicService;
use App\Services\Sync\QuestionScore as QuestionScoreSync;
use App\Traits\Client as ClientTrait;
use App\Validators\Answer as AnswerValidator;
@ -53,14 +53,14 @@ class AnswerCreate extends LogicService
$question->update();
$this->syncQuestionScore($question);
$this->incrUserAnswerCount($user);
$this->incrQuestionAnswerCount($question);
$this->handleAnswerPoint($answer);
$this->handleAnswerNotice($answer);
$this->eventsManager->fire('Answer:afterCreate', $this, $answer);
return $answer;
@ -80,11 +80,11 @@ class AnswerCreate extends LogicService
$user->update();
}
protected function syncQuestionScore(QuestionModel $question)
protected function handleAnswerNotice(AnswerModel $answer)
{
$sync = new QuestionScoreSync();
$notice = new QuestionAnsweredNotice();
$sync->addItem($question->id);
$notice->handle($answer);
}
protected function handleAnswerPoint(AnswerModel $answer)

View File

@ -38,6 +38,8 @@ class AnswerDelete extends LogicService
$this->updateQuestionScore($question);
$this->eventsManager->fire('Answer:afterDelete', $this, $answer);
return $answer;
}
protected function decrUserAnswerCount(UserModel $user)

View File

@ -40,6 +40,8 @@ class AnswerUpdate extends LogicService
$this->syncQuestionScore($question);
$this->eventsManager->fire('Answer:afterUpdate', $this, $answer);
return $answer;
}
protected function syncQuestionScore(QuestionModel $question)

View File

@ -0,0 +1,46 @@
<?php
namespace App\Services\Logic\Article;
use App\Models\Article as ArticleModel;
use App\Models\User as UserModel;
use App\Services\Logic\Service as LogicService;
class ArticleCreate extends LogicService
{
use ArticleDataTrait;
public function handle()
{
$post = $this->request->getPost();
$user = $this->getLoginUser();
$article = new ArticleModel();
$data = $this->handlePostData($post);
$data['owner_id'] = $user->id;
$article->create($data);
if (isset($post['xm_tag_ids'])) {
$this->saveTags($article, $post['xm_tag_ids']);
}
$this->incrUserArticleCount($user);
$this->eventsManager->fire('Article:afterCreate', $this, $article);
return $article;
}
protected function incrUserArticleCount(UserModel $user)
{
$user->article_count += 1;
$user->update();
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Services\Logic\Article;
use App\Library\Utils\Word as WordUtil;
use App\Models\Article as ArticleModel;
use App\Models\ArticleTag as ArticleTagModel;
use App\Repos\ArticleTag as ArticleTagRepo;
use App\Repos\Tag as TagRepo;
use App\Traits\Client as ClientTrait;
use App\Validators\Article as ArticleValidator;
trait ArticleDataTrait
{
use ClientTrait;
protected function handlePostData($post)
{
$data = [];
$validator = new ArticleValidator();
$data['title'] = $validator->checkTitle($post['title']);
$data['content'] = $validator->checkContent($post['content']);
$data['word_count'] = WordUtil::getWordCount($data['content']);
if (isset($post['category_id'])) {
$category = $validator->checkCategory($post['category_id']);
$data['category_id'] = $category->id;
}
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'] = $validator->checkAllowCommentStatus($post['allow_comment']);
}
if (isset($post['private'])) {
$data['private'] = $validator->checkPrivateStatus($post['private']);
}
return $data;
}
protected function saveTags(ArticleModel $article, $tagIds)
{
$originTagIds = [];
/**
* 修改数据后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();
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Services\Logic\Article;
use App\Models\Article as ArticleModel;
use App\Models\User as UserModel;
use App\Services\Logic\ArticleTrait;
use App\Services\Logic\Service as LogicService;
use App\Services\Sync\ArticleIndex as ArticleIndexSync;
use App\Validators\Article as ArticleValidator;
class ArticleDelete extends LogicService
{
use ArticleTrait;
public function handle($id)
{
$article = $this->checkArticle($id);
$user = $this->getLoginUser();
$validator = new ArticleValidator();
$validator->checkOwner($user->id, $article->owner_id);
$article->deleted = 1;
$article->update();
$this->decrUserArticleCount($user);
$this->rebuildArticleIndex($article);
$this->eventsManager->fire('Article:afterDelete', $this, $article);
}
protected function decrUserArticleCount(UserModel $user)
{
if ($user->article_count > 0) {
$user->article_count -= 1;
$user->update();
}
}
protected function rebuildArticleIndex(ArticleModel $article)
{
$sync = new ArticleIndexSync();
$sync->addItem($article->id);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Services\Logic\Article;
use App\Models\Article as ArticleModel;
use App\Services\Logic\ArticleTrait;
use App\Services\Logic\Service as LogicService;
class ArticleUpdate extends LogicService
{
use ArticleTrait;
use ArticleDataTrait;
public function handle($id)
{
$post = $this->request->getPost();
$article = $this->checkArticle($id);
$data = $this->handlePostData($post);
if ($article->published == ArticleModel::PUBLISH_REJECTED) {
$data['published'] = ArticleModel::PUBLISH_PENDING;
}
/**
* 当通过审核后,禁止修改部分文章属性
*/
if ($article->published == ArticleModel::PUBLISH_APPROVED) {
unset(
$data['title'],
$data['content'],
$data['source_type'],
$data['source_url'],
$data['category_id'],
$post['xm_tag_ids'],
);
}
$article->update($data);
if (isset($post['xm_tag_ids'])) {
$this->saveTags($article, $post['xm_tag_ids']);
}
$this->eventsManager->fire('Article:afterUpdate', $this, $article);
return $article;
}
}

View File

@ -6,6 +6,8 @@ use App\Models\Article as ArticleModel;
use App\Models\Chapter as ChapterModel;
use App\Models\Comment as CommentModel;
use App\Models\User as UserModel;
use App\Services\Logic\ArticleTrait;
use App\Services\Logic\ChapterTrait;
use App\Services\Logic\Notice\System\ArticleCommented as ArticleCommentedNotice;
use App\Services\Logic\Notice\System\ChapterCommented as ChapterCommentedNotice;
use Phalcon\Di as Di;
@ -14,6 +16,9 @@ use Phalcon\Events\Manager as EventsManager;
trait CommentCountTrait
{
use ArticleTrait;
use ChapterTrait;
protected function incrItemCommentCount(CommentModel $comment)
{
if ($comment->item_type == CommentModel::ITEM_CHAPTER) {

View File

@ -0,0 +1,46 @@
<?php
namespace App\Services\Logic\Notice\System;
use App\Models\Answer as AnswerModel;
use App\Models\Notification as NotificationModel;
use App\Repos\Question as QuestionRepo;
use App\Repos\User as UserRepo;
use App\Services\Logic\Service as LogicService;
class QuestionAnswered extends LogicService
{
public function handle(AnswerModel $answer)
{
$question = $this->findQuestion($answer->question_id);
$notification = new NotificationModel();
$notification->sender_id = $answer->owner_id;
$notification->receiver_id = $question->owner_id;
$notification->event_id = $answer->id;
$notification->event_type = NotificationModel::TYPE_QUESTION_ANSWERED;
$notification->event_info = [
'question' => ['id' => $question->id, 'title' => $question->title],
'answer' => ['id' => $answer->id, 'summary' => $answer->summary],
];
$notification->create();
}
protected function findQuestion($id)
{
$questionRepo = new QuestionRepo();
return $questionRepo->findById($id);
}
protected function findUser($id)
{
$userRepo = new UserRepo();
return $userRepo->findById($id);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Services\Logic\Notice\System;
use App\Models\Notification as NotificationModel;
use App\Models\Question as QuestionModel;
use App\Models\User as UserModel;
use App\Services\Logic\Service as LogicService;
class QuestionApproved extends LogicService
{
public function handle(QuestionModel $question, UserModel $sender)
{
$notification = new NotificationModel();
$notification->sender_id = $sender->id;
$notification->receiver_id = $question->owner_id;
$notification->event_id = $question->id;
$notification->event_type = NotificationModel::TYPE_QUESTION_APPROVED;
$notification->event_info = [
'question' => ['id' => $question->id, 'title' => $question->title],
];
$notification->create();
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Services\Logic\Notice\System;
use App\Models\Notification as NotificationModel;
use App\Models\Question as QuestionModel;
use App\Models\User as UserModel;
use App\Services\Logic\Service as LogicService;
class QuestionRejected extends LogicService
{
public function handle(QuestionModel $question, UserModel $sender, $reason = null)
{
$notification = new NotificationModel();
$notification->sender_id = $sender->id;
$notification->receiver_id = $question->owner_id;
$notification->event_id = $question->id;
$notification->event_type = NotificationModel::TYPE_QUESTION_REJECTED;
$notification->event_info = [
'question' => ['id' => $question->id, 'title' => $question->title],
'reason' => $reason ?: '',
];
$notification->create();
}
}

View File

@ -9,7 +9,7 @@ use App\Repos\Question as QuestionRepo;
use App\Repos\User as UserRepo;
use App\Services\Logic\Point\PointHistory;
class AnswerAccept extends PointHistory
class AnswerAccepted extends PointHistory
{
public function handle(AnswerModel $answer)
@ -22,16 +22,16 @@ class AnswerAccept extends PointHistory
$eventRule = json_decode($setting['event_rule'], true);
$eventEnabled = $eventRule['answer_accept']['enabled'] ?? 0;
$eventEnabled = $eventRule['answer_accepted']['enabled'] ?? 0;
if ($eventEnabled == 0) return;
$eventPoint = $eventRule['answer_accept']['point'] ?? 0;
$eventPoint = $eventRule['answer_accepted']['point'] ?? 0;
if ($eventPoint <= 0) return;
$eventId = $answer->id;
$eventType = PointHistoryModel::EVENT_ANSWER_ACCEPT;
$eventType = PointHistoryModel::EVENT_ANSWER_ACCEPTED;
$historyRepo = new PointHistoryRepo();

View File

@ -0,0 +1,85 @@
<?php
namespace App\Services\Logic\Point\History;
use App\Models\AnswerLike as AnswerLikeModel;
use App\Models\PointHistory as PointHistoryModel;
use App\Repos\Answer as AnswerRepo;
use App\Repos\PointHistory as PointHistoryRepo;
use App\Repos\Question as QuestionRepo;
use App\Repos\User as UserRepo;
use App\Services\Logic\Point\PointHistory;
class AnswerLiked extends PointHistory
{
public function handle(AnswerLikeModel $answerLike)
{
$setting = $this->getSettings('point');
$pointEnabled = $setting['enabled'] ?? 0;
if ($pointEnabled == 0) return;
$eventRule = json_decode($setting['event_rule'], true);
$eventEnabled = $eventRule['answer_liked']['enabled'] ?? 0;
if ($eventEnabled == 0) return;
$eventPoint = $eventRule['answer_liked']['point'] ?? 0;
if ($eventPoint <= 0) return;
$dailyPointLimit = $eventRule['answer_liked']['limit'] ?? 0;
if ($dailyPointLimit <= 0) return;
$eventId = $answerLike->id;
$eventType = PointHistoryModel::EVENT_ANSWER_LIKED;
$historyRepo = new PointHistoryRepo();
$history = $historyRepo->findEventHistory($eventId, $eventType);
if ($history) return;
$answerRepo = new AnswerRepo();
$answer = $answerRepo->findById($answerLike->answer_id);
/**
* @todo 使用缓存优化
*/
$dailyPoints = $historyRepo->sumUserDailyEventPoints($answer->owner_id, $eventType, date('Ymd'));
if ($dailyPoints >= $dailyPointLimit) return;
$questionRepo = new QuestionRepo();
$question = $questionRepo->findById($answer->question_id);
$userRepo = new UserRepo();
$user = $userRepo->findById($answer->owner_id);
$eventInfo = [
'question' => [
'id' => $question->id,
'title' => $question->title,
]
];
$history = new PointHistoryModel();
$history->user_id = $user->id;
$history->user_name = $user->name;
$history->event_id = $eventId;
$history->event_type = $eventType;
$history->event_info = $eventInfo;
$history->event_point = $eventPoint;
$this->handlePointHistory($history);
}
}

View File

@ -2,9 +2,11 @@
namespace App\Services\Logic\Question;
use App\Models\Question as QuestionModel;
use App\Models\User as UserModel;
use App\Services\Logic\QuestionTrait;
use App\Services\Logic\Service as LogicService;
use App\Services\Sync\QuestionIndex as QuestionIndexSync;
use App\Validators\Question as QuestionValidator;
class QuestionDelete extends LogicService
@ -22,7 +24,19 @@ class QuestionDelete extends LogicService
$validator->checkOwner($user->id, $question->owner_id);
$validator->checkIfAllowDelete($question);
$question->deleted = 1;
$question->update();
$this->decrUserQuestionCount($user);
$this->rebuildQuestionIndex($question);
$this->eventsManager->fire('Question:afterDelete', $this, $question);
return $question;
}
protected function decrUserQuestionCount(UserModel $user)
@ -33,4 +47,11 @@ class QuestionDelete extends LogicService
}
}
protected function rebuildQuestionIndex(QuestionModel $question)
{
$sync = new QuestionIndexSync();
$sync->addItem($question->id);
}
}

View File

@ -37,9 +37,9 @@ class QuestionInfo extends LogicService
{
$content = kg_parse_markdown($question->content);
$lastReplier = $this->handleUserInfo($question->last_replier_id);
$category = $this->handleCategoryInfo($question);
$owner = $this->handleUserInfo($question->owner_id);
$replier = $this->handleUserInfo($question->replier_id);
$me = $this->handleMeInfo($question, $user);
return [
@ -48,10 +48,6 @@ class QuestionInfo extends LogicService
'summary' => $question->summary,
'content' => $content,
'tags' => $question->tags,
'category' => $category,
'owner' => $owner,
'replier' => $replier,
'me' => $me,
'bounty' => $question->bounty,
'anonymous' => $question->anonymous,
'solved' => $question->solved,
@ -61,9 +57,13 @@ class QuestionInfo extends LogicService
'answer_count' => $question->answer_count,
'comment_count' => $question->comment_count,
'favorite_count' => $question->favorite_count,
'reply_time' => $question->reply_time,
'last_reply_time' => $question->last_reply_time,
'create_time' => $question->create_time,
'update_time' => $question->update_time,
'last_replier' => $lastReplier,
'category' => $category,
'owner' => $owner,
'me' => $me,
];
}

View File

@ -63,6 +63,18 @@ class Question extends Handler
$item['cover'] = $baseUrl . $item['cover'];
}
$lastAnswer = json_decode($item['last_answer'], true);
if (!empty($lastAnswer['cover']) && !Text::startsWith($lastAnswer['cover'], 'http')) {
$item['last_answer'] = $baseUrl . $lastAnswer['cover'];
}
$acceptAnswer = json_decode($item['accept_answer'], true);
if (!empty($acceptAnswer['cover']) && !Text::startsWith($acceptAnswer['cover'], 'http')) {
$item['accept_answer'] = $baseUrl . $acceptAnswer['cover'];
}
$items[] = [
'id' => (int)$item['id'],
'title' => (string)$item['title'],
@ -72,16 +84,18 @@ class Question extends Handler
'anonymous' => (int)$item['anonymous'],
'solved' => (int)$item['solved'],
'create_time' => (int)$item['create_time'],
'reply_time' => (int)$item['reply_time'],
'last_reply_time' => (int)$item['last_reply_time'],
'view_count' => (int)$item['view_count'],
'like_count' => (int)$item['like_count'],
'answer_count' => (int)$item['answer_count'],
'comment_count' => (int)$item['comment_count'],
'favorite_count' => (int)$item['favorite_count'],
'category' => json_decode($item['category'], true),
'tags' => json_decode($item['tags'], true),
'owner' => json_decode($item['owner'], true),
'replier' => json_decode($item['replier'], true),
'category' => json_decode($item['category'], true),
'last_replier' => json_decode($item['last_replier'], true),
'last_answer' => $item['last_answer'],
'accept_answer' => $item['accept_answer'],
];
}

View File

@ -4,7 +4,7 @@ namespace App\Services\Logic\User\Console;
use App\Library\Paginator\Query as PagerQuery;
use App\Repos\Article as ArticleRepo;
use App\Services\Logic\Article\TagList as ArticleListService;
use App\Services\Logic\Article\ArticleList as ArticleListService;
use App\Services\Logic\Service as LogicService;
class ArticleList extends LogicService

View File

@ -4,9 +4,11 @@ namespace App\Services\Logic\User\Console;
use App\Builders\ArticleFavoriteList as ArticleFavoriteListBuilder;
use App\Builders\CourseFavoriteList as CourseFavoriteListBuilder;
use App\Builders\QuestionFavoriteList as QuestionFavoriteListBuilder;
use App\Library\Paginator\Query as PagerQuery;
use App\Repos\ArticleFavorite as ArticleFavoriteRepo;
use App\Repos\CourseFavorite as CourseFavoriteRepo;
use App\Repos\QuestionFavorite as QuestionFavoriteRepo;
use App\Services\Logic\Service as LogicService;
class FavoriteList extends LogicService
@ -43,6 +45,14 @@ class FavoriteList extends LogicService
$pager = $favoriteRepo->paginate($params, $sort, $page, $limit);
return $this->handleArticles($pager);
} elseif ($type == 'question') {
$favoriteRepo = new QuestionFavoriteRepo();
$pager = $favoriteRepo->paginate($params, $sort, $page, $limit);
return $this->handleQuestions($pager);
}
}
@ -94,4 +104,28 @@ class FavoriteList extends LogicService
return $pager;
}
protected function handleQuestions($pager)
{
if ($pager->total_items == 0) {
return $pager;
}
$builder = new QuestionFavoriteListBuilder();
$relations = $pager->items->toArray();
$questions = $builder->getQuestions($relations);
$items = [];
foreach ($relations as $relation) {
$question = $questions[$relation['question_id']] ?? new \stdClass();
$items[] = $question;
}
$pager->items = $items;
return $pager;
}
}

View File

@ -58,20 +58,6 @@ class MyStorage extends Storage
return $this->putFile($key, $filename);
}
/**
* 上传默认文章封面
*
* @return false|mixed|string
*/
public function uploadDefaultArticleCover()
{
$filename = static_path('admin/img/default/article_cover.png');
$key = '/img/default/article_cover.png';
return $this->putFile($key, $filename);
}
/**
* 上传默认课程封面
*

View File

@ -3,8 +3,8 @@
namespace App\Services\Search;
use App\Models\Article as ArticleModel;
use App\Models\Category as CategoryModel;
use App\Models\User as UserModel;
use App\Repos\Category as CategoryRepo;
use App\Repos\User as UserRepo;
use Phalcon\Mvc\User\Component;
class ArticleDocument extends Component
@ -35,6 +35,10 @@ class ArticleDocument extends Component
*/
public function formatDocument(ArticleModel $article)
{
if (empty($article->summary)) {
$article->summary = kg_parse_summary($article->content);
}
if (is_array($article->tags) || is_object($article->tags)) {
$article->tags = kg_json_encode($article->tags);
}
@ -42,25 +46,13 @@ class ArticleDocument extends Component
$owner = '{}';
if ($article->owner_id > 0) {
$record = UserModel::findFirst($article->owner_id);
$owner = kg_json_encode([
'id' => $record->id,
'name' => $record->name,
]);
$owner = $this->handleUser($article->owner_id);
}
$category = '{}';
if ($article->category_id > 0) {
$record = CategoryModel::findFirst($article->category_id);
$category = kg_json_encode([
'id' => $record->id,
'name' => $record->name,
]);
}
if (empty($article->summary)) {
$article->summary = kg_parse_summary($article->content);
$category = $this->handleCategory($article->category_id);
}
return [
@ -68,17 +60,41 @@ class ArticleDocument extends Component
'title' => $article->title,
'cover' => $article->cover,
'summary' => $article->summary,
'tags' => $article->tags,
'category_id' => $article->category_id,
'owner_id' => $article->owner_id,
'create_time' => $article->create_time,
'tags' => $article->tags,
'category' => $category,
'owner' => $owner,
'view_count' => $article->view_count,
'like_count' => $article->like_count,
'comment_count' => $article->comment_count,
'favorite_count' => $article->favorite_count,
'category' => $category,
'owner' => $owner,
];
}
protected function handleUser($id)
{
$userRepo = new UserRepo();
$user = $userRepo->findById($id);
return kg_json_encode([
'id' => $user->id,
'name' => $user->name,
]);
}
protected function handleCategory($id)
{
$categoryRepo = new CategoryRepo();
$category = $categoryRepo->findById($id);
return kg_json_encode([
'id' => $category->id,
'name' => $category->name,
]);
}
}

View File

@ -2,10 +2,10 @@
namespace App\Services\Search;
use App\Models\Answer as AnswerModel;
use App\Models\Category as CategoryModel;
use App\Models\Question as QuestionModel;
use App\Models\User as UserModel;
use App\Repos\Answer as AnswerRepo;
use App\Repos\Category as CategoryRepo;
use App\Repos\User as UserRepo;
use Phalcon\Mvc\User\Component;
class QuestionDocument extends Component
@ -47,51 +47,31 @@ class QuestionDocument extends Component
$category = '{}';
if ($question->category_id > 0) {
$record = CategoryModel::findFirst($question->category_id);
$category = kg_json_encode([
'id' => $record->id,
'name' => $record->name,
]);
$category = $this->handleCategory($question->category_id);
}
$owner = '{}';
if ($question->owner_id > 0) {
$record = UserModel::findFirst($question->owner_id);
$owner = kg_json_encode([
'id' => $record->id,
'name' => $record->name,
]);
$owner = $this->handleUser($question->owner_id);
}
$lastReplier = '{}';
if ($question->last_replier_id > 0) {
$record = UserModel::findFirst($question->last_replier_id);
$lastReplier = kg_json_encode([
'id' => $record->id,
'name' => $record->name,
]);
$lastReplier = $this->handleUser($question->last_replier_id);
}
$lastAnswer = '{}';
if ($question->last_answer_id > 0) {
$record = AnswerModel::findFirst($question->last_answer_id);
$lastAnswer = kg_json_encode([
'id' => $record->id,
'summary' => kg_parse_summary($record->content),
]);
$lastAnswer = $this->handleAnswer($question->last_answer_id);
}
$acceptAnswer = '{}';
if ($question->accept_answer_id > 0) {
$record = AnswerModel::findFirst($question->accept_answer_id);
$lastAnswer = kg_json_encode([
'id' => $record->id,
'summary' => kg_parse_summary($record->content),
]);
$acceptAnswer = $this->handleAnswer($question->accept_answer_id);
}
return [
@ -99,16 +79,11 @@ class QuestionDocument extends Component
'title' => $question->title,
'cover' => $question->cover,
'summary' => $question->summary,
'tags' => $question->tags,
'category_id' => $question->category_id,
'owner_id' => $question->owner_id,
'create_time' => $question->create_time,
'last_reply_time' => $question->last_reply_time,
'tags' => $question->tags,
'category' => $category,
'owner' => $owner,
'last_replier' => $lastReplier,
'last_answer' => $lastAnswer,
'accept_answer' => $acceptAnswer,
'bounty' => $question->bounty,
'anonymous' => $question->anonymous,
'solved' => $question->solved,
@ -117,7 +92,49 @@ class QuestionDocument extends Component
'answer_count' => $question->answer_count,
'comment_count' => $question->comment_count,
'favorite_count' => $question->favorite_count,
'category' => $category,
'owner' => $owner,
'last_replier' => $lastReplier,
'last_answer' => $lastAnswer,
'accept_answer' => $acceptAnswer,
];
}
protected function handleUser($id)
{
$userRepo = new UserRepo();
$user = $userRepo->findById($id);
return kg_json_encode([
'id' => $user->id,
'name' => $user->name,
]);
}
protected function handleCategory($id)
{
$categoryRepo = new CategoryRepo();
$category = $categoryRepo->findById($id);
return kg_json_encode([
'id' => $category->id,
'name' => $category->name,
]);
}
protected function handleAnswer($id)
{
$answerRepo = new AnswerRepo();
$answer = $answerRepo->findById($id);
return kg_json_encode([
'id' => $answer->id,
'summary' => $answer->summary,
'cover' => $answer->cover,
]);
}
}

View File

@ -7,6 +7,7 @@ use App\Caches\MaxArticleId as MaxArticleIdCache;
use App\Exceptions\BadRequest as BadRequestException;
use App\Library\Validators\Common as CommonValidator;
use App\Models\Article as ArticleModel;
use App\Models\Reason as ReasonModel;
use App\Repos\Article as ArticleRepo;
class Article extends Validator
@ -84,30 +85,6 @@ class Article extends Validator
return $value;
}
public function checkCover($cover)
{
$value = $this->filter->sanitize($cover, ['trim', 'string']);
if (!CommonValidator::url($value)) {
throw new BadRequestException('article.invalid_cover');
}
return kg_cos_img_style_trim($value);
}
public function checkSummary($summary)
{
$value = $this->filter->sanitize($summary, ['trim', 'string']);
$length = kg_strlen($value);
if ($length > 255) {
throw new BadRequestException('article.summary_too_long');
}
return $value;
}
public function checkContent($content)
{
$value = $this->filter->sanitize($content, ['trim']);
@ -181,4 +158,11 @@ class Article extends Validator
return $status;
}
public function checkRejectReason($reason)
{
if (!array_key_exists($reason, ReasonModel::questionRejectOptions())) {
throw new BadRequestException('article.invalid_reject_reason');
}
}
}

View File

@ -6,6 +6,7 @@ use App\Caches\MaxQuestionId as MaxQuestionIdCache;
use App\Caches\Question as QuestionCache;
use App\Exceptions\BadRequest as BadRequestException;
use App\Models\Question as QuestionModel;
use App\Models\Reason as ReasonModel;
use App\Repos\Question as QuestionRepo;
class Question extends Validator
@ -59,6 +60,13 @@ class Question extends Validator
}
}
public function checkCategory($id)
{
$validator = new Category();
return $validator->checkCategory($id);
}
public function checkTitle($title)
{
$value = $this->filter->sanitize($title, ['trim', 'string']);
@ -116,6 +124,13 @@ class Question extends Validator
return $status;
}
public function checkRejectReason($reason)
{
if (!array_key_exists($reason, ReasonModel::questionRejectOptions())) {
throw new BadRequestException('question.invalid_reject_reason');
}
}
public function checkIfAllowEdit(QuestionModel $question)
{
$approved = $question->published == QuestionModel::PUBLISH_APPROVED;
@ -127,4 +142,15 @@ class Question extends Validator
}
}
public function checkIfAllowDelete(QuestionModel $question)
{
$approved = $question->published == QuestionModel::PUBLISH_APPROVED;
$answered = $question->answer_count > 0;
if ($approved && $answered) {
throw new BadRequestException('question.delete_not_allowed');
}
}
}

View File

@ -113,16 +113,15 @@ $error['nav.has_child_node'] = '不允许相关操作(存在子节点)';
$error['article.not_found'] = '课程不存在';
$error['article.title_too_short'] = '标题太短少于5个字符';
$error['article.title_too_long'] = '标题太长多于50个字符';
$error['article.summary_too_long'] = '概要太长多于255个字符';
$error['article.content_too_short'] = '内容太短少于10个字符';
$error['article.content_too_long'] = '内容太长多于30000个字符';
$error['course.invalid_cover'] = '无效的封面';
$error['article.invalid_source_type'] = '无效的来源类型';
$error['article.invalid_source_url'] = '无效的来源网址';
$error['course.invalid_feature_status'] = '无效的推荐状态';
$error['course.invalid_publish_status'] = '无效的发布状态';
$error['course.invalid_private_status'] = '无效的私有状态';
$error['course.invalid_allow_comment_status'] = '无效的允许评论状态';
$error['article.invalid_feature_status'] = '无效的推荐状态';
$error['article.invalid_publish_status'] = '无效的发布状态';
$error['article.invalid_private_status'] = '无效的私有状态';
$error['article.invalid_allow_comment_status'] = '无效的允许评论状态';
$error['article.invalid_reject_reason'] = '无效的拒绝理由';
/**
* 问答相关
@ -130,10 +129,10 @@ $error['course.invalid_allow_comment_status'] = '无效的允许评论状态';
$error['question.not_found'] = '问题不存在';
$error['question.title_too_short'] = '标题太短少于5个字符';
$error['question.title_too_long'] = '标题太长多于50个字符';
$error['question.summary_too_long'] = '概要太长多于255个字符';
$error['question.content_too_short'] = '内容太短少于10个字符';
$error['question.content_too_long'] = '内容太长多于30000个字符';
$error['question.invalid_publish_status'] = '无效的发布状态';
$error['question.invalid_reject_reason'] = '无效的拒绝理由';
$error['question.edit_not_allowed'] = '当前不允许编辑问题';
$error['question.delete_not_allowed'] = '当前不允许删除问题';

View File

@ -516,13 +516,22 @@ final class V20210430023157 extends AbstractMigration
'comment' => '封面',
'after' => 'question_id',
])
->addColumn('summary', 'string', [
'null' => false,
'default' => '',
'limit' => 255,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
'comment' => '概要',
'after' => 'cover',
])
->addColumn('content', 'text', [
'null' => false,
'limit' => 65535,
'collation' => 'utf8mb4_general_ci',
'encoding' => 'utf8mb4',
'comment' => '内容',
'after' => 'cover',
'after' => 'summary',
])
->addColumn('anonymous', 'integer', [
'null' => false,

View File

@ -333,3 +333,33 @@ img.kg-qrcode {
width: 100%;
height: 480px;
}
.kg-mod-preview {
width: 800px;
}
.kg-mod-preview .title {
margin-bottom: 15px;
font-size: 24px;
}
.kg-mod-preview .meta {
margin-bottom: 20px;
color: #666;
}
.kg-mod-preview .meta span {
margin-right: 10px;
}
.kg-mod-preview .content {
margin-bottom: 20px;
}
.kg-mod-preview .tags {
margin-bottom: 20px;
}
.kg-review-form {
width: 50%;
}

View File

@ -59,7 +59,8 @@ layui.use(['jquery'], function () {
markdown: {
chinesePunct: true,
autoSpace: true
}
},
actions: []
},
fullscreen: {
index: 9999

View File

@ -341,7 +341,9 @@
}
.search-course-card,
.search-article-card,
.search-group-card {
display: flex;
padding-bottom: 15px;
margin-bottom: 15px;
border-bottom: 1px solid #f6f6f6;
@ -355,7 +357,6 @@
}
.search-course-card .cover {
float: left;
width: 210px;
height: 118px;
margin-right: 15px;
@ -368,7 +369,6 @@
}
.search-course-card .info {
float: left;
width: 500px;
}
@ -378,7 +378,8 @@
white-space: nowrap;
}
.search-course-card .summary {
.search-course-card .summary,
.search-group-card .about {
margin-bottom: 10px;
line-height: 1.5em;
max-height: 4.5em;
@ -389,12 +390,16 @@
}
.search-course-card .meta,
.search-group-card .meta {
.search-group-card .meta,
.search-article-card .meta,
.search-question-card .meta {
color: #999;
}
.search-course-card .meta span,
.search-group-card .meta span {
.search-group-card .meta span,
.search-article-card .meta span,
.search-question-card .meta span {
margin-right: 10px;
}
@ -407,7 +412,6 @@
}
.search-group-card .avatar {
float: left;
width: 90px;
height: 90px;
margin-right: 15px;
@ -418,16 +422,9 @@
}
.search-group-card .info {
float: left;
width: 600px;
}
.search-group-card .about {
margin-bottom: 10px;
line-height: 1.5em;
color: #666;
}
.query-badge {
padding: 2px 5px;
margin-right: 5px;
@ -574,16 +571,27 @@
}
.article-info .meta {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
color: #666;
color: #999;
}
.article-info .meta span {
.article-info .meta a {
color: #999;
}
.article-info .meta .left span {
margin-right: 10px;
}
.article-info .meta .right span {
margin-left: 10px;
cursor: pointer;
}
.article-info .content {
margin-bottom: 15px;
margin-bottom: 20px;
}
.article-info .tags {
@ -598,10 +606,32 @@
color: #666;
}
.answer-form {
.answer-tips {
margin-bottom: 20px;
}
.answer-tips h3 {
margin-bottom: 5px;
}
.answer-tips ul {
color: #666;
}
.answer-tips .suggest-text,
.answer-tips .layui-icon-ok-circle {
color: green;
}
.answer-tips .not-suggest-text,
.answer-tips .layui-icon-close-fill {
color: red;
}
.answer-tips .suggest-list {
margin-bottom: 15px;
}
.question-card .stats {
display: flex;
width: 120px;
@ -666,7 +696,7 @@
}
.comment-card .info {
width: 680px;
width: 100%;
}
.comment-card .user {
@ -697,12 +727,12 @@
display: flex;
}
.comment-card .footer .column {
.comment-card .footer .left .column {
margin-right: 10px;
}
.comment-card .footer .column:last-child {
margin-right: 0;
.comment-card .footer .right .column {
margin-left: 10px;
}
.comment-card .footer .action:hover {

View File

@ -1,47 +1,23 @@
layui.use(['jquery', 'form', 'layer', 'helper'], function () {
layui.use(['jquery', 'layer', 'helper'], function () {
var $ = layui.jquery;
var form = layui.form;
var layer = layui.layer;
var helper = layui.helper;
form.on('submit(add_answer)', function (data) {
var index = parent.layer.getFrameIndex(window.name);
$('body').on('click', '.answer-report', function () {
var url = $(this).data('url');
helper.checkLogin(function () {
$.ajax({
type: 'POST',
url: data.form.action,
data: data.field,
success: function (res) {
parent.layer.close(index);
parent.location.reload();
},
error: function (xhr) {
var res = JSON.parse(xhr.responseText);
layer.msg(res.msg);
url: url,
success: function () {
}
});
return false;
});
});
form.on('submit(edit_answer)', function (data) {
var index = parent.layer.getFrameIndex(window.name);
$.ajax({
type: 'POST',
url: data.form.action,
data: data.field,
success: function (res) {
parent.layer.close(index);
parent.location.reload();
},
error: function (xhr) {
var res = JSON.parse(xhr.responseText);
layer.msg(res.msg);
}
});
return false;
});
$('body').on('click', '.action-like', function () {
$('body').on('click', '.answer-like', function () {
var $this = $(this);
var $likeCount = $this.prev();
var likeCount = $likeCount.data('count');
@ -63,44 +39,8 @@ layui.use(['jquery', 'form', 'layer', 'helper'], function () {
});
});
$('body').on('click', '.action-accept', function () {
});
$('body').on('click', '.action-edit', function () {
layer.open({
type: 2,
title: '编辑答案',
content: $(this).data('url'),
area: ['800px', '600px']
});
});
$('body').on('click', '.action-delete', function () {
var $this = $(this);
var id = $this.data('id');
var parentId = $this.data('parent-id');
var $answer = $('#answer-' + id);
var $tbAnswerCount = $('#toolbar-answer > .text');
var tbAnswerCount = $tbAnswerCount.data('count');
layer.confirm('确定要删除吗?', function () {
$.ajax({
type: 'POST',
url: $this.data('url'),
success: function () {
if (parentId > 0) {
var $replyCount = $('#answer-' + parentId).find('.reply-count');
var replyCount = $replyCount.data('count');
replyCount--;
$replyCount.data('count', replyCount).text(replyCount);
}
tbAnswerCount--;
$tbAnswerCount.data('count', tbAnswerCount).text(tbAnswerCount);
$answer.remove();
layer.msg('删除评论成功');
}
});
});
$('body').on('click', '.answer-edit', function () {
window.location.href = $(this).data('url');
});
});

View File

@ -14,6 +14,19 @@ layui.use(['jquery', 'helper'], function () {
helper.ajaxLoadHtml($commentList.data('url'), $commentList.attr('id'));
}
$('.article-report').on('click', function () {
/**
* @todo
*/
});
$('.article-edit').on('click', function () {
var url = $(this).data('url');
helper.checkLogin(function () {
window.location.href = url;
});
});
$('.icon-star').on('click', function () {
var $this = $(this);
var $parent = $this.parent();

View File

@ -102,7 +102,7 @@ layui.use(['jquery', 'form', 'layer', 'helper'], function () {
$('#comment-form-' + id).hide();
});
$('body').on('click', '.action-toggle', function () {
$('body').on('click', '.comment-toggle', function () {
var $this = $(this);
var id = $this.data('id');
var url = $this.data('url');
@ -127,7 +127,7 @@ layui.use(['jquery', 'form', 'layer', 'helper'], function () {
}
});
$('body').on('click', '.action-reply', function () {
$('body').on('click', '.comment-reply', function () {
var id = $(this).data('id');
var $block = $('#comment-form-' + id);
var $textarea = $block.find('textarea');
@ -135,7 +135,7 @@ layui.use(['jquery', 'form', 'layer', 'helper'], function () {
$textarea.focus();
});
$('body').on('click', '.action-like', function () {
$('body').on('click', '.comment-like', function () {
var $this = $(this);
var $likeCount = $this.prev();
var likeCount = $likeCount.data('count');
@ -157,7 +157,7 @@ layui.use(['jquery', 'form', 'layer', 'helper'], function () {
});
});
$('body').on('click', '.action-delete', function () {
$('body').on('click', '.comment-delete', function () {
var $this = $(this);
var id = $this.data('id');
var parentId = $this.data('parent-id');

View File

@ -1,7 +1,6 @@
layui.use(['jquery', 'layer', 'helper'], function () {
layui.use(['jquery', 'helper'], function () {
var $ = layui.jquery;
var layer = layui.layer;
var helper = layui.helper;
var $answerList = $('#answer-list');
@ -15,15 +14,27 @@ layui.use(['jquery', 'layer', 'helper'], function () {
helper.ajaxLoadHtml($sidebarRelated.data('url'), $sidebarRelated.attr('id'));
}
$('.layui-tab-title > li').on('click', function () {
helper.ajaxLoadHtml($(this).data('url'), $answerList.attr('id'));
});
$('.question-report').on('click', function () {
/**
* @todo
*/
});
$('.question-edit').on('click', function () {
var url = $(this).data('url');
helper.checkLogin(function () {
window.location.href = url;
});
});
$('.btn-answer').on('click', function () {
var url = $(this).data('url');
helper.checkLogin(function () {
layer.open({
type: 2,
title: '回答问题',
content: url,
area: ['800px', '600px']
});
window.location.href = url;
});
});