1
0
mirror of https://gitee.com/koogua/course-tencent-cloud.git synced 2025-06-24 20:06:09 +08:00

Merge branch 'develop'

This commit is contained in:
koogua 2021-04-29 18:41:00 +08:00
commit 1ce43f6a75
156 changed files with 4402 additions and 793 deletions

View File

@ -1,3 +1,16 @@
### [v1.3.3](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.3)(2021-04-30)
### 更新
- 前台增加文章发布功能
- 增加文章,咨询,评价,评论相关事件站内提醒
- 增加文章,咨询,评价,评论事件埋点
- 后台首页增加若干统计项目
- 后台增加文章审核功能
- 重构积分历史记录
- 优化在线统计方式
- 优化前台界面
### [v1.3.2](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.2)(2021-04-20) ### [v1.3.2](https://gitee.com/koogua/course-tencent-cloud/releases/v1.3.2)(2021-04-20)
### 更新 ### 更新

View File

@ -0,0 +1,44 @@
<?php
namespace App\Builders;
use App\Repos\User as UserRepo;
class NotificationList extends Builder
{
public function handleUsers(array $notifications)
{
$users = $this->getUsers($notifications);
foreach ($notifications as $key => $notification) {
$notifications[$key]['sender'] = $users[$notification['sender_id']] ?? new \stdClass();
$notifications[$key]['receiver'] = $users[$notification['receiver_id']] ?? new \stdClass();
}
return $notifications;
}
public function getUsers(array $notifications)
{
$senderIds = kg_array_column($notifications, 'sender_id');
$receiverIds = kg_array_column($notifications, 'receiver_id');
$ids = array_merge($senderIds, $receiverIds);
$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

@ -40,7 +40,7 @@ class ArticleRelatedList extends Cache
$where = [ $where = [
'tag_id' => $tagIds[$randKey], 'tag_id' => $tagIds[$randKey],
'published' => 1, 'published' => ArticleModel::PUBLISH_APPROVED,
]; ];
$pager = $articleRepo->paginate($where); $pager = $articleRepo->paginate($where);

View File

@ -0,0 +1,35 @@
<?php
namespace App\Caches;
use App\Repos\Stat as StatRepo;
class ModerationStat extends Cache
{
protected $lifetime = 15 * 60;
public function getLifetime()
{
return $this->lifetime;
}
public function getKey($id = null)
{
return 'moderation_stat';
}
public function getContent($id = null)
{
$statRepo = new StatRepo();
$articleCount = $statRepo->countPendingArticles();
$commentCount = $statRepo->countPendingComments();
return [
'article_count' => $articleCount,
'comment_count' => $commentCount,
];
}
}

View File

@ -2,10 +2,11 @@
namespace App\Caches; namespace App\Caches;
use App\Repos\Article as ArticleRepo;
use App\Repos\Comment as CommentRepo;
use App\Repos\Consult as ConsultRepo; use App\Repos\Consult as ConsultRepo;
use App\Repos\Course as CourseRepo; use App\Repos\Course as CourseRepo;
use App\Repos\ImGroup as GroupRepo; use App\Repos\ImGroup as GroupRepo;
use App\Repos\Order as OrderRepo;
use App\Repos\Package as PackageRepo; use App\Repos\Package as PackageRepo;
use App\Repos\Review as ReviewRepo; use App\Repos\Review as ReviewRepo;
use App\Repos\Topic as TopicRepo; use App\Repos\Topic as TopicRepo;
@ -14,7 +15,7 @@ use App\Repos\User as UserRepo;
class SiteGlobalStat extends Cache class SiteGlobalStat extends Cache
{ {
protected $lifetime = 2 * 3600; protected $lifetime = 15 * 60;
public function getLifetime() public function getLifetime()
{ {
@ -29,9 +30,10 @@ class SiteGlobalStat extends Cache
public function getContent($id = null) public function getContent($id = null)
{ {
$courseRepo = new CourseRepo(); $courseRepo = new CourseRepo();
$articleRepo = new ArticleRepo();
$commentRepo = new CommentRepo();
$consultRepo = new ConsultRepo(); $consultRepo = new ConsultRepo();
$groupRepo = new GroupRepo(); $groupRepo = new GroupRepo();
$orderRepo = new OrderRepo();
$packageRepo = new PackageRepo(); $packageRepo = new PackageRepo();
$reviewRepo = new ReviewRepo(); $reviewRepo = new ReviewRepo();
$topicRepo = new TopicRepo(); $topicRepo = new TopicRepo();
@ -39,9 +41,11 @@ class SiteGlobalStat extends Cache
return [ return [
'course_count' => $courseRepo->countCourses(), 'course_count' => $courseRepo->countCourses(),
'article_count' => $articleRepo->countArticles(),
'comment_count' => $commentRepo->countComments(),
'consult_count' => $consultRepo->countConsults(), 'consult_count' => $consultRepo->countConsults(),
'group_count' => $groupRepo->countGroups(), 'group_count' => $groupRepo->countGroups(),
'order_count' => $orderRepo->countOrders(), 'vip_count' => $userRepo->countVipUsers(),
'package_count' => $packageRepo->countPackages(), 'package_count' => $packageRepo->countPackages(),
'review_count' => $reviewRepo->countReviews(), 'review_count' => $reviewRepo->countReviews(),
'topic_count' => $topicRepo->countTopics(), 'topic_count' => $topicRepo->countTopics(),

View File

@ -7,7 +7,7 @@ use App\Repos\Stat as StatRepo;
class SiteTodayStat extends Cache class SiteTodayStat extends Cache
{ {
protected $lifetime = 1 * 3600; protected $lifetime = 15 * 60;
public function getLifetime() public function getLifetime()
{ {
@ -26,15 +26,19 @@ class SiteTodayStat extends Cache
$date = date('Y-m-d'); $date = date('Y-m-d');
$saleCount = $statRepo->countDailySales($date); $saleCount = $statRepo->countDailySales($date);
$refundCount = $statRepo->countDailyRefunds($date);
$saleAmount = $statRepo->sumDailySales($date); $saleAmount = $statRepo->sumDailySales($date);
$refundAmount = $statRepo->sumDailyRefunds($date); $refundAmount = $statRepo->sumDailyRefunds($date);
$registerCount = $statRepo->countDailyRegisteredUser($date); $registerCount = $statRepo->countDailyRegisteredUsers($date);
$pointRedeemCount = $statRepo->countDailyPointRedeems($date);
return [ return [
'sale_count' => $saleCount, 'sale_count' => $saleCount,
'refund_count' => $refundCount,
'sale_amount' => $saleAmount, 'sale_amount' => $saleAmount,
'refund_amount' => $refundAmount, 'refund_amount' => $refundAmount,
'register_count' => $registerCount, 'register_count' => $registerCount,
'point_redeem_count' => $pointRedeemCount,
]; ];
} }

View File

@ -14,7 +14,7 @@ use App\Repos\ImGroupUser as ImGroupUserRepo;
use App\Repos\Order as OrderRepo; use App\Repos\Order as OrderRepo;
use App\Repos\User as UserRepo; use App\Repos\User as UserRepo;
use App\Services\Logic\Notice\OrderFinish as OrderFinishNotice; use App\Services\Logic\Notice\OrderFinish as OrderFinishNotice;
use App\Services\Logic\Point\PointHistory as PointHistoryService; use App\Services\Logic\Point\History\OrderConsume as OrderConsumePointHistory;
use Phalcon\Mvc\Model; use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface; use Phalcon\Mvc\Model\ResultsetInterface;
@ -205,9 +205,9 @@ class DeliverTask extends Task
protected function handleOrderConsumePoint(OrderModel $order) protected function handleOrderConsumePoint(OrderModel $order)
{ {
$service = new PointHistoryService(); $service = new OrderConsumePointHistory();
$service->handleOrderConsume($order); $service->handle($order);
} }
protected function handleOrderFinishNotice(OrderModel $order) protected function handleOrderFinishNotice(OrderModel $order)

View File

@ -14,7 +14,7 @@ use App\Repos\ImGroupUser as ImGroupUserRepo;
use App\Repos\PointGift as PointGiftRepo; use App\Repos\PointGift as PointGiftRepo;
use App\Repos\PointRedeem as PointRedeemRepo; use App\Repos\PointRedeem as PointRedeemRepo;
use App\Services\Logic\Notice\DingTalk\PointRedeem as PointRedeemNotice; use App\Services\Logic\Notice\DingTalk\PointRedeem as PointRedeemNotice;
use App\Services\Logic\Point\PointHistory as PointHistoryService; use App\Services\Logic\Point\History\PointRefund as PointRefundPointHistory;
use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface; use Phalcon\Mvc\Model\ResultsetInterface;
@ -54,9 +54,6 @@ class PointGiftDeliverTask extends Task
case PointGiftModel::TYPE_GOODS: case PointGiftModel::TYPE_GOODS:
$this->handleGoodsRedeem($redeem); $this->handleGoodsRedeem($redeem);
break; break;
case PointGiftModel::TYPE_CASH:
$this->handleCashRedeem($redeem);
break;
} }
$task->status = TaskModel::STATUS_FINISHED; $task->status = TaskModel::STATUS_FINISHED;
@ -169,16 +166,11 @@ class PointGiftDeliverTask extends Task
$notice->createTask($redeem); $notice->createTask($redeem);
} }
protected function handleCashRedeem(PointRedeemModel $redeem)
{
}
protected function handlePointRefund(PointRedeemModel $redeem) protected function handlePointRefund(PointRedeemModel $redeem)
{ {
$service = new PointHistoryService(); $service = new PointRefundPointHistory();
$service->handlePointRefund($redeem); $service->handle($redeem);
} }
/** /**

View File

@ -10,7 +10,7 @@ use App\Repos\ChapterUser as ChapterUserRepo;
use App\Repos\Course as CourseRepo; use App\Repos\Course as CourseRepo;
use App\Repos\CourseUser as CourseUserRepo; use App\Repos\CourseUser as CourseUserRepo;
use App\Repos\Learning as LearningRepo; use App\Repos\Learning as LearningRepo;
use App\Services\Logic\Point\PointHistory as PointHistoryService; use App\Services\Logic\Point\History\ChapterStudy as ChapterStudyPointHistory;
use App\Services\Sync\Learning as LearningSyncService; use App\Services\Sync\Learning as LearningSyncService;
class SyncLearningTask extends Task class SyncLearningTask extends Task
@ -184,9 +184,9 @@ class SyncLearningTask extends Task
*/ */
protected function handleStudyPoint(ChapterUserModel $chapterUser) protected function handleStudyPoint(ChapterUserModel $chapterUser)
{ {
$service = new PointHistoryService(); $service = new ChapterStudyPointHistory();
$service->handleChapterStudy($chapterUser); $service->handle($chapterUser);
} }
} }

View File

@ -31,10 +31,12 @@ class ArticleController extends Controller
{ {
$articleService = new ArticleService(); $articleService = new ArticleService();
$publishTypes = $articleService->getPublishTypes();
$sourceTypes = $articleService->getSourceTypes(); $sourceTypes = $articleService->getSourceTypes();
$categories = $articleService->getCategories(); $categories = $articleService->getCategories();
$xmTags = $articleService->getXmTags(0); $xmTags = $articleService->getXmTags(0);
$this->view->setVar('publish_types', $publishTypes);
$this->view->setVar('source_types', $sourceTypes); $this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories); $this->view->setVar('categories', $categories);
$this->view->setVar('xm_tags', $xmTags); $this->view->setVar('xm_tags', $xmTags);
@ -52,6 +54,19 @@ class ArticleController extends Controller
$this->view->setVar('pager', $pager); $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") * @Get("/add", name="admin.article.add")
*/ */
@ -64,6 +79,40 @@ class ArticleController extends Controller
$this->view->setVar('categories', $categories); $this->view->setVar('categories', $categories);
} }
/**
* @Get("/{id:[0-9]+}/edit", name="admin.article.edit")
*/
public function editAction($id)
{
$articleService = new ArticleService();
$publishTypes = $articleService->getPublishTypes();
$sourceTypes = $articleService->getSourceTypes();
$categories = $articleService->getCategories();
$article = $articleService->getArticle($id);
$xmTags = $articleService->getXmTags($id);
$this->view->setVar('publish_types', $publishTypes);
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('article', $article);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Get("/{id:[0-9]+}/show", name="admin.article.show")
*/
public function showAction($id)
{
$articleService = new ArticleService();
$rejectOptions = $articleService->getRejectOptions();
$article = $articleService->getArticle($id);
$this->view->setVar('reject_options', $rejectOptions);
$this->view->setVar('article', $article);
}
/** /**
* @Post("/create", name="admin.article.create") * @Post("/create", name="admin.article.create")
*/ */
@ -86,24 +135,6 @@ class ArticleController extends Controller
return $this->jsonSuccess($content); return $this->jsonSuccess($content);
} }
/**
* @Get("/{id:[0-9]+}/edit", name="admin.article.edit")
*/
public function editAction($id)
{
$articleService = new ArticleService();
$sourceTypes = $articleService->getSourceTypes();
$categories = $articleService->getCategories();
$article = $articleService->getArticle($id);
$xmTags = $articleService->getXmTags($id);
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('article', $article);
$this->view->setVar('xm_tags', $xmTags);
}
/** /**
* @Post("/{id:[0-9]+}/update", name="admin.article.update") * @Post("/{id:[0-9]+}/update", name="admin.article.update")
*/ */
@ -152,4 +183,23 @@ class ArticleController extends Controller
return $this->jsonSuccess($content); return $this->jsonSuccess($content);
} }
/**
* @Post("/{id:[0-9]+}/review", name="admin.article.review")
*/
public function reviewAction($id)
{
$articleService = new ArticleService();
$articleService->reviewArticle($id);
$location = $this->url->get(['for' => 'admin.article.pending_list']);
$content = [
'location' => $location,
'msg' => '审核文章成功',
];
return $this->jsonSuccess($content);
}
} }

View File

@ -37,11 +37,13 @@ class IndexController extends Controller
$globalStat = $indexService->getGlobalStat(); $globalStat = $indexService->getGlobalStat();
$todayStat = $indexService->getTodayStat(); $todayStat = $indexService->getTodayStat();
$modStat = $indexService->getModerationStat();
$appInfo = $indexService->getAppInfo(); $appInfo = $indexService->getAppInfo();
$serverInfo = $indexService->getServerInfo(); $serverInfo = $indexService->getServerInfo();
$this->view->setVar('global_stat', $globalStat); $this->view->setVar('global_stat', $globalStat);
$this->view->setVar('today_stat', $todayStat); $this->view->setVar('today_stat', $todayStat);
$this->view->setVar('mod_stat', $modStat);
$this->view->setVar('app_info', $appInfo); $this->view->setVar('app_info', $appInfo);
$this->view->setVar('server_info', $serverInfo); $this->view->setVar('server_info', $serverInfo);
} }

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Admin\Controllers;
use App\Http\Admin\Services\Moderation as ModerationService;
/**
* @RoutePrefix("/admin/moderation")
*/
class ModerationController extends Controller
{
/**
* @Get("/articles", name="admin.mod.articles")
*/
public function articlesAction()
{
$modService = new ModerationService();
$pager = $modService->getArticles();
$this->view->setVar('pager', $pager);
}
}

View File

@ -9,16 +9,31 @@ use App\Library\Utils\Word as WordUtil;
use App\Models\Article as ArticleModel; use App\Models\Article as ArticleModel;
use App\Models\ArticleTag as ArticleTagModel; use App\Models\ArticleTag as ArticleTagModel;
use App\Models\Category as CategoryModel; 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\Article as ArticleRepo;
use App\Repos\ArticleTag as ArticleTagRepo; use App\Repos\ArticleTag as ArticleTagRepo;
use App\Repos\Category as CategoryRepo; use App\Repos\Category as CategoryRepo;
use App\Repos\Tag as TagRepo; use App\Repos\Tag as TagRepo;
use App\Repos\User as UserRepo;
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;
use App\Services\Sync\ArticleIndex as ArticleIndexSync; use App\Services\Sync\ArticleIndex as ArticleIndexSync;
use App\Validators\Article as ArticleValidator; use App\Validators\Article as ArticleValidator;
class Article extends Service class Article extends Service
{ {
public function getArticleModel()
{
$article = new ArticleModel();
$article->afterFetch();
return $article;
}
public function getXmTags($id) public function getXmTags($id)
{ {
$tagRepo = new TagRepo(); $tagRepo = new TagRepo();
@ -61,11 +76,21 @@ class Article extends Service
]); ]);
} }
public function getPublishTypes()
{
return ArticleModel::publishTypes();
}
public function getSourceTypes() public function getSourceTypes()
{ {
return ArticleModel::sourceTypes(); return ArticleModel::sourceTypes();
} }
public function getRejectOptions()
{
return ReasonModel::articleRejectOptions();
}
public function getArticles() public function getArticles()
{ {
$pagerQuery = new PagerQuery(); $pagerQuery = new PagerQuery();
@ -98,7 +123,7 @@ class Article extends Service
{ {
$post = $this->request->getPost(); $post = $this->request->getPost();
$loginUser = $this->getLoginUser(); $user = $this->getLoginUser();
$validator = new ArticleValidator(); $validator = new ArticleValidator();
@ -107,12 +132,16 @@ class Article extends Service
$article = new ArticleModel(); $article = new ArticleModel();
$article->owner_id = $loginUser->id; $article->owner_id = $user->id;
$article->category_id = $category->id; $article->category_id = $category->id;
$article->title = $title; $article->title = $title;
$article->create(); $article->create();
$this->incrUserArticleCount($user);
$this->eventsManager->fire('Article:afterCreate', $this, $article);
return $article; return $article;
} }
@ -159,6 +188,10 @@ class Article extends Service
$data['allow_comment'] = $post['allow_comment']; $data['allow_comment'] = $post['allow_comment'];
} }
if (isset($post['private'])) {
$data['private'] = $validator->checkPrivateStatus($post['private']);
}
if (isset($post['featured'])) { if (isset($post['featured'])) {
$data['featured'] = $validator->checkFeatureStatus($post['featured']); $data['featured'] = $validator->checkFeatureStatus($post['featured']);
} }
@ -173,24 +206,99 @@ class Article extends Service
$article->update($data); $article->update($data);
$this->rebuildArticleIndex($article);
$this->eventsManager->fire('Article:afterUpdate', $this, $article);
return $article; return $article;
} }
public function deleteArticle($id) public function deleteArticle($id)
{ {
$article = $this->findOrFail($id); $article = $this->findOrFail($id);
$article->deleted = 1; $article->deleted = 1;
$article->update(); $article->update();
$userRepo = new UserRepo();
$owner = $userRepo->findById($article->owner_id);
$this->decrUserArticleCount($owner);
$this->rebuildArticleIndex($article);
$this->eventsManager->fire('Article:afterDelete', $this, $article);
return $article; return $article;
} }
public function restoreArticle($id) public function restoreArticle($id)
{ {
$article = $this->findOrFail($id); $article = $this->findOrFail($id);
$article->deleted = 0; $article->deleted = 0;
$article->update(); $article->update();
$userRepo = new UserRepo();
$owner = $userRepo->findById($article->owner_id);
$this->incrUserArticleCount($owner);
$this->rebuildArticleIndex($article);
$this->eventsManager->fire('Article:afterRestore', $this, $article);
return $article;
}
public function reviewArticle($id)
{
$type = $this->request->getPost('type', ['trim', 'string']);
$reason = $this->request->getPost('reason', ['trim', 'string']);
$article = $this->findOrFail($id);
if ($type == 'approve') {
$article->published = ArticleModel::PUBLISH_APPROVED;
} elseif ($type == 'reject') {
$article->published = ArticleModel::PUBLISH_REJECTED;
}
$article->update();
$sender = $this->getLoginUser();
if ($type == 'approve') {
$this->rebuildArticleIndex($article);
$this->handlePostPoint($article);
$notice = new ArticleApprovedNotice();
$notice->handle($article, $sender);
$this->eventsManager->fire('Article:afterApprove', $this, $article);
} elseif ($type == 'reject') {
$options = ReasonModel::articleRejectOptions();
if (array_key_exists($reason, $options)) {
$reason = $options[$reason];
}
$notice = new ArticleRejectedNotice();
$notice->handle($article, $sender, $reason);
$this->eventsManager->fire('Article:afterReject', $this, $article);
}
return $article; return $article;
} }
@ -201,24 +309,15 @@ class Article extends Service
return $validator->checkArticle($id); return $validator->checkArticle($id);
} }
protected function rebuildArticleCache(ArticleModel $article)
{
$cache = new ArticleCache();
$cache->rebuild($article->id);
}
protected function rebuildArticleIndex(ArticleModel $article)
{
$sync = new ArticleIndexSync();
$sync->addItem($article->id);
}
protected function saveTags(ArticleModel $article, $tagIds) protected function saveTags(ArticleModel $article, $tagIds)
{ {
$originTagIds = []; $originTagIds = [];
/**
* 修改数据后afterFetch设置的属性会失效重新执行
*/
$article->afterFetch();
if ($article->tags) { if ($article->tags) {
$originTagIds = kg_array_column($article->tags, 'id'); $originTagIds = kg_array_column($article->tags, 'id');
} }
@ -284,4 +383,42 @@ class Article extends Service
return $pager; return $pager;
} }
protected function incrUserArticleCount(UserModel $user)
{
$user->article_count += 1;
$user->update();
}
protected function decrUserArticleCount(UserModel $user)
{
if ($user->article_count > 0) {
$user->article_count -= 1;
$user->update();
}
}
protected function rebuildArticleCache(ArticleModel $article)
{
$cache = new ArticleCache();
$cache->rebuild($article->id);
}
protected function rebuildArticleIndex(ArticleModel $article)
{
$sync = new ArticleIndexSync();
$sync->addItem($article->id);
}
protected function handlePostPoint(ArticleModel $article)
{
if ($article->published != ArticleModel::PUBLISH_APPROVED) return;
$service = new ArticlePostPointHistory();
$service->handle($article);
}
} }

View File

@ -140,36 +140,36 @@ class AuthNode extends Service
], ],
[ [
'id' => '1-4', 'id' => '1-4',
'title' => '题管理', 'title' => '题管理',
'type' => 'menu', 'type' => 'menu',
'children' => [ 'children' => [
[ [
'id' => '1-4-1', 'id' => '1-4-1',
'title' => '题列表', 'title' => '题列表',
'type' => 'menu', 'type' => 'menu',
'route' => 'admin.topic.list', 'route' => 'admin.topic.list',
], ],
[ [
'id' => '1-4-5', 'id' => '1-4-5',
'title' => '搜索题', 'title' => '搜索题',
'type' => 'menu', 'type' => 'menu',
'route' => 'admin.topic.search', 'route' => 'admin.topic.search',
], ],
[ [
'id' => '1-4-2', 'id' => '1-4-2',
'title' => '添加题', 'title' => '添加题',
'type' => 'menu', 'type' => 'menu',
'route' => 'admin.topic.add', 'route' => 'admin.topic.add',
], ],
[ [
'id' => '1-4-3', 'id' => '1-4-3',
'title' => '编辑题', 'title' => '编辑题',
'type' => 'button', 'type' => 'button',
'route' => 'admin.topic.edit', 'route' => 'admin.topic.edit',
], ],
[ [
'id' => '1-4-4', 'id' => '1-4-4',
'title' => '删除题', 'title' => '删除题',
'type' => 'button', 'type' => 'button',
'route' => 'admin.topic.delete', 'route' => 'admin.topic.delete',
], ],
@ -272,6 +272,12 @@ class AuthNode extends Service
'type' => 'button', 'type' => 'button',
'route' => 'admin.article.edit', 'route' => 'admin.article.edit',
], ],
[
'id' => '1-7-6',
'title' => '文章分类',
'type' => 'menu',
'route' => 'admin.article.category',
],
[ [
'id' => '1-7-5', 'id' => '1-7-5',
'title' => '删除文章', 'title' => '删除文章',
@ -279,10 +285,16 @@ class AuthNode extends Service
'route' => 'admin.article.delete', 'route' => 'admin.article.delete',
], ],
[ [
'id' => '1-7-6', 'id' => '1-7-9',
'title' => '文章分类', 'title' => '文章详情',
'type' => 'menu', 'type' => 'button',
'route' => 'admin.article.category', 'route' => 'admin.article.review',
],
[
'id' => '1-7-10',
'title' => '审核文章',
'type' => 'button',
'route' => 'admin.article.review',
], ],
], ],
], ],
@ -364,6 +376,19 @@ class AuthNode extends Service
'id' => '2', 'id' => '2',
'title' => '运营管理', 'title' => '运营管理',
'children' => [ 'children' => [
[
'id' => '2-10',
'title' => '审核队列',
'type' => 'menu',
'children' => [
[
'id' => '2-10-1',
'title' => '文章列表',
'type' => 'menu',
'route' => 'admin.mod.articles',
],
],
],
[ [
'id' => '2-1', 'id' => '2-1',
'title' => '学员管理', 'title' => '学员管理',

View File

@ -2,6 +2,7 @@
namespace App\Http\Admin\Services; namespace App\Http\Admin\Services;
use App\Caches\ModerationStat;
use App\Caches\SiteGlobalStat; use App\Caches\SiteGlobalStat;
use App\Caches\SiteTodayStat; use App\Caches\SiteTodayStat;
use App\Library\AppInfo; use App\Library\AppInfo;
@ -53,6 +54,13 @@ class Index extends Service
return $cache->get(); return $cache->get();
} }
public function getModerationStat()
{
$cache = new ModerationStat();
return $cache->get();
}
public function getReleases() public function getReleases()
{ {
$url = 'https://koogua.com/api-releases.json'; $url = 'https://koogua.com/api-releases.json';

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\Admin\Services;
use App\Builders\ArticleList as ArticleListBuilder;
use App\Library\Paginator\Query as PagerQuery;
use App\Models\Article as ArticleModel;
use App\Repos\Article as ArticleRepo;
class Moderation extends Service
{
public function getArticles()
{
$pagerQuery = new PagerQuery();
$params = $pagerQuery->getParams();
$params['published'] = ArticleModel::PUBLISH_PENDING;
$params['deleted'] = 0;
$sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage();
$limit = $pagerQuery->getLimit();
$articleRepo = new ArticleRepo();
$pager = $articleRepo->paginate($params, $sort, $page, $limit);
return $this->handleArticles($pager);
}
protected function handleArticles($pager)
{
if ($pager->total_items > 0) {
$builder = new ArticleListBuilder();
$items = $pager->items->toArray();
$pipeA = $builder->handleArticles($items);
$pipeB = $builder->handleCategories($pipeA);
$pipeC = $builder->handleUsers($pipeB);
$pipeD = $builder->objects($pipeC);
$pager->items = $pipeD;
}
return $pager;
}
}

View File

@ -50,6 +50,14 @@
</div> </div>
</div> </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"> <div class="layui-form-item">
<label class="layui-form-label">允许评论</label> <label class="layui-form-label">允许评论</label>
<div class="layui-input-block"> <div class="layui-input-block">
@ -57,6 +65,13 @@
<input type="radio" name="allow_comment" value="0" title="否" {% if article.allow_comment == 0 %}checked="checked"{% endif %}> <input type="radio" name="allow_comment" value="0" title="否" {% if article.allow_comment == 0 %}checked="checked"{% endif %}>
</div> </div>
</div> </div>
<div class="layui-form-item">
<label class="layui-form-label">仅我可见</label>
<div class="layui-input-block">
<input type="radio" name="private" value="1" title="是" {% if article.private == 1 %}checked="checked"{% endif %}>
<input type="radio" name="private" value="0" title="否" {% if article.private == 0 %}checked="checked"{% endif %}>
</div>
</div>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label"></label> <label class="layui-form-label"></label>
<div class="layui-input-block"> <div class="layui-input-block">

View File

@ -38,13 +38,13 @@
<thead> <thead>
<tr> <tr>
<th>文章</th> <th>文章</th>
<th>状态</th>
<th>浏览</th> <th>浏览</th>
<th>评论</th> <th>评论</th>
<th>点赞</th> <th>点赞</th>
<th>收藏</th> <th>收藏</th>
<th>推荐</th> <th>推荐</th>
<th>评论</th> <th>评论</th>
<th>发布</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
@ -74,13 +74,13 @@
<span>创建:{{ date('Y-m-d',item.create_time) }}</span> <span>创建:{{ date('Y-m-d',item.create_time) }}</span>
</p> </p>
</td> </td>
<td>{{ publish_status(item.published) }}</td>
<td>{{ item.view_count }}</td> <td>{{ item.view_count }}</td>
<td>{{ item.comment_count }}</td> <td>{{ item.comment_count }}</td>
<td>{{ item.like_count }}</td> <td>{{ item.like_count }}</td>
<td>{{ item.favorite_count }}</td> <td>{{ item.favorite_count }}</td>
<td><input type="checkbox" name="featured" value="1" lay-skin="switch" lay-text="是|否" lay-filter="featured" data-url="{{ update_url }}" {% if item.featured == 1 %}checked="checked"{% endif %}></td> <td><input type="checkbox" name="featured" value="1" lay-skin="switch" lay-text="是|否" lay-filter="featured" data-url="{{ update_url }}" {% if item.featured == 1 %}checked="checked"{% endif %}></td>
<td><input type="checkbox" name="comment" value="1" lay-skin="switch" lay-text="开|关" lay-filter="comment" data-url="{{ update_url }}" {% if item.allow_comment == 1 %}checked="checked"{% endif %}></td> <td><input type="checkbox" name="comment" value="1" lay-skin="switch" lay-text="开|关" lay-filter="comment" data-url="{{ update_url }}" {% if item.allow_comment == 1 %}checked="checked"{% endif %}></td>
<td><input type="checkbox" name="published" value="1" lay-skin="switch" lay-text="是|否" lay-filter="published" data-url="{{ update_url }}" {% if item.published == 1 %}checked="checked"{% endif %}></td>
<td class="center"> <td class="center">
<div class="layui-dropdown"> <div class="layui-dropdown">
<button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button> <button class="layui-btn layui-btn-sm">操作 <i class="layui-icon layui-icon-triangle-d"></i></button>

View File

@ -41,6 +41,14 @@
<div id="xm-tag-ids"></div> <div id="xm-tag-ids"></div>
</div> </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"> <div class="layui-form-item">
<label class="layui-form-label">来源类型</label> <label class="layui-form-label">来源类型</label>
<div class="layui-input-block"> <div class="layui-input-block">
@ -56,13 +64,6 @@
<input type="radio" name="featured" value="0" title="否"> <input type="radio" name="featured" value="0" title="否">
</div> </div>
</div> </div>
<div class="layui-form-item">
<label class="layui-form-label">发布</label>
<div class="layui-input-block">
<input type="radio" name="published" value="1" title="是">
<input type="radio" name="published" value="0" title="否">
</div>
</div>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">删除</label> <label class="layui-form-label">删除</label>
<div class="layui-input-block"> <div class="layui-input-block">

View File

@ -0,0 +1,82 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set list_url = url({'for':'admin.article.pending_list'}) %}
<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>
</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>
<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

@ -7,6 +7,7 @@
<div class="layui-col-md8"> <div class="layui-col-md8">
{{ partial('index/main_global_stat') }} {{ partial('index/main_global_stat') }}
{{ partial('index/main_today_stat') }} {{ partial('index/main_today_stat') }}
{{ partial('index/main_mod_stat') }}
{{ partial('index/main_app_trend') }} {{ partial('index/main_app_trend') }}
</div> </div>
<div class="layui-col-md4"> <div class="layui-col-md4">

View File

@ -2,54 +2,78 @@
<div class="layui-card-header">全局统计</div> <div class="layui-card-header">全局统计</div>
<div class="layui-card-body"> <div class="layui-card-body">
<div class="layui-row layui-col-space10"> <div class="layui-row layui-col-space10">
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">课程数</div>
<div class="count">{{ global_stat.course_count }}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">用户数</div> <div class="name">用户数</div>
<div class="count">{{ global_stat.user_count }}</div> <div class="count">{{ global_stat.user_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">群组数</div> <div class="name">会员数</div>
<div class="count">{{ global_stat.group_count }}</div> <div class="count">{{ global_stat.vip_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">订单数</div> <div class="name">课程数</div>
<div class="count">{{ global_stat.order_count }}</div> <div class="count">{{ global_stat.course_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">评价数</div>
<div class="count">{{ global_stat.review_count }}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card">
<div class="name">咨询数</div>
<div class="count">{{ global_stat.consult_count }}</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">套餐数</div> <div class="name">套餐数</div>
<div class="count">{{ global_stat.package_count }}</div> <div class="count">{{ global_stat.package_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">专题数</div> <div class="name">专题数</div>
<div class="count">{{ global_stat.topic_count }}</div> <div class="count">{{ global_stat.topic_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">群组数</div>
<div class="count">{{ global_stat.group_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">评价数</div>
<div class="count">{{ global_stat.review_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">咨询数</div>
<div class="count">{{ global_stat.consult_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">文章数</div>
<div class="count">{{ global_stat.article_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">提问数</div>
<div class="count">0</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">回答数</div>
<div class="count">0</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">评论数</div>
<div class="count">{{ global_stat.comment_count }}</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,39 @@
<div class="layui-card layui-text kg-stats">
<div class="layui-card-header">审核队列</div>
<div class="layui-card-body">
<div class="layui-row layui-col-space10">
<div class="layui-col-md3">
<div class="kg-stat-card">
<div class="name">文章</div>
<div class="count">
<a href="{{ url({'for':'admin.mod.articles'}) }}">{{ mod_stat.article_count }}</a>
</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card">
<div class="name">提问</div>
<div class="count">
<a href="javascript:">0</a>
</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card">
<div class="name">回答</div>
<div class="count">
<a href="javascript:">0</a>
</div>
</div>
</div>
<div class="layui-col-md3">
<div class="kg-stat-card">
<div class="name">评论</div>
<div class="count">
<a href="javascript:">0</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,25 +2,37 @@
<div class="layui-card-header">今日统计</div> <div class="layui-card-header">今日统计</div>
<div class="layui-card-body"> <div class="layui-card-body">
<div class="layui-row layui-col-space10"> <div class="layui-row layui-col-space10">
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">用户注册</div> <div class="name">用户注册</div>
<div class="count">{{ today_stat.register_count }}</div> <div class="count">{{ today_stat.register_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">积分兑换</div>
<div class="count">{{ today_stat.point_redeem_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">成交订单</div> <div class="name">成交订单</div>
<div class="count">{{ today_stat.sale_count }}</div> <div class="count">{{ today_stat.sale_count }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card">
<div class="name">成功退款</div>
<div class="count">{{ today_stat.refund_count }}</div>
</div>
</div>
<div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">销售金额</div> <div class="name">销售金额</div>
<div class="count">{{ '¥%0.2f'|format(today_stat.sale_amount) }}</div> <div class="count">{{ '¥%0.2f'|format(today_stat.sale_amount) }}</div>
</div> </div>
</div> </div>
<div class="layui-col-md3"> <div class="layui-col-md2">
<div class="kg-stat-card"> <div class="kg-stat-card">
<div class="name">退款金额</div> <div class="name">退款金额</div>
<div class="count">{{ '¥%0.2f'|format(today_stat.refund_amount) }}</div> <div class="count">{{ '¥%0.2f'|format(today_stat.refund_amount) }}</div>

View File

@ -1,3 +1,15 @@
{%- macro publish_status(type) %}
{% if type == 1 %}
审核中
{% elseif type == 2 %}
已发布
{% elseif type == 3 %}
未通过
{% else %}
未知
{% endif %}
{%- endmacro %}
{%- macro source_info(type,url) %} {%- macro source_info(type,url) %}
{% if type == 1 %} {% if type == 1 %}
原创 原创

View File

@ -0,0 +1,79 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/article') }}
<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>
<col width="10%">
</colgroup>
<thead>
<tr>
<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 show_url = url({'for':'admin.article.show','id':item.id}) %}
<tr>
<td>
<p>标题:{{ item.title }}{{ 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>
</td>
<td>
<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 %}
</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="{{ show_url }}" class="layui-btn layui-btn-sm">详情</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ partial('partials/pager') }}
{% endblock %}

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">3</span> <span class="layui-anim layui-anim-loop">3</span>
</h1> </h1>
</div> </div>

View File

@ -97,6 +97,24 @@
<td><input class="layui-input" type="text" name="event_rule[im_discuss][point]" value="{{ event_rule.im_discuss.point }}" lay-verify="required"></td> <td><input class="layui-input" type="text" name="event_rule[im_discuss][point]" value="{{ event_rule.im_discuss.point }}" lay-verify="required"></td>
<td>N/A</td> <td>N/A</td>
</tr> </tr>
<tr>
<td>发表评论</td>
<td>
<input type="radio" name="event_rule[comment_post][enabled]" value="1" title="是" {% if event_rule.comment_post.enabled == "1" %}checked="checked"{% endif %}>
<input type="radio" name="event_rule[comment_post][enabled]" value="0" title="否" {% if event_rule.comment_post.enabled == "0" %}checked="checked"{% endif %}>
</td>
<td><input class="layui-input" type="text" name="event_rule[comment_post][point]" value="{{ event_rule.comment_post.point }}" lay-verify="required"></td>
<td><input class="layui-input" type="text" name="event_rule[comment_post][limit]" value="{{ event_rule.comment_post.limit }}" lay-verify="required"></td>
</tr>
<tr>
<td>发表文章</td>
<td>
<input type="radio" name="event_rule[article_post][enabled]" value="1" title="是" {% if event_rule.article_post.enabled == "1" %}checked="checked"{% endif %}>
<input type="radio" name="event_rule[article_post][enabled]" value="0" title="否" {% if event_rule.article_post.enabled == "0" %}checked="checked"{% endif %}>
</td>
<td><input class="layui-input" type="text" name="event_rule[article_post][point]" value="{{ event_rule.article_post.point }}" lay-verify="required"></td>
<td><input class="layui-input" type="text" name="event_rule[article_post][limit]" value="{{ event_rule.article_post.limit }}" lay-verify="required"></td>
</tr>
</tbody> </tbody>
</table> </table>
<br> <br>

View File

@ -4,7 +4,7 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.topic.create'}) }}"> <form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.topic.create'}) }}">
<fieldset class="layui-elem-field layui-field-title"> <fieldset class="layui-elem-field layui-field-title">
<legend>添加题</legend> <legend>添加题</legend>
</fieldset> </fieldset>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">标题</label> <label class="layui-form-label">标题</label>

View File

@ -4,7 +4,7 @@
<form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.topic.update','id':topic.id}) }}"> <form class="layui-form kg-form" method="POST" action="{{ url({'for':'admin.topic.update','id':topic.id}) }}">
<fieldset class="layui-elem-field layui-field-title"> <fieldset class="layui-elem-field layui-field-title">
<legend>编辑题</legend> <legend>编辑题</legend>
</fieldset> </fieldset>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">标题</label> <label class="layui-form-label">标题</label>

View File

@ -8,15 +8,15 @@
<div class="kg-nav"> <div class="kg-nav">
<div class="kg-nav-left"> <div class="kg-nav-left">
<span class="layui-breadcrumb"> <span class="layui-breadcrumb">
<a><cite>题管理</cite></a> <a><cite>题管理</cite></a>
</span> </span>
</div> </div>
<div class="kg-nav-right"> <div class="kg-nav-right">
<a class="layui-btn layui-btn-sm" href="{{ add_url }}"> <a class="layui-btn layui-btn-sm" href="{{ add_url }}">
<i class="layui-icon layui-icon-add-1"></i>添加 <i class="layui-icon layui-icon-add-1"></i>添加
</a> </a>
<a class="layui-btn layui-btn-sm" href="{{ search_url }}"> <a class="layui-btn layui-btn-sm" href="{{ search_url }}">
<i class="layui-icon layui-icon-search"></i>搜索 <i class="layui-icon layui-icon-search"></i>搜索
</a> </a>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.topic.list'}) }}"> <form class="layui-form kg-form" method="GET" action="{{ url({'for':'admin.topic.list'}) }}">
<fieldset class="layui-elem-field layui-field-title"> <fieldset class="layui-elem-field layui-field-title">
<legend>搜索题</legend> <legend>搜索题</legend>
</fieldset> </fieldset>
<div class="layui-form-item"> <div class="layui-form-item">
<label class="layui-form-label">编号</label> <label class="layui-form-label">编号</label>

View File

@ -8,6 +8,10 @@ use App\Services\Logic\User\Console\CourseList as CourseListService;
use App\Services\Logic\User\Console\FavoriteList as FavoriteListService; use App\Services\Logic\User\Console\FavoriteList as FavoriteListService;
use App\Services\Logic\User\Console\FriendList as FriendListService; use App\Services\Logic\User\Console\FriendList as FriendListService;
use App\Services\Logic\User\Console\GroupList as GroupListService; use App\Services\Logic\User\Console\GroupList as GroupListService;
use App\Services\Logic\User\Console\NotificationList as NotificationListService;
use App\Services\Logic\User\Console\NotificationRead as NotificationReadService;
use App\Services\Logic\User\Console\NotifyStats as NotifyStatsService;
use App\Services\Logic\User\Console\Online as OnlineService;
use App\Services\Logic\User\Console\OrderList as OrderListService; use App\Services\Logic\User\Console\OrderList as OrderListService;
use App\Services\Logic\User\Console\ProfileInfo as ProfileInfoService; use App\Services\Logic\User\Console\ProfileInfo as ProfileInfoService;
use App\Services\Logic\User\Console\ProfileUpdate as ProfileUpdateService; use App\Services\Logic\User\Console\ProfileUpdate as ProfileUpdateService;
@ -140,6 +144,34 @@ class UserConsoleController extends Controller
return $this->jsonSuccess(['pager' => $pager]); return $this->jsonSuccess(['pager' => $pager]);
} }
/**
* @Get("/notifications", name="api.uc.notifications")
*/
public function notificationsAction()
{
$service = new NotificationListService();
$pager = $service->handle();
$service = new NotificationReadService();
$service->handle();
return $this->jsonSuccess(['pager' => $pager]);
}
/**
* @Get("/notify/stats", name="api.uc.notify_stats")
*/
public function notifyStatsAction()
{
$service = new NotifyStatsService();
$stats = $service->handle();
return $this->jsonSuccess(['stats' => $stats]);
}
/** /**
* @Post("/profile/update", name="api.uc.update_profile") * @Post("/profile/update", name="api.uc.update_profile")
*/ */
@ -152,4 +184,16 @@ class UserConsoleController extends Controller
return $this->jsonSuccess(); return $this->jsonSuccess();
} }
/**
* @Post("/online", name="api.uc.online")
*/
public function onlineAction()
{
$service = new OnlineService();
$service->handle();
return $this->jsonSuccess();
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Home\Controllers; namespace App\Http\Home\Controllers;
use App\Http\Home\Services\Article as ArticleService;
use App\Http\Home\Services\ArticleQuery as ArticleQueryService; use App\Http\Home\Services\ArticleQuery as ArticleQueryService;
use App\Services\Logic\Article\ArticleFavorite as ArticleFavoriteService; use App\Services\Logic\Article\ArticleFavorite as ArticleFavoriteService;
use App\Services\Logic\Article\ArticleInfo as ArticleInfoService; use App\Services\Logic\Article\ArticleInfo as ArticleInfoService;
@ -65,6 +66,47 @@ class ArticleController extends Controller
$this->view->setVar('authors', $authors); $this->view->setVar('authors', $authors);
} }
/**
* @Get("/add", name="home.article.add")
*/
public function addAction()
{
$service = new ArticleService();
$sourceTypes = $service->getSourceTypes();
$categories = $service->getCategories();
$article = $service->getArticleModel();
$xmTags = $service->getXmTags(0);
$this->seo->prependTitle('撰写文章');
$this->view->pick('article/edit');
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('article', $article);
$this->view->setVar('xm_tags', $xmTags);
}
/**
* @Get("/{id:[0-9]+}/edit", name="home.article.edit")
*/
public function editAction($id)
{
$service = new ArticleService();
$sourceTypes = $service->getSourceTypes();
$categories = $service->getCategories();
$article = $service->getArticle($id);
$xmTags = $service->getXmTags($id);
$this->seo->prependTitle('编辑文章');
$this->view->setVar('source_types', $sourceTypes);
$this->view->setVar('categories', $categories);
$this->view->setVar('article', $article);
$this->view->setVar('xm_tags', $xmTags);
}
/** /**
* @Get("/{id:[0-9]+}", name="home.article.show") * @Get("/{id:[0-9]+}", name="home.article.show")
*/ */
@ -74,6 +116,12 @@ class ArticleController extends Controller
$article = $service->handle($id); $article = $service->handle($id);
$owned = $this->authUser->id == $article['owner']['id'];
if ($article['private'] == 1 && !$owned) {
$this->response->redirect(['for' => 'home.error.403']);
}
$this->seo->prependTitle($article['title']); $this->seo->prependTitle($article['title']);
$this->view->setVar('article', $article); $this->view->setVar('article', $article);
@ -105,6 +153,61 @@ class ArticleController extends Controller
$this->view->setVar('comments', $comments); $this->view->setVar('comments', $comments);
} }
/**
* @Post("/create", name="home.article.create")
*/
public function createAction()
{
$service = new ArticleService();
$service->createArticle();
$location = $this->url->get(['for' => 'home.uc.articles']);
$content = [
'location' => $location,
'msg' => '创建文章成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/update", name="home.article.update")
*/
public function updateAction($id)
{
$service = new ArticleService();
$service->updateArticle($id);
$location = $this->url->get(['for' => 'home.uc.articles']);
$content = [
'location' => $location,
'msg' => '更新文章成功',
];
return $this->jsonSuccess($content);
}
/**
* @Post("/{id:[0-9]+}/delete", name="home.article.delete")
*/
public function deleteAction($id)
{
$service = new ArticleService();
$service->deleteArticle($id);
$content = [
'location' => $this->request->getHTTPReferer(),
'msg' => '删除文章成功',
];
return $this->jsonSuccess($content);
}
/** /**
* @Post("/{id:[0-9]+}/favorite", name="home.article.favorite") * @Post("/{id:[0-9]+}/favorite", name="home.article.favorite")
*/ */
@ -133,4 +236,5 @@ class ArticleController extends Controller
return $this->jsonSuccess(['data' => $data, 'msg' => $msg]); return $this->jsonSuccess(['data' => $data, 'msg' => $msg]);
} }
} }

View File

@ -26,6 +26,17 @@ class TeacherConsoleController extends Controller
return true; return true;
} }
public function initialize()
{
parent::initialize();
$authUser = $this->getAuthUser(false);
$this->seo->prependTitle('教学中心');
$this->view->setVar('auth_user', $authUser);
}
/** /**
* @Get("/", name="home.tc.index") * @Get("/", name="home.tc.index")
*/ */

View File

@ -41,11 +41,54 @@ class UploadController extends Controller
return $this->jsonSuccess(['data' => $data]); return $this->jsonSuccess(['data' => $data]);
} }
/**
* @Post("/cover/img", name="home.upload.cover_img")
*/
public function uploadCoverImageAction()
{
$service = new StorageService();
$file = $service->uploadCoverImage();
if (!$file) {
return $this->jsonError(['msg' => '上传文件失败']);
}
$data = [
'src' => $service->getImageUrl($file->path),
'title' => $file->name,
];
return $this->jsonSuccess(['data' => $data]);
}
/**
* @Post("/content/img", name="home.upload.content_img")
*/
public function uploadContentImageAction()
{
$service = new StorageService();
$file = $service->uploadContentImage();
if (!$file) {
return $this->jsonError(['msg' => '上传文件失败']);
}
$data = [
'src' => $service->getImageUrl($file->path),
'title' => $file->name,
];
return $this->jsonSuccess(['data' => $data]);
}
/** /**
* @Post("/im/img", name="home.upload.im_img") * @Post("/im/img", name="home.upload.im_img")
*/ */
public function uploadImImageAction() public function uploadImImageAction()
{ {
} }
/** /**

View File

@ -15,6 +15,10 @@ use App\Services\Logic\User\Console\CourseList as CourseListService;
use App\Services\Logic\User\Console\FavoriteList as FavoriteListService; use App\Services\Logic\User\Console\FavoriteList as FavoriteListService;
use App\Services\Logic\User\Console\FriendList as FriendListService; use App\Services\Logic\User\Console\FriendList as FriendListService;
use App\Services\Logic\User\Console\GroupList as GroupListService; use App\Services\Logic\User\Console\GroupList as GroupListService;
use App\Services\Logic\User\Console\NotificationList as NotificationListService;
use App\Services\Logic\User\Console\NotificationRead as NotificationReadService;
use App\Services\Logic\User\Console\NotifyStats as NotifyStatsService;
use App\Services\Logic\User\Console\Online as OnlineService;
use App\Services\Logic\User\Console\OrderList as OrderListService; use App\Services\Logic\User\Console\OrderList as OrderListService;
use App\Services\Logic\User\Console\PointHistory as PointHistoryService; use App\Services\Logic\User\Console\PointHistory as PointHistoryService;
use App\Services\Logic\User\Console\PointRedeemList as PointRedeemListService; use App\Services\Logic\User\Console\PointRedeemList as PointRedeemListService;
@ -48,6 +52,8 @@ class UserConsoleController extends Controller
$authUser = $this->getAuthUser(false); $authUser = $this->getAuthUser(false);
$this->seo->prependTitle('用户中心');
$this->view->setVar('auth_user', $authUser); $this->view->setVar('auth_user', $authUser);
} }
@ -268,6 +274,23 @@ class UserConsoleController extends Controller
$this->view->setVar('pager', $pager); $this->view->setVar('pager', $pager);
} }
/**
* @Get("/notifications", name="home.uc.notifications")
*/
public function notificationsAction()
{
$service = new NotificationListService();
$pager = $service->handle();
$service = new NotificationReadService();
$service->handle();
$this->view->pick('user/console/notifications');
$this->view->setVar('pager', $pager);
}
/** /**
* @Get("/subscribe", name="home.uc.subscribe") * @Get("/subscribe", name="home.uc.subscribe")
*/ */
@ -283,6 +306,18 @@ class UserConsoleController extends Controller
$this->view->setVar('subscribed', $subscribed); $this->view->setVar('subscribed', $subscribed);
} }
/**
* @Get("/notify/stats", name="home.uc.notify_stats")
*/
public function notifyStatsAction()
{
$service = new NotifyStatsService();
$stats = $service->handle();
return $this->jsonSuccess(['stats' => $stats]);
}
/** /**
* @Post("/profile/update", name="home.uc.update_profile") * @Post("/profile/update", name="home.uc.update_profile")
*/ */
@ -335,4 +370,16 @@ class UserConsoleController extends Controller
return $this->jsonSuccess($content); return $this->jsonSuccess($content);
} }
/**
* @Post("/online", name="home.uc.online")
*/
public function onlineAction()
{
$service = new OnlineService();
$service->handle();
return $this->jsonSuccess();
}
} }

View File

@ -0,0 +1,144 @@
<?php
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;
class Article extends ArticleService
{
use ClientTrait;
public function createArticle()
{
$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);
return $article;
}
public function updateArticle($id)
{
$post = $this->request->getPost();
$article = $this->findOrFail($id);
$data = $this->handlePostData($post);
$data['client_type'] = $this->getClientType();
$data['client_ip'] = $this->getClientIp();
if ($article->published == ArticleModel::PUBLISH_REJECTED) {
$data['published'] = ArticleModel::PUBLISH_PENDING;
}
/**
* 当通过审核后,禁止修改部分文章属性
*/
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'],
);
}
$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;
}
public function deleteArticle($id)
{
$article = $this->findOrFail($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);
return $article;
}
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['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;
}
}

View File

@ -0,0 +1,125 @@
{% extends 'templates/main.volt' %}
{% block content %}
{% set title = article.id > 0 ? '编辑文章' : '撰写文章' %}
{% set action_url = article.id > 0 ? url({'for':'home.article.update','id':article.id}) : url({'for':'home.article.create'}) %}
{% set source_url_display = article.source_type == 1 ? 'display:none' : 'display:block' %}
<div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/">首页</a>
<a><cite>{{ title }}</cite></a>
</span>
</div>
<form class="layui-form" method="POST" action="{{ action_url }}">
<div class="layout-main clearfix">
<div class="layout-content">
<div class="writer-content wrap">
<div class="layui-form-item">
<div class="layui-input-block">
<input class="layui-input" type="text" name="title" value="{{ article.title }}" placeholder="请输入标题..." lay-verify="required">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div id="vditor"></div>
<textarea name="content" class="layui-hide" id="vditor-textarea">{{ article.content }}</textarea>
</div>
</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="kg-back layui-btn layui-btn-primary" type="button">返回</button>
</div>
</div>
</div>
</div>
<div class="layout-sidebar">
<div class="layui-card">
<div class="layui-card-header">基本信息</div>
<div class="layui-card-body">
<div class="writer-sidebar">
<div class="layui-form-item cover-wrap">
<label class="layui-form-label">封面</label>
<div class="layui-input-block">
<img id="img-cover" class="cover" src="{{ article.cover }}!cover_270">
<input type="hidden" name="cover" value="{{ article.cover }}">
</div>
<button id="change-cover" class="layui-btn layui-btn-sm btn-change" type="button">更换</button>
</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">
<div id="xm-tag-ids"></div>
<input type="hidden" name="xm_tags" value='{{ xm_tags|json_encode }}'>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">来源类型</label>
<div class="layui-input-block">
<select name="source_type" lay-filter="source_type" lay-verify="required">
<option value="">请选择</option>
{% for value,title in source_types %}
<option value="{{ value }}" {% if article.source_type == value %}selected="selected"{% endif %}>{{ title }}</option>
{% endfor %}
</select>
</div>
</div>
<div id="source-url-block" style="{{ source_url_display }}">
<div class="layui-form-item">
<label class="layui-form-label">来源网址</label>
<div class="layui-input-block">
<input class="layui-input" type="text" name="source_url" value="{{ article.source_url }}">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">允许评论</label>
<div class="layui-input-block">
<input type="radio" name="allow_comment" value="1" title="是" {% if article.allow_comment == 1 %}checked="checked"{% endif %}>
<input type="radio" name="allow_comment" value="0" title="否" {% if article.allow_comment == 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="private" value="1" title="是" {% if article.private == 1 %}checked="checked"{% endif %}>
<input type="radio" name="private" value="0" title="否" {% if article.private == 0 %}checked="checked"{% endif %}>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
{% 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('home/js/article.edit.js') }}
{{ js_include('home/js/vditor.js') }}
{% endblock %}

View File

@ -54,7 +54,7 @@
</div> </div>
<div class="right"> <div class="right">
<span class="column"> <span class="column">
<span class="action action-delete" data-id="{{ item.id }}" data-url="{{ delete_url }}">删除</span> <span class="action action-delete" data-id="{{ comment.id }}" data-url="{{ delete_url }}">删除</span>
</span> </span>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">5</span> <span class="layui-anim layui-anim-loop">5</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">3</span> <span class="layui-anim layui-anim-loop">3</span>
</h1> </h1>
</div> </div>

View File

@ -9,7 +9,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">0</span> <span class="layui-anim layui-anim-loop">0</span>
</h1> </h1>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">1</span> <span class="layui-anim layui-anim-loop">1</span>
</h1> </h1>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">3</span> <span class="layui-anim layui-anim-loop">3</span>
</h1> </h1>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">4</span> <span class="layui-anim layui-anim-loop">4</span>
</h1> </h1>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">5</span> <span class="layui-anim layui-anim-loop">5</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">0</span> <span class="layui-anim layui-anim-loop">0</span>
</h1> </h1>
</div> </div>

View File

@ -8,7 +8,7 @@
<div class="layui-text"> <div class="layui-text">
<h1> <h1>
<span class="layui-anim layui-anim-loop">5</span> <span class="layui-anim layui-anim-loop">5</span>
<span class="layui-anim layui-anim-loop layui-anim-rotate">0</span> <span class="layui-anim layui-anim-loop">0</span>
<span class="layui-anim layui-anim-loop">3</span> <span class="layui-anim layui-anim-loop">3</span>
</h1> </h1>
</div> </div>

View File

@ -8,4 +8,16 @@
{% else %} {% else %}
未知 未知
{% endif %} {% endif %}
{%- endmacro %}
{%- macro publish_status(type) %}
{% if type == 1 %}
审核中
{% elseif type == 2 %}
已发布
{% elseif type == 3 %}
未通过
{% else %}
未知
{% endif %}
{%- endmacro %} {%- endmacro %}

View File

@ -0,0 +1,41 @@
{%- macro event_info(notify) %}
{% set sender = notify.sender %}
{% set type = notify.event_type %}
{% set info = notify.event_info %}
{% if type == 0 %}
<p>未知类型</p>
{% elseif type == 147 %}
{% set course_url = url({'for':'home.course.show','id':info.course.id}) %}
<p>{{ sender.name }} 喜欢了你在课程 <a href="{{ course_url }}" target="_blank">{{ info.course.title }}</a> 中的咨询</p>
<p>咨询内容:{{ info.consult.question }}</p>
{% elseif type == 167 %}
{% set course_url = url({'for':'home.course.show','id':info.course.id}) %}
<p>{{ sender.name }} 喜欢了你在课程 <a href="{{ course_url }}" target="_blank">{{ info.course.title }}</a> 中的评价</p>
<p>评价内容:{{ info.review.content }}</p>
{% elseif type == 184 %}
{% set article_url = url({'for':'home.article.show','id':info.article.id}) %}
<p>你的文章 <a href="{{ article_url }}" target="_blank">{{ info.article.title }}</a> 通过了审核</p>
{% elseif type == 185 %}
{% set article_url = url({'for':'home.article.show','id':info.article.id}) %}
<p>你的文章 <a href="{{ article_url }}" target="_blank">{{ info.article.title }}</a> 审核未通过</p>
<p>拒绝原因:{{ info.reason }}</p>
{% elseif type == 187 %}
{% 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>
<p>评论内容:{{ info.comment.content }}</p>
{% elseif type == 188 %}
{% 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 == 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 == 506 %}
<p>{{ sender.name }} 回复了你的评论:{{ info.comment.content }}</p>
<p>回复内容:{{ info.reply.content }}</p>
{% elseif type == 507 %}
<p>{{ sender.name }} 喜欢了你的评论:{{ info.comment.content }}</p>
{% endif %}
{%- endmacro %}

View File

@ -43,32 +43,40 @@
<span class="type">课程评价</span> <span class="type">课程评价</span>
{% elseif value == 8 %} {% elseif value == 8 %}
<span class="type">微聊讨论</span> <span class="type">微聊讨论</span>
{% elseif value == 9 %}
<span class="type">发布评论</span>
{% elseif value == 10 %}
<span class="type">发布文章</span>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}
{%- macro event_detail_info(history) %} {%- macro event_detail_info(type,info) %}
{% set event_info = history.event_info %} {% if type == 1 %}
{% if history.event_type == 1 %} <p class="order">{{ info.order.subject }}</p>
<p class="order">{{ event_info.order.subject }}</p> {% elseif type == 2 %}
{% elseif history.event_type == 2 %} {% set gift_url = url({'for':'home.point_gift.show','id':info.point_redeem.gift_id}) %}
{% set gift_url = url({'for':'home.point_gift.show','id':event_info.point_redeem.gift_id}) %} <p class="gift"><a href="{{ gift_url }}" target="_blank">{{ info.point_redeem.gift_name }}</a></p>
<p class="gift"><a href="{{ gift_url }}" target="_blank">{{ event_info.point_redeem.gift_name }}</a></p> {% elseif type == 3 %}
{% elseif history.event_type == 3 %} {% set gift_url = url({'for':'home.point_gift.show','id':info.point_redeem.gift_id}) %}
{% set gift_url = url({'for':'home.point_gift.show','id':event_info.point_redeem.gift_id}) %} <p class="gift"><a href="{{ gift_url }}" target="_blank">{{ info.point_redeem.gift_name }}</a></p>
<p class="gift"><a href="{{ gift_url }}" target="_blank">{{ event_info.point_redeem.gift_name }}</a></p> {% elseif type == 4 %}
{% elseif history.event_type == 4 %}
<span class="none">N/A</span> <span class="none">N/A</span>
{% elseif history.event_type == 5 %} {% elseif type == 5 %}
<span class="none">N/A</span> <span class="none">N/A</span>
{% elseif history.event_type == 6 %} {% elseif type == 6 %}
{% set course_url = url({'for':'home.course.show','id':event_info.course.id}) %} {% set course_url = url({'for':'home.course.show','id':info.course.id}) %}
{% set chapter_url = url({'for':'home.chapter.show','id':event_info.chapter.id}) %} {% set chapter_url = url({'for':'home.chapter.show','id':info.chapter.id}) %}
<p class="course">课程:<a href="{{ course_url }}" target="_blank">{{ event_info.course.title }}</a></p> <p class="course">课程:<a href="{{ course_url }}" target="_blank">{{ info.course.title }}</a></p>
<p class="chapter">章节:<a href="{{ chapter_url }}" target="_blank">{{ event_info.chapter.title }}</a></p> <p class="chapter">章节:<a href="{{ chapter_url }}" target="_blank">{{ info.chapter.title }}</a></p>
{% elseif history.event_type == 7 %} {% elseif type == 7 %}
{% set course_url = url({'for':'home.course.show','id':event_info.course.id}) %} {% set course_url = url({'for':'home.course.show','id':info.course.id}) %}
<p class="course"><a href="{{ course_url }}" target="_blank">{{ event_info.course.title }}</a></p> <p class="course"><a href="{{ course_url }}" target="_blank">{{ info.course.title }}</a></p>
{% elseif history.event_type == 8 %} {% elseif type == 8 %}
<span class="none">N/A</span> <span class="none">N/A</span>
{% elseif type == 9 %}
<span class="comment">N/A</span>
{% elseif type == 10 %}
{% set article_url = url({'for':'home.article.show','id':info.article.id}) %}
<p class="article"><a href="{{ article_url }}" target="_blank">{{ info.article.title }}</a></p>
{% endif %} {% endif %}
{%- endmacro %} {%- endmacro %}

View File

@ -39,6 +39,16 @@
<a href="{{ url({'for':'home.im.index'}) }}" class="nav-im" target="im"><i class="layui-icon layui-icon-chat"></i> 微聊</a> <a href="{{ url({'for':'home.im.index'}) }}" class="nav-im" target="im"><i class="layui-icon layui-icon-chat"></i> 微聊</a>
</li> </li>
{% if auth_user.id > 0 %} {% if auth_user.id > 0 %}
<li class="layui-nav-item">
<a href="javascript:">创建</a>
<dl class="layui-nav-child">
<dd><a href="javascript:">提问题</a></dd>
<dd><a href="{{ url({'for':'home.article.add'}) }}" target="_blank">写文章</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="{{ url({'for':'home.uc.notifications'}) }}" target="notify"><i class="layui-icon layui-icon-notice"></i> 消息<span id="notify-dot"></span></a>
</li>
<li class="layui-nav-item"> <li class="layui-nav-item">
<a href="javascript:">{{ auth_user.name }}</a> <a href="javascript:">{{ auth_user.name }}</a>
<dl class="layui-nav-child"> <dl class="layui-nav-child">

View File

@ -4,12 +4,20 @@
{{ partial('macros/article') }} {{ partial('macros/article') }}
{% set published_types = {'0':'全部','1':'审核中','2':'已发布','3':'未通过'} %}
{% set published = request.get('published','trim','0') %}
<div class="layout-main clearfix"> <div class="layout-main clearfix">
<div class="my-sidebar">{{ partial('user/console/menu') }}</div> <div class="my-sidebar">{{ partial('user/console/menu') }}</div>
<div class="my-content"> <div class="my-content">
<div class="wrap"> <div class="wrap">
<div class="my-nav"> <div class="my-nav">
<span class="title">我的文章</span> <span class="title">我的文章</span>
{% for key,value in published_types %}
{% set class = (published == key) ? 'layui-btn layui-btn-xs' : 'none' %}
{% set url = (key == '0') ? url({'for':'home.uc.articles'}) : url({'for':'home.uc.articles'},{'published':key}) %}
<a class="{{ class }}" href="{{ url }}">{{ value }}</a>
{% endfor %}
</div> </div>
{% if pager.total_pages > 0 %} {% if pager.total_pages > 0 %}
<table class="layui-table"> <table class="layui-table">
@ -19,6 +27,7 @@
<col> <col>
<col> <col>
<col> <col>
<col>
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
@ -27,24 +36,34 @@
<th>点赞</th> <th>点赞</th>
<th>评论</th> <th>评论</th>
<th>收藏</th> <th>收藏</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for item in pager.items %} {% for item in pager.items %}
{% set article_url = url({'for':'home.article.show','id':item.id}) %} {% set show_url = url({'for':'home.article.show','id':item.id}) %}
{% set edit_url = url({'for':'home.article.edit','id':item.id}) %}
{% set delete_url = url({'for':'home.article.delete','id':item.id}) %}
<tr> <tr>
<td> <td>
<p>标题:<a href="{{ article_url }}" target="_blank">{{ item.title }}</a></p> <p>
标题:<a href="{{ show_url }}" target="_blank">{{ item.title }}</a>
<span>{{ item.create_time|time_ago }}</span>
</p>
<p class="meta"> <p class="meta">
来源:<span class="layui-badge layui-bg-gray">{{ source_type(item.source_type) }}</span> 来源:<span class="layui-badge layui-bg-gray">{{ source_type(item.source_type) }}</span>
分类:<span class="layui-badge layui-bg-gray">{{ item.category.name }}</span> 分类:<span class="layui-badge layui-bg-gray">{{ item.category.name }}</span>
时间:{{ item.create_time|time_ago }} 状态:<span class="layui-badge layui-bg-gray">{{ publish_status(item.published) }}</span>
</p> </p>
</td> </td>
<td>{{ item.view_count }}</td> <td>{{ item.view_count }}</td>
<td>{{ item.like_count }}</td> <td>{{ item.like_count }}</td>
<td>{{ item.comment_count }}</td> <td>{{ item.comment_count }}</td>
<td>{{ item.favorite_count }}</td> <td>{{ item.favorite_count }}</td>
<td class="center">
<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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -0,0 +1,51 @@
{% extends 'templates/main.volt' %}
{% block content %}
{{ partial('macros/notification') }}
<div class="layout-main clearfix">
<div class="my-sidebar">{{ partial('user/console/menu') }}</div>
<div class="my-content">
<div class="wrap">
<div class="my-nav">
<span class="title">消息提醒</span>
</div>
<div class="notice-list">
{% for item in pager.items %}
{% set sender_url = url({'for':'home.user.show','id':item.sender.id}) %}
{% set receiver_url = url({'for':'home.user.show','id':item.receiver.id}) %}
<div class="comment-card notice-card clearfix">
<div class="avatar">
<a href="{{ sender_url }}" title="{{ item.sender.name }}" target="_blank">
<img src="{{ item.sender.avatar }}!avatar_160" alt="{{ item.sender.name }}">
</a>
</div>
<div class="info">
<div class="user">
<a href="{{ sender_url }}" target="_blank">{{ item.sender.name }}</a>
</div>
<div class="content">{{ event_info(item) }}</div>
<div class="footer">
<div class="left">
<div class="column">
<span class="time" title="{{ date('Y-m-d H:i:s',item.create_time) }}">{{ item.create_time|time_ago }}</span>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{{ partial('partials/pager') }}
</div>
</div>
</div>
{% endblock %}
{% block include_js %}
{{ js_include('home/js/user.console.js') }}
{% endblock %}

View File

@ -32,7 +32,7 @@
<tr> <tr>
<td>{{ event_type_info(item.event_type) }}</td> <td>{{ event_type_info(item.event_type) }}</td>
<td>{{ event_point_info(item.event_point) }}</td> <td>{{ event_point_info(item.event_point) }}</td>
<td>{{ event_detail_info(item) }}</td> <td>{{ event_detail_info(item.event_type,item.event_info) }}</td>
<td>{{ date('Y-m-d',item.create_time) }}</td> <td>{{ date('Y-m-d',item.create_time) }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -11,7 +11,7 @@ class AppInfo
protected $link = 'https://koogua.com'; protected $link = 'https://koogua.com';
protected $version = '1.3.2'; protected $version = '1.3.3';
public function __get($name) public function __get($name)
{ {

View File

@ -4,7 +4,7 @@ namespace App\Listeners;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Services\Logic\Notice\AccountLogin as AccountLoginNoticeService; use App\Services\Logic\Notice\AccountLogin as AccountLoginNoticeService;
use App\Services\Logic\Point\PointHistory as PointHistoryService; use App\Services\Logic\Point\History\AccountRegister as AccountRegisterPointHistory;
use Phalcon\Events\Event as PhEvent; use Phalcon\Events\Event as PhEvent;
class Account extends Listener class Account extends Listener
@ -27,9 +27,9 @@ class Account extends Listener
protected function handleRegisterPoint(UserModel $user) protected function handleRegisterPoint(UserModel $user)
{ {
$service = new PointHistoryService(); $service = new AccountRegisterPointHistory();
$service->handleAccountRegister($user); $service->handle($user);
} }
protected function handleLoginNotice(UserModel $user) protected function handleLoginNotice(UserModel $user)

66
app/Listeners/Article.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace App\Listeners;
use App\Models\Article as ArticleModel;
use Phalcon\Events\Event as PhEvent;
class Article extends Listener
{
public function afterCreate(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterUpdate(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterDelete(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterRestore(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterApprove(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterReject(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterView(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterFavorite(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterUndoFavorite(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterLike(PhEvent $event, $source, ArticleModel $article)
{
}
public function afterUndoLike(PhEvent $event, $source, ArticleModel $article)
{
}
}

46
app/Listeners/Comment.php Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace App\Listeners;
use App\Models\Comment as CommentModel;
use Phalcon\Events\Event as PhEvent;
class Comment extends Listener
{
public function afterCreate(PhEvent $event, $source, CommentModel $comment)
{
}
public function afterUpdate(PhEvent $event, $source, CommentModel $comment)
{
}
public function afterDelete(PhEvent $event, $source, CommentModel $comment)
{
}
public function afterRestore(PhEvent $event, $source, CommentModel $comment)
{
}
public function afterReply(PhEvent $event, $source, CommentModel $reply)
{
}
public function afterLike(PhEvent $event, $source, CommentModel $comment)
{
}
public function afterUndoLike(PhEvent $event, $source, CommentModel $comment)
{
}
}

56
app/Listeners/Consult.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Listeners;
use App\Models\Consult as ConsultModel;
use Phalcon\Events\Event as PhEvent;
class Consult extends Listener
{
public function afterCreate(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterUpdate(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterDelete(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterRestore(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterApprove(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterReject(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterReply(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterLike(PhEvent $event, $source, ConsultModel $consult)
{
}
public function afterUndoLike(PhEvent $event, $source, ConsultModel $consult)
{
}
}

View File

@ -3,7 +3,7 @@
namespace App\Listeners; namespace App\Listeners;
use App\Models\ImMessage as ImMessageModel; use App\Models\ImMessage as ImMessageModel;
use App\Services\Logic\Point\PointHistory as PointHistoryService; use App\Services\Logic\Point\History\ImDiscuss as ImPointHistory;
use Phalcon\Events\Event as PhEvent; use Phalcon\Events\Event as PhEvent;
class ImMessage extends Listener class ImMessage extends Listener
@ -26,9 +26,9 @@ class ImMessage extends Listener
if ($content) return; if ($content) return;
$service = new PointHistoryService(); $service = new ImPointHistory();
$service->handleImDiscuss($message); $service->handle($message);
$tomorrow = strtotime($todayDate) + 86400; $tomorrow = strtotime($todayDate) + 86400;

View File

@ -3,11 +3,14 @@
namespace App\Listeners; namespace App\Listeners;
use App\Services\Service as AppService; use App\Services\Service as AppService;
use App\Traits\Auth as AuthTrait;
use Phalcon\Mvc\User\Plugin as UserPlugin; use Phalcon\Mvc\User\Plugin as UserPlugin;
class Listener extends UserPlugin class Listener extends UserPlugin
{ {
use AuthTrait;
public function getConfig() public function getConfig()
{ {
$appService = new AppService(); $appService = new AppService();

View File

@ -3,7 +3,6 @@
namespace App\Listeners; namespace App\Listeners;
use App\Models\Review as ReviewModel; use App\Models\Review as ReviewModel;
use App\Services\Logic\Point\PointHistory as PointHistoryService;
use Phalcon\Events\Event as PhEvent; use Phalcon\Events\Event as PhEvent;
class Review extends Listener class Review extends Listener
@ -11,14 +10,42 @@ class Review extends Listener
public function afterCreate(PhEvent $event, $source, ReviewModel $review) public function afterCreate(PhEvent $event, $source, ReviewModel $review)
{ {
$this->handleReviewPoint($review);
} }
protected function handleReviewPoint(ReviewModel $review) public function afterUpdate(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterDelete(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterRestore(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterApprove(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterReject(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterLike(PhEvent $event, $source, ReviewModel $review)
{
}
public function afterUndoLike(PhEvent $event, $source, ReviewModel $review)
{ {
$service = new PointHistoryService();
$service->handleCourseReview($review);
} }
} }

View File

@ -2,11 +2,7 @@
namespace App\Listeners; namespace App\Listeners;
use App\Library\Utils\Lock as LockUtil;
use App\Models\Online as OnlineModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Repos\Online as OnlineRepo;
use App\Services\Logic\Point\PointHistory as PointHistoryService;
use App\Traits\Client as ClientTrait; use App\Traits\Client as ClientTrait;
use Phalcon\Events\Event as PhEvent; use Phalcon\Events\Event as PhEvent;
@ -17,102 +13,7 @@ class Site extends Listener
public function afterView(PhEvent $event, $source, UserModel $user) public function afterView(PhEvent $event, $source, UserModel $user)
{ {
if ($user->id > 0) {
$this->handleOnline($user);
$this->handleVisitPoint($user);
/**
* 更新会重置afterFetch重新执行
*/
$user->afterFetch();
}
}
protected function handleOnline(UserModel $user)
{
$now = time();
if ($now - $user->active_time < 900) {
return;
}
$itemId = "user_online:{$user->id}";
$clientType = $this->getClientType();
$clientIp = $this->getClientIp();
$lockId = LockUtil::addLock($itemId);
if ($lockId === false) return;
$user->active_time = $now;
$user->update();
$onlineRepo = new OnlineRepo();
$records = $onlineRepo->findByUserDate($user->id, date('Ymd'));
if ($records->count() > 0) {
$online = null;
foreach ($records as $record) {
$case1 = $record->client_type == $clientType;
$case2 = $record->client_ip == $clientIp;
if ($case1 && $case2) {
$online = $record;
break;
}
}
if ($online) {
$online->active_time = $now;
$online->update();
} else {
$this->createOnline($user->id, $clientType, $clientIp);
}
} else {
$this->createOnline($user->id, $clientType, $clientIp);
}
LockUtil::releaseLock($itemId, $lockId);
}
protected function createOnline($userId, $clientType, $clientIp)
{
$online = new OnlineModel();
$online->user_id = $userId;
$online->client_type = $clientType;
$online->client_ip = $clientIp;
$online->active_time = time();
$online->create();
return $online;
}
protected function handleVisitPoint(UserModel $user)
{
$todayDate = date('Ymd');
$keyName = sprintf('site_visit:%s:%s', $user->id, $todayDate);
$cache = $this->getCache();
$content = $cache->get($keyName);
if ($content) return;
$service = new PointHistoryService();
$service->handleSiteVisit($user);
$tomorrow = strtotime($todayDate) + 86400;
$lifetime = $tomorrow - time();
$cache->save($keyName, 1, $lifetime);
} }
} }

View File

@ -17,6 +17,13 @@ class Article extends Model
const SOURCE_REPRINT = 2; // 转载 const SOURCE_REPRINT = 2; // 转载
const SOURCE_TRANSLATE = 3; // 翻译 const SOURCE_TRANSLATE = 3; // 翻译
/**
* 发布状态
*/
const PUBLISH_PENDING = 1; // 审核中
const PUBLISH_APPROVED = 2; // 已发布
const PUBLISH_REJECTED = 3; // 未通过
/** /**
* 主键编号 * 主键编号
* *
@ -87,6 +94,27 @@ class Article extends Model
*/ */
public $source_url = ''; public $source_url = '';
/**
* 终端类型
*
* @var integer
*/
public $client_type = 0;
/**
* 终端IP
*
* @var integer
*/
public $client_ip = '';
/**
* 私有标识
*
* @var int
*/
public $private = 0;
/** /**
* 推荐标识 * 推荐标识
* *
@ -99,7 +127,7 @@ class Article extends Model
* *
* @var int * @var int
*/ */
public $published = 0; public $published = self::PUBLISH_PENDING;
/** /**
* 删除标识 * 删除标识
@ -191,7 +219,7 @@ class Article extends Model
$this->cover = self::getCoverPath($this->cover); $this->cover = self::getCoverPath($this->cover);
} }
if (is_array($this->tags)) { if (is_array($this->tags) || is_object($this->tags)) {
$this->tags = kg_json_encode($this->tags); $this->tags = kg_json_encode($this->tags);
} }
@ -213,14 +241,10 @@ class Article extends Model
$this->summary = kg_parse_summary($this->content); $this->summary = kg_parse_summary($this->content);
} }
if (is_array($this->tags)) { if (is_array($this->tags) || is_array($this->tags)) {
$this->tags = kg_json_encode($this->tags); $this->tags = kg_json_encode($this->tags);
} }
if ($this->deleted == 1) {
$this->published = 0;
}
$this->update_time = time(); $this->update_time = time();
} }
@ -260,6 +284,15 @@ class Article extends Model
]; ];
} }
public static function publishTypes()
{
return [
self::PUBLISH_PENDING => '审核中',
self::PUBLISH_APPROVED => '已发布',
self::PUBLISH_REJECTED => '未通过',
];
}
public static function sortTypes() public static function sortTypes()
{ {
return [ return [

View File

@ -124,7 +124,7 @@ class Chapter extends Model
/** /**
* 扩展属性 * 扩展属性
* *
* @var string|array * @var array|string
*/ */
public $attrs = []; public $attrs = [];
@ -235,7 +235,7 @@ class Chapter extends Model
} }
} }
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }
@ -244,7 +244,7 @@ class Chapter extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }

View File

@ -63,7 +63,7 @@ class ChapterVod extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (is_array($this->file_transcode)) { if (is_array($this->file_transcode) || is_object($this->file_transcode)) {
$this->file_transcode = kg_json_encode($this->file_transcode); $this->file_transcode = kg_json_encode($this->file_transcode);
} }
@ -72,7 +72,7 @@ class ChapterVod extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->file_transcode)) { if (is_array($this->file_transcode) || is_object($this->file_transcode)) {
$this->file_transcode = kg_json_encode($this->file_transcode); $this->file_transcode = kg_json_encode($this->file_transcode);
} }

View File

@ -13,7 +13,15 @@ class Comment extends Model
*/ */
const ITEM_CHAPTER = 1; // 章节 const ITEM_CHAPTER = 1; // 章节
const ITEM_ARTICLE = 2; // 文章 const ITEM_ARTICLE = 2; // 文章
const ITEM_ANSWER = 3; // 回答 const ITEM_QUESTION = 3; // 问题
const ITEM_ANSWER = 4; // 回答
/**
* 发布状态
*/
const PUBLISH_PENDING = 1; // 审核中
const PUBLISH_APPROVED = 2; // 已发布
const PUBLISH_REJECTED = 3; // 未通过
/** /**
* 主键编号 * 主键编号
@ -83,7 +91,7 @@ class Comment extends Model
* *
* @var integer * @var integer
*/ */
public $published = 1; public $published = self::PUBLISH_PENDING;
/** /**
* 删除标识 * 删除标识
@ -144,10 +152,6 @@ class Comment extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if ($this->deleted == 1) {
$this->published = 0;
}
$this->update_time = time(); $this->update_time = time();
} }
@ -167,4 +171,13 @@ class Comment extends Model
]; ];
} }
public static function publishTypes()
{
return [
self::PUBLISH_PENDING => '审核中',
self::PUBLISH_APPROVED => '已发布',
self::PUBLISH_REJECTED => '未通过',
];
}
} }

View File

@ -49,6 +49,20 @@ class Consult extends Model
*/ */
public $replier_id = 0; public $replier_id = 0;
/**
* 终端类型
*
* @var integer
*/
public $client_type = 0;
/**
* 终端IP
*
* @var integer
*/
public $client_ip = '';
/** /**
* 提问 * 提问
* *

View File

@ -190,7 +190,7 @@ class Course extends Model
/** /**
* 扩展属性 * 扩展属性
* *
* @var string|array * @var array|string
*/ */
public $attrs = []; public $attrs = [];
@ -311,7 +311,7 @@ class Course extends Model
} }
} }
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }
@ -342,7 +342,7 @@ class Course extends Model
$this->summary = kg_parse_summary($this->details); $this->summary = kg_parse_summary($this->details);
} }
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }

View File

@ -125,11 +125,11 @@ class FlashSale extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
if (is_array($this->schedules)) { if (is_array($this->schedules) || is_object($this->schedules)) {
$this->schedules = kg_json_encode($this->schedules); $this->schedules = kg_json_encode($this->schedules);
} }
@ -138,11 +138,11 @@ class FlashSale extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
if (is_array($this->schedules)) { if (is_array($this->schedules) || is_object($this->schedules)) {
$this->schedules = kg_json_encode($this->schedules); $this->schedules = kg_json_encode($this->schedules);
} }

View File

@ -53,9 +53,9 @@ class ImNotice extends Model
/** /**
* 条目内容 * 条目内容
* *
* @var string * @var array|string
*/ */
public $item_info = ''; public $item_info = [];
/** /**
* 阅读标识 * 阅读标识
@ -85,7 +85,7 @@ class ImNotice extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (!empty($this->item_info)) { if (is_array($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
@ -94,7 +94,7 @@ class ImNotice extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->item_info) && !empty($this->item_info)) { if (is_array($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
@ -103,7 +103,7 @@ class ImNotice extends Model
public function afterFetch() public function afterFetch()
{ {
if (is_string($this->item_info) && !empty($this->item_info)) { if (is_string($this->item_info)) {
$this->item_info = json_decode($this->item_info, true); $this->item_info = json_decode($this->item_info, true);
} }
} }

206
app/Models/Notification.php Normal file
View File

@ -0,0 +1,206 @@
<?php
namespace App\Models;
class Notification extends Model
{
/* -------------- 课程相关 -------------- */
const TYPE_COURSE_CREATED = 100;
const TYPE_COURSE_UPDATED = 101;
const TYPE_COURSE_DELETED = 102;
const TYPE_COURSE_RESTORED = 103;
const TYPE_COURSE_APPROVED = 104;
const TYPE_COURSE_REJECTED = 105;
const TYPE_COURSE_FEATURED = 106;
const TYPE_COURSE_CONSULTED = 107;
const TYPE_COURSE_FAVORITED = 108;
const TYPE_COURSE_REVIEWED = 109;
/* -------------- 章节相关 -------------- */
const TYPE_CHAPTER_CREATED = 120;
const TYPE_CHAPTER_UPDATED = 121;
const TYPE_CHAPTER_DELETED = 122;
const TYPE_CHAPTER_RESTORED = 123;
const TYPE_CHAPTER_APPROVED = 124;
const TYPE_CHAPTER_REJECTED = 125;
const TYPE_CHAPTER_CONSULTED = 126;
const TYPE_CHAPTER_COMMENTED = 127;
const TYPE_CHAPTER_LIKED = 128;
/* -------------- 咨询相关 -------------- */
const TYPE_CONSULT_CREATED = 140;
const TYPE_CONSULT_UPDATED = 141;
const TYPE_CONSULT_DELETED = 142;
const TYPE_CONSULT_RESTORED = 143;
const TYPE_CONSULT_APPROVED = 144;
const TYPE_CONSULT_REJECTED = 145;
const TYPE_CONSULT_COMMENTED = 146;
const TYPE_CONSULT_LIKED = 147;
/* -------------- 评价相关 -------------- */
const TYPE_REVIEW_CREATED = 160;
const TYPE_REVIEW_UPDATED = 161;
const TYPE_REVIEW_DELETED = 162;
const TYPE_REVIEW_RESTORED = 163;
const TYPE_REVIEW_APPROVED = 164;
const TYPE_REVIEW_REJECTED = 165;
const TYPE_REVIEW_COMMENTED = 166;
const TYPE_REVIEW_LIKED = 167;
/* -------------- 文章相关 -------------- */
const TYPE_ARTICLE_CREATED = 180;
const TYPE_ARTICLE_UPDATED = 181;
const TYPE_ARTICLE_DELETED = 182;
const TYPE_ARTICLE_RESTORED = 183;
const TYPE_ARTICLE_APPROVED = 184;
const TYPE_ARTICLE_REJECTED = 185;
const TYPE_ARTICLE_FEATURED = 186;
const TYPE_ARTICLE_COMMENTED = 187;
const TYPE_ARTICLE_FAVORITED = 188;
const TYPE_ARTICLE_LIKED = 189;
/* -------------- 问题相关 -------------- */
const TYPE_QUESTION_CREATED = 200;
const TYPE_QUESTION_UPDATED = 201;
const TYPE_QUESTION_DELETED = 202;
const TYPE_QUESTION_RESTORED = 203;
const TYPE_QUESTION_APPROVED = 204;
const TYPE_QUESTION_REJECTED = 205;
const TYPE_QUESTION_ANSWERED = 206;
const TYPE_QUESTION_COMMENTED = 207;
const TYPE_QUESTION_FAVORITED = 208;
const TYPE_QUESTION_LIKED = 209;
/* -------------- 回答相关 -------------- */
const TYPE_ANSWER_CREATED = 220;
const TYPE_ANSWER_UPDATED = 221;
const TYPE_ANSWER_DELETED = 222;
const TYPE_ANSWER_RESTORED = 223;
const TYPE_ANSWER_APPROVED = 224;
const TYPE_ANSWER_REJECTED = 225;
const TYPE_ANSWER_ACCEPTED = 226;
const TYPE_ANSWER_COMMENTED = 227;
const TYPE_ANSWER_LIKED = 228;
/* -------------- 微聊相关 -------------- */
const TYPE_FRIEND_REQUEST = 0; // 好友请求
const TYPE_FRIEND_ACCEPTED = 0; // 好友被接受
const TYPE_FRIEND_REFUSED = 0; // 好友被拒绝
const TYPE_GROUP_REQUEST = 0; // 入群请求
const TYPE_GROUP_ACCEPTED = 0; // 入群被接受
const TYPE_GROUP_REFUSED = 0; // 入群被拒绝
/* -------------- 评论相关 -------------- */
const TYPE_COMMENT_CREATED = 500;
const TYPE_COMMENT_UPDATED = 501;
const TYPE_COMMENT_DELETED = 502;
const TYPE_COMMENT_RESTORED = 503;
const TYPE_COMMENT_APPROVED = 504;
const TYPE_COMMENT_REJECTED = 505;
const TYPE_COMMENT_REPLIED = 506;
const TYPE_COMMENT_LIKED = 507;
/**
* 主键编号
*
* @var int
*/
public $id = 0;
/**
* 发送方编号
*
* @var int
*/
public $sender_id = 0;
/**
* 接收方编号
*
* @var int
*/
public $receiver_id = 0;
/**
* 事件编号
*
* @var int
*/
public $event_id = 0;
/**
* 事件类型
*
* @var int
*/
public $event_type = 0;
/**
* 事件内容
*
* @var array|string
*/
public $event_info = [];
/**
* 阅读标识
*
* @var int
*/
public $viewed = 0;
/**
* 创建时间
*
* @var int
*/
public $create_time = 0;
/**
* 更新时间
*
* @var int
*/
public $update_time = 0;
public function getSource(): string
{
return 'kg_notification';
}
public function beforeCreate()
{
if (is_array($this->event_info) || is_object($this->event_info)) {
$this->event_info = kg_json_encode($this->event_info);
}
$this->create_time = time();
}
public function beforeUpdate()
{
if (is_array($this->event_info) || is_object($this->event_info)) {
$this->event_info = kg_json_encode($this->event_info);
}
$this->update_time = time();
}
public function afterFetch()
{
if (is_string($this->event_info)) {
$this->event_info = json_decode($this->event_info, true);
}
}
}

View File

@ -80,7 +80,7 @@ class Order extends Model
/** /**
* 条目信息 * 条目信息
* *
* @var string|array * @var array|string
*/ */
public $item_info = []; public $item_info = [];
@ -101,7 +101,7 @@ class Order extends Model
/** /**
* 促销信息 * 促销信息
* *
* @var string|array * @var array|string
*/ */
public $promotion_info = []; public $promotion_info = [];
@ -170,11 +170,11 @@ class Order extends Model
{ {
$this->sn = date('YmdHis') . rand(1000, 9999); $this->sn = date('YmdHis') . rand(1000, 9999);
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
if (is_array($this->promotion_info)) { if (is_array($this->promotion_info) || is_object($this->promotion_info)) {
$this->promotion_info = kg_json_encode($this->promotion_info); $this->promotion_info = kg_json_encode($this->promotion_info);
} }
@ -183,11 +183,11 @@ class Order extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
if (is_array($this->promotion_info)) { if (is_array($this->promotion_info) || is_object($this->promotion_info)) {
$this->promotion_info = kg_json_encode($this->promotion_info); $this->promotion_info = kg_json_encode($this->promotion_info);
} }

View File

@ -68,7 +68,7 @@ class PointGift extends Model
/** /**
* 属性 * 属性
* *
* @var string|array * @var array|string
*/ */
public $attrs = []; public $attrs = [];
@ -162,7 +162,7 @@ class PointGift extends Model
} }
} }
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }
@ -181,7 +181,7 @@ class PointGift extends Model
$this->cover = self::getCoverPath($this->cover); $this->cover = self::getCoverPath($this->cover);
} }
if (is_array($this->attrs)) { if (is_array($this->attrs) || is_object($this->attrs)) {
$this->attrs = kg_json_encode($this->attrs); $this->attrs = kg_json_encode($this->attrs);
} }

View File

@ -16,6 +16,10 @@ class PointHistory extends Model
const EVENT_CHAPTER_STUDY = 6; // 课时学习 const EVENT_CHAPTER_STUDY = 6; // 课时学习
const EVENT_COURSE_REVIEW = 7; // 课程评价 const EVENT_COURSE_REVIEW = 7; // 课程评价
const EVENT_IM_DISCUSS = 8; // 微聊讨论 const EVENT_IM_DISCUSS = 8; // 微聊讨论
const EVENT_COMMENT_POST = 9; // 发布评论
const EVENT_ARTICLE_POST = 10; // 发布文章
const EVENT_QUESTION_POST = 11; // 发布问题
const EVENT_ANSWER_POST = 12; // 发布答案
/** /**
* 主键编号 * 主键编号
@ -50,12 +54,12 @@ class PointHistory extends Model
* *
* @var int * @var int
*/ */
public $event_type = ''; public $event_type = 0;
/** /**
* 事件内容 * 事件内容
* *
* @var string|array * @var array|string
*/ */
public $event_info = []; public $event_info = [];
@ -87,7 +91,7 @@ class PointHistory extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (is_array($this->event_info) && !empty($this->event_info)) { if (is_array($this->event_info) || is_object($this->event_info)) {
$this->event_info = kg_json_encode($this->event_info); $this->event_info = kg_json_encode($this->event_info);
} }
@ -96,12 +100,16 @@ class PointHistory extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->event_info) || is_object($this->event_info)) {
$this->event_info = kg_json_encode($this->event_info);
}
$this->update_time = time(); $this->update_time = time();
} }
public function afterFetch() public function afterFetch()
{ {
if (is_string($this->event_info) && !empty($this->event_info)) { if (is_string($this->event_info)) {
$this->event_info = json_decode($this->event_info, true); $this->event_info = json_decode($this->event_info, true);
} }
} }

26
app/Models/Reason.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
class Reason
{
public static function articleRejectOptions()
{
return [
101 => '内容质量差',
102 => '旧闻重提',
103 => '内容不实',
104 => '标题夸张',
105 => '题文不符',
106 => '低俗色情',
107 => '广告软文',
108 => '封面反感',
109 => '归类与主题不符',
110 => '抄袭他人作品',
111 => '内容涉嫌违法',
112 => '其它问题',
];
}
}

View File

@ -28,6 +28,20 @@ class Review extends Model
*/ */
public $owner_id = 0; public $owner_id = 0;
/**
* 终端类型
*
* @var integer
*/
public $client_type = 0;
/**
* 终端IP
*
* @var integer
*/
public $client_ip = '';
/** /**
* 评价内容 * 评价内容
* *

View File

@ -103,7 +103,7 @@ class Role extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (is_array($this->routes)) { if (is_array($this->routes) || is_object($this->routes)) {
$this->routes = kg_json_encode($this->routes); $this->routes = kg_json_encode($this->routes);
} }
@ -112,7 +112,7 @@ class Role extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->routes)) { if (is_array($this->routes) || is_object($this->routes)) {
$this->routes = kg_json_encode($this->routes); $this->routes = kg_json_encode($this->routes);
} }

View File

@ -131,7 +131,7 @@ class Slide extends Model
$this->cover = self::getCoverPath($this->cover); $this->cover = self::getCoverPath($this->cover);
} }
if (is_array($this->target_attrs)) { if (is_array($this->target_attrs) || is_object($this->target_attrs)) {
$this->target_attrs = kg_json_encode($this->target_attrs); $this->target_attrs = kg_json_encode($this->target_attrs);
} }
@ -144,7 +144,7 @@ class Slide extends Model
$this->cover = self::getCoverPath($this->cover); $this->cover = self::getCoverPath($this->cover);
} }
if (is_array($this->target_attrs)) { if (is_array($this->target_attrs) || is_object($this->target_attrs)) {
$this->target_attrs = kg_json_encode($this->target_attrs); $this->target_attrs = kg_json_encode($this->target_attrs);
} }

View File

@ -73,7 +73,7 @@ class Task extends Model
/** /**
* 条目内容 * 条目内容
* *
* @var string|array * @var array|string
*/ */
public $item_info = []; public $item_info = [];
@ -126,7 +126,7 @@ class Task extends Model
public function beforeCreate() public function beforeCreate()
{ {
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }
@ -135,7 +135,7 @@ class Task extends Model
public function beforeUpdate() public function beforeUpdate()
{ {
if (is_array($this->item_info)) { if (is_array($this->item_info) || is_object($this->item_info)) {
$this->item_info = kg_json_encode($this->item_info); $this->item_info = kg_json_encode($this->item_info);
} }

View File

@ -66,6 +66,10 @@ class Volt extends Provider
return 'kg_object_array(' . $resolvedArgs . ')'; return 'kg_object_array(' . $resolvedArgs . ')';
}); });
$compiler->addFilter('parse_markdown', function ($resolvedArgs) {
return 'kg_parse_markdown(' . $resolvedArgs . ')';
});
$compiler->addFilter('duration', function ($resolvedArgs) { $compiler->addFilter('duration', function ($resolvedArgs) {
return 'kg_duration(' . $resolvedArgs . ')'; return 'kg_duration(' . $resolvedArgs . ')';
}); });

View File

@ -7,6 +7,7 @@ use App\Models\Article as ArticleModel;
use App\Models\ArticleFavorite as ArticleFavoriteModel; use App\Models\ArticleFavorite as ArticleFavoriteModel;
use App\Models\ArticleLike as ArticleLikeModel; use App\Models\ArticleLike as ArticleLikeModel;
use App\Models\ArticleTag as ArticleTagModel; use App\Models\ArticleTag as ArticleTagModel;
use App\Models\Comment as CommentModel;
use App\Models\Tag as TagModel; use App\Models\Tag as TagModel;
use Phalcon\Mvc\Model; use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\Resultset;
@ -43,16 +44,20 @@ class Article extends Repository
$builder->andWhere('owner_id = :owner_id:', ['owner_id' => $where['owner_id']]); $builder->andWhere('owner_id = :owner_id:', ['owner_id' => $where['owner_id']]);
} }
if (isset($where['source_type'])) {
$builder->andWhere('source_type = :source_type:', ['source_type' => $where['source_type']]);
}
if (!empty($where['title'])) { if (!empty($where['title'])) {
$builder->andWhere('title LIKE :title:', ['title' => "%{$where['title']}%"]); $builder->andWhere('title LIKE :title:', ['title' => "%{$where['title']}%"]);
} }
if (isset($where['featured'])) { if (isset($where['private'])) {
$builder->andWhere('featured = :featured:', ['featured' => $where['featured']]); $builder->andWhere('private = :private:', ['private' => $where['private']]);
} }
if (isset($where['source_type'])) { if (isset($where['featured'])) {
$builder->andWhere('source_type = :source_type:', ['source_type' => $where['source_type']]); $builder->andWhere('featured = :featured:', ['featured' => $where['featured']]);
} }
if (isset($where['published'])) { if (isset($where['published'])) {
@ -140,9 +145,9 @@ class Article extends Repository
public function countComments($articleId) public function countComments($articleId)
{ {
return (int)ArticleCommentModel::count([ return (int)CommentModel::count([
'conditions' => 'article_id = :article_id: AND deleted = 0', 'conditions' => 'item_id = ?1 AND item_type = ?2 AND deleted = 0',
'bind' => ['article_id' => $articleId], 'bind' => [1 => $articleId, 2 => CommentModel::ITEM_ARTICLE],
]); ]);
} }

View File

@ -9,6 +9,7 @@ use App\Models\ChapterOffline as ChapterOfflineModel;
use App\Models\ChapterRead as ChapterReadModel; use App\Models\ChapterRead as ChapterReadModel;
use App\Models\ChapterUser as ChapterUserModel; use App\Models\ChapterUser as ChapterUserModel;
use App\Models\ChapterVod as ChapterVodModel; use App\Models\ChapterVod as ChapterVodModel;
use App\Models\Comment as CommentModel;
use Phalcon\Mvc\Model; use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface; use Phalcon\Mvc\Model\ResultsetInterface;
@ -176,4 +177,12 @@ class Chapter extends Repository
]); ]);
} }
public function countComments($chapterId)
{
return (int)CommentModel::count([
'conditions' => 'item_id = ?1 AND item_type = ?2 AND deleted = 0',
'bind' => [1 => $chapterId, 2 => CommentModel::ITEM_CHAPTER],
]);
}
} }

View File

@ -92,4 +92,9 @@ class Comment extends Repository
->execute(); ->execute();
} }
public function countComments()
{
return (int)CommentModel::count(['conditions' => 'deleted = 0']);
}
} }

106
app/Repos/Notification.php Normal file
View File

@ -0,0 +1,106 @@
<?php
namespace App\Repos;
use App\Library\Paginator\Adapter\QueryBuilder as PagerQueryBuilder;
use App\Models\Notification as NotificationModel;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Resultset;
use Phalcon\Mvc\Model\ResultsetInterface;
class Notification extends Repository
{
public function paginate($where = [], $sort = 'latest', $page = 1, $limit = 15)
{
$builder = $this->modelsManager->createBuilder();
$builder->from(NotificationModel::class);
$builder->where('1 = 1');
if (!empty($where['sender_id'])) {
$builder->andWhere('sender_id = :sender_id:', ['sender_id' => $where['sender_id']]);
}
if (!empty($where['receiver_id'])) {
$builder->andWhere('receiver_id = :receiver_id:', ['receiver_id' => $where['receiver_id']]);
}
if (!empty($where['event_id'])) {
$builder->andWhere('event_id = :event_id:', ['event_id' => $where['event_id']]);
}
if (!empty($where['event_type'])) {
$builder->andWhere('event_type = :event_type:', ['event_type' => $where['event_type']]);
}
if (isset($where['viewed'])) {
$builder->andWhere('viewed = :viewed:', ['viewed' => $where['viewed']]);
}
if (isset($where['deleted'])) {
$builder->andWhere('deleted = :deleted:', ['deleted' => $where['deleted']]);
}
switch ($sort) {
case 'oldest':
$orderBy = 'id ASC';
break;
default:
$orderBy = 'id DESC';
break;
}
$builder->orderBy($orderBy);
$pager = new PagerQueryBuilder([
'builder' => $builder,
'page' => $page,
'limit' => $limit,
]);
return $pager->paginate();
}
/**
* @param int $id
* @return NotificationModel|Model|bool
*/
public function findById($id)
{
return NotificationModel::findFirst([
'conditions' => 'id = :id:',
'bind' => ['id' => $id],
]);
}
/**
* @param array $ids
* @param string|array $columns
* @return ResultsetInterface|Resultset|NotificationModel[]
*/
public function findByIds($ids, $columns = '*')
{
return NotificationModel::query()
->columns($columns)
->inWhere('id', $ids)
->execute();
}
public function findByUserEvent($senderId, $eventId, $eventType)
{
return NotificationModel::findFirst([
'conditions' => 'sender_id = ?1 AND event_id = ?2 AND event_type = ?3',
'bind' => [1 => $senderId, 2 => $eventId, 3 => $eventType],
]);
}
public function markAllAsViewed($userId)
{
$phql = sprintf('UPDATE %s SET viewed = 1 WHERE receiver_id = :user_id: AND viewed = 0', NotificationModel::class);
return $this->modelsManager->executeQuery($phql, ['user_id' => $userId]);
}
}

View File

@ -102,4 +102,21 @@ class PointHistory extends Repository
->execute(); ->execute();
} }
/**
* @param int $userId
* @param int $eventType
* @param string $date
* @return int
*/
public function sumUserDailyEventPoints($userId, $eventType, $date)
{
$createTime = strtotime($date);
return (int)PointHistoryModel::sum([
'column' => 'event_point',
'conditions' => 'user_id = ?1 AND event_type = ?2 AND create_time > ?3',
'bind' => [1 => $userId, 2 => $eventType, 3 => $createTime],
]);
}
} }

View File

@ -2,9 +2,12 @@
namespace App\Repos; namespace App\Repos;
use App\Models\Article as ArticleModel;
use App\Models\Comment as CommentModel;
use App\Models\Online as OnlineModel; use App\Models\Online as OnlineModel;
use App\Models\Order as OrderModel; use App\Models\Order as OrderModel;
use App\Models\OrderStatus as OrderStatusModel; use App\Models\OrderStatus as OrderStatusModel;
use App\Models\PointRedeem as PointRedeemModel;
use App\Models\Refund as RefundModel; use App\Models\Refund as RefundModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use Phalcon\Mvc\Model\Resultset; use Phalcon\Mvc\Model\Resultset;
@ -13,7 +16,23 @@ use Phalcon\Mvc\Model\ResultsetInterface;
class Stat extends Repository class Stat extends Repository
{ {
public function countDailyRegisteredUser($date) public function countPendingArticles()
{
return (int)ArticleModel::count([
'conditions' => 'published = :published: AND deleted = 0',
'bind' => ['published' => ArticleModel::PUBLISH_PENDING],
]);
}
public function countPendingComments()
{
return (int)CommentModel::count([
'conditions' => 'published = :published: AND deleted = 0',
'bind' => ['published' => CommentModel::PUBLISH_PENDING],
]);
}
public function countDailyRegisteredUsers($date)
{ {
$startTime = strtotime($date); $startTime = strtotime($date);
@ -74,6 +93,22 @@ class Stat extends Repository
]); ]);
} }
public function countDailyPointRedeems($date)
{
$startTime = strtotime($date);
$endTime = $startTime + 86400;
return (int)PointRedeemModel::count([
'conditions' => 'status = ?1 AND create_time BETWEEN ?2 AND ?3',
'bind' => [
1 => PointRedeemModel::STATUS_PENDING,
2 => $startTime,
3 => $endTime,
],
]);
}
public function sumDailySales($date) public function sumDailySales($date)
{ {
$sql = "SELECT sum(o.amount) AS total_amount FROM %s AS os JOIN %s AS o ON os.order_id = o.id "; $sql = "SELECT sum(o.amount) AS total_amount FROM %s AS os JOIN %s AS o ON os.order_id = o.id ";

View File

@ -8,6 +8,7 @@ use App\Models\ArticleFavorite as ArticleFavoriteModel;
use App\Models\CourseFavorite as CourseFavoriteModel; use App\Models\CourseFavorite as CourseFavoriteModel;
use App\Models\CourseUser as CourseUserModel; use App\Models\CourseUser as CourseUserModel;
use App\Models\ImUser as ImUserModel; use App\Models\ImUser as ImUserModel;
use App\Models\Notification as NotificationModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Models\UserBalance as UserBalanceModel; use App\Models\UserBalance as UserBalanceModel;
use App\Models\UserContact as UserContactModel; use App\Models\UserContact as UserContactModel;
@ -156,7 +157,12 @@ class User extends Repository
public function countUsers() public function countUsers()
{ {
return (int)UserModel::count(['conditions' => 'deleted = 0']); return (int)UserModel::count();
}
public function countVipUsers()
{
return (int)UserModel::count(['conditions' => 'vip = 1']);
} }
public function countCourses($userId) public function countCourses($userId)
@ -191,4 +197,12 @@ class User extends Repository
]); ]);
} }
public function countUnreadNotifications($userId)
{
return (int)NotificationModel::count([
'conditions' => 'receiver_id = :user_id: AND viewed = 0',
'bind' => ['user_id' => $userId],
]);
}
} }

View File

@ -7,6 +7,7 @@ use App\Models\ArticleFavorite as ArticleFavoriteModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Repos\ArticleFavorite as ArticleFavoriteRepo; use App\Repos\ArticleFavorite as ArticleFavoriteRepo;
use App\Services\Logic\ArticleTrait; use App\Services\Logic\ArticleTrait;
use App\Services\Logic\Notice\System\ArticleFavorited as ArticleFavoritedNotice;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Validators\UserLimit as UserLimitValidator; use App\Validators\UserLimit as UserLimitValidator;
@ -41,8 +42,13 @@ class ArticleFavorite extends LogicService
$favorite->create(); $favorite->create();
$this->incrArticleFavoriteCount($article); $this->incrArticleFavoriteCount($article);
$this->incrUserFavoriteCount($user); $this->incrUserFavoriteCount($user);
$this->handleFavoriteNotice($article, $user);
$this->eventsManager->fire('Article:afterFavorite', $this, $article);
} else { } else {
$action = 'undo'; $action = 'undo';
@ -50,7 +56,10 @@ class ArticleFavorite extends LogicService
$favorite->delete(); $favorite->delete();
$this->decrArticleFavoriteCount($article); $this->decrArticleFavoriteCount($article);
$this->decrUserFavoriteCount($user); $this->decrUserFavoriteCount($user);
$this->eventsManager->fire('Article:afterUndoFavorite', $this, $article);
} }
return [ return [
@ -89,4 +98,11 @@ class ArticleFavorite extends LogicService
} }
} }
protected function handleFavoriteNotice(ArticleModel $article, UserModel $sender)
{
$notice = new ArticleFavoritedNotice();
$notice->handle($article, $sender);
}
} }

View File

@ -27,6 +27,8 @@ class ArticleInfo extends LogicService
$this->incrArticleViewCount($article); $this->incrArticleViewCount($article);
$this->eventsManager->fire('Article:afterView', $this, $article);
return $result; return $result;
} }
@ -48,6 +50,7 @@ class ArticleInfo extends LogicService
'category' => $category, 'category' => $category,
'owner' => $owner, 'owner' => $owner,
'me' => $me, 'me' => $me,
'private' => $article->private,
'allow_comment' => $article->allow_comment, 'allow_comment' => $article->allow_comment,
'source_type' => $article->source_type, 'source_type' => $article->source_type,
'source_url' => $article->source_url, 'source_url' => $article->source_url,
@ -57,6 +60,7 @@ class ArticleInfo extends LogicService
'comment_count' => $article->comment_count, 'comment_count' => $article->comment_count,
'favorite_count' => $article->favorite_count, 'favorite_count' => $article->favorite_count,
'create_time' => $article->create_time, 'create_time' => $article->create_time,
'update_time' => $article->update_time,
]; ];
} }

View File

@ -7,6 +7,7 @@ use App\Models\ArticleLike as ArticleLikeModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Repos\ArticleLike as ArticleLikeRepo; use App\Repos\ArticleLike as ArticleLikeRepo;
use App\Services\Logic\ArticleTrait; use App\Services\Logic\ArticleTrait;
use App\Services\Logic\Notice\System\ArticleLiked as ArticleLikedNotice;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Validators\UserLimit as UserLimitValidator; use App\Validators\UserLimit as UserLimitValidator;
@ -42,6 +43,10 @@ class ArticleLike extends LogicService
$this->incrArticleLikeCount($article); $this->incrArticleLikeCount($article);
$this->handleLikeNotice($article, $user);
$this->eventsManager->fire('Article:afterLike', $this, $article);
} else { } else {
$action = 'undo'; $action = 'undo';
@ -49,6 +54,8 @@ class ArticleLike extends LogicService
$articleLike->delete(); $articleLike->delete();
$this->decrArticleLikeCount($article); $this->decrArticleLikeCount($article);
$this->eventsManager->fire('Article:afterUndoLike', $this, $article);
} }
$this->incrUserDailyArticleLikeCount($user); $this->incrUserDailyArticleLikeCount($user);
@ -79,4 +86,11 @@ class ArticleLike extends LogicService
$this->eventsManager->fire('UserDailyCounter:incrArticleLikeCount', $this, $user); $this->eventsManager->fire('UserDailyCounter:incrArticleLikeCount', $this, $user);
} }
protected function handleLikeNotice(ArticleModel $article, UserModel $sender)
{
$notice = new ArticleLikedNotice();
$notice->handle($article, $sender);
}
} }

View File

@ -4,6 +4,7 @@ namespace App\Services\Logic\Article;
use App\Builders\ArticleList as ArticleListBuilder; use App\Builders\ArticleList as ArticleListBuilder;
use App\Library\Paginator\Query as PagerQuery; use App\Library\Paginator\Query as PagerQuery;
use App\Models\Article as ArticleModel;
use App\Repos\Article as ArticleRepo; use App\Repos\Article as ArticleRepo;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Validators\ArticleQuery as ArticleQueryValidator; use App\Validators\ArticleQuery as ArticleQueryValidator;
@ -19,7 +20,9 @@ class ArticleList extends LogicService
$params = $this->checkQueryParams($params); $params = $this->checkQueryParams($params);
$params['published'] = 1; $params['published'] = ArticleModel::PUBLISH_APPROVED;
$params['private'] = 0;
$params['deleted'] = 0;
$sort = $pagerQuery->getSort(); $sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage(); $page = $pagerQuery->getPage();
@ -74,6 +77,9 @@ class ArticleList extends LogicService
'tags' => $article['tags'], 'tags' => $article['tags'],
'category' => $category, 'category' => $category,
'owner' => $owner, 'owner' => $owner,
'private' => $article['private'],
'published' => $article['published'],
'allow_comment' => $article['allow_comment'],
'view_count' => $article['view_count'], 'view_count' => $article['view_count'],
'like_count' => $article['like_count'], 'like_count' => $article['like_count'],
'comment_count' => $article['comment_count'], 'comment_count' => $article['comment_count'],

View File

@ -23,9 +23,9 @@ class CommentList extends LogicService
$params = $pagerQuery->getParams(); $params = $pagerQuery->getParams();
$params['item_type'] = CommentModel::ITEM_ARTICLE;
$params['item_id'] = $article->id; $params['item_id'] = $article->id;
$params['published'] = 1; $params['item_type'] = CommentModel::ITEM_ARTICLE;
$params['published'] = CommentModel::PUBLISH_APPROVED;
$sort = $pagerQuery->getSort(); $sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage(); $page = $pagerQuery->getPage();

View File

@ -23,9 +23,9 @@ class CommentList extends LogicService
$params = $pagerQuery->getParams(); $params = $pagerQuery->getParams();
$params['item_type'] = CommentModel::ITEM_CHAPTER;
$params['item_id'] = $chapter->id; $params['item_id'] = $chapter->id;
$params['published'] = 1; $params['item_type'] = CommentModel::ITEM_CHAPTER;
$params['published'] = CommentModel::PUBLISH_APPROVED;
$sort = $pagerQuery->getSort(); $sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage(); $page = $pagerQuery->getPage();

View File

@ -6,12 +6,54 @@ use App\Models\Article as ArticleModel;
use App\Models\Chapter as ChapterModel; use App\Models\Chapter as ChapterModel;
use App\Models\Comment as CommentModel; use App\Models\Comment as CommentModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Services\Logic\Notice\System\ArticleCommented as ArticleCommentedNotice;
use App\Services\Logic\Notice\System\ChapterCommented as ChapterCommentedNotice;
use Phalcon\Di as Di; use Phalcon\Di as Di;
use Phalcon\Events\Manager as EventsManager; use Phalcon\Events\Manager as EventsManager;
trait CommentCountTrait trait CommentCountTrait
{ {
protected function incrItemCommentCount(CommentModel $comment)
{
if ($comment->item_type == CommentModel::ITEM_CHAPTER) {
$chapter = $this->checkChapter($comment->item_id);
$this->incrChapterCommentCount($chapter);
$notice = new ChapterCommentedNotice();
$notice->handle($comment);
} elseif ($comment->item_type == CommentModel::ITEM_ARTICLE) {
$article = $this->checkArticle($comment->item_id);
$this->incrArticleCommentCount($article);
$notice = new ArticleCommentedNotice();
$notice->handle($comment);
}
}
protected function decrItemCommentCount(CommentModel $comment)
{
if ($comment->item_type == CommentModel::ITEM_CHAPTER) {
$chapter = $this->checkChapter($comment->item_id);
$this->decrChapterCommentCount($chapter);
} elseif ($comment->item_type == CommentModel::ITEM_ARTICLE) {
$article = $this->checkArticle($comment->item_id);
$this->decrArticleCommentCount($article);
}
}
protected function incrCommentReplyCount(CommentModel $comment) protected function incrCommentReplyCount(CommentModel $comment)
{ {
$comment->reply_count += 1; $comment->reply_count += 1;

View File

@ -5,6 +5,9 @@ namespace App\Services\Logic\Comment;
use App\Models\Comment as CommentModel; use App\Models\Comment as CommentModel;
use App\Services\Logic\ArticleTrait; use App\Services\Logic\ArticleTrait;
use App\Services\Logic\ChapterTrait; use App\Services\Logic\ChapterTrait;
use App\Services\Logic\Notice\System\ArticleCommented as ArticleCommentedNotice;
use App\Services\Logic\Notice\System\ChapterCommented as ChapterCommentedNotice;
use App\Services\Logic\Point\History\CommentPost as CommentPostPointHistory;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Traits\Client as ClientTrait; use App\Traits\Client as ClientTrait;
use App\Validators\Comment as CommentValidator; use App\Validators\Comment as CommentValidator;
@ -38,31 +41,63 @@ class CommentCreate extends LogicService
'item_id' => $post['item_id'], 'item_id' => $post['item_id'],
'item_type' => $post['item_type'], 'item_type' => $post['item_type'],
'owner_id' => $user->id, 'owner_id' => $user->id,
'published' => 1,
]; ];
$data['content'] = $validator->checkContent($post['content']); $data['content'] = $validator->checkContent($post['content']);
$data['client_type'] = $this->getClientType(); $data['client_type'] = $this->getClientType();
$data['client_ip'] = $this->getClientIp(); $data['client_ip'] = $this->getClientIp();
if ($post['item_type'] == CommentModel::ITEM_CHAPTER) { /**
* @todo 引入自动审核机制
$chapter = $this->checkChapter($post['item_id']); */
$data['published'] = CommentModel::PUBLISH_APPROVED;
$this->incrChapterCommentCount($chapter);
} elseif ($post['item_type'] == CommentModel::ITEM_ARTICLE) {
$article = $this->checkArticle($post['item_id']);
$this->incrArticleCommentCount($article);
}
$comment->create($data); $comment->create($data);
$this->incrUserDailyCommentCount($user); $this->incrUserDailyCommentCount($user);
$this->incrItemCommentCount($comment);
$this->handlePostNotice($comment);
$this->handlePostPoint($comment);
$this->eventsManager->fire('Comment:afterCreate', $this, $comment);
return $comment; return $comment;
} }
protected function handlePostNotice(CommentModel $comment)
{
if ($comment->item_type == CommentModel::ITEM_CHAPTER) {
$chapter = $this->checkChapter($comment->item_id);
$this->incrChapterCommentCount($chapter);
$notice = new ChapterCommentedNotice();
$notice->handle($comment);
} elseif ($comment->item_type == CommentModel::ITEM_ARTICLE) {
$article = $this->checkArticle($comment->item_id);
$this->incrArticleCommentCount($article);
$notice = new ArticleCommentedNotice();
$notice->handle($comment);
}
}
protected function handlePostPoint(CommentModel $comment)
{
if ($comment->published != CommentModel::PUBLISH_APPROVED) return;
$service = new CommentPostPointHistory();
$service->handle($comment);
}
} }

View File

@ -2,9 +2,6 @@
namespace App\Services\Logic\Comment; namespace App\Services\Logic\Comment;
use App\Models\Comment as CommentModel;
use App\Services\Logic\ArticleTrait;
use App\Services\Logic\ChapterTrait;
use App\Services\Logic\CommentTrait; use App\Services\Logic\CommentTrait;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Validators\Comment as CommentValidator; use App\Validators\Comment as CommentValidator;
@ -12,8 +9,6 @@ use App\Validators\Comment as CommentValidator;
class CommentDelete extends LogicService class CommentDelete extends LogicService
{ {
use ArticleTrait;
use ChapterTrait;
use CommentTrait; use CommentTrait;
use CommentCountTrait; use CommentCountTrait;
@ -38,18 +33,9 @@ class CommentDelete extends LogicService
$this->decrCommentReplyCount($parent); $this->decrCommentReplyCount($parent);
} }
if ($comment->item_type == CommentModel::ITEM_CHAPTER) { $this->decrItemCommentCount($comment);
$chapter = $this->checkChapter($comment->item_id); $this->eventsManager->fire('Comment:afterDelete', $this, $comment);
$this->decrChapterCommentCount($chapter);
} elseif ($comment->item_type == CommentModel::ITEM_ARTICLE) {
$article = $this->checkArticle($comment->item_id);
$this->decrArticleCommentCount($article);
}
} }
} }

View File

@ -7,6 +7,7 @@ use App\Models\CommentLike as CommentLikeModel;
use App\Models\User as UserModel; use App\Models\User as UserModel;
use App\Repos\CommentLike as CommentLikeRepo; use App\Repos\CommentLike as CommentLikeRepo;
use App\Services\Logic\CommentTrait; use App\Services\Logic\CommentTrait;
use App\Services\Logic\Notice\System\CommentLiked as CommentLikedNotice;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
use App\Validators\UserLimit as UserLimitValidator; use App\Validators\UserLimit as UserLimitValidator;
@ -42,6 +43,12 @@ class CommentLike extends LogicService
$this->incrCommentLikeCount($comment); $this->incrCommentLikeCount($comment);
$this->incrUserDailyCommentLikeCount($user);
$this->handleLikeNotice($comment, $user);
$this->eventsManager->fire('Comment:afterLike', $this, $comment);
} else { } else {
$action = 'undo'; $action = 'undo';
@ -49,9 +56,9 @@ class CommentLike extends LogicService
$commentLike->delete(); $commentLike->delete();
$this->decrCommentLikeCount($comment); $this->decrCommentLikeCount($comment);
}
$this->incrUserDailyCommentLikeCount($user); $this->eventsManager->fire('Comment:afterUndoLike', $this, $comment);
}
return [ return [
'action' => $action, 'action' => $action,
@ -66,6 +73,7 @@ class CommentLike extends LogicService
$comment->update(); $comment->update();
} }
protected function decrCommentLikeCount(CommentModel $comment) protected function decrCommentLikeCount(CommentModel $comment)
{ {
if ($comment->like_count > 0) { if ($comment->like_count > 0) {
@ -79,4 +87,11 @@ class CommentLike extends LogicService
$this->eventsManager->fire('UserDailyCounter:incrCommentLikeCount', $this, $user); $this->eventsManager->fire('UserDailyCounter:incrCommentLikeCount', $this, $user);
} }
protected function handleLikeNotice(CommentModel $comment, UserModel $sender)
{
$notice = new CommentLikedNotice();
$notice->handle($comment, $sender);
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Services\Logic\Comment; namespace App\Services\Logic\Comment;
use App\Library\Paginator\Query as PagerQuery; use App\Library\Paginator\Query as PagerQuery;
use App\Models\Comment as CommentModel;
use App\Repos\Comment as CommentRepo; use App\Repos\Comment as CommentRepo;
use App\Services\Logic\Service as LogicService; use App\Services\Logic\Service as LogicService;
@ -18,7 +19,8 @@ class CommentList extends LogicService
$params = $pagerQuery->getParams(); $params = $pagerQuery->getParams();
$params['parent_id'] = 0; $params['parent_id'] = 0;
$params['published'] = 1; $params['published'] = CommentModel::PUBLISH_APPROVED;
$params['deleted'] = 0;
$sort = $pagerQuery->getSort(); $sort = $pagerQuery->getSort();
$page = $pagerQuery->getPage(); $page = $pagerQuery->getPage();

View File

@ -37,6 +37,7 @@ trait CommentListTrait
'like_count' => $comment['like_count'], 'like_count' => $comment['like_count'],
'reply_count' => $comment['reply_count'], 'reply_count' => $comment['reply_count'],
'create_time' => $comment['create_time'], 'create_time' => $comment['create_time'],
'update_time' => $comment['update_time'],
'to_user' => $toUser, 'to_user' => $toUser,
'owner' => $owner, 'owner' => $owner,
'me' => $me, 'me' => $me,

Some files were not shown because too many files have changed in this diff Show More